/* ============================================================================
   THE LAST PREY, "The Stronghold Wall"
   Every material here is the game's own UI kit: hazard tape, slate
   panels, X-head screws, olive enamel plaques, acid-green LEDs.

   Structure
     1  fonts
     2  animatable progress properties
     3  tokens
     4  reset + base + wall
     5  atmosphere (light, vignette, grain, mottle)
     6  materials (steel, tape, plaque, screws, LED)
     7  type
     8  motion primitives
     9  sprite engine
    10  chrome: boot, rail, drawer
    11  ACT I  , hero, brief
    12  ACT II , modes, dossiers, board, reaper
    13  ACT III, media, news, sign up
    14  footer
    15  dialogs
    16  sub-pages
    17  reduced motion
    18  responsive
   ========================================================================== */

/* ---------------------------------------------------------------- 1 fonts -- */

@font-face {
  font-family: 'VikingSquad';
  src: url('../fonts/vikingsquad.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: 'Big Shoulders Display';
  src: url('../fonts/bigshoulders-var.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;
}
@font-face {
  /* US Highway Gothic's direct descendant, road and hazard-sign lineage,
     which is the site's whole chrome metaphor, instead of Archivo's
     generic-grotesk neutrality. No italic file: the one <em> in the copy
     (index.html's sign-up h2) is `font-style:normal` in its own rule, never
     rendered slanted, and there is no italic body copy anywhere on the four
     pages (checked by grep). */
  font-family: 'Overpass';
  src: url('../fonts/overpass-var.woff2') format('woff2');
  font-weight: 300 900;
  font-display: swap;
}
@font-face {
  /* metric-adjusted so `font-display:swap`'s fallback-to-webfont swap does
     not reflow the page, tuned empirically (see PROTOCOL report) against a
     60-character line of --font-body copy rendered in each. */
  font-family: 'Overpass Fallback';
  src: local('Arial');
  /* measured empirically (Playwright, canvas + DOM): a 59-char line of
     --font-body copy at weight 500 rendered 467.44px wide in Overpass vs
     459.25px in Arial (Overpass ~1.8% wider), size-adjust closes that gap.
     ascent/descent read off canvas fontBoundingBoxAscent/Descent at 100px:
     Overpass 88/38 vs Arial's native 91/21; overriding Arial to Overpass's
     own ratios is what keeps the line box height (not just glyph width)
     from changing when the swap happens. */
  size-adjust: 101.8%;
  ascent-override: 88%;
  descent-override: 38%;
  line-gap-override: 0%;
}
@font-face {
  /* J2: Overpass's own U+00B7 (middot) has a measured zero advance width, so
     captions like "Co-Op · hold" collapse the dot onto the following word.
     Scope a fallback to just that one codepoint via unicode-range, the
     browser only reaches for this face when it needs U+00B7, so it patches
     the glyph everywhere Overpass is used in --font-body without touching
     any other character. */
  font-family: 'Overpass Middot Fix';
  /* Android has no Arial, give it Roboto/Helvetica too, any of which advance the dot. */
  src: local('Arial'), local('Helvetica Neue'), local('Helvetica'), local('Roboto');
  unicode-range: U+00B7;
}

/* ------------------------------------------- 2 animatable progress props -- */
/* Every scroll-linked effect reads one of these numbers. They are written by
   CSS scroll-driven keyframes where the browser supports them, and by the rAF
   fallback in js/motion.js where it does not, one set of visual rules, two
   drivers. Registering them makes them typed and interpolable. */

@property --descent {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

/* --------------------------------------------------------------- 3 tokens -- */

:root {
  /* ground */
  --void: #08090a;
  --slate-900: #121315;
  --slate-800: #1a1b1d;
  --slate-700: #232426;
  --slate-600: #34363a;

  /* ink */
  --bone: #d8d2c0;
  --bone-dim: #a5a08f; /* lifted from the brief's #8f8a7a to clear 4.5:1 on slate */
  --bone-faint: #9b968a; /* lifted to clear 4.5:1 on --slate-700 panels */

  /* signal */
  --acid: #b6d64a;
  --acid-deep: #3c4a17;
  --acid-glow: #d6f56b;
  --amber: #ffcc00;
  --alarm: #e50914;
  --alarm-lit: #ff5a5a;
  --blood: #7a1512;
  --toxic: #5be317;

  /* material */
  --hazard-y: #e8c41a;
  --hazard-k: #141414;
  --rust: #8a4b26;
  --oxide: #5c3a22;
  --olive: #4b5320;
  --olive-lit: #6b7a3a;
  --enamel-ink: #1c2508; /* type on the olive plaque, as the game sets it */

  /* faces */
  --font-display: 'VikingSquad', 'Big Shoulders Display', Impact, sans-serif;
  --font-stencil: 'Big Shoulders Display', 'Arial Narrow', sans-serif;
  --font-body: 'Overpass Middot Fix', 'Overpass', 'Overpass Fallback', 'Helvetica Neue', Arial, sans-serif;
  /* three faces only (site cut 2026-10-03): the old pixel and typewriter
     roles now fall back to the condensed label face and the body face */
  --font-data: var(--font-stencil);
  --font-type: var(--font-body);

  /* scale */
  --t-hero: clamp(4rem, 13vw, 11rem);
  --t-h2: clamp(2.4rem, 6vw, 4.5rem);
  --t-h3: clamp(1.4rem, 2.4vw, 1.9rem);
  --t-body: 1.0625rem;
  --t-label: 0.75rem;
  --t-data: 0.8125rem;

  /* rhythm */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;
  --s7: 48px;
  --s8: 64px;
  --s9: 96px;
  --s10: 128px;
  --s11: 192px;
  --wrap: 1240px;
  /* the worst section boundaries stack two of these (one section's bottom
     padding against the next section's top), landing at up to 200px of
     dead air between .brief/.dossiers, .board/.feeds and .feeds/.wire , 
     tightened from the original 64-100px range without flattening the
     rhythm the way Phase A's 28-48px did. */
  --pad-y: clamp(56px, 6vh, 84px);

  /* motion */
  --dur-tick: 90ms;
  --dur-fast: 160ms;
  --dur-base: 260ms;
  --dur-slow: 440ms;
  --dur-cine: 900ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-mech: cubic-bezier(0.2, 0.9, 0.1, 1);
  --ease-in: cubic-bezier(0.7, 0, 0.84, 0);

  /* film grain, 8 feTurbulence seeds, cycled at 8fps by @keyframes grain */
  --grain-1: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='4' seed='13' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23g)'/%3E%3C/svg%3E");
  --grain-2: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='4' seed='26' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23g)'/%3E%3C/svg%3E");
  --grain-3: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='4' seed='39' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23g)'/%3E%3C/svg%3E");
  --grain-4: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='4' seed='52' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23g)'/%3E%3C/svg%3E");
  --grain-5: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='4' seed='65' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23g)'/%3E%3C/svg%3E");
  --grain-6: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='4' seed='78' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23g)'/%3E%3C/svg%3E");
  --grain-7: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='4' seed='91' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23g)'/%3E%3C/svg%3E");
  --grain-8: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='4' seed='104' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23g)'/%3E%3C/svg%3E");

  /* organic soft-alpha reveal mask (blood bloom) */
  --splatter: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='s' x='-20%25' y='-20%25' width='140%25' height='140%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.012' numOctaves='4' seed='23' result='n'/%3E%3CfeDisplacementMap in='SourceGraphic' in2='n' scale='108' xChannelSelector='R' yChannelSelector='G'/%3E%3CfeGaussianBlur stdDeviation='6'/%3E%3C/filter%3E%3CradialGradient id='r'%3E%3Cstop offset='0' stop-color='%23fff'/%3E%3Cstop offset='.5' stop-color='%23fff'/%3E%3Cstop offset='1' stop-color='%23000'/%3E%3C/radialGradient%3E%3Crect width='300' height='300' fill='url(%23r)' filter='url(%23s)'/%3E%3C/svg%3E");

  /* the game's own frame backdrop, used at cover on every large panel.
     panel-slate.webp is NOT tileable (flat colour + a grunge alpha mask whose
     edges jump hard), so tiling it across a wide panel prints a visible grid;
     menu-bg.webp is the same surface at 1600x900 and needs no repeat. */
  --tex-panel: url('/play/assets/ui/menu-bg.webp');

  /* coarse wall mottling under the panels */
  --mottle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='420' height='420'%3E%3Cfilter id='d'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.014 .05' numOctaves='5' seed='9' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='420' height='420' filter='url(%23d)' opacity='.5'/%3E%3C/svg%3E");
}

/* ------------------------------------------------ 4 reset + base + wall --- */

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

html {
  /* clip on html ONLY, putting overflow on body makes body the scroller and
     breaks programmatic scrolling and every scroll-linked effect on the page */
  overflow-x: clip;
  scroll-behavior: smooth;
  scroll-padding-top: 96px;
  -webkit-text-size-adjust: 100%;
}
/* Mobile Chrome sizes the layout viewport to the content before html's clip applies, so the hero
   rubble and walking sprites made the 390px phone page 398px wide (8px sideways pan, fixed rail cut
   off at the right). clip (not hidden) on main is not a scroll container, so scroll-linked effects
   and sticky children keep working. */
main {
  overflow-x: clip;
}
/* reduced motion and the footer Motion switch: css/calm.css */

body {
  margin: 0;
  min-height: 100%;
  background: var(--void);
  color: var(--bone);
  font-family: var(--font-body);
  font-size: var(--t-body);
  /* Overpass at 400 reads thin on a dark ground (it's cut for dark-on-light
     road signage); 500 plus tighter leading than Archivo needed and no
     added tracking is the under-compensation, character over correction. */
  font-weight: 500;
  line-height: 1.65;
  letter-spacing: 0.005em;
  -webkit-font-smoothing: antialiased;
}

