/* The game's stylesheet. It was two <style> blocks inside index.html until 2026-07-30 — 64KB
   of CSS in the middle of the document, re-downloaded with every page because it could not be
   cached on its own. Nothing here was rewritten in the move: the two blocks are concatenated in
   document order, so the cascade is byte-for-byte what it was.
   Read the section banners below; they are the same ones the markup is grouped by. */

/* ===================== TOKENS — the single source of the look ===================== */
:root{
  /* palette */
  --bg:#0A1218; --bg-panel:#121D26; --bg-panel-2:#16232E; --bg-inset:#080F14;
  --steel-line:#24384A;
  --cyan:#3CD6F0; --cyan-bright:#8AEFFF; --cyan-dim:rgba(60,214,240,.35); --cyan-faint:rgba(60,214,240,.10);
  --amber:#F0A830; --amber-bright:#FFC95A; --amber-ink:#1A1206;
  --warn:#F0D030; --danger:#F04848; --success:#48E0A0;
  --text:#D6E8F0; --text-cyan:#9ADCEE; --text-muted:#5A7284;
  --hp:#48E070; --shield:#3CD6F0; --energy:#F0A830;
  /* world palette (canvas reads these — tokens stay the only source) */
  --floor:#17252F; --floor-line:rgba(60,214,240,.07); --wall:#1B2A36; --wall-edge:#3B5A70;
  --fog:#05090D; --hostile:#F04848; --self:#3CD6F0; --loot:#F0A830; --chip:#8AEFFF;
  /* spacing */
  --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:24px; --sp-6:32px; --sp-7:48px;
  /* type */
  --font-display:'Quantico',sans-serif; --font-ui:'Play',sans-serif;
  /* Two places already asked for this and neither got it: the token was never defined, so
     `font-family:var(--font-mono)` was an invalid declaration and the invite link has been
     rendering in the UI face since it shipped. Digits that line up are the whole reason to
     want a monospace, and both users of it are numbers or a URL. */
  --font-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  --fs-xs:0.75rem; --fs-sm:0.875rem; --fs-md:1rem; --fs-lg:1.25rem; --fs-xl:1.5rem; --fs-2xl:2rem; --fs-3xl:2.75rem;
  --lh-tight:1.2; --lh-body:1.5; --ls-wide:.14em; --ls-xwide:.22em;
  /* shape */
  --chamfer:8px; --r-sm:2px; --r-md:4px; --r-lg:8px; --border:1px; --border-2:2px;
  --hairline:rgba(60,214,240,.14);
  --shadow-1:0 2px 10px rgba(2,6,10,.55); --shadow-2:0 8px 34px rgba(2,6,10,.75);
  --glow-cyan:0 0 6px rgba(60,214,240,.5); --glow-amber:0 0 8px rgba(240,168,48,.5);
  --glow-danger:0 0 8px rgba(240,72,72,.5);
  /* layers */
  --z-base:0; --z-hud:10; --z-nav:20; --z-popup:30; --z-toast:40;
  /* motion */
  --dur-fast:120ms; --dur-base:220ms; --dur-slow:400ms;
  --ease-out:cubic-bezier(.16,.84,.34,1); --ease-in:cubic-bezier(.6,.04,.98,.34);
  --ease-step:cubic-bezier(.2,0,0,1);
  /* derived colors (kept here so the swap test stays true) */
  --bg-glow-1:#0F1B26; --bg-glow-2:#0C1822; --scrim:rgba(4,8,12,.78); --vignette:rgba(4,8,12,.8);
  --comms-bg:rgba(8,15,20,.86); --amber-edge:rgba(26,18,6,.45); --danger-faint:rgba(240,72,72,.12);
  --warn-dim:rgba(240,208,48,.4); --hp-dim:rgba(72,224,112,.6); --shield-dim:rgba(60,214,240,.55);
  --hurt-glow:rgba(240,72,72,.5); --hurt-arc:rgba(240,72,72,.62); --transparent:rgba(0,0,0,0);
  --laser:rgba(240,72,72,.5); --laser-core:rgba(255,150,140,.72); --laser-dot:rgba(255,120,110,.8);
  /* --- asset palette: operators, weapons, gore (canvas art reads these) --- */
  --gun:#61798B; --gun-dark:#1A232B; --gun-hi:#93A7B6; --gun-lens:#8AEFFF;
  --kevlar:#31424F; --kevlar-hi:#4A6274; --kevlar-dark:#1B252E; --strap:#111A21;
  --visor-self:#8AEFFF; --visor-foe:#FF6A5A;
  --shadow-drop:rgba(2,6,10,.55); --ink:#070C10;
  --sheen:rgba(226,244,252,.16); --shade:rgba(6,12,17,.34);
  --blood:#8E1322; --blood-dark:#3E060D; --blood-mist:#C8202F;
  --shell:#C79242; --flash:#FFDF9A; --flash-core:#FFF6E0; --smoke:rgba(148,168,184,.42);
  --scorch:rgba(4,8,11,.7);
  /* OPERATOR CLOTH — what a man is made of when he comes apart, and the base note of his
     palette. One entry per MATERIAL in the wardrobe rather than one per operator, because six
     of them wear the same black and a torn sleeve should not care which of the six it was.
     None of the darks are true black: an outline needs something to be darker than. */
  --op-sand:#8A7047; --op-rust:#C2601E; --op-moss:#4F5B33; --op-loam:#6B5537;
  --op-sulphur:#C9A62B; --op-frost:#A9B4BC; --op-carmine:#8E2230; --op-graphite:#262B31;
  --op-void:#1B2026; --op-amethyst:#5B3A6E; --op-brass:#7A5A32; --op-mail:#6A7078;
  --op-hide:#33291F; --op-tribe:#6E5233; --op-oxblood:#6E2A2A; --op-drab:#4A5240;
  --op-lacquer:#5E1E22;
  /* the shadow one piece of kit drops on the piece beneath it — the whole reason a pouch reads
     as being ON a vest rather than painted into it. It belongs to the light, not to a skin. */
  --op-cast:rgba(2,5,8,.55);
  --bone:#D2CCBA; --fur:#E8E3D6; --scrap:#6B4A2E;
  /* rarity ramp — shop tiles and skin effects read off these */
  --r-common:#5A7284; --r-rare:#3CD6F0; --r-epic:#A46BE0; --r-leg:#FFC95A;
  /* mythic sits ABOVE legendary and must not read as "another gold". Ember orange is the one
     hue nothing else in this palette uses, and it is also what its footprints and its rounds
     burn — the tile and the effect are deliberately the same colour, so the card tells you
     what you will see on the floor. */
  --r-myth:#FF7A2E;
  --fx-ember:rgba(240,120,48,.55); --fx-abyss:rgba(60,214,240,.5); --fx-spark:rgba(255,225,140,.9);
  --fx-shade:rgba(164,107,224,.5); --fx-frost:rgba(214,240,255,.85); --fx-cinder:rgba(255,140,60,.9);
  /* --- environment art --- */
  --floor-b:#1B2B37; --floor-c:#132029; --floor-grime:rgba(5,10,14,.4);
  --floor-mark:rgba(240,168,48,.12); --floor-hazard:rgba(240,208,48,.13);
  --wall-top:#2A3F4E; --wall-panel:rgba(60,90,112,.45); --wall-rivet:rgba(70,104,128,.75);
  --ao:rgba(3,7,10,.62); --light-pool:rgba(120,200,230,.028);
  --cone-light:rgba(150,214,240,.13); --self-glow:rgba(60,214,240,.16);
  /* metrics */
  --hud-gap:var(--sp-3); --stick-size:132px; --bar-h:10px; --tap:44px; --knob:52px;
  --fire-size:88px;
  /* --mini went 96 -> 118 at the user's request. It is a 60x44 map in a square, so every pixel
     is about a fifth of a tile; below ~90 the corridors stop reading as corridors. */
  --mark:88px; --art:112px; --mini:118px; --chev-w:64px; --chev-h:10px; --grid-cell:36px;
  --sw-w:52px; --sw-h:24px; --icon:18px; --icon-sm:14px; --wpn-art:26px;
}
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
/* THE `hidden` ATTRIBUTE HAS TO WIN, and without this line it loses. The browser's own rule is
   `[hidden]{display:none}` at the very bottom of the cascade, so ANY author rule that sets a
   display — `.btn{display:inline-flex}` does — beats it and the element stays on screen with its
   attribute cheerfully set to true. That is not a theory: the "connect Google" button sat in the
   menu of a linked account for a day, drawn as a bare G because syncLink only fills its label
   when it believes the button is shown. Every `el.hidden = x` in this file depends on this rule.
   IT ALSO MEANS `hidden` CANNOT BE VERIFIED BY READING el.hidden — that property is just the
   attribute reflected back and was `true` the whole time. Check getComputedStyle().display. */
[hidden]{display:none!important}
html,body{height:100%}
/* This is a canvas game, not a document: text selection, the iOS long-press callout and
   the double-tap zoom are all noise here. Inputs opt back in below. */
body{background:var(--bg);color:var(--text);font-family:var(--font-ui);font-size:var(--fs-md);
  line-height:var(--lh-body);overflow:hidden;overscroll-behavior:none;
  font-variant-numeric:tabular-nums;
  -webkit-user-select:none;user-select:none;-webkit-touch-callout:none;
  touch-action:manipulation}
