*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* Canvas ACHIEVEMENT-PAGE geometry -- SINGLE SOURCE OF TRUTH, read into the JS ACH
   object (js/screens.js), the same CSS-token pattern the font/glow scales use. BASE,
   EXPERT and EGGS all draw from these, so the three pages cannot drift apart: change
   a position or a card size here, once, and every page follows.
   The headline is the shared --ui-title-y row; indicator and first card row sit where
   SHOP puts its tab row and first item, so the two screens read as one family
   (achievements have no tabs). */
:root {
    --ach-sub-y:    53px;   /* page-indicator centre (SHOP's tab-row centre)       */
    --ach-top:      72px;   /* top of the first card row                          */
    --ach-cols:      3;     /* cards per row                                      */
    --ach-card-w:  188px;   /* card width                                         */
    --ach-card-h:   66px;   /* card height: 4 rows must still clear the tally line */
    --ach-gap-x:     4px;
    --ach-gap-y:     4px;
}
/* Canvas SCREEN-FURNITURE geometry -- SINGLE SOURCE OF TRUTH, read into the JS UI object
   (js/assets.js), the same CSS-token pattern the font, glow and achievement scales use.
   Every screen in the game parks the same things: a headline, a list of menu items from
   the top, the status toaster, the way out, and the key hints; a dialog and an overlay
   each carry a title of their own. They are only recognisable AS the same furniture if
   they are at the same height on every screen, so the heights live here and nowhere else
   -- change one, once, and the whole game follows. Each row is drawn by ONE painter in
   js/screens.js (drawTitle, drawDialogTitle, drawOverlayTitle, drawSubhead, menuItem,
   drawStatus ...)
   and test/check-layout.js refuses a row written as a number. */
:root {
    --ui-title-y:   24px;   /* the screen headline, centre                          */
    --ui-subhead-y: 50px;   /* the one line under it: state, notice, who you are     */
    --ui-body-y:    72px;   /* the first body line under the subhead (motto, hint)   */
    --ui-dialog-title-y: 116px;  /* a modal's title (INVITE, REMOVE FRIEND, ...)    */
    --ui-overlay-title-y: 182px; /* an in-game overlay's title (LEVEL n, PAUSED ...) */
    --ui-menu-top:  90px;   /* first menu item: one empty line below the headline   */
    --ui-menu-row:  28px;   /* menu-item pitch, and the high-score row pitch        */
    --ui-status-y: 324px;   /* the one in-menu status/toaster line                 */
    --ui-back-y:   348px;   /* BACK, parked at the bottom of every list             */
    --ui-band-y:   370px;   /* what the screen AMOUNTS to: balance, tally, summary  */
    --ui-hint-y:   390px;   /* key hints, the lowest line on every screen           */
    --ui-corner-y: 384px;   /* what sits in a BOTTOM CORNER: the version, NEWS      */
}
/* Layout in two rules:
   R1 (fit): the visible game (canvas + chrome that is actually shown) must fit the
             viewport. #wrap is flex:1 and takes whatever space the chrome leaves, so
             chrome that is display:none (e.g. the gamepad on non-touch) frees its space
             automatically. overflow:hidden is the final safety net.
   R2 (maximize): the canvas is the largest box of its native aspect that fits #wrap --
             aspect-ratio + max-width/max-height:100% let the browser pick it. In portrait
             #wrap is tall+narrow so width binds; in landscape it is short+wide so height
             binds. 4px breathing room via #wrap padding.
   JS reads the resulting canvas width back into --ui-scale (=cw/CW) and --stage-w, which
   drive the chrome font sizes (rounded to whole px so they never blur) and the HUD width. */