img,
svg,
video {
  display: block;
  max-width: 100%;
}
img {
  height: auto;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

a {
  color: var(--bone);
  text-decoration: none;
}

::selection {
  background: var(--acid-deep);
  color: var(--acid-glow);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.visually-hidden:focus {
  position: fixed;
  top: var(--s3);
  left: var(--s3);
  z-index: 400;
  width: auto;
  height: auto;
  margin: 0;
  padding: 10px 16px;
  clip-path: none;
  background: var(--acid);
  color: var(--void);
  font: 800 var(--t-label) / 1 var(--font-stencil);
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

/* the wall itself: dark steel, welded from plates, mottled, unevenly lit */
.wall {
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background:
    repeating-linear-gradient(
      90deg,
      transparent 0 118px,
      rgba(255, 255, 255, 0.014) 118px 119px,
      rgba(0, 0, 0, 0.5) 119px 121px,
      transparent 121px 240px
    ),
    repeating-linear-gradient(
      0deg,
      transparent 0 206px,
      rgba(255, 255, 255, 0.012) 206px 207px,
      rgba(0, 0, 0, 0.44) 207px 209px,
      transparent 209px 420px
    ),
    var(--void);
}
.wall::before {
  /* concrete/steel mottling, drifting slowly with descent */
  content: '';
  position: absolute;
  inset: -12% -4%;
  background-image: var(--mottle);
  background-size: 420px;
  opacity: 0.5;
  mix-blend-mode: overlay;
  transform: translate3d(0, calc(var(--descent) * -6vh), 0);
}
.wall::after {
  /* the dead-zone ground seen through the glass, parallaxed */
  content: '';
  position: absolute;
  inset: -20% -10%;
  background: url('/play/assets/floor.webp') center / 130% auto repeat;
  opacity: 0.32;
  filter: brightness(1.9) saturate(0.5) contrast(1) sepia(0.1);
  transform: translate3d(calc(var(--descent) * -3vw), calc(var(--descent) * 14vh), 0);
}

/* -------------------------------------------------------- 5 atmosphere ---- */

/* Each act is lit by one failing fixture. Content sits in the pool; the edges
   fall into the void. Act III is barely lit at all. */
.act {
  position: relative;
  isolation: isolate;
}
.act::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(
    var(--lamp-w, 120% 60%) at var(--lamp-x, 50%) var(--lamp-y, 0%),
    var(--lamp-c, rgba(107, 122, 58, 0.16)) 0%,
    transparent 68%
  );
}
.act-1 {
  --lamp-c: rgba(122, 134, 74, 0.2);
  --lamp-w: 130% 52%;
}
.act-2 {
  --lamp-c: rgba(84, 96, 104, 0.15);
  --lamp-w: 110% 40%;
  --lamp-y: 6%;
}
.act-3 {
  --lamp-c: rgba(122, 21, 18, 0.16);
  --lamp-w: 96% 36%;
  --lamp-y: 10%;
}

/* vignette + grain: one fixed layer each, both deepening with descent */
.vignette,
.grain {
  position: fixed;
  inset: 0;
  pointer-events: none;
}
.vignette {
  z-index: 190;
  background: radial-gradient(118% 88% at 50% 42%, transparent 32%, #000 100%);
  opacity: calc(0.48 + var(--descent) * 0.38);
}
.grain {
  z-index: 191;
  background-image: var(--grain-1);
  background-size: 240px;
  mix-blend-mode: overlay;
  opacity: calc(0.06 + var(--descent) * 0.05);
  animation: grain 1s steps(1, jump-none) infinite;
}
/* real film grain is chunky: advance the turbulence seed at 8fps, not 60 */
@keyframes grain {
  0% {
    background-image: var(--grain-1);
  }
  12.5% {
    background-image: var(--grain-2);
  }
  25% {
    background-image: var(--grain-3);
  }
  37.5% {
    background-image: var(--grain-4);
  }
  50% {
    background-image: var(--grain-5);
  }
  62.5% {
    background-image: var(--grain-6);
  }
  75% {
    background-image: var(--grain-7);
  }
  87.5% {
    background-image: var(--grain-8);
  }
}

/* --- the scroll drivers -----------------------------------------------------
   Native CSS scroll-driven animation writes --descent. Where the browser has
   no scroll timelines the @supports block is skipped and js/motion.js writes
   the same number from one rAF loop. */
@keyframes descend {
  to {
    --descent: 1;
  }
}
@supports (animation-timeline: scroll(root block)) {
  html {
    animation: descend linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 88%;
  }
}

/* ---------------------------------------------------------- 6 materials --- */

/* --- steel panel: slate fill, tiled game texture, bevelled, always bolted -- */
.steel {
  position: relative;
  background-color: var(--slate-700);
  background-image: var(--tex-panel);
  background-position: center;
  background-size: cover;
  background-blend-mode: overlay;
  border-radius: 3px;
  box-shadow:
    inset 1px 1px 0 var(--slate-600),
    inset -1px -1px 0 rgba(0, 0, 0, 0.9),
    inset 0 0 60px rgba(0, 0, 0, 0.6),
    0 18px 44px rgba(0, 0, 0, 0.55);
}

/* --- hazard tape: never a clean stripe. chewed edges + a dark wear pass ---- */
.tape {
  position: relative;
  height: 18px;
  background: repeating-linear-gradient(
    45deg,
    var(--hazard-y) 0 18px,
    var(--hazard-k) 18px 36px
  );
  filter: url('#chew');
}
.tape::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(rgba(0, 0, 0, 0.34), transparent 40%, rgba(0, 0, 0, 0.42)),
    var(--mottle);
  background-size: auto, 200px;
  mix-blend-mode: multiply;
  opacity: 0.6;
}

/* --- clamp housing: every button and CTA sits in one -----------------------
   plaque-clamp.webp is a bright olive enamel plate with iron clamps reaching
   past it, so two things matter: the type on the plate is dark (as the game
   sets it), and the artwork is never stretched. aspect-ratio pins the box to
   the file's own 335:154, and the em-based side padding leaves room for the
   ironwork, so the housing grows with its label instead of distorting. */
.clamp {
  display: inline-grid;
  place-items: center;
  aspect-ratio: 335 / 154;
  min-width: 150px;
  padding: 0 2.7em 0.35em;
  border: 0;
  background: url('/play/assets/ui/plaque-clamp.webp') center / 100% 100% no-repeat;
  font-family: var(--font-display);
  font-size: 1.02rem;
  line-height: 1;
  letter-spacing: 0.04em;
  text-align: center;
  white-space: nowrap;
  color: var(--enamel-ink);
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.32);
  filter: drop-shadow(0 5px 8px rgba(0, 0, 0, 0.6));
  cursor: pointer;
  transition:
    transform var(--dur-fast) var(--ease-mech),
    filter var(--dur-fast) linear;
}
.clamp:hover {
  filter: drop-shadow(0 5px 8px rgba(0, 0, 0, 0.6)) brightness(1.12);
}
.clamp:active {
  transform: translateY(2px);
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.7)) brightness(0.95);
}
.clamp:not(.clamp--primary):not(.clamp--rail) {
  /* the glass is ~53% of the plate; this keeps the label on it */
  padding-inline: 3.7em;
}
.clamp--primary {
  min-width: 210px;
  font-size: 1.44rem;
  letter-spacing: 0.05em;
}
.clamp--primary:hover {
  filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.6)) brightness(1.16)
    drop-shadow(0 0 22px rgba(182, 214, 74, 0.32));
}
.clamp--rail {
  min-width: 112px;
  font-size: 0.82rem;
  padding: 0 2.2em 0.3em;
}
/* polish item 1: the rail's one lit canister says Play Now; Sign up steps
   down beside it as the quiet ghost (Cult of the Lamb's one-loud-action
   header). Sized to sit level with the 40px mute button. */
.ghost--rail {
  padding: 13px 16px;
}
/* 404-only: "Back to the wall" is long enough that the base .clamp padding
   (sized for shorter labels like "Sign up") leaves the ink overhanging the
   plate art instead of sitting on it (A8). Wider inline padding grows the
   plate along with the box instead of touching the shared rail/drawer clamp. */
.clamp--404 {
  padding: 0 3.85em 0.35em;
}
/* J6: bottom padding lives here (not the inline style) so the ≤430 rule
   below can raise it without needing !important over an inline style */
.clamp[disabled] {
  cursor: wait;
  transform: none;
  filter: grayscale(0.8) brightness(0.66);
}

/* ghost button: bare metal, acid on hover */
.ghost {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 14px 22px;
  border: 1px solid var(--slate-600);
  border-radius: 2px;
  background: rgba(10, 11, 12, 0.66);
  font: 800 var(--t-label) / 1 var(--font-stencil);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bone);
  cursor: pointer;
  transition:
    border-color var(--dur-fast) linear,
    color var(--dur-fast) linear,
    transform var(--dur-fast) var(--ease-mech);
}
.ghost:hover {
  border-color: var(--acid);
  color: var(--acid-glow);
}
.ghost:active {
  transform: translateY(2px);
}

/* --- LED: the game's own indicator, with a real bloom ---------------------- */
.led {
  flex: none;
  width: 15px;
  height: 15px;
  background: url('/play/assets/ui/led-dark.webp') center / contain no-repeat;
}
.led[data-state='on'] {
  background-image: url('/play/assets/ui/led-green.webp');
  filter: drop-shadow(0 0 5px rgba(182, 214, 74, 0.85));
}
.led[data-state='warn'] {
  background-image: url('/play/assets/ui/led-green.webp');
  filter: sepia(1) saturate(5.5) hue-rotate(-6deg) brightness(1.18)
    drop-shadow(0 0 6px rgba(255, 204, 0, 0.75));
}
.led[data-state='alarm'] {
  background-image: url('/play/assets/ui/led-green.webp');
  filter: sepia(1) saturate(9) hue-rotate(-34deg) brightness(0.92)
    drop-shadow(0 0 7px rgba(229, 9, 20, 0.85));
}

/* --- live presence badge (Ren's list item 12): reuses .players/.led -------- */
.players .led {
  display: inline-block;
  vertical-align: -2px;
  margin-right: 8px;
}
.players-head {
  display: inline-block;
}
.players .n {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  font-size: 0.92em;
  letter-spacing: 0;
  display: inline-block;
}
@media (max-width: 479px) {
  /* Below 480px `.players`' own min-width pin (js/site.js's pinPlaqueWidth,
     same breakpoint) is wider than the viewport itself down here. Instead
     the head goes on its own line. The four states' head strings are
     different lengths ("Live now" vs "7 survivors in the zone") but all
     comfortably fit one line at this width, so putting the head on its own
     row means every state uses exactly one line for it, then the fixed
     tail text ("· online · endless", identical in every
     state) wraps the same way every time below it. That is what keeps
     .players' and the paragraph's rects identical across the four states
     at this width, same width (the .mode .players box still tops out at
     its container's max-width:100%, which the tail alone exceeds and wraps
     against), same line count, same height, regardless of which state is
     showing.
     */
  /* [data-presence] bumps specificity above the unconditional `.mode
     .players { padding: 5px 12px; }` rule further down the file (same
     selector, later in source order, which would
     otherwise win regardless of this media query and put the LED right
     back on top of the text). */
  .mode-panel .players[data-presence] {
    position: relative;
    padding-left: 35px;
  }
  .players .led {
    position: absolute;
    left: 12px;
    top: 7px;
    margin-right: 0;
    vertical-align: 0;
  }
  .players-head {
    display: block;
  }
}
@keyframes presence-idle {
  from {
    opacity: 1;
    filter: sepia(1) saturate(5.5) hue-rotate(-6deg) brightness(1.18)
      drop-shadow(0 0 6px rgba(255, 204, 0, 0.75));
  }
  to {
    opacity: 0.55;
    filter: sepia(1) saturate(5.5) hue-rotate(-6deg) brightness(1.18)
      drop-shadow(0 0 2px rgba(255, 204, 0, 0.35));
  }
}
.players .led[data-state='warn'] {
  animation: presence-idle 3.2s ease-in-out infinite alternate;
}

/* --- lightbox frame: plain bezel, the image at full colour --------------- */
.viewer-frame {
  position: relative;
  display: block;
  overflow: hidden;
  padding: 8px;
  border-radius: 4px;
  background: #111214;
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.6);
}

/* --- draped cable + cracked glass, straight from the game's kit ---------- */
.hose {
  position: absolute;
  z-index: 6;
  width: 122px;
  pointer-events: none;
  opacity: 0.95;
  filter: drop-shadow(0 4px 5px rgba(0, 0, 0, 0.7));
}

/* ------------------------------------------------------------- 7 type ----- */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: clamp(16px, 4vw, 32px);
}
/* big screens (site polish 17): from 1680 up the page grid and the type grow
   together, continuous at the breakpoint (74vw is the old 1240 wrap there,
   0.9524vw is 16px), topping out at a 1464 wrap and an 18px rem by 1920.
   max(100%, ...) so a visitor's larger default font size always wins.
   Nothing changes below 1680. */
@media (min-width: 1680px) {
  :root {
    --wrap: min(1464px, 74vw);
  }
  html {
    font-size: max(100%, min(112.5%, 0.9524vw));
  }
}

.label {
  display: block;
  font: 800 var(--t-label) / 1.2 var(--font-stencil);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--acid);
}
.label--dim {
  color: var(--bone-dim);
}

.data {
  font: 400 var(--t-data) / 1.5 var(--font-data);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  color: var(--bone-dim);
}
.data b {
  font-weight: 600;
  color: var(--bone);
}

.lede {
  max-width: 34ch;
  font-size: clamp(1.2rem, 2.1vw, 1.55rem);
  line-height: 1.42;
  /* lead copy stays at body's pre-Overpass weight so the larger size does
     the work of hierarchy, bumping both size AND weight the way the body
     default now does would flatten lede against its own surrounding prose */
  font-weight: 400;
  color: var(--bone);
}

.prose {
  /* 68ch was tuned for Archivo's narrower advance widths; Overpass runs
     wider at the same size, so 64ch keeps the measure in the same
     comfortable reading range instead of running long */
  max-width: 64ch;
}
.prose p {
  margin: 0 0 1.05em;
}
.prose p:last-child {
  margin-bottom: 0;
}
.prose strong {
  color: var(--amber);
  font-weight: 700;
}

h1,
h2,
h3 {
  font-weight: 400;
  text-wrap: balance;
}
h3 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--t-h3);
  line-height: 1.05;
  letter-spacing: 0.02em;
  color: var(--bone);
}

/* section head: a plaque, a stencil kicker, and a failing lamp above it */
.head {
  position: relative;
  text-align: center;
  padding-block: var(--s6) var(--s7);
}
.head .label {
  margin-bottom: var(--s4);
}
.head .sub {
  max-width: 58ch;
  margin: var(--s5) auto 0;
  font-weight: 400;
  color: var(--bone-dim);
}
.lamp {
  position: absolute;
  top: -18px;
  left: 50%;
  width: min(560px, 84%);
  height: 210px;
  transform: translateX(-50%);
  pointer-events: none;
  background: radial-gradient(58% 100% at 50% 0%, rgba(226, 236, 190, 0.16), transparent 72%);
  animation: flicker 7.4s linear infinite;
}
/* a failing fluorescent: uneven percentages, never a smooth sine */
@keyframes flicker {
  0%,
  100% {
    opacity: 1;
  }
  3% {
    opacity: 0.42;
  }
  4% {
    opacity: 0.95;
  }
  7% {
    opacity: 0.68;
  }
  8% {
    opacity: 1;
  }
  41% {
    opacity: 1;
  }
  42% {
    opacity: 0.28;
  }
  43% {
    opacity: 0.9;
  }
  44% {
    opacity: 0.5;
  }
  46% {
    opacity: 1;
  }
  77% {
    opacity: 1;
  }
  78% {
    opacity: 0.62;
  }
  79% {
    opacity: 1;
  }
}

