/* Nothing in the UI is text you are meant to select. Drag-selecting a label
   mid-firefight leaves a blue highlight stuck on screen, and on the globe it
   makes city names look like broken links. Inputs opt back in below. */
:root, body {
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
input, textarea, [contenteditable="true"] {
  -webkit-user-select: text;
  -moz-user-select: text;
  user-select: text;
}

/* In-game HUD. Everything here is non-interactive overlay except the buttons,
   so pointer-events stay off by default and are re-enabled selectively. */

#game-screen { position: fixed; inset: 0; }

#game-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.crosshair {
  position: absolute;
  left: 50%; top: 50%;
  width: 22px; height: 22px;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 3;
}
.crosshair span {
  position: absolute;
  background: rgba(255, 255, 255, 0.86);
  box-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
}
.crosshair span:nth-child(1) { left: 50%; top: 0; width: 1.5px; height: 7px; margin-left: -0.75px; }
.crosshair span:nth-child(2) { left: 50%; bottom: 0; width: 1.5px; height: 7px; margin-left: -0.75px; }
.crosshair span:nth-child(3) { top: 50%; left: 0; height: 1.5px; width: 7px; margin-top: -0.75px; }
.crosshair span:nth-child(4) { top: 50%; right: 0; height: 1.5px; width: 7px; margin-top: -0.75px; }
.crosshair.hit span { background: #ff5f4a; }
.crosshair.kill {
  animation: kill-confirm 0.19s cubic-bezier(0.16, 0.84, 0.28, 1);
}
.crosshair.kill span {
  background: #fff3d2;
  box-shadow: 0 0 8px rgba(255, 182, 72, 0.9);
}
.crosshair.kill::after {
  content: "";
  position: absolute;
  inset: -6px;
  border: 1px solid rgba(255, 182, 72, 0.9);
  border-radius: 50%;
  animation: kill-ring 0.19s ease-out forwards;
}
@keyframes kill-confirm {
  0% { transform: translate(-50%, -50%) rotate(45deg) scale(0.7); }
  52% { transform: translate(-50%, -50%) rotate(45deg) scale(1.45); }
  100% { transform: translate(-50%, -50%) rotate(45deg) scale(1); }
}
@keyframes kill-ring {
  from { opacity: 1; transform: scale(0.45); }
  to { opacity: 0; transform: scale(1.45); }
}
@media (prefers-reduced-motion: reduce) {
  .crosshair.kill { animation: none; }
  .crosshair.kill::after { animation: none; opacity: 0; }
}

/* Directional damage. The wedge rotates to point at whatever hit you, which is
   the only reliable way to locate a shooter you never saw. */
.damage-ring {
  position: absolute;
  left: 50%; top: 50%;
  width: 360px; height: 360px;
  margin: -180px 0 0 -180px;
  z-index: 5;
  pointer-events: none;
}
.damage-mark {
  position: absolute;
  left: 50%; top: 50%;
  width: 0; height: 0;
  transform-origin: 0 0;
  animation: dmg-fade 1.1s ease-out forwards;
}
.damage-mark::before {
  content: '';
  position: absolute;
  left: -46px;
  top: -172px;
  width: 92px;
  height: 26px;
  border-radius: 50% 50% 0 0 / 100% 100% 0 0;
  background: linear-gradient(to top, rgba(255, 70, 60, 0), rgba(255, 90, 74, 0.92));
}
@keyframes dmg-fade {
  0% { opacity: 0; }
  12% { opacity: 1; }
  100% { opacity: 0; }
}

/* A restrained red edge on the screen, scaled by how hurt you are. */
.damage-vignette {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.35s ease;
  background: radial-gradient(ellipse at center,
    rgba(255, 40, 40, 0) 45%,
    rgba(255, 40, 40, 0.16) 78%,
    rgba(200, 20, 20, 0.42) 100%);
}
.damage-vignette.hit { transition: opacity 0.06s ease; }

/* The sky door cut. Cyan, because the doors are cyan and because red is the
   one colour a full-screen wash must never be — a player who has just been
   moved must not read it as damage. Opacity is set from JS and released by
   the transition. */
.warp-flash {
  position: absolute;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.9s ease-out;
  background: radial-gradient(ellipse at center,
    rgba(79, 216, 255, 0.85) 0%,
    rgba(79, 216, 255, 0.55) 45%,
    rgba(20, 60, 90, 0.9) 100%);
}
.warp-flash.on { transition: none; opacity: 1; }

/* The glider, from inside it: the wing is drawn in the world; only the fold
   hint lives here. Hidden except while gliding. */
.glider-hud {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
}
.glider-note {
  position: absolute;
  top: 17vh;
  left: 50%;
  transform: translateX(-50%);
  font-size: 11px;
  letter-spacing: 0.22em;
  color: rgba(79, 216, 255, 0.85);
  text-shadow: 0 0 10px rgba(79, 216, 255, 0.5);
  white-space: nowrap;
}
.glider-note b {
  color: #fff;
  font-weight: 700;
}
body.touch-mode .glider-note { top: 12vh; font-size: 10px; }

/* Comfort mode's movement vignette. Neutral dark rather than tinted: it must
   never be mistaken for the red damage vignette, or the one signal a player
   cannot afford to misread becomes ambiguous. Sits below it in the stack.
   Opacity is driven from the frame loop, so no CSS transition here — the
   easing happens in JS against real frame time. */
.comfort-vignette {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(ellipse at center,
    rgba(0, 0, 0, 0) 42%,
    rgba(2, 4, 9, 0.35) 76%,
    rgba(2, 4, 9, 0.85) 100%);
}

/* TEMPORARY dev console. Deliberately plain and unbranded — it is a tool, not
   part of the game, and it should look like something that will be deleted. */
.dev-console {
  position: absolute;
  left: 50%; top: 18%;
  transform: translateX(-50%);
  display: flex; align-items: center; gap: 8px;
  min-width: 420px;
  padding: 8px 12px;
  background: rgba(4, 8, 14, 0.92);
  border: 1px solid rgba(140, 170, 210, 0.3);
  border-radius: 3px;
  z-index: 30;
  pointer-events: auto;
}
.dev-console-caret { color: var(--muted); font-size: 12px; }
.dev-console-input {
  flex: 1;
  background: transparent;
  border: 0;
  outline: none;
  color: var(--ink);
  font-family: ui-monospace, Menlo, monospace;
  font-size: 12.5px;
}
.dev-console-note { color: var(--muted); font-size: 10.5px; letter-spacing: 0.12em; }

/* Controls, grouped by what you are doing. Thirteen bindings running down one
   column is a wall nobody reads; four short topics is something you can find a
   key in. Two across on a wide menu, one on a narrow one. */
.key-groups {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 14px 18px;
}
.key-group { display: flex; flex-direction: column; gap: 3px; }
.key-group-name {
  font-size: 8.5px;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--accent);
  opacity: 0.75;
  margin-bottom: 3px;
}
.key-group span { font-size: 11px; color: var(--muted); }
.key-group b {
  display: inline-block;
  min-width: 42px;
  color: var(--ink);
  font-weight: 600;
}

/* Explanatory line under a setting. Comfort mode needs one: the name alone
   does not tell someone who is feeling ill whether it will help them. */
.setting-note {
  font-size: 10.5px;
  line-height: 1.55;
  color: var(--muted);
  margin: -2px 0 12px;
  max-width: 42ch;
}

/* Simulated players are no longer dimmed. The dimming only ever meant anything
   to someone who already knew what it encoded; to a stranger it read as a
   rendering fault or a player who had dropped. */

.board-fps {
  float: right;
  color: var(--muted);
  letter-spacing: 0.18em;
}

/* Your own shield, seen from inside a dome.
   Straight hatching reads as flat glass, so the geometry here is curved: rings
   that compress toward the edge to imply a surface falling away from you, and
   meridians radiating from directly overhead. Together they read as a
   hemisphere rather than a pane. The centre stays clear for aiming. */
.shield-overlay {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  animation: shield-breathe 2.2s ease-in-out infinite;
}
/* Latitude rings. Spacing tightens with distance from centre, which is what
   sells curvature — evenly spaced rings would look like a flat target. */
.shield-overlay::before {
  content: '';
  position: absolute;
  inset: -20%;
  background:
    repeating-radial-gradient(circle at 50% 58%,
      rgba(111, 232, 255, 0) 0 58px,
      rgba(111, 232, 255, 0.16) 58px 60px),
    repeating-radial-gradient(circle at 50% 58%,
      rgba(111, 232, 255, 0) 0 178px,
      rgba(111, 232, 255, 0.10) 178px 181px);
  -webkit-mask-image: radial-gradient(circle at 50% 58%,
    rgba(0, 0, 0, 0) 34%, rgba(0, 0, 0, 0.75) 72%, rgba(0, 0, 0, 1) 100%);
  mask-image: radial-gradient(circle at 50% 58%,
    rgba(0, 0, 0, 0) 34%, rgba(0, 0, 0, 0.75) 72%, rgba(0, 0, 0, 1) 100%);
}
/* Meridians converging on the apex, plus the rim glow where the dome meets
   the ground. */
.shield-overlay::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    repeating-conic-gradient(from 0deg at 50% 58%,
      rgba(111, 232, 255, 0.13) 0deg 0.5deg,
      rgba(0, 0, 0, 0) 0.5deg 15deg),
    radial-gradient(ellipse at 50% 58%,
      rgba(79, 216, 255, 0) 46%,
      rgba(79, 216, 255, 0.10) 76%,
      rgba(79, 216, 255, 0.30) 100%);
  -webkit-mask-image: radial-gradient(circle at 50% 58%,
    rgba(0, 0, 0, 0) 30%, rgba(0, 0, 0, 0.9) 78%, rgba(0, 0, 0, 1) 100%);
  mask-image: radial-gradient(circle at 50% 58%,
    rgba(0, 0, 0, 0) 30%, rgba(0, 0, 0, 0.9) 78%, rgba(0, 0, 0, 1) 100%);
}
@keyframes shield-breathe {
  0%, 100% { opacity: 0.7; }
  50% { opacity: 1; }
}
/* The last second flickers, so the dome failing is never a surprise. */
.shield-overlay.expiring { animation: shield-fail 0.22s steps(2) infinite; }
@keyframes shield-fail {
  0% { opacity: 0.95; }
  100% { opacity: 0.3; }
}

/* Night Ops goggles. The scene grade and amplification live in the owned post
   pipeline; this is only the headset's physical edge and instrumentation. The
   middle two thirds stay completely clear, because a premium vision tool must
   improve target acquisition rather than decorate over it. */
.night-vision-ui {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  color: #a8ffd0;
  text-shadow: 0 0 10px rgba(88, 255, 168, 0.5);
}
.night-vision-ui::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(90deg, rgba(2, 14, 10, 0.42), transparent 8%, transparent 92%, rgba(2, 14, 10, 0.42)),
    linear-gradient(0deg, rgba(2, 12, 8, 0.30), transparent 10%, transparent 88%, rgba(2, 12, 8, 0.22));
  box-shadow: inset 0 0 85px rgba(21, 255, 139, 0.07);
  opacity: 0.75;
}

/* The four optic housings seat against the top edge when the goggles come
   down, then recede to a quiet silhouette. Four shallow pods echo the actual
   panoramic headset sold in the booth without turning the view into binoculars. */