body {
    background: #07070e;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    height: 100vh; height: 100dvh; min-height: 100vh; min-height: 100dvh;
    /* Top breathing room for the HUD text (scales with it); canvas bottom margin is
       handled in layout(). Horizontal stays 0 so the canvas keeps its full width. The
       top inset keeps the HUD clear of the portrait notch once viewport-fit=cover lets
       the page draw under it (env is 0px on desktop/Android, so no change there). */
    padding: calc(var(--pad-top-px, 8px) + env(safe-area-inset-top, 0px)) 0 0; gap: 8px;
    font-family: 'Press Start 2P', 'Courier New', monospace; color: #ccc;
    user-select: none; -webkit-user-select: none;
    -webkit-touch-callout: none;
    overflow: hidden;
    /* whole-screen swipes: keep the browser from claiming any of it for scroll/zoom */
    touch-action: none;
}
#hud {
    /* 2x2 grid: cells a b / c d. Classic: LIVES SCORE / GEMS LEVEL. Duel: P1-lives
       P2-lives / P1-score P2-score (what each cell SHOWS is swapped by updateHUD). */
    display: grid; grid-template-columns: 1fr 1fr; gap: 3px;
    /* width+max-width, not min(): a var() whose fallback the engine cannot parse is invalid at
       COMPUTED-value time, so a pre-min() engine (Chrome < 79) would not fall back to an earlier
       declaration -- it would take the width to auto. The pair is the same result, ES5-era CSS.
       (--stage-w is a plain px value set by layout() within the first frame.) */
    width: var(--stage-w, 600px); max-width: 100vw;
    font-size: var(--fs-menu-px, 14px); letter-spacing: 0;
    opacity: 1; transition: opacity 0.15s, visibility 0s;
    order: 1;
}
/* visibility, not just opacity: an opacity-0 element still holds a paint slot, and iOS
   WebKit leaves stale tiles of the faded HUD in the landscape side panel. Delayed so the
   fade still plays; the layout box stays (layout() measures hud.offsetHeight). */
#hud.hidden { opacity: 0; visibility: hidden; pointer-events: none; transition: opacity 0.15s, visibility 0s 0.15s; }
.hud-cell { display: inline-flex; align-items: center; gap: 4px; }
#hud-b, #hud-d { justify-self: end; }
.hl { color: #888; } .hv { color: #7fff7f; }
/* Lives hearts canvases: never let flex shrink them; size off the HUD scale, crisp pixels. */
#hv-lives-cv, #hv-lives2-cv { flex: none; image-rendering: pixelated; height: var(--lives-h-px, 12px); width: auto; }
.hv-wrap { display: inline-flex; align-items: baseline; }
#topbar {
    width: var(--stage-w, 600px); max-width: 100vw;
    display: flex; justify-content: space-between; align-items: center; padding: 0 2px;
    order: 2;
}
/* Mute button and FPS box: identical box, sizes + font rounded to whole px so they never
   blur or overflow. appearance:none + flex:none kill <button> vs <span> UA differences. */
#fps-el, #btn-mute {
    box-sizing: border-box;
    width: var(--box-w-px, 92px);
    height: var(--box-h-px, 28px); min-height: var(--box-h-px, 28px);
    flex: 0 0 auto; line-height: 1;
    display: flex; align-items: center; justify-content: center;
    border-radius: 3px; padding: 0 4px;
    -webkit-appearance: none; appearance: none;
}
/* Hidden on the splash screen (JS sets body[data-phase]); shown on every other phase. */
body[data-phase="splash"] #btn-mute, body[data-phase="splash"] #fps-el { opacity: 0; pointer-events: none; }
#fps-el { font-family: 'Press Start 2P',monospace; font-size: var(--fs-hint-px, 10px); color: #7aaa7a; background: #111; border: 1px solid #4a4a4a; white-space: nowrap; overflow: hidden; cursor: pointer; -webkit-tap-highlight-color: transparent; }
#fps-el:hover { border-color: #7fff7f; }
#btn-mute {
    background: #0d180d; border: 1px solid #3a5a3a;
    cursor: pointer; -webkit-tap-highlight-color: transparent;
}
#btn-mute canvas { image-rendering: pixelated; display: block; height: var(--mute-icon-h, 16px); width: calc(var(--mute-icon-h, 16px) * 2); }
#btn-mute:hover { border-color: #7fff7f; }
#btn-mute.muted { border-color: #5a2a2a; }
/* Click-only controls: never show a focus ring or a text-selection highlight. Mute also carries
   tabindex="-1" so Tab skips it; the FPS span was never tab-focusable but its text is drag-selectable. */
#fps-el, #btn-mute { -webkit-user-select: none; user-select: none; }
#fps-el:focus, #btn-mute:focus { outline: none; }
/* Own rule: an engine that does not know :focus-visible drops the whole selector list it
   sits in, and the plain :focus line above must survive that. */
#fps-el:focus-visible, #btn-mute:focus-visible { outline: none; }
/* MUTE also takes a ten-second hold (the ringtone easter egg), so iOS must not answer that
   hold with its own long-press callout. */
#btn-mute { -webkit-touch-callout: none; touch-action: manipulation; }
/* The ringtone panel that hold opens: same chrome as the gamepad buttons, so it reads as
   part of the cabinet rather than as a browser dialog. Built by js/input.js on first open. */
