:root {
  /* Neutral Depth, from tokens.css. The panel stays translucent: it floats
     over the flythrough. */
  --bg: var(--mm-bg);
  --panel: rgba(19, 19, 19, .82);
  --line: var(--mm-rule);
  --text: var(--mm-ink-soft);
  --muted: var(--mm-ink-muted);
  --accent: var(--mm-accent-soft);
  --warn: var(--mm-load-busy);
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
  display: grid;
  grid-template-rows: var(--nav-h) 1fr;
  background: var(--bg);
  color: var(--text);
  font: 13px/1.5 var(--mm-font-mono);
  overflow: hidden;
}
/* A match owns the screen. The shell above the stage is a browser's chrome —
   the tabs out of here and the mod strip under them — and `play/`'s Instant
   Battle screen, which is the same game from the other page, has neither. So
   the bar and its grid row both go while a match is up. Declared after
   `shell.css`, where the mod strip grows `--nav-h` on the same selector.
   All three rules are needed. `display: none` takes the bar out of the grid
   entirely, so the row has to be written again by hand: left as
   `var(--nav-h) 1fr` the stage would become the first grid item, land in that
   now-empty `0px` row and lose the whole pane. One row is the whole layout
   here, because the body holds exactly the bar and the stage — the spawn
   screen and everything else the match draws are inside `#stage` already. */
:root.shell-playing { --nav-h: 0px; }
:root.shell-playing body { grid-template-rows: 1fr; }
:root.shell-playing .shell-nav { display: none; }
/* The flythrough owns everything below the bar. Positioning it makes the
   canvas and the three overlays resolve against the stage rather than the
   viewport, so nothing has to know the bar's height. */
#stage {
  position: relative;
  min-height: 0;
  overflow: hidden;
}
canvas {
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  touch-action: none;
}
#side {
  position: absolute;
  pointer-events: none;
}
#side {
  pointer-events: auto;
  z-index: 3;
  top: 16px; left: 16px;
  width: 260px;
  background: var(--panel);
  border: 1px solid var(--line);
  padding: 14px;
  backdrop-filter: blur(8px);
}
h1 {
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 10px;
}
select {
  width: 100%;
  margin: 0 0 12px;
  padding: 6px 8px;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--line);
  font: inherit;
}
.stat { color: var(--muted); font-size: 12px; }
.stat strong { color: var(--text); font-weight: 500; }
label.opt {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-top: 8px;
  color: var(--muted);
  cursor: pointer;
}
/* On-foot fire control. The crosshair is DOM over the canvas rather than
   painted into it: four bars around a zero-size anchor at screen centre,
   the bar gap driven per frame through --ch-gap by the deviation cone
   mapped through the camera projection (updateCrosshair()). The weapon's
   own crossHairType decides the shape: CHTCrossHair is the full cross,
   CHTIcon is the game's own `hk.tga` at the size and place menu/InGame
   draws it, CHTNone and a scoped aim draw nothing. The dot survives only
   as the fallback for a pack that has no `hk` in it.

   The look is measured off retail footage, not styled: no crosshair
   texture exists in any archive (the engine draws it, `BfCrosshairNode`).
   Two captures agree once they are read in the HUD's 800x600 virtual
   screen, stretched per axis like the rest of the HUD (VHUD-11): a 960-wide
   one of the Thompson on Wake (bars 13 px long, 10.8 units) and the owner's
   2560x1440 one (every arm 10 units long, 1 unit thick -- the node's own
   `Thickness 1` -- and the gap 3.75 units on both axes while moving; ledger
   XHIT-11). So the sizes below are units, `--ch-ux`/`--ch-uy` px each, set
   per frame by updateCrosshair(), and the arms MEET at centre when the
   shooter stands still. Each arm, and the 1x1 centre point the owner's
   server draws (`serverCrossHairCenterPoint`, the layout's fill at
   (400,300)), sits on the (400..401) line, just below and right of the
   exact centre. The ink is the profile's `game.setCrossHairColor` / 256,
   written by updateCrosshair(); the fallback here is the shipped profile's. */