.nv-hardware {
  position: absolute;
  top: -7px;
  left: 50%;
  width: min(370px, 52vw);
  height: 42px;
  transform: translateX(-50%);
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 7px;
  padding: 0 18px 8px;
  opacity: 0.28;
  filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.55));
}
.nv-hardware::before {
  content: '';
  position: absolute;
  left: 28%; right: 28%; top: 0; height: 15px;
  background: linear-gradient(180deg, #101a18, #040807);
  border: 1px solid rgba(142, 255, 194, 0.15);
  border-top: 0;
  clip-path: polygon(8% 0, 92% 0, 100% 66%, 82% 100%, 18% 100%, 0 66%);
}
.nv-hardware i {
  align-self: end;
  height: 20px;
  border-radius: 4px 4px 12px 12px;
  border: 1px solid rgba(142, 255, 194, 0.18);
  border-top-color: rgba(190, 255, 218, 0.08);
  background: linear-gradient(180deg, rgba(16, 27, 24, 0.98), rgba(2, 7, 5, 0.96));
  box-shadow: inset 0 -3px 0 rgba(71, 255, 153, 0.06);
}
.night-vision-ui.engaging .nv-hardware {
  animation: nv-seat 620ms cubic-bezier(.16, 1, .3, 1);
}
@keyframes nv-seat {
  0% { transform: translate(-50%, -58px) scale(1.08); opacity: 0.95; }
  42% { transform: translate(-50%, 3px) scale(1); opacity: 0.72; }
  100% { transform: translate(-50%, 0) scale(1); opacity: 0.28; }
}

.nv-telemetry {
  position: absolute;
  left: 24px;
  right: 24px;
  top: 22px;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  opacity: 0.78;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 9px;
  letter-spacing: 0.17em;
  text-transform: uppercase;
}
.nv-mode { display: flex; flex-direction: column; gap: 3px; }
.nv-mode b { color: #d8ffe9; font-size: 10px; letter-spacing: 0.22em; }
.nv-mode span { opacity: 0.62; font-size: 7.5px; }
.nv-power { display: flex; align-items: center; gap: 8px; font-variant-numeric: tabular-nums; }
.nv-battery {
  position: relative;
  width: 86px;
  height: 7px;
  border: 1px solid rgba(154, 255, 201, 0.4);
  background: rgba(0, 15, 8, 0.5);
  overflow: hidden;
  transform: skewX(-8deg);
}
.nv-battery::after {
  content: '';
  position: absolute;
  inset: 1px;
  background: repeating-linear-gradient(90deg, transparent 0 8px, rgba(0, 8, 4, 0.75) 8px 10px);
}
.nv-battery i {
  display: block;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, #42d889, #c9ffe0);
  box-shadow: 0 0 10px rgba(74, 255, 157, 0.75);
  transition: width 0.12s linear;
}
.night-vision-ui.low { color: #ffd679; text-shadow: 0 0 10px rgba(255, 182, 72, 0.5); }
.night-vision-ui.low .nv-battery { border-color: rgba(255, 194, 89, 0.6); }
.night-vision-ui.low .nv-battery i { background: #ffc25a; box-shadow: 0 0 10px rgba(255, 182, 72, 0.7); }
.night-vision-ui.low .nv-power { animation: nv-low 0.8s steps(2) infinite; }
@keyframes nv-low { 50% { opacity: 0.42; } }
body.comfort .night-vision-ui.low .nv-power { animation: none; }

body.touch-mode .nv-telemetry {
  left: 50%;
  right: auto;
  top: calc(10px + env(safe-area-inset-top, 0px));
  width: min(320px, 52vw);
  transform: translateX(-50%);
}
body.touch-mode .nv-mode span { display: none; }
body.touch-mode .nv-hardware { width: min(280px, 48vw); }
@media (prefers-reduced-motion: reduce) {
  .night-vision-ui.engaging .nv-hardware { animation: none; }
  .night-vision-ui.low .nv-power { animation: none; }
}

/* Charge meter sits directly under the crosshair. You have to be able to judge
   it without taking your eyes off the target. */
.charge-meter {
  position: absolute;
  left: 50%; top: 50%;
  width: 90px; height: 3px;
  margin: 26px 0 0 -45px;
  background: rgba(255, 255, 255, 0.14);
  border-radius: 2px;
  overflow: hidden;
  z-index: 5;
  pointer-events: none;
}
.charge-fill {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, #2f7f96, #9fe8ff);
  transition: width 0.05s linear;
}
.charge-meter.full .charge-fill {
  background: #cff4ff;
  box-shadow: 0 0 10px #9fe8ff;
}
.charge-meter.full { animation: pulse-mark 0.5s ease-in-out infinite; }

.shield-state {
  position: absolute;
  bottom: 62px; left: 50%;
  transform: translateX(-50%);
  z-index: 4;
  font-size: 9.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--cool);
  border: 1px solid rgba(79, 216, 255, 0.3);
  background: rgba(6, 16, 26, 0.5);
  padding: 4px 10px;
  border-radius: 2px;
  pointer-events: none;
}
.shield-state.cooling { color: var(--muted); border-color: var(--line); opacity: 0.6; }

.hud-corner { position: absolute; z-index: 4; pointer-events: none; }
.hud-bl { left: 30px; bottom: 28px; }
.hud-br { right: 30px; bottom: 28px; text-align: right; }
.hud-tr { right: 30px; top: 26px; text-align: right; }

.hearts { display: flex; gap: 5px; margin-bottom: 14px; }
.heart {
  width: 15px; height: 15px;
  background: #ff4d5e;
  clip-path: polygon(50% 100%, 0 38%, 0 16%, 22% 0, 50% 16%, 78% 0, 100% 16%, 100% 38%);
  box-shadow: 0 0 9px rgba(255, 77, 94, 0.55);
}
/* Lost: dark, flat and still. Nothing about it suggests it is coming back,
   because it is not — only a heart pickup or a Forge Reinforce restores it. */
.heart.empty {
  background: rgba(255, 255, 255, 0.11);
  box-shadow: none;
}

/* Damaged: drained to its real level rather than a flat 50% fade, so you can
   watch it climb instead of guessing. The gradient fills from the bottom, which
   is the direction health returns. */
.heart.partial {
  background: linear-gradient(
    to top,
    #ff4d5e 0%,
    #ff4d5e var(--fill, 50%),
    rgba(255, 255, 255, 0.11) var(--fill, 50%),
    rgba(255, 255, 255, 0.11) 100%
  );
  box-shadow: 0 0 6px rgba(255, 77, 94, 0.3);
}

/* Actively refilling: it breathes. Motion is the whole message — a still
   partial heart and a still empty heart look alike, and that is exactly why
   nobody could tell that health comes back on its own. */
.heart.partial.healing {
  animation: heartHeal 1.6s ease-in-out infinite;
}
@keyframes heartHeal {
  0%, 100% { box-shadow: 0 0 6px rgba(255, 77, 94, 0.3); filter: brightness(1); }
  50%      { box-shadow: 0 0 14px rgba(255, 120, 135, 0.75); filter: brightness(1.35); }
}

.stamina-bar {
  width: 132px;
  height: 3px;
  background: rgba(255, 255, 255, 0.13);
  border-radius: 2px;
  overflow: hidden;
  margin-bottom: 13px;
}
.stamina-fill {
  height: 100%;
  width: 100%;
  transform: scaleX(1);
  transform-origin: left center;
  background: var(--cool);
  border-radius: 2px;
  transition: transform 0.08s linear, background 0.25s ease;
}
/* Exhausted has to be unmistakable — it is why you just stopped sprinting. */
.stamina-fill.exhausted {
  background: #ff5f4a;
  animation: pulse-mark 0.9s ease-in-out infinite;
}

.stat-line { display: flex; align-items: baseline; gap: 9px; margin-top: 5px; }
.stat-label {
  font-size: 9.5px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--muted);
}
.stat-value {
  font-size: 21px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}
.stat-value.muted { font-size: 14px; color: var(--ink); opacity: 0.75; }

/* Carried value escalates with wealth: brighter, warmer, and finally lit. */
.stat-value.carried-t0 { color: var(--ink); }
.stat-value.carried-t1 { color: #ffd591; }
.stat-value.carried-t2 { color: var(--accent); text-shadow: 0 0 8px rgba(245, 179, 66, 0.4); }
.stat-value.carried-t3 {
  color: #ffc766;
  font-size: 24px;
  text-shadow: 0 0 14px rgba(245, 179, 66, 0.65);
}
.stat-value.carried-t4 {
  color: #ffdca0;
  font-size: 27px;
  text-shadow: 0 0 22px rgba(255, 200, 110, 0.9);
  animation: pulse-mark 1.5s ease-in-out infinite;
}
.stat-value.bump { animation: gain-bump 0.28s ease; }
@keyframes gain-bump {
  0% { transform: scale(1); }
  40% { transform: scale(1.22); }
  100% { transform: scale(1); }
}

.gain-layer { position: relative; width: 0; height: 0; }
.gold-gain {
  position: absolute;
  left: 6px;
  bottom: -6px;
  white-space: nowrap;
  font-size: 15px;
  font-weight: 700;
  color: var(--accent);
  text-shadow: 0 0 10px rgba(245, 179, 66, 0.6);
  animation: gain-float 0.9s ease-out forwards;
  pointer-events: none;
}
.gold-gain.heal { color: #ff6b72; text-shadow: 0 0 10px rgba(255, 107, 114, 0.6); }
@keyframes gain-float {
  0% { opacity: 0; transform: translateY(0); }
  20% { opacity: 1; }
  100% { opacity: 0; transform: translateY(-30px); }
}

.seg { display: flex; gap: 4px; }
.seg button {
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 5px 10px;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.seg button:hover { color: var(--ink); }
.seg button.active { border-color: var(--accent); color: var(--accent); }

.glow-pip {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--accent);
  opacity: 0;
  transition: opacity 0.3s ease, box-shadow 0.3s ease;
}
.glow-pip.t1 { opacity: 0.5; box-shadow: 0 0 6px var(--accent); }
.glow-pip.t2 { opacity: 0.7; box-shadow: 0 0 10px var(--accent); }
.glow-pip.t3 { opacity: 0.85; box-shadow: 0 0 16px var(--accent); }
.glow-pip.t4 { opacity: 1; box-shadow: 0 0 24px var(--accent); animation: pulse-mark 1.4s ease-in-out infinite; }

.weapon-name {
  font-size: 12px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink);
}
.weapon-ammo {
  font-size: 30px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.weapon-ammo.empty { color: #ff5f4a; }
/* The battery: a rectangle of slanted cells, always on screen. The fill layer
   is clipped to the charge; the cells come from a repeating diagonal stripe so
   the bar reads as segments without a segment per element. Cyan, like every
   piece of your own kit; dimmed while the gun in hand does not draw from it;
   red only when empty. */
.weapon-ammo.energy { display: none; }
.energy-bar {
  position: relative;
  width: 260px;
  height: 14px;
  margin: 4px 0 6px auto;
  border: 1px solid rgba(79, 216, 255, 0.45);
  border-radius: 2px;
  background: rgba(6, 16, 26, 0.6);
  overflow: hidden;
  transition: opacity 0.2s ease;
}
.energy-cells {
  position: absolute;
  inset: 2px;
  background: repeating-linear-gradient(115deg,
    transparent 0 3px, currentColor 3px 12px);
}
.energy-cells.bg { color: rgba(79, 216, 255, 0.14); }
.energy-cells.fill {
  color: var(--cool);
  clip-path: inset(0 0% 0 0);
  transition: clip-path 0.12s linear;
}
.energy-bar.idle { opacity: 0.62; }
.energy-bar.low .energy-cells.fill { color: #ffb648; }
.energy-bar.empty { border-color: rgba(255, 95, 74, 0.7); animation: energy-empty 0.9s ease-in-out infinite; }
@keyframes energy-empty { 50% { background: rgba(120, 30, 20, 0.5); } }
.gold-gain.cell { color: var(--cool); }
/* Thumb targets: the pill look stays, the target does not shrink below 44px. */
body.touch-mode .touch-weapon { min-height: 44px; }
body.touch-mode .touch-weapon.active { border-color: var(--cool); background: rgba(79, 216, 255, 0.14); color: #cff4ff; }

/* The store card: a small gold card that empties when spent and fills back
   over the cooldown. Full = the door opens for you. */
/* Inline on the Extracted row, beating the plain .stat-line rule hud.js
   re-applies on every update. */
.stat-line .store-card-line { display: inline-flex; align-items: center; align-self: center; gap: 6px; margin-top: 0; margin-left: 6px; }
.store-card-line { align-items: center; }
.store-card {
  position: relative;
  display: inline-block;
  width: 24px;
  height: 15px;
  border: 1px solid var(--accent);
  border-radius: 2px;
  overflow: hidden;
  background: rgba(255, 182, 72, 0.08);
  vertical-align: middle;
}
.store-card::after {
  /* the chip on the card */
  content: "";
  position: absolute;
  left: 4px; top: 4px;
  width: 6px; height: 5px;
  border: 1px solid rgba(20, 14, 6, 0.55);
  border-radius: 1px;
}
.store-card-fill {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 100%;
  background: var(--accent);
  transition: width 0.4s linear;
}
.store-card-state {
  font-size: 9.5px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.store-card-line.charging .store-card-state { color: var(--muted); }
.store-card-line.charging .store-card { opacity: 0.85; }
.store-card-line.inuse .store-card-state { color: var(--muted); }
/* The rack: a fixed 3x2 grid of the six weapon slots, numbers 1-3 over 4-6.
   A grid's width never depends on which names are held, so it cannot creep
   left into the module chips the way the old elastic flex row did — and a
   slot's position becomes muscle memory. Shares the 260px measure with the
   energy bar above it so the whole bottom-right block reads as one column. */
.weapon-slots {
  display: grid;
  grid-template-columns: repeat(3, 84px);
  gap: 4px;
  width: 260px;
  margin: 8px 0 0 auto;
}
.weapon-slot {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 9px;
  letter-spacing: 0.08em;
  padding: 3px 6px;
  border: 1px solid rgba(79, 216, 255, 0.3);
  border-radius: 2px;
  background: rgba(6, 16, 26, 0.55);
  color: var(--ink);
  overflow: hidden;
  white-space: nowrap;
}
/* Cyan, not amber: a gun is friendly utility, and this sits right under the
   cyan battery and thirty pixels from the amber money. Fill only on the
   active cell — the fill is the "you are here" signal. */
.weapon-slot.active {
  border-color: var(--cool);
  background: rgba(79, 216, 255, 0.14);
  color: #cff4ff;
}
.weapon-slot.active .slot-key { background: var(--cool); color: #04121a; border-color: var(--cool); }
/* Dry = held, just unpayable right now. Same geometry, same border, no
   strikethrough, no ghosting — a gun you own must never read as broken. The
   name dims and the cell says the price, which is the answer to "why not". */
.weapon-slot.dry { color: var(--muted); }
.weapon-slot.dry::after {
  content: attr(data-cost);
  margin-left: auto;
  font-size: 8px;
  color: var(--cool);
  opacity: 0.8;
}
/* Unheld: borderless, so it stops being one more box in the soup. The name
   stays, muted — a rack that teaches 5 and 6 exist without shouting. */
.weapon-slot.locked {
  border-color: transparent;
  background: none;
  color: var(--muted);
  opacity: 0.55;
}
.weapon-slot.locked .slot-key { border-color: var(--line); background: transparent; color: var(--muted); }
.slot-name { overflow: hidden; text-overflow: ellipsis; }

.portal-compass { display: flex; align-items: center; gap: 8px; justify-content: flex-end; }
.compass-arrow {
  font-size: 19px;
  color: var(--cool);
  display: inline-block;
  transition: transform 0.08s linear;
}
.compass-dist {
  font-size: 19px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--cool);
}
.compass-label {
  font-size: 9.5px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--muted);
}
/* The minimap itself is drawn by WebGL into a scissored corner of the main
   canvas; this is only the frame sitting on top of it. */
/* Position and size come from game.js, which draws the map itself into a WebGL
   viewport from the same numbers. The fallbacks are the desktop values, so this
   still renders correctly if the properties are ever not set. */
.minimap-frame {
  position: absolute;
  top: var(--mini-y, 26px); left: var(--mini-x, 30px);
  width: var(--mini-size, 190px); height: var(--mini-size, 190px);
  border: 1px solid var(--line);
  border-radius: 3px;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 24px rgba(0, 0, 0, 0.55);
  z-index: 4;
  pointer-events: none;
}
.minimap-tag {
  position: absolute;
  bottom: 5px; right: 7px;
  font-size: 8.5px;
  letter-spacing: 0.2em;
  color: var(--muted);
  opacity: 0.7;
}

.killfeed {
  position: absolute;
  /* Below the menu button, which sits at the same corner. Both were at
     top:26px left:30px, so the first kill printed straight through it. */
  top: 68px; left: 30px;
  z-index: 4;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  gap: 5px;
  transition: top 0.2s ease;
}
.minimap-on .killfeed { top: 232px; }
.kill-row {
  font-size: 11.5px;
  letter-spacing: 0.05em;
  color: var(--ink);
  background: rgba(6, 10, 18, 0.6);
  padding: 5px 10px;
  border-left: 2px solid var(--line);
  border-radius: 2px;
  animation: feed-in 0.25s ease;
}
.kill-row .killer { color: var(--accent); font-weight: 600; }
.kill-row .victim { color: var(--muted); }
.kill-row.event { border-left-color: var(--cool); color: var(--cool); }
/* The feed is now the only place the game speaks, so it has to be able to
   shout. A line that means "this can kill you in the next few seconds" — a trap
   under your feet, a Sunburst in the air, the socket gone — is red, per the
   colour language, and never cyan: cyan is the friendly channel and a live trap
   is not friendly. Brighter ink and a stronger rule so it separates from the
   ordinary rows at a glance without needing to be read first. */
.kill-row.urgent {
  border-left-color: #ff5f4a;
  color: #ff8b78;
  background: rgba(30, 8, 8, 0.66);
}
@keyframes feed-in { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: none; } }

.tag-warning {
  position: absolute;
  top: 16%; left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  font-size: 11px;
  letter-spacing: 0.26em;
  color: #ff5f4a;
  border: 1px solid rgba(255, 95, 74, 0.4);
  background: rgba(30, 6, 6, 0.55);
  padding: 7px 15px;
  border-radius: 2px;
  pointer-events: none;
  animation: pulse-mark 1.3s ease-in-out infinite;
}
/* Out in the field this is context, not an alarm — no pulse, no red panic. */
.tag-warning.subtle {
  color: var(--muted);
  border-color: var(--line);
  background: rgba(6, 10, 18, 0.5);
  animation: none;
  font-size: 10px;
}

/* Calm, not alarming — the bubble is a good place to be. It just explains why
   your weapons are doing nothing. */
.safe-zone {
  position: absolute;
  top: 15%; left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  font-size: 10px;
  letter-spacing: 0.26em;
  color: var(--cool);
  border: 1px solid rgba(79, 216, 255, 0.3);
  background: rgba(6, 16, 26, 0.45);
  padding: 5px 13px;
  border-radius: 2px;
  pointer-events: none;
}

.boundary-warning {
  position: absolute;
  top: 22%; left: 50%;
  transform: translateX(-50%);
  z-index: 5;
  font-size: 11px;
  letter-spacing: 0.24em;
  color: var(--cool);
  border: 1px solid rgba(79, 216, 255, 0.35);
  background: rgba(6, 16, 26, 0.5);
  padding: 6px 14px;
  border-radius: 2px;
  pointer-events: none;
}
.boundary-warning.critical {
  color: #ff5f4a;
  border-color: rgba(255, 95, 74, 0.45);
  background: rgba(30, 6, 6, 0.55);
  animation: pulse-mark 1s ease-in-out infinite;
}

/* Field Forge. Sits low-centre so it never fights the crosshair, and only
   appears while standing on a terminal. */
.forge-panel {
  position: absolute;
  bottom: 20%; left: 50%;
  transform: translateX(-50%);
  width: 340px;
  z-index: 5;
  padding: 16px 18px 14px;
  background: rgba(6, 14, 24, 0.82);
  border: 1px solid rgba(79, 216, 255, 0.3);
  border-radius: 3px;
  pointer-events: none;
  /* see .forge-strip: inside the store this stops being a bordered box */
  /* Its own fade rather than the killfeed's feed-in. That animation ends on
     `transform: none`, and an animated transform outranks the declared one —
     so for its first 180ms this panel lost the `translateX(-50%)` that centres
     it and sat half its own width to the right before snapping back. At 340px
     that was a 170px jump nobody had called out; widening the panel for phones
     made it 280px and pushed the right-hand column off the screen entirely.
     Anything centred with a transform cannot borrow an animation that sets
     one. */
  animation: forge-in 0.18s ease;
}
@keyframes forge-in { from { opacity: 0; } to { opacity: 1; } }
.forge-head {
  font-size: 10px;
  letter-spacing: 0.3em;
  color: var(--cool);
  margin-bottom: 4px;
}

.forge-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 9px;
  border: 1px solid var(--line);
  border-radius: 2px;
}
.forge-key {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 2px;
  padding: 2px 6px;
  min-width: 18px;
  text-align: center;
}
.forge-name { flex: 1; font-size: 12px; color: var(--ink); }
.forge-desc { font-size: 9.5px; color: var(--muted); letter-spacing: 0.05em; }
.forge-cost {
  font-size: 12px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}
/* Unaffordable and already-owned states have to be readable at a glance.
   Not red for "cannot afford": red is danger, and the store is the one calm
   place in the game. An unaffordable price says what to extract instead. */
.forge-item.poor { opacity: 0.55; }
.forge-item.poor .forge-cost { color: var(--muted); font-size: 10px; letter-spacing: 0.06em; }
.forge-item.owned { border-color: rgba(79, 216, 255, 0.45); opacity: 0.6; }
.forge-item.owned .forge-cost { color: var(--cool); font-size: 10px; }
/* The last five seconds of a visit, told from inside. */
.forge-panel.expiring .forge-head { color: var(--accent); animation: store-expiring 0.5s steps(2, end) infinite; }
@keyframes store-expiring { 50% { opacity: 0.35; } }

/* At a store door. Sits where the sky door prompt sits — you cannot be at
   both — with a bar for the wait, since nothing else says the door is working
   on you. Amber, because the store is where money goes. */
.store-prompt {
  position: absolute;
  left: 50%; bottom: 28%;
  transform: translateX(-50%);
  z-index: 6;
  min-width: 220px;
  padding: 6px 14px 8px;
  font-size: 10.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  text-align: center;
  color: var(--accent);
  border: 1px solid rgba(255, 182, 72, 0.45);
  background: rgba(20, 14, 6, 0.62);
  border-radius: 2px;
  pointer-events: none;
}
.store-prompt.unavailable { color: var(--muted); border-color: var(--line); }
.store-bar { margin-top: 6px; height: 3px; background: rgba(255, 255, 255, 0.08); border-radius: 2px; overflow: hidden; }
.store-bar-inner { height: 100%; width: 0; background: var(--accent); transition: width 0.1s linear; }
.store-prompt.unavailable .store-bar { display: none; }
/* Inside the store the panel is the business of the screen: solid enough to
   read at leisure, amber-edged like the doors, and lifted so a phone can
   tap the rows. */
body.in-store .forge-panel {
  background: rgba(10, 12, 20, 0.92);
  border-color: rgba(255, 182, 72, 0.4);
  bottom: 12%;
  /* The warp flash is the arrival; no second fade on top of it. */
  animation: none;
}
body.in-store .forge-head { color: var(--accent); }
/* On the way down there is nothing to aim with. */
body.dropping .crosshair { display: none; }
/* Inside the store there is nothing to aim, and nothing twenty kilometres
   away is yours to worry about: the strike and crate trackers stay outside. */
body.in-store .crosshair, body.in-store .portal-compass, body.in-store .strike-mark { display: none; }

/* The store is a room of booths now, not a list: the panel shrinks to a strip
   at the top — how long you have, what you can spend, how many of the visit's
   three are left — and the list stays hidden. Buying happens at the booth. */
body.in-store .forge-panel {
  top: 14px; bottom: auto;
  width: auto; min-width: 0;
  padding: 9px 20px 7px;
  text-align: center;
  /* No border and no hard edge. A bordered box of sentences was the thing
     being read past; this is a soft-edged cluster of things you can count. */
  border: 0;
  border-radius: 18px;
  background: radial-gradient(ellipse at 50% 45%,
    rgba(6, 12, 22, 0.9) 0%, rgba(6, 12, 22, 0.78) 55%, rgba(6, 12, 22, 0) 100%);
}
.forge-strip {
  display: flex;
  align-items: center;
  gap: 14px;
  white-space: nowrap;
}
/* The visit's three purchases. Segments, not a sentence — amber while you
   still have them, hollow once they are gone, so the count is a shape. */
.forge-slots { display: inline-flex; gap: 5px; }
.forge-slots i {
  display: block;
  width: 26px; height: 6px;
  border-radius: 3px;
  background: var(--accent);
  box-shadow: 0 0 8px rgba(255, 182, 72, 0.55);
  transition: background 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
}
.forge-slots i.spent {
  background: transparent;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.22);
  opacity: 0.55;
}
.forge-bal { font-size: 11px; letter-spacing: 0.12em; color: var(--muted); }
.forge-bal b {
  color: var(--accent);
  font-size: 13px;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
  margin-right: 5px;
}
.forge-bal em { font-style: normal; text-transform: uppercase; font-size: 9px; }
body.in-store .forge-head {
  margin-bottom: 0;
  font-size: 12px;
  letter-spacing: 0.08em;
  font-variant-numeric: tabular-nums;
  color: var(--cool);
}
/* The clock as a bar as well as a number: it is the thing actually running
   out, and a bar shrinking is read without being looked at. */
.forge-time {
  display: block;
  margin: 7px auto 0;
  width: 100%; height: 2px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.1);
  overflow: hidden;
}
.forge-time i {
  display: block; height: 100%; width: 100%;
  background: var(--accent);
  transition: width 0.2s linear;
}
/* Running out blinks the bar the same way it already blinks the clock —
   the store's existing language for "your visit is ending", not a new one. */
.forge-panel.expiring .forge-time i { animation: store-expiring 0.5s steps(2, end) infinite; }

/* Out of purchases. The word is short and the pulse is what carries it —
   this is the state that was being missed entirely when it was the "0" in
   "0 of 3 left". */
.forge-tag {
  display: none;
  font-size: 10px;
  letter-spacing: 0.24em;
  color: var(--accent);
}
body.in-store .forge-panel.spent .forge-tag { display: inline; }
body.in-store .forge-panel.spent .forge-slots i { box-shadow: inset 0 0 0 1px rgba(255, 182, 72, 0.5); }
@keyframes forge-spent-pulse {
  0%   { transform: scale(1);    filter: none; }
  18%  { transform: scale(1.09); filter: brightness(1.7); }
  40%  { transform: scale(1);    filter: none; }
  58%  { transform: scale(1.06); filter: brightness(1.5); }
  100% { transform: scale(1);    filter: none; }
}
body.in-store .forge-panel.just-spent .forge-strip {
  animation: forge-spent-pulse 1.05s ease-out 1;
}
@media (prefers-reduced-motion: reduce) {
  body.in-store .forge-panel.just-spent .forge-strip { animation: none; }
}

/* The booth prompt is gone — see hud.js. Its name and price are on the sign
   over the booth, its state is the colour of the pad, and its progress bar is
   the disc growing under your feet. It only ever repeated the room back at you,
   from a box parked over the crosshair. */

/* On a phone the top corners belong to RANK and MENU, so the store strip
   shrinks and RANK gets out of its way — the scoreboard means nothing in a
   room without a fight anyway. */
body.touch-mode.in-store .forge-panel {
  top: 8px;
  min-width: 0;
  max-width: 58vw;
  padding: 6px 12px 7px;
}
body.touch-mode.in-store .forge-strip { gap: 9px; }
body.touch-mode.in-store .forge-slots i { width: 18px; height: 5px; }
body.touch-mode.in-store .forge-bal { font-size: 9px; letter-spacing: 0.06em; }
body.touch-mode.in-store .forge-bal b { font-size: 11px; }
body.touch-mode.in-store .forge-bal em { display: none; }
body.touch-mode.in-store .forge-head { font-size: 10px; }
body.touch-mode.in-store .forge-tag { font-size: 8.5px; letter-spacing: 0.16em; }
body.touch-mode.in-store #touch-board { display: none; }

.forge-channel { margin-top: 12px; }
.forge-channel-label {
  font-size: 9.5px;
  letter-spacing: 0.28em;
  color: var(--cool);
  margin-bottom: 6px;
}
.forge-bar {
  height: 4px;
  background: rgba(255, 255, 255, 0.12);
  border-radius: 2px;
  overflow: hidden;
}
.forge-bar-inner {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, var(--cool), var(--accent));
  transition: width 0.1s linear;
}
.forge-channel-warn {
  margin-top: 5px;
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #ff9d6b;
}

/* Active kit, bottom-centre above the hearts. Present but quiet. */
.module-strip {
  position: absolute;
  bottom: 28px; left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 7px;
  z-index: 4;
  pointer-events: none;
  /* Centred on the screen but kept clear of both bottom corners. The strip runs
     into the weapon rack on the right and the hearts on the left as soon as it
     gets wide, and it grows every time an item is added to the game — so the
     width is bounded and it wraps upward instead of sliding under the rack. */
  max-width: calc(100vw - 700px);
  min-width: 0;
}
@media (max-width: 1200px) {
  /* Narrow windows have no room between the corners at all. Lift the strip
     above both corner blocks instead of widening into them — widening is
     exactly what produced the chips-through-the-rack soup. */
  .module-strip { bottom: 160px; max-width: calc(100vw - 120px); }
}
.module-chip {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 6px;
  /* Fixed floor so a count ticking 9 -> 10 does not re-centre the whole row. */
  min-width: 88px;
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--cool);
  border: 1px solid rgba(79, 216, 255, 0.35);
  background: rgba(6, 16, 26, 0.55);
  padding: 4px 9px;
  border-radius: 2px;
  animation: feed-in 0.25s ease;
}
/* Freight-door prompt. Sits low-centre, out of the crosshair. */
.use-prompt {
  position: absolute;
  left: 50%; bottom: 28%;
  transform: translateX(-50%);
  z-index: 6;
  padding: 6px 14px;
  font-size: 10.5px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--cool);
  border: 1px solid rgba(79, 216, 255, 0.4);
  background: rgba(6, 16, 26, 0.62);
  border-radius: 2px;
  pointer-events: none;
}
.use-prompt b {
  display: inline-block;
  min-width: 15px;
  margin-right: 8px;
  padding: 0 4px;
  border: 1px solid rgba(127, 228, 255, 0.55);
  border-radius: 2px;
  background: rgba(127, 228, 255, 0.12);
  color: #cff4ff;
  letter-spacing: 0;
}
/* Greyed when you have no call. Still shown, because the door existing and
   being unusable is the thing that sends you to the Forge. */