#ring-egg {
    position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 99998;   /* inset is Chrome 87 / Safari 14.1 */
    display: flex; align-items: center; justify-content: center;
    padding: 16px; background: rgba(0,0,0,0.8);
    font-family: 'Press Start 2P',monospace; font-size: var(--fs-hint-px, 10px);
    -webkit-user-select: none; user-select: none;
}
#ring-egg .panel {
    box-sizing: border-box; width: 100%; max-width: 420px; max-height: 100%; overflow-y: auto;
    background: #0d180d; border: 1px solid #3a5a3a; border-radius: 6px; padding: 14px;
    box-shadow: 0 0 24px #000;
}
#ring-egg h2 { margin: 0 0 12px; font-weight: normal; font-size: var(--fs-menu-px, 14px); color: #7fff7f; text-shadow: 0 0 8px #7fff7f; }
#ring-egg p { margin: 0 0 10px; color: #888; line-height: 1.8; }
#ring-egg ol { margin: 0 0 14px; padding-left: 20px; color: #ccc; line-height: 2; }
#ring-egg .row { display: flex; gap: 8px; }
#ring-egg .sbtn { flex: 1 1 0; height: 40px; }
/* Canvas region: flex:1 takes the space chrome leaves; the canvas is the largest box of
   its native aspect that fits, centred, with a 4px margin. */
#wrap {
    /* Column layouts (desktop + portrait): shrink-wrap the JS-sized canvas so the body
       groups [HUD + SND/FPS + canvas + gamepad] and centers (desktop) / bottom-aligns
       (portrait) them together -- the SND/FPS bar hugs the canvas instead of being flung to
       the top. Landscape (touch) overrides back to flex:1 to fit between the side panels. */
    order: 3; flex: 0 0 auto; min-width: 0; min-height: 0;
    align-self: stretch;
    display: flex; align-items: center; justify-content: center;
}
#c {
    display: block; border: 1px solid #2a2a4e; touch-action: none;
    /* width/height are set by layout() = the largest CW:CH box that fits #wrap (minus 4px). */
}
/* Hide the mouse cursor over the board while playing with the keyboard (JS toggles the
   class on keydown; a mousemove clears it so the cursor reappears for mouse users). */
body.cursor-hidden #c { cursor: none; }
/* JS-created utilities: the file-input for Restore, and the DEBUGGING canvas-props overlay. */
.util-hidden { display: none; }
.debug-overlay { position: fixed; z-index: 99999; background: #000d; color: #0f0;
    font: 11px monospace; padding: 3px; white-space: pre; pointer-events: none; max-width: 49vw; }
/* Four corner docks (DEBUG LEVEL 2+): TL=network, TR=timing, BL=sim/game, BR=graphics. */
.dbg-tl { top: env(safe-area-inset-top, 0px); left: env(safe-area-inset-left, 0px);  text-align: left; }
.dbg-tr { top: env(safe-area-inset-top, 0px); right: env(safe-area-inset-right, 0px); text-align: right; }
/* Level 3: clickable snapshot-capture button, centred along the top edge. */
.dbg-snap { position: fixed; z-index: 100000; top: calc(2px + env(safe-area-inset-top, 0px)); left: 50%; transform: translateX(-50%); padding: 3px 9px;
    background: #0a2a0a; color: #7fff7f; border: 1px solid #7fff7f; border-radius: 3px;
    font: bold 11px monospace; cursor: pointer; pointer-events: auto; user-select: none;
    transition: background 0.1s, color 0.1s, box-shadow 0.1s; }
