/* Surviversus — shared styles. Menus wear the bossfeed parchment/ink/crimson
   identity (EB Garamond display + Inter UI); in-game HUD is dark + minimal. */
:root {
  --paper: #f5efe2;
  --ink: #2b2118;
  --ink-soft: #6a6253;
  --line: rgba(0, 0, 0, .18);
  --crimson: #b3242e;
  --gold: #a9802f;
  /* WHAT A BUTTON MEANS, as two colours. See "the button system" below.
     `--go` is the same crimson the brand already uses; it is named separately
     because the meaning is what the rules key on, not the hue.
     `--exit` is deliberately COOL against a warm palette — on the game-over
     screen "play again" and "leave" sit next to each other, and those two must
     never be one glance apart. Two tones because a slate that reads on
     parchment disappears on the dark HUD. */
  --go: #b3242e;
  --exit: #66727e;
  --exit-dark: #a3b2be;
  --hud-bg: rgba(16, 13, 10, .72);
  --hud-line: rgba(255, 255, 255, .14);
  --ui: Inter, system-ui, -apple-system, sans-serif;
  --display: 'EB Garamond', Georgia, serif;
}
* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { margin: 0; padding: 0; }
body { font-family: var(--ui); background: #171310; color: var(--paper); }

/* ------------------------------------------------- landing (index.html) */
body.landing {
  background: var(--paper); color: var(--ink);
  min-height: var(--app-h, 100vh); display: flex; flex-direction: column; align-items: center;
  padding: 24px 16px 60px;
  -webkit-tap-highlight-color: transparent;   /* the game pages already reset it */
}
.logo {
  font-family: var(--display); font-weight: 700; letter-spacing: .5px;
  font-size: clamp(30px, calc(var(--app-w, 100vw) * 0.09), 84px); margin: 24px 0 4px; color: var(--ink);
  text-align: center; line-height: 1;
}
.logo .bang { color: var(--crimson); }
/* THE SITE'S EXP BAR RESERVES NOTHING. It is position:fixed at the top and publishes its height as
   --fb-bar-h with `fb-has-bar` on <html> (games/_back-to-games.js); consuming it is the page's job.
   Measured 2026-09-07 on three phone sizes: the logo cleared it by 11px at every one — which is the
   body's 24px padding plus the logo's 24px margin happening to beat a 37px bar, not a decision, and
   a bar that ever wraps to two lines erases it. Stated as a rule instead. */
html.fb-has-bar body.landing { padding-top: calc(var(--fb-bar-h, 0px) + 12px); }
html.fb-has-bar .logo { margin-top: 0; }
.tagline { color: var(--ink-soft); margin: 0 0 34px; font-size: 15px; text-align: center; }
/* Section heading over each pair of cards. Small and quiet: it is a signpost
   between two decisions, not a headline competing with the logo. */
/* YOUR RECORD on the menu: a trip meter over an odometer, under the size picker, quiet enough
   that the mode cards stay the loudest thing on the page. Two rows of the same three cells, so
   the eye compares straight down a column. */
.statblock { margin: 0 0 26px; display: flex; flex-direction: column; gap: 6px; align-items: center; }
/* A GRID, not a flex row, and the same one on both: the reset button is a real column that the
   all-time row simply leaves empty, so the three numbers line up straight down. As a flex row the
   button's width pushed the session's cells left and the columns did not agree. */
.statrow {
  display: grid; grid-template-columns: 84px repeat(3, 88px) 76px;
  gap: 10px; align-items: center; justify-content: center;
}
/* Fixed width so both labels end at the same place and the columns line up under each other. */
.statlabel {
  font: 700 11px var(--ui); letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-soft); text-align: right;
}
/* ADJUST, not commit: outlined, never filled, and disabled while the trip meter is already zero. */
.statreset {
  font: 600 11px var(--ui); letter-spacing: .06em; text-transform: uppercase;
  padding: 7px 12px; border-radius: 10px; border: 1.5px solid var(--line);
  background: transparent; color: var(--ink-soft); cursor: pointer;
}
.statreset:hover:not(:disabled) { background: #fff; color: var(--ink); }
.statreset:disabled { opacity: .35; cursor: default; }
.statnone { font: 500 12px var(--ui); color: var(--ink-soft); margin: 2px 0 0; text-align: center; }
/* The phone stacks: the label goes above its row rather than beside it, or three cells and a
   button on one 390px line wrap into an unreadable pile. */
@media (max-width: 560px) {
  .statrow { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; max-width: 340px; }
  .statlabel { grid-column: 1 / -1; width: auto; text-align: center; }
  .statreset { grid-column: 1 / -1; justify-self: center; margin-top: 2px; }
  .statcell { min-width: 0; padding: 6px 8px; }
}
.statcell {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  min-width: 76px; padding: 8px 14px; border-radius: 12px;
  background: #fff8; border: 1.5px solid var(--line);
}
.statcell b { font: 700 20px var(--display); color: var(--ink); line-height: 1; }
.statcell span { font: 600 11px var(--ui); letter-spacing: .06em; text-transform: uppercase; color: var(--ink-soft); }
body.landing.hasscene .statcell { background: #fdfaf2; }
.modehead {
  font: 700 12px var(--ui); letter-spacing: 1.4px; text-transform: uppercase;
  color: var(--ink-soft); margin: 0 0 12px; text-align: center;
}
.modehead + .menucards { margin-bottom: 32px; }
.menucards { display: flex; flex-wrap: wrap; gap: 18px; justify-content: center; max-width: 900px; }
.menucard {
  background: #fff9; border: 1.5px solid var(--line); border-radius: 16px;
  /* max-width, not just width: 290px alone overflows a 320px phone once the
     body's 16px of padding is taken off each side. */
  padding: 22px 24px 20px; width: 290px; max-width: 100%; cursor: pointer; text-align: left;
  font-family: var(--ui); font-size: inherit; color: var(--ink);
  transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease;
  text-decoration: none; display: flex; flex-direction: column; align-items: stretch;
}
/* Hover states everywhere on this sheet are gated on a pointer that CAN hover:
   a tap on a touch screen leaves :hover set until the next tap lands somewhere
   else, so the lift, the gold ring and the crimson border all stuck (2026-09-05). */
@media (hover: hover) {
  .menucard:hover { transform: translateY(-3px); box-shadow: 0 14px 30px -18px #0008; }
}
.menucard h2 { margin: 0 0 6px; font-family: var(--display); font-size: 26px; text-align: left; }
.menucard p { margin: 0; color: var(--ink-soft); font-size: 13.5px; line-height: 1.5; }
.menucard .emoji { font-size: 30px; display: block; margin-bottom: 8px; }
/* WHO YOU ARE PLAYING WITH, stated before the mode's name. This is the one fact
   that separates the four cards, and it was previously only implied by the prose
   inside them — so "Play Solo" and the melee looked like the same offer. */
.modetag {
  align-self: flex-start; font: 700 10.5px var(--ui); letter-spacing: .7px;
  text-transform: uppercase; color: var(--ink-soft);
  background: #0000000a; border: 1px solid var(--line); border-radius: 999px;
  padding: 3px 9px; margin-bottom: 12px;
}
/* Online is the exception worth colouring — it is the only mode that needs the
   internet, and the only one where the other survivors are people. */
.modetag.online {
  color: var(--crimson);
  /* Plain rgba first, color-mix second: a browser too old for color-mix drops
     that line as invalid and keeps the fallback, rather than falling all the way
     back to the neutral border and losing the distinction this tag exists for. */
  border-color: rgba(179, 36, 46, .45);
  border-color: color-mix(in srgb, var(--crimson) 45%, transparent);
  background: rgba(179, 36, 46, .07);
  background: color-mix(in srgb, var(--crimson) 7%, transparent);
}
.cardgo { margin-top: 14px; font: 700 13px var(--ui); color: var(--crimson); }
@media (hover: hover) { .menucard:hover .cardgo { text-decoration: underline; } }
.menucard:disabled { opacity: .75; cursor: default; transform: none; box-shadow: none; }
.menucard:focus-visible { outline: 2px solid var(--crimson); outline-offset: 3px; }

/* ------------------------------------------------- game size (index.html)
   ONE CONTROL, because it is ONE DECISION. How many survivors and which map
   are not two questions: thirty players on the eight-player island is not "the
   big mode", it is the small mode with everybody standing on each other, and
   eight players rattling around The Expanse never meet. The table that pairs
   them lives in js/presets.js (SIZES) and this only renders it.

   Above the cards, not inside one, because it applies to every mode below it. */
.sizepick {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  justify-content: center; margin: 0 0 18px;
  background: #fff9; border: 1.5px solid var(--line); border-radius: 999px;
  padding: 8px 8px 8px 18px; max-width: 100%;
}
.sizepick > label {
  font: 700 11px var(--ui); letter-spacing: 1.1px; text-transform: uppercase;
  color: var(--ink-soft);
}
.sizepick select {
  font: 700 14px var(--ui); padding: 9px 14px; border-radius: 999px;
  border: 1.5px solid var(--line); background: #fff; color: var(--ink); cursor: pointer;
  max-width: min(320px, 62vw);
}
.sizenote {
  width: 100%; text-align: center; margin: -8px 0 26px;
  font: 400 12.5px/1.5 var(--ui); color: var(--ink-soft);
}
/* The size a card will actually open at, printed on the card. A dropdown one
   screen-region away from the button it changes is a dropdown people do not
   believe changed anything. */
.cardsize {
  align-self: flex-start; margin-top: 10px;
  font: 700 10.5px var(--ui); letter-spacing: .5px; text-transform: uppercase;
  color: var(--gold);
}

/* How to play. Below the cards on purpose — the rules used to be split across
   three card descriptions, so you only learned the controls by reading a box
   you might never click. */
.howto { max-width: 620px; margin: 44px auto 0; text-align: left; }
.howto h2 {
  font: 700 12px var(--ui); letter-spacing: 1.4px; text-transform: uppercase;
  color: var(--ink-soft); margin: 0 0 14px; text-align: center;
}
.howto ol {
  margin: 0; padding: 0; list-style: none;
  counter-reset: howstep; display: grid; gap: 10px;
}
.howto li {
  counter-increment: howstep; position: relative;
  padding: 0 0 0 34px; color: var(--ink-soft);
  font: 400 13.5px/1.55 var(--ui);
}
.howto li::before {
  content: counter(howstep); position: absolute; left: 0; top: 1px;
  width: 22px; height: 22px; border-radius: 50%;
  background: #0000000d; border: 1px solid var(--line);
  color: var(--ink); font: 800 11px var(--ui);
  display: flex; align-items: center; justify-content: center;
}
.howto b { color: var(--ink); font-weight: 700; }

/* ------------------------------------------------- quick links (go.html) */
.linkopts {
  display: flex; flex-wrap: wrap; gap: 14px; justify-content: center;
  margin: 0 0 24px; max-width: 700px;
}
.linkopts label {
  display: flex; flex-direction: column; gap: 5px;
  font: 700 11px var(--ui); letter-spacing: .6px; text-transform: uppercase;
  color: var(--ink-soft);
}
.linkopts .hint { text-transform: none; letter-spacing: 0; font-weight: 400; opacity: .8; }
.linkopts input, .linkopts select {
  font: 400 14px var(--ui); padding: 8px 10px; min-width: 150px;
  border: 1.5px solid var(--line); border-radius: 9px; background: #fff; color: var(--ink);
}
.linklist { display: grid; gap: 14px; width: 100%; max-width: 700px; }
.linkrow {
  background: #fff9; border: 1.5px solid var(--line); border-radius: 14px;
  padding: 18px 20px; text-align: left;
}
.linkrow h2 { margin: 0 0 4px; font-family: var(--display); font-size: 22px; }
.linkrow p { margin: 0; color: var(--ink-soft); font: 400 13.5px/1.5 var(--ui); }
.linkrow .modetag { margin-bottom: 8px; }
/* The URL is the product here, so it gets to be selectable, wrappable and
   monospaced rather than a tooltip on a button. It must never widen the card:
   a query string has no spaces to break at. */
.linkurl {
  margin: 12px 0 10px; padding: 9px 11px;
  background: #0000000a; border: 1px solid var(--line); border-radius: 8px;
  font: 400 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: var(--ink); overflow-wrap: anywhere; user-select: all;
}
.linkacts { display: flex; gap: 8px; flex-wrap: wrap; }
.linkacts .ghostbtn2 { text-decoration: none; text-align: center; }

/* On a phone the cards are already one per row, so the section headings do the
   whole job of separating the two decisions — give them room and tighten the
   cards, which otherwise push "How to play" a long way down the page. */
@media (max-width: 620px) {
  .menucard { padding: 18px 18px 16px; }
  .menucard h2 { font-size: 23px; }
  .menucard .emoji { font-size: 26px; }
  .modehead { margin-top: 4px; }
  .modehead + .menucards { margin-bottom: 26px; }
  .howto { margin-top: 34px; }
}
.joinrow { display: flex; gap: 8px; margin-top: 12px; }
.joinrow input {
  flex: 1; min-width: 0; font: 700 20px var(--ui); letter-spacing: 4px; text-transform: uppercase;
  padding: 10px 12px; border: 1.5px solid var(--line); border-radius: 10px; background: #fff;
  color: var(--ink); text-align: center;
}
.joinrow button {
  font: 600 15px var(--ui); padding: 10px 16px; border-radius: 10px; border: none;
  background: var(--crimson); color: #fff; cursor: pointer;
}
.footnote { margin-top: 40px; color: var(--ink-soft); font-size: 12.5px; text-align: center; line-height: 1.7; }
.footnote a { color: var(--crimson); }
.linkbtn {
  background: none; border: none; padding: 0; font: inherit; color: var(--crimson);
  text-decoration: underline; cursor: pointer;
}

/* ------------------------------------------------- tuning panel
   NOTE the doubled class selectors: the generic .sheetcard rules (dark, for
   the in-game pause/victory sheets) are defined LATER in this file, so a bare
   .tunecard would lose to them and render dark text on a dark card. */
.tunesheet { align-items: flex-start; overflow-y: auto; padding: 20px 14px 40px; }
.sheetcard.tunecard {
  background: var(--paper); color: var(--ink); border-color: var(--line);
  width: min(600px, 96vw); max-height: none; gap: 0;
}
.tunehead { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.sheetcard.tunecard h2 { text-align: left; color: var(--ink); }
.tuneclose {
  background: none; border: none; font-size: 26px; line-height: 1; color: var(--ink-soft);
  cursor: pointer; padding: 0 4px;
}
.tuneintro { margin: 6px 0 4px; font-size: 13px; color: var(--ink-soft); line-height: 1.55; }
.tuneintro b { color: var(--ink); }
.tunebody { max-height: 58vh; overflow-y: auto; margin: 10px 0; padding-right: 6px; }
.tunecard .tune-group {
  font-family: var(--display); font-size: 18px; font-weight: 700; margin: 18px 0 6px;
  padding-bottom: 4px; border-bottom: 1px solid var(--line); color: var(--ink);
}
.tune-group:first-child { margin-top: 0; }
.tune-row { padding: 7px 0 9px; border-bottom: 1px solid var(--line-2, rgba(0,0,0,.06)); }
.tune-row.changed .tune-val { color: var(--crimson); }
.tune-row.changed .tune-label > span::after { content: " •"; color: var(--crimson); }
.tunecard .tune-label {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  font: 600 13px var(--ui); color: var(--ink);
}
.tune-label em { color: var(--ink-soft); font-weight: 400; font-style: italic; font-size: 11.5px; }
.tune-val { font-variant-numeric: tabular-nums; color: var(--ink-soft); }
.tune-row input[type="range"] { width: 100%; margin: 5px 0 0; accent-color: var(--crimson); }
.tune-help { margin: 3px 0 0; font-size: 11.5px; color: var(--ink-soft); line-height: 1.45; }
.tunestatus { min-height: 18px; font-size: 12.5px; color: var(--crimson); text-align: center; }
.tunejson {
  width: 100%; font: 11px/1.4 ui-monospace, Consolas, monospace; margin: 8px 0 0;
  border: 1px solid var(--line); border-radius: 8px; padding: 8px; background: #fff; color: var(--ink);
}
/* item portal */
.itemtabs { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 2px; }
.itemtab {
  font: 600 12.5px var(--ui); padding: 7px 12px; border-radius: 999px;
  border: 1.5px solid var(--line); background: #fff8; color: var(--ink-soft); cursor: pointer;
}
.itemtab.on { border-color: var(--crimson); color: var(--crimson); background: #fff; }
.itemcard { border: 1px solid var(--line); border-radius: 10px; margin: 6px 0; background: #fff9; }
.itemcard.isnew { border-color: var(--gold); }
.itemcard > summary {
  cursor: pointer; padding: 8px 12px; font: 600 13.5px var(--ui); color: var(--ink);
  display: flex; align-items: baseline; gap: 8px;
}
.itemcard > summary::-webkit-details-marker { display: none; }
.itemid { font: 500 11px ui-monospace, Consolas, monospace; color: var(--ink-soft); }
.newtag {
  font: 700 9.5px var(--ui); letter-spacing: .08em; text-transform: uppercase;
  color: #fff; background: var(--gold); border-radius: 999px; padding: 2px 7px;
}
.itemfields {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 8px; padding: 4px 12px 10px;
}
.itemfield { display: flex; flex-direction: column; gap: 3px; font: 600 11px var(--ui); color: var(--ink-soft); }
.itemfield.wide { grid-column: 1 / -1; }
.itemfield select,
.itemfield input[type="text"], .itemfield input[type="number"] {
  font: 500 12.5px var(--ui); padding: 5px 8px; border-radius: 7px;
  border: 1.5px solid var(--line); background: #fff; color: var(--ink); width: 100%;
}
.itemfield input.bad { border-color: var(--crimson); background: #fff0ef; }
.rarityprev { margin: 0 12px 10px; padding: 8px 10px; border-radius: 8px; background: #00000008; border: 1px solid var(--line); }
.rarityhead { font: 500 11px/1.5 var(--ui); color: var(--ink-soft); margin-bottom: 5px; }
.raritytable { border-collapse: collapse; font: 500 11.5px var(--ui); color: var(--ink); }
.raritytable td { padding: 2px 12px 2px 0; white-space: nowrap; }
.raritytable .rr { font-weight: 800; text-transform: capitalize; }

.itemdel {
  margin: 0 12px 10px; font: 600 11.5px var(--ui); color: var(--crimson);
  background: none; border: 1px solid var(--line); border-radius: 7px; padding: 5px 10px; cursor: pointer;
}
@media (hover: hover) { .itemdel:hover { border-color: var(--crimson); } }

.tunebtns { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.tunebtns .bigbtn { width: auto; flex: 1 1 100%; }
.ghostbtn2 {
  flex: 1 1 46%; font: 600 13px var(--ui); padding: 10px 12px; border-radius: 10px;
  border: 1.5px solid var(--line); background: #fff8; color: var(--ink); cursor: pointer;
}
@media (hover: hover) { .ghostbtn2:hover { border-color: var(--crimson); color: var(--crimson); } }

/* ------------------------------------------------- join screen (play) */
/* The game surface must never scroll, rubber-band, text-select or show a tap
   highlight under a dragging thumb — iOS Safari does all four by default. */
body.gamepage {
  overflow: hidden; position: fixed; inset: 0;
  /* --app-h is the VISUAL viewport, published by js/ui/viewport.js. `100%` of a
     fixed box, like `var(--app-h, 100vh)`, is measured against the page as it would be with
     the URL bar retracted — so on a phone the bottom ~60px of this element, the
     strip that holds the thumbstick and the item dock, sat underneath the
     browser chrome and could be neither seen nor touched. The percentage stays
     as the fallback for the frame before the module runs. */
  width: var(--app-w, 100%); height: var(--app-h, 100%);
  touch-action: none; overscroll-behavior: none;
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}
.joinscreen {
  position: fixed; inset: 0; z-index: 40; background: var(--paper); color: var(--ink);
  height: var(--app-h, 100%);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  /* `center` + `overflow` is the trap the TV lobby documents: once the content
     is taller than the box, centring pushes the first item off the TOP and no
     amount of scrolling reaches it. On a phone with the keyboard up, that is
     the name field. */
  justify-content: safe center;
  padding: 18px; gap: 8px; overflow-y: auto;
}
.joinscreen .logo { font-size: clamp(26px, calc(var(--app-w, 100vw) * 0.08), 56px); margin: 0 0 18px; }
.card {
  background: #fff9; border: 1.5px solid var(--line); border-radius: 16px;
  padding: 22px; width: min(360px, 92vw); display: flex; flex-direction: column; gap: 12px;
}
.fieldlabel { font-size: 12.5px; font-weight: 600; color: var(--ink-soft); letter-spacing: .4px; text-transform: uppercase; }
#nameInput {
  font: 700 22px var(--ui); letter-spacing: 2px; text-transform: uppercase;
  padding: 12px; border: 1.5px solid var(--line); border-radius: 10px; background: #fff; color: var(--ink);
  text-align: center; width: 100%;
}
.joininfo { font-size: 13.5px; color: var(--ink-soft); text-align: center; }
/* ------------------------------------------------- the button system
   FOUR SHAPES, and the shape carries the meaning — the same trick the power-up
   chips play with circle / triangle / square. Colour alone cannot do this job
   here: this UI is read on a TV from three metres, on a phone at arm's length,
   and by people who do not see red and green as different things.

     filled rounded rect   .bigbtn     GO FORWARD — start, join, resume, play
                                       again, take over. One per screen.
     outlined rounded rect .ghostbtn   ADJUST — add a bot, fill, remove, export.
                           .ghostbtn2  Never final, never destructive.
     outlined PILL, slate  .exitbtn    LEAVE — back, exit to menu, leave match.
     circle                .iconbtn    A TOOL — settings, the spectate arrows.

   An exit is never filled and never crimson, anywhere in the game. A player
   who has learnt "the red block is forward" on the menu must be able to spend
   that knowledge on the death screen without reading a word. */
.bigbtn {
  font: 700 18px var(--ui); padding: 14px 18px; border-radius: 12px;
  border: 2px solid var(--go); background: var(--go); color: #fff;
  cursor: pointer; width: 100%;
  transition: filter .14s ease, transform .1s ease;
}
@media (hover: hover) { .bigbtn:hover:not(:disabled) { filter: brightness(1.09); } }
.bigbtn:active:not(:disabled) { transform: translateY(1px) scale(.995); }
/* Disabled has to look UNAVAILABLE rather than broken — half-opacity alone
   reads as "still loading" on a dark sheet. It keeps its outline and loses its
   fill, which is the same grammar the dashed empty item slot uses. */
.bigbtn:disabled {
  opacity: 1; cursor: default;
  background: transparent; border-color: rgba(255,255,255,.22); color: #8b8577;
}
.landing .bigbtn:disabled, .tvlobby .bigbtn:disabled, .joinscreen .bigbtn:disabled {
  border-color: var(--line); color: var(--ink-soft); background: #0000000a;
}

.exitbtn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  font: 700 14px var(--ui); padding: 10px 20px;
  /* A PILL, so it is not the go button's silhouette even in peripheral vision. */
  border-radius: 999px;
  border: 1.5px solid var(--exit); background: transparent; color: var(--exit);
  cursor: pointer; text-decoration: none; width: auto; margin: 0 auto;
  /* Both, on purpose. Most of the places this lands are flex columns where a
     stretched pill would be indistinguishable from the go button above it;
     `margin: auto` centres it in the ones that are not. */
  align-self: center;
  transition: background .14s ease, border-color .14s ease;
}
/* The glyph is part of the identity: an exit is a pill, it is slate, and it
   points backwards. Three signals, so no one of them has to be perfect. */
.exitbtn::before { content: "←"; font-size: 15px; line-height: 1; }
@media (hover: hover) { .exitbtn:hover { background: rgba(102,114,126,.12); } }
/* On the dark surfaces — sheets, the death bar, anything over the play area. */
/* Every DARK surface in the game, listed rather than inferred: the light tone
   is unreadable on #171310 and the dark tone is unreadable on parchment, and
   both pages exist. `body.lab` is the icon lab. */
.sheet .exitbtn, .deathbar .exitbtn, .panechrome .exitbtn,
.tvover .exitbtn, #hud .exitbtn, .spotcol .exitbtn, body.lab .exitbtn {
  border-color: var(--exit-dark); color: var(--exit-dark);
}
@media (hover: hover) {
  .sheet .exitbtn:hover, .deathbar .exitbtn:hover, .panechrome .exitbtn:hover,
  .tvover .exitbtn:hover, #hud .exitbtn:hover, body.lab .exitbtn:hover {
    background: rgba(163,178,190,.14);
  }
}

.iconbtn {
  width: 40px; height: 40px; flex: none; padding: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1.5px solid var(--hud-line); background: var(--hud-bg); color: #e8ddc8;
  font-size: 18px; line-height: 1; cursor: pointer;
}
@media (hover: hover) { .iconbtn:hover { border-color: var(--gold); color: #ffd34d; } }

/* A visible focus ring on everything, in the accent that suits the surface. */
.bigbtn:focus-visible, .exitbtn:focus-visible, .ghostbtn:focus-visible,
.ghostbtn2:focus-visible, .iconbtn:focus-visible, .paneltab:focus-visible,
.linkbtn:focus-visible, .sizepick select:focus-visible, .joinrow input:focus-visible,
#nameInput:focus-visible, #codeInput:focus-visible {
  outline: 2px solid var(--crimson); outline-offset: 2px;
}
.joinscreen .exitbtn { color: var(--exit); border-color: var(--exit); }
.sheet .bigbtn:focus-visible, .sheet .exitbtn:focus-visible,
.deathbar .bigbtn:focus-visible, .deathbar .exitbtn:focus-visible,
.deathbar .iconbtn:focus-visible, #hud .iconbtn:focus-visible,
.panechrome .bigbtn:focus-visible, .panechrome .exitbtn:focus-visible,
.tvover .bigbtn:focus-visible, .tvover .exitbtn:focus-visible {
  outline-color: var(--gold);
}

/* Kept for genuinely tertiary text actions. NOT for leaving — that is .exitbtn
   everywhere now, so "the way out" has one appearance in the whole game. */
.textbtn { background: none; border: none; color: var(--ink-soft); font: 600 14px var(--ui); cursor: pointer; text-decoration: underline; text-align: center; display: block; margin: 4px auto 0; }
.sheet .textbtn { color: #cbbfa8; }
.joinstatus { font-size: 13.5px; color: var(--crimson); text-align: center; min-height: 18px; }
.backlink { color: var(--ink-soft); font-size: 13px; text-align: center; text-decoration: none; }
.lobbytitle { font-family: var(--display); font-size: 24px; font-weight: 700; text-align: center; }
.lobbyplayers { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.lobbytile {
  display: flex; align-items: center; gap: 7px; background: #fff; border: 1.5px solid var(--line);
  border-radius: 999px; padding: 6px 13px; font: 600 13.5px var(--ui);
}
.lobbytile .dot { width: 11px; height: 11px; border-radius: 50%; }
.lobbytile.bot { opacity: .65; }
/* THE BUILD STAMP (js/ui/version-tag.js). Bottom-right on every page, inert
   to every pointer, a fixed layer above everything so a lobby, a match and a
   results card all show it. On a coarse screen in a match the item dock owns
   that corner (bottom: max(16px, inset)); the tag sits under its bottom edge
   in the inset band, which is empty by definition. Never brighter than this:
   it is read when looked for, not noticed. */
/* Anchored off `--app-h` like .overpill, never `bottom`: a fixed box's bottom is
   the LAYOUT viewport, under the URL bar on a phone, so the tag sat below the
   fold. The landing page never sets the variable and keeps the old edge. */
.vtag {
  position: fixed; z-index: 200; pointer-events: none; user-select: none;
  right: max(6px, env(safe-area-inset-right));
  top: var(--app-h, 100%); transform: translateY(-100%); margin-top: -3px;
  font: 600 9.5px/1 var(--ui); letter-spacing: .04em; color: #fff; opacity: .42;
  text-shadow: 0 1px 2px #000c; white-space: nowrap;
}
.vtag.coarse { margin-top: calc(-1 * max(2px, calc(env(safe-area-inset-bottom) - 12px))); }
/* The stamp is how you confirm which build is loaded, so it stays bottom-RIGHT. On a narrow
   screen that corner is the item dock and the 92px Active button, so it LIFTS above them
   rather than moving: same corner, still readable, nothing drawn over. Swapped, the dock is
   on the left and there is nothing to clear. */
/* ...and NOT on the menu, which has no dock to clear. The landing page also SCROLLS, so a
   214px lift off a fixed bottom edge put the stamp in the middle of the page on a phone
   (the owner's screenshot, 2026-09-07). Only a game page owns a dock. */
body.narrow:not(.landing) .vtag { margin-top: calc(-214px - max(16px, env(safe-area-inset-bottom))); }
body.narrow #hud.swapped ~ .vtag, body.narrow .vtag.swapped { margin-top: -3px; }
.waitnote { color: var(--ink-soft); font-size: 13.5px; text-align: center; }
/* status() writes bad news here once you are seated (main-play.js): the join
   card it used to write into is display:none by then. */
.waitnote.bad { color: var(--crimson); }

/* ------------------------------------------------- game canvas + HUD */
/* Both sized from the visual viewport — see body.gamepage. An `inset: 0` box is
   NOT the visible area on a phone, and the HUD is anchored to its bottom edge. */
#game { position: fixed; inset: 0; width: var(--app-w, 100%); height: var(--app-h, 100%); display: block; background: #20261c; }

/* THE PHONE FRAME (ui/viewport.js): the handset layout, letterboxed on a desktop screen.
   --app-w/h are already the phone-shaped box, so the ONE thing this has to do is make
   `position: fixed` mean "the phone" instead of "the monitor".

   A transform on an element makes it the containing block for every fixed descendant.
   That is the whole trick, and it is why this is four rules rather than a list of every
   overlay: the pause sheet, the results card, tooltips, toasts, the death bar, the
   version tag and anything added later are all fixed to the frame automatically. Moving
   #game and #hud by hand (the first cut) left every one of them centred on the monitor. */
/* THE MENU BACKDROP (ui/menu-scene.js): the game's own terrain, generated and painted
   behind the cards, with the scrim baked into the canvas so nothing here has to guess. */
.menuscene {
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  width: 100%; height: 100%; display: block;
}
/* The page's flat paper would cover the canvas, so it steps aside — and the cards go opaque,
   because a translucent card over terrain is exactly where text stops being readable.
   BOTH menus are named: client/index.html uses .card/.sizebox, the packaged template
   .menucard/.sizepick, and styling one of them leaves the other see-through. */
body.landing.hasscene { background: transparent; }
body.landing.hasscene .card,
body.landing.hasscene .menucard,
body.landing.hasscene .sizebox,
body.landing.hasscene .sizepick { background: #fdfaf2; box-shadow: 0 10px 30px -14px #0007; }
/* The loose text — tagline, section heads, the size note — has no card under it, so it gets a
   paper halo instead. Cheaper than another panel and it keeps the terrain visible behind. */
body.landing.hasscene .tagline,
body.landing.hasscene .sizenote,
body.landing.hasscene .modehead,
body.landing.hasscene .footnote { text-shadow: 0 1px 0 #f4eee0, 0 0 8px #f4eee0; }
body.landing.hasscene .logo { text-shadow: 0 2px 0 #f4eee0, 0 0 18px #f4eee0cc; }
@media (prefers-reduced-motion: reduce) { .menuscene { display: none; } }

html.phoneframe { background: #0d0f0a; overflow: hidden; }
/* The letterbox needs a reason or it reads as a broken layout. It lives in the MARGIN beside
   the phone, never across it — the frame is usually full-height, so a centred line at the
   bottom of the window ran straight over the game. The max-width is the margin itself, so
   when there is no margin (a narrow window) it computes to 0 and the caption disappears. */
html.phoneframe::after {
  content: 'Phone view — built for a handset. Switch views from the menu, or in Settings.';
  position: fixed; left: 14px; bottom: 14px;
  max-width: calc((100vw - var(--app-w, 100vw)) / 2 - 28px);
  font: 500 12px var(--ui); color: #6f7364; pointer-events: none; letter-spacing: .2px;
  overflow: hidden;
}
body.phoneframe {
  position: fixed;
  left: var(--app-x, 0px); top: var(--app-y, 0px);
  width: var(--app-w, 100%); height: var(--app-h, 100%);
  transform: translateZ(0);      /* load-bearing: see above */
  overflow: hidden;
  border-radius: 18px;
  box-shadow: 0 0 0 2px #2b3324, 0 18px 60px rgba(0, 0, 0, .55);
}
/* The game pages never scroll. The menu does — on a short window its Start button is below
   the fold, and `overflow: hidden` would put it out of reach with no way to get to it. */
body.phoneframe.landing { overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; }

/* THE VIEW TOGGLE, on the menu. The game pages have a settings sheet to hold it; the menu has
   nothing, so the only way back out of the phone view was a URL — which a judge does not know.
   Fixed inside the frame (body is the containing block), out of the way of the title. */
.framebtn {
  /* BOTTOM-left. Top-right is the site's own header where this page is embedded (the owner's
     screenshot, 2026-09-07), and bottom-right is the build stamp. */
  position: fixed; z-index: 120; bottom: max(10px, env(safe-area-inset-bottom));
  left: max(10px, env(safe-area-inset-left));
  padding: 7px 12px; border-radius: 999px; cursor: pointer; white-space: nowrap;
  font: 700 11.5px/1 var(--ui); letter-spacing: .04em; text-transform: uppercase;
  background: var(--ink); color: var(--paper); border: 1.5px solid var(--ink);
  box-shadow: 0 3px 12px -4px #0008;
}
.framebtn:hover { background: var(--crimson); border-color: var(--crimson); }
.framebtn:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
#hud { position: fixed; inset: 0; width: var(--app-w, 100%); height: var(--app-h, 100%); pointer-events: none; z-index: 10; overflow: hidden; /* the floating stick ring may overhang the edge */ }
#hud > * { pointer-events: auto; }

/* A COLUMN, explicitly. These are absolutely-positioned shrink-to-fit boxes, so
   the topbar's width is set by its widest child — which is the build panel. The
   zone chip is a block, so it used to STRETCH to that width: a deep run turned
   the "⚔ 8 · 7:30" pill into a bar most of the way across the screen. */
.topbar {
  position: absolute; top: max(8px, env(safe-area-inset-top)); left: 50%; transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center;
}
#hud > .topbar { pointer-events: none; }      /* the ID, not the class: #hud > * (fifth) */
/* Above the stick zone, which starts a quarter of the way down and is a LATER sibling: a deep
   build's lowest row of coins reaches into it, and without this a tap on one of them drove the stick. */
#hud > .topbar { z-index: 1; }
/* THE PHONE LAYOUT, ON WIDTH RATHER THAN ON A MEDIA QUERY. `body.narrow` is published from
   --app-w (ui/viewport.js), so it is true on a handset AND inside the phone frame, where a
   `@media (max-width: 560px)` never fires because the WINDOW is still 1600 wide.

   Same values as that block, for the three that matter. The topbar pins RIGHT (its width is
   set by the build panel, so centred it slides as the build grows) — centred and
   left-anchored, it and the HP row were the same 200px of space and the zone chip sat under
   the HP bar for the whole match. The FEED moves to the free left column: right-anchored it
   painted over the coins from a two-row build on, and in the frame it ran off the edge. */
body.narrow .topbar {
  top: calc(max(8px, env(safe-area-inset-top)) + 42px);
  left: auto; right: 8px; transform: none; align-items: flex-end;
}
body.narrow .chip { font-size: 12px; padding: 6px 12px; }
body.narrow .feed {
  top: calc(max(8px, env(safe-area-inset-top)) + 40px);
  right: auto; left: 10px; align-items: flex-start; max-width: 60%;
}
body.narrow .feed.below-toast { top: calc(max(8px, env(safe-area-inset-top)) + 94px); }
body.narrow .feedline { --feed-from: -12px; }   /* left-anchored here, so it arrives from the left */
.chip {
  font: 700 14px var(--ui); color: #fff; background: var(--hud-bg);
  border: 1px solid var(--hud-line); border-radius: 999px; padding: 7px 16px; white-space: nowrap;
}
.chip .warn { color: #ff9a5c; }

.paneltabs { display: flex; gap: 4px; justify-content: center; margin-top: 6px; pointer-events: none; }
.paneltab {
  pointer-events: auto; cursor: pointer;
  font: 700 10px var(--ui); letter-spacing: .04em; text-transform: uppercase;
  color: #a99e88; background: var(--hud-bg); border: 1px solid var(--hud-line);
  border-radius: 999px; padding: 3px 10px;
}
.paneltab.on { color: #ffd34d; border-color: var(--gold); }
/* SOMETHING LANDED IN HERE. The panel can be scrolled inside its own box, or
   folded away on a small screen, so the coin itself may arrive somewhere the
   player cannot see — the tab is the part that is always on screen, so the tab
   is what has to move. Restarted from JS by removing and re-adding the class
   (see hud.js), because a second pickup would otherwise not re-fire it. */
.paneltab.gained { animation: tabGained .9s ease both; }
@keyframes tabGained {
  0%   { color: #ffd34d; border-color: var(--gold); transform: scale(1); box-shadow: 0 0 0 0 rgba(255,211,77,.6); }
  22%  { color: #fff3c4; border-color: #ffd34d; transform: scale(1.22); box-shadow: 0 0 0 6px rgba(255,211,77,0); }
  100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255,211,77,0); }
}
.stackpanel.gained { animation: panelGained .9s ease both; }
@keyframes panelGained {
  0%, 100% { transform: none; }
  25% { transform: scale(1.045); }
}
@media (prefers-reduced-motion: reduce) {
  .paneltab.gained, .stackpanel.gained { animation: none; }
  .paneltab.gained { color: #fff3c4; border-color: #ffd34d; }
}
/* Column layout, and it must clip horizontally: an equipment line carries its
   whole stat list ("8 dmg · 1.6s cd · 2.6 range · 3.7 knockback · …") and
   without this the nowrap text pushed the panel wider than the screen and grew
   a horizontal scrollbar across the play area. */
/* The pickup log shares .stackpanel but is still a list of SENTENCES, so it
   keeps the old width. The build tab shrank to fit rows of emoji coins; letting
   that narrowing apply here would just ellipsis away the end of every line. */
.pickuppanel {
  flex-direction: column; align-items: stretch; gap: 2px; overflow-x: hidden;
  max-width: min(560px, 92vw); max-height: 26vh;
}
.pickupline {
  pointer-events: auto; min-width: 0;
  font: 500 10.5px var(--ui); color: #e8ddc8; background: var(--hud-bg);
  border: 1px solid var(--hud-line); border-radius: 7px; padding: 2px 8px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pickupline.eq { border-left: 3px solid var(--gold); }
.pickupline.gn { border-left: 3px solid #b789ff; }
.pickupempty { font: 500 10.5px var(--ui); color: #a99e88; padding: 2px 8px; }

/* Your build, under the clock. Scrolls on its own once a run gets deep —
   a 25-stack build must never push the play area around. */
.stackpanel {
  margin-top: 6px; display: flex; flex-wrap: wrap; gap: 6px;
  justify-content: center; align-items: flex-start;
  /* Coins are ~30px, so this holds a 40-stack build in three rows instead of
     the eighteen rows of prose the old text pills needed. */
  /* A SET PORTION OF THE SCREEN, and it is a hard stop rather than a hope: the
     panel scrolls inside these bounds so a 40-stack run occupies exactly the
     same rectangle as a 4-stack one. The play area never moves. */
  max-width: min(420px, 84vw); max-height: 17vh; overflow-y: auto; overflow-x: hidden;
  scrollbar-width: thin;
  /* The panel itself must stay click-through: it sits over the play area, and
     right-click-to-move through the GAPS between chips has to keep working.
     Only the chips take pointer events. */
  pointer-events: none;
}
.stackpanel::-webkit-scrollbar { width: 5px; }
.stackpanel::-webkit-scrollbar-thumb { background: #ffffff26; border-radius: 3px; }
/* A stack chip is a COIN: one emoji, one count, nothing else. It used to be a
   pill carrying the name and the full effect text, and a deep build turned that
   into eighteen lines of prose covering the top third of the play area — you
   could not see the monsters walking toward you. The emoji is the identity and
   the badge is the count; everything the pill used to say now lives one tap
   away in the tooltip, which is where reading belongs in a real-time game. */
.stackchip {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0; cursor: pointer;
  font-size: 16px; line-height: 1;
  color: #e8ddc8; background: var(--hud-bg);
  border: 1px solid var(--hud-line); border-radius: 50%;
  pointer-events: auto;   /* see .stackpanel */
  transition: border-color .15s ease, transform .12s ease;
  /* Emoji fall back to a text glyph on a device with no colour font; keep them
     from inheriting the UI face, which renders several of these as tofu. */
  font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", var(--ui);
}
.stackchip:focus-visible { border-color: var(--gold); transform: translateY(-1px); }
@media (hover: hover) { .stackchip:hover { border-color: var(--gold); transform: translateY(-1px); } }

/* The Items portal's IN-CIRCULATION switch. A switched-off item stays in the
   editor, greyed, so you can see what you turned off and put it back — which is
   the whole difference between this and "Remove from the game" below it. */
.itemonoff { display:flex; align-items:center; gap:8px; margin:6px 0 2px; font:600 12.5px var(--ui); cursor:pointer }
.itemonoff input { accent-color: var(--gold) }
.itemcard.isoff > summary { opacity:.55 }
.itemcard.isoff .itemfields { opacity:.55 }
.offtag { margin-left:8px; font:700 9.5px var(--ui); letter-spacing:.1em; text-transform:uppercase;
  color:#8a7f6c; border:1px solid #c9bfa9; border-radius:999px; padding:1px 7px }

/* TOOK A HIT — a red vignette around the edges of the screen.
   The health bar is 13px of text in a corner and, on a phone, nowhere near
   where you are looking: people were dying without ever registering that they
   were being hurt. This is the peripheral-vision channel, which is exactly what
   a vignette is for.

   `inset` box-shadow rather than a radial-gradient background, so the middle of
   the screen — where the fight is — stays completely untouched at any strength.
   Sized in vmin so it is the same fraction of the screen on a phone and a TV.
   Opacity is set from JS per hit (bigger hit, stronger flash) and eased back to
   zero by the transition, so a rain of small hits stays lit while a single
   scratch fades. */
/* `#hud > .hurtvig`, NOT `.hurtvig`, and this is the whole reason the rule is
   written that way: `#hud > *` sets `pointer-events: auto` for every direct
   child, an ID selector outranks a class, and a full-screen overlay that
   accepts pointers swallows every tap on the page — the thumbstick, the Active
   button, all of it. Measured with elementFromPoint at the middle of the screen
   and it really did return this element. Specificity, not !important, because
   the `> *` rule is deliberate and should keep working for everything else. */
/* No z-index: it is the FIRST child, and with every sibling positioned that alone
   paints it under the HP bar and the dock. `z-index: 3` put the glow over both —
   densest exactly where the one readout you look at during a hit lives. */
#hud > .hurtvig {
  position: absolute; inset: 0;
  pointer-events: none; opacity: 0;
  box-shadow: inset 0 0 12vmin 3vmin rgba(190, 20, 24, .85);
  transition: opacity var(--vig-ms, 450ms) ease-out;
}
/* The killing blow fades faster: the death card is about to explain it.
   With the ID: the rule above is (1,1,0) and a class pair (0,2,0) never won. */
#hud > .hurtvig.fatal { transition-duration: .12s; }
@media (prefers-reduced-motion: reduce) {
  /* Still shown — it is information, not decoration — but it does not pulse. */
  #hud > .hurtvig { transition: opacity .3s linear; }
}

/* AN EXTRA WEAPON, not a stat. Spare Quiver, Seeker Sigil and Phantom Blade
   fire on their own timers and a stacked one out-damages the thing in your
   hand — but in a row of identical coins there was no way to know which those
   were. A warm ring rather than a new SHAPE, because the shape language is
   already spoken for: circle means "stacking passive", and these are stacking
   passives. Set on the chip's own border, which is free — the proc flicker
   below deliberately lives on ::before so it cannot clobber this or :hover. */
.stackchip.wpn { border-color: rgba(255,178,86,.75); }
.stackchip.wpn:focus-visible { border-color: var(--gold); }
@media (hover: hover) { .stackchip.wpn:hover { border-color: var(--gold); } }

/* THE PASSIVE JUST FIRED. A quick flicker of the ring around the icon, driven
   from Sim.notePassive via hud.flickerPassives — this is the only thing on
   screen that says WHICH part of a build is doing the work, in a game whose
   whole premise is that eventually your passives out-damage your weapon.

   On a ::before rather than on the chip's own border, and this is not fussiness:
   a CSS ANIMATION BEATS A NORMAL DECLARATION, so animating border-color here
   would win against `.stackchip:hover` for the whole 420 ms and quietly delete
   the hover state. That exact mistake killed the Active button's hold-to-take
   ring once already; the fix then was a pseudo-element and it is the fix now.

   Two flickers rather than one: a single pulse at this size reads as a redraw
   glitch, two reads as deliberate. Fast enough (420 ms) that Ember Aura ticking
   once a second still returns to rest in between, so a continuously-working
   passive pulses steadily instead of sitting lit. */
.stackchip::before {
  content: ""; position: absolute; inset: -3px;
  border-radius: 50%; pointer-events: none;
  border: 2px solid transparent;
  opacity: 0;
}
.stackchip.procced::before { animation: passiveProc var(--proc-ms, 420ms) ease-out; }
@keyframes passiveProc {
  0%   { opacity: 0;   border-color: rgba(255,211,77,0);   transform: scale(.82); }
  18%  { opacity: 1;   border-color: rgba(255,232,138,.95); transform: scale(1.06); }
  46%  { opacity: .25; border-color: rgba(255,211,77,.5);  transform: scale(1); }
  64%  { opacity: .9;  border-color: rgba(255,232,138,.8); transform: scale(1.03); }
  100% { opacity: 0;   border-color: rgba(255,211,77,0);   transform: scale(1); }
}
/* Reduced motion still gets the INFORMATION — which passive fired — just held
   still instead of pulsed. A flicker is exactly the kind of motion this setting
   exists to turn off, and dropping it entirely would leave those players with
   no answer to "what is doing the damage". */
@media (prefers-reduced-motion: reduce) {
  .stackchip.procced::before {
    animation: none; opacity: 1; border-color: rgba(255,211,77,.8);
    transition: opacity .3s ease .12s;
  }
}
/* The count rides the chip as a badge rather than sitting beside it, so the row
   stays a row of coins. 1x is not drawn at all — a badge on every chip is just
   noise, and "no badge" reads as one perfectly well. */
.stackchip .sn {
  position: absolute; right: -3px; bottom: -3px; min-width: 15px; height: 15px;
  padding: 0 3px; border-radius: 8px; box-sizing: border-box;
  background: var(--gold); color: #1a1206;
  font: 800 10px var(--ui); font-variant-numeric: tabular-nums;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(0,0,0,.45);
}
.stackchip.s1 .sn { display: none; }

/* SHAPE TELLS YOU THE SLOT, before you have read anything.
   circle = a stacking passive · triangle = an Active trinket · square = gear.
   Colour is already spent on rarity and on the count badge, and an icon at 22px
   has no room for a label, so the outline is the only channel left that reads
   instantly — and unlike colour it survives being colour-blind. */
.chip-tri {
  border-radius: 4px;
  clip-path: polygon(50% 2%, 98% 96%, 2% 96%);
  /* A triangle wastes its lower corners, so the art sits low and small inside
     it or it overflows the sloped edges. */
  align-items: flex-end; padding-bottom: 8%;
}
.chip-tri .si { inset: auto 22% 6% 22%; width: 56%; height: 56%; }
.chip-tri .sg { font-size: .78em; margin-bottom: -2px; }
.chip-tri .sn { right: -1px; bottom: -1px; }
.chip-sq { border-radius: 6px; }
/* Generated icon art, when a file exists for this power-up. TRANSPARENT
   cut-outs (tools/surviversus-icons.mjs keys the background out), so the object
   floats on the chip the way a Risk of Rain item does rather than filling it as
   a disc. Inset so it does not crowd the rim or sit under the count badge.
   Sits above the emoji and covers it; a 404 removes the element and the emoji
   shows through untouched. */
.stackchip .si {
  position: absolute; inset: 13%; width: 74%; height: 74%;
  object-fit: contain; pointer-events: none;
}

/* COMPACT MODE. For when a deep run turns the grid into a wall — smaller coins,
   tighter packing, a shorter box. Everything still reads and still taps; there
   is just less of the screen spent on it. Toggled per device (#compactTog). */
.stackpanel.compact { gap: 3px; max-height: 11vh; max-width: min(340px, 80vw); }
.stackpanel.compact .stackchip { width: 22px; height: 22px; font-size: 12px; }
.stackpanel.compact .stackchip .sn {
  min-width: 12px; height: 12px; font-size: 8px; right: -2px; bottom: -2px; padding: 0 2px;
}
.compacttog { font-size: 12px; line-height: 1; padding: 3px 8px; }
.compacttog[aria-pressed="true"] { color: #ffd34d; border-color: var(--gold); }
/* The name and effect are tooltip-only now. Kept in the DOM for screen readers
   and hidden visually — an aria-label alone would lose the effect text. */
.stackchip .st, .stackchip .se {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
@media (max-width: 620px) {
  /* a deep build must not bury the play area on a phone */
  .stackpanel { max-height: 15vh; gap: 3px; }
  .stackchip { width: 26px; height: 26px; font-size: 14px; }
  .stackchip .sn { min-width: 13px; height: 13px; font-size: 9px; }
}

.feed {
  position: absolute; top: calc(max(8px, env(safe-area-inset-top)) + 4px); right: max(10px, env(safe-area-inset-right));
  display: flex; flex-direction: column; gap: 3px; align-items: flex-end;
  max-width: 60vw;
}
#hud > .feed { pointer-events: none; }        /* the ID, not the class: #hud > * */
/* SEE-THROUGH. Five lines at the HUD's 72% were a 213 x 142 px slab of the phone's top-left
   corner with a monster behind it (owner, 2026-09-07); 45% shows what is coming, and the
   text-shadow keeps the words on grass. `--feed-from` is the side the line slides in from. */
.feedline {
  --feed-from: 12px;
  font: 600 12px var(--ui); color: #e8ddc8; background: rgba(16, 13, 10, .45);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .9), 0 0 1px rgba(0, 0, 0, .7);
  border: 1px solid var(--hud-line); border-radius: 8px; padding: 4px 9px;
  animation: feedin .25s ease, feedout .5s ease 5.5s forwards;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}
.feedline .k { font-weight: 800; }
@keyframes feedin { from { opacity: 0; transform: translateX(var(--feed-from)); } }
@keyframes feedout { to { opacity: 0; } }

/* Both pages ship viewport-fit=cover so the canvas runs under a notch — which
   means everything anchored to a SIDE has to step around it too, or in
   landscape the gear sits in the corner radius and the HP bar starts under the
   island. Top and bottom insets were honoured from the start; left and right
   were not, because every phone that tested this was held upright. */
.statusrow {
  position: absolute; top: calc(max(8px, env(safe-area-inset-top)) + 2px); left: calc(max(10px, env(safe-area-inset-left)) + 46px);
  display: flex; align-items: center; gap: 8px;
}
#hud > .statusrow { pointer-events: none; }   /* the ID, not the class: #hud > * */
.lvlbadge {
  width: 34px; height: 34px; border-radius: 50%; background: var(--hud-bg); border: 1.5px solid var(--gold);
  color: #ffd34d; font: 800 15px var(--ui); display: flex; align-items: center; justify-content: center;
}
.bars { display: flex; flex-direction: column; gap: 3px; }
.hpbar { position: relative; width: 130px; height: 15px; background: rgba(0,0,0,.55); border-radius: 8px; overflow: hidden; border: 1px solid var(--hud-line); }
/* The fill moves in 120 ms; the pale GHOST under it follows after a beat, so a
   bite leaves a visible chunk for half a second — the size of the hit, read
   off the bar, not off the number. */
.hpghost { position: absolute; inset: 0; width: 100%; background: #f6c4b6; opacity: .6; transform-origin: left; transition: transform .45s ease-out .2s; }
.hpfill { position: absolute; inset: 0; width: 100%; background: linear-gradient(#f06a60, #d33b34); transform-origin: left; transition: transform .12s ease-out; }
.shfill { position: absolute; left: 0; top: 0; height: 5px; width: 0; background: #5ab0ff; border-radius: 0 3px 3px 0; }
.hptext { position: absolute; inset: 0; font: 800 10.5px var(--ui); color: #fff; display: flex; align-items: center; justify-content: center; text-shadow: 0 1px 2px #000; }
.xpbar { width: 130px; height: 5px; background: rgba(0,0,0,.55); border-radius: 4px; overflow: hidden; }
.xpfill { height: 100%; width: 0; background: #41e0d8; transition: width .18s ease-out; }
/* A number that changed pops once (hud.js restarts .pop by reflow). */
.lvlbadge.pop, .dmgbadge.pop { animation: badgePop .38s cubic-bezier(.2, 1.6, .4, 1) both; }
@keyframes badgePop { 0% { transform: scale(1); } 40% { transform: scale(1.35); } 100% { transform: scale(1); } }
@media (prefers-reduced-motion: reduce) { .lvlbadge.pop, .dmgbadge.pop { animation: none; } .hpghost, .hpfill, .xpfill { transition: none; } }

#minimap {
  position: absolute; right: max(10px, env(safe-area-inset-right)); top: calc(max(8px, env(safe-area-inset-top)) + 40px);
  width: 130px; height: 130px; border-radius: 10px; border: 1px solid var(--hud-line);
  background: #000a; opacity: .92; pointer-events: none;
}

/* joystick (left by default; .swapped flips) */
/* INERT UNTIL THERE IS A THUMB. This box is 46% x 55% of the screen and
   invisible; with `pointer-events: auto` on a PC it swallowed every click in
   the bottom-left third of the play area, drove the touch stick with the mouse,
   and opened the browser context menu (the suppression lived on the canvas,
   which this sits on top of). Measured: 36 of 42 leaking grid points were in
   here. `hastouch` is set by hud.js on a coarse pointer or the first real
   touch — never on coarse alone, because that is false on a touchscreen
   laptop. Same rule the pane HUD already follows with `sawTouch`. */
/* Edge to edge below CONFIG.stick.zoneTop — hud.js sets the height from it; 75% here is only the
   fallback before it runs. The dock is a later sibling, so its buttons stack above the zone. */
.joyzone { position: absolute; left: 0; right: 0; bottom: 0; height: 75%; touch-action: none; }
/* `#hud > .joyzone`, NOT `.joyzone`: `#hud > *` sets `pointer-events: auto` and
   an ID selector outranks a class, so the plain version lost and the zone went
   on swallowing clicks — measured with elementFromPoint, which still returned
   joyZone in the bottom-left. This is the SECOND time that rule has quietly won
   against a class today; see the note on .hurtvig. */
#hud > .joyzone { pointer-events: none; }
#hud.hastouch > .joyzone { pointer-events: auto; }
/* And the ring itself is clutter over the play area until then. */
#hud:not(.hastouch) .joybase { display: none; }
/* Anchored stick: fixed position, always visible, brightens while grabbed. */
.joybase {
  position: absolute; border-radius: 50%;
  background: rgba(255,255,255,.07); border: 2px solid rgba(255,255,255,.20);
  pointer-events: none; transition: background .12s ease, border-color .12s ease;
}
.joybase.grabbed { background: rgba(255,255,255,.13); border-color: rgba(255,255,255,.38); }
.joynub {
  position: absolute; left: 50%; top: 50%; width: 46%; height: 46%; border-radius: 50%;
  background: rgba(255,255,255,.34); border: 2px solid rgba(255,255,255,.5);
  transform: translate(-50%, -50%);
}
.joybase.grabbed .joynub { background: rgba(255,255,255,.5); }

.itemdock {
  position: absolute; right: max(12px, env(safe-area-inset-right)); bottom: max(16px, env(safe-area-inset-bottom));
  display: flex; flex-direction: column; gap: 10px; align-items: flex-end;
}
#hud.swapped .itemdock { right: auto; left: max(12px, env(safe-area-inset-left)); align-items: flex-start; }
/* The dock's BOX is 84 x 200 px over the stick zone, and `#hud > *` made all of it a target: a thumb
   landing beside a chip or 2 px under the Active button was swallowed and drove nothing (measured
   2026-09-07). Only its buttons take a touch now; the gaps and the margin fall through to the stick. */
#hud > .itemdock { pointer-events: none; }
.itemdock button { pointer-events: auto; }
/* SQUARE, matching .chip-sq in the build panel. The dock and the build panel
   describe the same items, and until this radius came down from 14 they did it
   in two different alphabets: a rounded lozenge here, a crisp square there. */
.itemchip {
  width: 54px; height: 54px; border-radius: 7px; background: var(--hud-bg);
  border: 2px solid var(--hud-line); color: #fff; font: 700 10px var(--ui);
  display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0;
  overflow: visible; position: relative;
  touch-action: none; -webkit-user-select: none; user-select: none;
}
/* A chip and its LOCK share a wrapper so the dock's column stays one chip wide.
   It SHRINK-WRAPS rather than repeating 54px: the phone block resizes the chip,
   and a fixed wrapper left the padlock 6px off its corner there. */
.slotwrap { position: relative; display: flex; }
/* THE OFFERED CHIP (2026-09-05): gear underfoot, drawn as a second chip beside the slot it would fill,
   with its own power on the badge; a tap takes it. Absolute, hung off the wrapper toward the middle of
   the screen, so the wrapper keeps the held chip's size and the padlock stays on its corner. It
   replaced a 300px card over the play area. */
.itemchip.offerchip {
  position: absolute; top: 0; right: calc(100% + 10px); left: auto;
  touch-action: none; -webkit-user-select: none; user-select: none;
  animation: offerChipIn .18s cubic-bezier(.2, 1.4, .4, 1) both;
}
#hud.swapped .itemchip.offerchip { right: auto; left: calc(100% + 10px); }
@keyframes offerChipIn { from { opacity: 0; transform: scale(.7); } }
/* The ring breathes so the chip is noticed in peripheral vision — same clock as .actbtn.offering, and on
   ::before for the same reason (an animated box-shadow would beat the rarity border). */
.itemchip.offerchip::before {
  content: ""; position: absolute; inset: -4px; border-radius: 10px; pointer-events: none;
  border: 2px solid rgba(110, 231, 110, .7);
  animation: actOfferGlow 1.25s ease-in-out infinite;
}
/* "TAP" / "CLICK" / "HOLD SPACE", above the chip: the one word that says the chip is a button. */
.offercap {
  position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
  font: 800 9px var(--ui); letter-spacing: .08em; color: #6ee76e; white-space: nowrap;
  text-shadow: 0 1px 2px #000; pointer-events: none;
}
/* a hold charging on either chip, the way .actbtn.holding shows it */
.itemchip.holding { box-shadow: 0 0 0 3px rgba(255, 211, 77, .55) inset; }
@media (prefers-reduced-motion: reduce) { .itemchip.offerchip, .itemchip.offerchip::before { animation: none; } }
.slotlock {
  position: absolute; top: -9px; right: -9px; width: 24px; height: 24px; padding: 0;
  border-radius: 50%; border: 1.5px solid rgba(255,255,255,.35); background: rgba(16,13,10,.9);
  font-size: 12px; line-height: 1; color: #fff; opacity: .7; cursor: pointer;
}
.slotlock.on { border-color: var(--gold); background: rgba(90,70,20,.95); opacity: 1; box-shadow: 0 0 8px rgba(255,211,77,.35); }
/* Adrenaline: information beside the health bar, not a button — the burst fires on its own.
   A conic arc fills as the cooldown runs (--cd 0..1); gold when ready; pulsing while on. */
.adrpip {
  width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  font-size: 12px; color: rgba(255,255,255,.4); background: rgba(0,0,0,.55);
  border: 2px solid rgba(255,211,77,.35); position: relative;
}
.adrpip::before {
  content: ""; position: absolute; inset: -2px; border-radius: 50%;
  background: conic-gradient(var(--gold) calc(var(--cd, 0) * 360deg), transparent 0);
  -webkit-mask: radial-gradient(circle, transparent 9px, #000 10px); mask: radial-gradient(circle, transparent 9px, #000 10px);
}
.adrpip.ready { color: var(--gold); border-color: var(--gold); }
.adrpip.on { color: #fff3b0; border-color: #fff3b0; animation: adrPulse .35s ease-in-out infinite alternate; }
@keyframes adrPulse { from { transform: scale(1); } to { transform: scale(1.18); } }
@media (prefers-reduced-motion: reduce) { .adrpip.on { animation: none; } }
/* The slot's SHAPE TAG. circle = a stacking passive · triangle = an Active
   trinket · square = gear — the language the power-up chips already speak, now
   said on the dock too. A tag rather than a silhouette because a triangular
   Active BUTTON would be a lovely diagram and a terrible thumb target.
   A pseudo-element on purpose: hud.js rewrites these buttons' innerHTML twelve
   times a second, and a real child element would be wiped by every frame. */
.itemchip::after, .actbtn::after {
  content: ""; position: absolute; top: -5px; left: -5px;
  width: 13px; height: 13px; pointer-events: none;
  background: #d9cdb4; box-shadow: 0 0 0 1px rgba(0,0,0,.55);
}
.itemchip::after { border-radius: 3px; }             /* square: gear */
.actbtn::after {                                     /* triangle: an Active */
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
  box-shadow: none; background: #ffd34d; width: 14px; height: 13px;
}
.actbtn.empty::after { background: rgba(255,255,255,.3); }
.itemchip.slotempty::after { background: rgba(255,255,255,.3); }
.itemchip canvas { width: 40px; height: 40px; image-rendering: pixelated; }
/* The number your weapon actually lands for, on the icon. Not the item's
   printed damage — the real per-hit figure after level scaling and every
   multiplier, so it matches the floaters coming off the monsters. */
.itemchip .dmgbadge {
  position: absolute; right: -3px; bottom: -4px;
  min-width: 20px; padding: 0 4px; height: 16px; box-sizing: border-box;
  border-radius: 8px; background: #d33b34; color: #fff;
  border: 1px solid rgba(0,0,0,.5);
  font: 800 10px var(--ui); font-variant-numeric: tabular-nums;
  display: flex; align-items: center; justify-content: center;
  pointer-events: none;
}
.itemchip .dmgbadge.arm { background: #3f7fbf; }
/* Five rarities, five rows: the common one was missing, so a common chip kept the
   neutral border while the pane's canvas dock drew it grey. RARITY_COLORS (core/config.js). */
.itemchip.r-common { border-color: #b8b8b8; }
.itemchip.r-uncommon { border-color: #4ade80; }
.itemchip.r-rare { border-color: #38bdf8; }
.itemchip.r-epic { border-color: #c084fc; }
.itemchip.r-legendary { border-color: #fbbf24; }
.itemchip:empty { opacity: .4; }
/* an empty slot still shows its glyph, just faded — see _chip() in hud.js */
.itemchip.slotempty { border-style: dashed; }
.itemchip.slotempty canvas { opacity: .3; }
.actbtn {
  position: relative;   /* the triangle slot tag hangs off its corner */
  width: 92px; height: 92px; border-radius: 24px; border: 3px solid #ffd34d;
  background: radial-gradient(circle at 35% 30%, #4a3b17, #2b230e);
  color: #ffd34d; font: 800 13px var(--ui); letter-spacing: .5px; cursor: pointer;
  display: flex; align-items: center; justify-content: center; text-align: center; padding: 6px;
  box-shadow: 0 6px 18px -6px #000c;
  touch-action: manipulation;
}
.actbtn:active { transform: scale(.94); }
/* The Active's own art, above its name. This button is the single most-looked-at
   control on the HUD, and until the trinkets had icons it could only say the name
   in small caps — which you read rather than recognise. The icon is what you
   actually navigate by; the label stays because a name still settles ties between
   two similar-looking bombs. Column layout, so neither one covers the other. */
.actbtn { flex-direction: column; gap: 2px; line-height: 1.05; }
.acticon { width: 40px; height: 40px; display: none; }
/* Three lines in a 92px box: art, name, key. The art gives up the space.
   A CLASS, not `:has(.actkey:not([hidden]))` — hud.js is already toggling state
   on this button every frame, and `:has` is the one selector here that a
   slightly older phone browser would silently drop. */
.actbtn.withkey .acticon { width: 34px; height: 34px; }
.actbtn.withkey .acticon .ag { font-size: 26px; line-height: 34px; }
.acticon.on { display: block; }
.acticon img { width: 100%; height: 100%; object-fit: contain; display: block; }
.acticon .ag { font-size: 30px; line-height: 40px; }
/* An offer under your feet is a verb, not an item — no icon, so the word gets
   the whole button and stays readable at the size it was designed for. */
.actbtn.offering .acticon { display: none; }
.actbtn:not(.empty) #actLabel { font-size: 10px; letter-spacing: .3px; }
.actbtn.offering #actLabel, .actbtn.empty #actLabel { font-size: 13px; }
.actbtn.empty { border-color: rgba(255,255,255,.2); color: rgba(255,255,255,.4); background: var(--hud-bg); }
.actbtn span { overflow: hidden; }

/* Top-left, NOT bottom-left: the bottom corners are where thumbs live (the
   joystick zone is everything below CONFIG.stick.zoneTop), and a 40px Settings button
   parked there opens the pause sheet mid-fight. It stays top-left in swapped
   mode too — the top-right corner belongs to the kill feed and minimap. */
.pausebtn {
  position: absolute; left: max(10px, env(safe-area-inset-left)); top: calc(max(8px, env(safe-area-inset-top)) - 1px);
  width: 40px; height: 40px; border-radius: 50%; background: var(--hud-bg); color: #cbbfa8;
  border: 1px solid var(--hud-line); font-size: 19px; cursor: pointer; z-index: 2;
}

/* Offer prompt — an ALTAR or a LIAR'S CHEST only, since 2026-09-05 (gear is the .offerchip beside the
   slot). Sits just above the item dock, on the same side as it, so it reads next to the button it is
   telling you to press. 236 = the dock (92 + 10 + 54 + 10 + 54) + the padlock's 9px overhang + 7. */
.offerprompt {
  position: absolute; z-index: 40;
  right: max(12px, env(safe-area-inset-right)); bottom: calc(max(16px, env(safe-area-inset-bottom)) + 236px);
  width: min(300px, calc(var(--app-w, 100vw) - 24px));
  background: var(--hud-bg); border: 1.5px solid var(--hud-line); border-left-width: 4px;
  border-radius: 14px; padding: 8px 12px; color: #f0e8d6;
  font: 500 12px/1.4 var(--ui); text-align: left;
  -webkit-user-select: none; user-select: none;
  box-shadow: 0 12px 32px -14px #000d;
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
  animation: offerIn .16s ease both;
}
/* The card is 300px wide over the stick zone and has nothing to press — "Hold
   Active" is a sentence. Inert, with the ID: `#hud > *` beats a class (fourth
   victim; see .hurtvig). As a class it swallowed the thumb's landing while an
   offer showed, so the drag target shrank exactly when you wanted to move. */
#hud > .offerprompt { pointer-events: none; }
#hud.swapped .offerprompt { right: auto; left: max(12px, env(safe-area-inset-left)); }
@keyframes offerIn { from { opacity: 0; transform: translateY(6px); } }
.offer-head { font: 700 10px var(--ui); letter-spacing: .12em; text-transform: uppercase; }
.offer-name { font: 700 14px var(--ui); margin: 1px 0 2px; }
.offer-stats { color: #cbbfa8; font-size: 11.5px; }
.offer-act { margin-top: 5px; padding-top: 5px; border-top: 1px solid var(--hud-line); font-size: 12px; color: #e8ddc8; }
.offer-act + .offer-act { margin-top: 4px; padding-top: 4px; border-top: none; }
.offer-act b { color: #ffd34d; }
.offer-act.bad b, .offer-act.bad { color: #ff8a80; }
@media (prefers-reduced-motion: reduce) { .offerprompt { animation: none; } }

/* THE BUTTON HAS CHANGED ITS MIND, and that has to be noticed in peripheral
   vision while you are watching a monster.
   Colour alone was doing the whole job — gold trinket to green INTERACT — and
   colour is the one channel a player scanning the middle of the screen does not
   check. So: a pop when it arrives, then a slow breathing glow for as long as
   the offer is under your feet. The pop fires ONCE because hud.js adds the
   class only on the transition (classList.add is a no-op when it is already
   there), which is what keeps this from restarting twelve times a second. */
/* THE GLOW LIVES ON A PSEUDO-ELEMENT, and that is not decoration — it is the
   whole reason this works. A running animation beats a normal declaration in
   the cascade, so animating `box-shadow` on the button itself silently killed
   `.actbtn.holding` / `.actbtn.held` — the inset ring that shows hold-to-take
   charging. And hold-to-take only EXISTS while an offer is showing, so the one
   state that lost its feedback was the only state that needed it.
   The ring is its own element; the button keeps its box-shadow. */
.actbtn.offering { border-color: #6ee76e; color: #6ee76e; }
.actbtn.offering::before {
  content: ""; position: absolute; inset: -3px;
  border-radius: 27px; pointer-events: none;
  border: 2px solid rgba(110, 231, 110, .55);
  animation: actOfferPop .34s cubic-bezier(.2, 1.5, .4, 1) 1,
             actOfferGlow 1.25s ease-in-out .34s infinite;
}
@keyframes actOfferPop {
  0%   { transform: scale(1);    opacity: .95; border-width: 3px; }
  45%  { transform: scale(1.16); opacity: 0;   border-width: 2px; }
  100% { transform: scale(1);    opacity: .55; border-width: 2px; }
}
@keyframes actOfferGlow {
  0%, 100% { transform: scale(1);    opacity: .35; box-shadow: 0 0 8px 0 rgba(110,231,110,.25); }
  50%      { transform: scale(1.06); opacity: .95; box-shadow: 0 0 20px 2px rgba(110,231,110,.5); }
}
/* The WORD breathes too while an offer stands — it is what the player reads,
   and the ring alone was judged too quiet from play. Same 1.25s as the glow. */
.actbtn.offering #actLabel { animation: actBreathe 1.25s ease-in-out infinite; }
@keyframes actBreathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.16); }
}
/* And a wiggle when the button changes what it does (hud.js re-adds .ctx on a
   label change). Beats `.actbtn:active`'s transform for 340 ms, which is fine. */
.actbtn.ctx { animation: actWiggle .34s ease both; }
@keyframes actWiggle {
  0%   { transform: rotate(0) scale(1); }
  25%  { transform: rotate(-4deg) scale(1.07); }
  55%  { transform: rotate(3deg) scale(1.05); }
  80%  { transform: rotate(-1.5deg) scale(1.01); }
  100% { transform: rotate(0) scale(1); }
}
/* Motion is the cue for people who want it; for people who do not, the ring
   stays bright and steady rather than the button simply going quiet. */
@media (prefers-reduced-motion: reduce) {
  .actbtn.offering::before {
    animation: none; opacity: .85; border-width: 3px;
  }
  .actbtn.offering #actLabel, .actbtn.ctx { animation: none; }
  /* The feed line keeps its fade (it is about to be removed) and loses the slide;
     the two hover lifts go still. */
  .feedline { animation: feedout .5s ease 5.5s forwards; }
  .menucard:hover, .stackchip:hover { transform: none; }
}

/* HOW TO PRESS IT, in the words of the control this player is holding. Filled
   in by hud.js from the live bindings and empty on a touch screen, where a
   thumb is already on the thing. */
.actkey {
  font: 800 8px var(--ui); letter-spacing: .06em; opacity: .7;
  line-height: 1.1; white-space: nowrap;
}
/* "HOLD RIGHT-CLICK" while an item is offered. The tracking goes: the narrow
   layout's 84px button has 66px inside the border and padding, and the key
   hint had never been shown on a coarse screen before, so that box had never
   held one. Measured: 71px with the tracking, on the fallback font. */
.actbtn.offering .actkey { letter-spacing: 0; }
/* a passive Active-slot item (the revive Ankh) — held, never pressed */
.actbtn.passiveitem { border-color: #b789ff; color: #b789ff; }
.actbtn.holding { box-shadow: 0 0 0 3px rgba(255, 211, 77, .55) inset; }
.actbtn.held { box-shadow: 0 0 0 6px rgba(110, 231, 110, .7) inset; }

/* first-run coaching — shown once, dismissed by the first real input */
.firsthint {
  position: absolute; left: 50%; top: 42%; transform: translate(-50%, -50%);
  z-index: 55; width: min(340px, calc(var(--app-w, 100vw) - 40px));
  background: var(--hud-bg); border: 1.5px solid var(--gold); border-radius: 16px;
  padding: 18px 20px; text-align: center; color: #f0e8d6;
  box-shadow: 0 14px 40px -14px #000d;
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
  animation: hintIn .35s ease both;
}
/* CLICK-THROUGH. The card floats over the middle of the screen, which on a
   phone is inside the thumbstick's grab zone — so the card telling you to drag
   to move was itself swallowing the drag. Only its button takes input; a drag
   anywhere else reaches the stick and dismisses the hint on its own, which is
   what "auto-clears on the player's first real input" was always meant to do.
   `#hud > .firsthint`, NOT `.firsthint`: `#hud > *` sets pointer-events:auto
   and an ID beats a class — the third time that rule has won against a class
   (see .hurtvig, .joyzone), and for as long as it did the card swallowed the
   very drag it was asking for. .hintgo is a grandchild, so `> *` never
   matches it and its own declaration stands. */
#hud > .firsthint { pointer-events: none; }
.hintgo { pointer-events: auto; }
.firsthint .hintline { margin: 0 0 9px; font: 500 14px/1.5 var(--ui); color: #cbbfa8; }
.firsthint .hintline b { color: #f0e8d6; font-weight: 700; }
.firsthint .hintline:first-child { font-size: 15px; color: #f0e8d6; }
.hintgo {
  margin-top: 4px; font: 700 14px var(--ui); padding: 9px 22px; border-radius: 10px;
  border: 1px solid var(--gold); background: transparent; color: #ffd34d; cursor: pointer;
}
.hintgo:active { transform: scale(.96); }
@keyframes hintIn { from { opacity: 0; transform: translate(-50%, -46%); } }
.firsthint.out { animation: hintOut .3s ease forwards; }
@keyframes hintOut { to { opacity: 0; transform: translate(-50%, -54%); } }
@media (prefers-reduced-motion: reduce) {
  .firsthint, .firsthint.out { animation-duration: .01s; }
}

.tooltip {
  position: fixed; z-index: 60; background: var(--hud-bg); color: #f0e8d6;
  border: 1px solid var(--hud-line); border-radius: 12px; padding: 10px 14px;
  font: 500 13px/1.5 var(--ui); max-width: 260px; pointer-events: none;
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
}
.tooltip .tname { font-weight: 800; margin-bottom: 3px; }
.tooltip .tline { color: #cbbfa8; }

/* toasts — adapted from the bossfeed notification components.
   74px under the top inset clears the zone chip AND the Build/Picked-up tabs
   (tabs end at 67): every pickup toast used to land on the tab it was flashing. */
.sv-toast {
  position: fixed; top: calc(max(8px, env(safe-area-inset-top)) + 74px); left: 50%;
  transform: translateX(-50%); z-index: 80;
  width: max-content; max-width: min(440px, calc(var(--app-w, 100vw) - 28px));
  font: 600 13.5px var(--ui); color: #f0e8d6; background: var(--hud-bg);
  border: 1.5px solid var(--gold); border-radius: 999px; padding: 9px 20px; margin: 0;
  box-shadow: 0 8px 24px -10px #000e; animation: svToast 3.4s ease forwards;
  pointer-events: none; text-align: center;
}
.sv-toast.bad { border-color: #d33b34; }
.sv-toast.info { border-color: var(--hud-line); }
/* The drop-in rides margin-top, not transform: the phone block below pins the
   toast right with `transform: none`, and a running animation beats that. */
@keyframes svToast { 0% { opacity: 0; margin-top: -10px; } 10%, 80% { opacity: 1; margin-top: 0; } to { opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .sv-toast { animation-duration: .01s; animation-delay: 3s; } }

/* sheets (pause, game over) */
.sheet {
  position: fixed; inset: 0; z-index: 90; background: rgba(10, 8, 6, .78);
  height: var(--app-h, 100%); overflow-y: auto;
  display: flex; align-items: center; justify-content: center; padding: 18px;
  align-items: safe center;
}
.sheetcard {
  background: #241d16; border: 1.5px solid #4a3f30; border-radius: 18px; padding: 24px;
  width: min(420px, 94vw);
  /* 88vh is 88% of the LARGE viewport, which on a phone is taller than the
     screen — the standings list ran off the bottom with no way to reach the
     Exit link under it. */
  max-height: min(88vh, calc(var(--app-h, 100vh) - 40px)); overflow-y: auto;
  display: flex; flex-direction: column; gap: 14px; color: #f0e8d6;
}
.sheetcard h2 { font-family: var(--display); margin: 0; font-size: 26px; text-align: center; }
.setrow { display: flex; align-items: center; justify-content: space-between; gap: 14px; font: 600 14px var(--ui); }
.setrow input[type="range"] { width: 130px; }
.setnote { font: 500 12px/1.35 var(--ui); color: #a99a80; margin: -6px 0 2px; }
/* THE LIST SCROLLS, THE BUTTONS DO NOT. A 30-player Royale's standings are
   thirty rows, and with the whole card scrolling the "Play again" and "Exit"
   under them were below the fold with nothing to say so — the owner's
   screenshot had the card cut off at row 27. The card itself stops scrolling
   (overflow hidden beats .sheetcard's auto) and the standings take whatever
   height is left between the title and the buttons. */
.overcard { text-align: center; overflow: hidden; position: relative; }
/* Minimize: the card folds into a pill so the HUD underneath — the build, the
   items, the numbers — can be read; the pill brings it back. */
.overcard .minbtn { position: absolute; top: 10px; right: 10px; width: 34px; height: 34px; font-size: 18px; }
.overpill {
  /* Anchored off `--app-h`, never `bottom`: a FIXED box's bottom edge is the layout
     viewport, which on a phone sits under the URL bar — the trap that put the dock
     below the fold. This pill is the only way back to a minimized card. */
  position: fixed; left: 50%; z-index: 95;
  top: var(--app-h, 100%); transform: translate(-50%, -100%);
  margin-top: calc(-1 * (max(16px, env(safe-area-inset-bottom)) + 12px));
  padding: 10px 18px; border-radius: 999px; border: 1.5px solid var(--gold);
  background: rgba(36, 29, 22, .96); color: #ffd34d; font: 700 14px var(--ui); letter-spacing: .04em;
  box-shadow: 0 10px 28px -12px #000e; cursor: pointer; pointer-events: auto;
}
@media (hover: hover) { .overpill:hover { background: #2f2619; } }
/* Folded to the pill, the world greys out and the HUD does not — the build and the chips are why it was
   folded. Between the canvas and #hud (z 10), OUTSIDE #hud (a child would be a sixth `#hud > *` victim),
   and the colour is OVER_VEIL in render/panes.js — test/panes.mjs pins the two equal. */
body.overmin::after { content: ''; position: fixed; inset: 0; height: var(--app-h, 100%); background: rgba(30, 30, 34, 0.55); pointer-events: none; z-index: 5; }
.overcard .standings { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 2px; }
.crown { font-size: 44px; }
.standings { display: flex; flex-direction: column; gap: 5px; }
.standrow {
  display: grid; grid-template-columns: 30px 1fr auto auto; gap: 8px; align-items: center;
  font: 600 13.5px var(--ui); background: #00000030; padding: 7px 10px; border-radius: 9px; text-align: left;
}
.standrow .pl { color: #cbbfa8; font-weight: 800; }
.standrow.first { border: 1.5px solid var(--gold); }
.standrow .st { color: #a99a80; font-weight: 500; font-size: 12px; }

/* YOU ARE OUT — and the match is not over for you.
   THREE WAYS FORWARD, all of them on screen: take over a bot, watch somebody,
   or leave. This used to be the middle one only, which quietly said the other
   two did not exist — the natural reading of a death screen is that your game
   is finished, so anything not shown here is not discovered. Each action wears
   its own shape from the button system, so under a thumb "take over" and
   "exit" can never be confused. */
.deathbar {
  position: fixed; left: 50%; transform: translateX(-50%);
  /* --app-h, NOT a bare bottom. A `position: fixed` box anchors to the initial
     containing block -- the page as it would be with the URL bar RETRACTED --
     and this page can never scroll it away, so the Exit button sat under the
     phone toolbar for the whole match. Same bug .panechrome and .sheet were
     rewritten for; this card sits outside #hud in play.html and was missed.
     `top` + the visible height keeps the existing translateX and lets the card
     grow upward exactly as before. */
  top: calc(var(--app-h, 100%) - max(18px, env(safe-area-inset-bottom)));
  transform: translate(-50%, -100%);
  z-index: 30; width: min(340px, calc(var(--app-w, 100vw) - 24px));
  background: rgba(16, 13, 10, .90); border: 1.5px solid #d33b34; border-radius: 18px;
  padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; align-items: stretch;
  color: #f0e8d6; font: 600 14px var(--ui);
  box-shadow: 0 16px 44px -18px #000e;
  -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
}
.deathinfo { display: flex; gap: 10px; align-items: baseline; justify-content: center; }
#deathPlace { font: 800 20px var(--ui); color: #ff8a80; }
#deathBy { color: #cbbfa8; font-size: 13px; }
.spectrow {
  display: flex; gap: 8px; align-items: center; justify-content: space-between;
  font-size: 13px; color: #cbbfa8;
  background: #00000038; border: 1px solid var(--hud-line); border-radius: 999px; padding: 4px;
}
.spectrow #specName {
  flex: 1; min-width: 0; text-align: center;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* 34px is a comfortable mouse target and a poor thumb one, and this card is
   shown far more often on a phone than on a desktop. Grow them where a finger
   is doing the pressing — 44px is the size the pane's own Active button is
   held to (test/panes.mjs), for the same reason. */
.spectrow .iconbtn { width: 34px; height: 34px; font-size: 17px; }
@media (pointer: coarse) {
  .spectrow .iconbtn { width: 44px; height: 44px; font-size: 20px; }
}
.deathbar .bigbtn { font-size: 15px; padding: 11px 14px; }
/* Why the button is off, in the sim's own words. Present but empty the rest of
   the time, so nothing under it moves when the answer changes. */
.deathhint { font: 500 11.5px var(--ui); color: #a99e88; text-align: center; min-height: 14px; }

/* ------------------------------------------------- TV (host.html) */
/* Also a PHONE. This page is where "Public Melee" lands with no room live, and
   it is the whole of the packaged itch.io build (`?mode=offline`) — so a seat
   here can be played by thumb, and the page has to behave like a game surface
   and not like a document. */
body.tv {
  background: #171310; color: var(--paper); min-height: var(--app-h, 100vh); overflow: hidden;
  overscroll-behavior: none;   /* no pull-to-refresh mid-match */
  -webkit-tap-highlight-color: transparent;
}
.tvlobby {
  position: fixed; inset: 0; height: var(--app-h, 100%);
  display: flex; flex-direction: column; align-items: center;
  gap: 20px; padding: 26px; text-align: center; background: var(--paper); color: var(--ink);
  overflow-y: auto; z-index: 20;
  /* `center` + `overflow` is the classic trap: once the content is taller than
     the box, centring pushes the first item off the TOP and no amount of
     scrolling reaches it — the logo simply vanished. `safe center` centres
     while it fits and falls back to start-aligned when it does not. The plain
     value stays first for anything that does not understand `safe`. */
  justify-content: center;
  justify-content: safe center;
}
.tvlobby .logo { margin: 0; }
/* Row gap FIRST: 40px was chosen as the space BESIDE the QR, and portrait is the
   case where the box wraps and spends it vertically instead -- 40px of dead air
   stacked on .tvlobby's own gap, in a lobby already too tall for a phone. 14px is
   what .tvlobby drops to on a narrow screen, so the stacked code joins its rhythm. */
.codebox { display: flex; gap: 14px 40px; align-items: center; flex-wrap: wrap; justify-content: center; }
/* text-indent = letter-spacing: the tracking trails the LAST glyph too, which
   put the code 7px left of centre under its label. */
.roomcode { font: 800 clamp(70px, 12vw, 140px)/1 ui-monospace, Consolas, monospace; letter-spacing: 14px; text-indent: 14px; color: var(--crimson); }
.codelabel { font: 600 14px var(--ui); color: var(--ink-soft); letter-spacing: 1px; text-transform: uppercase; }
/* COUCH-SCANNABLE. A QR reads at roughly 10x its width, so a sofa three metres
   from a 55" panel needs about half a metre of code — ~560px at 1080p. This was
   capped at 240px, which is the same "everyone stands up and walks to the TV"
   problem boss-bluff and marble-rush each hit and fixed; the formula is theirs,
   so all three TVs now scan from the same distance.

   Viewport-proportional so a projector scales too, and the vh bound stops it
   crowding the lobby on a short, wide window. qrDataUrl returns an SVG with
   shape-rendering=crispEdges, so scaling up costs nothing and stays sharp.
   .codebox already wraps, so the room code drops below on narrow screens. */
.qr {
  width: clamp(200px, min(36vw, 46vh), 620px); height: auto; aspect-ratio: 1;
  border: 8px solid #fff; border-radius: 10px; box-shadow: 0 10px 30px -14px #0007;
}
.joinurl { font: 600 15px var(--ui); color: var(--ink-soft); }
.joinurl b { color: var(--ink); }
/* The share row sits under the join URL in the lobby's code box. Wraps, because
   on a phone in portrait the two buttons plus the status will not fit a line. */
.sharerow { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 10px; }
.sharerow .bigbtn, .sharerow .ghostbtn2 { padding: 10px 16px; font-size: 15px; }
/* 44px is the thumb target the pane's Active button is already held to; these
   are the two controls a phone host reaches for first. */
@media (pointer: coarse) { .sharerow .bigbtn, .sharerow .ghostbtn2 { min-height: 44px; } }
.sharesaid { font: 600 13px var(--ui); color: var(--ink-soft); }
.sharefallback {
  flex: 1 1 100%; min-width: 0; padding: 8px 10px; border-radius: 8px;
  border: 1px solid var(--line); background: var(--paper); color: var(--ink);
  font: 500 13px ui-monospace, Consolas, monospace;
}
.tvplayers { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; max-width: 860px; }
.tvtile {
  display: flex; align-items: center; gap: 8px; background: #fff; border: 2px solid var(--line);
  border-radius: 999px; padding: 9px 18px; font: 700 17px var(--ui); color: var(--ink);
}
.tvtile .dot { width: 14px; height: 14px; border-radius: 50%; }
.tvtile.bot { opacity: .6; font-style: italic; }
.tvtile.empty { opacity: .35; border-style: dashed; font-weight: 500; }
/* A human seat with no socket. Faded and named, so the person kicking one of two identical names
   can see which is the dead phone — that is the whole diagnosis (owner, 2026-09-07). */
.tvtile.off { opacity: .55; }
.tvtile.off .dot { box-shadow: inset 0 0 0 3px #fff; }
/* Kick. Circle = a tool, per the button language; it sits INSIDE the pill so it cannot be
   mistaken for a seat of its own, and it is never crimson-filled — leaving is never alarming. */
.tvtile .kick {
  margin: -4px -10px -4px 2px; width: 26px; height: 26px; flex: none; border-radius: 50%;
  border: 1px solid var(--line); background: transparent; color: var(--exit, #64748b);
  font: 700 14px var(--ui); line-height: 1; cursor: pointer; opacity: .5;
}
.tvtile .kick:hover, .tvtile .kick:focus-visible { opacity: 1; background: #fff; border-color: currentColor; }
/* Not a seat — a COUNT of the ones not drawn. Solid and quiet, so it does not
   read as one more box somebody could sit in. */
.tvtile.empty.more { border-style: solid; opacity: .5; font-style: italic; }
.tvbtns { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; }
.tvbtns .bigbtn { width: auto; padding: 14px 30px; }
/* ADJUST, not commit — an outlined rounded rect, never the filled crimson one.
   Defined unscoped so the same class means the same thing anywhere it lands. */
.ghostbtn {
  font: 600 15px var(--ui); padding: 14px 22px; border-radius: 12px;
  border: 1.5px solid var(--line); background: #fff8; color: var(--ink); cursor: pointer;
  transition: border-color .14s ease, color .14s ease;
}
@media (hover: hover) { .ghostbtn:hover:not(:disabled) { border-color: var(--crimson); color: var(--crimson); } }
.ghostbtn:disabled { opacity: .45; cursor: default; }
/* "Play on this screen" — local seats in the TV lobby */
.localplay {
  /* 100%, not 94vw: .tvlobby has 12-26px of padding per side, so 94vw is wider
     than its content box below 400px and the card overflowed the screen. It is a
     COLUMN flex container, so cross-axis shrink does not rescue an over-wide item
     the way it does for .sheetcard. Above 772px the 720px arm still wins, so
     desktop and landscape are unchanged. */
  width: min(720px, 100%); border: 1.5px solid var(--line); border-radius: 16px;
  background: #fff6; padding: 16px 18px; text-align: left;
}
.localhead h2 { font-family: var(--display); font-size: 21px; margin: 0; color: var(--ink); }
.localhead p { margin: 2px 0 0; font-size: 13px; color: var(--ink-soft); }
/* main-host.js stamps body.coarse from isCoarse() (a media query cannot see
   ?touch=1). The blurb tells a phone the truth: one seat, no split. */
.coarse-only { display: none; }
body.coarse .coarse-only { display: block; }
body.coarse .fine-only { display: none; }
.localseats { display: flex; flex-direction: column; gap: 8px; margin: 12px 0 0; }
.localseats:empty { display: none; }
.localseat {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  background: #fff; border: 1.5px solid var(--line); border-radius: 12px; padding: 8px 12px;
}
.localseat .dot { width: 13px; height: 13px; border-radius: 50%; flex: none; }
.localseat .who { font: 700 14px var(--ui); color: var(--ink); min-width: 92px; }
.localseat input[type="text"] {
  font: 700 13px var(--ui); text-transform: uppercase; letter-spacing: 1px;
  padding: 6px 9px; border: 1.5px solid var(--line); border-radius: 8px;
  background: #fff; color: var(--ink); width: 130px;
}
.localseat select {
  font: 600 13px var(--ui); padding: 6px 9px; border-radius: 8px;
  border: 1.5px solid var(--line); background: #fff; color: var(--ink);
}
.localseat .taken { color: var(--crimson); font: 600 12px var(--ui); }
.localbtns { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
.localbtns .ghostbtn { padding: 9px 16px; font-size: 14px; }

#tvCanvas { position: fixed; inset: 0; width: var(--app-w, 100%); height: var(--app-h, 100%); display: block; }
/* THE PLAY SURFACE for local seats. `touch-action: none` is what makes the
   thumbstick work at all: without it the browser claims a vertical drag as a
   scroll (or a pull-to-refresh), fires pointercancel, and the stick dies half a
   centimetre into every push. The rest stops a dragging thumb selecting text or
   raising the iOS callout menu. */
.panewrap {
  position: fixed; inset: 0; width: var(--app-w, 100%); height: var(--app-h, 100%);
  z-index: 5; background: #12100d;
  touch-action: none; overscroll-behavior: none;
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none;
}
.panewrap canvas {
  position: absolute; display: block;
  box-shadow: 0 0 0 1px rgba(240, 232, 214, .18);
}

/* The buttons that float over the panes.
   THIS LAYER EXISTS TO OWN THE BOTTOM EDGE. Both of these are anchored to the
   bottom of the screen, and a `position: fixed` child of <body> measures that
   against the initial containing block — the page as it would be with the URL
   bar retracted — so on a phone they sat below the fold. Sized from --app-h,
   this box IS the visible area, and `position: absolute` inside it lands where
   a thumb can reach. */
.panechrome {
  position: fixed; left: 0; top: 0;
  width: var(--app-w, 100%); height: var(--app-h, 100%);
  z-index: 16; pointer-events: none;
}
.panechrome > * { pointer-events: auto; }
/* Bottom-centre. Not top-left, which is where the pane draws its own name and
   health bars.
   WHAT IS IN HERE IS DELIBERATELY BIG AND RARE: the jump-in button (melee only,
   and only while you are out) and the spectator's exit. Both are things you go
   looking for, not things a thumb crosses by accident — which is why the ⚙ was
   MOVED OUT of here on 2026-08-28 and now lives top-right; see .panesetbtn.
   A COLUMN, because more than one can be on screen at once: in a melee you can
   be dead — jump-in showing — and still want the way out. They used to be two
   absolutely-positioned boxes with a hand-computed 50px offset between them,
   which was one button away from overlapping. */
.panedock {
  position: absolute; left: 50%; transform: translateX(-50%);
  bottom: max(10px, env(safe-area-inset-bottom));
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  pointer-events: none; max-width: calc(var(--app-w, 100vw) - 20px);
}
.panedockrow { display: flex; gap: 8px; align-items: center; }
.panedock > *, .panedockrow > * { pointer-events: auto; }

/* TOP-RIGHT, and the one piece of pane chrome that is not in the bottom dock.
   It was bottom-centre, in the "dead ground" between the two thumb corners —
   except that on a phone that ground is not dead: it is where the thumbs pass
   through on the way to the stick and the Active button, and it is directly
   under the offer prompt. A 38px circle that opens a modal sheet, sitting in
   the middle of the bottom edge, gets pressed mid-fight.
   The top-right corner belongs to the rail, so the compact rail — which is the
   only rail a split screen ever shows — starts below this button instead. */
.panesetbtn {
  position: absolute; z-index: 1;
  right: max(12px, env(safe-area-inset-right)); top: max(12px, env(safe-area-inset-top));
  width: 38px; height: 38px; font-size: 18px; touch-action: manipulation;
}
.panedock .exitbtn {
  background: rgba(16, 13, 10, .78); touch-action: manipulation;
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
}
/* Compact means SPLIT SCREEN, which is the only case where the settings gear is
   drawn — so the rail starts under it rather than behind it. */
.tvrail[data-compact="true"] {
  width: 190px; opacity: .92;
  top: calc(max(12px, env(safe-area-inset-top)) + 46px);
  /* Landscape phone: the rail ran onto the pane's weapon chip (y 219 of 390 at
     844x390). 175 is the band the dock owns; the feed shrinks and clips its
     OLDEST lines (flex-end overflows at the start). main-host.js keeps 4 there. */
  max-height: calc(var(--app-h, 100vh) - max(12px, env(safe-area-inset-top)) - 46px - 175px);
}
.tvrail[data-compact="true"] .tvroster { display: none; }
.tvrail[data-compact="true"] .tvfeed { flex: 0 1 auto; min-height: 0; overflow: hidden; justify-content: flex-end; }
.tvrail {
  position: fixed; top: 12px; right: 12px; width: 240px; display: flex; flex-direction: column; gap: 8px;
  z-index: 15; pointer-events: none;
}
/* THE SCREEN BOUNDS THE RAIL. Ninety-nine rows (Ultra Royale, 2026-09-07) are 2650 px on a
   900 px TV, and thirty were already 51 px past it with the feed below the fold. The roster
   yields and main-host.js folds it ("+N more"); the feed RESERVES its six lines (26 px + 3
   gap) so the fold does not breathe with every headline. */
.tvrail[data-compact="false"] { max-height: calc(var(--app-h, 100vh) - 24px); }
.tvrail[data-compact="false"] .tvroster { flex: 0 1 auto; min-height: 0; overflow: hidden; }
.tvrail[data-compact="false"] .tvfeed { flex: 0 0 auto; min-height: 171px; }
.tvrosterrow.tvmore { grid-template-columns: 1fr; text-align: center; color: #cbbfae; font-weight: 500; }
/* PORTRAIT: THE RAIL HAS NO 'BESIDE' TO SIT IN.
   The overview is a SQUARE fitted to the SHORT axis (tvrender.js: S =
   min(W,H) * 0.96), so in landscape a wide canvas letterboxes it and leaves
   room either side -- which is the only reason a 240px fixed column ever
   cleared the arena. Turn the canvas portrait and the map becomes 96% of the
   WIDTH, the side margin becomes 2% of it, and the rail lands directly on the
   playfield: measured 41% of the map hidden on a 393x780 phone and 60% on an
   820x1180 tablet, over exactly the dots and storm ring the screen exists to
   show. Portrait has ~400px of unused vertical band and none horizontal, so
   the rail goes ACROSS THE TOP instead of down the side.

   Keyed on aspect, not width: a 1024px portrait iPad has the same problem as a
   393px phone and would sail past any max-width test. The camera column is
   handled in JS (buildSpots gates on the room beside the map, not on width),
   so it builds zero cameras here rather than being hidden after the fact. */
@media (max-aspect-ratio: 1/1) {
  /* OVERVIEW ONLY. A split-screen pane is the play surface — its compact rail
     has no map to clear and the pane's top-left corner is the name / HP /
     build-toggle corner (panes.js paneControls). The attribute is here for
     SPECIFICITY as much as scope: the phone block further down sets
     `.tvrail { width: 156px }` and, being later, won on every portrait phone —
     left, right AND width set, so `right` was ignored and the band collapsed
     into a 136-156px ROW pinned top-left with a ~40px kill feed. (0,2,0)
     beats (0,1,0) whatever the source order. Same shape as `#hud > .hurtvig`. */
  .tvrail[data-compact="false"] {
    left: 8px; right: 8px; width: auto;
    flex-direction: row; align-items: flex-start; gap: 8px;
  }
  .tvrail[data-compact="false"] .tvroster, .tvrail[data-compact="false"] .tvfeed { flex: 1 1 0; min-width: 0; }
  /* Across the top there is no column to bound: cap the roster at 38% of the screen or ninety-nine
     rows blanket the map it sits over; the fold does the rest. The feed needs no reserve beside it. */
  .tvrail[data-compact="false"] .tvroster { max-height: calc(var(--app-h, 100vh) * .38); }
  .tvrail[data-compact="false"] .tvfeed { min-height: 0; }
  .tvrail[data-compact="false"] .tvchip { flex: 0 0 auto; }
}
.tvchip { background: var(--hud-bg); border: 1px solid var(--hud-line); border-radius: 12px; padding: 10px 14px; font: 700 15px var(--ui); text-align: center; }
.tvroster { background: var(--hud-bg); border: 1px solid var(--hud-line); border-radius: 12px; padding: 8px; display: flex; flex-direction: column; gap: 4px; }
.tvrosterrow { display: grid; grid-template-columns: 12px 1fr auto; gap: 8px; align-items: center; font: 600 12.5px var(--ui); padding: 3px 6px; border-radius: 6px; }
.tvrosterrow .dot { width: 10px; height: 10px; border-radius: 50%; }
.tvrosterrow.dead { opacity: .38; text-decoration: line-through; }
.tvrosterrow .hp { color: #a9d38a; font-weight: 500; font-size: 11.5px; }
.tvfeed { display: flex; flex-direction: column; gap: 3px; align-items: stretch; }
.tvfeed .feedline { text-align: left; }
/* The mini-camera column. Down the LEFT, top to bottom, beside the overview
   map — one camera shows a fight, several show a melee. Populated by
   buildSpots() in main-host.js, which decides the count from the window height
   rather than trusting the config alone. */
.spotcol {
  position: fixed; left: 12px; top: 12px; bottom: 12px; z-index: 15;
  display: flex; flex-direction: column; justify-content: flex-start; gap: 10px;
  pointer-events: none;
}
.spotlight {
  border: 2px solid var(--hud-line); border-radius: 14px;
  overflow: hidden; background: #000; box-shadow: 0 10px 30px -10px #000d;
  position: relative; flex: 0 0 auto;
}
.spotlight canvas { display: block; }   /* sized by buildSpots(): the margin beside the map decides */
.spotlabel {
  position: absolute; top: 6px; left: 8px; font: 700 13px var(--ui); color: #fff;
  text-shadow: 0 1px 3px #000; letter-spacing: .3px;
}
/* The room code, up all match. Overview: top-left, and the camera column starts under it
   (.spotcol.undercode). Split screen: top-centre, small, out of the seats' HP corners. */
.tvcode {
  position: fixed; left: 12px; top: 12px; z-index: 15; pointer-events: none;
  background: var(--hud-bg); border: 1px solid var(--hud-line); border-radius: 12px;
  padding: 8px 14px; display: flex; flex-direction: column; align-items: center; gap: 1px;
}
.tvcodeval { font: 800 26px/1 ui-monospace, Consolas, monospace; letter-spacing: 6px; text-indent: 6px; color: var(--crimson); }
.tvcodehint { font: 600 11px var(--ui); color: #d9cbb4; opacity: .85; }
.tvcode.split { left: 50%; transform: translateX(-50%); top: max(8px, env(safe-area-inset-top)); padding: 4px 10px; }
.tvcode.split .tvcodeval { font-size: 18px; letter-spacing: 4px; text-indent: 4px; }
.tvcode.split .tvcodehint { display: none; }
.spotcol.undercode { top: 76px; }
.tvover {
  position: fixed; inset: 0; z-index: 40; height: var(--app-h, 100%);
  display: flex; align-items: center; justify-content: center; align-items: safe center;
  overflow-y: auto; padding: 16px;
  background: rgba(10, 8, 6, .82);
}

/* ------------------------------------------------- phone-size layout */
@media (max-width: 560px) {
  /* top-left: bars. top-right: zone chip, feed below it. minimap defaults off. */
  /* flex-end, not the default centre: the topbar's width is set by the build
     panel, so a centred chip would slide left as the build grew. Pinned right,
     the clock stays where the player last looked for it. */
  .topbar { left: auto; right: 8px; transform: none; align-items: flex-end; }
  .chip { font-size: 12px; padding: 6px 12px; }
  /* THE FEED MOVES TO THE LEFT COLUMN ON A PHONE, and this is a portrait fix.
     The rule above pins .topbar into the top-RIGHT corner, and the topbar is no
     longer just the zone chip: it carries the Build/Picked-up tabs and
     #stackPanel, which grows to a 15vh cap. #feed is right-anchored in that same
     column and comes LATER in play.html's DOM, and .feedline is 72% opaque -- so
     from a two-row build onward every kill line painted straight over the coins.
     The left column is free here: .statusrow and the pause gear are short, and
     the thumbstick zone starts far below. Vertical offsets are unchanged. */
  .feed {
    top: calc(max(8px, env(safe-area-inset-top)) + 40px);
    right: auto; left: 10px; align-items: flex-start; max-width: 60vw;
  }
  /* the toast band runs across the top-centre on a narrow screen — drop the
     feed below it so a pickup line never lands on a kill line */
  .feed.below-toast { top: calc(max(8px, env(safe-area-inset-top)) + 94px); }
  .hpbar, .xpbar { width: 104px; }
  .lvlbadge { width: 30px; height: 30px; font-size: 13px; }
  .statusrow { left: 52px; }
  .actbtn { width: 84px; height: 84px; }
  /* "HOLD SPACE" was 7px — under the 9px floor the world text gets (MIN_TEXT_CSS)
     and a smear on a 3x phone. 9px wraps to two lines in the 66px the button has;
     the icon is hidden while offering, so the height is there. */
  .actbtn.offering .actkey { font-size: 9px; white-space: normal; line-height: 1.05; max-width: 66px; }
  .itemchip { width: 48px; height: 48px; }
  #minimap { width: 104px; height: 104px; top: calc(max(8px, env(safe-area-inset-top)) + 76px); }
  /* the offer prompt gets the full width above the dock rather than a column
     squeezed beside it. 216 = dock (84 + 10 + 48 + 10 + 48) + 9 lock + 7. */
  .offerprompt {
    left: 10px; right: 10px; width: auto;
    bottom: calc(max(16px, env(safe-area-inset-bottom)) + 216px);
    font-size: 12px; padding: 7px 10px;
  }
  /* Tabs are the tap target that is always on screen; 3px 10px made them ~20px tall. */
  .paneltab { padding: 6px 12px; font-size: 11px; }
  /* Right-pinned, so it stacks in the topbar's column instead of across the screen
     (the feed owns the left one). transform:none only works because the keyframes
     carry no X — see svToast. */
  .sv-toast { left: auto; right: 8px; transform: none; max-width: 60vw; }
  #hud.swapped .offerprompt { left: 10px; right: 10px; }
  .offer-name { font-size: 13px; }
  /* The build panel and the minimap both hang off the top-right corner. The
     minimap defaults OFF on a touch device so they rarely met, but turning it on
     used to drop a 104px square straight onto the power-up grid. Give the panel
     the column beside it instead. */
  /* ONLY the pickup log gets the margin. Its lines are nowrap, so its min-content
     IS its max-width -- and min-content is the floor of the shrink-to-fit topbar,
     so 328 + 112 against the 382px a 390px phone has grew the topbar off the LEFT
     edge and clipped the start of every line. The build grid needs no budget: it
     WRAPS, so its min-content is one coin. */
  #hud.withmini .pickuppanel { margin-right: 112px; }
  #hud.withmini .stackpanel { max-width: calc(100% - 120px); }

  /* ---- host.html on a phone ---- */
  /* The rail is a 240px column of roster over a 360px-wide screen. In a match
     with local seats it is already compact (roster hidden), so this is the
     clock and the feed — worth about a third of the width, not two thirds. */
  .tvrail { width: 156px; top: 8px; right: 8px; gap: 5px; }
  .tvrail[data-compact="true"] { width: 136px; }
  .tvchip { font-size: 11.5px; padding: 6px 9px; }
  .tvrosterrow { font-size: 11px; padding: 2px 4px; }
  .tvrail .feedline { font-size: 10.5px; padding: 3px 7px; }
  /* A camera column does not fit beside a phone-width map. buildSpots also stops
     building them, so this is belt and braces rather than the fix. */
  .spotcol { display: none; }
  .tvcode { padding: 5px 10px; }
  .tvcodeval { font-size: 18px; letter-spacing: 4px; text-indent: 4px; }
  .tvcodehint { display: none; }
  /* The lobby is a long page on a phone. Tighten it so Start is reachable
     without three flicks. */
  .tvlobby { padding: 16px 12px 28px; gap: 14px; }
  .tvlobby .logo { font-size: clamp(30px, 9vw, 44px); }
  .roomcode { letter-spacing: 8px; text-indent: 8px; }
  .localplay { padding: 12px 13px; }
  .localhead h2 { font-size: 18px; }
  .tvbtns .bigbtn { padding: 12px 22px; }
  .tvbtns .ghostbtn { padding: 10px 14px; font-size: 13.5px; }
  .localseat input[type="text"] { width: 104px; }
  /* The size picker is the first thing on the menu; on a phone it must not eat
     a third of the first screen. */
  .sizepick { padding: 8px 8px 8px 14px; gap: 8px; }
  .sizepick > label { font-size: 10px; letter-spacing: .8px; }
  .sizepick select { font-size: 13px; padding: 8px 11px; }
  .sizenote { margin: -6px 0 20px; font-size: 12px; }
  /* The death bar sits above the safe area on a notched phone AND above the
     home-indicator strip, which is where a thumb reaching for "exit" lands. */
  .deathbar { width: min(340px, calc(var(--app-w, 100vw) - 16px)); padding: 12px 13px; gap: 8px; }
  .deathbar .bigbtn { font-size: 14px; padding: 10px 12px; }
}

/* LANDSCAPE PHONE: the offer card sits BESIDE the dock, not above it. At 844x390
   a 5-line card stacked 236px up had its top at -43px and #hud clipped the item
   name. 104 = the 92px Active button + 12; 280 wide keeps its left edge 13px off
   your own sprite at 844 (centre 422). After the phone block, so it wins. */
@media (max-height: 480px) {
  .offerprompt {
    bottom: max(16px, env(safe-area-inset-bottom)); left: auto;
    right: calc(max(12px, env(safe-area-inset-right)) + 104px); max-width: 280px;
  }
  #hud.swapped .offerprompt { right: auto; left: calc(max(12px, env(safe-area-inset-left)) + 104px); }
}

/* Series standings on the TV's game-over screen. */
.standrow.serieshead {
  margin-top: 10px; padding-top: 8px;
  border-top: 1px solid var(--hud-line); color: #ffd34d; font-weight: 800;
}

/* Series toggle and Diagnostics toggle in the host lobby.
   INK, not paper. Both labels sit inside #tvLobby, which is a paper card on a
   dark page, and both were `var(--paper)` on `var(--paper)` — a contrast ratio
   of exactly 1.00. "Play a series of 3 matches" and "Diagnostics on every
   screen" were invisible; only their grey sub-lines showed, and the room
   diagnostics switch — the one control here you reach for when something is
   wrong — had no readable name at all. The sub-line was 2.3:1 on the same
   ground, itself under AA for 12px text. Both are over 7:1 now. */
.serieslabel {
  display: flex; align-items: flex-start; gap: 9px; cursor: pointer;
  margin: 12px auto 0; max-width: 460px; text-align: left;
  font: 600 13px var(--ui); color: var(--ink);
}
.serieslabel input { margin-top: 2px; accent-color: var(--gold); flex: none; }
.serieslabel em { display: block; font-style: normal; font-weight: 400; color: #5d5344; font-size: 12px; }
body.melee .serieslabel { display: none; }  /* a melee always runs a series */
body.offline #diagLabel { display: none; }  /* "on every phone" — offline has none */

/* Melee: take over a bot when you have nothing alive. Positioned by .panedock. */
.bigbtn.jumpin { max-width: 320px; width: auto; padding: 12px 22px; }