input,textarea{-webkit-user-select:text;user-select:text;-webkit-touch-callout:default}
h1,h2,h3{font-family:var(--font-display);font-weight:700;text-transform:uppercase;
  letter-spacing:var(--ls-wide);line-height:var(--lh-tight);color:var(--cyan-bright)}
h1{font-size:var(--fs-3xl);text-shadow:var(--glow-cyan)}
h2{font-size:var(--fs-xl)} h3{font-size:var(--fs-md)}
button{font-family:inherit;font-size:inherit;color:inherit;background:none;border:0;cursor:pointer}
:focus-visible{outline:var(--border-2) solid var(--cyan);outline-offset:var(--sp-1)}

/* ===================== deck (atmosphere layer) ===================== */
.deck{position:fixed;inset:0;z-index:-1;background:
  radial-gradient(ellipse 70% 55% at 50% 0%, var(--bg-glow-1) 0%, transparent 60%),
  radial-gradient(ellipse 55% 45% at 90% 100%, var(--bg-glow-2) 0%, transparent 60%), var(--bg)}
.deck::before{content:'';position:absolute;inset:0;opacity:.5;
  background-image:linear-gradient(var(--floor-line) 1px,transparent 1px),
    linear-gradient(90deg,var(--floor-line) 1px,transparent 1px);
  background-size:var(--grid-cell) var(--grid-cell)}
.deck::after{content:'';position:absolute;inset:0;
  background:radial-gradient(ellipse at center, transparent 45%, var(--vignette))}
.scanband{position:fixed;left:0;right:0;height:var(--border-2);z-index:-1;pointer-events:none;
  background:linear-gradient(90deg,transparent,var(--cyan-dim),transparent);opacity:.5;
  animation:scan 12s linear infinite}
@keyframes scan{0%{transform:translateY(-10vh)}100%{transform:translateY(110vh)}}

/* ===================== plates, brackets, headers ===================== */
.plate{position:relative;background:var(--bg-panel);border:var(--border) solid var(--steel-line);
  clip-path:polygon(0 0,calc(100% - var(--chamfer)) 0,100% var(--chamfer),100% 100%,
    var(--chamfer) 100%,0 calc(100% - var(--chamfer)));padding:var(--sp-4)}
.plate.lit{border-color:var(--cyan-dim);box-shadow:inset 0 0 20px var(--cyan-faint)}
.plate.inset{background:var(--bg-inset)}
.brackets{position:absolute;inset:0;pointer-events:none}
.brackets i{position:absolute;width:var(--sp-3);height:var(--sp-3);border:var(--border-2) solid var(--cyan);
  transition:inset var(--dur-fast) var(--ease-out),opacity var(--dur-fast) linear;opacity:.75}
.brackets i:nth-child(1){top:0;left:0;border-right:0;border-bottom:0}
.brackets i:nth-child(2){top:0;right:0;border-left:0;border-bottom:0}
.brackets i:nth-child(3){bottom:0;left:0;border-right:0;border-top:0}
.brackets i:nth-child(4){bottom:0;right:0;border-left:0;border-top:0}
.hud-hdr{display:flex;align-items:center;gap:var(--sp-3);font-family:var(--font-display);
  font-weight:700;font-size:var(--fs-sm);letter-spacing:var(--ls-wide);text-transform:uppercase;
  color:var(--cyan-bright);text-shadow:var(--glow-cyan);min-width:0}
.hud-hdr .chev{height:var(--chev-h);width:var(--chev-w);flex:none;
  background:repeating-linear-gradient(-45deg,var(--cyan) 0 6px,transparent 6px 12px)}
.hud-hdr .rule{flex:1;height:var(--border-2);background:var(--steel-line);position:relative;min-width:var(--sp-5)}
.hud-hdr .rule::after{content:'';position:absolute;left:0;top:0;height:var(--border-2);width:34%;
  background:var(--cyan);box-shadow:var(--glow-cyan)}
.label{font-size:var(--fs-xs);letter-spacing:var(--ls-wide);text-transform:uppercase;color:var(--text-muted)}
.value{font-weight:700;color:var(--cyan-bright)}
.row{display:flex;align-items:baseline;gap:var(--sp-2);min-width:0}
.row .lead{flex:1;min-width:var(--sp-4);border-bottom:var(--border) dotted var(--steel-line);transform:translateY(-3px)}

/* ===================== buttons ===================== */
.btn{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-2);
  min-height:var(--tap);padding:var(--sp-3) var(--sp-5);font-family:var(--font-display);font-weight:700;
  font-size:var(--fs-sm);letter-spacing:var(--ls-wide);text-transform:uppercase;color:var(--text-cyan);
  border:var(--border) solid var(--cyan-dim);background:transparent;
  clip-path:polygon(0 0,calc(100% - var(--chamfer)) 0,100% var(--chamfer),100% 100%,
    var(--chamfer) 100%,0 calc(100% - var(--chamfer)));
  transition:color var(--dur-fast) linear,border-color var(--dur-fast) linear,
    background-color var(--dur-fast) linear,transform var(--dur-fast) var(--ease-out)}
.btn:hover:not(:disabled){color:var(--cyan-bright);border-color:var(--cyan);background:var(--cyan-faint)}
.btn:active:not(:disabled){transform:translateY(1px)}
.btn:disabled{opacity:.4;cursor:not-allowed}
.btn--cta{color:var(--amber-ink);border-color:var(--amber);
  background:linear-gradient(180deg,var(--amber-bright),var(--amber));
  box-shadow:inset 0 -2px 0 var(--amber-edge)}
.btn--cta:hover:not(:disabled){color:var(--amber-ink);background:var(--amber-bright);
  border-color:var(--amber-bright);box-shadow:var(--glow-amber),inset 0 -2px 0 var(--amber-edge)}
.btn--danger{color:var(--danger);border-color:var(--danger)}
.btn--danger:hover:not(:disabled){background:var(--danger-faint);color:var(--danger);border-color:var(--danger)}
.btn--big{min-height:56px;font-size:var(--fs-md);padding:var(--sp-4) var(--sp-6)}
.btn--wide{width:100%}
.btn--icon{min-width:var(--tap);min-height:var(--tap);padding:var(--sp-2)}
.btn svg{width:var(--icon);height:var(--icon);flex:none;stroke:currentColor;fill:none;stroke-width:2}
.btn--cta svg{stroke:var(--amber-ink)}


/* ===================== bars, gauges, chips ===================== */
.bar{position:relative;height:var(--bar-h);background:var(--bg-inset);
  border:var(--border) solid var(--steel-line);overflow:hidden;
  clip-path:polygon(0 0,calc(100% - var(--sp-1)) 0,100% var(--sp-1),100% 100%,var(--sp-1) 100%,0 calc(100% - var(--sp-1)))}
.bar__fill{position:absolute;inset:0;transform-origin:left center;
  transition:transform var(--dur-slow) var(--ease-out)}
.bar__fill--hp{background:linear-gradient(180deg,var(--hp),var(--hp-dim))}
.bar__fill--shield{background:repeating-linear-gradient(-45deg,var(--shield) 0 8px,var(--shield-dim) 8px 16px)}
.bar__fill--load{background:var(--amber)}
.bar__tick{position:absolute;top:0;bottom:0;width:var(--border-2);background:var(--cyan-bright);
  box-shadow:var(--glow-cyan);transition:left var(--dur-slow) var(--ease-out)}
.meter{display:flex;flex-direction:column;gap:var(--sp-1);min-width:0}
.meter__top{display:flex;justify-content:space-between;gap:var(--sp-2);font-size:var(--fs-xs);
  letter-spacing:var(--ls-wide);text-transform:uppercase}
.meter__top b{color:var(--cyan-bright);font-weight:700}
.chip{display:inline-flex;align-items:center;gap:var(--sp-2);padding:var(--sp-1) var(--sp-3);
  background:var(--bg-panel-2);border:var(--border) solid var(--steel-line);font-size:var(--fs-xs);
  letter-spacing:var(--ls-wide);text-transform:uppercase;color:var(--text-cyan);
  clip-path:polygon(0 0,calc(100% - var(--sp-1)) 0,100% var(--sp-1),100% 100%,var(--sp-1) 100%,0 calc(100% - var(--sp-1)))}
.chip b{color:var(--cyan-bright);font-size:var(--fs-sm)}
.chip svg{width:var(--icon-sm);height:var(--icon-sm);stroke:var(--cyan);fill:none;stroke-width:2}
.chip--amber{border-color:var(--amber);color:var(--amber-bright)} .chip--amber svg{stroke:var(--amber)}

/* WHICH MODE'S HUD THIS IS, decided in CSS off one class on <body> — the same mechanism that
   already switches this interface for touch and for the debug build. The alternative is a
   function that walks the HUD hiding elements on entry and remembers to put every one of them
   back on exit, and "remembers to" is where that always breaks. Everything unmarked belongs to
   both modes; a marked element states which one it is for and then never has to be managed. */