#crosshair {
  position: absolute;
  z-index: 2;
  left: 50%; top: 50%;
  width: 0; height: 0;
  pointer-events: none;
  --ch-ux: 1.2px;
  --ch-uy: 1.2px;
  --ch-gap-x: 0px;
  --ch-gap-y: 0px;
  --ch-ink: rgb(254, 254, 0);
}
#crosshair[hidden] { display: none; }
#crosshair span {
  position: absolute;
  background: var(--ch-ink);
}
#crosshair .ch-dot { display: none; width: var(--ch-ux); height: var(--ch-uy); left: 0; top: 0; }
#crosshair.ch-centre .ch-dot { display: block; }
#crosshair.ch-icon .ch-dot { display: block; }
#crosshair.ch-art .ch-dot { display: none; }
/* The CHTIcon picture. Sized per frame from the layout's own rect through
   --ch-icon-w/h, because the HUD stretches its 800x600 virtual screen
   independently on each axis (hud.js `_scaleFor`, VHUD-11) and this has to
   land where the canvas would have put it. `background`, not `<img>`, so
   the shared `#crosshair span` rule's ink does not paint behind it. */
#crosshair .ch-pic {
  display: none;
  background: center / 100% 100% no-repeat;
  image-rendering: pixelated;
  width: var(--ch-icon-w, 20px); height: var(--ch-icon-h, 20px);
  left: calc(var(--ch-icon-w, 20px) / -2);
  top: calc(var(--ch-icon-h, 20px) / -2);
}
#crosshair.ch-icon.ch-art .ch-pic { display: block; }
#crosshair .ch-n { width: var(--ch-ux); height: calc(10 * var(--ch-uy)); left: 0; bottom: var(--ch-gap-y); }
#crosshair .ch-s { width: var(--ch-ux); height: calc(10 * var(--ch-uy)); left: 0; top: var(--ch-gap-y); }
#crosshair .ch-w { height: var(--ch-uy); width: calc(10 * var(--ch-ux)); top: 0; right: var(--ch-gap-x); }
#crosshair .ch-e { height: var(--ch-uy); width: calc(10 * var(--ch-ux)); top: 0; left: var(--ch-gap-x); }
#crosshair.ch-icon .ch-n, #crosshair.ch-icon .ch-s,
#crosshair.ch-icon .ch-w, #crosshair.ch-icon .ch-e { display: none; }
/* The in-game HUD proper -- health bar, stance figure, magazine picture,
   vehicle panels -- painted by hud.js from the game's own hud-layout.json
   rects and sprites, not styled here. Sits above the 3D canvas (z-index 0)
   and below the DOM crosshair (both z-index 2, untouched this
   round); the generic `canvas` rule above already gives it position/inset/
   size, so only the stacking and click-through need overriding. */
#hud-canvas {
  z-index: 2;
  pointer-events: none;
}
/* The radio strip and the message log (comms.js): the engine draws both in
   the same overlay pass as the HUD, after it. */
#comms-canvas {
  z-index: 3;
  pointer-events: none;
}
/* The in-game console (viewer/console.js). The engine draws its wash and
   its text over everything the 3D view has already put up -- the drawer
   `FUN_00464ee0` runs from the frame's overlay pass, after the HUD -- so
   this sits above every other layer the page owns, the Escape menu
   included: in the game the console comes down over the front end too. It
   is never interactive: the console is a keyboard surface, exactly as it
   is in the game. */
#console-canvas {
  z-index: 13;
  pointer-events: none;
}
#console-canvas[hidden] { display: none !important; }
/* The briefing screen's gate hides the HUD and the radio strip through the
   hidden attribute; the generic canvas rule would defeat it. */
