/* game-shell.css - shared full-game chrome for every game page: a pause
   button (upper-left) and a close/X button (upper-right).
   - Real small/mobile viewports, EITHER orientation (<=480px on the
     short axis -- width in portrait, height in landscape): the shell
     goes edge-to-edge fullscreen, and the pause/close/mute buttons float
     as an overlay over the top corners of the gameplay itself (there's
     no room to spare). Rotating a real phone to landscape before
     starting a game (or mid-game) fills the screen the same way instead
     of falling into the centered-frame treatment below -- every control
     is positioned via CSS relative to the shell's own corners (see
     .game-shell-joystick etc.), so it just naturally follows whichever
     way the shell itself is currently laid out; no per-orientation
     control-position logic needed.
   - Larger viewports (tablet/desktop -- NOT a real phone's short axis in
     either orientation): the shell renders as a centered, "large mobile
     size" phone-shaped frame instead of stretching to fill the whole
     browser window, and the pause/close buttons sit in their own bar
     ABOVE the play area -- outside of gameplay, not overlaid on top of
     it. */

/* Real bug found & fixed: the mobile fullscreen shell is position:fixed,
   which should ignore page scroll entirely -- but a stray scroll can
   still sneak in on some mobile browsers (their own chrome animating,
   an odd touch gesture) with nothing else on the page TO scroll, since
   the shell already covers the whole viewport. Once that happens the
   fixed shell doesn't move but the page's own scroll offset does,
   shifting what's visible through it -- reported as "the top of the
   play area is hidden and the interface prevents scrolling" (there's
   no visible scrollbar/content to grab to scroll back). html+body get
   locked to a fixed, non-scrollable box here so there's never a scroll
   position to drift from in the first place; game-shell.js additionally
   forces scrollTo(0,0) at the moments that scroll tends to sneak in
   (load, resize, orientation change, tab refocus) as a second layer, in
   case some mobile browser quirk defeats the CSS lock. */
html, body.has-game-shell {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}

body.has-game-shell {
  margin: 0;
}

/* Real-mobile stats-row clearance for the shell's floating button
   cluster -- shared here so every game's own stats-row padding
   references ONE number instead of each re-measuring/duplicating it
   independently. That duplication is exactly what went stale: Slither's
   own stats-row padding (slither.css) was hand-tuned for a 2-button left
   cluster (pause + mute) and documented "if the shell's button sizes/
   count ever change, re-measure and adjust these" -- and then adding
   this settings gear button as a 3rd button did exactly that, silently
   breaking Slither's own already-tuned value AND leaving Spiders/Vines
   (which never got an equivalent fix at all) newly broken too, caught
   via a live screenshot showing FLOOR/LEVEL text overlapping the gear
   icon. Centralizing the two numbers here means a future 4th button
   only needs updating in one place, not N per-game copies.
   Left = bar's own 14px inset + 3 buttons (38px each) + 2 gaps (8px
   each) = 144px to the cluster's right edge, minus a little slack so
   stats sit clearly clear rather than flush. Right = bar inset + 1
   button (close, alone) = 52px, minus the same slack. */
:root {
  --gs-bar-left-clear: 136px;
  --gs-bar-right-clear: 46px;
}