.only-horde{display:none} body.horde .only-horde{display:inline-flex}
body.horde .only-raid{display:none}
/* THE MODE IS HIDDEN FROM THE MENU, NOT FROM THE BUILD — see THE HORDE IS HIDDEN FROM PLAYERS in
   the boot block for the two ways back in (?horde=1 and an invite link). `display:none` rather than
   removing the markup, so the buttons keep their comments, their strings and their handlers and the
   day this reverts it is one class that stops being set. */
body.horde-hidden #btn-horde,
body.horde-hidden #btn-horde-online,
/* AND THE SETTING THAT ONLY MEANS ANYTHING INSIDE IT. The bot-mates toggle stayed on the settings
   panel after the mode came off the menu — a switch offering to change something a player has no
   way of seeing, which is worse than a missing feature: it reads as a broken one. The stored value
   is left alone, so turning the mode back on turns the switch back on with it. */
body.horde-hidden #tg-mates{display:none}

/* ===================== comms line (toasts) ===================== */
#comms{position:fixed;z-index:var(--z-toast);
  top:calc(max(var(--sp-3),env(safe-area-inset-top)) + 44px);
  left:max(var(--sp-3),env(safe-area-inset-left));
  width:min(380px,calc(100vw - var(--sp-6)));display:flex;flex-direction:column;gap:var(--sp-1);
  pointer-events:none;align-items:flex-start}
.comms__line{width:100%;display:flex;align-items:center;gap:var(--sp-2);padding:var(--sp-2) var(--sp-3);
  background:var(--comms-bg);border-bottom:var(--border) solid var(--cyan-dim);font-size:var(--fs-xs);
  color:var(--text);letter-spacing:.04em;animation:commsIn var(--dur-base) var(--ease-out);
  pointer-events:auto;overflow-wrap:anywhere}
@keyframes commsIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
.comms__line.out{animation:commsOut var(--dur-base) var(--ease-in) forwards}
@keyframes commsOut{to{opacity:0;transform:translateX(-8px)}}
.comms__caret{color:var(--cyan);font-weight:700;animation:blink 1s steps(1) infinite}
@keyframes blink{50%{opacity:.2}}
.comms__line b{color:var(--cyan-bright)}
.comms__line.is-danger{border-bottom-color:var(--danger)} .comms__line.is-danger b{color:var(--danger)}
.comms__line.is-good{border-bottom-color:var(--success)} .comms__line.is-good b{color:var(--success)}
.comms__x{margin-left:auto;min-width:24px;min-height:24px;color:var(--text-muted);flex:none}
.comms__x:hover{color:var(--cyan-bright)}

/* ===================== screens & popups ===================== */
.screen{position:fixed;inset:0;display:none;flex-direction:column;align-items:center;justify-content:center;
  padding:var(--sp-5);gap:var(--sp-5);z-index:var(--z-nav);overflow:auto}
.screen.is-on{display:flex;animation:plateIn var(--dur-base) var(--ease-out)}
@keyframes plateIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.stack{display:flex;flex-direction:column;gap:var(--sp-4);width:min(460px,100%);min-width:0}

/* ---- THE MENU'S CORNER RAILS ----
   Everything that is not "start a raid" lives in a corner, and that is the whole reorganisation:
   the menu had eleven buttons in one column, so nothing in it was the main one. The centre now
   holds four — deploy online, bots, skills, shop — and settings, account, rules, achievements
   and fullscreen sit in the corners where a player looks for them rather than reads past them.
   Absolute inside .screen, which is already a fixed full-bleed flex box, so the rails hang off
   the viewport corners while the stack stays centred and never has to make room for them. */
.rail{position:absolute;display:flex;align-items:center;gap:var(--sp-2);z-index:2}
.rail--br{bottom:var(--sp-4);right:var(--sp-4)}
.rail--bl{bottom:var(--sp-4);left:var(--sp-4)}
/* THE BACKDROP SWITCH SAYS WHICH STATE IT IS IN, not just what it does. An icon button that
   looks identical whether the thing is on or off makes the player press it to find out — so off
   is drawn as off: dimmed, and struck through. The slash is a pseudo-element rather than a
   second symbol so there is one icon to keep.
   A CLASS AND NOT AN ID, because there are two of them now — one under the menu and one under
   the sign-in screen — and everything that touches this switch (the styling here, syncBackBtn,
   the click binding in boot) keys off the same class. Two ids would be three places to remember
   a second selector in, and the third one is always the one that gets forgotten. */
.back-sw[aria-pressed="false"]{color:var(--dim);border-color:var(--line)}
.back-sw{position:relative}
.back-sw[aria-pressed="false"]::after{content:'';position:absolute;left:22%;right:22%;top:50%;
  height:var(--border-2);background:currentColor;transform:rotate(-45deg);pointer-events:none}
.rail__row{display:flex;align-items:center;gap:var(--sp-2)}

/* ---- THE MENU'S TOP ROW ----
   One row, both corners, and which element sits in which corner is decided by a single
   `margin-right:auto`. See the markup for the two layouts it produces. It is absolute like the
   rails, so the centred stack never has to make room for it. */
/* THE HEADER READS OVER A MOVING PICTURE NOW. Every number in this row — funds, level, best
   score, raids, frags — is thin cyan text that was designed against a flat dark page, and with a
   raid behind it a pale floor or a muzzle flash underneath is enough to lose a digit. So the
   page gets DARKER WHERE THE WORDS ARE: a band anchored to the top of the viewport, fading out
   below the row.
   ON THE SCREEN AND NOT ON .topbar, deliberately. .topbar is inset from the edges, so a
   background on it would be a floating bar with gaps at the corners — which reads as a panel,
   and this is not a panel. ::before on the screen is already the full viewport, needs no markup,
   and cannot introduce a scrollbar the way negative insets can. It sits behind every child
   because it comes first in paint order, so nothing in the row had to change. */
#menu::before,#entry::before{content:'';position:absolute;left:0;right:0;top:0;height:132px;
  pointer-events:none;
  background:linear-gradient(to bottom,rgba(4,8,11,.94) 0%,rgba(4,8,11,.78) 42%,rgba(4,8,11,0) 100%)}