/* links: an acid underline that wipes in from the left, never a fade */
.link,
.prose a,
.head .sub a,
.wire-body p a,
.foot-fine a {
  color: var(--acid);
  background-image: linear-gradient(var(--acid), var(--acid));
  background-size: 0 1px;
  background-position: 0 100%;
  background-repeat: no-repeat;
  transition: background-size var(--dur-base) var(--ease-out);
}
.link:hover,
.prose a:hover,
.head .sub a:hover,
.wire-body p a:hover,
.foot-fine a:hover {
  background-size: 100% 1px;
}

/* focus: this world has indicator lights. every one of them is visible. */
:focus-visible {
  outline: 2px solid var(--acid);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px rgba(8, 9, 10, 0.95), 0 0 16px rgba(182, 214, 74, 0.4);
  border-radius: 2px;
}

/* --------------------------------------------------- 8 motion primitives -- */

/* 3 · stencil decode, JS scrambles the glyphs; CSS keeps the box steady */
.decode {
  font-variant-ligatures: none;
}

/* 4 · plate slam: drop, overshoot 4px, settle, one-frame dust puff */
@keyframes slam {
  0% {
    opacity: 0;
    transform: translate3d(0, -18px, 0);
  }
  62% {
    opacity: 1;
    transform: translate3d(0, 4px, 0);
  }
  100% {
    opacity: 1;
    transform: translate3d(0, 0, 0);
  }
}
.js-anim .slam {
  opacity: 0;
}
.js-anim .slam.in {
  animation: slam var(--dur-slow) var(--ease-mech) both;
  animation-delay: calc(min(var(--i, 0), 8) * 40ms);
}
@keyframes puff {
  0% {
    opacity: 0.9;
    transform: scaleY(0.4);
  }
  100% {
    opacity: 0;
    transform: scaleY(1.5) translateY(-8px);
  }
}

/* 6 · threat pulse, amplitude scales with the enemy's tier. It rides an
   overlay rather than the frame itself so it can coexist with the rattle. */
@keyframes threat {
  0%,
  100% {
    box-shadow: inset 0 0 0 rgba(229, 9, 20, 0);
  }
  50% {
    box-shadow:
      inset 0 0 calc(10px + var(--amp, 1) * 18px)
        rgba(229, 9, 20, calc(0.14 + var(--amp, 1) * 0.32)),
      0 0 calc(6px + var(--amp, 1) * 14px) rgba(229, 9, 20, calc(0.1 + var(--amp, 1) * 0.22));
  }
}

/* 8 · blood bloom, soft organic reveal (clip-path cannot do soft edges) */
.js-anim .bloom {
  mask-image: var(--splatter);
  mask-repeat: no-repeat;
  mask-position: center;
  mask-size: 0% 0%;
}
.js-anim .bloom.in {
  animation: bloom var(--dur-cine) var(--ease-out) both;
}
@keyframes bloom {
  to {
    mask-size: 280% 280%;
  }
}

/* 11 · signal loss, RGB split burst at act boundaries, max 3 per page */
.split-r,
.split-c {
  position: absolute;
  inset: 0;
  z-index: 60;
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: screen;
}
.split-r {
  background: rgba(229, 9, 20, 0.5);
}
.split-c {
  background: rgba(74, 214, 214, 0.44);
}
.fire > .split-r,
.fire > .split-c {
  animation: split-burst 260ms steps(4) 1;
}
.fire > .split-c {
  animation-direction: reverse;
}
@keyframes split-burst {
  0% {
    opacity: 0.6;
    transform: translate3d(-7px, 2px, 0);
    clip-path: inset(18% 0 62% 0);
  }
  33% {
    opacity: 0.5;
    transform: translate3d(6px, -3px, 0);
    clip-path: inset(58% 0 22% 0);
  }
  66% {
    opacity: 0.45;
    transform: translate3d(-4px, 1px, 0);
    clip-path: inset(74% 0 8% 0);
  }
  100% {
    opacity: 0;
    transform: none;
    clip-path: inset(0);
  }
}

/* 12 · spotlight, cursor-tracked light through the veil over key art.
   The veil is a dark layer with a hole punched in it and bleeds past the
   element's edges, so nothing is ever clipped to a hard rectangle. */
.spot {
  --mx: 50%;
  --my: 46%;
}
.veil {
  position: absolute;
  inset: -14%;
  z-index: 2;
  pointer-events: none;
  background: rgba(3, 4, 5, 0.88);
  mask-image: radial-gradient(
    circle var(--spot-r, 300px) at var(--mx) var(--my),
    transparent 0%,
    rgba(0, 0, 0, 0.4) 44%,
    #000 78%
  );
  mask-repeat: no-repeat;
}

/* 13 · magnetic pull, CTA leans toward the cursor, springs back */
.magnet {
  --mag-x: 0px;
  --mag-y: 0px;
}

/* 14 · tape peel, redacted lore wipes open on scroll */
.peel {
  position: relative;
  display: block;
}
.js-anim .peel::before {
  content: '';
  position: absolute;
  inset: -2px -5px;
  background: repeating-linear-gradient(-12deg, #3a3d42 0 7px, #101113 7px 14px);
  clip-path: inset(0 0 0 0);
  transition: clip-path var(--dur-slow) var(--ease-mech);
  transition-delay: calc(var(--i, 0) * 110ms);
}
.js-anim .peel.in::before {
  clip-path: inset(0 0 0 100%);
}

.count {
  font-variant-numeric: tabular-nums;
}

/* generic view-triggered reveal, where a slam would be too heavy */
.js-anim .rise {
  opacity: 0;
  transform: translate3d(0, 22px, 0);
  transition:
    opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
  transition-delay: calc(min(var(--i, 0), 8) * 40ms);
}
.js-anim .rise.in {
  opacity: 1;
  transform: none;
}

/* ------------------------------------------------------- 9 sprite engine -- */

/* Every sprite is a game strip played with steps(). Geometry lives in CSS so
   the box is reserved before first paint; js/motion.js only fills in --sheet
   once the element nears the viewport (sheets are background-images, so
   loading="lazy" does not apply to them). */
.sp-anchor {
  position: absolute;
  width: 0;
  height: 0;
  pointer-events: none;
}
/* contact shadow at the pivot, without one, every sprite floats. --sh is the
   pool's width; override it inline wherever the sprite is much bigger. */
.sp-anchor::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 0;
  width: var(--sh, 72px);
  height: calc(var(--sh, 72px) * 0.3);
  transform: translate(-50%, -44%);
  border-radius: 50%;
  background: radial-gradient(ellipse, rgba(0, 0, 0, 0.6), transparent 70%);
}
.sprite {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--fw);
  height: var(--fh);
  margin-left: calc(var(--fw) / -2);
  margin-top: calc(var(--pv) * -1);
  transform-origin: calc(var(--fw) / 2) var(--pv);
  transform: rotate(var(--rot, 0deg)) scale(var(--scale, 1));
  background-image: var(--sheet, none);
  background-repeat: no-repeat;
  background-size: auto;
  image-rendering: auto; /* painted art, never pixelated */
  animation: sp-play calc(var(--frames) * var(--fms, 92ms)) steps(var(--frames)) infinite;
}
@keyframes sp-play {
  to {
    background-position-x: calc(var(--fw) * var(--frames) * -1);
  }
}
/* a one-shot (Kyle's shot, a death) gets its own keyframes so swapping it in
   restarts from frame 1 rather than inheriting the loop's elapsed time, and
   jump-none ends ON the last frame, not one frame past the strip (blank). */
.sprite[data-once] {
  animation: sp-once calc(var(--frames) * var(--fms, 92ms)) steps(var(--frames), jump-none) 1 forwards;
}
@keyframes sp-once {
  to {
    background-position-x: calc(var(--fw) * (var(--frames) - 1) * -1);
  }
}
.sprite.paused {
  animation-play-state: paused;
}

/* a zombie that is not going anywhere does not walk on the spot. Every walk
   sheet that stays put (hero, roster, character art), and a scroll
   walker once the scroll stops, holds frame 0, the feet-together pass on
   all eleven zombie strips (measured: the narrowest stride of each cycle),
   and only sways and breathes, with a twitch once a cycle. Longhands, so
   .paused still parks it offscreen. js/motion.js desyncs each one with
   --still-at/--still-dur. An attack or death sheet swapped in still plays
   sp-play, because the selector keys on the sheet. */
.sprite[data-sheet$='_walk']:not(.z-walker .sprite){
  animation-name: z-still;
  animation-duration: var(--still-dur, 8.2s);
  animation-timing-function: ease-in-out;
  animation-delay: var(--still-at, 0s);
  animation-iteration-count: infinite;
  animation-direction: normal;
}
/* a slow rock (about 3s each way at 8.2s) with the shoulders swelling on
   each lean, then one sharp twitch: ~120ms up with a fast start, ~200ms
   back, and it settles. A smooth half-second nod read as a shrug. */
@keyframes z-still {
  0%,
  100% {
    transform: rotate(calc(var(--rot, 0deg) - 1.2deg)) scale(var(--scale, 1));
  }
  18% {
    transform: rotate(calc(var(--rot, 0deg) + 1.5deg)) scale(calc(var(--scale, 1) * 1.018));
  }
  36% {
    transform: rotate(calc(var(--rot, 0deg) - 1.5deg)) scale(var(--scale, 1));
  }
  54% {
    transform: rotate(calc(var(--rot, 0deg) + 1.3deg)) scale(calc(var(--scale, 1) * 1.018));
  }
  73% {
    transform: rotate(calc(var(--rot, 0deg) - 0.4deg)) scale(var(--scale, 1));
    animation-timing-function: cubic-bezier(0.1, 0.9, 0.3, 1);
  }
  74.5% {
    transform: rotate(calc(var(--rot, 0deg) + 2.8deg)) scale(calc(var(--scale, 1) * 1.012));
    animation-timing-function: ease-out;
  }
  77% {
    transform: rotate(calc(var(--rot, 0deg) - 0.9deg)) scale(var(--scale, 1));
  }
  81% {
    transform: rotate(calc(var(--rot, 0deg) - 0.6deg)) scale(var(--scale, 1));
  }
}

/* the game's uneven loop: grind, brace, the tear held, the chew. Frame
   holds are DormantPose's FEED_HOLD_MS (1820 ms a loop), each feeder
   desynced by the same --still-at js/motion.js gives every sprite. */
.sprite[data-sheet$='_feed'] {
  animation: sp-feed 1820ms step-end infinite;
  animation-delay: var(--still-at, 0s);
}
@keyframes sp-feed {
  0% {
    background-position-x: 0;
  }
  17.58% {
    background-position-x: calc(var(--fw) * -1);
  }
  30.22% {
    background-position-x: calc(var(--fw) * -2);
  }
  39.56% {
    background-position-x: calc(var(--fw) * -3);
  }
  46.15% {
    background-position-x: calc(var(--fw) * -4);
  }
  51.1% {
    background-position-x: calc(var(--fw) * -5);
  }
  65.93% {
    background-position-x: calc(var(--fw) * -6);
  }
  74.18% {
    background-position-x: calc(var(--fw) * -7);
  }
  85.71%,
  100% {
    background-position-x: calc(var(--fw) * -8);
  }
}

/* spacing kept from the old walker placements, the layout depends on it */
@media (max-width: 1080px) {
  .signup-grid .plate {
    margin-bottom: 80px;
  }
}
@media (max-width: 640px) {
  .foot-grid {
    padding-bottom: 88px;
  }
}

/* ------------------------------------------- 10 chrome: rail, drawer, cursor */

/* nav: a bolted steel rail, not a floating pill.
   The rail is fixed with a spacer holding its place in flow, and its box stays
   a constant 84px. Compressing on scroll clips the painted plate up to 66px,
   slides the hazard strip in at the new bottom edge, and scales the brand , 
   all clip-path and transform, so nothing below the rail ever reflows. */