.game-shell {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  /* Desktop portrait lock (per explicit instruction, Nov 7 2026: on
     desktop the game stays in a portrait phone-shaped frame, since
     phone players are the target market): this centered "large mobile"
     frame IS the default -- every viewport gets it unless the
     real-mobile fullscreen query below overrides it. That ordering is
     deliberate: a viewport matching NEITHER query (a device reporting
     `pointer: none`, a desktop window of an odd shape, a browser that
     doesn't understand `pointer` at all) still gets a bounded portrait
     frame instead of an unbounded full-width shell. The fullscreen
     query is the only thing that takes the frame away, and only for
     real touch phones. */
  width: min(430px, 94vw);
  /* Real bug found & fixed: same root cause as the mobile-fullscreen
     dvh fix below ("games are too tall and going under the bottom
     bar" on tablet) -- plain vh resolves against the LARGE viewport
     (as if a collapsible browser toolbar were already hidden), not
     the currently-visible one, so when that toolbar IS showing on a
     real tablet, this frame sized itself taller than the actual
     visible viewport and its bottom (joysticks, stats) ran under the
     toolbar. dvh tracks the real visible viewport live instead. */
  height: min(880px, 92vh);  /* fallback for browsers without dvh */
  height: min(880px, 92dvh);
  margin: 28px auto;
  border-radius: 32px;
  overflow: hidden;
  box-shadow: var(--shadow);
  border: 1px solid var(--border);
  /* Per explicit instruction ("none of the games should zoom or show
     context menus in game mode"): `manipulation` disables double-tap
     -zoom (and removes the browser's click-delay after a tap) without
     affecting normal scrolling where a game actually needs it (e.g.
     Doors' guess history) -- pinch-zoom itself is blocked separately,
     at the JS level, in game-shell.js (`manipulation` alone still
     permits pinch per spec). Site-wide on the shared shell rather than
     per-game, so every page that wraps its content in .game-shell gets
     this for free. */
  touch-action: manipulation;
  /* A real, reported bug: a long-press anywhere in a game (e.g. on the
     Score/Coins stat text) triggers the browser's own native text
     -selection UI (drag handles + a blue highlight) on mobile Safari --
     same "browser chrome interrupting gameplay" family as the zoom/
     context-menu fixes above, just a different gesture (`touchmove`'s
     multi-touch guard and `touch-action` don't cover a single-finger
     long-press). `user-select: none` is a normal INHERITED property (
     unlike `touch-action`), so setting it once here covers every
     descendant's text automatically. `-webkit-touch-callout: none`
     additionally suppresses iOS's own long-press callout/preview menu
     (e.g. on an image or link), which `user-select` alone doesn't. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

.game-shell-bar {
  /* Frame default: the bar is its own row ABOVE the play area -- outside
     of gameplay, not overlaid on top of it. (The fullscreen query below
     flips it back to an absolute overlay for real phones.) */
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  pointer-events: none; /* clicks pass through the empty middle */
}

.game-shell-bar > * {
  pointer-events: auto;
}

/* Groups the pause + mute buttons together in the bar's left corner,
   leaving close on its own in the right corner -- see game-shell.js's
   markup contract. */
.game-shell-bar-left {
  display: flex;
  gap: 8px;
}

.game-shell-btn {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  /* Frame default: quiet buttons on the panel bar. (The fullscreen
     query below restores the dark translucent overlay buttons.) */
  background: transparent;
  color: var(--muted);
}

.game-shell-btn:hover {
  background: var(--bg);
  color: var(--accent);
}

.game-shell-stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
}

.game-shell-pause-overlay {
  position: absolute;
  inset: 0;
  z-index: 15;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(10, 13, 16, .72);
  color: #fff;
  font-family: Georgia, serif;
  font-size: 20px;
  cursor: pointer;
}

.game-shell-pause-overlay[hidden] {
  display: none;
}

/* -- Real mobile: fullscreen (minus a reserved bottom strip), buttons
   overlay the gameplay's top corners -- Deliberately NOT reserving top
   padding for them here -- per explicit instruction, gameplay stays truly
   full-bleed at the top and spawn logic for movable/collectible items
   instead avoids the buttons' circles (see GameShell.controlZones()). A
   thin UI strip (a game's own stats row, say) can still add its own small
   clearance if it needs to; that's a per-game call, not a shell-wide one.

   The fullscreen treatment is scoped to real touch phones ONLY
   (`pointer: coarse` on both conditions): a DESKTOP browser window that
   merely happens to be narrow (<=480px wide) or short (<=480px tall in
   landscape) must NOT get it -- before this scoping, a short desktop
   window (e.g. 1280x400) matched the landscape clause and the game
   stretched edge-to-edge across the whole desktop instead of staying
   in its portrait phone-shaped frame. The frame is the default (see
   .game-shell above), so a desktop window that matches no fullscreen
   condition simply keeps the frame. A real phone is touch-primary, so
   `pointer: coarse` still catches every phone in either orientation
   while a mouse-driven window never matches.

   This query undoes the frame default piece by piece: the fixed shell
   must reset the frame's width/margin/border-radius/border/shadow
   (note `width: auto` -- with left:0 AND right:0 set, a leftover
   430px width would over-constrain the box and pin it to the left
   instead of stretching), and the bar/buttons go back to their
   overlay look (absolute bar over the gameplay's top corners, dark
   translucent buttons) instead of the frame's panel row.

   The BOTTOM edge is different: real phones (iPhone/Samsung standard
   portrait dimensions) reserve a home-indicator / gesture-nav strip there
   that a fullscreen page can't draw controls into reliably -- content and
   joysticks were getting clipped or made hard to tap under it. So the
   shell itself stops 60px above the true viewport bottom (5/8in at the
   standard 96dpi CSS-px convention used elsewhere in this project -- see
   spiders.js's inchesToPx) rather than running edge-to-edge like the
   other three sides; every game's
   bottom-anchored joystick(s) live inside the shell, so they shift up
   with it automatically. The strip below is left blank (shows the page's
   own --bg through), not styled as its own element -- and this same
   reserve carries over correctly in landscape too, since it's expressed
   relative to whichever edge is currently the visual bottom, not a
   fixed physical edge of the device.

   Real bug found & fixed: this used to be `bottom: 60px` on a
   `position:fixed` box with no explicit height -- on real iPhone Safari
   (not just simulated/desktop-emulated mobile), a fixed element's
   implicit height resolves against the LARGE viewport (as if Safari's
   own collapsible bottom URL/tab bar were already hidden), not the
   currently-visible one. When that bar is actually showing, it covers
   real content down there that our 60px reserve never accounted for --
   reported as "can't use the bottom 1/2in on an iPhone (browser bar)".
   `height: 100dvh` (the CSS *dynamic* viewport height unit, unlike
   plain `vh`) tracks Safari's chrome live instead of assuming it's
   hidden, so switching to an explicit height built from it fixes this;
   `100vh` is kept as a fallback declared first for browsers that don't
   support `dvh` (an unsupported unit invalidates the whole declaration,
   which CSS then just ignores, leaving the vh version in effect). */