@media(max-width:900px){ #menu::before,#entry::before{height:104px} }
.topbar{position:absolute;top:var(--sp-4);left:var(--sp-4);right:var(--sp-4);z-index:2;
  display:flex;align-items:center;gap:var(--sp-2);
  /* it spans the whole width with nothing but air in the middle, and air must not swallow a
     press aimed at the screen underneath it */
  pointer-events:none}
.topbar>*{pointer-events:auto}
/* DESKTOP: numbers left, account right. */
.topbar .dossier{margin-right:auto}
.topbar__funds,.topbar__exit{display:none}
/* PHONE: no room for the numbers beside a badge — 812x375 leaves 200px for a line that needs
   341 — so the line goes, the badge takes the left, and funds and the way out take the right.
   EVERY SELECTOR HERE IS `.topbar x` ON PURPOSE. A media query adds no specificity, and the
   `.dossier` base rule is defined further down this stylesheet: as a bare `.dossier{display:none}`
   this block lost to it and the phone kept a 630px line that shoved the badge off the screen. */
@media(max-width:900px){
  .topbar{top:var(--sp-2);left:var(--sp-2);right:var(--sp-2)}
  .topbar .dossier{display:none}
  .topbar .badge{margin-right:auto;max-width:52vw}
  .topbar .topbar__funds{display:inline-flex}
  .topbar .topbar__exit{display:inline-flex}
}

/* The two resource PANELS that stood in the top-right corner (.rsc, .rsc--funds, .rsc--lvl,
   .rsc__pts) are gone: funds and level are cells in the left-hand line now, with labels, because
   an icon and a bare number did not say which number it was. If a panelled resource readout is
   ever wanted again, note the name .res is taken by the results screen. */
/* WHO YOU ARE, as a thing you press. Two lines: the callsign you play under and the email behind
   it — or "guest" said plainly, because the difference decides whether a ban has teeth and
   whether progress survives this browser. */
.badge{display:flex;flex-direction:column;align-items:flex-end;gap:1px;
  padding:var(--sp-2) var(--sp-3);background:var(--bg-panel);
  border:var(--border) solid var(--steel-line);cursor:pointer;max-width:46vw;
  transition:border-color var(--dur-fast) var(--ease-out)}
.badge:hover{border-color:var(--cyan-dim)}
.badge__name{font-family:var(--font-display);font-size:var(--fs-sm);letter-spacing:var(--ls-wide);
  text-transform:uppercase;color:var(--text);line-height:1.1;
  max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.badge__sub{font-size:var(--fs-xs);color:var(--text-muted);line-height:1.1;
  max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* The link offer: ACCENTED, BUT NOT AS LOUD AS DEPLOY. Cyan outline against the CTA's filled
   amber — it should read as the second-best thing on the screen, never as the first. */
.btn--link{border-color:var(--cyan);color:var(--text-cyan);background:var(--cyan-faint)}
.btn--link:hover:not(:disabled){background:var(--cyan-dim);color:var(--cyan-bright)}
/* The ad reward, deliberately QUIETER than the deploy button and than the funds panel it hangs
   under: it is a top-up, not the point of the screen, and a game whose loudest control is the
   advert has said what it thinks of its own raid. Amber because it pays currency. */
.btn--ad{font-size:var(--fs-xs);letter-spacing:var(--ls-wide);text-transform:uppercase;
  padding:var(--sp-1) var(--sp-3);min-height:0;color:var(--amber);
  border-color:rgba(240,168,48,.42);background:transparent}
.btn--ad:hover:not(:disabled){background:rgba(240,168,48,.12);color:var(--amber-bright);
  border-color:var(--amber)}
.btn--ad svg{width:14px;height:14px}
/* The dossier: funds, level, best score, raids, frags — one line, one style, top-left corner.
   Every cell is `label value`, which is the whole reason this beat the two panels it replaced:
   the panels showed an icon and a number, and a number whose name you have to guess is not a
   readout. It does not wrap; in a corner a second line grows INTO the screen instead of staying
   out of the way, and the media query below trims the gaps first. */
.dossier{display:flex;align-items:center;gap:var(--sp-4);white-space:nowrap;
  font-size:var(--fs-xs);letter-spacing:var(--ls-wide);text-transform:uppercase;
  color:var(--text-muted)}
.dossier b{color:var(--text-cyan);font-variant-numeric:tabular-nums;font-weight:400}
/* funds lead in amber — the one figure here that anything on this screen can spend */
.dossier .lead b{color:var(--amber-bright)}
/* an unspent point is a node not taken, so the level goes amber the moment there is one to
   spend and drops back to cyan when there is not */
.dossier .pts b{color:var(--cyan-bright)}
/* Narrow screens: the rails stay put but stop competing with the stack for width. */
@media(max-width:560px){
  .rail{gap:var(--sp-1)}
  .topbar{gap:var(--sp-1)}
  .rail--br{bottom:var(--sp-2);right:var(--sp-2)}
  .badge{padding:var(--sp-1) var(--sp-2);max-width:44vw}
  .btn--link span{display:none}          /* the icon alone still reads as "Google" */
}
.brand{text-align:center;display:flex;flex-direction:column;gap:var(--sp-2);align-items:center}
.brand__sub{font-size:var(--fs-xs);letter-spacing:var(--ls-xwide);text-transform:uppercase;color:var(--text-muted)}
.brand__mark{width:var(--mark);height:var(--mark)}
/* THE WORDMARK. It is a heading that contains an image, so the heading's own type metrics —
   line-height, letter-spacing, the uppercase transform — have nothing to act on and would only
   add stray space around the picture. Width is capped against the viewport rather than fixed:
   the artwork is 2.77:1 and the intrinsic width/height on the <img> reserves the right box
   before it loads, so the screen does not jump when it arrives. */
.brand__logo{margin:0;line-height:0;width:100%;max-width:min(420px,78vw)}
.brand__logo img{display:block;width:100%;height:auto}
/* A LANDSCAPE PHONE IS 375px TALL and the sign-in screen stacks eight things into it, so the
   wordmark gives back the height it does not need there. 300px still reads at arm's length and
   is 44px the profile list gets to keep. Height, not width, is the scarce axis on this screen. */
@media (max-height:560px){ .brand__logo{max-width:min(300px,60vw)} }
.overlay{position:fixed;inset:0;z-index:var(--z-popup);display:none;align-items:center;justify-content:center;
  padding:var(--sp-4);background:var(--scrim)}
.overlay.is-on{display:flex;animation:fadeIn var(--dur-fast) linear}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
.modal{width:min(500px,100%);max-height:88vh;overflow:auto;display:flex;flex-direction:column;gap:var(--sp-4);
  animation:plateIn var(--dur-base) var(--ease-out)}
.modal__hdr{display:flex;align-items:center;gap:var(--sp-3)}
.modal__hdr h2{flex:1;min-width:0}
.list{display:flex;flex-direction:column;gap:var(--sp-2);min-width:0}
.opt{display:flex;align-items:center;gap:var(--sp-3);width:100%;padding:var(--sp-3);text-align:left;
  background:var(--bg-panel-2);border:var(--border) solid var(--steel-line);min-height:52px;
  transition:border-color var(--dur-fast) linear,background-color var(--dur-fast) linear}
.opt:hover{border-color:var(--cyan);background:var(--cyan-faint)}
.opt__name{flex:1;min-width:0;font-weight:700;color:var(--text);overflow-wrap:anywhere}
.opt__meta{font-size:var(--fs-xs);color:var(--text-muted);letter-spacing:var(--ls-wide);text-transform:uppercase}
.opt__del{color:var(--text-muted);min-width:32px;min-height:32px;flex:none}
.opt__del:hover{color:var(--danger)}
.field{display:flex;flex-direction:column;gap:var(--sp-2)}
.field input{width:100%;min-height:48px;padding:var(--sp-3);font-family:var(--font-ui);font-size:var(--fs-md);
  color:var(--text);background:var(--bg-inset);border:var(--border) solid var(--steel-line);
  letter-spacing:.06em}
.field input:hover{border-color:var(--cyan-dim)}
.field input:focus{outline:none;border-color:var(--cyan);box-shadow:inset 0 0 18px var(--cyan-faint)}
.empty{padding:var(--sp-5) var(--sp-4);text-align:center;color:var(--text-muted);font-size:var(--fs-sm);
  border:var(--border) dashed var(--steel-line)}
.note{font-size:var(--fs-xs);color:var(--text-muted);line-height:var(--lh-body)}
/* .badge-local lived here and is gone with the last line that used it — the menu's room-status
   badge. Deleted rather than kept "in case": an unused class is a thing the next person styles
   around, and the screens that report a problem already have `.note` and the comms toast. */
/* ---- maintenance ----
   It has to sit above the SPLASH, which is the one screen that outranks the others: the flag
   can arrive before the splash has timed out, and a maintenance screen that waits its turn
   behind an animation is a maintenance screen that lets somebody press play. */
#maint{z-index:calc(var(--z-toast) + 6);background:var(--bg)}
#maint .brand__mark{color:var(--danger);filter:drop-shadow(0 0 10px var(--hurt-glow))}
#maint-h{color:var(--danger)}
#maint #maint-plate{border-color:var(--danger);box-shadow:inset 0 0 24px var(--danger-faint)}
#maint .brackets i{border-color:var(--danger)}
#maint-msg{font-size:var(--fs-sm);color:var(--text-cyan);line-height:var(--lh-body)}
/* a dead channel: the sweep never resolves into anything, which is the whole read */
.maint__bar{position:relative;height:var(--border-2);background:var(--steel-line);overflow:hidden}
.maint__bar i{position:absolute;inset:0 auto 0 0;width:28%;background:var(--danger);
  box-shadow:var(--glow-danger);animation:maintSweep 2.4s var(--ease-step) infinite}
@keyframes maintSweep{0%{transform:translateX(-100%)}100%{transform:translateX(400%)}}

/* ---- promo stripe ----
   Above the screens so it survives entry -> menu, below the popups so it never sits on top of
   a confirm. Hidden outright during a raid: that corner belongs to the HUD. */
#stripe{position:fixed;left:0;right:0;top:0;z-index:calc(var(--z-nav) + 2);display:flex;align-items:center;
  gap:var(--sp-3);padding:var(--sp-2) var(--sp-3);background:var(--comms-bg);backdrop-filter:blur(6px);
  border-bottom:var(--border) solid var(--amber-edge);font-size:var(--fs-sm);color:var(--text);
  animation:plateIn var(--dur-base) var(--ease-out)}
#stripe[hidden]{display:none}
.stripe__dot{width:var(--sp-2);height:var(--sp-2);flex:none;background:var(--amber);box-shadow:var(--glow-amber)}
.stripe__txt{flex:1;min-width:0;overflow-wrap:anywhere}
.stripe__go{flex:none;color:var(--amber-bright);text-transform:uppercase;letter-spacing:var(--ls-wide);
  font-size:var(--fs-xs);border-bottom:var(--border) solid var(--amber-edge);padding-bottom:1px}
.stripe__go:hover{border-bottom-color:var(--amber-bright)}
.stripe__x{flex:none;color:var(--text-muted);min-width:32px;min-height:32px;display:grid;place-items:center}
.stripe__x:hover{color:var(--text)}
/* the screens centre their content, so a stripe over them needs the room back */
body.striped .screen{padding-top:calc(var(--sp-5) + 52px)}   /* measured: the bar is 49px tall */

.toggle{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);min-height:48px;
  padding:var(--sp-2) var(--sp-3);background:var(--bg-panel-2);border:var(--border) solid var(--steel-line);width:100%}
.toggle span{font-size:var(--fs-sm);letter-spacing:.04em;min-width:0}
.toggle__sw{position:relative;width:var(--sw-w);height:var(--sw-h);flex:none;background:var(--bg-inset);
  border:var(--border) solid var(--steel-line)}
.toggle__sw::after{content:'';position:absolute;top:2px;left:2px;width:20px;height:18px;background:var(--text-muted);
  transition:transform var(--dur-fast) var(--ease-out),background-color var(--dur-fast) linear;
  clip-path:polygon(0 0,calc(100% - 4px) 0,100% 4px,100% 100%,4px 100%,0 calc(100% - 4px))}
.toggle[aria-pressed="true"] .toggle__sw{border-color:var(--cyan-dim)}
.toggle[aria-pressed="true"] .toggle__sw::after{transform:translateX(26px);background:var(--cyan);box-shadow:var(--glow-cyan)}
.toggle:hover{border-color:var(--cyan-dim)}
/* volume sliders — same plate as .toggle so the settings list reads as one stack */
/* the acceleration row. It is a READOUT, not a control — see the markup for why there is no
   switch in it — so it is styled as a note that happens to have a state word on it. */
.gpu{display:flex;flex-direction:column;gap:var(--sp-1);margin-top:var(--sp-2)}
.gpu__line{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3)}
.gpu__line b{font-size:var(--fs-sm);letter-spacing:.04em;color:var(--success)}
.gpu.is-soft .gpu__line b{color:var(--warn)}
.gpu__path{display:block;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size:var(--fs-xs);color:var(--cyan-bright);background:var(--bg-inset);
  border:var(--border) solid var(--steel-line);border-radius:var(--r-sm);
  padding:var(--sp-1) var(--sp-2);user-select:all;word-break:break-all}