.use-prompt.unavailable { color: var(--muted); border-color: var(--line); }
.use-prompt.unavailable b { border-color: var(--line); background: transparent; color: var(--muted); }

/* The key, boxed and bright at the front of the chip. A player who already
   knows the binding ignores the strip entirely; one reading it is looking for
   exactly this, so it should be the first and loudest thing on the chip. */
.chip-key,
.slot-key {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 15px;
  height: 15px;
  padding: 0 3px;
  border: 1px solid rgba(127, 228, 255, 0.55);
  border-radius: 2px;
  background: rgba(127, 228, 255, 0.12);
  color: #cff4ff;
  font-weight: 700;
  letter-spacing: 0;
}
.chip-label { color: var(--cool); white-space: nowrap; }
/* VAULT-STEP was breaking across two lines and dragging the strip's height
   with it, which shoved every other chip around. */
.module-chip { white-space: nowrap; }
.chip-extra { opacity: 0.55; }
/* During a cooldown the seconds ARE the answer — full strength, not a whisper. */
.chip-extra.cooling { opacity: 1; color: var(--cool); }
/* No key to show, so say why rather than leaving a gap the eye searches. */
.chip-passive { font-size: 8.5px; opacity: 0.45; letter-spacing: 0.18em; }
.module-chip.spent .chip-key {
  border-color: rgba(140, 170, 210, 0.25);
  background: transparent;
  color: var(--muted);
}