.rail {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 180;
  height: 84px;
}
.rail-spacer {
  height: 84px;
}
.rail-plate {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(rgba(10, 11, 12, 0.9), rgba(10, 11, 12, 0.97)),
    var(--tex-panel) center / cover;
  box-shadow:
    inset 0 -1px 0 rgba(255, 255, 255, 0.05),
    0 1px 0 #000,
    0 10px 26px rgba(0, 0, 0, 0.6);
  clip-path: inset(0 0 var(--cut, 0px) 0);
  transition: clip-path var(--dur-base) var(--ease-out);
}
.rail.compact {
  --cut: 18px;
}
.rail-inner {
  position: relative;
  display: flex;
  align-items: center;
  gap: clamp(10px, 2.2vw, 26px);
  height: 84px;
  transform: translateY(calc(var(--cut, 0px) / -2));
  transition: transform var(--dur-base) var(--ease-out);
}
.rail-tape {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 8px;
  overflow: hidden;
  transform-origin: 50% 100%;
  transform: translateY(calc(var(--cut, 0px) * -1)) scaleY(var(--tape-on, 0));
  transition: transform var(--dur-base) var(--ease-out);
}
.rail.compact .rail-tape {
  --tape-on: 1;
}
.rail-tape .tape {
  height: 8px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 14px;
  flex: none;
  transform-origin: 0 50%;
  transform: scale(var(--brand-s, 1));
  transition: transform var(--dur-base) var(--ease-out);
}
.rail.compact .brand {
  --brand-s: 0.6;
}
/* the eyebrow's own font-size scales down with --brand-s too (it inherits
   the .brand transform), and 12px * 0.6 renders at an unreadable ~7px ,
   bump the source size so the compact rail still shows real letterforms */
.rail.compact .brand-eyebrow {
  font-size: 15px;
}
/* the crest: a free roundel hanging over the hazard tape, not boxed in a
   slate plate. 96px base; the compact rail scales the whole .brand unit via
   --brand-s (58px) and cancels the hang so it sits fully inside the rail. */
.brand-crest {
  position: relative;
  display: block;
  width: 96px;
  height: 96px;
  flex: none;
  margin-bottom: -26px;
  border-radius: 50%;
  background: #0b0b0c;
  box-shadow:
    0 0 0 3px #0b0b0c,
    0 0 0 5px var(--amber),
    0 0 0 7px #0b0b0c,
    0 10px 18px rgba(0, 0, 0, 0.85);
  transition:
    transform 160ms var(--ease-out),
    box-shadow 160ms var(--ease-out);
}
.brand-crest img {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  display: block;
}
.rail.compact .brand-crest {
  margin-bottom: 0;
}
.brand:hover .brand-crest,
.brand:focus-visible .brand-crest {
  transform: rotate(-4deg);
  box-shadow:
    0 0 0 3px #0b0b0c,
    0 0 0 5px var(--alarm),
    0 0 0 7px #0b0b0c,
    0 10px 18px rgba(0, 0, 0, 0.85);
}
.brand-wm {
  display: flex;
  flex-direction: column;
  line-height: 0.86;
  white-space: nowrap;
}
.brand-eyebrow {
  font-family: var(--font-stencil);
  font-weight: 800;
  font-size: 12px;
  line-height: 1;
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--amber);
  margin-bottom: 4px;
}
.brand-prey {
  font-family: var(--font-display);
  font-size: 44px;
  letter-spacing: 0.03em;
  color: var(--bone);
  text-shadow:
    3px 3px 0 var(--alarm),
    5px 5px 0 #000;
}
/* footer keeps the same crest-and-wordmark lockup, smaller and flush (no
   hang, the footer has no tape seam to hang over) */
.wall-foot .brand-crest {
  width: 72px;
  height: 72px;
  margin-bottom: 0;
}

.rail-nav {
  position: relative;
  flex: 1;
  min-width: 0;
}
.rail-links {
  position: relative;
  display: flex;
  gap: clamp(4px, 1.2vw, 18px);
  margin: 0;
  padding: 0;
  list-style: none;
}
.rail-links a {
  display: block;
  padding: 11px 6px;
  font: 800 var(--t-label) / 1 var(--font-stencil);
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--bone-dim);
  white-space: nowrap;
  transition: color var(--dur-fast) linear;
}
.rail-links a:hover,
.rail-links a[aria-current] {
  color: var(--bone);
}
/* the acid underline slides between items */
.rail-slide {
  position: absolute;
  bottom: 0;
  left: 0;
  height: 2px;
  width: var(--slide-w, 0px);
  transform: translateX(var(--slide-x, 0px));
  background: var(--acid);
  box-shadow: 0 0 8px rgba(182, 214, 74, 0.75);
  opacity: 0;
  transition:
    transform var(--dur-base) var(--ease-mech),
    width var(--dur-base) var(--ease-mech),
    opacity var(--dur-fast) linear;
}
.rail-slide.show {
  opacity: 1;
}

.rail-right {
  display: flex;
  align-items: center;
  gap: clamp(8px, 1.2vw, 14px);
  flex: none;
}

/* The always-on server chip and the scroll-depth "infection" meter were deleted:
   a status that is permanently green and a gauge that reports scroll position are
   fabricated instruments, not information. Server state now speaks only on failure
   (see .foot-signal.down); scroll position is the scrollbar's job. */

.burger {
  display: none;
  padding: 12px 12px;
  border: 1px solid var(--slate-600);
  border-radius: 2px;
  background: rgba(8, 9, 10, 0.7);
  font: 800 var(--t-label) / 1 var(--font-stencil);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
}

/* sound toggle, off by default, choice persisted, never autoplays */
.sound-btn {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--slate-600);
  border-radius: 2px;
  background: rgba(8, 9, 10, 0.7);
  color: var(--bone-faint);
  cursor: pointer;
}
.sound-btn:hover {
  color: var(--acid);
  border-color: rgba(182, 214, 74, 0.4);
}
.sound-btn[aria-pressed='true'] {
  color: var(--acid);
  border-color: var(--acid);
  box-shadow: inset 0 0 12px rgba(182, 214, 74, 0.2);
}
.sound-btn svg {
  width: 18px;
  height: 18px;
  fill: currentColor;
}
.sound-btn[aria-pressed='false'] .snd-on,
.sound-btn[aria-pressed='true'] .snd-off {
  display: none;
}

/* mobile drawer */
.drawer {
  position: fixed;
  inset: 0;
  z-index: 260;
  display: none;
  flex-direction: column;
  gap: var(--s3);
  padding: clamp(20px, 7vw, 56px);
  background:
    linear-gradient(rgba(6, 7, 8, 0.97), rgba(6, 7, 8, 0.99)),
    var(--tex-panel) center / cover;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.drawer.open {
  display: flex;
}
.drawer > a:not(.clamp) {
  padding: 14px 4px;
  border-bottom: 1px solid #1b1c1e;
  font-family: var(--font-display);
  font-size: 1.5rem;
  letter-spacing: 0.04em;
  color: var(--bone);
}
.drawer > a:not(.clamp):hover {
  color: var(--acid);
}
.drawer-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
  margin-top: var(--s4);
}
.drawer-close {
  align-self: flex-end;
  width: 44px;
  height: 44px;
  border: 0;
  background: url('/play/assets/ui/btn-close.webp') center / contain no-repeat;
  cursor: pointer;
  font-size: 0;
}

/* ========================================================= 11 ACT I ======= */

/* --- hero: full viewport, built in depth layers ------------------------- */
.hero {
  position: relative;
  min-height: min(100svh, 1000px);
  display: grid;
  align-content: end;
  overflow: hidden;
  padding-bottom: clamp(104px, 15vh, 190px);
  isolation: isolate;
}
/* 9 · depth drift, four layers, four rates */
@keyframes pan {
  to {
    background-position: 62% 52%;
  }
}

/* desktop (site polish 18): the far walker steps down out of the top corner
   into the empty right half of the floor, same size, still the furthest of
   the three closing on Kyle */

.hero-inner {
  position: relative;
  z-index: 4;
  /* grid items default to min-width:auto; without this the CTA's min-width
     forces the whole column wider than a narrow viewport and clips the copy */
  min-width: 0;
}
.hero h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--t-hero);
  line-height: 0.68;
  letter-spacing: -0.02em;
  color: var(--bone);
  text-shadow: 0 0 60px rgba(0, 0, 0, 0.95), 0 6px 0 rgba(0, 0, 0, 0.55);
}
.hero h1 span {
  display: block;
}
.hero h1 .l2 {
  color: var(--acid);
  text-shadow:
    0 0 70px rgba(0, 0, 0, 0.95),
    0 0 44px rgba(182, 214, 74, 0.22),
    0 6px 0 rgba(0, 0, 0, 0.55);
}
.hero-tag {
  max-width: 46ch;
  margin: var(--s5) 0 var(--s7);
  font-size: clamp(1.05rem, 1.6vw, 1.28rem);
  font-weight: 400;
  color: var(--bone);
}
.hero-tag strong {
  color: var(--amber);
  font-weight: 700;
}
.hero-cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s5);
}

/* scroll cue: a cable running off the bottom edge, not a chevron */
.cue {
  position: absolute;
  z-index: 5;
  left: 50%;
  bottom: 0;
  width: 66px;
  height: clamp(74px, 12vh, 122px);
  transform: translateX(-50%);
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--bone-faint);
  transition: opacity var(--dur-slow) linear;
}
.cue svg {
  width: 100%;
  height: 100%;
  overflow: visible;
}
.cue path {
  fill: none;
  stroke: #2b2f36;
  stroke-width: 7;
  stroke-linecap: round;
}
.cue path + path {
  stroke: #454c3c;
  stroke-width: 3;
}
.cue circle {
  fill: var(--acid);
  filter: drop-shadow(0 0 6px rgba(182, 214, 74, 0.7));
}
.cue g {
  transform-origin: 50% 0;
  animation: sway 5.6s ease-in-out infinite;
}
@keyframes sway {
  0%,
  100% {
    transform: rotate(-2.4deg);
  }
  50% {
    transform: rotate(2.4deg);
  }
}
.cue.gone {
  opacity: 0;
  pointer-events: none;
}
.cue span {
  position: absolute;
  left: 50%;
  bottom: 1px;
  transform: translateX(-50%);
  font: 800 0.75rem / 1 var(--font-stencil);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* 7 · muzzle flash, the CTA hover that sells the whole game */
@keyframes flash {
  0% {
    opacity: 1;
    transform: scale(0.68);
  }
  50% {
    opacity: 0.68;
    transform: scale(1.04);
  }
  100% {
    opacity: 0;
    transform: scale(1.2);
  }
}
/* the button recoils with the shot */
.hero.firing .clamp--primary {
  transform: translateX(2px);
}

/* --- brief / outbreak ---------------------------------------------------- */
.brief {
  position: relative;
  padding-block: var(--pad-y);
}
/* clipboard plate with a bulldog clip */
.clipboard {
  position: relative;
  isolation: isolate;
  padding: clamp(40px, 5vw, 58px) clamp(24px, 3.4vw, 44px) clamp(28px, 3.4vw, 40px);
  background-color: #2a2822;
  background-image: var(--tex-panel);
  background-position: center;
  background-size: cover;
  background-blend-mode: soft-light;
  border-radius: 3px;
  box-shadow:
    inset 1px 1px 0 #45423a,
    inset -1px -1px 0 rgba(0, 0, 0, 0.9),
    inset 0 0 70px rgba(0, 0, 0, 0.55),
    0 20px 46px rgba(0, 0, 0, 0.6);
}
.clipboard::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: 3px;
  background: var(--mottle) 0 0 / 300px;
  mix-blend-mode: overlay;
  opacity: 0.32;
}
.clipboard .prose,
.clipboard .lede {
  position: relative;
}
.clipboard .lede {
  margin: 0 0 var(--s5);
}

/* ========================================================= 12 ACT II ====== */

/* --- 4 · modes: sticky-pinned horizontal scroll -------------------------- */
.modes {
  position: relative;
  /* offering count drives every track dimension below, so the pin-scroll math
     is never hardcoded to a panel count again (design-system.md §6.3). */
  --panels: 2;
  /* polish item 2: a full viewport of pinned runway per panel (62vh left
     only ~216px at 1440x900, so Survival was never seen whole at rest) */
  height: calc(var(--panels) * 100vh);
}
/* wraps .modes-track so mobile's horizontal scrolling can live on this
   element instead of on .modes-pin, .modes-pin also holds .belt as a
   sibling, and needs to shed non-visible overflow at mobile so .belt's
   sticky positioning can reference the page's own scroll rather than a
   non-scrolling ancestor (see the mobile media queries below). Plain
   pass-through box otherwise: same width/height .modes-track always read
   through .modes-pin directly, so desktop's percentage math (100%, and
   .modes-track's own calc(var(--panels)*100%)) still resolves to the same
   pixel values as before. */