.slider{display:flex;align-items:center;gap:var(--sp-3);min-height:48px;width:100%;
  padding:var(--sp-2) var(--sp-3);background:var(--bg-panel-2);border:var(--border) solid var(--steel-line)}
.slider:hover{border-color:var(--cyan-dim)}
.slider label{font-size:var(--fs-sm);letter-spacing:.04em;flex:none;width:7.5rem;
  text-transform:lowercase;color:var(--text)}
.slider b{flex:none;width:2.6rem;text-align:right;font-size:var(--fs-sm);color:var(--cyan-bright);
  font-variant-numeric:tabular-nums}
.slider input[type=range]{-webkit-appearance:none;appearance:none;flex:1;min-width:0;height:var(--bar-h);
  background:var(--bg-inset);border:var(--border) solid var(--steel-line);cursor:pointer}
.slider input[type=range]:focus-visible{outline:var(--border-2) solid var(--cyan);outline-offset:2px}
.slider input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:20px;
  background:var(--cyan);box-shadow:var(--glow-cyan);border:0;cursor:pointer;
  clip-path:polygon(0 0,calc(100% - 4px) 0,100% 4px,100% 100%,4px 100%,0 calc(100% - 4px))}
.slider input[type=range]::-moz-range-thumb{width:14px;height:20px;background:var(--cyan);border:0;
  box-shadow:var(--glow-cyan);border-radius:0;cursor:pointer}
.slider input[type=range][value="0"],.slider input[type=range].mute::-webkit-slider-thumb{background:var(--text-muted)}

/* ===================== onboarding ===================== */
.cards{display:flex;flex-direction:column;gap:var(--sp-4);text-align:center;align-items:center}
.cards__art{width:var(--art);height:var(--art)}
.cards__body{min-height:6.5rem;display:flex;flex-direction:column;gap:var(--sp-2)}
.cards__body p{font-size:var(--fs-sm);color:var(--text-cyan)}
.dots{display:flex;gap:var(--sp-2);justify-content:center}
.dots i{width:22px;height:var(--border-2);background:var(--steel-line);transition:background-color var(--dur-fast) linear}
.dots i.on{background:var(--cyan);box-shadow:var(--glow-cyan)}
.modal__foot{display:flex;gap:var(--sp-2);align-items:center}
.modal__foot .grow{flex:1}

/* ===================== game HUD ===================== */
#game{position:fixed;inset:0;display:none;z-index:var(--z-base)}
#game.is-on{display:block}
/* THE MENU BACKDROP. The canvas is shown but the raid is not: no HUD, and nothing on it may be
   clicked or dragged. The screens already sit above it at --z-nav, so the only thing this has to
   guarantee is that the picture never takes a press meant for a button — pointer-events, not a
   z-index fight. The dimming is painted into the canvas itself (see Backdrop.paint) rather than
   layered over it here, so the amount of it lives next to the picture it darkens. */
#game.is-back{display:block;pointer-events:none}
#game.is-back .hud,#game.is-back #hint{display:none}
/* width/height are REQUIRED: a canvas is a replaced element, so with width:auto it lays
   out at its intrinsic (backing-store) size. On a DPR-2 phone that is twice the viewport,
   which pushed the centred fighter into the bottom-right corner. */
#field{position:absolute;inset:0;display:block;width:100%;height:100%;touch-action:none}
/* Scoped to the canvas: the laser is the pointer during a fight, but HUD buttons above it
   keep a normal cursor so they stay clickable. */
/* A crosshair instead of the OS arrow while a fight is live. Done with a CSS cursor rather
   than drawn on the canvas so it never lags a frame behind the real pointer. The dark
   halo underneath keeps it readable over muzzle flashes and pale floors. */
body.aiming #field{cursor:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36'%3E%3Cg fill='none' stroke='%23070C10' stroke-width='4.5' stroke-linecap='round'%3E%3Ccircle cx='18' cy='18' r='8'/%3E%3Cpath d='M18 2v6M18 28v6M2 18h6M28 18h6'/%3E%3C/g%3E%3Cg fill='none' stroke='%233CD6F0' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='18' cy='18' r='8'/%3E%3Cpath d='M18 2v6M18 28v6M2 18h6M28 18h6'/%3E%3C/g%3E%3Ccircle cx='18' cy='18' r='1.7' fill='%238AEFFF'/%3E%3C/svg%3E") 18 18, crosshair}
.hud{position:absolute;inset:0;z-index:var(--z-hud);pointer-events:none;
  padding:max(var(--sp-3),env(safe-area-inset-top)) max(var(--sp-3),env(safe-area-inset-right))
    max(var(--sp-3),env(safe-area-inset-bottom)) max(var(--sp-3),env(safe-area-inset-left));
  display:grid;grid-template-columns:1fr auto;grid-template-rows:auto 1fr auto;gap:var(--hud-gap)}
.hud > *{min-width:0}
/* TOP-LEFT IS A COLUMN NOW: the controls-and-counters row, then the feed under it. The 16px is
   the user's figure and it is the only hard-coded gap in this file — everything else uses the
   spacing scale, but a number that was asked for by eye should not silently follow a token that
   somebody retunes later. */
.hud__tl{grid-column:1;grid-row:1;display:flex;flex-direction:column;align-items:flex-start;
  gap:16px;pointer-events:auto}
/* THE BOARD AND THE MAP, SIDE BY SIDE, board first. A column put the map below five rows of
   names, which on a phone pushed it into the middle of the screen. */
.hud__tr{grid-column:2;grid-row:1;display:flex;flex-direction:row;gap:var(--sp-2);
  align-items:flex-start;pointer-events:auto}
.hud__bl{grid-column:1;grid-row:3;display:flex;flex-direction:column;gap:var(--sp-2);width:min(300px,52vw)}
.hud__br{grid-column:2;grid-row:3;display:flex;flex-direction:column;gap:var(--sp-2);align-items:flex-end}
/* THE NET READOUT. Under the meters, so it is the last thing in the bottom-left corner and the
   first thing to go when the corner is short. Monospace and tabular so the digits do not shuffle
   the line sideways four times a second — a number that jitters visually is a number nobody can
   read while playing, which is the whole point of showing it. */
.netline{display:flex;gap:var(--sp-2);align-items:baseline;font-family:var(--font-mono);
  font-size:10px;line-height:1.2;letter-spacing:var(--ls-wide);color:var(--text-muted);
  font-variant-numeric:tabular-nums;white-space:nowrap;text-shadow:0 1px 2px rgba(0,0,0,.9)}
.netline b{font-weight:700;color:var(--success)}
.netline.is-fair b{color:var(--warn)} .netline.is-bad b{color:var(--danger)}
/* A STALL IS NOT A HIGH PING and reads as neither: the round trip can be forty milliseconds
   right up to the moment a quarter of a second of it arrives at once. It gets its own mark. */
.netline i{font-style:normal;color:var(--danger);display:none}
.netline.is-stall i{display:inline}
body.touch .netline{font-size:9px;gap:var(--sp-1)}
.board{width:min(230px,44vw);padding:var(--sp-2) var(--sp-3);display:flex;flex-direction:column;gap:var(--sp-1)}
.board__row{display:flex;align-items:center;gap:var(--sp-2);font-size:var(--fs-xs);min-width:0}
.board__row .n{color:var(--text-muted);width:1.5rem;flex:none}
.board__row .nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)}
.board__row .sc{color:var(--cyan-bright);font-weight:700}
.board__row.me{color:var(--amber-bright)} .board__row.me .nm{color:var(--amber-bright)}
.board__row.me .sc{color:var(--amber-bright)} .board__row.dead .nm{color:var(--text-muted);text-decoration:line-through}
/* A SQUADMATE'S ROW. Cyan against your own amber, so at a glance the board answers two
   different questions — where am I, and where are my people — without either colour having to
   be learned. The icon carries it for anyone who cannot separate the two hues. */
.board__row.mate .nm{color:var(--cyan-bright)}
.board__row .mate-i{width:.85rem;height:.85rem;flex:none;color:var(--cyan-bright);opacity:.9}
/* the elision between the top of the board and your own pinned row */
.board__gap{font-size:var(--fs-xs);line-height:.7;color:var(--text-muted);text-align:center;
  letter-spacing:.2em}