.module-chip b {
  /* Bright cyan, not amber: these were bought with money, they are not money. */
  color: #cff4ff;
  font-variant-numeric: tabular-nums;
}
.module-chip.night-ops {
  min-width: 176px;
  transition: border-color 0.18s ease, background 0.18s ease;
}
.module-chip.night-ops.active {
  color: #a8ffd0;
  border-color: rgba(94, 255, 165, 0.66);
  background: linear-gradient(90deg, rgba(13, 67, 43, 0.76), rgba(4, 20, 15, 0.66));
  box-shadow: inset 0 0 16px rgba(64, 255, 148, 0.08), 0 0 10px rgba(64, 255, 148, 0.08);
}
.module-chip.night-ops.active .chip-label,
.module-chip.night-ops.active b,
.module-chip.night-ops.active .chip-extra { color: #caffdf; }
.module-chip.night-ops.low { border-color: rgba(255, 182, 72, 0.5); }
.night-ops-cell {
  position: relative;
  width: 34px;
  height: 5px;
  flex: 0 0 auto;
  overflow: hidden;
  border: 1px solid rgba(111, 232, 255, 0.32);
  transform: skewX(-8deg);
}
.night-ops-cell::after {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(90deg, transparent 0 6px, rgba(3, 12, 15, 0.8) 6px 7px);
}
.night-ops-cell i {
  display: block;
  height: 100%;
  background: linear-gradient(90deg, #39c985, #9fffc8);
}
.module-chip.night-ops.low .night-ops-cell i { background: #ffbd52; }
/* Empty is a fact, not a disability: full border and label, the count reads
   as a muted dash (hud.js renders it), only the key cap hollows out. */

.extract-panel {
  position: absolute;
  bottom: 16%; left: 50%;
  transform: translateX(-50%);
  width: 320px;
  text-align: center;
  z-index: 5;
  pointer-events: none;
}
.extract-label {
  font-size: 11px;
  letter-spacing: 0.32em;
  color: var(--cool);
  margin-bottom: 9px;
}
.extract-bar {
  height: 5px;
  background: rgba(255, 255, 255, 0.12);
  border-radius: 3px;
  overflow: hidden;
}
.extract-bar-inner {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, var(--cool), var(--accent));
  transition: width 0.12s linear;
}
.extract-sub {
  margin-top: 8px;
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
}

.overlay {
  position: absolute;
  inset: 0;
  /* Must sit above .click-to-play, or that layer swallows clicks meant for
     these buttons and silently re-locks the pointer. */
  z-index: 14;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  background: rgba(4, 6, 12, 0.82);
  backdrop-filter: blur(3px);
  /* Transient overlays (death) must not eat clicks; buttons re-enable this. */
  pointer-events: none;
}
.overlay-title {
  font-size: 46px;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: #ff5f4a;
}
.overlay-title.success { color: var(--accent); }
.overlay-sub { font-size: 12.5px; letter-spacing: 0.12em; color: var(--muted); }
.overlay-hint {
  font-size: 10.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  opacity: 0.6;
  margin-top: -4px;
}
.overlay-timer {
  font-size: 15px;
  letter-spacing: 0.2em;
  color: var(--ink);
  margin-top: 8px;
  font-variant-numeric: tabular-nums;
}
.overlay .enter-btn { width: 220px; margin-top: 18px; pointer-events: auto; }
.overlay .ghost-btn { margin-top: 4px; pointer-events: auto; }

/* TEMPORARY DEV TUNING PANEL — REMOVE BEFORE LAUNCH */
.admin-panel {
  position: absolute;
  top: 20px; right: 20px;
  width: 330px;
  max-height: calc(100vh - 40px);
  overflow-y: auto;
  z-index: 20;
  padding: 18px 20px 20px;
  background: rgba(8, 11, 20, 0.94);
  border: 1px solid var(--accent-dim);
  border-radius: 4px;
  pointer-events: auto;
}
.admin-title {
  font-size: 9.5px;
  letter-spacing: 0.28em;
  color: var(--accent);
  margin-bottom: 16px;
}
.admin-note { font-size: 11px; color: var(--muted); }
.admin-panel .setting-name { font-size: 11px; }
.admin-panel .setting-row { margin-bottom: 8px; gap: 8px; }
.admin-panel input[type="range"] { width: 110px; }
.admin-val { width: 42px; }
.admin-panel .ghost-btn { width: 100%; margin-top: 6px; }

/* Second panel: world-level switches, parked beside the tuning panel. */
.world-panel { right: 370px; width: 280px; }
.world-toggle {
  display: flex; align-items: center; justify-content: space-between;
  width: 100%; margin-bottom: 8px; padding: 7px 10px;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--accent-dim);
  border-radius: 3px;
  color: var(--ink);
  font-family: inherit; font-size: 11px; letter-spacing: 0.06em;
  cursor: pointer;
}
.world-toggle:hover { background: rgba(255, 255, 255, 0.07); }
.world-toggle em { font-style: normal; font-size: 10px; letter-spacing: 0.16em; color: var(--muted); }
.world-toggle.on { border-color: var(--accent); }
.world-toggle.on em { color: var(--accent); }
.world-toggle.off em { color: #ff7a6b; }

/* Field guide. The colour chip on each row is the actual in-world colour, so
   the guide teaches recognition rather than vocabulary. */
.guide-overlay {
  position: absolute;
  inset: 0;
  z-index: 18;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(4, 6, 12, 0.86);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
}
.guide-panel {
  width: 520px;
  max-width: calc(100vw - 40px);
  max-height: calc(100vh - 60px);
  overflow-y: auto;
  padding: 28px 30px 24px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 4px;
}
.guide-title {
  font-size: 12px;
  letter-spacing: 0.34em;
  color: var(--accent);
  margin-bottom: 14px;
}
.guide-loop {
  font-size: 12px;
  line-height: 1.65;
  color: var(--ink);
  padding-bottom: 16px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--line);
}
.guide-group {
  font-size: 9.5px;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 18px 0 10px;
}
.guide-row {
  display: grid;
  /* A fixed gutter wide enough for the largest shape at 45 degrees, so rows
     align regardless of what is in them and nothing reaches past the column. */
  grid-template-columns: 44px 1fr;
  align-items: start;
  gap: 12px;
  margin-bottom: 13px;
}
.guide-chip {
  width: 15px; height: 15px;
  border-radius: 3px;
  margin: 4px auto 0;      /* centred in the gutter, whatever its size */
  box-shadow: 0 0 9px currentColor;
  background: currentColor;
}
.guide-chip.ring {
  background: transparent;
  border: 2px solid currentColor;
  box-shadow: 0 0 8px currentColor;
}
.guide-text { flex: 1; }
.guide-name {
  font-size: 12.5px;
  color: var(--ink);
  margin-bottom: 2px;
}
.guide-name b {
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  margin-left: 6px;
  font-size: 11px;
}
.guide-desc {
  font-size: 11px;
  line-height: 1.5;
  color: var(--muted);
}
.guide-panel .enter-btn { width: 100%; margin-top: 18px; }

.pause-menu {
  position: absolute;
  inset: 0;
  z-index: 16;
  display: flex;
  justify-content: center;
  /* Anchored near the top rather than centred. A centred panel changes its top
     edge every time its content changes height, which moves Resume under the
     cursor as you switch tabs. Anchored, Resume is at the same screen position
     on every open, at every window size, whatever tab you left it on — which is
     what lets it become muscle memory instead of something you look for. */
  align-items: flex-start;
  padding: clamp(14px, 5vh, 70px) 20px;
  background: rgba(4, 6, 12, 0.78);
  backdrop-filter: blur(5px);
  -webkit-backdrop-filter: blur(5px);
}
.pause-panel {
  /* One fixed-width column. The old 940px of three columns exceeded the window
     on the machines this actually broke on; 760px fits inside 1000px with room
     to spare, and above that it simply stops growing rather than spreading the
     settings into a shape you have to sweep your eyes across. */
  width: min(760px, calc(100vw - 40px));
  max-height: 100%;
  display: flex;
  flex-direction: column;
  padding: 26px 28px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 4px;
}
.pause-title {
  font-size: 12px;
  letter-spacing: 0.34em;
  color: var(--muted);
  margin-bottom: 14px;
}

.settings-section { margin-bottom: 22px; }
.settings-heading {
  font-size: 9.5px;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--accent);
  padding-bottom: 8px;
  margin-bottom: 12px;
  border-bottom: 1px solid var(--line);
}
.setting-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 12px;
}
.setting-name {
  flex: 1;
  font-size: 12px;
  color: var(--ink);
}
.setting-val {
  font-size: 11px;
  font-style: normal;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  width: 26px;
  text-align: right;
}

.setting-row input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 140px;
  height: 3px;
  background: rgba(255, 255, 255, 0.15);
  border-radius: 2px;
  outline: none;
}
.setting-row input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 13px; height: 13px;
  border-radius: 50%;
  background: var(--accent);
  cursor: pointer;
}
.setting-row input[type="range"]::-moz-range-thumb {
  width: 13px; height: 13px;
  border: none;
  border-radius: 50%;
  background: var(--accent);
  cursor: pointer;
}

.toggle {
  width: 40px; height: 21px;
  border-radius: 11px;
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.07);
  position: relative;
  cursor: pointer;
  padding: 0;
  transition: background 0.18s ease, border-color 0.18s ease;
}
.toggle i {
  position: absolute;
  top: 2px; left: 2px;
  width: 15px; height: 15px;
  border-radius: 50%;
  background: var(--muted);
  transition: transform 0.18s ease, background 0.18s ease;
}
.toggle[aria-checked="true"] {
  background: var(--accent-dim);
  border-color: var(--accent);
}
.toggle[aria-checked="true"] i {
  transform: translateX(19px);
  background: var(--accent);
}

.pause-panel .enter-btn { margin: 0; }
.pause-panel .ghost-btn { margin: 0; }

/* The header: the state you are in, and the two ways out of it. It is a fixed
   part of the panel, outside the scroll region, so no amount of content below
   can push it off screen. The previous version pinned the same row to the
   bottom with position:sticky, which worked only as long as the panel really
   was the scrolling element — a fragile guarantee to hang the only exit on. */
.pause-head {
  flex: 0 0 auto;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--line);
}
.pause-actions {
  display: flex;
  gap: 12px;
  align-items: center;
}
/* Resume is the answer to almost every opening of this menu, so it is the only
   filled button in the panel and it is oversized. Amber, like everything else
   here that is worth something. */
.pause-actions .enter-btn {
  flex: 0 0 auto;
  width: auto;
  padding: 14px 48px;
}
/* Leaving mid-match is the consequential choice, so it sits at the opposite end
   of the row from Resume — far enough that a fast, half-aimed click cannot land
   on the wrong one. It stays quiet at rest rather than being permanently red,
   because a red button beside Resume draws the eye to the thing you did not
   come for; the red arrives on hover and focus, at the moment it is a warning
   and not just decoration. */
.pause-actions .leave-btn { flex: 0 0 auto; margin-left: auto; }
.pause-actions .leave-btn:hover,
.pause-actions .leave-btn:focus-visible {
  color: #ff8a72;
  border-color: #ff5a3a;
  background: rgba(255, 90, 58, 0.08);
}

/* Three sections, one visible at a time. The reference and the settings are
   read once or twice each in a player's life; showing all of them at once cost
   the width and height that buried the buttons people open this menu for. */
.pause-tabs {
  flex: 0 0 auto;
  display: flex;
  gap: 2px;
  border-bottom: 1px solid var(--line);
}
.pause-tab {
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  padding: 13px 18px;
  font-family: inherit;
  font-size: 10px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--muted);
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.pause-tab:hover { color: var(--ink); }
.pause-tab[aria-selected="true"] {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

/* The one scrolling region in the menu, and it contains nothing you need in a
   hurry. flex-shrink lets it give up height first on a short window, so the
   header and the tabs stay whole and the panel never outgrows the screen. */
.pause-body {
  flex: 1 1 auto;
  min-height: 0;
  max-height: min(52vh, 520px);
  overflow-y: auto;
  /* A single column of min-width:0 children cannot overflow sideways, so this
     is a guard against one unbreakable string in the guide rather than the
     layout, and it is why the menu can no longer scroll in two directions. */
  overflow-x: hidden;
  /* Room for the guide chips' glow, which extends past the shape on every side. */
  padding: 20px 12px 4px 2px;
}
.pause-body::-webkit-scrollbar { width: 6px; }
.pause-body::-webkit-scrollbar-thumb {
  background: rgba(150, 180, 220, 0.22);
  border-radius: 3px;
}
.pause-pane { min-width: 0; }

/* Two columns of settings when the panel can hold them, one when it cannot.
   auto-fit with a minimum means the browser drops to one column on its own
   instead of us guessing a breakpoint, and min-width:0 stops a column that is
   wider than its share from pushing the panel past the window — which is the
   exact mechanism that hid the buttons before. */
.settings-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 4px 24px;
  align-items: start;
}
.settings-grid > * { min-width: 0; }
.pause-body .settings-section { margin-bottom: 20px; }