#hud-canvas[hidden], #comms-canvas[hidden] { display: none !important; }

/* The Escape menu (viewer/play/skirmish.js): the front end, back up over
   the battle. Opaque, and over everything the level draws -- the spawn
   screen at 11 included, though Escape takes that down before it gets
   here. The arrow cursor is the pointer's own, as it is on the way-in
   screen. */
#menu-canvas, #menu-canvas-opt {
  z-index: 12;
  touch-action: manipulation;
}
#menu-canvas[hidden], #menu-canvas-opt[hidden] { display: none !important; }
#menu-canvas:focus-visible, #menu-canvas-opt:focus-visible { outline: none; }
/* The controls screen's preview (controls-preview.js) sits over its frame;
   its place and size are set inline from the menu's stage. */
.controls-preview[hidden] { display: none !important; }

/* The debug gate. Everything in `#side` -- the level picker, fog,
   wireframe, vehicles, render-entire-map, pilot, spawn-on-foot and sound --
   is developer gear, and so is the fab that brings it back. A regular
   player gets neither until `show.dev 1`. The elements stay in the DOM so
   that the headless hooks (`__setOnFoot`, `__deploy`, the `?shots` flow)
   and every `optOnFoot.checked` read in this file keep working untouched;
   only their pixels go away. */
body:not(.dev-on) #side,
body:not(.dev-on) .map-controls-fab {
  display: none !important;
}
#flare-canvas {
  z-index: 1;
  pointer-events: none;
}
/* A lost WebGL context leaves the canvas exactly as it last drew -- there
   is no repaint to hint anything went wrong, so this has to be loud enough
   to read over a frozen frame rather than blend into the picture. */
#gl-lost {
  position: absolute;
  z-index: 8;
  inset: 0;
  display: grid;
  place-items: center;
  background: rgba(10, 8, 6, .72);
}
#gl-lost[hidden] { display: none; }
#gl-lost .gl-lost-card {
  pointer-events: auto;
  max-width: min(420px, calc(100% - 32px));
  border: 1px solid var(--warn);
  padding: 14px 18px;
  background: var(--panel);
  text-align: center;
}
#gl-lost .gl-lost-title {
  letter-spacing: .08em;
  text-transform: uppercase;
  font-size: 11px;
  color: var(--warn);
  margin-bottom: 8px;
}
#gl-lost .gl-lost-body { color: var(--text); font-size: 12px; line-height: 1.5; }
#gl-lost button {
  margin-top: 12px;
  cursor: pointer;
  border: 1px solid var(--accent);
  padding: 8px 14px;
  letter-spacing: .08em;
  text-transform: uppercase;
  font-size: 11px;
  background: transparent;
  color: var(--text);
}
/* The map art, top right. That corner is free: #side is top left and the
   stage's bottom centre carries no UI of the page's. No CSS chrome: the game's own `icon_mapbar_small` bezel is
   drawn into the canvas, over the art, the way the engine composes it.
   Retail geometry (measured off the 2559x1441 capture): the ticket plate's
   LEAF rect is x 620..876 y 4..36, but `icon_ticketbar` only inks the first
   176x24 of its 256x32 texture, so the bar a player sees is x 620..796
   y 4..28. The minimap frame is x 620..795.4, y 30..204.9 — flush with the
   painted bar at both sides, two units of sky below it. `MINIMAP_RECT` and
   `syncMinimapToTickets` hold exactly that, put through the HUD's own
   stageW/800 and stageH/600, so the box tracks the bar at any stage size
   instead of sitting at a fixed corner. */