.mode {
  position: relative;
  flex: 0 0 calc(100% / var(--panels));
  display: grid;
  /* D2 fix: .mode-head used to be position:absolute, floating independent of
     the in-flow heading stack below it, at 1440x900 the kicker's clamped
     `top` landed 11px into the bottom-packed h3, viewport-HEIGHT dependent
     since one side used vh and the other used align-content:end against the
     panel's real content height. Both now live in ONE grid instead: kicker
     in row 1 (auto), copy in row 3 (auto, bottom-anchored by sitting last),
     and the middle `1fr` row is the only thing that can grow or shrink , 
     it absorbs whatever slack the card has left over the two content rows,
     down to 0. That reproduces the original composition (kicker pinned top,
     title/badge/paragraph pinned bottom, art breathing in between) while
     keeping the guarantee: if the copy ever grows tall enough to eat the
     whole middle row, the two content rows can touch but the grid can never
     lay them on top of each other, unlike the old independent absolute +
     align-content:end pairing. */
  grid-template-rows: auto 1fr auto;
  align-content: stretch;
  overflow: hidden;
  /* the fixed rail owns the top 84px; keep the panel's own chrome clear of it , 
     this same reserved band is now also the kicker's clearance from the rail */
  /* A2 fix: .mode is a full-bleed 100vw flex item (not wrapped in `.wrap`),
     so its own inline padding IS the page's content edge here, it has to
     reproduce `.wrap`'s edge (centering margin once the viewport exceeds
     --wrap, plus .wrap's own clamp(16px,4vw,32px) gutter) instead of an
     independent vw-clamp that happened to drift from it at both ends of
     the desktop range. The centering term uses `cqw` off `.modes-pin`
     (query container, see above) rather than `vw`, `vw` counts the
     vertical scrollbar and `.wrap`'s own centering margin (plain %
     block layout) does not, so a `vw`-only version drifted from `.wrap`
     by exactly the scrollbar's width. */
  padding: clamp(100px, 13vh, 148px)
    calc(max(0px, (100cqw - var(--wrap)) / 2) + clamp(16px, 4vw, 32px)) clamp(96px, 13vh, 140px);
}

/* wraps .mode-head + .mode-copy so the ≤940px rule below can drop one
   opaque plate behind the whole stack. display:contents above that width
   removes this element from the box tree entirely, its two children stay
   direct grid items of .mode, in rows 1 and 3, exactly as if this wrapper
   didn't exist. */
.mode h3 {
  margin: var(--s3) 0 var(--s2);
  font-size: clamp(2.4rem, 7vw, 5.2rem);
  line-height: 0.9;
  text-shadow: 0 0 50px rgba(0, 0, 0, 0.9);
}
.mode .players {
  /* a bare <span> stays `inline`, and an inline box that wraps across two
     lines gets its border drawn per line-fragment instead of as one
     contiguous box, at mobile widths the longer badges ("Live now · online
     · endless") wrap and read as two split fragments.
     inline-block makes width/max-width apply and keeps the border a single
     box no matter how many lines the text takes. */
  display: inline-block;
  justify-self: start;
  width: max-content;
  max-width: 100%;
  margin-bottom: var(--s4);
  padding: 5px 12px;
  border: 1px solid rgba(255, 204, 0, 0.42);
  background: rgba(8, 9, 10, 0.66);
  font: 800 var(--t-label) / 1.4 var(--font-stencil);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--amber);
}
.mode p {
  max-width: 52ch;
  margin: 0;
  color: var(--bone);
}

/* --- 5 · survivors: dossier slabs, portrait sticky while the file scrolls -- */
.dossiers {
  position: relative;
  padding-block: var(--pad-y) 0;
}
.file {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1fr);
  gap: clamp(24px, 4vw, 60px);
  align-items: start;
  padding-block: clamp(20px, 3vh, 40px) clamp(32px, 5vh, 72px);
}
.file--flip .file-art {
  order: 2;
}
.file-art {
  position: sticky;
  top: clamp(96px, 14vh, 150px);
}
.file-plate {
  position: relative;
  overflow: hidden;
  border-radius: 3px;
  background: #0c0d0e;
  box-shadow:
    inset 0 0 0 2px #0a0b0c,
    inset 0 0 0 5px var(--slate-700),
    inset 0 0 80px rgba(0, 0, 0, 0.8),
    0 22px 50px rgba(0, 0, 0, 0.7);
}
/* The paintings are 1600x582 banners with the character sitting at ~62% and
   half the plate empty wall. Cropping to a portrait puts the survivor on the
   board at full size instead of as a thin strip. */
.file-plate > img {
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: 63% 14%;
  filter: brightness(0.9) saturate(0.94) contrast(1.06);
}
@media (max-width: 940px) {
  .file-plate > img {
    aspect-ratio: 16 / 9;
    object-position: 62% 18%;
  }
}
.file-plate::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to top, rgba(4, 5, 6, 0.72), transparent 46%);
}
/* ID photo inset behind cracked glass */

.file-body {
  position: relative;
  padding: clamp(22px, 3vw, 38px);
}
.file-rank {
  display: inline-block;
  padding: 4px 10px;
  background: var(--acid-deep);
  font: 800 var(--t-label) / 1.4 var(--font-stencil);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--acid-glow);
}
.file-body h3 {
  margin: var(--s3) 0 var(--s4);
  font-size: clamp(2rem, 4.4vw, 3.3rem);
  line-height: 0.92;
}
/* quote plate, chiselled into the slab: the bevel runs the wrong way round
   (dark at the top, light at the bottom) so the panel reads as cut in, and the
   glyphs carry the same inverted shadow. */
.engrave {
  margin: 0 0 var(--s5);
  padding: var(--s5) var(--s5) calc(var(--s5) + 2px);
  background: linear-gradient(#151618, #1d1e20);
  box-shadow:
    inset 0 2px 3px rgba(0, 0, 0, 0.9),
    inset 0 -1px 0 rgba(255, 255, 255, 0.09),
    0 1px 0 rgba(255, 255, 255, 0.05);
  font-family: var(--font-type);
  font-size: 1.05rem;
  line-height: 1.5;
  color: #b3ae9d;
  text-shadow: 0 -1px 0 #000, 0 1px 0 rgba(255, 255, 255, 0.13);
}
.file-bio {
  margin: 0 0 var(--s5);
  color: var(--bone-dim);
}

/* weapon rack on a rail */
.rack {
  position: relative;
  padding-top: var(--s5);
}
.rack::before {
  content: '';
  position: absolute;
  top: 13px;
  left: 0;
  right: 0;
  height: 5px;
  border-radius: 3px;
  background: linear-gradient(#3d4046, #0a0b0c);
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.7);
}
.rack-rail {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  margin: 0;
  padding: 0;
  list-style: none;
}
/* phones (site polish 13): five weapons wrap 3 over 2, centred on the rail
   so the short row reads as a deliberate rack, not a hole on the right */
@media (max-width: 640px) {
  .rack-rail {
    justify-content: center;
  }
}
/* both dossiers' racks hold exactly five weapons: pin them five-across on
   any width wide enough to read them (1280-1920), rather than leaving the
   count to however flex-wrap happens to fill the available row */
@media (min-width: 1280px) {
  .rack-rail {
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
  .rack-btn {
    width: auto;
  }
}
.rack-btn {
  position: relative;
  display: grid;
  place-items: center;
  gap: 6px;
  width: 84px;
  min-height: 84px;
  padding: 10px 6px 8px;
  border: 1px solid var(--slate-600);
  border-radius: 2px;
  background: rgba(10, 11, 8, 0.78);
  cursor: pointer;
  transition:
    border-color var(--dur-fast) linear,
    box-shadow var(--dur-fast) linear,
    transform var(--dur-fast) var(--ease-mech);
}
.rack-btn::before {
  /* the hook it hangs from */
  content: '';
  position: absolute;
  top: -12px;
  left: 50%;
  width: 3px;
  height: 12px;
  transform: translateX(-50%);
  background: #4a4d52;
}
.rack-btn img {
  height: 34px;
  max-width: 62px;
  object-fit: contain;
  filter: brightness(1.08);
  transition: filter var(--dur-fast) linear;
}
.rack-btn span {
  font: 800 0.75rem / 1 var(--font-stencil); /* 12px floor */
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bone-faint);
}
.rack-btn:hover {
  border-color: var(--amber);
  transform: translateY(-2px);
}
.rack-btn:hover img {
  filter: brightness(1.2);
}
.rack-btn[aria-pressed='true'] {
  border-color: var(--amber);
  box-shadow: 0 0 14px rgba(255, 204, 0, 0.26), inset 0 0 16px rgba(255, 204, 0, 0.1);
}
.rack-btn[aria-pressed='true'] span {
  color: var(--amber);
}
.rack-btn[aria-pressed='true'] img {
  filter: brightness(1.25);
}
/* the survivor's live sprite, swapped by the rack */
.file-stage {
  position: relative;
  height: clamp(210px, 26vh, 290px);
  margin-top: var(--s5);
  overflow: hidden;
  border-radius: 3px;
  background:
    radial-gradient(70% 100% at 50% 100%, rgba(107, 122, 58, 0.16), transparent 70%),
    url('/play/assets/floor.webp') center / 150% auto no-repeat;
  box-shadow: inset 0 0 0 1px #0a0b0c, inset 0 0 70px rgba(0, 0, 0, 0.85);
}
.file-stage::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(80% 70% at 50% 60%, transparent 34%, rgba(4, 5, 6, 0.8) 100%),
    repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.16) 0 1px, transparent 1px 3px);
}
.file-stage .sp-anchor {
  left: 50%;
  top: 68%;
}
.file-stage .stage-osd {
  position: absolute;
  z-index: 3;
  left: 10px;
  bottom: 8px;
  font-size: 0.75rem;
}

/* --- 6 · enemies --------------------------------------------------------- */
.board {
  position: relative;
  padding-block: var(--pad-y);
}

/* armed: attack animation, alarm LED, the frame rattles 3px */
@keyframes rattle {
  0% {
    transform: translate3d(-3px, -2px, 0);
  }
  33% {
    transform: translate3d(3px, -1px, 0);
  }
  66% {
    transform: translate3d(-2px, -3px, 0);
  }
  100% {
    transform: translate3d(0, -2px, 0);
  }
}