.dbg-snap.snapped { background: #7fff7f; color: #0a2a0a; box-shadow: 0 0 10px #7fff7f; }
.dbg-bl { bottom: env(safe-area-inset-bottom, 0px); left: env(safe-area-inset-left, 0px);  text-align: left; }
.dbg-br { bottom: env(safe-area-inset-bottom, 0px); right: env(safe-area-inset-right, 0px); text-align: right; }
#gamepad { display: none; order: 4; }
#dpad-c { touch-action: none; display: block; border: 1px solid #3a5a3a; border-radius: 6px; overflow: hidden; }
.sbtn {
    background: #0d180d; border: 1px solid #3a5a3a; color: #7fff7f;
    font-family: 'Press Start 2P',monospace; font-size: var(--fs-hint-px, 10px); cursor: pointer; border-radius: 6px;
    -webkit-tap-highlight-color: transparent; display: flex; align-items: center; justify-content: center;
}
.sbtn:active { background: #1a3a1a; border-color: #7fff7f; }
@media (pointer: coarse) {
    #gamepad { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
    #btn-esc   { grid-column: 1; grid-row: 1; }
    #btn-pause { grid-column: 2; grid-row: 1; }
    #dpad-c    { grid-column: 1 / span 2; grid-row: 2; width: 45vw; max-width: 150px; height: 45vw; max-height: 150px; }
    #btn-start { grid-column: 1; grid-row: 3; }
    #btn-ok    { grid-column: 2; grid-row: 3; }
    body.lefty #btn-pause { grid-column: 1; }
    body.lefty #btn-esc   { grid-column: 2; }
    body.lefty #btn-ok    { grid-column: 1; }
    body.lefty #btn-start { grid-column: 2; }
    .sbtn { height: 48px; }
}
/* Dimmed/inactive control (button or d-pad) -- driven by the CONTROLS table in game.js. */
.sbtn.dim, #dpad-c.dim { opacity: 0.3; pointer-events: none; }
#side-info { display: contents; }
/* Portrait touch: keep the gamepad low for thumb reach. */
@media (pointer: coarse) and (orientation: portrait) {
    /* Stack from the bottom for thumb reach: #wrap shrink-wraps the (JS-sized) canvas
       instead of flex-growing, so HUD + SND/FPS + canvas + gamepad sit as one cluster that
       justify-content:flex-end pushes to the bottom, leaving the empty space up top. */
    body { justify-content: flex-end; padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px)); }
    #gamepad { margin-top: 16px; }
}
@media (pointer: coarse) and (orientation: landscape) {
    /* Edge padding + panel gap scale with the chrome like the side panel does -- a fixed
       3px reads as zero next to a scaled-up panel on big tablets (phones stay ~3px).
       Only the LEFT/RIGHT get a safe-area inset -- that is where the notch/Dynamic Island
       lives in landscape. iOS reports a big inset on the notch side and ~0 on the other,
       and swaps them when the phone is flipped, so the notch side stays clear while the
       opposite side goes full-bleed. Top/bottom stay 0 so the panels keep the full height
       (a bottom inset for the thin home-indicator only shoved everything up). The swipe
       layer is document-wide, so the notch-side padding band stays touchable for steering. */
    body { flex-direction: row; align-items: stretch;
           padding-top: 0; padding-bottom: 0;
           padding-left:  calc(3px * var(--ui-scale, 1) + env(safe-area-inset-left, 0px));
           padding-right: calc(3px * var(--ui-scale, 1) + env(safe-area-inset-right, 0px));
           gap: calc(6px * var(--ui-scale, 1)); }
    /* The Dynamic Island / notch is a pill on ONE landscape edge, but iOS reserves a big inset
       on BOTH, letterboxing the board between two ~60px black bands. Keep the notch side full
       (no control may hide under the Island) and trim the opposite edge -- a rounded corner
       only -- to a few px, handing that width back to #wrap. JS tags the notch side; untagged
       (symmetric insets + unknown orientation) leaves both full: safe, just no reclaim. */
    /* The two min() calls below are the one place this stylesheet may sit above the browser
       floor: an engine that cannot parse them drops the declaration and falls back to the
       untrimmed rule above, which is exactly the "no reclaim: safe" case just described. */
    body.notch-left  { padding-right: calc(3px * var(--ui-scale, 1) + min(env(safe-area-inset-right, 0px), 8px)); }  /* compat-ok */
    body.notch-right { padding-left:  calc(3px * var(--ui-scale, 1) + min(env(safe-area-inset-left, 0px), 8px)); }   /* compat-ok */
    #wrap { flex: 1 1 0; }   /* landscape fits the canvas into the flex region between panels */
    /* Side info panel -- floats left or right depending on handed setting. Width scales
       with the chrome (--ui-scale): its content (mute/FPS boxes, HUD text) is sized off
       the canvas scale, so a fixed panel would be outgrown on large tablets and clip at
       the screen edge. layout()'s ResizeObserver re-converges the canvas fit after the
       panel width settles (the _lastCw guard stops the feedback loop). */
    #side-info {
        display: flex; flex-direction: column; justify-content: flex-start;
        align-items: flex-end; gap: 14px;
        width: calc(100px * var(--ui-scale, 1)); flex-shrink: 0; overflow: hidden;
        position: relative;
        padding: 16px 0;
    }
    #topbar { width: auto; padding: 0; gap: 8px; order: 0; }
    #fps-el { position: absolute; bottom: 16px; right: 0; left: auto; transform: none; }
    body.lefty #side-info { align-items: flex-start; }
    body.lefty #fps-el { right: auto; left: 0; }
    /* Single column, label above value. Cell order = DOM order (P1 lives topmost);
       in duel the P2-lives cell is pushed to the bottom, per design. */
    #hud { width: 100%; grid-template-columns: 1fr; gap: 8px; justify-items: start; margin-top: auto; margin-bottom: auto; }
    .hud-cell { flex-direction: column; align-items: flex-start; gap: 2px; max-width: 100%; min-width: 0; }
    #hud-b, #hud-d { justify-self: start; }
    /* Duel names run up to MAX_NAME chars in a wide pixel font. Clamp the label to the panel
       width (ellipsis) so a long name can't widen its cell and shove the hearts canvas off the
       left edge -- #side-info's overflow:hidden was clipping them out of view. */
    #hud-a-l, #hud-b-l { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    body[data-phase^="duel"] #hud-b { order: 9; }
    /* Right controls panel -- ESC, PAUSE, dpad, A:OK, START stacked top to bottom */
    #gamepad {
        display: flex; flex-direction: column; flex-shrink: 0;
        width: 28vw; max-width: 120px; align-items: center; justify-content: center; gap: 12px;
    }
    #btn-esc   { order: 1; }
    #btn-pause { order: 2; }
    #dpad-c    { order: 3; width: 28vw; max-width: 120px; height: 28vw; max-height: 120px; }
    #btn-ok    { order: 4; }
    #btn-start { order: 5; }
    /* Scoped to #gamepad: max-width DOES cap a flex item's growth where the width it replaces
       did not, and .sbtn is shared with the ring-egg panel, whose buttons flex to fill the row. */
    #gamepad .sbtn { width: 28vw; max-width: 120px; height: 40px; }
    /* Left-handed: controls move to left panel, info to right; canvas leans right */
    body.lefty #gamepad   { order: 1; }
    body.lefty #wrap      { order: 2; }
    body.lefty #side-info { order: 3; }
}
/* On-screen-keyboard bridge: focused programmatically when name entry opens, its
   keystrokes drive the canvas field. It must stay INSIDE the viewport -- iOS Safari
   shows the keyboard for an off-screen input (left:-9999px) but silently refuses to
   deliver keystrokes to it. So it lives centred but fully transparent, with a 16px
   font so focusing it never triggers the iOS auto-zoom. */