/* THE INVITE LINK, a row in the pause menu. The url takes the width and the button is an icon,
   because the thing you want to check before you hand it over is the link itself. `user-select:
   all` is load-bearing rather than a nicety: one tap selects the whole url, which is the manual
   path on any page where the clipboard API is unavailable. */
.invrow{display:flex;align-items:center;gap:var(--sp-2);min-width:0;
  padding:var(--sp-2) var(--sp-3);background:var(--bg-inset);
  border:var(--border) solid var(--steel-line)}
.invrow__i{width:1rem;height:1rem;flex:none;fill:none;stroke:var(--cyan-bright);stroke-width:2}
.invrow__u{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font-family:var(--font-mono);font-size:var(--fs-xs);letter-spacing:var(--ls-wide);
  color:var(--text-muted);user-select:all;-webkit-user-select:all}
.invrow.ok{border-color:var(--success)} .invrow.ok .invrow__u{color:var(--success);user-select:none}
/* the queue's own number, big enough to be the answer to the only question on that screen */
.queue__n{font-family:var(--font-display);font-size:clamp(2.6rem,9vw,4.2rem);line-height:1;
  font-weight:700;color:var(--amber-bright);text-align:center}
.wpn{padding:var(--sp-2) var(--sp-3);display:flex;flex-direction:column;gap:var(--sp-1);min-width:148px}
.wpn__name{font-family:var(--font-display);font-weight:700;font-size:var(--fs-sm);letter-spacing:var(--ls-wide);
  text-transform:uppercase;color:var(--cyan-bright);text-align:right}
.wpn__art{display:block;width:100%;height:var(--wpn-art);align-self:flex-end}
.wpn__ammo{text-align:right;font-size:var(--fs-lg);font-weight:700;color:var(--text)}
.wpn__ammo small{font-size:var(--fs-xs);color:var(--text-muted)}
.wpn__ammo.low{color:var(--danger)}
/* grenade pouch: one pip per carry slot, filled ones glow. Desktop has no grenade button,
   so this row is the only place the held types are readable there. */
.wpn__nades{display:flex;align-items:center;justify-content:flex-end;gap:var(--sp-1);
  font-size:var(--fs-sm);line-height:1;color:var(--text-muted)}
.wpn__nades .pip{color:var(--steel-line)}
.wpn__nades .pip.on{color:var(--cyan-bright)}
.wpn__nades small{font-size:var(--fs-xs);letter-spacing:var(--ls-wide);margin-left:var(--sp-1)}
body.touch .wpn__nades{display:none}   /* the thumb button carries it there */
.hud__hint{grid-column:1/-1;grid-row:2;align-self:end;justify-self:center;font-size:var(--fs-xs);
  color:var(--text-muted);letter-spacing:var(--ls-wide);text-transform:uppercase;text-align:center;
  transition:opacity var(--dur-slow) linear}
.hud__hint.hide{opacity:0}
.lvlup{grid-column:1/-1;grid-row:2;align-self:center;justify-self:center;text-align:center;
  padding:var(--sp-3) var(--sp-5);pointer-events:none;opacity:0}
.lvlup.show{animation:lvl 2s var(--ease-out) forwards}
@keyframes lvl{0%{opacity:0;transform:translateY(10px)}15%{opacity:1;transform:none}75%{opacity:1}100%{opacity:0}}
.lvlup__t{font-family:var(--font-display);font-weight:700;letter-spacing:var(--ls-xwide);text-transform:uppercase;
  color:var(--amber-bright);text-shadow:var(--glow-amber);font-size:var(--fs-lg)}
.lvlup__s{font-size:var(--fs-xs);color:var(--text-cyan);letter-spacing:var(--ls-wide);text-transform:uppercase}
.hud__btns{display:flex;gap:var(--sp-2);align-items:center;flex-wrap:wrap}
#minimap{width:var(--mini);height:var(--mini);background:var(--bg-inset);border:var(--border) solid var(--steel-line);display:block}

/* ---- THE KILL FEED ----
   Read at a glance and then gone. No dismiss button, no pointer events at all: it is the one
   thing on this HUD you are never meant to interact with, and a target you might catch with a
   thumb is a target that eats a shot. Lines age out on their own clock (Feed.push).
   NEWEST AT THE TOP, because it hangs off the counters above it and a list that grows downward
   keeps its newest line in the same place while the old ones walk away. */
.feed{display:flex;flex-direction:column;gap:2px;pointer-events:none;max-width:min(340px,46vw)}
.feed__line{font-size:var(--fs-xs);letter-spacing:var(--ls-wide);color:var(--text-muted);
  background:var(--comms-bg);padding:2px var(--sp-2);
  border-left:2px solid var(--steel-line);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  animation:feedIn 160ms var(--ease-out)}
@keyframes feedIn{from{opacity:0;transform:translateX(-6px)}to{opacity:1;transform:none}}
.feed__line.out{opacity:0;transition:opacity 240ms linear}
.feed__line b{color:var(--text);font-weight:400}
/* your own kill, and the one that killed you: the two lines in this list that are about YOU */
.feed__line.mine{color:var(--amber-bright);border-left-color:var(--amber)}
.feed__line.mine b{color:var(--amber-bright)}
.feed__line.death{color:var(--danger);border-left-color:var(--danger)}
.feed__line.death b{color:var(--danger)}
/* Floating touch sticks: left/top mark where the thumb landed, the negative margin
   centres the ring on it. Events are handled on the canvas, so the rings themselves
   stay pointer-transparent. */
.stick{position:absolute;left:0;top:0;width:var(--stick-size);height:var(--stick-size);
  margin:calc(var(--stick-size) / -2) 0 0 calc(var(--stick-size) / -2);
  border:var(--border) solid var(--cyan-dim);border-radius:50%;
  background:radial-gradient(circle,var(--cyan-faint),transparent 70%);display:none;
  pointer-events:none;z-index:var(--z-hud);opacity:.72}
.stick.on{display:block}
.stick--r{border-color:var(--amber);background:radial-gradient(circle,rgba(240,168,48,.10),transparent 70%)}
.stick__knob{position:absolute;left:50%;top:50%;width:var(--knob);height:var(--knob);
  margin:calc(var(--knob) / -2) 0 0 calc(var(--knob) / -2);border-radius:50%;
  background:var(--bg-panel-2);border:var(--border-2) solid var(--cyan);box-shadow:var(--glow-cyan)}
.stick--r .stick__knob{border-color:var(--amber);box-shadow:var(--glow-amber)}
/* shop */
.shop{width:min(560px,100%)}
.shop__bank{margin-left:auto;margin-right:var(--sp-3);font-family:var(--font-display);font-weight:700;
  color:var(--amber-bright);font-variant-numeric:tabular-nums}
.shop__grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:var(--sp-2);
  margin-top:var(--sp-3);max-height:52vh;overflow-y:auto}
/* the room is not answering, so nothing here can be bought. Dimmed rather than removed: the
   wardrobe is still worth looking at, and hiding it would read as "your skins are gone". */
.shop__grid.is-off{opacity:.45;pointer-events:none}
.skin{display:flex;flex-direction:column;align-items:center;gap:var(--sp-1);padding:var(--sp-2);
  background:var(--bg-panel-2);border:var(--border) solid var(--steel-line);cursor:pointer;width:100%}
.skin:hover{border-color:var(--cyan-dim)}
.skin.on{box-shadow:var(--glow-amber)}
/* THE CARD IS A PORTRAIT NOW. Both pictures occupy the same box and cross-fade, so the tile
   never changes size when you point at it — a grid that reflows under the cursor is unusable.
   Without an authored portrait (.has-art absent) the sprite simply stays put at all times, which
   is the state every skin is in until its file lands. */
.skin__art{position:relative;width:96px;height:128px;display:block}
.skin__front,.skin__top{position:absolute;left:0;top:0;width:100%;height:100%;
  transition:opacity .14s ease}
.skin__front{object-fit:contain;opacity:0}
.skin__top{object-fit:contain;image-rendering:auto;opacity:1}
.skin.has-art .skin__front{opacity:1}
.skin.has-art .skin__top{opacity:0}
.skin.has-art:hover .skin__front,.skin.has-art:focus-visible .skin__front{opacity:0}
.skin.has-art:hover .skin__top,.skin.has-art:focus-visible .skin__top{opacity:1}
/* touch has no hover, so the swap would be unreachable: there the sprite rides in the corner as
   a permanent inset instead of a state. Same information, no interaction to discover. */