/* Sliders shrink with their column rather than holding a fixed 140px and
   forcing the row wider than the space available. They do not grow, so the
   label keeps whatever width is left and "Mouse sensitivity" stays on one
   line — a wrapped label makes a settings list look broken. */
.pause-body .setting-row input[type="range"] {
  flex: 0 1 110px;
  width: auto;
  min-width: 60px;
}
/* A row-sized button, so Fullscreen reads as the value of its setting rather
   than as a fourth action competing with Resume. */
.mini-btn { padding: 6px 14px; font-size: 9.5px; letter-spacing: 0.16em; }

.settings-foot {
  padding-top: 6px;
  border-top: 1px solid var(--line);
}

/* Every control in here is reachable by keyboard, and the ring has to be
   visible against a dark panel that is already full of amber. Cyan is the
   friendly-utility colour in this game and is used for nothing else in this
   menu, so a focus ring can never be mistaken for a value or a warning. */
.pause-panel :focus-visible {
  outline: 2px solid var(--cool);
  outline-offset: 2px;
  border-radius: 3px;
}

/* The panel itself deliberately has no entrance animation: it is opened while
   someone is being shot at, and any transition on the way in is latency you
   feel. What remains is the toggle knob and the tab underline, which are
   feedback for a deliberate act — and which someone who is motion-sensitive
   should still be able to turn off. */
@media (prefers-reduced-motion: reduce) {
  .pause-panel .toggle,
  .pause-panel .toggle i,
  .pause-panel .enter-btn,
  .pause-tab {
    transition: none;
  }
}

/* Narrower than the panel wants to be. The device gate turns phones away, so
   this is a small desktop window rather than a handset: give back the padding
   and let the settings fall into one column. */
@media (max-width: 640px) {
  .pause-panel { padding: 20px 18px; }
  .pause-actions { flex-wrap: wrap; }
  .pause-actions .enter-btn { flex: 1 1 100%; padding: 14px 20px; }
  .pause-actions .leave-btn { margin-left: 0; }
  .pause-tab { padding: 12px 12px; }
}

.click-to-play {
  position: absolute;
  inset: 0;
  z-index: 12;
  display: flex;
  align-items: center;
  justify-content: center;
  /* The whole arena remains the required pointer-lock target, but it is not a
     modal screen. Players see the finished drop immediately. */
  background: transparent;
  border: 0;
  padding: 0;
  color: inherit;
  font: inherit;
  outline: none;
  cursor: pointer;
}
.click-to-play:focus-visible .ctp-inner {
  border-color: rgba(230, 242, 255, 0.82);
  box-shadow: 0 0 0 2px rgba(102, 217, 255, 0.22), 0 10px 32px rgba(0, 0, 0, 0.24);
}
.ctp-inner {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 8px 12px;
  border: 1px solid rgba(102, 217, 255, 0.32);
  border-radius: 3px;
  background: rgba(4, 9, 17, 0.72);
  box-shadow: 0 10px 32px rgba(0, 0, 0, 0.24), inset 0 0 18px rgba(58, 205, 255, 0.035);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  transform: translateY(15vh);
  pointer-events: none;
  animation: control-cue-in 0.32s ease-out both;
}
.ctp-inner::before {
  content: '';
  width: 5px;
  height: 5px;
  flex: none;
  border-radius: 50%;
  background: var(--cool);
  box-shadow: 0 0 9px rgba(102, 217, 255, 0.9);
  animation: control-cue-pulse 1.4s ease-in-out infinite;
}
.ctp-title {
  font-size: 9px;
  line-height: 1;
  letter-spacing: 0.2em;
  color: rgba(230, 242, 255, 0.92);
  margin: 0;
  text-transform: uppercase;
}
@keyframes control-cue-in {
  from { opacity: 0; transform: translateY(calc(15vh + 8px)); }
  to { opacity: 1; transform: translateY(15vh); }
}
@keyframes control-cue-pulse {
  50% { opacity: 0.45; box-shadow: 0 0 4px rgba(102, 217, 255, 0.45); }
}
@media (prefers-reduced-motion: reduce) {
  .ctp-inner, .ctp-inner::before { animation: none; }
}


/* Sunburst drop tracker: loud, central-top, impossible to miss. */
/* The Sunburst marker (hud.js setStrike positions it each frame from the
   renderer's projection). The ring is an SVG stroke — Safari renders
   calc()-in-conic and masked gradients unreliably, and this must exist on
   phones. The arc is the timer, the diamond the thing, the number the
   distance. Red falling, amber landed: the colour IS the phase. */