#name-inp {
    position: fixed; top: 50%; left: 50%; width: 2px; height: 2px;
    opacity: 0; font-size: 16px; border: 0; padding: 0; margin: 0;
    background: transparent; color: transparent; caret-color: transparent;
    z-index: -1;   /* behind the canvas: never intercepts taps, focus is programmatic */
}
/* ---------------------------------------------------------------------------
   FLEX-GAP FALLBACK -- the one modern thing this stylesheet cannot express any
   other way. `gap` on a FLEX container is Chrome 84 / Safari 14.1 (grid gap, used
   for the HUD and the portrait pad, is far older and needs nothing), and an engine
   without it collapses the spacing SILENTLY -- no error, just a HUD and a button
   column glued together. @supports cannot tell the two gaps apart, so game.js
   measures flex gap once at boot and tags <html>; only a failing engine gets these
   rules, so the modern path is byte-for-byte what it was.
   Half-margins on the children, not `+ sibling` margins: several of these containers
   reorder with `order:`, which a sibling selector cannot follow. Margins never
   collapse inside a flex container, so the space BETWEEN two items is exact; the
   only difference from gap is a half-gap left at each end of the container. */
html.no-flexgap body > *,
html.no-flexgap #side-info > *   { margin: 4px 0; }   /* body column gap: 8px */
html.no-flexgap .hud-cell > *    { margin: 0 2px; }   /* label/value row: 4px */
html.no-flexgap #ring-egg .row > * { margin: 0 4px; } /* panel button row: 8px */
@media (pointer: coarse) and (orientation: landscape) {
    html.no-flexgap body > *      { margin: 0 calc(3px * var(--ui-scale, 1)); }
    html.no-flexgap #side-info > * { margin: 7px 0; }
    html.no-flexgap #topbar > *   { margin: 0 4px; }
    html.no-flexgap .hud-cell > * { margin: 1px 0; }
    html.no-flexgap #gamepad > *  { margin: 6px 0; }
}