#minimap {
  --mm-size: 196px;
  /* Not covered by the `#side` positioning rule above. */
  position: absolute;
  z-index: 3;
  top: 44px; right: 16px;
  width: var(--mm-size);
}
#minimap[hidden] { display: none; }
#minimap canvas {
  position: static;
  display: block;
  width: 100%;
  /* The box height comes off `MINIMAP_RECT` and the stage
     (`syncMinimapToTickets`); the canvas fills the box it is given. */
  height: 100%;
  /* The art is a top-down render with hard grid lines; smoothing it while
     zoomed in just makes it muddy. */
  image-rendering: pixelated;
}
/* The fullscreen map. Covers the stage, not the viewport — the shared nav
   bar stays reachable. */
#fullmap {
  position: absolute;
  z-index: 5;
  inset: 0;
  display: grid;
  place-items: center;
  background: rgba(8, 8, 8, .86);
  backdrop-filter: blur(3px);
  pointer-events: auto;
}
#fullmap[hidden] { display: none; }
#fullmap .fm-frame { position: relative; }
#fullmap .fm-map-frame {
  border: 1px solid var(--line);
  background: var(--panel);
  box-shadow: 0 18px 60px rgba(0, 0, 0, .6);
}
#fullmap canvas {
  position: static;
  display: block;
  /* Square art, and the stage is the limit in both axes. */
  width: min(78vh, 78vw);
  height: min(78vh, 78vw);
}
#fullmap .fm-cap {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
  padding: 8px 12px;
  border-top: 1px solid var(--line);
  font-size: 11px;
  letter-spacing: .08em;
  color: var(--muted);
}
#fullmap .fm-cap b {
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: .14em;
  font-weight: 500;
}
/* The deploy state: the game's own spawn screen, drawn from `menu/InGame`.
   The engine lays the interface out in an 800x600 virtual space and
   stretches it to the screen — a 1280x720 capture shows the kit rows on a
   100 px pitch where the data says 83, which is 720/600 exactly.
   `maps/_shared/hud/spawn-layout.json` is that node graph flattened
   (extract_spawn_layout.py), and #deploy-chrome paints it: panel plates,
   colour-fill quads, class glyphs, button plates and bitmap-font text, in
   the game's paint order and under the game's own show/hide conditions.
   The level map sits in the rectangle the engine reserves for it, and the
   invisible .fm-hit buttons sit where the game's pointer regions are, so
   the harness selectors (.fm-tab, #deploy-resume) keep working. Below a
   4:3 aspect the stage scales uniformly and letterboxes instead. */
#fullmap .fm-chrome, #fullmap .fm-hit { display: none; }
#fullmap.deploy {
  place-items: stretch;
  /* No backdrop: the capture shows the live world around and through the
     screen — the dim is the map pane's own (drawArt's `dim` fill), not an
     overlay. The engine keeps rendering behind the spawn screen. */
  background: transparent;
  backdrop-filter: none;
  /* Over the flight-speed buttons too: the screen owns the whole stage. */
  z-index: 11;
}
#fullmap.deploy .fm-frame {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}
#fullmap.deploy .fm-map-frame {
  position: absolute;
  border: 0;
  background: none;
  box-shadow: none;
  /* The open/close transition turns the art inside the pane (MMAP-2) with
     enough over-scale to keep the quad covered; the engine clips its map
     quad the same way, so nothing spills past the pane's edge. */
  overflow: hidden;
}
#fullmap.deploy .fm-cap { display: none; }
/* In deploy the map is the target surface, and the cursor should say so. */
#fullmap.deploy canvas#fullmap-canvas {
  width: 100%;
  height: 100%;
  cursor: crosshair;
}
/* The chrome is plain CullNode visibility (MEME-4) — the capture has it
   appear fully formed about 0.13 s after the pane starts moving, which is
   the eased zoom crossing ~0.7. `.deploy-ready` is that threshold. */