/* --- the Prime Reaper breaks the grid entirely --------------------------- */
.reaper {
  position: relative;
  overflow: hidden;
  padding-block: clamp(72px, 12vh, 140px);
  background: #030405;
  isolation: isolate;
}
.reaper::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: url('/play/assets/floor.webp') center / 165% auto no-repeat;
  filter: brightness(0.16) saturate(0.4);
}
.reaper-inner {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(28px, 4vw, 64px);
  align-items: center;
}
.reaper-art {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 420px;
  /* the veil deliberately overhangs the box; clip it so it cannot dim the
     file column beside it */
  overflow: hidden;
}
.reaper-art > img {
  width: 480px;
  max-width: 100%;
  -webkit-mask-image: radial-gradient(closest-side, #000 72%, transparent 100%);
  mask-image: radial-gradient(closest-side, #000 72%, transparent 100%);
  filter: brightness(0.96) contrast(1.1) drop-shadow(0 30px 40px rgba(0, 0, 0, 0.9));
}
.reaper-art .glow {
  position: absolute;
  z-index: -1;
  inset: 0;
  background: radial-gradient(circle at 50% 46%, rgba(229, 9, 20, 0.26), transparent 62%);
}
/* the bile puddle, second and last use of --toxic */
@keyframes bile {
  0%,
  100% {
    opacity: 0.5;
  }
  50% {
    opacity: 0.95;
  }
}
.reaper-file {
  padding-block: var(--s5);
}
/* site polish 6: two columns, the Reaper stands on the section floor beside
   its file instead of floating mid-height over a black slab, and the floor
   comes up to meet him */
@media (min-width: 1081px) {
  .reaper {
    padding-bottom: clamp(40px, 6vh, 72px);
  }
  .reaper-art {
    align-self: end;
  }
}
.reaper-file h2 {
  margin: var(--s3) 0 var(--s4);
  font-family: var(--font-display);
  font-size: clamp(2.2rem, 6vw, 4.2rem);
  line-height: 0.9;
  color: var(--bone);
}
.reaper-file h2 em {
  display: block;
  font-style: normal;
  color: var(--alarm-lit);
  text-shadow: 0 0 40px rgba(229, 9, 20, 0.4);
}

/* ======================================================== 13 ACT III ====== */

/* --- 7 · media: a bank of security monitors ------------------------------ */
.feeds {
  position: relative;
  padding-block: var(--pad-y);
}
/* the army walker crosses the floor under the caption row, give it clean
   empty ground there at every viewport instead of walking through text */
#media.feeds {
  padding-bottom: max(var(--pad-y), 120px);
}

/* --- 8 · news: post cards ------------------------------------------------- */
.wire {
  position: relative;
  padding-block: var(--pad-y);
}
.wire-roll {
  position: relative;
  display: grid;
  gap: clamp(12px, 1.6vw, 18px);
  max-width: 880px;
  margin-inline: auto;
}
.wire-item {
  position: relative;
  display: grid;
  grid-template-columns: 132px minmax(0, 1fr);
  gap: clamp(14px, 2.4vw, 28px);
  padding: clamp(20px, 3vw, 30px) clamp(22px, 3vw, 32px);
  background-color: #201f1c;
  background-image: var(--tex-panel);
  background-position: center;
  background-size: cover;
  background-blend-mode: soft-light;
  border-radius: 3px;
  box-shadow: inset 1px 1px 0 var(--slate-600), 0 10px 24px rgba(0, 0, 0, 0.5);
  color: inherit;
  transition: background-color var(--dur-fast) linear;
}
a.wire-item:hover {
  background-color: #272621;
}
.wire-item time {
  font: 400 0.75rem / 1.5 var(--font-data);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  color: var(--bone-faint);
}
.wire-body h3 {
  margin: var(--s2) 0 var(--s3);
  font-size: clamp(1.22rem, 2.4vw, 1.55rem);
}
.wire-body p {
  margin: 0;
  font-family: var(--font-type);
  font-size: 1rem;
  color: var(--bone-dim);
}
a.wire-item:hover .wire-body h3 {
  color: var(--acid-glow);
}
.wire-more {
  margin: var(--s6) 0 0;
  text-align: center;
}

/* --- 9 · sign up ---------------------------------------------------------- */
.signup {
  position: relative;
  /* clip, not hidden: overflow: hidden makes this a scroll container, and
     view() picks the nearest scroll container as its scroller, freezing
     the zombie's --zp instead of scrubbing it with the page */
  overflow: clip;
  padding-block: clamp(72px, 10vh, 132px);
  background: #030405;
  isolation: isolate;
}
.signup::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(70% 50% at 50% 30%, rgba(122, 21, 18, 0.24), transparent 70%),
    url('/play/assets/floor.webp') center / 150% auto no-repeat;
  filter: brightness(0.2) saturate(0.5);
}
.signup-grid {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 460px);
  gap: clamp(32px, 5vw, 72px);
  align-items: center;
}
.signup-copy {
  position: relative;
}
.signup-copy::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset: -8% -12% -12% -14%;
  background: radial-gradient(72% 62% at 34% 46%, rgba(3, 4, 5, 0.88), transparent 74%);
}
.signup h2 {
  margin: var(--s4) 0;
  font-family: var(--font-display);
  font-size: clamp(2.4rem, 6.2vw, 4.4rem);
  line-height: 0.9;
  color: var(--bone);
}
.signup h2 em {
  font-style: normal;
  color: var(--acid);
}
.signup p {
  max-width: 46ch;
  color: var(--bone);
}
/* the sign-up plate */
.plate {
  position: relative;
  z-index: 1;
  padding: clamp(24px, 3.2vw, 38px);
  background-color: var(--slate-700);
  background-image: var(--tex-panel);
  background-position: center;
  background-size: cover;
  background-blend-mode: overlay;
  border-radius: 3px;
  box-shadow:
    inset 1px 1px 0 var(--slate-600),
    inset -1px -1px 0 rgba(0, 0, 0, 0.9),
    inset 0 0 70px rgba(0, 0, 0, 0.7),
    0 24px 54px rgba(0, 0, 0, 0.75);
}
.plate-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-bottom: var(--s5);
  padding-bottom: var(--s4);
  border-bottom: 1px solid rgba(0, 0, 0, 0.6);
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05);
}
.plate-head h3 {
  font-size: 1.3rem;
}

/* register/login as a physical selector switch */
.switch {
  display: inline-flex;
  gap: 2px;
  margin-bottom: var(--s5);
  padding: 3px;
  background: #0c0d0e;
  border-radius: 2px;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.07);
}
.switch button {
  padding: 10px 18px;
  border: 0;
  border-radius: 1px;
  background: none;
  font: 800 var(--t-label) / 1 var(--font-stencil);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bone-faint);
  cursor: pointer;
  transition: background var(--dur-fast) linear, color var(--dur-fast) linear;
}
.switch button[aria-pressed='true'] {
  background: var(--acid-deep);
  color: var(--acid-glow);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
/* locked while a submit is in flight, so the mode can't change under the request */
.switch button:disabled {
  cursor: default;
  opacity: 0.72;
}

/* form: visible labels, errors below the field, fields in the game's housing */
.field-row {
  margin-bottom: var(--s4);
}
.field-row > label {
  display: block;
  margin-bottom: 6px;
  font: 800 0.75rem / 1.2 var(--font-stencil);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bone-dim);
}
.field {
  width: 100%;
  min-height: 46px;
  padding: 10px 12px;
  /* one frame (site polish 15): the bone hairline alone, the dark input.webp
     housing that boxed it a second time is gone */
  border: 1px solid rgba(216, 210, 192, 0.42);
  border-radius: 1px;
  background: var(--slate-700) padding-box;
  color: var(--bone);
  font-family: var(--font-body);
  font-size: 1rem;
  filter: none;
  outline: none;
}
.field::placeholder {
  color: var(--bone-faint);
  opacity: 1;
}
.field:focus-visible {
  background: var(--slate-600) padding-box;
  outline: 2px solid var(--acid);
  outline-offset: 1px;
  border-color: rgba(182, 214, 74, 0.55);
  box-shadow: 0 0 14px rgba(182, 214, 74, 0.18);
}
.field[aria-invalid='true'] {
  border-color: var(--alarm-lit);
  background: color-mix(in srgb, var(--blood) 26%, var(--slate-700)) padding-box;
}
.field-err {
  display: block;
  margin-top: 5px;
  min-height: 1.25em;
  font: 400 var(--t-data) / 1.4 var(--font-data);
  color: var(--alarm-lit);
}
.field-err:not(:empty)::before {
  content: '▲ ';
  color: var(--alarm-lit);
}
.form-msg {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  min-height: 3em;
  margin: var(--s4) 0 0;
  font: 400 0.78rem / 1.55 var(--font-data);
  letter-spacing: -0.02em;
  color: var(--bone-dim);
}
.form-msg .led {
  margin-top: 3px;
}
.form-msg:not(.ok):not(.err):not(.warn) {
  visibility: hidden;
}
.form-msg.ok {
  color: var(--acid);
}
.form-msg.err {
  color: var(--amber);
}
.form-foot {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--s4);
  flex-wrap: wrap;
  margin-top: var(--s4);
}
/* denied: a decaying-sine buzz */
.plate.denied {
  animation: buzz 420ms linear 1;
}
@keyframes buzz {
  0%,
  100% {
    transform: translateX(0);
  }
  12% {
    transform: translateX(-9px);
  }
  25% {
    transform: translateX(8px);
  }
  38% {
    transform: translateX(-6px);
  }
  52% {
    transform: translateX(5px);
  }
  66% {
    transform: translateX(-3px);
  }
  80% {
    transform: translateX(2px);
  }
}

/* --------------------------------------------------------- 14 footer ----- */

.wall-foot {
  position: relative;
  /* clip, not hidden: same view()-scroller trap as .signup above */
  overflow: clip;
  /* the bottom pad is the hoses' strip (site polish 7): they hang in it,
     below the last line of fine print */
  padding-block: var(--s8) 64px;
  background: #060708;
}
.wall-foot .lamp {
  top: -70px;
  animation-duration: 9.2s;
}
.foot-grid {
  position: relative;
  z-index: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s5);
  justify-content: space-between;
}
.foot-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4);
  margin: 0;
  padding: 0;
  list-style: none;
}
.foot-nav a {
  font: 800 0.75rem / 1 var(--font-stencil);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bone-faint);
}
.foot-nav a:hover {
  color: var(--acid);
}
/* Footer server signal, silent while the backend is up. It stays a
   visually-hidden live region until the health check fails, then surfaces as an
   alarm strip stating what a down backend means for the visitor. Styling exists
   only for the failure state; healthy = nothing to show. */
.foot-signal.down {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  margin-top: var(--s5);
  padding: 8px 13px;
  border: 1px solid var(--alarm-lit);
  border-radius: 2px;
  background: rgba(122, 21, 18, 0.2);
  font: 600 0.75rem / 1.4 var(--font-data);
  letter-spacing: 0.01em;
  color: var(--alarm-lit);
  max-width: 48ch;
  white-space: normal;
}
.foot-signal.down::before {
  content: '';
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--alarm-lit);
  box-shadow: 0 0 6px var(--alarm);
}
footer.wall-foot .wrap {
  position: relative;
}
.foot-fine {
  isolation: isolate;
  max-width: 92ch;
  margin: var(--s6) 0 0;
  padding-top: var(--s5);
  font: 400 0.75rem / 1.7 var(--font-data); /* 12px floor */
  letter-spacing: -0.02em;
  color: var(--bone-faint);
}
/* full-width hairline, independent of the 92ch text measure above */
.foot-fine::before {
  content: '';
  position: absolute;
  /* match .wrap's own padding-inline so the hairline lands on the same
     content edge as the nav row above it, not the wrap's outer box */
  left: clamp(16px, 4vw, 32px);
  right: clamp(16px, 4vw, 32px);
  border-top: 1px solid #131415;
}
/* the hoses hang below the footer copy, never across it (site polish 7):
   the wrap stacks over the .hose layer (z 6), the hoses sit lower inline */
footer.wall-foot .wrap {
  z-index: 7;
}

/* Motion switch (site polish 19): the sign-up selector, shrunk to a footer
   fitting. Hidden until js/site.js wires it, so JS-off visitors never see a
   control that cannot act; they follow the OS setting through css/calm.css. */
.motion-switch {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: var(--s5);
}
.motion-switch[hidden] {
  display: none;
}
.motion-label {
  font: 500 0.75rem / 1 var(--font-data);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--bone-faint);
}
.switch--motion {
  margin: 0;
}
.switch--motion button {
  padding: 8px 13px;
  font-size: 0.68rem;
}

/* phone footer (site polish 14): links on a two-column grid of 44px rows,
   and the zombie floor only where a footer zombie actually walks */
@media (max-width: 620px) {
  .foot-nav {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: var(--s4);
    row-gap: 0;
    width: 100%;
  }
  .foot-nav a {
    display: flex;
    align-items: center;
    min-height: 44px;
  }
  .foot-grid:not(:has(.z-walker--footer)) {
    padding-bottom: 0;
  }
  .foot-fine {
    margin-top: var(--s5);
  }
}

/* --------------------------------------------------------- 15 dialogs ---- */

dialog {
  border: 0;
  padding: 0;
  max-width: none;
  max-height: none;
  color: var(--bone);
  background: none;
}
/* .viewer sets display:grid, which would otherwise beat the UA's display:none
   on a closed dialog and leave the lightbox stacked at the end of the page */
dialog:not([open]) {
  display: none;
}
dialog::backdrop {
  background: rgba(3, 4, 5, 0.88);
}
.dialog-close {
  position: absolute;
  z-index: 8;
  top: 14px;
  right: 14px;
  width: 40px;
  height: 40px;
  border: 0;
  background: url('/play/assets/ui/btn-close.webp') center / contain no-repeat;
  cursor: pointer;
  font-size: 0;
}
.dialog-close:hover {
  filter: brightness(1.25);
}

/* lightbox as a full-screen feed with physical channel buttons */
.viewer {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100dvh;
  margin: 0;
  display: grid;
  place-items: center;
  padding: clamp(52px, 8vh, 84px) clamp(66px, 9vw, 130px);
  background: rgba(3, 4, 5, 0.96);
}
.viewer figure {
  margin: 0;
  max-width: 100%;
}
.viewer .viewer-frame {
  display: grid;
  place-items: center;
}
.viewer img {
  max-width: 100%;
  max-height: 74vh;
  width: auto;
  object-fit: contain;
}
.viewer figcaption {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4);
  justify-content: space-between;
  margin-top: var(--s4);
  font: 400 0.75rem / 1.5 var(--font-data);
  letter-spacing: -0.02em;
  color: var(--acid);
}
.viewer figcaption .idx {
  color: var(--bone-faint);
}
.chan {
  position: absolute;
  top: 50%;
  display: grid;
  place-items: center;
  width: 54px;
  height: 76px;
  transform: translateY(-50%);
  border: 1px solid var(--slate-600);
  border-radius: 3px;
  background: linear-gradient(#26282b, #0d0e0f);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12), 0 6px 14px rgba(0, 0, 0, 0.7);
  color: var(--bone);
  font-size: 1.2rem;
  cursor: pointer;
}
.chan:hover {
  color: var(--acid);
  border-color: var(--acid);
}
.chan:active {
  box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.8);
}
.chan.prev {
  left: clamp(10px, 2vw, 26px);
}
.chan.next {
  right: clamp(10px, 2vw, 26px);
}

/* --------------------------------------------------------- 16 sub-pages -- */

/* -------------------------------------------------- 17 reduced motion ---- */

/* ------------------------------------------------------- 18 responsive --- */