@media (hover:none){
  .skin.has-art .skin__top{opacity:1;left:auto;right:0;top:auto;bottom:0;width:42px;height:42px;
    background:rgba(6,12,17,.72);border:1px solid var(--steel-line);border-radius:4px}
  .skin.has-art:hover .skin__front{opacity:1}
}
@media (prefers-reduced-motion:reduce){ .skin__front,.skin__top{transition:none} }
.skin__n{font-size:var(--fs-xs);letter-spacing:.04em;color:var(--text);text-transform:uppercase}
/* rarity ramp: the tile border and the label carry it, so value reads before the price does */
.skin.r-common{border-color:var(--r-common)} .skin.r-common .skin__r{color:var(--r-common)}
.skin.r-rare{border-color:var(--r-rare)}     .skin.r-rare .skin__r{color:var(--r-rare)}
.skin.r-epic{border-color:var(--r-epic)}     .skin.r-epic .skin__r{color:var(--r-epic)}
.skin.r-leg{border-color:var(--r-leg)}       .skin.r-leg .skin__r{color:var(--r-leg)}
.skin.r-myth{border-color:var(--r-myth)}     .skin.r-myth .skin__r{color:var(--r-myth)}
.skin.r-epic{box-shadow:0 0 10px rgba(164,107,224,.28)}
.skin.r-leg{box-shadow:0 0 12px rgba(255,201,90,.32)}
.skin.r-leg .skin__n{color:var(--r-leg)}
/* mythic burns rather than glows: the tile animates, which is the only tile in the shop that
   does, and it is doing the same thing its footprints do on the floor */
.skin.r-myth{box-shadow:0 0 16px rgba(255,122,46,.38);animation:myth 2.6s ease-in-out infinite}
.skin.r-myth .skin__n{color:var(--r-myth)}
@keyframes myth{0%,100%{box-shadow:0 0 12px rgba(255,122,46,.30)}
                50%{box-shadow:0 0 22px rgba(255,122,46,.52)}}
@media (prefers-reduced-motion:reduce){ .skin.r-myth{animation:none} }
.skin__r{font-size:9px;letter-spacing:.1em;text-transform:uppercase}
.skin__c{font-size:var(--fs-xs);font-weight:700;color:var(--amber-bright);font-variant-numeric:tabular-nums}
.skin.owned .skin__c{color:var(--text-muted)}
.skin.on .skin__c{color:var(--amber-bright)}
/* skill tree — five branch columns, nodes stack downward and gate on spend */
.tree{width:min(720px,100%)}
.tree__pts{margin-left:auto;margin-right:var(--sp-3);font-family:var(--font-display);font-weight:700;
  color:var(--amber-bright);font-variant-numeric:tabular-nums}
.tree__lvl{display:flex;align-items:center;gap:var(--sp-2);margin-bottom:var(--sp-2)}
.tree__lvl .bar{flex:1}
.tree__xpn{font-size:var(--fs-xs);color:var(--text-muted);font-variant-numeric:tabular-nums}
.tree__grid{display:grid;grid-template-columns:repeat(5,1fr);gap:var(--sp-2);
  max-height:56vh;overflow-y:auto}
/* the node popup. Positioned by JS against the panel, so the panel is the containing block. */
.tree{position:relative}
.sktip{position:absolute;z-index:5;pointer-events:none;max-width:15rem;
  display:flex;flex-direction:column;gap:2px;
  padding:var(--sp-2) var(--sp-3);background:var(--bg-inset);
  border:var(--border) solid var(--cyan-dim,var(--steel-line));border-radius:var(--r-md);
  box-shadow:0 6px 18px rgba(2,6,10,.55)}
.sktip b{font-size:var(--fs-sm);letter-spacing:.04em;color:var(--text)}
.sktip span{font-size:var(--fs-xs);color:var(--cyan-bright)}
.sktip i{font-size:var(--fs-xs);font-style:normal;color:var(--text-muted)}
.respec{display:flex;align-items:center;gap:var(--sp-3);flex-wrap:wrap;margin-top:var(--sp-3)}
.respec .note{flex:1;min-width:12rem}
.respec button.armed{box-shadow:var(--glow-danger,0 0 12px rgba(240,72,72,.4))}
.tree__col{display:flex;flex-direction:column;gap:var(--sp-1);min-width:0}
.tree__bh{font-family:var(--font-display);font-weight:700;font-size:10px;letter-spacing:.1em;
  text-transform:uppercase;color:var(--cyan-bright);text-align:center;padding-bottom:2px;
  border-bottom:var(--border) solid var(--steel-line)}
.node{padding:var(--sp-2) 4px;background:var(--bg-panel-2);border:var(--border) solid var(--steel-line);
  cursor:pointer;text-align:center;width:100%;min-width:0}
.node:hover:not([aria-disabled="true"]){border-color:var(--cyan-dim)}
.node__n{display:block;font-size:9px;letter-spacing:.03em;color:var(--text);line-height:1.25}
.node__r{display:block;font-size:10px;font-weight:700;color:var(--cyan-bright);
  font-variant-numeric:tabular-nums;margin-top:2px}
.node__p{display:block;font-size:8px;letter-spacing:.14em;line-height:1;margin-top:3px;
  color:var(--steel-line)}
/* Invested nodes have to stay legible: `has` tints the whole cell and `max` goes green, and
   both opt out of the disabled dim, because "already bought" is exactly what the player is
   scanning for when they cannot afford anything. */
.node.has{background:var(--cyan-faint);border-color:var(--cyan-dim)}
.node.has .node__p{color:var(--cyan-bright)}
.node.max{border-color:var(--success);background:rgba(72,224,160,.10)}
.node.max .node__r{color:var(--success)} .node.max .node__p{color:var(--success)}
.node.can{border-color:var(--amber);box-shadow:var(--glow-amber)}
.node[aria-disabled="true"]{opacity:.4;cursor:default}
.node[aria-disabled="true"] .node__r{color:var(--text-muted)}
.node.has[aria-disabled="true"],.node.max[aria-disabled="true"]{opacity:.85}
.node.has[aria-disabled="true"] .node__r{color:var(--cyan-bright)}
.node.max[aria-disabled="true"] .node__r{color:var(--success)}
/* achievements */
.ach{width:min(520px,100%)}
.ach__list{display:flex;flex-direction:column;gap:var(--sp-2);max-height:58vh;overflow-y:auto;
  margin-top:var(--sp-2)}
.ach__it{padding:var(--sp-2) var(--sp-3);background:var(--bg-panel-2);
  border:var(--border) solid var(--steel-line);cursor:pointer;width:100%;text-align:left}
.ach__it:hover{border-color:var(--cyan-dim)}
.ach__it.got{border-color:var(--success)}
.ach__top{display:flex;align-items:baseline;gap:var(--sp-2)}
.ach__n{font-family:var(--font-display);font-weight:700;font-size:var(--fs-sm);
  text-transform:uppercase;letter-spacing:.06em;color:var(--text)}
.ach__it.got .ach__n{color:var(--success)}
.ach__p{margin-left:auto;font-size:var(--fs-xs);color:var(--text-muted);font-variant-numeric:tabular-nums}
.ach__d{font-size:var(--fs-xs);color:var(--text-cyan);margin-top:2px}
.ach__bar{height:4px;background:var(--bg-inset);border:var(--border) solid var(--steel-line);margin-top:var(--sp-1)}
.ach__fill{height:100%;background:var(--cyan);box-shadow:var(--glow-cyan);transform-origin:left;
  transition:transform var(--dur-base) var(--ease-out)}
.ach__it.got .ach__fill{background:var(--success)}
.ach__rare{display:none;font-size:var(--fs-xs);color:var(--amber-bright);margin-top:var(--sp-1)}
.ach__it.open .ach__rare{display:block}
/* raid results — rows land one by one, then drain into the bank */
.res{width:min(460px,100%)}
.res__rows{display:flex;flex-direction:column;gap:var(--sp-2)}
.res__row{display:flex;align-items:center;gap:var(--sp-3);opacity:0;transform:translateY(8px)}
.res__row.in{animation:resIn var(--dur-base) var(--ease-out) forwards}
@keyframes resIn{to{opacity:1;transform:none}}
.res__row .label{font-size:var(--fs-sm);color:var(--text-cyan);white-space:nowrap}
.res__row .lead{flex:1;border-bottom:var(--border) dashed var(--steel-line);min-width:var(--sp-4)}
.res__row .value{font-family:var(--font-display);font-weight:700;font-size:var(--fs-lg);
  color:var(--text);min-width:3.5rem;text-align:right;font-variant-numeric:tabular-nums}
.res__row.spent .value{color:var(--text-muted)}
.res__bank{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);
  margin-top:var(--sp-3);padding:var(--sp-3);background:var(--bg-inset);
  border:var(--border) solid var(--amber);box-shadow:var(--glow-amber)}
.res__money{font-family:var(--font-display);font-weight:700;font-size:var(--fs-2xl);
  color:var(--amber-bright);text-shadow:var(--glow-amber);font-variant-numeric:tabular-nums}
.res__money.tick{transform:scale(1.06)}
.res__money{transition:transform 90ms var(--ease-out)}
/* death */
#death .modal{width:min(420px,100%);text-align:center}
.death__who{font-family:var(--font-display);font-weight:700;letter-spacing:var(--ls-wide);text-transform:uppercase;
  color:var(--danger);font-size:var(--fs-xl);text-shadow:var(--glow-danger)}
.death__stats{display:flex;flex-direction:column;gap:var(--sp-2);text-align:left}
.hurt{position:absolute;inset:0;pointer-events:none;z-index:var(--z-hud);opacity:0;
  background:radial-gradient(ellipse at center,transparent 55%,var(--hurt-glow))}