#fullmap.deploy.deploy-ready .fm-chrome {
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
#fullmap.deploy.deploy-ready .fm-hit {
  display: block;
  position: absolute;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  opacity: 0;
  cursor: pointer;
}
#fullmap.deploy .fm-hit:disabled { cursor: default; }
/* The score board (`Scoreboard/SpawnScoreBoard` in menu/InGame), painted
   into its canvas from scoreboard-layout.json the way #deploy-chrome paints
   the spawn screen. It stands in for the spawn interface when SCORE BOARD
   opens it (`Kit/ScoreboardSpawnInterface`), so it sits at the same level
   and, later in the document, over it; held on Tab it takes no pointer. */
#scoreboard {
  position: absolute;
  inset: 0;
  z-index: 11;
  pointer-events: none;
}
#scoreboard[hidden] { display: none; }
#scoreboard.from-spawn { pointer-events: auto; }
#scoreboard canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
#scoreboard-done {
  position: absolute;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  opacity: 0;
  cursor: pointer;
}
/* While the board stands in for it the spawn interface is not drawn; the
   state underneath (team, kit, chosen flag) is untouched. */
#fullmap.deploy.board-open { visibility: hidden; }
a { color: var(--accent); }

.side-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 10px;
}
.side-header h1 { margin: 0; }
.controls-collapse-btn {
  display: none;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 2px;
  color: var(--muted);
  font: 10px var(--mm-font-mono);
  letter-spacing: .08em;
  text-transform: uppercase;
  cursor: pointer;
}
.controls-collapse-btn:hover { color: var(--text); border-color: var(--accent); }

.map-controls-fab {
  position: absolute;
  bottom: 18px;
  right: 18px;
  z-index: 10;
  display: none;
  align-items: center;
  gap: 6px;
  padding: 7px 14px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 999px;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--text);
  font: 11px var(--mm-font-mono);
  letter-spacing: .06em;
  text-transform: uppercase;
  cursor: pointer;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
}
.map-controls-fab:hover { border-color: var(--accent); }
.map-controls-fab[hidden] { display: none !important; }

/* The sidebar is debug gear — fog, wireframe, the pilot and on-foot hacks —
   so it starts pulled away on every screen size and the fab brings it back.
   The touch block below only re-homes it. */
#side.side-collapsed { display: none; }
.controls-collapse-btn { display: inline-flex; }
.map-controls-fab { display: inline-flex; }

/* The mobile HUD (features/mobile-four-finger-controls).
   The whole stage is the look surface and the buttons are islands on it, so a
   finger steers from wherever it happens to be and the buttons go wherever a
   thumb naturally rests. That is the tenth pass's answer to "I have to move
   the view, then fire": aiming is not a place you have to put your thumb, it
   is everywhere the button is not.
   Four places, and nothing else:
     the MOVE ring at the bottom-left corner (112 px hit box, 64 px visible),
     a short row of three above it on the left (JUMP, CROUCH, PRONE),
     a short row of three above the corner on the right (AIM, PREV, NEXT),
     the bottom row with RELOAD against FIRE and the MORE tab in between.
   Nothing crosses the middle, and nothing reaches the top two thirds, so the
   game is what you are looking at.
   z order, bottom to top: the look zone (9), the sheet's scrim (10), the
   buttons (11), the move ring (12). A tap with the sheet open closes the
   sheet instead of turning the camera, and the ring is never under it. */
.mobile-zone {
  position: absolute;
  top: 0;
  height: 100%;
  z-index: 9;
  pointer-events: auto;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}
.mobile-zone[hidden] { display: none; }
/* The whole stage, not a half and not two margins: any touch that starts
   where there is no button is the camera. */
#mobile-zone-look { inset: 0; }