@media (max-width: 1080px) {
  .reaper-inner {
    grid-template-columns: 1fr;
  }
  .signup-grid {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 940px) {
  .rail-nav {
    display: none;
  }
  .burger {
    display: block;
  }
  
  .file{
    grid-template-columns: 1fr;
  }
  .file--flip .file-art {
    order: 0;
  }
  .file-art {
    position: static;
  }
  /* stacked single-column on mobile: the two dossiers no longer need the
     desktop side-by-side offset, so tighten the vertical rhythm between them */
  .file {
    padding-block: clamp(16px, 2.4vh, 26px) clamp(20px, 3.2vh, 36px);
  }
  /* mobile (≤940): the horizontal snap carousel becomes a plain vertical
     stack, each card sized to its own content, the carousel's flex row
     used to be as tall as its tallest child (Campaign), leaving ~620px of
     empty texture under the shorter Survival card, plus a neighbour peeking
     in from the side with its own text cut mid-word. No belt, no snap: with
     both cards simply stacked there is nothing to page between. The desktop
     pinned/scroll-driven carousel above 940px is untouched. */
  .modes {
    height: auto;
  }
  .mode {
    flex: none;
    grid-template-rows: none;
    /* .mode-art/.mode-vig stay position:absolute (below) so their aspect-ratio
       gives them a real rendered height even though .mode itself is no longer
       height:100% of a pinned parent, but absolutely positioned boxes don't
       contribute to a content-sized ancestor's height, so the in-flow content
       (mode-head + .mode-copy) needs its own padding-top reserving that same
       band. .mode has no explicit width constraint at this breakpoint, so it
       renders the full section width (100vw here, since nothing above wraps
       it in a `.wrap`), aspect-ratio 2/1 makes the art exactly half that tall.
       A2 fix: the horizontal component now matches `.wrap`'s own gutter
       (clamp(16px,4vw,32px), no centering term needed below --wrap) instead
       of an independent clamp that drifted from it; the ≤430px block below
       further overrides this to `.wrap`'s flat 12px there. */
    padding: calc(50vw + 20px) clamp(16px, 4vw, 32px) clamp(28px, 6vh, 48px);
  }
  /* one opaque plate behind the whole text stack, not five separate boxes.
     Real-pixel sampling of the sprite sheets against .mode-no/.label/h3/
     .players/p found worst-case contrast down at 1.1-1.6:1 wherever a
     sprite's opaque pixels land under any of them, the same problem the
     five individual backdrops solved, minus the patchwork of edges that
     created between the kicker, heading, badge and paragraph. `.mode-text`
     is `display:contents` above 940px (see base rule), so it costs nothing
     to the desktop 3-row grid; here it becomes the real box. */
  .mode h3 {
    display: inline-block;
  }
  .mode p {
    padding: 0;
  }
}

/* survivor tabs exist only on phones/tablets, at desktop both files render side by side and the
   control must not paint (Sloane, D177 review: an author display:grid beat the UA [hidden] rule) */
@media (max-width: 940px) {
  .file-tabs {
    display: grid;
    grid-template-columns: 1fr 1fr;
    width: 100%;
    margin: var(--s4) 0 var(--s5);
    padding: 3px;
    background: #0c0d0e;
    border-radius: 2px;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.07);
  }
  .file-tab {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    min-height: 46px;
    border: 0;
    border-radius: 2px;
    background: none;
    font: 800 var(--t-label) / 1 var(--font-stencil);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--bone-faint);
    cursor: pointer;
    transition: background var(--dur-fast) linear, color var(--dur-fast) linear;
  }
}
.file-tabs[hidden] { display: none; }

/* the plate system's own "this circuit is live" signal */
.file-tab::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: transparent;
}
.file-tab[aria-selected='true'] {
  background: var(--acid-deep);
  color: var(--acid-glow);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.file-tab[aria-selected='true']::before {
  background: var(--acid);
  box-shadow: 0 0 6px var(--acid);
}
.file-tab:focus-visible {
  outline: 2px solid var(--acid);
  outline-offset: 2px;
}
/* panel swap motion, site.js toggles .file--entering off on the next frame
   so the transition runs from this state back to the base (visible) one */
.file[role='tabpanel'] {
  transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}
.file[role='tabpanel'].file--entering {
  opacity: 0;
  transform: translateY(8px);
}
/* .file's own `display: grid` and the [hidden] UA rule share specificity, and
   author rules win ties, without this the inactive panel keeps rendering
   underneath the active one instead of actually hiding */
.file[hidden] {
  display: none;
}

@media (max-width: 620px) {
  :root {
    --t-body: 1rem;
  }
  .hero {
    padding-bottom: clamp(96px, 15vh, 150px);
  }
  /* polish item 1: the rail keeps one small Play canister between mute and
     menu; Sign up steps out to the drawer at this width */
  .rail-right .ghost--rail {
    display: none;
  }
  .rail-right .clamp--rail {
    min-width: 92px;
    font-size: 0.68rem;
    padding: 0 1.9em 0.28em;
  }
  /* pull the firefight into the upper band, clear of the type and the buttons */
  /* the old lift-and-shrink hack that lived here only covered ≤620px and
     only cleared Survival's shorter kicker, superseded by the .mode-art /
     .mode-vig aspect-ratio containment in the ≤940px block, which covers
     the full mobile range for both cards (see site.css around .mode-vig). */
  .wire-item {
    grid-template-columns: 1fr;
    gap: var(--s2);
  }
  /* Age/Service/Rank stays three columns at every width, collapsing to two
     orphans the third stat onto its own row for no reason at this size. */
  .foot-fine {
    font-size: 0.75rem;
  }
  /* J4: the chan buttons used to pin bottom:14px to the viewport floor , 
     221px below the figcaption with nothing between, and under the iPhone
     home-indicator strip since site.css had no env(safe-area-inset-*)
     anywhere. Anchored to the figure now (see .chan-row) and padded clear
     of the safe area instead. */
  .viewer {
    padding: 56px 12px max(24px, env(safe-area-inset-bottom));
  }
  .viewer figure {
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  .chan {
    position: static;
    width: 48px;
    height: 48px;
    transform: none;
  }
}

/* 390px at 200% browser zoom is a ~195px layout viewport: the rail has to shed
   the wordmark and the burger's label to fit without a horizontal scrollbar. */
@media (max-width: 430px) {
  .brand-wm {
    display: none;
  }
  .brand-crest {
    width: 60px;
    height: 60px;
    margin-bottom: 0;
  }
  /* the compact rail's own --brand-s (0.6) stacks on top of the 60px crest
     above, dropping it to ~36px, the "small logo" complaint again. 0.8
     here holds it at 48px and still clears the compact rail. */
  .rail.compact .brand {
    --brand-s: 0.8;
  }
  /* three nowrap columns get tight below ~400px; shrink the value type
     rather than let it wrap onto a second line or force a fourth column */
  .clamp--primary {
    min-width: 168px;
    font-size: 1.24rem;
  }
  /* the mid-ground decals are wider than the viewport at this size */
  .burger {
    width: 44px;
    height: 40px;
    padding: 0;
    font-size: 0;
    display: grid;
    place-items: center;
  }
  .burger::before {
    content: '\2261';
    font-size: 1.5rem;
    line-height: 1;
  }
  .rail-inner {
    gap: 8px;
  }
  .wrap {
    padding-inline: 12px;
  }
  /* A2 fix: `.mode`'s ≤940px rule still carries clamp(16px,4vw,32px) here
     (6vw/4vw agree above ~430 but diverge below it), pin the inline
     padding to `.wrap`'s own flat 12px so the modes content edge keeps
     matching the rest of the page at this width too. */
  .mode {
    padding-inline: 12px;
  }
  /* J9: the clamp bottoms out at 22px here, 9px inside the 13px perforation
     strip, give the copy a hair more clearance than the strip itself. */
  .wire-item {
    padding-inline: 26px;
  }
}

/* Below ~340px (a 390px device at 200% zoom, or a very small phone) the hero's
   cast and its stamp have nowhere to stand without landing on the type. The
   horde still runs everywhere else on the page; here the title carries it. */
@media (max-width: 340px) {
  :root {
    /* the 4rem floor is wider than the content box at this size */
    --t-hero: clamp(2.3rem, 16vw, 4rem);
  }
  .clamp {
    min-width: 118px;
  }
  .clamp--primary {
    min-width: 132px;
    font-size: 1.02rem;
  }
  .ghost {
    padding: 12px 14px;
  }
  .hero {
    min-height: auto;
    padding-block: clamp(40px, 8vh, 72px) clamp(72px, 12vh, 110px);
  }
}

/* touch: 44px targets, 8px apart, native cursor */
@media (pointer: coarse) {
  .rail-links a,
  .foot-nav a,
  .drawer > a:not(.clamp) {
    display: flex;
    align-items: center;
    min-height: 44px;
  }
  
  .sound-btn,
  .dialog-close,
  .drawer-close,
  .burger {
    width: 44px;
    height: 44px;
  }
  
  .switch button,
  .link,
  #news .sub a,
  .wire-body a[href='/play/'],
  .ghost {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
}

/* ------------------------------------------------- sprite geometry ------- */
/* ------------------------------------------------ 19 site cut 2026-10-03 -- */
/* docs/design/site-cut-2026-10-03.md. Plain section titles, a gameplay video
   hero, two mode panels, one enemy roster, a plain gallery. Sits after the
   responsive block so it wins over the older section rules it replaces. */

/* plain section titles: display type, no plaque */
.sec-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2rem, 5vw, 3.6rem);
  font-weight: 400;
  line-height: 0.95;
  letter-spacing: 0.02em;
  color: var(--bone);
  text-shadow: 0 4px 0 rgba(0, 0, 0, 0.55), 0 0 40px rgba(0, 0, 0, 0.8);
}
.head .sec-title + .sub {
  margin-top: var(--s4);
}

/* hero: the gameplay loop behind the title */
.hero {
  min-height: min(100svh, 920px);
  padding-bottom: clamp(88px, 13vh, 160px);
}
.hero-film {
  position: absolute;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background: var(--void);
}
.hero-film picture,
.hero-film img,
.hero-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 45%;
}
.hero-video {
  opacity: 0;
  transition: opacity 0.6s linear;
}
.hero.playing .hero-video {
  opacity: 1;
}
.hero-shade {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    linear-gradient(90deg, rgba(8, 9, 10, 0.86) 0%, rgba(8, 9, 10, 0.55) 38%, rgba(8, 9, 10, 0) 70%),
    linear-gradient(0deg, var(--void) 0%, rgba(8, 9, 10, 0.4) 26%, rgba(8, 9, 10, 0) 50%),
    linear-gradient(180deg, rgba(8, 9, 10, 0.55) 0%, rgba(8, 9, 10, 0) 18%);
}
.hero-logo {
  display: block;
  width: clamp(64px, 7vw, 92px);
  height: auto;
  margin: 0 0 var(--s4);
  filter: drop-shadow(0 6px 14px rgba(0, 0, 0, 0.8));
}
.hero-tag {
  margin-bottom: var(--s3);
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.9);
}
.hero-free {
  margin: 0 0 var(--s6);
  font: 800 var(--t-label) / 1.2 var(--font-stencil);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--amber);
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.9);
}
@media (max-width: 700px) {
  .hero-shade {
    background:
      linear-gradient(0deg, var(--void) 0%, rgba(8, 9, 10, 0.78) 34%, rgba(8, 9, 10, 0.2) 62%, rgba(8, 9, 10, 0.35) 100%);
  }
}

/* the outbreak: one plain card */
.brief .story {
  max-width: 760px;
  margin: 0 auto;
}