@media (max-width: 480px) and (pointer: coarse), (max-height: 480px) and (orientation: landscape) and (pointer: coarse) {
  .game-shell {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    width: auto; /* stretch between left/right -- see the comment above */
    height: calc(100vh - 60px);  /* fallback for browsers without dvh */
    height: calc(100dvh - 60px); /* 5/8in @ 96dpi reserved, against the REAL visible viewport */
    margin: 0;
    border-radius: 0;
    border: none;
    box-shadow: none;
    z-index: 100;
  }

  .game-shell-bar {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: 20;
    background: transparent;
    border-bottom: none;
    padding: 14px;
  }

  .game-shell-btn {
    background: rgba(10, 13, 16, .55);
    color: #fff;
    backdrop-filter: blur(2px);
  }

  .game-shell-btn:hover {
    background: rgba(10, 13, 16, .75);
  }
}

/* -- Shared virtual joystick -------------------------------------------
   Per the controls convention: right side = move, always; left side =
   fire (games with firing) or jump (games without). Fixed near the
   bottom corners for natural thumb reach, clear of the top-corner
   pause/close buttons. Markup: a ".game-shell-joystick" container (plus
   ".gs-joy-right"/".gs-joy-left" for placement) with one
   ".game-shell-joy-nub" child -- see GameShell.makeJoystick(). */
.game-shell-joystick {
  position: absolute;
  bottom: 22px;
  width: 96px;
  height: 96px;
  border-radius: 50%;
  background: rgba(23, 30, 37, .55);
  border: 2px solid rgba(255, 255, 255, .18);
  touch-action: none;
  z-index: 10;
}

.gs-joy-right {
  right: 18px;
}

.gs-joy-left {
  left: 18px;
}

.game-shell-joy-nub {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 40px;
  height: 40px;
  margin: -20px 0 0 -20px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .5);
  pointer-events: none;
  transition: background .15s ease;
}

.game-shell-joystick.is-active .game-shell-joy-nub {
  background: var(--accent);
}

/* -- (The centered frame itself is the base .game-shell/.game-shell-bar/
   .game-shell-btn above -- there is no separate "frame" media query any
   more, so a viewport matching no query below still gets the frame.) -- */

/* -- Tablet: same centered frame as above, but the size caps are 25%
   bigger -- per explicit instruction/follow-up: a tablet's actual screen
   offers much more room than the ~430px frame above was using (that
   frame was originally sized for "large mobile," with tablet and desktop
   both landing on the exact same cap), so tablet should visibly get more
   real estate, not just bigger game elements inside the same-sized box
   (see static/slither.js's TABLET_SCALE for the sibling per-element
   scaling). Shared shell infrastructure, so every game gets the bigger
   frame on tablet, not just the one that prompted it.

   Detected the same way TABLET_SCALE is: a touch-primary device
   (pointer:coarse) past the real-mobile-phone breakpoint, layered on
   top of the centered-frame base above so desktop (mouse,
   pointer:fine) is unaffected -- desktop keeps the original 430/880 caps
   per explicit instruction ("desktop stays as-is, ... usually
   landscape"). The 94vw/92vh viewport-relative safety caps are left
   alone (still there to stop the frame overflowing a smaller tablet),
   only the fixed px caps grow. This is a plain top-level comma-separated
   @media list (always-valid standard CSS, distinct from the nested-
   comma-inside-parens mistake documented in slither.js's own
   TABLET_SCALE comment -- that one was inside a JS matchMedia() STRING
   using boolean and/or syntax, not a top-level stylesheet @media list). */