#mobile-controls {
  /* A stage-wide glass pane: the buttons anchor to their own corners.
     `pointer-events: none` lets the whole area fall through; the buttons
     and the sheet opt back in. */
  position: absolute;
  inset: 0;
  z-index: 11;
  pointer-events: none;
}
#mobile-controls[hidden] { display: none; }
#mobile-more-scrim {
  position: absolute;
  inset: 0;
  z-index: 10;
  background: rgba(8, 8, 8, 0.38);
  touch-action: none;
}
#mobile-more-scrim[hidden] { display: none; }
#mobile-pad {
  /* FIXED, in stage coordinates at the bottom-left under the left thumb. The
     112 px box is the HIT AREA; the visible ring is its inner 64 px circle
     (#mobile-pad-ring) and the 28 px ball's 18 px of travel is the whole
     input range. MOBILE_PAD_TRAVEL in touch-controls.js describes the same
     geometry; keep the three together. The generous hit box is load bearing:
     a thumb aiming for the ring and landing a little off a 64 px target
     fell through to the pan zone and snapped the view. */
  position: absolute;
  left: 16px;
  bottom: 24px;
  width: 112px; height: 112px;
  z-index: 12;
  border-radius: 50%;
  /* The ring itself owns the input. */
  pointer-events: auto;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}
#mobile-pad-ring {
  position: absolute;
  left: 50%; top: 50%;
  width: 64px; height: 64px;
  transform: translate(-50%, -50%);
  border: 1px solid var(--line);
  border-radius: 50%;
  background: rgba(18, 17, 14, 0.72);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.42), inset 0 0 0 1px rgba(255, 255, 255, 0.04);
  pointer-events: none;
}
#mobile-pad-ring::before,
#mobile-pad-ring::after {
  content: '';
  position: absolute;
  background: rgba(190, 190, 175, 0.18);
}
#mobile-pad-ring::before { left: 50%; top: 8px; bottom: 8px; width: 1px; }
#mobile-pad-ring::after { top: 50%; left: 8px; right: 8px; height: 1px; }
#mobile-pad.is-active #mobile-pad-ring { border-color: var(--accent); }
#mobile-pad-puck {
  position: absolute;
  left: 50%; top: 50%;
  width: 28px; height: 28px;
  border: 1px solid var(--accent);
  border-radius: 50%;
  background: rgba(42, 42, 35, 0.94);
  box-shadow: 0 3px 12px rgba(0, 0, 0, 0.45);
  transform: translate(-50%, -50%);
  pointer-events: none;
}
#mobile-pad-puck.is-firing { border-color: var(--warn); }
/* The mode caption (MOVE / DRIVE / AIM / STICK) under the ring: the ball is
   too small to carry a label. */
#mobile-pad-label {
  position: absolute;
  top: calc(100% + 2px);
  left: 50%;
  transform: translateX(-50%);
  color: var(--muted);
  font: 8px var(--mm-font-mono);
  letter-spacing: .08em;
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none;
}
/* Two short rows of three, one per thumb, at the same height above the bottom
   row. They hug the sides and stop well short of the middle, so the centre
   corridor and the whole upper screen stay clear for the game and for
   dragging. 50 px targets with 6 px between neighbours, so a thumb never
   travels more than one button width. `pointer-events: none` on the row and
   `auto` on the buttons, so the gaps fall through and pan like any other open
   screen. */
.mobile-row {
  position: absolute;
  bottom: 146px;
  display: flex;
  align-items: stretch;
  gap: 6px;
  pointer-events: none;
}
.mobile-row-left { left: 14px; }
.mobile-row-right { right: 14px; }
.mobile-band-btn {
  flex: 0 0 auto;
  width: 50px;
  height: 50px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 25px;
  background: rgba(18, 17, 14, 0.82);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--text);
  font: 9px var(--mm-font-mono);
  letter-spacing: .02em;
  text-transform: uppercase;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
  pointer-events: auto;
  /* `none`, not `manipulation`: every action runs on its pointerdown edge,
     because a tap the browser has to synthesise a `click` for is a tap the
     browser drops when a second finger is already down. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  cursor: pointer;
}
.mobile-band-btn:active,
.mobile-band-btn.is-active {
  border-color: var(--accent);
  background: rgba(72, 72, 58, 0.92);
}
/* PRONE is a toggle where the rest of the band is holds, and the button has to
   say so: a lit PRONE reads as the state it is, in the warn colour the rest of
   the HUD uses for the trigger. */