/* modes: two panels, side by side, stacked on phones */
.modes {
  height: auto;
  padding-bottom: var(--s8);
}
.mode-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(18px, 3vw, 36px);
}
.mode-panel {
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
  background-color: var(--slate-700);
  background-image: var(--tex-panel);
  background-size: cover;
  background-blend-mode: overlay;
  border-radius: 3px;
  box-shadow:
    inset 1px 1px 0 var(--slate-600),
    0 22px 48px rgba(0, 0, 0, 0.7);
}
.mode-shot {
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--void);
}
.mode-shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.mode-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s4);
  flex: 1;
  padding: clamp(20px, 2.6vw, 32px);
}
.mode-body h3 {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.7rem, 3vw, 2.4rem);
  font-weight: 400;
  line-height: 1;
  color: var(--bone);
}
.mode-body p {
  margin: 0;
  max-width: 48ch;
  color: var(--bone-dim);
}
.mode-body .players {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  max-width: 100%;
  font: 700 var(--t-label) / 1.3 var(--font-stencil);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--bone-faint);
}
.mode-body .players .n {
  color: var(--acid);
}
.mode-body .clamp {
  margin-top: auto;
}
@media (max-width: 800px) {
  .mode-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* characters: portrait, name, quote, bio, loadout. No ID inset or stats. */
.file .stage-osd {
  font: 800 var(--t-label) / 1 var(--font-stencil);
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* enemies: one compact roster */
.roster-sec {
  position: relative;
  padding-bottom: var(--s7);
}
.roster {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(12px, 1.6vw, 18px);
}
.roster + .roster {
  margin-top: clamp(12px, 1.6vw, 18px);
}
@media (min-width: 600px) {
  .roster,
  .roster--boss {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (min-width: 1100px) {
  .roster:not(.roster--boss) {
    grid-template-columns: repeat(4, 1fr);
  }
}
.foe {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr);
  align-items: center;
  gap: var(--s4);
  padding: var(--s4);
  background: rgba(26, 27, 29, 0.85);
  border-radius: 3px;
  box-shadow: inset 1px 1px 0 var(--slate-600), 0 10px 24px rgba(0, 0, 0, 0.5);
}
.foe img {
  width: 96px;
  height: 96px;
  object-fit: contain;
  filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.7));
}
.foe h3 {
  margin: 0 0 var(--s2);
  font-family: var(--font-display);
  font-size: 1.3rem;
  font-weight: 400;
  line-height: 1;
  color: var(--bone);
}
.foe p {
  margin: 0;
  font-size: 0.95rem;
  line-height: 1.5;
  color: var(--bone-dim);
}
.foe--boss {
  box-shadow: inset 0 0 0 1px rgba(229, 9, 20, 0.45), 0 10px 24px rgba(0, 0, 0, 0.5);
}
.boss-tag {
  display: inline-block;
  margin-left: 6px;
  padding: 3px 7px 2px;
  vertical-align: 3px;
  font: 800 0.68rem / 1 var(--font-stencil);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bone);
  background: var(--alarm, #b3121b);
  border-radius: 2px;
}
.reaper-file .boss-tag {
  margin: 0 0 var(--s4);
}
.reaper-file .prose {
  max-width: 52ch;
}

/* media: a plain gallery, bigger images */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 420px), 1fr));
  gap: clamp(14px, 2vw, 24px);
}
.gallery + .gallery-h {
  margin-top: var(--s8);
}
.gallery-h {
  margin: 0 0 var(--s5);
  font-family: var(--font-display);
  font-size: clamp(1.4rem, 2.6vw, 1.9rem);
  font-weight: 400;
  color: var(--bone);
}
.shot {
  display: block;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  text-align: left;
  cursor: zoom-in;
}
.shot img {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: 3px;
  box-shadow: 0 14px 30px rgba(0, 0, 0, 0.6);
  transition: filter var(--dur-fast) linear;
}
.shot:hover img,
.shot:focus-visible img {
  filter: brightness(1.12);
}
.shot-cap {
  display: block;
  margin-top: var(--s3);
  font-size: 0.98rem;
  color: var(--bone-dim);
}

/* news: plain labels and dates, body face */
.wire-item time {
  font: 700 0.85rem / 1.4 var(--font-stencil);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.wire-body .tag {
  display: inline-block;
  font: 800 var(--t-label) / 1 var(--font-stencil);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--amber);
}
@media (max-width: 560px) {
  .wire-item {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* sign up: section title left-aligned */
.signup-copy .sec-title {
  margin-bottom: var(--s5);
}

/* ---- generated by tools/build-sprite-css.mjs — do not hand-edit ---- */
.sp-man_gun_idle{--fw:90px;--fh:104px;--frames:8;--pv:46px}
.sp-man_gun_walk{--fw:86px;--fh:103px;--frames:6;--pv:44px}
.sp-man_gun_shoot{--fw:96px;--fh:150px;--frames:5;--pv:51px}
.sp-man_riffle_idle{--fw:92px;--fh:145px;--frames:8;--pv:48px}
.sp-man_riffle_walk{--fw:90px;--fh:145px;--frames:6;--pv:47px}
.sp-man_riffle_shoot{--fw:106px;--fh:258px;--frames:9;--pv:64px}
.sp-man_flamethrower_idle{--fw:90px;--fh:131px;--frames:8;--pv:50px}
.sp-man_flamethrower_walk{--fw:90px;--fh:130px;--frames:6;--pv:48px}
.sp-man_flamethrower_shoot{--fw:100px;--fh:255px;--frames:9;--pv:61px}
.sp-man_knife_idle{--fw:102px;--fh:101px;--frames:8;--pv:41px}
.sp-man_knife_walk{--fw:122px;--fh:100px;--frames:6;--pv:39px}
.sp-man_knife_shoot{--fw:210px;--fh:119px;--frames:8;--pv:41px}
.sp-man_bat_idle{--fw:98px;--fh:112px;--frames:8;--pv:60px}
.sp-man_bat_walk{--fw:128px;--fh:121px;--frames:6;--pv:61px}
.sp-man_bat_shoot{--fw:184px;--fh:200px;--frames:12;--pv:69px}
.sp-man_death{--fw:192px;--fh:95px;--frames:6;--pv:42px}
.sp-girl_gun_idle{--fw:72px;--fh:89px;--frames:8;--pv:38px}
.sp-girl_gun_walk{--fw:70px;--fh:88px;--frames:6;--pv:37px}
.sp-girl_gun_shoot{--fw:92px;--fh:136px;--frames:5;--pv:44px}
.sp-girl_riffle_idle{--fw:78px;--fh:120px;--frames:8;--pv:41px}
.sp-girl_riffle_walk{--fw:74px;--fh:126px;--frames:6;--pv:40px}
.sp-girl_riffle_shoot{--fw:106px;--fh:240px;--frames:9;--pv:58px}
.sp-girl_flamethrower_idle{--fw:76px;--fh:103px;--frames:8;--pv:41px}
.sp-girl_flamethrower_walk{--fw:74px;--fh:105px;--frames:6;--pv:40px}
.sp-girl_flamethrower_shoot{--fw:100px;--fh:230px;--frames:9;--pv:53px}
.sp-girl_knife_idle{--fw:84px;--fh:83px;--frames:8;--pv:35px}
.sp-girl_knife_walk{--fw:100px;--fh:86px;--frames:6;--pv:34px}
.sp-girl_knife_shoot{--fw:162px;--fh:110px;--frames:8;--pv:41px}
.sp-girl_bat_idle{--fw:82px;--fh:96px;--frames:8;--pv:53px}
.sp-girl_bat_walk{--fw:104px;--fh:102px;--frames:6;--pv:50px}
.sp-girl_bat_shoot{--fw:168px;--fh:167px;--frames:12;--pv:54px}
.sp-girl_death{--fw:168px;--fh:82px;--frames:6;--pv:35px}
.sp-bighands_walk{--fw:174px;--fh:158px;--frames:9;--pv:60px}
.sp-bighands_attack{--fw:288px;--fh:249px;--frames:9;--pv:121px}
.sp-bighands_vomit{--fw:192px;--fh:325px;--frames:9;--pv:74px}
.sp-bighands_death{--fw:218px;--fh:288px;--frames:6;--pv:209px}
.sp-bighead_walk{--fw:164px;--fh:150px;--frames:9;--pv:58px}
.sp-bighead_attack{--fw:274px;--fh:232px;--frames:9;--pv:112px}
.sp-bighead_vomit{--fw:186px;--fh:330px;--frames:8;--pv:78px}
.sp-bighead_death{--fw:214px;--fh:284px;--frames:6;--pv:211px}
.sp-boss1_walk{--fw:304px;--fh:295px;--frames:8;--pv:96px}
.sp-boss1_attack{--fw:536px;--fh:429px;--frames:14;--pv:259px}
.sp-boss1_special{--fw:480px;--fh:504px;--frames:10;--pv:199px}
.sp-boss1_death{--fw:442px;--fh:528px;--frames:10;--pv:152px}
.sp-boss2_walk{--fw:294px;--fh:271px;--frames:8;--pv:103px}
.sp-boss2_attack{--fw:284px;--fh:256px;--frames:8;--pv:105px}
.sp-boss2_special{--fw:444px;--fh:496px;--frames:10;--pv:206px}
.sp-boss2_death{--fw:390px;--fh:482px;--frames:10;--pv:124px}
.sp-megaboss_walk{--fw:530px;--fh:489px;--frames:8;--pv:187px}
.sp-megaboss_attack{--fw:752px;--fh:494px;--frames:16;--pv:214px}
.sp-megaboss_flame{--fw:698px;--fh:580px;--frames:9;--pv:171px}
.sp-megaboss_spike{--fw:570px;--fh:618px;--frames:9;--pv:190px}
.sp-megaboss_slam{--fw:796px;--fh:579px;--frames:9;--pv:213px}
.sp-megaboss_death{--fw:790px;--fh:681px;--frames:14;--pv:231px}
.sp-zombie1_walk{--fw:102px;--fh:104px;--frames:9;--pv:42px}
.sp-zombie1_attack{--fw:166px;--fh:147px;--frames:9;--pv:60px}
.sp-zombie1_death{--fw:140px;--fh:192px;--frames:6;--pv:145px}
.sp-zombie2_walk{--fw:98px;--fh:102px;--frames:9;--pv:42px}
.sp-zombie2_attack{--fw:166px;--fh:135px;--frames:9;--pv:54px}
.sp-zombie2_death{--fw:146px;--fh:191px;--frames:6;--pv:146px}
.sp-zombie3_walk{--fw:150px;--fh:125px;--frames:9;--pv:50px}
.sp-zombie3_attack{--fw:216px;--fh:154px;--frames:9;--pv:56px}
.sp-zombie3_death{--fw:184px;--fh:242px;--frames:6;--pv:185px}
.sp-zombie4_walk{--fw:142px;--fh:122px;--frames:9;--pv:46px}
.sp-zombie4_attack{--fw:200px;--fh:154px;--frames:9;--pv:62px}
.sp-zombie4_death{--fw:184px;--fh:225px;--frames:6;--pv:167px}
.sp-army_walk{--fw:132px;--fh:132px;--frames:9;--pv:50px}
.sp-army_attack{--fw:218px;--fh:190px;--frames:9;--pv:87px}
.sp-army_death{--fw:168px;--fh:239px;--frames:6;--pv:176px}
.sp-cop_walk{--fw:130px;--fh:134px;--frames:9;--pv:53px}
.sp-cop_attack{--fw:216px;--fh:182px;--frames:9;--pv:79px}
.sp-cop_death{--fw:160px;--fh:242px;--frames:6;--pv:180px}
.sp-man_gun_idle_shift{--fw:83px;--fh:102px;--frames:14;--pv:46px}
.sp-man_gun_idle_scan{--fw:88px;--fh:106px;--frames:18;--pv:46px}
.sp-man_gun_reload{--fw:96px;--fh:109px;--frames:10;--pv:48px}
.sp-man_riffle_reload{--fw:120px;--fh:153px;--frames:14;--pv:59px}
.sp-man_flamethrower_reload{--fw:123px;--fh:144px;--frames:16;--pv:63px}
.sp-girl_gun_idle_shift{--fw:70px;--fh:89px;--frames:14;--pv:39px}
.sp-girl_gun_idle_scan{--fw:75px;--fh:92px;--frames:18;--pv:41px}
.sp-girl_gun_reload{--fw:85px;--fh:94px;--frames:10;--pv:42px}
.sp-girl_riffle_reload{--fw:106px;--fh:127px;--frames:14;--pv:47px}
.sp-girl_flamethrower_reload{--fw:107px;--fh:110px;--frames:16;--pv:47px}
.sp-zombie3_feed{--fw:106px;--fh:129px;--frames:9;--pv:71px}
.sp-zombie3_feed_rise{--fw:122px;--fh:133px;--frames:4;--pv:71px}
.sp-zombie1_feed{--fw:89px;--fh:108px;--frames:9;--pv:58px}
.sp-zombie1_feed_rise{--fw:104px;--fh:108px;--frames:4;--pv:58px}
.sp-zombie2_feed{--fw:88px;--fh:109px;--frames:9;--pv:57px}
.sp-zombie2_feed_rise{--fw:101px;--fh:108px;--frames:4;--pv:57px}
.sp-zombie4_feed{--fw:103px;--fh:130px;--frames:9;--pv:72px}
.sp-zombie4_feed_rise{--fw:125px;--fh:135px;--frames:4;--pv:72px}
.sp-cop_feed{--fw:115px;--fh:141px;--frames:9;--pv:75px}
.sp-cop_feed_rise{--fw:132px;--fh:141px;--frames:4;--pv:75px}
.sp-army_feed{--fw:117px;--fh:135px;--frames:9;--pv:73px}
.sp-army_feed_rise{--fw:133px;--fh:140px;--frames:4;--pv:73px}
/* ---- end generated ---- */