@media (min-width: 481px) and (min-height: 481px) and (pointer: coarse),
       (min-width: 481px) and (orientation: portrait) and (pointer: coarse) {
  .game-shell {
    width: min(538px, 94vw);
    height: min(1100px, 92vh);  /* fallback for browsers without dvh -- see the base centered-frame rule's own dvh comment above */
    height: min(1100px, 92dvh);
  }
}

/* -- Volume settings panel -- injected into <body> at runtime by
   game-shell.js (see its own "Volume settings panel" section), not part
   of any game's own markup. Same dark-overlay-plus-panel language as
   highscores.css's .hs-overlay/.hs-panel, kept independent (not a shared
   class) since the two are unrelated features that just happen to look
   similar. */
.gs-settings-overlay {
  position: fixed;
  inset: 0;
  z-index: 500;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, .75);
  padding: 16px;
}

.gs-settings-overlay[hidden] {
  display: none;
}

.gs-settings-panel {
  width: min(320px, 92vw);
  background: #0f172a;
  border: 1px solid rgba(255, 255, 255, .15);
  border-radius: 12px;
  padding: 20px 18px;
  color: #fff;
  font-family: system-ui, -apple-system, sans-serif;
}

.gs-settings-title {
  font-size: 16px;
  font-weight: 700;
  margin-bottom: 16px;
  text-align: center;
}

.gs-settings-row {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 16px;
}

.gs-settings-label {
  font-size: 13px;
  opacity: .85;
}

.gs-settings-slider {
  width: 100%;
  touch-action: manipulation;
}

.gs-settings-close-btn {
  display: block;
  width: 100%;
  margin-top: 4px;
  padding: 10px 0;
  border: 1px solid rgba(255, 255, 255, .25);
  border-radius: 8px;
  background: transparent;
  color: #fff;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}

.gs-settings-close-btn:hover {
  background: rgba(255, 255, 255, .1);
}

/* -- Personal player code row -- see game-shell.js's own "fourth
   settings item" comment. A divider marks it as visually distinct from
   the volume sliders above it, since it isn't a sound setting at all. */
.gs-settings-divider {
  height: 1px;
  background: rgba(255, 255, 255, .15);
  margin: 4px 0 16px;
}

.gs-settings-row-playercode {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.gs-settings-checkbox-label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  opacity: .85;
  flex: 1;
}

.gs-settings-checkbox-label input[type="checkbox"] {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  touch-action: manipulation;
}

.gs-settings-code-input {
  width: 56px;
  flex-shrink: 0;
  padding: 8px 6px;
  border: 1px solid rgba(255, 255, 255, .25);
  border-radius: 6px;
  background: rgba(255, 255, 255, .06);
  color: #fff;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  text-align: center;
  text-transform: uppercase;
  letter-spacing: 2px;
}

.gs-settings-code-input::placeholder {
  color: rgba(255, 255, 255, .35);
  letter-spacing: 2px;
}

/* -- Shared toast -- injected into <body> at runtime by game-shell.js's
   showToast(), the first time it's actually used. Fixed near the top of
   the viewport, clear of every game's own bottom-anchored joystick/stats
   and above the shell's own pause/mute/close bar. */
.gs-toast {
  position: fixed;
  top: max(64px, env(safe-area-inset-top, 0px) + 56px);
  left: 50%;
  z-index: 600;
  max-width: min(320px, 88vw);
  padding: 12px 18px;
  background: rgba(15, 23, 42, .92);
  border: 1px solid rgba(255, 255, 255, .15);
  border-radius: 10px;
  color: #fff;
  font-family: system-ui, -apple-system, sans-serif;
  font-size: 14px;
  line-height: 1.4;
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, -8px);
  transition: opacity .3s ease, transform .3s ease;
}

.gs-toast[hidden] {
  display: none;
}

.gs-toast.is-visible {
  opacity: 1;
  transform: translate(-50%, 0);
}

/* Visually hidden but read by screen readers and crawlers -- used for each
   game page's <h1> (the game title), so pages have a real top-level heading
   without changing the visible game layout. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