.mobile-band-btn.prone-btn.is-active {
  border-color: var(--warn);
  background: rgba(88, 60, 28, 0.92);
  color: #f0e0c8;
}
/* The tab for the sheet, in the bottom gap between the ring's hit box (ends
   128 px) and RELOAD (starts 200 px). It is placed on those numbers rather than
   centred on the screen, because centring it put it under RELOAD. */
.mobile-more-btn {
  position: absolute;
  left: 136px;
  bottom: 28px;
  width: 56px;
  height: 36px;
  padding: 0;
  border: 1px dashed var(--line);
  border-radius: 18px;
  background: rgba(18, 17, 14, 0.66);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--muted);
  font: 8px var(--mm-font-mono);
  letter-spacing: .08em;
  text-transform: uppercase;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.36);
  pointer-events: auto;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  cursor: pointer;
}
.mobile-more-btn.is-active {
  border-style: solid;
  border-color: var(--accent);
  color: var(--text);
}
/* The right corner: RELOAD against FIRE. Reloading is common, so it is the
   button next to the trigger rather than a tab's worth of taps away. */
.mobile-fire-cluster {
  position: absolute;
  right: 12px;
  bottom: 20px;
  display: flex;
  align-items: flex-end;
  gap: 8px;
  pointer-events: none;
}
.mobile-action-btn {
  min-width: 48px;
  height: 40px;
  padding: 0 8px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: rgba(18, 17, 14, 0.82);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--text);
  font: 9px var(--mm-font-mono);
  letter-spacing: .06em;
  text-transform: uppercase;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
  pointer-events: auto;
  /* `none`, not `manipulation`: every action runs on its pointerdown edge,
     because a tap the browser has to synthesise a `click` for is a tap the
     browser drops when a second finger is already down. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  cursor: pointer;
}
.mobile-action-btn:hover { border-color: var(--accent); }
.mobile-action-btn:active,
.mobile-action-btn.is-active {
  border-color: var(--accent);
  background: rgba(72, 72, 58, 0.92);
}
.mobile-action-btn:disabled {
  opacity: 0.38;
  cursor: default;
  border-color: var(--line);
  background: rgba(18, 17, 14, 0.62);
}
.mobile-action-btn.fire-btn {
  /* The right thumb's button, at the bottom-right corner, so moving (left
     thumb, ring) and firing happen at the same time. Big enough to hold and
     still drag off, since a deliberate drag off a held FIRE turns the camera. */
  width: 104px;
  height: 82px;
  padding: 0;
  border-radius: 12px;
  border-color: var(--warn);
  color: var(--warn);
  font-size: 12px;
}
.mobile-fire-cluster #mobile-reload-btn {
  width: 66px;
  height: 66px;
  padding: 0;
  border-radius: 12px;
  font-size: 9px;
}
.mobile-action-btn.fire-btn.is-active {
  background: rgba(105, 35, 25, 0.92);
  color: #f4d7cf;
}
.mobile-action-btn[hidden],
.mobile-band-btn[hidden],
.mobile-more-btn[hidden],
.mobile-throttle[hidden],
#mobile-more-sheet[hidden],
#mobile-pad[hidden] { display: none; }

/* The action sheet: a three-column grid of full-size buttons hanging from the
   TOP of the stage. A drop-down rather than a bottom sheet, because both bottom
   corners already belong to a thumb and a bottom sheet would either cover the
   posture column or need an offset tuned per screen height. Up here it is clear
   of the ring, the posture pair, FIRE and AIM in every orientation, and it is
   inside index-finger reach. */