.strike-mark {
  position: absolute;
  left: 0; top: 0;
  z-index: 5;
  pointer-events: none;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  --c: #ffb648;
  will-change: transform;
}
.strike-mark.inbound { --c: #ff5a3a; }
.strike-mark b {
  display: block;
  width: 28px; height: 28px;
  position: relative;
}
.strike-mark b svg { width: 100%; height: 100%; transform: rotate(-90deg); display: block; }
.strike-mark b svg circle {
  fill: none;
  stroke: var(--c);
  stroke-width: 3;
}
.strike-mark .sm-core {
  position: absolute;
  left: 50%; top: 50%;
  width: 9px; height: 9px;
  transform: translate(-50%, -50%) rotate(45deg);
  background: var(--c);
}
.strike-mark i {
  font-style: normal;
  font-size: 10px;
  letter-spacing: 0.08em;
  color: var(--c);
  text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
  font-variant-numeric: tabular-nums;
}
.strike-mark.urgent b { animation: strike-urgent 0.5s ease-in-out infinite; }
@keyframes strike-urgent { 50% { transform: scale(1.18); } }
.strike-mark.near { opacity: 0.35; }
.strike-mark.near i { display: none; }
/* The chevron is a warning by definition, so it is red by definition —
   an amber arrow read as "loot this way" (amber is value; owner's call,
   22 Aug). It only appears while a strike is inbound. */
.strike-mark.edge { --c: #ff5a3a; }
.strike-mark.edge b {
  width: 0; height: 0;
  border-left: 9px solid transparent;
  border-right: 9px solid transparent;
  border-bottom: 16px solid var(--c);
  transform: rotate(var(--rot, 0rad));
}
.strike-mark.edge b svg, .strike-mark.edge .sm-core { display: none; }
body.touch-mode .strike-mark b { width: 22px; height: 22px; }
@keyframes strikePulse {
  0%, 100% { border-color: #ff5a3a; box-shadow: 0 0 0 rgba(255, 90, 58, 0); }
  50%      { border-color: #ffb08a; box-shadow: 0 0 22px rgba(255, 90, 58, 0.45); }
}
.module-chip.owned { opacity: 0.9; }


/* .pause-keys is gone: the pause menu stopped reusing the floating control list
   when the keys moved into their own tab with their own grouping. */

/* Menu affordance, always on screen. Clickable when the cursor is free, and a
   standing reminder of the key when it is not.
   Just the glyph: the word MENU was three times the size of the thing it named,
   in the busiest corner of the screen, labelling a control every player already
   finds with Escape. */
.menu-tab {
  position: absolute;
  /* Top-left. The comment that used to be here said the minimap was in the
     bottom-left; it is not, it is at this exact position, and with the minimap
     now on by default the button sat directly on top of it. */
  top: 26px; left: 30px;
  z-index: 7;
  width: 26px; height: 26px;
  display: grid;
  place-items: center;
  line-height: 1;
  padding: 0;
  background: rgba(8, 11, 20, 0.6);
  border: 1px solid rgba(150, 180, 220, 0.28);
  border-radius: 3px;
  color: rgba(220, 232, 248, 0.75);
  font-family: inherit;
  font-size: 15px; letter-spacing: 0;
  cursor: pointer;
  pointer-events: auto;
}
.menu-tab b { color: var(--accent); font-weight: 600; margin-right: 5px; }
/* Stepped aside rather than layered on top of the map. */
.minimap-on .menu-tab { left: 230px; }
.menu-tab:hover {
  background: rgba(8, 11, 20, 0.85);
  border-color: var(--accent-dim);
  color: #fff;
}
/* The minimap occupies the bottom-left, so the tab is unaffected by it. */


/* Re-entry is the same small target, only quieter; no full-screen wash ever
   returns over an active match. */
.click-to-play.returning .ctp-inner { animation: none; opacity: 0.72; }


/* Direction pointer on the drop tracker, in place of a compass label. */
.strike-arrow {
  display: inline-block;
  font-size: 17px; line-height: 1;
  color: #ffb08a;
  /* Rotation is applied inline; this only keeps it spinning about its centre. */
  transform-origin: 50% 50%;
}


/* Per-section reset, sitting on the heading it undoes. */
.heading-row { display: flex; align-items: center; justify-content: space-between; }
.reset-mini {
  padding: 2px 8px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 2px;
  color: var(--muted);
  font-family: inherit;
  font-size: 8.5px; letter-spacing: 0.16em; text-transform: uppercase;
  cursor: pointer;
}
.reset-mini:hover { border-color: var(--accent-dim); color: var(--accent); }


/* Empty rack slots: present but plainly not yours yet. Dim enough to read as
   unavailable at a glance, legible enough to tell you what to go looking for. */


/* Leaderboard, held on TAB. */
.board {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  min-width: 460px;
  z-index: 9;
  padding: 20px 24px;
  background: rgba(6, 9, 16, 0.93);
  border: 1px solid var(--accent-dim);
  border-radius: 4px;
  pointer-events: none;
}
.board-title {
  font-size: 9.5px; letter-spacing: 0.3em;
  color: var(--accent);
  margin-bottom: 14px;
}
.board-row {
  display: grid;
  grid-template-columns: 22px 1fr 76px 76px 44px;
  gap: 10px;
  align-items: center;
  padding: 5px 0;
  font-size: 12px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.board.with-ping { min-width: 548px; }
.board.with-ping .board-row {
  grid-template-columns: 22px minmax(110px, 1fr) 76px 76px 44px 58px;
}
.board-row.head {
  font-size: 8.5px; letter-spacing: 0.2em;
  color: var(--muted);
  border-bottom-color: var(--accent-dim);
  padding-bottom: 7px;
}
.board-row .rank { color: var(--muted); font-size: 10px; }
.board-row .who { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-row .num { text-align: right; font-variant-numeric: tabular-nums; }
.board-row .carried { color: var(--accent); }
.board-row .bankedv { color: rgba(200, 214, 234, 0.7); }
.board-row .kills { text-align: right; color: rgba(200, 214, 234, 0.55); font-variant-numeric: tabular-nums; }
/* You, so you can find yourself without reading every name. */
.board-row.self { background: rgba(255, 176, 60, 0.09); }
.board-row.self .who { color: #fff; font-weight: 600; }
.board-row.down .who { color: rgba(200, 214, 234, 0.35); text-decoration: line-through; }
.board-row.extracting .who::after {
  content: ' EXTRACTING';
  font-size: 8.5px; letter-spacing: 0.16em;
  color: #7dffa8;
}
.board-empty { font-size: 11px; color: var(--muted); }

/* The full board must still fit on the shortest landscape phones. Ping is an
   optional sixth column, so it gets its own compressed grid instead of forcing
   the panel wider than the visual viewport. */
@media (max-width: 620px), (max-height: 430px) {
  .board,
  .board.with-ping {
    width: calc(100vw - 24px);
    min-width: 0;
    max-height: calc(100vh - 20px);
    overflow: hidden auto;
    box-sizing: border-box;
    padding: 13px 12px;
  }
  .board-row {
    grid-template-columns: 17px minmax(62px, 1fr) 54px 54px 32px;
    gap: 5px;
    font-size: 10px;
  }
  .board.with-ping .board-row {
    grid-template-columns: 17px minmax(62px, 1fr) 50px 50px 29px 39px;
  }
  .board-row.head { font-size: 7px; letter-spacing: 0.1em; }
  .board-row .ping { font-size: 9px; }
}


/* Compact standing, always on screen. Small enough to ignore, present enough
   that nobody has to be told the scoreboard exists. */
.mini-board {
  position: absolute;
  top: 96px; right: 30px;
  width: 190px;
  z-index: 5;
  padding: 9px 11px 7px;
  background: rgba(6, 9, 16, 0.55);
  border: 1px solid rgba(150, 180, 220, 0.14);
  border-radius: 3px;
  pointer-events: none;
  text-align: left;
}
.mini-row {
  display: grid;
  grid-template-columns: 12px 1fr auto;
  gap: 7px;
  align-items: baseline;
  padding: 2px 0;
  font-size: 10.5px;
}
.mini-row .r { color: rgba(150, 165, 190, 0.6); font-size: 9px; }
.mini-row .n { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: rgba(220, 232, 248, 0.8); }
.mini-row .v { color: var(--accent); font-variant-numeric: tabular-nums; }
.mini-row.self .n { color: #fff; font-weight: 600; }
.mini-foot {
  margin-top: 5px; padding-top: 5px;
  border-top: 1px solid rgba(150, 180, 220, 0.12);
  font-size: 8.5px; letter-spacing: 0.16em;
  color: rgba(150, 165, 190, 0.5);
}
.mini-foot b { color: var(--accent); font-weight: 600; }
.board-row .ping { text-align: right; font-variant-numeric: tabular-nums; font-size: 11px; }
.ping-good { color: #7dffa8; }
.ping-ok   { color: #ffd166; }
.ping-bad  { color: #ff7a6b; }

/* Guide swatches show the object, not a colour chip. Identical squares made the
   legend a colour chart; shape and relative size are what actually distinguish
   these things on the ground, so the key uses the same language the world does. */
.guide-chip.shape-diamond {
  border-radius: 2px;
  transform: rotate(45deg);
  animation: guideSpin 3.4s linear infinite;
}
.guide-chip.shape-round { border-radius: 50%; }
/* A death drop is a pile, so it reads as several things rather than one. */
.guide-chip.shape-stack {
  border-radius: 2px;
  box-shadow: 0 0 8px currentColor, 0 -4px 0 -1px currentColor, 0 -8px 0 -2px currentColor;
}
.guide-chip.size-sm { width: 10px; height: 10px; }
.guide-chip.size-md { width: 15px; height: 15px; }
.guide-chip.size-lg { width: 21px; height: 21px; }
@keyframes guideSpin {
  from { transform: rotate(45deg) scaleX(1); }
  25%  { transform: rotate(45deg) scaleX(0.25); }
  50%  { transform: rotate(45deg) scaleX(1); }
  75%  { transform: rotate(45deg) scaleX(0.25); }
  to   { transform: rotate(45deg) scaleX(1); }
}


/* Cache channel. The one pickup you cannot simply walk over, so it needs to say
   so while you are standing on it — not in a guide you read afterwards. */
.cache-channel {
  position: absolute;
  left: 50%; bottom: 26%;
  transform: translateX(-50%);
  width: 260px;
  z-index: 6;
  text-align: center;
  pointer-events: none;
}
.cache-label {
  font-size: 10px; letter-spacing: 0.22em;
  color: var(--accent);
  margin-bottom: 7px;
}
.cache-bar {
  height: 4px;
  background: rgba(255, 255, 255, 0.12);
  border-radius: 2px;
  overflow: hidden;
}
.cache-fill {
  height: 100%; width: 0%;
  background: var(--accent);
  box-shadow: 0 0 10px var(--accent);
  transition: width 0.08s linear;
}
/* The cost is the noise, so the warning sits with the progress that causes it. */
.cache-warn {
  margin-top: 6px;
  font-size: 9px; letter-spacing: 0.12em;
  color: rgba(255, 160, 90, 0.75);
}


/* Spawn protection, stated plainly. Invisible protection is indistinguishable
   from none — and its ending is the moment that gets you killed. */
.spawn-protect {
  position: absolute;
  left: 50%; top: 20%;
  transform: translateX(-50%);
  padding: 6px 14px;
  border: 1px solid rgba(127, 228, 255, 0.5);
  border-radius: 3px;
  background: rgba(6, 20, 30, 0.6);
  color: #7fe4ff;
  font-size: 10px; letter-spacing: 0.22em;
  z-index: 6;
  pointer-events: none;
  animation: protectPulse 1s ease-in-out infinite;
}
@keyframes protectPulse {
  0%, 100% { opacity: 0.75; }
  50%      { opacity: 1; }
}

/* In the Sunburst marker column. Red, because this is the danger colour
   everywhere else in the game and the beam it refers to is red. Faster pulse
   than the protection banner it replaced: this one wants you to move. */
.spawn-protect.burning {
  border-color: rgba(255, 90, 69, 0.7);
  background: rgba(38, 8, 6, 0.72);
  color: #ff8a72;
  animation: beamPulse 0.42s ease-in-out infinite;
}
@keyframes beamPulse {
  0%, 100% { opacity: 0.7; }
  50%      { opacity: 1; }
}


/* Grid integrity, while you are shooting the substation. Cyan because it is
   infrastructure, turning red as it fails. */
.grid-damage {
  position: absolute;
  left: 50%; bottom: 32%;
  transform: translateX(-50%);
  width: 240px;
  z-index: 6;
  text-align: center;
  pointer-events: none;
}
.grid-label {
  font-size: 9.5px; letter-spacing: 0.22em;
  color: #7fe4ff;
  margin-bottom: 6px;
}
.grid-bar {
  height: 5px;
  background: rgba(255, 255, 255, 0.12);
  border-radius: 3px;
  overflow: hidden;
}
.grid-fill {
  height: 100%; width: 100%;
  background: #7fe4ff;
  box-shadow: 0 0 10px #7fe4ff;
  transition: width 0.12s linear;
}
.grid-damage.critical .grid-label { color: #ff7a6b; }
.grid-damage.critical .grid-fill { background: #ff7a6b; box-shadow: 0 0 12px #ff7a6b; }
.grid-damage.projector:not(.critical) .grid-label { color: #ffc46c; }
.grid-damage.projector:not(.critical) .grid-fill {
  background: linear-gradient(90deg, #dd8e32, #ffd18a);
  box-shadow: 0 0 12px rgba(255, 184, 77, 0.92);
}

/* Way out of the tuning panel. For a while there was none: the title said
   "T TO CLOSE" after the T key had been removed, and the only other caller of
   toggle() was the console command sitting behind the panel. */
.admin-title.heading-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.admin-close {
  background: none;
  border: 1px solid rgba(150, 180, 220, 0.28);
  border-radius: 3px;
  color: rgba(220, 232, 248, 0.8);
  font: inherit;
  font-size: 13px;
  line-height: 1;
  padding: 1px 7px 3px;
  cursor: pointer;
}
.admin-close:hover { border-color: var(--accent-dim); color: #fff; }

/* The Forge pitch under the key list. Sits in the one screen every player
   opens, because the store is otherwise something you either walk into or
   never learn about. */
.forge-promo {
  display: grid;
  grid-template-columns: 120px minmax(0, 1fr);
  align-items: start;
  gap: 18px;
  margin-top: 22px;
  padding: 16px 18px 14px;
  border: 1px solid rgba(240, 179, 80, 0.22);
  border-radius: 4px;
  /* A warm wash rather than a flat panel: the Forge reads amber in the world
     and this should be recognisable as the same object. */
  background:
    radial-gradient(120% 140% at 0% 0%, rgba(240, 179, 80, 0.10), transparent 60%),
    rgba(8, 11, 20, 0.55);
}
.fp-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.fp-title {
  color: var(--accent);
  font-size: 11px;
  letter-spacing: 0.22em;
}
.fp-sub {
  color: var(--muted);
  font-size: 10.5px;
  letter-spacing: 0.06em;
}
.fp-lede {
  margin: 9px 0 13px;
  max-width: 62ch;
  color: rgba(220, 232, 248, 0.72);
  font-size: 11.5px;
  line-height: 1.65;
}
.fp-lede b { color: var(--accent); font-weight: 600; }
.fp-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 5px 22px;
}
.fp-list li {
  display: grid;
  /* min-content on the flexible column, not auto: a grid child defaults to
     min-width:auto and refuses to shrink below its longest word, which is how
     a three-column layout silently pushes itself off the side of the panel. */
  grid-template-columns: 18px minmax(0, auto) minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0 8px;
  padding: 3px 0;
  font-size: 11.5px;
}
.fp-list li b {
  color: var(--accent);
  font-weight: 600;
  font-size: 10.5px;
}
.fp-name { color: rgba(232, 240, 252, 0.92); }
.fp-desc {
  color: var(--muted);
  font-size: 10.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.fp-cost {
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.fp-cost::after {
  content: ' C';
  color: rgba(240, 179, 80, 0.5);
}

.fp-art { width: 120px; height: 110px; display: block; }
.fp-body { min-width: 0; }
@media (max-width: 720px) {
  .forge-promo { grid-template-columns: 1fr; }
  .fp-art { margin: 0 auto; }
}


/* =========================================================================
   TOUCH CONTROLS
   =========================================================================

   Colour follows the language the rest of the game already speaks: amber is
   value, cyan is friendly utility, red is danger. The controls are the
   player's own kit, so they are cyan and neutral glass — never amber. A
   control tinted the colour of money is a control that competes with the loot
   it is being used to collect, and at a glance in peripheral vision the two
   would not separate.

   Everything here is sized for a thumb rather than a cursor. The smallest
   target is 46px; the fire button is 84px because it is the one control that
   is pressed under pressure, and a missed shot that was really a missed button
   is the least forgivable failure a touch shooter has. */

.touch-layer {
  position: absolute;
  inset: 0;
  z-index: 8;
  /* The layer itself takes pointers — the stick and the look drag are gestures
     on empty screen, not on any element. Individual HUD readouts above it stay
     pointer-events: none, so nothing steals a drag. */
  pointer-events: auto;
  /* Without this the browser claims the gesture first: a look drag scrolls the
     page, a two-finger reach for a gear button zooms, and a quick double tap
     to fire zooms in instead. */
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}

/* Nothing on the layer overlaps the notch or the home indicator. In landscape
   the cutout is on one of the short edges, which is exactly where the fire
   button and the stick want to live. */
.touch-layer,
.touch-top,
.touch-weapons {
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

/* --- The floating stick -------------------------------------------------- */

.touch-stick {
  position: absolute;
  width: 132px; height: 132px;
  margin: -66px 0 0 -66px;
  pointer-events: none;
  /* Faded in at the moment of contact rather than snapped, so the stick reads
     as the screen answering the thumb rather than as UI appearing. */
  animation: stick-in 0.14s ease-out;
}
@keyframes stick-in {
  from { opacity: 0; transform: scale(0.82); }
  to   { opacity: 1; transform: scale(1); }
}
.touch-stick-ring {
  position: absolute; inset: 0;
  border-radius: 50%;
  border: 1.5px solid rgba(140, 180, 220, 0.28);
  background: radial-gradient(circle at 50% 50%,
              rgba(12, 22, 38, 0.34), rgba(12, 22, 38, 0.14) 68%, transparent 72%);
}
.touch-knob {
  position: absolute; left: 50%; top: 50%;
  width: 54px; height: 54px;
  border-radius: 50%;
  background: rgba(180, 210, 240, 0.22);
  border: 1.5px solid rgba(190, 220, 245, 0.5);
  box-shadow: 0 2px 14px rgba(0, 0, 0, 0.35);
  transform: translate(-50%, -50%);
  /* Left un-transitioned on purpose: the knob must sit exactly under the
     thumb. Any easing here reads as the stick lagging the finger, which feels
     like input lag even though the movement it reports is instant. */
}
.touch-sprint-tag {
  position: absolute;
  left: 50%; top: -22px;
  transform: translateX(-50%);
  font-size: 9px; letter-spacing: 0.22em;
  color: var(--cool);
  opacity: 0;
  transition: opacity 0.12s ease;
}
/* Sprinting is the one state the stick reports back, because it is the one
   the player asked for with a gesture rather than a button and therefore the
   one they might not be sure they got. */
.touch-stick.sprinting .touch-stick-ring {
  border-color: rgba(79, 216, 255, 0.6);
  box-shadow: 0 0 18px rgba(79, 216, 255, 0.25);
}
.touch-stick.sprinting .touch-knob {
  background: rgba(79, 216, 255, 0.26);
  border-color: rgba(120, 228, 255, 0.75);
}
.touch-stick.sprinting .touch-sprint-tag { opacity: 1; }

/* --- Buttons ------------------------------------------------------------- */

.touch-btn {
  pointer-events: auto;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font);
  color: rgba(220, 235, 250, 0.9);
  background: rgba(10, 18, 32, 0.72);
  border: 1.5px solid rgba(140, 180, 220, 0.3);
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
  /* Only the press state animates, and fast. A button that eases into being
     pressed does not feel like a trigger. */
  transition: transform 0.06s ease, background 0.06s ease, border-color 0.06s ease;
}
.touch-btn.down {
  transform: scale(0.93);
  background: rgba(79, 216, 255, 0.22);
  border-color: var(--cool);
}
.touch-btn.dim { opacity: 0.4; }

.touch-right {
  position: absolute;
  right: calc(16px + env(safe-area-inset-right, 0px));
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  display: flex; flex-direction: column; align-items: flex-end;
  gap: 12px;
  pointer-events: none;
}
.touch-gear { display: flex; gap: 10px; pointer-events: none; }
/* Jump above the trigger, on the thumb's arc: the thumb pivots at the
   corner, so its comfortable reach is a quarter-circle up-and-left of the
   trigger — the same diagonal every mobile shooter uses. The offset also
   keeps jump outside the trigger's catch wedge, so a jump can never
   charge a shot. */
.touch-actions { display: flex; flex-direction: column; align-items: flex-end; gap: 16px; pointer-events: none; }
.touch-actions .touch-jump { margin-right: 62px; }

.touch-gear-btn {
  width: 58px; height: 58px;
  border-radius: 50%;
  flex-direction: column;
  gap: 1px;
}
.tg-glyph { font-size: 19px; line-height: 1; color: var(--cool); }
.tg-label {
  font-size: 7.5px; letter-spacing: 0.13em;
  color: rgba(190, 210, 235, 0.7);
}

.touch-jump {
  width: 62px; height: 62px;
  border-radius: 50%;
}
.touch-jump .tg-glyph { font-size: 24px; }

.touch-fire {
  width: 84px; height: 84px;
  border-radius: 50%;
  position: relative;
  background: rgba(20, 30, 46, 0.55);
  border-color: rgba(180, 210, 235, 0.45);
}
.touch-fire-ring {
  width: 30px; height: 30px;
  border-radius: 50%;
  border: 2px solid rgba(225, 240, 255, 0.85);
  position: relative;
}
.touch-fire-ring::after {
  content: '';
  position: absolute; left: 50%; top: 50%;
  width: 4px; height: 4px;
  margin: -2px 0 0 -2px;
  border-radius: 50%;
  background: rgba(225, 240, 255, 0.9);
}
.touch-fire.down { background: rgba(79, 216, 255, 0.3); }

/* --- Weapons and chips --------------------------------------------------- */

.touch-weapons {
  position: absolute;
  left: 50%;
  bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  display: flex; gap: 7px;
  pointer-events: none;
}
.touch-weapon {
  height: 34px;
  padding: 0 13px;
  border-radius: 17px;
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
}
.touch-weapon.active {
  border-color: var(--accent);
  color: var(--accent);
  background: rgba(255, 182, 72, 0.14);
}

.touch-top {
  position: absolute;
  top: calc(10px + env(safe-area-inset-top, 0px));
  right: calc(14px + env(safe-area-inset-right, 0px));
  display: flex; gap: 8px;
  pointer-events: none;
}
.touch-chip {
  min-height: 44px;
  padding: 0 13px;
  border-radius: 22px;
  font-size: 9.5px;
  letter-spacing: 0.16em;
}

/* --- Layout changes that only apply while a thumb is driving -------------- */

/* The vitals move out of thumb country. On desktop the bottom-left corner is
   the calmest part of the screen; on a phone held in landscape it is directly
   under the left thumb and the stick that follows it, so health and stamina —
   the two things you check while being shot at — would be read through a
   finger. They go to the top-left instead, which touch leaves empty. */
body.touch-mode .hud-bl {
  top: calc(10px + env(safe-area-inset-top, 0px));
  bottom: auto;
  left: calc(14px + env(safe-area-inset-left, 0px));
}
/* The minimap is also top-left on a phone, so the vitals go under it rather
   than through it. Derived from the map's own size instead of a copied number,
   because the two were laid out independently once already and the result was
   health drawn on top of the map. */
body.touch-mode.minimap-on .hud-bl {
  top: calc(var(--mini-y, 10px) + var(--mini-size, 118px) + 12px);
}
body.touch-mode .hud-tr {
  top: calc(50px + env(safe-area-inset-top, 0px));
  right: calc(14px + env(safe-area-inset-right, 0px));
}
/* The weapon readout would sit on top of the fire button. The strip already
   names the active weapon, so this becomes ammo only, lifted clear. */
body.touch-mode .weapon-slots { display: none; }

/* --- The phone's right hand: two pills, a trigger, a jump ---------------- */
/* The pills stack at the edge above the trigger; each opens a row of pill
   options to its LEFT. The gun in hand is the top pill's label; MORE carries
   the shield count. Nothing else lives on the right. */
.touch-pills {
  display: flex; flex-direction: column; align-items: flex-end;
  gap: 10px;
  pointer-events: none;
  /* The action column below is taller now (jump stacked over the trigger),
     so a modest gap keeps the pills above it all. */
  margin-bottom: 20px;
}
.touch-pill-wrap { display: flex; flex-direction: row; justify-content: flex-end; align-items: center; gap: 8px; pointer-events: none; }
.touch-pill {
  min-height: 44px;
  padding: 0 18px;
  border-radius: 22px;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
  position: relative;
  gap: 7px;
}
.touch-pill.open { border-color: var(--cool); background: rgba(79, 216, 255, 0.16); color: #cff4ff; }
.touch-pill .tg-count {
  position: static;
  margin-left: 4px;
  min-width: 18px; height: 18px;
  font-size: 11px;
}
.touch-row { display: flex; flex-direction: row; gap: 8px; pointer-events: none; }
.touch-row .touch-option.active { border-color: var(--cool); background: rgba(79, 216, 255, 0.2); color: #cff4ff; }
.touch-row .tg-glyph { font-size: 15px; }
/* The old rail and strip are retired on touch. */
body.touch-mode .touch-gear, body.touch-mode .touch-weapons { display: none; }

/* The trigger: half there at rest, fully there under a thumb, and it slides
   to wherever the thumb landed within reach (touch.js sets --fx/--fy). */
.touch-fire { opacity: 0.55; transition: transform 0.06s ease, opacity 0.1s ease, background 0.06s ease; }
.touch-fire.down { opacity: 1; }
.touch-fire.slid { transform: translate(var(--fx, 0px), var(--fy, 0px)) scale(0.96); }

/* The battery, bottom centre, where the weapon strip used to be. The Lance
   draws from it; this is where you look to know whether you can. */
body.touch-mode .hud-br {
  left: 50%; right: auto;
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%) !important;   /* beats the short-viewport scale; the bar sizes itself */
  width: 170px;
  text-align: center;
}
body.touch-mode .energy-bar { width: 170px; height: 14px; margin: 0 auto; }
body.touch-mode .hud-bl .stat-line { margin-top: 3px; }

/* The crosshair sits at the centre of the screen, and on a phone the centre of
   the screen is where the aim is — but it is drawn 1.5px wide for a monitor an
   arm's length away. Thicker and slightly larger, or it disappears against a
   bright wall on a screen held at arm's length in daylight. */
body.touch-mode .crosshair { width: 26px; height: 26px; }
body.touch-mode .crosshair span:nth-child(1),
body.touch-mode .crosshair span:nth-child(2) { width: 2px; margin-left: -1px; height: 8px; }
body.touch-mode .crosshair span:nth-child(3),
body.touch-mode .crosshair span:nth-child(4) { height: 2px; margin-top: -1px; width: 8px; }

/* The chip uses the verb that matches the current input device. */
.ctp-touch { display: none; }
body.touch-mode .ctp-title { display: none; }
body.touch-mode .ctp-touch {
  display: block;
  font-size: 9px;
  line-height: 1;
  color: rgba(230, 242, 255, 0.92);
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

/* --- Portrait ------------------------------------------------------------ */

.rotate-gate {
  position: fixed; inset: 0;
  z-index: 90;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle at 50% 45%, #101a2e, #04060c 72%);
  text-align: center;
}
.rotate-inner { display: flex; flex-direction: column; align-items: center; gap: 18px; }
.rotate-phone {
  width: 42px; height: 72px;
  border: 2px solid var(--cool);
  border-radius: 7px;
  animation: rotate-hint 2.4s ease-in-out infinite;
}
@keyframes rotate-hint {
  0%, 30%   { transform: rotate(0deg); }
  55%, 85%  { transform: rotate(-90deg); }
  100%      { transform: rotate(0deg); }
}
.rotate-text {
  font-size: 13px; letter-spacing: 0.26em;
  color: var(--ink);
}
.rotate-sub { font-size: 11.5px; color: var(--muted); letter-spacing: 0.04em; }

/* --- Small screens ------------------------------------------------------- */

/* Landscape on a phone is roughly 390px tall, and the HUD was laid out for a
   window ten times that area. Everything below is the same information at a
   size that leaves room for the game. */
@media (max-height: 460px) {
  .hud-bl, .hud-br, .hud-tr { transform: scale(0.86); }
  .hud-bl { transform-origin: top left; }
  .hud-br { transform-origin: bottom right; }
  .hud-tr { transform-origin: top right; }

  .heart { width: 15px; height: 15px; }
  .hearts { gap: 4px; margin-bottom: 9px; }
  .stamina-bar { width: 92px; }

  .killfeed { font-size: 10px; }
  .mini-board { transform: scale(0.85); transform-origin: top right; }

  .touch-gear-btn { width: 52px; height: 52px; }
  .touch-jump { width: 56px; height: 56px; }
  .touch-fire { width: 76px; height: 76px; }
  .touch-right { gap: 9px; bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }
  .touch-weapon { height: 30px; padding: 0 11px; font-size: 9px; }
}

/* The pause menu, the guide and the extraction summary are full-screen panels
   built for a desktop window. On a phone they need to scroll, and their
   controls need to be thumb-sized rather than cursor-sized. */
@media (max-width: 900px), (max-height: 520px) {
  .pause-panel, .guide-panel {
    width: 100%;
    max-width: none;
    max-height: 100%;
    border-radius: 0;
  }
  .pause-body, .guide-body {
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }
  .enter-btn, .ghost-btn { min-height: 46px; }
  .pause-tab { min-height: 42px; }
  .seg button { min-height: 38px; padding: 0 12px; }
  /* A range slider with a 12px thumb cannot be grabbed with a finger. */
  input[type="range"] { height: 34px; }
}

/* Two control references, one device. The map key stays on the keyboard list
   because M is still how the map is toggled — but on touch that lives in
   Settings, which is what the touch list says. */
.touch-groups { display: none; }
body.touch-mode .touch-groups { display: grid; }
body.touch-mode .key-groups:not(.touch-groups) { display: none; }
body.touch-mode .touch-groups span { font-size: 10.5px; line-height: 1.5; }

/* The module strip keeps its counts on touch — knowing you have two shields
   left is worth the space — but loses the key badges. B, T, H and G are not
   things a phone has, and a chip that leads with a key nobody can press reads
   as a control rather than as the readout it actually is. The gear rail beside
   it is where those same items are used. */
body.touch-mode .chip-key { display: none; }
/* And it moves up above the weapon strip, which now occupies the bottom centre.
   Both were laid out for the middle of the bottom edge and drew straight
   through each other — the weapon buttons sat on top of the module counts. */
body.touch-mode .module-strip {
  bottom: calc(52px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%) scale(0.88);
}

/* --- Counts on the controls they belong to ------------------------------- */

/* A ring of segments around the button, one per charge, plus the digit. The
   segments are what you read without reading — "two notches" arrives as a
   shape while a number still has to be parsed, and this is information you
   want mid-fight. */
.tg-ring {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  transform: rotate(-90deg);
  pointer-events: none;
}
.tg-ring circle {
  fill: none;
  stroke: var(--cool);
  stroke-width: 2.5;
  opacity: 0.85;
}
.touch-gear-btn { position: relative; }
.tg-count {
  position: absolute;
  top: -2px; right: -2px;
  min-width: 17px; height: 17px;
  padding: 0 3px;
  border-radius: 9px;
  background: var(--cool);
  color: #04121a;
  font-size: 10px;
  font-weight: 700;
  line-height: 17px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.touch-btn.dim .tg-ring circle { stroke: var(--muted); opacity: 0.5; }
.touch-btn.dim .tg-count { background: var(--muted); color: #0b1018; }

/* --- Reclaiming the screen ----------------------------------------------- */

/* The module strip is gone on touch. It was a row of rectangles repeating what
   the gear buttons now say, in a corner of the screen it had to share with the
   weapon strip and the portal prompt — three things laid out for a desktop
   window all landing in the same 40px of a phone. Its counts moved onto the
   buttons; nothing was lost but the collision. */
body.touch-mode .module-strip { display: none; }

/* The weapon name too: the strip along the bottom already names the active
   weapon and highlights it, so this was the same word twice, and its corner is
   where the gear rail lives. Ammo stays — that one is not written anywhere
   else — and moves clear of the buttons. */
body.touch-mode .weapon-name { display: none; }
body.touch-mode .weapon-ammo { font-size: 15px; }

/* --- Left-handed --------------------------------------------------------- */

/* Everything that had a side swaps sides. The stick half is mirrored in
   touch.js to match; the two have to move together or the stick lands under
   the hand reaching for the trigger. */
body.touch-mode.lefty .touch-right {
  right: auto;
  left: calc(16px + env(safe-area-inset-left, 0px));
  align-items: flex-start;
}
body.touch-mode.lefty .touch-actions { flex-direction: column; align-items: flex-start; }
body.touch-mode.lefty .touch-actions .touch-jump { margin-right: 0; margin-left: 62px; }
body.touch-mode.lefty .touch-pills { align-items: flex-start; }
body.touch-mode.lefty .touch-pill-wrap { flex-direction: row-reverse; justify-content: flex-start; }
body.touch-mode.lefty .touch-row { flex-direction: row-reverse; }
body.touch-mode.lefty .touch-gear { flex-direction: row-reverse; }
body.touch-mode.lefty .touch-top {
  right: auto;
  left: calc(14px + env(safe-area-inset-left, 0px));
  flex-direction: row-reverse;
}
/* The vitals and the minimap move out of the way of the newly-busy left side. */
body.touch-mode.lefty .hud-bl {
  left: auto;
  right: calc(14px + env(safe-area-inset-right, 0px));
  text-align: right;
}
body.touch-mode.lefty .hud-br {
  right: auto;
  left: calc(14px + env(safe-area-inset-left, 0px));
  text-align: left;
}
body.touch-mode.lefty .mini-board { left: 14px; right: auto; }

/* --- Settings rows that only apply to one kind of device ------------------ */

.touch-only, .touch-only-section { display: none; }
body.touch-mode .touch-only { display: flex; }
body.touch-mode .touch-only-section { display: block; }
body.touch-mode .desktop-only { display: none; }

/* --- Install / fullscreen offer ------------------------------------------ */

.install-hint {
  position: absolute;
  left: 50%;
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: 8;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: calc(100% - 28px);
  padding: 11px 12px 11px 16px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: rgba(8, 13, 24, 0.92);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  animation: install-in 0.3s ease-out;
}
@keyframes install-in {
  from { opacity: 0; transform: translateX(-50%) translateY(10px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}
.install-text { font-size: 12px; line-height: 1.45; color: var(--muted); }
.install-text b { display: block; color: var(--ink); font-weight: 600; }
.install-text em { font-style: normal; color: var(--cool); }
.install-go {
  flex: none;
  min-height: 44px;
  padding: 0 14px;
  border: none;
  border-radius: 6px;
  background: var(--accent);
  color: #150d00;
  font-family: var(--font);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.14em;
}
.install-close {
  flex: none;
  width: 44px; height: 44px;
  background: none;
  border: none;
  color: var(--muted);
  font-size: 22px;
  line-height: 1;
}

/* An installed launch has no browser bars, so the game finally owns the whole
   screen — but on iOS the status bar is then drawn over the top of it. The
   safe-area insets already keep the HUD clear; this just makes sure the page
   itself is measured against the real display. */
@media (display-mode: standalone), (display-mode: fullscreen) {
  html, body { height: 100vh; }
}

/* The killfeed clears the vitals on touch. On desktop the vitals are in the
   bottom-left and this corner is free; on a phone they moved to the top-left
   to get out from under the thumb, so the first kill printed straight through
   BANKED. Both positions are derived from the minimap the same way the vitals
   are, so turning the map on or off keeps the whole column consistent. */
body.touch-mode .killfeed {
  /* The vitals column is four rows again (the card rides the Extracted
     line), so the feed sits back where it clears them. */
  top: calc(88px + env(safe-area-inset-top, 0px));
  left: calc(14px + env(safe-area-inset-left, 0px));
}
body.touch-mode.minimap-on .killfeed {
  top: calc(var(--mini-y, 10px) + var(--mini-size, 118px) + 106px);
}
body.touch-mode .kill-row { font-size: 10px; padding: 4px 8px; }

/* Left-handed puts the vitals on the right, so the feed follows them. */
body.touch-mode.lefty .killfeed {
  left: auto;
  right: calc(14px + env(safe-area-inset-right, 0px));
  align-items: flex-end;
}

/* The Forge, on a phone.
   The key badge goes — the row is the control here, and a number badge in a
   list you buy from by tapping is an instruction to press something that does
   not exist. Rows also grow to a thumb-sized target, and the panel is allowed
   to scroll: seven modules at desktop row heights do not fit in 360px of
   landscape phone, and a shop you cannot reach the bottom of is a shop that
   sells five things. */
body.touch-mode .forge-promo li > b { display: none; }

/* Two columns, so all seven modules are on screen at once.
   One column of seven at a 44px touch target is 400px of list on a screen that
   is 360px tall — the last three sat below the panel entirely. Scrolling was
   the obvious answer and the wrong one: you buy at a Forge while exposed, and
   a shop you have to scroll is a shop you are standing still in front of for
   longer. Wider and squarer fits the shape of a landscape phone, and fits the
   whole menu in one look.

   The descriptions come off in this layout — there is no room for them beside
   a name and a price in half a panel, and they are still in full in the pause
   menu's Forge section, which is where someone reads about the kit rather than
   shops for it. */
body.touch-mode .forge-panel {
  width: min(92vw, 560px);
  max-height: calc(100vh - 90px);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}
body.touch-mode .forge-list {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
}
body.touch-mode .forge-item {
  min-height: 44px;
  align-items: center;
}
body.touch-mode .forge-desc { display: none; }
/* The tap target is the row, so it has to look like one. */
body.touch-mode .forge-item:not(.owned) { cursor: pointer; }
body.touch-mode .forge-item.pressed {
  background: rgba(79, 216, 255, 0.16);
  border-color: var(--cool);
}
body.touch-mode .forge-key { display: none; }

/* The overflow tray. Opens upward from the MORE button so the items do not
   appear under the thumb that just opened it, and stacks rather than fanning
   because a column is easier to hit than an arc when the phone is moving. */
.touch-more { position: relative; }
.touch-tray {
  position: absolute;
  bottom: calc(100% + 10px);
  right: 0;
  display: flex;
  flex-direction: column-reverse;
  gap: 10px;
  pointer-events: auto;
  animation: tray-in 0.14s ease-out;
}
.touch-tray.hidden { display: none; }
@keyframes tray-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* Open state reads on the button itself, so it is obvious the tray belongs to
   it and that tapping again puts it away. */
.touch-more[aria-expanded="true"] {
  background: rgba(79, 216, 255, 0.2);
  border-color: var(--cool);
}
/* Left-handed: the rail is on the left, so the tray hangs the other way. */
body.touch-mode.lefty .touch-tray { right: auto; left: 0; }

/* The Forge has to be reachable by a finger.
   It was not, and this is the bug that made a phone unable to buy anything at
   all. Two things were wrong at once, and either alone was enough: the panel
   declares `pointer-events: none` — correct while the number row was the only
   way to buy — and it sits at z-index 5, underneath the touch layer at 8,
   which covers the whole screen and takes pointers so the stick and the look
   drag can be gestures on empty space. So a tap on a module was either ignored
   by the panel or swallowed by the layer above it, and either way it read as
   the game refusing to sell anything.

   Lifted above the layer, but only the ROWS take pointers. The panel's
   background stays transparent to them, so you can still swing the view around
   while standing at a Forge instead of the shop becoming a 340px hole in the
   middle of the screen where looking does not work. */
body.touch-mode .forge-panel { z-index: 10; }
body.touch-mode .forge-item { pointer-events: auto; }


/* --- The Horizon's scope ------------------------------------------------- */
/* A hard vignette: everything outside the circle goes black, a hairline
   cross inside it, the magnification in the lower right of the glass. */
/* Below the touch layer (8): on a phone the trigger, the stick and the
   pills must stay visible through the glass — a scope you cannot fire or
   un-scope from is a trap. On desktop nothing else lives that high. */
.scope {
  position: fixed;
  inset: 0;
  z-index: 7;
  pointer-events: none;
  overflow: hidden;
}
.scope.hidden { display: none; }

/* The iris.
   The glass used to appear and vanish with display:none — no transition at
   all, which is what made a ten-power scope feel like a UI panel instead of
   a rifle. The black surround now lives on its own layer, drawn far larger
   than the viewport, so SCALING it opens and closes the hole while the
   surround still covers the screen. Transform only: no layout, no repaint
   of the gradient, which matters on a phone. */
.scope-iris {
  position: absolute;
  left: 50%;
  top: 50%;
  /* Sized in vmin so the aperture matches the original design (a ~38vmin
     radius) on any aspect. 400vmin square still covers the viewport out to
     a 4:1 screen, which is well past any phone held sideways. */
  width: 400vmin;
  height: 400vmin;
  margin: -200vmin 0 0 -200vmin;
  background: radial-gradient(circle at 50% 50%,
    transparent 0,
    transparent 9.5%,
    rgba(0, 0, 0, 0.985) 10.05%);
  transform: scale(1.55);
  opacity: 0;
  transition: transform 140ms cubic-bezier(.16, 1, .3, 1), opacity 110ms ease-out;
}
.scope.in .scope-iris { transform: scale(1); opacity: 1; }

/* The reticle assembles a beat after the glass arrives, rather than being
   there before the eye is. */
.scope-cross, .scope-dot {
  transform-origin: 50% 50%;
  opacity: 0;
  transition: opacity 110ms ease-out 40ms;
}
.scope.in .scope-cross, .scope.in .scope-dot { opacity: 1; }
.scope-cross {
  transform: scale(0.55);
  transition: opacity 110ms ease-out 40ms, transform 130ms cubic-bezier(.16, 1, .3, 1) 40ms;
}
.scope.in .scope-cross { transform: scale(1); }
.scope.in .scope-cup { opacity: 1; }
.scope-zoom {
  position: absolute;
  left: calc(50% + min(26vh, 26vw));
  top: calc(50% + min(26vh, 26vw));
  font: 600 14px/1 'Rajdhani', 'Segoe UI', sans-serif;
  letter-spacing: 0.12em;
  color: rgba(230, 240, 245, 0.75);
}


/* --- The Horizon's rounds -------------------------------------------------
   Two pips, because the rifle holds two. Fire both and the weapon leaves
   your hands until you walk back to a sky-door roof for another. Cyan =
   your own kit; amber here would claim it was loot. Lives in the ammo slot,
   which is empty for exactly this weapon. */
.rounds {
  display: flex;
  gap: 5px;
  align-items: center;
  justify-content: flex-end;
  margin-left: auto;
}
.rounds.hidden { display: none; }
.rounds i {
  display: block;
  width: 7px;
  height: 17px;
  border-radius: 4px;
  background: var(--friendly, #4fd8ff);
  box-shadow: 0 0 7px rgba(79, 216, 255, 0.5);
  transition: background 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
}
.rounds i.spent {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px rgba(120, 140, 160, 0.5);
  opacity: 0.8;
}
.rounds.load i:not(.spent) { animation: round-load 140ms ease-out; }
@keyframes round-load { 0% { transform: scaleY(0.4); } 100% { transform: scaleY(1); } }
body.touch-mode .rounds i { width: 6px; height: 14px; }

/* Scoped: the glass owns the reticle, so the plain crosshair stands down. */
body.scoped .crosshair { display: none; }

/* The reticle. Every stroke is doubled — a pale core over a dark shadow —
   so it holds on a black street and inside a lamp pool alike. The centre is
   a GAP, not a cross: a gap cannot be lost against a bright light. */
.scope-cross { position: absolute; inset: 0; }
.scope-cross::before,
.scope-cross::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  background: rgba(236, 244, 250, 0.9);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
}
/* Four hairlines that stop short of the middle, drawn as two pairs. */
.scope-cross::before {
  width: 34vmin; height: 1px;
  transform: translate(-50%, -0.5px);
  -webkit-mask-image: linear-gradient(to right, #000 0 44%, transparent 44% 56%, #000 56% 100%);
  mask-image: linear-gradient(to right, #000 0 44%, transparent 44% 56%, #000 56% 100%);
}
.scope-cross::after {
  width: 1px; height: 34vmin;
  transform: translate(-0.5px, -50%);
  -webkit-mask-image: linear-gradient(to bottom, #000 0 44%, transparent 44% 56%, #000 56% 100%);
  mask-image: linear-gradient(to bottom, #000 0 44%, transparent 44% 56%, #000 56% 100%);
}
/* One cyan pixel: the aim point itself. */
.scope-dot {
  position: absolute;
  left: 50%; top: 50%;
  width: 3px; height: 3px;
  margin: -1.5px 0 0 -1.5px;
  border-radius: 50%;
  background: var(--friendly, #4fd8ff);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55);
}
/* The eyecup: the glass is a tube, not a hole punched in the screen. */
.scope-cup {
  opacity: 0;
  transition: opacity 140ms ease-out;
  position: absolute;
  left: 50%; top: 50%;
  width: min(78vh, 78vw);
  height: min(78vh, 78vw);
  margin-left: min(-39vh, -39vw);
  margin-top: min(-39vh, -39vw);
  border-radius: 50%;
  box-shadow: inset 0 0 60px 20px rgba(0, 0, 0, 0.55);
  pointer-events: none;
}


/* Each wheel step gets one tick of feedback: the power reads out and the
   reticle takes a single small breath. 80ms, linear, never a loop. */
.scope-zoom.tick { animation: zoom-tick 80ms linear; }
@keyframes zoom-tick { 50% { opacity: 0.35; transform: scale(1.12); } }
.scope-cross.tick { animation: cross-tick 80ms linear; }
@keyframes cross-tick { 50% { transform: scale(1.03); } }

/* Above six power the reticle drifts: holding a long shot is not a tripod.
   DOM only — the camera never moves, so this costs nothing and cannot make
   anyone motion sick. Off entirely in comfort mode. */
.scope.sway .scope-cross,
.scope.sway .scope-dot { animation: scope-breath 4.2s ease-in-out infinite; }
body.comfort .scope.sway .scope-cross,
body.comfort .scope.sway .scope-dot { animation: none; }
@keyframes scope-breath {
  0%, 100% { margin-left: 0; margin-top: 0; }
  25%      { margin-left: 2.5px; margin-top: -1.5px; }
  50%      { margin-left: 0.5px; margin-top: 2.5px; }
  75%      { margin-left: -2.5px; margin-top: -0.5px; }
}
@media (prefers-reduced-motion: reduce) {
  .scope-iris, .scope-cross, .scope-dot, .scope-cup { transition: none !important; }
  .scope.sway .scope-cross, .scope.sway .scope-dot { animation: none !important; }
}

/* --- The Horizon's target readout ----------------------------------------
   What the glass reads off whoever is under it. Everything here is already
   visible on the body at close range — the weapon in their hands, a raised
   shield, a winding Lance, the glow of someone carrying a fortune. A long
   lens is not an X-ray, so health is HEARTS rather than exact hit points:
   "two of three" is what decides whether one round finishes this, and the
   precise number is the server's business.

   Sits on the LEFT of the glass, opposite the magnification readout, so the
   two never fight and neither is under a thumb on a phone. */
.target-read {
  position: absolute;
  left: calc(50% - min(30vh, 30vw));
  top: 50%;
  transform: translate(-100%, -50%);
  min-width: 132px;
  padding: 9px 11px;
  border-left: 2px solid var(--danger, #ff5a3a);
  background: linear-gradient(90deg, rgba(10, 14, 19, 0.82), rgba(10, 14, 19, 0.36));
  font-family: 'Rajdhani', 'Segoe UI', sans-serif;
  color: #dfe9f2;
  pointer-events: none;
  animation: tr-in 110ms ease-out;
}
.target-read.hidden { display: none; }
@keyframes tr-in { from { opacity: 0; transform: translate(-100%, -50%) translateX(6px); } }

.target-read .tr-name {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.1;
  margin-bottom: 5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 190px;
}
/* Hearts, not a health bar: same unit the player reads on themselves. */
.target-read .tr-hearts { display: flex; gap: 3px; margin-bottom: 6px; }
.target-read .tr-hearts i {
  width: 13px;
  height: 5px;
  border-radius: 1px;
  background: var(--danger, #ff5a3a);
}
.target-read .tr-hearts i.out { background: rgba(120, 140, 160, 0.32); }

.target-read .tr-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  font-size: 12px;
  letter-spacing: 0.08em;
  line-height: 1.5;
}
.target-read .tr-row span { color: #7e90a0; }
.target-read .tr-row b { font-weight: 600; color: #dfe9f2; font-variant-numeric: tabular-nums; }

.target-read .tr-flags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
.target-read .tr-flags em {
  font-style: normal;
  font-size: 9.5px;
  letter-spacing: 0.1em;
  padding: 2px 5px;
  border: 1px solid currentColor;
  border-radius: 2px;
}
/* Colour language: cyan is friendly kit, amber is value, red is danger. */
.target-read .tr-flags em.sh { color: var(--friendly, #4fd8ff); }
.target-read .tr-flags em.ch { color: var(--danger, #ff5a3a); }
.target-read .tr-flags em.rich { color: var(--accent, #ffb648); }

/* The controls, on screen the whole time the RIFLE is held — not the whole
   time the glass is up. A line that only appears once you are already scoped
   cannot answer "how do I scope?", which is the question that was actually
   being asked (four times, 23 Aug). Sits above the scope overlay's z-index of
   7, so one element serves both states from the same spot and never jumps.

   Anchored to the BOTTOM rather than measured from the middle: from the
   centre it landed inside the portal panel on a 720-tall window — which the
   old scoped hint did too, unnoticed, because nobody ever saw it — and the
   shorter the window the worse it got. From the bottom it sits in the one
   band nothing else uses, above the module chips and below the portal
   readout. */
.weapon-keys {
  position: fixed;
  left: 50%;
  bottom: calc(62px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: 8;
  font: 600 10.5px/1 'Rajdhani', 'Segoe UI', sans-serif;
  letter-spacing: 0.16em;
  color: rgba(190, 205, 218, 0.5);
  white-space: nowrap;
  pointer-events: none;
}
.weapon-keys.hidden { display: none; }
/* The glyphs read brighter than the words beside them, so the line scans as
   "E — SCOPE" at a glance instead of as a sentence to be read. */
.weapon-keys b {
  font-weight: 700;
  color: rgba(226, 238, 248, 0.92);
}
/* One pulse the moment the rifle reaches your hands, then it sits quiet. The
   line is permanent now, so it must not keep asking for attention — but it
   does have to be noticed once. */
.weapon-keys.flash { animation: weapon-keys-flash 1.1s ease-out 1; }
@keyframes weapon-keys-flash {
  0%, 100% { color: rgba(190, 205, 218, 0.5); }
  18% { color: rgba(255, 182, 72, 0.95); }
  60% { color: rgba(255, 182, 72, 0.72); }
}

body.touch-mode .target-read {
  left: calc(50% - min(26vh, 26vw));
  min-width: 108px;
  padding: 7px 9px;
}
body.touch-mode .target-read .tr-name { font-size: 13px; max-width: 130px; }
/* Never drawn on a phone at all — see hud.setWeaponKeys. Belt and braces, so
   a stray class can never put a desktop-length glyph line across the touch
   controls. */
body.touch-mode .weapon-keys { display: none; }