.hurt.hit{animation:hurt var(--dur-slow) var(--ease-out)}
@keyframes hurt{from{opacity:1}to{opacity:0}}
.take{position:absolute;left:50%;bottom:calc(var(--sp-7) + env(safe-area-inset-bottom));
  transform:translateX(-50%);pointer-events:auto;z-index:var(--z-hud);white-space:nowrap}
@media (pointer:coarse){ .take{bottom:calc(var(--sp-7) + var(--stick-size) * .55 + env(safe-area-inset-bottom))} }
/* splash */
#splash{background:var(--bg);z-index:var(--z-toast)}
/* The boot wipe now runs over the wordmark itself — the separate mark fade went with the mark,
   which the logo already contains. One reveal instead of two things arriving at once. */
#splash h1{overflow:hidden;animation:bootTitle var(--dur-slow) var(--ease-step)}
@keyframes bootTitle{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0)}}
.title-xl{font-size:var(--fs-3xl);text-shadow:var(--glow-cyan)}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
@media (max-width:520px){
  :root{--stick-size:112px;--fs-3xl:2rem}
  .hud__bl{width:min(210px,50vw)} .board{width:min(160px,38vw)}
  .wpn{min-width:112px} #minimap{width:86px;height:86px}
}
@media (max-height:520px) and (pointer:fine){ #minimap{display:none} }

/* ===================== touch layout =====================
   Thumbs own the bottom corners, so every readout moves to the top: resources and
   comms top-left, minimap over the board top-right. Everything shrinks — on a phone
   the HUD competes with the playfield, and the playfield has to win. */
body.touch .fire{display:flex}
body.touch .hud{grid-template-rows:auto auto 1fr auto;gap:var(--sp-2);
  padding:max(var(--sp-2),env(safe-area-inset-top)) max(var(--sp-2),env(safe-area-inset-right))
    max(var(--sp-2),env(safe-area-inset-bottom)) max(var(--sp-2),env(safe-area-inset-left))}
body.touch .hud__tl{grid-row:1;gap:var(--sp-1)}
body.touch .hud__tl .chip{padding:2px var(--sp-2);font-size:10px;gap:4px}
body.touch .hud__tl .chip svg{width:12px;height:12px}
/* meters leave the bottom-left corner — that is the movement stick's seat now */
body.touch .hud__bl{grid-column:1;grid-row:2;width:min(150px,38vw);gap:2px}
body.touch .hud__bl .meter__top{font-size:9px}
body.touch .hud__bl .label{font-size:9px}
body.touch .hud__bl .bar{height:5px}
/* board LEFT of the map on a phone too — same reading order as the desktop, and it keeps the
   map hard against the corner where a thumb never goes */
body.touch .hud__tr{grid-column:2;grid-row:1/span 2;gap:var(--sp-1);flex-direction:row}
body.touch #minimap{width:78px;height:78px;display:block;order:2}
body.touch .board{width:min(126px,30vw);padding:3px var(--sp-2);gap:0;order:1}
body.touch .board__row{font-size:9px}
/* FIVE ROWS EVERYWHERE. It used to be three on touch, hiding the pinned "you" row that is the
   only one a player is actually looking for. Five short rows is 45px — less than the map beside
   them, so the corner does not grow. */
body.touch .feed{max-width:44vw}
body.touch .feed__line{font-size:9px;padding:1px var(--sp-1)}
body.touch .hud__tl{gap:16px}
body.touch .hud__btns .btn--icon{min-width:34px;min-height:34px;padding:0}
body.touch .hud__btns .btn--icon svg{width:15px;height:15px}
/* weapon read shrinks to name + ammo and sits above the fire button */
body.touch .hud__br{grid-row:4;padding-bottom:calc(var(--fire-size) + var(--sp-4))}
body.touch .wpn{min-width:0;padding:2px var(--sp-2);gap:0}
body.touch .wpn__art{display:none}
body.touch .wpn__name{font-size:9px}
body.touch .wpn__ammo{font-size:var(--fs-sm)}
body.touch .hud__hint{font-size:9px}
/* comms: two short lines, no close button — a thumb should never need to dismiss them */
body.touch #comms{top:var(--comms-top,152px);
  left:max(var(--sp-2),env(safe-area-inset-left));width:min(190px,44vw);gap:2px}
body.touch .comms__line{padding:3px var(--sp-2);font-size:9px;gap:4px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block}
body.touch .comms__x,body.touch .comms__caret{display:none}
.fire{position:absolute;display:none;align-items:center;justify-content:center;
  right:calc(var(--sp-4) + env(safe-area-inset-right));
  bottom:calc(var(--sp-5) + env(safe-area-inset-bottom));
  width:var(--fire-size);height:var(--fire-size);border-radius:50%;
  background:radial-gradient(circle,var(--danger-faint),transparent 72%);
  border:var(--border-2) solid var(--danger);color:var(--danger);
  pointer-events:auto;z-index:var(--z-hud);touch-action:none}
.fire svg{width:38%;height:38%;fill:none;stroke:currentColor;stroke-width:2}
.fire:active{background:var(--danger);color:var(--amber-ink);box-shadow:var(--glow-danger)}
/* a phone held sideways is ~360-430px tall: shrink the thumb controls to match */
@media (max-height:480px){
  body.touch{--stick-size:100px;--knob:42px;--fire-size:70px}
  /* 54px was unreadable — a 60x44 map in a square that size gives each tile under a pixel. The
     board beside it is five rows of 9px, about 45px tall, so this costs nothing in height. */
  body.touch #minimap{width:68px;height:68px}
  body.touch .board{width:min(124px,30vw)}
  body.touch .fire{bottom:calc(var(--sp-3) + env(safe-area-inset-bottom))}
}
#dbg{display:none;position:fixed;left:50%;top:8px;transform:translateX(-50%);z-index:calc(var(--z-toast) + 20);
  background:var(--scrim);color:var(--cyan-bright);font:11px/1.45 monospace;padding:6px 10px;
  border:var(--border) solid var(--cyan-dim);pointer-events:none;white-space:pre;text-align:left}
body.dbg #dbg{display:block}
/* grenade button rides above the fire button on touch */
.nadebtn{position:absolute;display:none;align-items:center;justify-content:center;
  right:calc(var(--sp-4) + env(safe-area-inset-right));
  bottom:calc(var(--sp-5) + var(--fire-size) + var(--sp-3) + env(safe-area-inset-bottom));
  width:calc(var(--fire-size) * .62);height:calc(var(--fire-size) * .62);border-radius:50%;
  background:radial-gradient(circle,var(--cyan-faint),transparent 72%);
  border:var(--border-2) solid var(--cyan);color:var(--cyan-bright);font-size:var(--fs-lg);
  pointer-events:auto;z-index:var(--z-hud);touch-action:none}
body.touch .nadebtn{display:flex}
.nadebtn:disabled{opacity:.35;border-color:var(--steel-line);color:var(--text-muted)}
.nadebtn:active:not(:disabled){background:var(--cyan);color:var(--amber-ink)}
/* out-of-combat healing: the health bar breathes while it climbs */
@keyframes regenPulse{0%,100%{filter:none}50%{filter:brightness(1.5)}}
.bar__fill.regen{animation:regenPulse 1s var(--ease-out) infinite}
/* language picker: a segmented control, because with two options a dropdown hides half the
   answer behind a tap and the point is to be readable when the UI is in a language you cannot
   read. Each label is written in its own language. */
.langrow{display:flex;align-items:center;gap:var(--sp-3)}
.langrow .label{flex:none}
.seg{display:flex;margin-left:auto;border:var(--border) solid var(--steel-line)}
.seg button{padding:var(--sp-1) var(--sp-3);background:transparent;border:0;cursor:pointer;
  font-family:var(--font-ui);font-size:var(--fs-xs);letter-spacing:var(--ls-wide);
  text-transform:uppercase;color:var(--text-muted)}
.seg button+button{border-left:var(--border) solid var(--steel-line)}
.seg button:hover{color:var(--text-cyan)}
.seg button[aria-pressed="true"]{background:var(--cyan);color:var(--amber-ink);font-weight:700}
/* flashbang white-out: opacity is driven from the sim, not a CSS animation */
.blind{position:absolute;inset:0;background:#fff;pointer-events:none;opacity:0;
  z-index:calc(var(--z-hud) + 1)}
/* rotate guard */
#rotate{position:fixed;inset:0;z-index:calc(var(--z-toast) + 10);display:none;
  flex-direction:column;align-items:center;justify-content:center;gap:var(--sp-3);
  background:var(--bg);color:var(--text-cyan);text-align:center;padding:var(--sp-5)}
body.portrait #rotate{display:flex}
.rotate__ico{width:72px;height:72px;color:var(--cyan);animation:rot 2.4s var(--ease-out) infinite}
@keyframes rot{0%,40%{transform:rotate(0)}60%,100%{transform:rotate(-90deg)}}
.rotate__t{font-family:var(--font-display);font-weight:700;letter-spacing:var(--ls-wide);
  text-transform:uppercase;color:var(--cyan-bright);font-size:var(--fs-lg)}
.rotate__s{font-size:var(--fs-sm);color:var(--text-muted)}
@media (prefers-reduced-motion:reduce){ *{animation-duration:1ms!important;transition-duration:1ms!important} }