#mobile-more-sheet {
  position: absolute;
  left: 10px; right: 10px; top: 12px;
  z-index: 12;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: rgba(19, 19, 19, 0.92);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.55);
  pointer-events: auto;
}
.mobile-more-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
.mobile-more-grid .mobile-action-btn {
  min-width: 0;
  height: 54px;
  padding: 0 4px;
  font-size: 10px;
}
#mobile-more-grid .mobile-throttle,
.mobile-more-grid .mobile-throttle {
  grid-column: 1 / -1;
  justify-content: center;
}

.mobile-throttle {
  display: flex;
  align-items: center;
  gap: 4px;
  min-height: 28px;
  padding: 0 6px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: rgba(18, 17, 14, 0.82);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--muted);
  font: 8px var(--mm-font-mono);
  letter-spacing: .06em;
  pointer-events: auto;
  touch-action: none;
}
.mobile-throttle input {
  width: 72px;
  accent-color: var(--accent);
}
.mobile-throttle output {
  min-width: 22px;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

@media (max-width: 720px) {
  #minimap {
    --mm-size: 118px;
    top: 34px;
    right: 10px;
  }
  /* No pad-size override here on purpose: the ring and ball carry the input
     geometry (MOBILE_PAD_TRAVEL in touch-controls.js), so one CSS size keeps
     the two honest, and the phone is the case that matters, so there is
     nothing to shrink to. The band's own size is set by the flex row, so it
     is never smaller than a 56 px target either. */
  .mobile-action-btn {
    min-width: 44px;
    padding: 0 6px;
    font-size: 8px;
  }
  .mobile-more-grid .mobile-action-btn {
    height: 52px;
    font-size: 9px;
  }
  .mobile-throttle input {
    width: 64px;
  }

  #side {
    top: auto;
    bottom: 16px;
    left: 12px;
    right: 12px;
    width: auto;
    max-height: 48vh;
    overflow-y: auto;
    border-radius: 4px;
  }
  #side.side-collapsed {
    display: none;
  }
  .controls-collapse-btn { display: inline-flex; }
  .map-controls-fab {
    display: inline-flex;
    /* Top-left, out of both thumb arcs: the bottom-right corner belongs to
       FIRE and the bottom-left to the move ring. */
    top: 10px;
    left: 12px;
    right: auto;
    bottom: auto;
  }
}
/* Landscape on a phone needs nothing special: the band is already a row and
   the corner cluster is 102 px tall whatever the stage height. The one thing
   that moves is the ring, which wants the bottom corner of a wide screen. */

/* The room join modal (P3): the game's own overlay for a room that will
   not open -- a missing room server, a full room, a bad code. The panel
   is this page's tokens, the deploy screen's family; pre-game it floats
   over the level, under the console (13) and the Escape menu (12), which
   keep the z the engine's layering gives them. */
#roomIssue {
  position: absolute;
  inset: 0;
  z-index: 10;
  display: grid;
  place-items: center;
  background: rgba(0, 0, 0, .55);
}
#roomIssue[hidden] { display: none; }
#roomIssue .ri-panel {
  width: min(440px, calc(100% - 48px));
  background: var(--panel);
  border: 1px solid var(--line);
  padding: 20px 22px;
  font: 12px/1.55 var(--mm-font-mono);
  color: var(--text);
  box-shadow: 0 10px 40px rgba(0, 0, 0, .45);
}
#roomIssue .ri-title {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 10px;
  color: var(--accent);
  font-weight: 700;
  letter-spacing: .12em;
}
#roomIssue .ri-code { color: var(--muted); font-weight: 400; letter-spacing: 0; }
#roomIssue .ri-message { margin: 0 0 16px; white-space: pre-wrap; }
#roomIssue .ri-actions { display: flex; gap: 8px; }
#roomIssue button {
  flex: 1;
  padding: 8px 0;
  font: inherit;
  color: var(--text);
  background: #222;
  border: 1px solid var(--line);
  cursor: pointer;
}
#roomIssue button:hover { border-color: var(--accent); color: var(--accent); }
#roomIssue .ri-solo { color: var(--muted); }
