/* style.css — the HUD, the overlays and the menu.
 *
 * The canvas is the game. Everything in this file is a frame around it, so the
 * rules are: nothing moves that is not saying something, nothing is coloured
 * that is not carrying information, and no panel is bigger than the number it
 * holds. The layout numbers are not taste — they are the ones 02 §1.1 fixes,
 * and render.js subtracts the same 44 px top and 168 px bottom to find the
 * clear viewport the camera centres on. Change one here and the camera lies.
 *
 * The palette is 03 §5.1 verbatim. The accent is PAPER (#f4efe4), not a hue:
 * four faction colours and three semantics already use every hue a player can
 * hold in their head, and an accent that competed with them would make the
 * selection ring look like a fifth faction.
 *
 *   --good  something is going your way, or a sensor is up on purpose
 *   --warn  a ceiling is close, or something needs attention
 *   --bad   you are losing something — magazine, control capacity, the match
 *
 * Faction colours appear on the canvas, on the faction cards, in portrait
 * frames and in owner names. Nowhere else in this file.
 */

:root {
  /* Ground and panels (03 §5.1) */
  --bg: #0e1214;
  --panel: rgba(18, 22, 25, 0.92);
  --panel-solid: #14181b;
  --panel-2: #1b2024;
  --panel-3: #232a30;
  --line: #2a3136;
  --line-strong: #3a434a;

  /* Ink */
  --ink: #e6e1d6;
  --ink-2: #b9b3a6;
  --dim: #7f8a8f;

  /* Semantics — three, and only three */
  --good: #7fc96b;
  --warn: #f0b43c;
  --bad: #ff5c52;

  /* Accent: paper, not a hue */
  --accent: #f4efe4;
  --accent-ink: #12161a;
  --accent-15: rgba(244, 239, 228, 0.15);
  --accent-55: rgba(244, 239, 228, 0.55);

  /* Canvas-side colours the HUD occasionally echoes */
  --emit: #f2d98b;
  --sel: #e8f0ee;
  --enemy: #ff5c52;
  --neutral: #b9b4a4;

  /* Chrome details */
  --keycap: #1d252c;
  --danger-line: #5a2a26;
  --danger-ink: #ffb4ac;
  --r: 6px;
  --r-sm: 4px;
  --gap: 8px;
  --shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
  --inset: inset 0 1px 0 rgba(255, 255, 255, 0.04);
  --font: "Segoe UI", system-ui, -apple-system, Roboto, "Helvetica Neue", sans-serif;

  /* Layout. --mm is the minimap edge; the selection panel's left edge follows
     it, so shrinking the minimap widens the panel without a second rule. */
  --mm: 240px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: var(--bg);
  color: var(--ink);
  font: 13px/1.45 var(--font);
  /* The canvas is the interface. Selecting HUD text with a stray drag while
     giving orders is never what anybody meant. */
  -webkit-user-select: none;
  user-select: none;
  overscroll-behavior: none;
  -webkit-text-size-adjust: 100%;
}

/* -------------------------------------------------------------------- */
/* [hidden] guards                                                       */
/*                                                                       */
/* `el.hidden = true` sets an attribute and leans on the browser's own    */
/* `[hidden] { display: none }`, which is a bare attribute selector and   */
/* loses to any `#thing { display: … }` here on specificity. Nothing      */
/* throws, the DOM cheerfully reports hidden === true, and the element    */
/* stays on screen — the bug that shipped once as an opaque #loader over  */
/* a game that had already booted fine underneath it. So every id this    */
/* file gives a display gets its [hidden] back, with !important because   */
/* some state rules below are two ids deep and would outrank a plain one. */
/* tools/clientcheck.mjs fails the build if one is missing.               */
/* -------------------------------------------------------------------- */
#hud[hidden], #view[hidden], #topbar[hidden],
#cellMat[hidden], #cellMag[hidden], #cellSup[hidden], #cellCtl[hidden],
#cellChain[hidden], #cellSat[hidden], #cellClock[hidden], #cellDom[hidden],
#magBar[hidden], #magFill[hidden], #magTickA[hidden], #magTickB[hidden],
#supFill[hidden], #ctlFill[hidden], #satFill[hidden],
#domFill[hidden], #domMark[hidden], #domHold[hidden],
#matVal[hidden], #matRate[hidden], #magVal[hidden], #magRate[hidden],
#supVal[hidden], #ctlVal[hidden], #chainVal[hidden], #satVal[hidden],
#clockVal[hidden], #clockSub[hidden], #domText[hidden],
#btnDoctrine[hidden], #btnStrike[hidden], #btnOverlays[hidden],
#btnSound[hidden], #btnKeys[hidden], #btnMenu[hidden],
#picks[hidden], #strikeBadge[hidden], #linkDot[hidden], #linkMs[hidden],
#mission[hidden], #missionName[hidden], #missionList[hidden],
#feed[hidden], #notify[hidden], #tray[hidden],
#minimap[hidden], #mmcanvas[hidden], #mmName[hidden],
#selpanel[hidden], #selPortrait[hidden], #selRole[hidden], #selName[hidden],
#selLink[hidden], #selHealth[hidden], #selHealthFill[hidden], #selHp[hidden],
#selState[hidden], #selBlurb[hidden], #selRow[hidden], #selList[hidden],
#selQueue[hidden], #selRally[hidden], #selHint[hidden], #selStats[hidden],
#groups[hidden], #card[hidden], #cardHead[hidden],
#toasts[hidden], #tooltip[hidden], #tip[hidden], #tipText[hidden],
#tipClose[hidden], #keys[hidden],
#escmenu[hidden], #escInfo[hidden], #escResume[hidden], #escKeys[hidden],
#escLeave[hidden], #setEdge[hidden], #setRings[hidden], #setSound[hidden],
#confirm[hidden], #confirmText[hidden], #confirmYes[hidden], #confirmNo[hidden],
#end[hidden], #endTitle[hidden], #endReason[hidden], #endStats[hidden],
#endDoctrine[hidden], #endAgain[hidden], #endSide[hidden], #endWatch[hidden],
#matchover[hidden], #doctrineDrawer[hidden], #doctrine[hidden],
#doctrineClose[hidden], #touchMode[hidden],
#coach[hidden], #coachStep[hidden], #coachTitle[hidden], #coachSkip[hidden],
#coachBody[hidden], #coachMeter[hidden], #coachFill[hidden],
#coachProgress[hidden], #coachHint[hidden], #coachNext[hidden],
#brief[hidden], #briefTitle[hidden], #briefBody[hidden], #briefStart[hidden],
#briefSkip[hidden], #done[hidden], #doneTitle[hidden], #doneStats[hidden],
#doneNext[hidden], #doneSkirmish[hidden], #doneCampaign[hidden],
#doneMenu[hidden], #menu[hidden], #menuBoard[hidden], #factionRow[hidden],
#tutorialCard[hidden], #tutorialBtn[hidden], #tutorialSub[hidden],
#nameInput[hidden], #modeSel[hidden], #diffSel[hidden], #roomInput[hidden],
#playBtn[hidden], #campaignList[hidden], #noticeText[hidden], #loader[hidden] {
  display: none !important;
}
/* #overlays is the one element a module may toggle by CLASS as well as by
   attribute (MASTER §10 names `#overlays.open`), so its guard lets the class
   win and every other path still hides it. */
#overlays[hidden]:not(.open) { display: none !important; }

/* -------------------------------------------------------------------- */
/* The map, and the cursors that say what a click will do                */
/* -------------------------------------------------------------------- */
#view {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  cursor: default;
  touch-action: none;
  z-index: 0;
}

/* 02 §8.3. Each is a 24×24 inline SVG with the hotspot at its centre and a
   dark halo stroke underneath: half this map is pale open ground and half is
   dark woods, and a single-colour cursor vanishes into one of them. The
   keyword after the comma is what a browser that refuses data-URI cursors
   falls back to, which is better than reverting to an arrow. */
#view.cursor-attack {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cg fill='none' stroke='%23000' stroke-opacity='.5' stroke-width='3.5'%3E%3Ccircle cx='12' cy='12' r='6.5'/%3E%3Cpath d='M12 1.5v4M12 18.5v4M1.5 12h4M18.5 12h4'/%3E%3C/g%3E%3Cg fill='none' stroke='%23f4efe4' stroke-width='1.5'%3E%3Ccircle cx='12' cy='12' r='6.5'/%3E%3Cpath d='M12 1.5v4M12 18.5v4M1.5 12h4M18.5 12h4'/%3E%3C/g%3E%3C/svg%3E") 12 12, crosshair;
}
#view.cursor-enemy {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cg fill='none' stroke='%23000' stroke-opacity='.5' stroke-width='3.5'%3E%3Ccircle cx='12' cy='12' r='6.5'/%3E%3Cpath d='M12 1.5v4M12 18.5v4M1.5 12h4M18.5 12h4'/%3E%3C/g%3E%3Cg fill='none' stroke='%23ff5c52' stroke-width='1.5'%3E%3Ccircle cx='12' cy='12' r='6.5'/%3E%3Cpath d='M12 1.5v4M12 18.5v4M1.5 12h4M18.5 12h4'/%3E%3C/g%3E%3Ccircle cx='12' cy='12' r='1.4' fill='%23ff5c52'/%3E%3C/svg%3E") 12 12, crosshair;
}
#view.cursor-strike {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke='%23000' stroke-opacity='.5' stroke-width='4'/%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke='%23ff5c52' stroke-width='2' stroke-dasharray='3 3'/%3E%3Cpath d='M12 6.5v5.5' fill='none' stroke='%23ff5c52' stroke-width='2'/%3E%3C/svg%3E") 12 12, crosshair;
}
#view.cursor-tether {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cg fill='none' stroke='%23000' stroke-opacity='.5' stroke-width='3.5' stroke-linecap='round'%3E%3Ccircle cx='6' cy='18' r='2.2'/%3E%3Cpath d='M8 16.5c4-2 6.5-6 10.5-9'/%3E%3Ccircle cx='19' cy='6' r='2.2'/%3E%3C/g%3E%3Cg fill='none' stroke='%23f4efe4' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='6' cy='18' r='2.2'/%3E%3Cpath d='M8 16.5c4-2 6.5-6 10.5-9'/%3E%3Ccircle cx='19' cy='6' r='2.2'/%3E%3C/g%3E%3C/svg%3E") 12 12, alias;
}
#view.cursor-rally {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cg fill='none' stroke='%23000' stroke-opacity='.5' stroke-width='3.5' stroke-linejoin='round'%3E%3Cpath d='M8 21V3M8 4h11l-3 4 3 4H8'/%3E%3C/g%3E%3Cg fill='none' stroke='%23f4efe4' stroke-width='1.6' stroke-linejoin='round'%3E%3Cpath d='M8 21V3M8 4h11l-3 4 3 4H8'/%3E%3C/g%3E%3C/svg%3E") 8 21, cell;
}
/* The ghost IS the cursor while placing, so nothing else should be. */
#view.cursor-place, body.placing #view { cursor: none; }
#view.cursor-pointer { cursor: pointer; }
#view.cursor-pan { cursor: grabbing; }

/* -------------------------------------------------------------------- */
/* Shared atoms                                                          */
/* -------------------------------------------------------------------- */
.dim { color: var(--dim); }
.good { color: var(--good); }
.warn { color: var(--warn); }
.bad { color: var(--bad); }
.ink { color: var(--ink); }
.accent { color: var(--accent); }
.emit { color: var(--emit); }

h1, h2, h3, p, ul, ol { margin: 0; }
b, strong { font-weight: 600; }
i, em { font-style: normal; }
a { color: var(--ink-2); text-decoration: none; cursor: pointer; }
a:hover { color: var(--ink); }

/* Every panel: one border, one radius, one hairline of light on top. No drop
   shadow on the docked HUD — a shadow over a moving map reads as a smear. */
#topbar, #mission, #tray, #minimap, #selpanel, #card, #coach, #matchover,
#tip, #overlays, #tooltip {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--inset);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

/* Floating sheets get the shadow, because they are meant to be off the plane. */
.sheet {
  background: var(--panel-solid);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--shadow), var(--inset);
  padding: 18px 20px 20px;
}

/* Full-screen overlays. */
#keys, #escmenu, #confirm, #end, #brief, #done {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 24px;
  background: rgba(4, 6, 9, 0.55);
  overflow-y: auto;
}

kbd {
  display: inline-grid;
  place-items: center;
  min-width: 16px;
  height: 14px;
  padding: 0 3px;
  background: var(--keycap);
  border: 1px solid var(--line-strong);
  border-bottom-width: 2px;
  border-radius: 3px;
  font: 700 10px/1 var(--font);
  color: var(--ink-2);
  letter-spacing: 0.02em;
}

/* Buttons — 03 §5.3. Disabled loses opacity and never colour: a greyed button
   that also changed hue would read as a fourth semantic. */
button {
  font: 600 12px/1.2 var(--font);
  letter-spacing: 0.04em;
  color: var(--ink);
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 7px 12px;
  cursor: pointer;
  transition: background 90ms, border-color 90ms, opacity 90ms;
}
button:hover:not(:disabled):not(.slot) { background: var(--panel-3); border-color: var(--line-strong); }
button:active:not(:disabled):not(.slot) { background: #12161a; }
button:disabled { opacity: 0.38; cursor: default; }
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
button.primary:hover:not(:disabled) { background: #fffdf7; border-color: #fffdf7; }
button.primary kbd { background: rgba(0, 0, 0, 0.14); border-color: rgba(0, 0, 0, 0.22); color: var(--accent-ink); }
button.ghost { background: transparent; border-color: transparent; color: var(--dim); }
button.ghost:hover:not(:disabled) { background: transparent; border-color: var(--line); color: var(--ink-2); }
button.danger { background: transparent; border-color: var(--danger-line); color: var(--danger-ink); }
button.danger:hover:not(:disabled) { background: #2a1a19; border-color: var(--bad); }
button.big { font-size: 15px; padding: 12px 24px; }
/* Toggle on: a border and a bar along the bottom inside, so an active toggle
   is legible without inventing a colour for it. */
button.on { border: 1.5px solid var(--accent); box-shadow: inset 0 -2px 0 0 var(--accent); }

input, select {
  font: 13px/1.3 var(--font);
  color: var(--ink);
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 8px 9px;
  height: 36px;
}
input:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
input::placeholder { color: var(--dim); }
input[type="checkbox"] {
  height: auto;
  width: 14px;
  padding: 0;
  accent-color: var(--accent);
  vertical-align: -2px;
}

/* Every number that can change is tabular, so the layout never twitches. */
b, kbd, .v, .n, .t, .num, .secs, .cost, .sub, .pcost, .ptime, #matVal, #magVal,
#supVal, #ctlVal, #chainVal, #satVal, #clockVal, #domText, #linkMs, #selHp {
  font-variant-numeric: tabular-nums;
}

/* The standard bar: a track, a fill (<s>, width set inline) and tick marks
   (<u>, left set inline). Both children are in the markup, so JS never builds
   a node to move a pixel. */
.bar {
  position: relative;
  height: 5px;
  background: rgba(0, 0, 0, 0.42);
  border: 1px solid rgba(255, 255, 255, 0.05);
  border-radius: 3px;
  overflow: hidden;
}
.bar > s {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 0;
  background: var(--accent);
  text-decoration: none;
  transition: width 180ms linear;
}
.bar > u {
  position: absolute;
  top: 0; bottom: 0;
  width: 1px;
  background: rgba(230, 225, 214, 0.5);
  text-decoration: none;
}

/* -------------------------------------------------------------------- */
/* 1. Top bar — 44 px, z 20                                              */
/* -------------------------------------------------------------------- */
#topbar {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 44px;
  display: flex;
  align-items: stretch;
  /* A backstop, not a plan: the widths below are budgeted to fit at every
     breakpoint, and if one ever does not, the bar clips its own right edge
     rather than pushing the link readout off the screen. `clip` with a margin
     rather than `hidden`, because the tutorial outlines cells here and an
     outline at offset 2 would be the first thing a clean clip ate. */
  overflow: hidden;
  overflow: clip;
  overflow-clip-margin: 8px;
  border-width: 0 0 1px;
  border-radius: 0;
  z-index: 20;
}

/* A cell is a three-row grid, not a stack: the value and its rate belong on
   one line and the bar belongs under both. As a flex column each of the three
   markup children became its own line and the bar pushed the label out of a
   44 px bar — which is exactly what the first screenshot showed. */
/* Base cell rules use the CLASS alone on purpose — see the note above the
   top bar. */
.cell {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, auto);
  align-content: center;
  justify-content: start;
  column-gap: 5px;
  min-width: 110px;
  padding: 3px 9px;
  overflow: hidden;
  border-right: 1px solid var(--line);
}
.cell.wide { min-width: 122px; }
.cell.wider { min-width: 172px; }
#cellChain { min-width: 96px; }
#cellSup, #cellCtl { min-width: 102px; }
#cellSat { min-width: 122px; }
#cellClock { min-width: 116px; }
/* The domination cell takes what is left. Its own numbers say "domination"
   better than the word does, so on a full bar the label goes and the copy
   gets two lines instead. */
#cellDom {
  flex: 1 1 240px;
  min-width: 180px;
  justify-content: stretch;
  grid-template-columns: minmax(0, 1fr);
}

.cell > label {
  grid-column: 1 / -1;
  font: 600 10px/1.1 var(--font);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
  white-space: nowrap;
}
.cell > b {
  grid-column: 1;
  font-size: 17px;
  font-weight: 600;
  line-height: 1.15;
  white-space: nowrap;
}
.cell > i {
  grid-column: 2;
  align-self: end;
  padding-bottom: 2px;
  font-size: 10.5px;
  color: var(--dim);
  white-space: nowrap;
}
/* The clock's sub-line is a sentence, not a rate, so it sits under. */
#cellClock > i { grid-column: 1 / -1; align-self: auto; padding-bottom: 0; font-size: 9.5px; }
/* The satellite value is sometimes a whole clause ("JAMMED · retry 0:58"). */
#cellSat > b { font-size: 14px; }
.cell .bar { grid-column: 1 / -1; width: 90px; margin-top: 2px; }
#magBar { width: 150px; }
#cellSat .bar { height: 4px; }

/* Cell states. The colour lands on the value and the fill, never on the
   label: the label is the name of the thing and does not change meaning when
   the number does. */
.cell.warn > b { color: var(--warn); }
.cell.bad > b { color: var(--bad); }
.cell.good > b { color: var(--good); }
.cell.warn > .bar > s { background-color: var(--warn); }
.cell.bad > .bar > s { background-color: var(--bad); }
.cell.good > .bar > s { background-color: var(--good); }
.cell.pulse { animation: cell-pulse 1s ease-in-out infinite; }
@keyframes cell-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }

#magFill { background: var(--good); }
#magFill.good { background: var(--good); }
#magFill.warn { background: var(--warn); }
#magFill.bad { background: var(--bad); }
#supFill, #ctlFill { background: var(--ink-2); }
#satFill { background: var(--good); }

#topbar .spacer { flex: 0 1 12px; }

/* Domination: the share, a white marker at the share needed, and a second
   thin bar underneath for the hold clock. */
#cellDom > label { display: none; }
/* `#topbar .cell > b` is one class more specific than `#cellDom > b`, so the
   nowrap up there won and the whole domination sentence was clipped to one
   line. The bar id has to be in this selector too. */
#cellDom > b {
  grid-column: 1 / -1;
  font-size: 11.5px;
  font-weight: 600;
  line-height: 1.25;
  white-space: normal;
  max-height: 29px;
  overflow: hidden;
}
#cellDom .bar { width: 100%; max-width: 280px; height: 6px; }
#domFill { background: var(--ink-2); }
#domMark { width: 2px; background: var(--accent); }
#domHold {
  top: auto;
  height: 2px;
  bottom: 0;
  background: var(--accent);
  opacity: 0.85;
}
#cellDom.good > b { color: var(--good); }
#cellDom.good #domFill { background: var(--good); }
#cellDom.bad > b { color: var(--bad); }
#cellDom.bad #domFill { background: var(--bad); }
#cellDom.warn > b { color: var(--warn); }

/* The global strip. 36×32 each, in the bar's right end. */
#topbar .strip {
  display: flex;
  align-items: center;
  flex: none;
  gap: 2px;
  padding: 0 5px;
  margin-left: auto;
  border-left: 1px solid var(--line);
}
#topbar .strip button {
  position: relative;
  width: 33px;
  height: 32px;
  padding: 0;
  display: grid;
  place-items: center;
  background: transparent;
  border-color: transparent;
}
#topbar .strip button:hover:not(:disabled) { background: var(--panel-2); border-color: var(--line); }
#topbar .strip button.on { background: var(--panel-2); }
#topbar .strip .glyph { display: grid; place-items: center; width: 20px; height: 20px; color: var(--ink-2); }
#topbar .strip .glyph > svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
#topbar .strip button:hover .glyph { color: var(--ink); }
#topbar .strip button em {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  font-size: 8.5px;
  font-weight: 700;
  color: var(--dim);
  text-align: center;
  overflow: hidden;
  white-space: nowrap;
}
/* The strike badge is a price (`520·90`), not a count, and a price needs
   room — measured at 30 px, so 28 px of button clipped its last glyph on
   every device. Every other strip button stays square. */
#topbar .strip #btnStrike { width: 46px; }
#btnStrike em { font-size: 8px; letter-spacing: -0.01em; }
#btnStrike .glyph { transform: translateY(-3px); }
#topbar .strip button em:empty { display: none; }
#btnStrike em { color: var(--warn); }
#btnDoctrine em.spent { color: var(--dim); opacity: 0.5; }
/* hud.js toggles these on the buttons themselves, not on the badges — the
   `em.spent` rule above never actually matches anything, and disabled/pending
   need their own rules because the class is not the `:disabled` attribute. */
#btnDoctrine.spent em { color: var(--dim); opacity: 0.5; }
#btnStrike.disabled { opacity: 0.38; cursor: default; }
#btnStrike.pending em { color: var(--bad); animation: cell-pulse 1s ease-in-out infinite; }
/* A dot, not a badge: overlays have no number to show, only "not default". */
#btnOverlays.changed::after {
  content: "";
  position: absolute;
  top: 4px;
  right: 4px;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
}
/* Muted is a dimmer glyph AND a slash — either alone reads as a rendering
   glitch on a 20 px icon. */
#btnSound.off .glyph { opacity: 0.45; }
#btnSound.off::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 20px;
  height: 1.5px;
  background: var(--ink-2);
  transform: translate(-50%, -50%) rotate(-45deg);
  pointer-events: none;
}

/* The link readout sits at the far right, and the 100 px before it is left
   empty for the shared account pill that body.in-match parks there. */
#topbar .link {
  display: flex;
  align-items: center;
  flex: none;
  gap: 5px;
  padding: 0 9px 0 0;
  margin-left: 88px;
  font-size: 11px;
  color: var(--dim);
  white-space: nowrap;
}
#linkDot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--good);
  flex: none;
}
#linkDot.good { background: var(--good); }
#linkDot.warn { background: var(--warn); }
#linkDot.bad { background: var(--bad); }
#topbar .link.bad #linkMs { color: var(--bad); }

/* The account pill is shared code at z 9000, fixed bottom-left — which is
   exactly where the minimap goes. While a match is on screen it moves into
   the reserved slot in the top bar instead. One rule, ours, theirs untouched. */
body.in-match .sga-floating {
  left: auto;
  right: 44px;
  top: 5px;
  bottom: auto;
  transform: scale(0.88);
  transform-origin: right top;
}

/* -------------------------------------------------------------------- */
/* 2. Left column — mission panel, event feed                            */
/* -------------------------------------------------------------------- */
#mission {
  position: fixed;
  top: 52px;
  left: 8px;
  width: 300px;
  max-height: 200px;
  overflow: hidden auto;
  padding: 8px 10px;
  z-index: 15;
}
#missionName { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
#missionList { list-style: none; padding: 0; font-size: 12px; }
#missionList > li {
  position: relative;
  padding: 1px 0 1px 16px;
  color: var(--dim);
}
#missionList > li::before {
  content: "○";
  position: absolute;
  left: 1px;
  color: var(--dim);
}
#missionList > li.active { color: var(--ink); }
#missionList > li.active::before { content: "▸"; color: var(--accent); }
#missionList > li.done { color: var(--good); }
#missionList > li.done::before { content: "✓"; color: var(--good); }

#feed {
  position: fixed;
  top: 52px;
  left: 8px;
  width: 340px;
  max-height: 90px;
  overflow: hidden;
  padding: 0 2px;
  font-size: 12px;
  z-index: 15;
  /* Faded space over the map must not eat a click meant for it — and neither
     may the lines, which are 340 px wide and do not look like a panel. The
     only hoverable part is the strip below; everything else lets the drag
     through to the canvas. */
  pointer-events: none;
}
/* A 14 px column down the left edge is the whole hover affordance: it is the
   part of the feed the eye already treats as its margin, and it leaves the
   other 326 px of map alive for a box-select. */
#feed::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 14px;
  pointer-events: auto;
}
/* When the mission panel is up the feed drops below it. A sibling selector,
   not a JS class: the panel already knows whether it is hidden. */
#mission:not([hidden]) ~ #feed { top: 260px; }
/* Hovering freezes the feed and shows the backlog; L pins it the same way. */
#feed:hover, #feed.pinned {
  max-height: 260px;
  overflow-y: auto;
  padding: 4px 6px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r);
  scrollbar-width: thin;
  pointer-events: auto;
}
/* A plate, sized to the words: the feed sits on open olive ground at 12 px and
   a text-shadow alone left it at about 2.2:1. `width: max-content` keeps the
   dark box off the empty half of the line. */
#feed > .line {
  width: max-content;
  max-width: 100%;
  padding: 1px 5px;
  border-radius: 3px;
  background: rgba(8, 11, 14, 0.5);
  line-height: 1.3;
  color: var(--ink-2);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
  pointer-events: none;
}
/* Once the box is a panel it carries the plate, and the lines stop being
   clickable furniture over the map only while it is open. */
#feed:hover > .line, #feed.pinned > .line { background: none; pointer-events: auto; }
#feed > .line > .t { color: var(--dim); margin-right: 6px; font-size: 11px; }
#feed > .line.good { color: var(--good); }
#feed > .line.warn { color: var(--warn); }
#feed > .line.bad { color: var(--bad); }

/* -------------------------------------------------------------------- */
/* 3. Notification stack — top-centre, z 30                              */
/* -------------------------------------------------------------------- */
#notify {
  position: fixed;
  top: 52px;
  left: 50%;
  transform: translateX(-50%);
  width: 360px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  z-index: 30;
  pointer-events: none;
}
#notify > .note {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 36px;
  padding: 7px 10px 7px 12px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-left: 4px solid var(--dim);
  border-radius: var(--r-sm);
  box-shadow: var(--inset);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  font-size: 13px;
  line-height: 1.25;
  pointer-events: auto;
  animation: note-in 160ms ease-out;
}
@keyframes note-in { from { opacity: 0; transform: translateY(-6px); } }
/* The text is the one child with no fixed width of its own: it has to give
   up first, before it pushes the count badge or the jump label off the card.
   It gives up by WRAPPING, not by being cut — 36 px is the card's floor, not
   its ceiling, and one line threw away the sentence that explains how to
   build anything. */
#notify > .note > .txt {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}
#notify > .note.info { border-left-color: var(--ink-2); }
#notify > .note.good { border-left-color: var(--good); }
#notify > .note.warn { border-left-color: var(--warn); }
#notify > .note.bad { border-left-color: var(--bad); }
#notify > .note.jump { cursor: pointer; }
#notify > .note.jump:hover { border-color: var(--line-strong); background: var(--panel-solid); }
/* The jump affordance is a word, not an icon: an arrow glyph here reads as
   decoration and nobody clicks decoration. */
#notify > .note.jump::after {
  content: "jump";
  margin-left: auto;
  padding-left: 8px;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
}
#notify > .note > .count {
  margin-left: auto;
  padding: 1px 5px;
  background: var(--panel-2);
  border-radius: 8px;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--dim);
}
#notify > .note.jump > .count { margin-left: auto; }
#notify > .note.jump > .count ~ * { margin-left: 0; }

/* Space jumps to the newest, so the newest is the one that looks live. */
#notify > .note:first-child.bad { border-color: rgba(255, 92, 82, 0.35); }

/* -------------------------------------------------------------------- */
/* 4. Production tray and minimap — bottom-left                          */
/* -------------------------------------------------------------------- */
#tray {
  position: fixed;
  left: 8px;
  bottom: 252px;
  width: var(--mm);
  max-height: 182px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 3px;
  z-index: 21;
}
#tray:empty { display: none; }
#tray > .chip {
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr) auto;
  grid-template-rows: auto auto;
  align-items: center;
  column-gap: 6px;
  height: 28px;
  padding: 2px 5px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  cursor: pointer;
  text-align: left;
  font: 12px/1.15 var(--font);
  color: var(--ink);
}
#tray > .chip:hover { border-color: var(--line-strong); background: var(--panel-3); }
/* The "+n more" summary chip is a count, not a row: no sprite, no bar, and
   nothing happens when it is clicked, so it does not get the base chip's
   pointer cursor or hover state either. */
#tray > .chip.more {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--dim);
  cursor: default;
}
#tray > .chip.more:hover { border-color: var(--line); background: var(--panel-2); }
/* Only a chip a click handler was actually attached to says so with its
   cursor — hud.js adds this class in the same breath as the listener. */
#tray > .chip.clickable { cursor: pointer; }
#tray > .chip.clickable:hover { border-color: var(--line-strong); }
#tray > .chip > canvas { grid-row: 1 / 3; width: 20px; height: 20px; }
#tray > .chip > .name {
  grid-column: 2;
  align-self: end;
  font-size: 11.5px;
  line-height: 1.1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The bar sits low in its row with a lighter track: pressed against the name
   it read as an underline rather than as progress. */
#tray > .chip > .bar {
  grid-column: 2;
  align-self: start;
  margin-top: 3px;
  height: 4px;
  width: 100%;
  background: rgba(255, 255, 255, 0.07);
  border-color: transparent;
}
#tray > .chip > .bar > s { background: var(--accent); }
#tray > .chip > .job {
  grid-column: 3;
  grid-row: 1;
  font-size: 10.5px;
  color: var(--dim);
  white-space: nowrap;
}
#tray > .chip > .n {
  grid-column: 3;
  grid-row: 2;
  justify-self: end;
  font-size: 10.5px;
  font-weight: 600;
  color: var(--ink-2);
}
#tray > .chip.stalled { border-color: var(--warn); animation: cell-pulse 1.2s ease-in-out infinite; }
#tray > .chip.stalled > .job { color: var(--warn); }
#tray > .chip.stalled > .bar > s { background: var(--warn); }
#tray > .chip.idle { opacity: 0.7; }
#tray > .chip.idle > .name { color: var(--dim); }
#tray > .chip.site > .bar > s { background: var(--ink-2); }

#minimap {
  position: fixed;
  left: 8px;
  bottom: 4px;
  width: var(--mm);
  height: var(--mm);
  padding: 0;
  border-width: 2px;
  overflow: hidden;
  z-index: 22;
}
#mmcanvas {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--panel-solid);
  cursor: crosshair;
  touch-action: none;
}
/* M held: the class is the whole implementation (hud.js's own words). The
   canvas is already width/height 100%, so it grows with the box. Centred
   rather than anchored to its corner — a fixed 480 px square pinned at
   left: 6px ran off the right edge of a 390 px phone — and never wider than
   the viewport it is enlarged into. */
#minimap.big {
  left: 50%;
  top: 50%;
  right: auto;
  bottom: auto;
  width: min(480px, calc(100vw - 24px), calc(100vh - 140px));
  height: min(480px, calc(100vw - 24px), calc(100vh - 140px));
  transform: translate(-50%, -50%);
  z-index: 25;
}
/* The scrim says "this is the whole screen now" and takes the map out of the
   argument. It never swallows the tap that dismisses it: hud.js drops the
   class and the click carries on to the canvas underneath. */
body.minimap-big::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 24;
  background: rgba(6, 9, 12, 0.55);
  pointer-events: none;
}
#mmName {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 2px 6px;
  background: linear-gradient(to top, rgba(8, 11, 13, 0.85), rgba(8, 11, 13, 0));
  font-size: 11px;
  color: var(--ink-2);
  pointer-events: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#mmName:empty { display: none; }

/* -------------------------------------------------------------------- */
/* 5. Selection panel — bottom-centre, 740×160                           */
/* -------------------------------------------------------------------- */
#selpanel {
  position: fixed;
  left: calc(var(--mm) + 18px);
  right: 442px;
  bottom: 4px;
  height: 160px;
  display: grid;
  /* The middle track carries the name, the health bar and the state — it may
     never be the track that reaches zero. A 140 px floor on it and a stats
     column that can shrink instead is what keeps a narrow window showing a
     panel rather than a black rectangle. */
  grid-template-columns: 106px minmax(140px, 1fr) minmax(0, 232px);
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 0 10px;
  padding: 8px 10px 6px;
  z-index: 22;
}

.sel-portrait {
  grid-column: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  min-width: 0;
}
#selPortrait {
  width: 96px;
  height: 96px;
  border: 2px solid var(--fc, var(--line));
  border-radius: var(--r-sm);
  background: rgba(0, 0, 0, 0.28);
}
#selRole {
  font: 600 10px/1 var(--font);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
  text-align: center;
}

.sel-main {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}
.sel-head, .sel-health, #selState, #selBlurb, #selRow, #selQueue, #selRally,
#selHint { flex: none; }
.sel-head { display: flex; align-items: baseline; gap: 10px; }
#selName {
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#selLink {
  margin-left: auto;
  font-size: 11px;
  color: var(--dim);
  white-space: nowrap;
}
#selLink:hover { color: var(--accent); }

.sel-health { display: flex; align-items: center; gap: 8px; }
#selHealth {
  position: relative;
  width: 200px;
  height: 8px;
  background: rgba(0, 0, 0, 0.42);
  border: 1px solid rgba(255, 255, 255, 0.05);
  border-radius: 3px;
  overflow: hidden;
}
#selHealthFill {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 0;
  background: var(--good);
  text-decoration: none;
  transition: width 180ms linear;
}
#selHealthFill.mid { background: #e0c04a; }
#selHealthFill.low { background: var(--bad); }
#selHp { font-size: 11.5px; color: var(--ink-2); white-space: nowrap; }

#selState { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; font-size: 12.5px; }
#selState:empty { display: none; }
#selState > .badge {
  padding: 1px 6px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.045);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.05em;
  color: var(--ink-2);
  white-space: nowrap;
}
#selState > .badge.dim { color: var(--dim); }
#selState > .badge.ink { color: var(--ink); }
#selState > .badge.good { color: var(--good); background: rgba(127, 201, 107, 0.12); }
#selState > .badge.warn { color: var(--warn); background: rgba(240, 180, 60, 0.12); }
#selState > .badge.bad { color: var(--bad); background: rgba(255, 92, 82, 0.14); }
#selState > .badge.accent { color: var(--accent); background: var(--accent-15); }
#selState > .badge.emit { color: var(--emit); background: rgba(242, 217, 139, 0.12); }
#selState > .badge.bad.pulse { animation: cell-pulse 1s ease-in-out infinite; }

#selBlurb {
  font-size: 12px;
  line-height: 1.35;
  color: var(--dim);
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
#selBlurb:empty { display: none; }

/* Multi-select portrait row: one chip per type, count bottom-right. */
#selRow { display: flex; flex-wrap: wrap; gap: 4px; }
#selRow:empty { display: none; }
#selRow > .pchip {
  position: relative;
  width: 44px;
  height: 44px;
  padding: 0;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  cursor: pointer;
}
#selRow > .pchip > canvas { width: 40px; height: 40px; display: block; margin: 1px; }
#selRow > .pchip > .n {
  position: absolute;
  right: 1px;
  bottom: 0;
  padding: 0 3px;
  background: rgba(8, 11, 13, 0.85);
  border-radius: 3px;
  font-size: 10px;
  font-weight: 700;
  color: var(--ink);
}
#selRow > .pchip:hover { border-color: var(--line-strong); }
#selRow > .pchip.active { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }

/* Production list rows on a structure. */
#selList { flex: 1 1 0; display: flex; flex-direction: column; gap: 1px; min-height: 0; overflow-y: auto; scrollbar-width: thin; }
#selList:empty { display: none; }
/* Only a structure ever populates this list, so the floor belongs to that
   state rather than to :not(:empty) — one row should not read as a sliver. */
#selpanel.structure #selList { min-height: 48px; }
#selList > .prow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 42px 30px 34px 22px;
  align-items: center;
  gap: 6px;
  padding: 0 2px;
  font-size: 11.5px;
  line-height: 1.35;
  border-radius: 3px;
}
#selList > .prow:hover { background: rgba(255, 255, 255, 0.04); }
#selList > .prow > .pname { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#selList > .prow > .pcost { text-align: right; color: var(--ink-2); }
#selList > .prow > .psup { text-align: right; font-size: 11px; color: var(--dim); }
#selList > .prow > .ptime { text-align: right; font-size: 11px; color: var(--dim); }
#selList > .prow > button.add {
  width: 20px;
  height: 15px;
  padding: 0;
  font-size: 12px;
  line-height: 1;
}
#selList > .prow.disabled { opacity: 0.45; }
#selList > .prow.disabled > .pcost { color: var(--bad); }

/* Queue chips. Only the last one can be cancelled; the others say why. */
#selQueue { display: flex; flex-wrap: nowrap; gap: 4px; overflow-x: auto; scrollbar-width: none; }
#selQueue::-webkit-scrollbar { display: none; }
#selQueue > .qchip { flex: none; }
#selQueue:empty { display: none; }
#selQueue > .qchip {
  position: relative;
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 2px 5px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 3px;
  font-size: 11px;
}
#selQueue > .qchip > .bar { width: 60px; height: 4px; }
#selQueue > .qchip > .secs { color: var(--dim); }
#selQueue > .qchip > button.x {
  width: 16px;
  height: 16px;
  padding: 0;
  background: transparent;
  border-color: transparent;
  color: var(--dim);
  font-size: 12px;
  line-height: 1;
}
#selQueue > .qchip > button.x:hover:not(:disabled) { color: var(--bad); border-color: var(--danger-line); }

#selRally { font-size: 11.5px; color: var(--dim); }
#selRally:empty { display: none; }
/* A ghost, not a link: panel.js builds this button and sets its box model
   inline, so this is where its colour and hover live instead. */
#selRally > button.clear {
  height: auto;
  padding: 0 0 0 6px;
  background: transparent;
  border: 0;
  font: inherit;
  color: var(--dim);
  cursor: pointer;
}
#selRally > button.clear:hover { color: var(--ink-2); }
#selHint { font-size: 12px; color: var(--dim); }

.sel-stats {
  grid-column: 3;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  gap: 2px;
  min-width: 0;
  padding-left: 10px;
  border-left: 1px solid var(--line);
  font-size: 12px;
}
.sel-stats:empty { border-left: 0; }
#selStats > .stat {
  display: flex;
  align-items: baseline;
  gap: 6px;
  line-height: 1.3;
}
#selStats > .stat > .k { color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#selStats > .stat > .v { margin-left: auto; color: var(--ink-2); white-space: nowrap; }
#selStats > .stat > .v.emit { color: var(--emit); }

#groups {
  grid-column: 1 / -1;
  grid-row: 2;
  display: flex;
  align-items: center;
  gap: 4px;
  height: 20px;
  overflow: hidden;
}
#groups:empty { display: none; }
#groups > .gchip {
  display: flex;
  align-items: baseline;
  gap: 3px;
  padding: 1px 6px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: 9px;
  font-size: 10.5px;
  cursor: pointer;
}
#groups > .gchip:hover { border-color: var(--line-strong); }
#groups > .gchip > .g { font-weight: 700; color: var(--ink); }
#groups > .gchip > .n { color: var(--dim); }

/* Panel states. Nothing is destroyed and rebuilt between them — the same
   nodes are shown or hidden, which is why the panel never flickers when a
   selection changes forty times in a drag. */
#selpanel.empty .sel-portrait,
#selpanel.empty .sel-health,
#selpanel.empty .sel-stats { visibility: hidden; }
/* A live contact's health is public — the map tooltip prints it and the panel
   used to hide it. panel.js already withholds the bar for a STALE contact by
   returning no health at all, so the state that needs hiding is hidden by the
   model, not by a rule that cannot tell the two apart. */
/* On a multi-select the portrait row IS the portrait, so the 96 px column is
   106 px of nothing. Collapsing it is what lets the state summary stay on one
   line and the type chips show their count badges. */
#selpanel.multi { grid-template-columns: 0 minmax(140px, 1fr) minmax(0, 232px); }
#selpanel.multi .sel-portrait { display: none; }
#selpanel.multi #selBlurb { display: none; }
#selpanel:not(.empty) #selHint { display: none; }
#selpanel.enemy #selName { color: var(--enemy); }
#selpanel.site .sel-portrait { opacity: 0.6; }
#selpanel.enemy .sel-portrait { opacity: 0.75; }

/* A structure has to fit a production list, a queue and a rally line into the
   same 160 px a unit spends on four stats. Stacking them ran the rally two
   pixels off the bottom of the screen and left four of six queue chips
   invisible, so the middle column becomes two: the list on the left with its
   own scrollbar, the queue wrapping on the right, and the rally on a row of
   its own INSIDE the panel. Everything is budgeted; nothing is hoped for. */
#selpanel.structure .sel-main {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  gap: 3px 10px;
  align-content: start;
}
#selpanel.structure .sel-head { grid-column: 1 / -1; grid-row: 1; }
#selpanel.structure .sel-health { grid-column: 1; grid-row: 2; }
/* The bar shares the row with a number that must not wrap, and it is the only
   part of that row that can give. */
#selpanel.structure #selHealth { width: auto; flex: 1 1 auto; min-width: 40px; }
#selpanel.structure .sel-health { gap: 6px; }
/* One line is the row's whole budget, and the badge that matters — PRODUCING
   X · n s — is written first. */
#selpanel.structure #selState { grid-column: 2; grid-row: 2; flex-wrap: nowrap; overflow: hidden; }
#selpanel.structure #selList { grid-column: 1; grid-row: 3; }
#selpanel.structure #selQueue {
  grid-column: 2;
  grid-row: 3;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: 3px;
  overflow-x: hidden;
  overflow-y: auto;
  scrollbar-width: thin;
}
#selpanel.structure #selRally { grid-column: 1 / -1; grid-row: 4; text-align: left; }
#selpanel.structure #selHint { grid-column: 1 / -1; grid-row: 3; }
/* Half the width means the five columns of a row have to give some back. */
#selpanel.structure #selList > .prow {
  grid-template-columns: minmax(0, 1fr) 34px 26px 30px 20px;
  gap: 4px;
}
/* The scroll hint is a real scrollbar rather than a fade: a fade over a list
   of numbers reads as a rendering fault. */
#selpanel.structure #selList::-webkit-scrollbar,
#selpanel.structure #selQueue::-webkit-scrollbar { display: block; width: 6px; }
/* A chip is a name, a bar and a cancel; at half width the bar is the part
   that can lose 26 px without losing its meaning, and only the head is
   actually moving. */
#selpanel.structure #selQueue > .qchip { padding: 0 3px; gap: 3px; font-size: 10.5px; }
#selpanel.structure #selQueue > .qchip > button.x { width: 13px; height: 14px; }
#selpanel.structure #selQueue > .qchip > .bar { width: 30px; }
/* Only the head is moving. The rest are places in a line, and a place in a
   line is a name — which is what lets two of them share a row. */
#selpanel.structure #selQueue > .qchip:not(.head) > .bar,
#selpanel.structure #selQueue > .qchip:not(.head) > .secs { display: none; }
#selpanel.structure #selList::-webkit-scrollbar-thumb,
#selpanel.structure #selQueue::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border-radius: 3px;
}
#selpanel.structure #selList, #selpanel.structure #selQueue {
  scrollbar-color: var(--line-strong) transparent;
}

/* -------------------------------------------------------------------- */
/* 6. Command card — bottom-right, 424×160                               */
/* -------------------------------------------------------------------- */
#card {
  position: fixed;
  right: 8px;
  bottom: 4px;
  width: 424px;
  height: 160px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 3px 4px 4px;
  z-index: 22;
}
#cardHead {
  flex: none;
  min-height: 12px;
  padding: 0 2px;
  font: 600 10px/1.2 var(--font);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The roster and enemy pages need a full sentence, not a label, so they get
   two lines and the grid gives them up. The enemy page has fifteen empty
   slots underneath and no business cutting its one instruction in half. */
#card.page-roster #cardHead,
#card.page-enemy #cardHead {
  min-height: 26px;
  white-space: normal;
  text-transform: none;
  letter-spacing: 0;
  font-size: 11px;
  color: var(--ink-2);
}
#card .grid {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  grid-template-rows: repeat(3, minmax(0, 1fr));
  gap: 4px;
}

/* A slot is a fixed piece of furniture: the verb is always in the same place,
   so the key is always where the button is. Even an unused one keeps its
   rectangle — the grid has to read as a grid or the muscle memory never
   forms. */
.slot {
  position: relative;
  overflow: hidden;
  padding: 0;
  text-align: left;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
}
.slot:hover:not(:disabled):not(.empty) { background: var(--panel-3); border-color: var(--line-strong); }
.slot:active:not(:disabled):not(.empty) { background: #12161a; }
.slot > .glyph {
  position: absolute;
  left: 4px;
  top: 50%;
  transform: translateY(-50%);
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
  color: var(--ink-2);
}
.slot > .glyph > svg {
  width: 20px;
  height: 20px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Unit and structure slots use the sprite itself, so the button looks like
   the thing that will appear on the map. */
.slot > .glyph > canvas { width: 24px; height: 24px; margin: -2px; }
/* 10.5 px, not the 11.5 the lens asks for: at 80 px wide with a glyph and a
   keycap the label gets 50 px, and at 11.5 "Resupply" and "Command" both
   broke mid-word. A word a player can read beats a font size nobody can
   measure. */
.slot > .label {
  position: absolute;
  left: 26px;
  right: 3px;
  top: 9px;
  bottom: 5px;
  display: flex;
  align-items: center;
  font-size: 10.5px;
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -0.005em;
  color: var(--ink);
  overflow: hidden;
  overflow-wrap: break-word;
  hyphens: auto;
}
/* A cost line only claims the bottom strip when there is one. */
.slot:has(.sub:not(:empty)) > .label { bottom: 11px; }
/* The roster page's two-line header (below) shrinks the grid it hands the
   slots, so the label reclaims the strip the rule above gives up to the cost
   line — an id beats the :has() above it regardless of the sub line. */
#card.page-roster .slot > .label { bottom: 5px; }
.slot > kbd {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 15px;
  height: 12px;
  border-bottom-width: 1px;
  font-size: 9px;
}
.slot > .sub {
  position: absolute;
  left: 2px;
  right: 3px;
  bottom: 1px;
  font-size: 9.5px;
  color: var(--dim);
  text-align: right;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#card.page-roster .slot > .sub { font-size: 9px; }
.slot > .sub:empty { display: none; }
.slot > .bar {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: 0;
  background: var(--accent);
  text-decoration: none;
}

.slot.empty {
  background: transparent;
  border-style: solid;
  border-color: rgba(42, 49, 54, 0.7);
  cursor: default;
  pointer-events: none;
}
.slot.empty > .glyph, .slot.empty > .label, .slot.empty > kbd, .slot.empty > .sub { opacity: 0; }
.slot.disabled, .slot:disabled { opacity: 0.45; cursor: default; }
.slot.disabled .sub.bad, .slot .sub.bad { color: var(--bad); }
.slot.disabled .sub.warn, .slot .sub.warn { color: var(--warn); }
.slot.active { border-color: var(--accent); box-shadow: inset 0 -2px 0 0 var(--accent); }
.slot.armed { border-color: var(--accent); box-shadow: inset 0 0 0 2px var(--accent); }
.slot.armed > .label::after { content: "…"; }
.slot.progress > .bar { background: var(--accent); }

/* -------------------------------------------------------------------- */
/* 7. Floating layers: toasts, tooltip, tip strip, overlays popover      */
/* -------------------------------------------------------------------- */
#toasts {
  position: fixed;
  inset: 0;
  z-index: 35;
  pointer-events: none;
}
#toasts > .toast {
  position: absolute;
  max-width: 320px;
  padding: 6px 9px;
  background: var(--panel-solid);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow);
  font-size: 12.5px;
  line-height: 1.3;
  /* JS sets left/top to the exact click point (02 §7.2), so the box has to
     supply the rest of the offset: centred on it horizontally, and clear of
     the cursor above it rather than sitting under the player's own hand. */
  transform: translate(-50%, calc(-100% - 12px));
  animation: toast-in 120ms ease-out;
}
@keyframes toast-in { from { opacity: 0; transform: translate(-50%, calc(-100% - 7px)); } }
#toasts > .toast.info { border-color: var(--line-strong); }
#toasts > .toast.warn { border-color: var(--warn); color: var(--warn); }
#toasts > .toast.bad { border-color: var(--bad); color: var(--bad); }
#toasts > .toast.out { opacity: 0; transition: opacity 220ms linear; }
/* Button-born refusals have no click point, so they land above the card —
   and above the tip strip, which occupies the line directly over it. */
#toasts > .toast.centre {
  left: 50%;
  bottom: 236px;
  top: auto;
  transform: translateX(-50%);
}

#tooltip {
  position: fixed;
  z-index: 40;
  max-width: 300px;
  padding: 7px 9px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--ink-2);
  box-shadow: var(--shadow);
  pointer-events: none;
}
/* The map's own hover tooltip (02 §8.1): the same box, positioned at the
   cursor by JS rather than anchored to an element, so it carries no offset
   rules of its own — just the id's own size, restated so it cannot drift. */
#tooltip.map { font-size: 12px; max-width: 300px; }
#tooltip b { display: block; color: var(--ink); font-size: 12.5px; }
/* The bodies are composed with newlines — what it does, then where it may
   go — and one grey block ran the two together. */
#tooltip .k { color: var(--dim); white-space: pre-line; }
#tooltip .reason { color: var(--warn); }
#tooltip .cost { color: var(--ink); }
#tooltip .row { display: flex; justify-content: space-between; gap: 12px; }
#tooltip .row > .v { color: var(--ink); }

/* The contextual tip strip: one line, amber edge, a dismiss button. It sits
   above the bottom row rather than over it — a tip that covers the command
   card is a tip that stops the thing it is explaining. */
#tip {
  position: fixed;
  left: 50%;
  bottom: 172px;
  transform: translateX(-50%);
  max-width: 560px;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 8px 8px 8px 11px;
  border-left: 3px solid var(--warn);
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--ink-2);
  z-index: 26;
}
#tipText { min-width: 0; }
#tipClose {
  flex: none;
  width: 20px;
  height: 20px;
  padding: 0;
  background: transparent;
  border-color: transparent;
  color: var(--dim);
  font-size: 14px;
  line-height: 1;
}
#tipClose:hover:not(:disabled) { color: var(--ink); border-color: var(--line); }

#overlays {
  position: fixed;
  top: 48px;
  right: 8px;
  display: grid;
  gap: 5px;
  padding: 9px 11px;
  z-index: 45;
  box-shadow: var(--shadow);
}
#overlays.open, #overlays:not([hidden]) { display: grid; }
#overlays label {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 12px;
  color: var(--ink-2);
  cursor: pointer;
  white-space: nowrap;
}
#overlays label:hover { color: var(--ink); }

#matchover {
  position: fixed;
  top: 52px;
  left: 50%;
  transform: translateX(-50%);
  padding: 6px 12px;
  font-size: 12.5px;
  color: var(--ink-2);
  z-index: 34;
}

/* -------------------------------------------------------------------- */
/* 8. Hotkeys overlay                                                    */
/* -------------------------------------------------------------------- */
#keys { background: rgba(4, 6, 9, 0.72); z-index: 60; cursor: pointer; }
#keys > .sheet {
  width: min(1000px, 96vw);
  max-height: 92vh;
  overflow-y: auto;
  background: rgba(20, 24, 27, 0.96);
  cursor: default;
}
#keys h2 { font-size: 15px; font-weight: 600; margin-bottom: 10px; }
#keys .cols {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 22px;
}
#keys > .sheet > .col, #keys .cols > .col { min-width: 0; }
#keys .col > h3 {
  font: 600 10px/1 var(--font);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 7px;
  padding-bottom: 5px;
  border-bottom: 1px solid var(--line);
}
#keys .row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 2.5px 0;
  font-size: 13px;
}
#keys .row > kbd { flex: none; }
#keys .row > span { color: var(--ink-2); }
#keys .foot { margin-top: 14px; padding-top: 10px; border-top: 1px solid var(--line); font-size: 12px; color: var(--dim); }
/* The 5×3 diagram: the card, drawn as itself, so "the button is where the key
   is" is shown rather than asserted. */
#keys .cardmap {
  float: right;
  display: grid;
  grid-template-columns: repeat(5, 34px);
  gap: 4px;
  margin: 0 0 12px 20px;
}
#keys .cols { clear: both; }
#keys .cardmap > kbd { width: 34px; height: 26px; font-size: 11px; }

/* -------------------------------------------------------------------- */
/* 9. Escape menu, confirm, end card, doctrine drawer                    */
/* -------------------------------------------------------------------- */
#escmenu { z-index: 70; }
#escmenu > .sheet { width: 360px; display: flex; flex-direction: column; gap: 8px; }
#escmenu .dim { font-size: 11px; }
#escInfo { font-size: 12.5px; color: var(--ink-2); margin-bottom: 2px; }
#escmenu button { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; }
#escmenu .settings {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 9px 10px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
}
#escmenu .settings label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--ink-2);
  cursor: pointer;
}

#confirm { z-index: 75; }
#confirm > .sheet { width: 380px; display: flex; flex-wrap: wrap; gap: 8px; }
#confirmText { width: 100%; font-size: 13px; line-height: 1.45; margin-bottom: 4px; }
#confirm button { flex: 1 1 0; justify-content: center; }

#end { z-index: 80; background: rgba(4, 6, 9, 0.62); }
#end > .sheet { width: 520px; max-width: 100%; }
#endTitle {
  font-size: 34px;
  font-weight: 800;
  letter-spacing: -0.01em;
  line-height: 1.05;
  color: var(--dim);
}
#endTitle.won { color: var(--good); }
#endTitle.lost { color: var(--bad); }
#endTitle.draw { color: var(--dim); }
#endReason { margin-top: 6px; font-size: 13px; color: var(--ink-2); }
#endStats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px 26px;
  margin: 16px 0;
  padding: 14px 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  font-size: 13px;
}
#endStats > .stat { display: flex; align-items: baseline; gap: 10px; }
#endStats > .stat > .k { color: var(--dim); }
#endStats > .stat > .v { margin-left: auto; font-weight: 600; }
#endDoctrine { font-size: 12px; color: var(--dim); margin-bottom: 16px; }
#endDoctrine:empty { display: none; }
#end .row, #done .row { display: flex; flex-wrap: wrap; gap: 8px; }

#doctrineDrawer {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(900px, 94vw);
  max-height: 78vh;
  overflow-y: auto;
  padding: 14px 16px 16px;
  background: var(--panel-solid);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--shadow);
  z-index: 50;
}
#doctrineDrawer .head { position: relative; padding-right: 34px; margin-bottom: 10px; }
#doctrineDrawer h2 { font-size: 15px; font-weight: 600; }
#doctrineDrawer .head p { font-size: 12px; margin-top: 2px; }
/* The dynamic line hud.js appends after the static one: how many picks are
   left, right now, in the same small dim voice as the rule above it. */
#doctrineDrawer .head > p.picks-line { color: var(--dim); margin-top: 4px; }
#doctrineClose {
  position: absolute;
  right: 0;
  top: -2px;
  width: 28px;
  height: 28px;
  padding: 0;
  background: transparent;
  border-color: transparent;
  color: var(--dim);
  font-size: 16px;
  line-height: 1;
}
#doctrineClose:hover:not(:disabled) { color: var(--ink); border-color: var(--line); }
#doctrine {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 7px;
}
.doc { display: block; text-align: left; padding: 8px 10px; line-height: 1.35; }
.doc > .slot {
  display: block;
  position: static;
  padding: 0;
  background: none;
  border: 0;
  overflow: visible;
  font: 600 9.5px/1 var(--font);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
  margin-bottom: 3px;
}
.doc > b { display: block; font-size: 13px; margin-bottom: 3px; }
.doc > .gain { display: block; font-size: 11.5px; color: var(--good); }
.doc > .cost { display: block; font-size: 11.5px; color: var(--warn); }
.doc.taken { border-color: var(--accent); opacity: 0.6; }

/* -------------------------------------------------------------------- */
/* 10. The tutorial: coach card, brief, completion, highlights           */
/* -------------------------------------------------------------------- */
#coach {
  position: fixed;
  top: 56px;
  left: 50%;
  transform: translateX(-50%);
  width: 460px;
  max-width: calc(100vw - 260px);
  padding: 9px 12px 10px;
  background: var(--panel-solid);
  border-left: 3px solid var(--accent);
  z-index: 28;
}
.coach-head { display: flex; align-items: baseline; gap: 8px; }
#coachStep { font-size: 10px; letter-spacing: 0.09em; color: var(--dim); }
#coachTitle { font-size: 14px; font-weight: 600; flex: 1; }
#coachSkip { font-size: 10px; color: var(--dim); padding: 1px 6px; }
#coachBody { margin-top: 4px; font-size: 12.5px; line-height: 1.45; }
/* 22, not 16: #coachProgress sits top:0 over the same row, and a 16 px box
   let the fill bar run straight through the token text below it. */
#coachMeter { position: relative; height: 22px; margin-top: 6px; }
#coachFill {
  position: absolute;
  left: 0;
  right: 0;
  top: 11px;
  height: 4px;
  width: var(--w, 0%);
  background: var(--accent);
  border-radius: 2px;
  transition: width 300ms;
}
#coachProgress { position: absolute; right: 0; top: 0; font-size: 11px; color: var(--dim); }
/* A token that turned green as the player did the thing it names. */
#coachProgress .tok.on { color: var(--good); }
#coachHint { margin-top: 4px; font-size: 12px; color: var(--warn); }
#coachNext { margin-top: 8px; }
/* S5's second line (tutorial.js's own words): a callout under the body,
   under its own hairline rather than the body's accent bar on the left. */
.coach-callout {
  margin-top: 4px;
  padding-top: 4px;
  border-top: 1px solid var(--line);
  font-size: 12px;
  color: var(--ink-2);
}
#coach.done { border-left-color: var(--good); }
#coach.enter { animation: coach-in 180ms ease-out; }
@keyframes coach-in { from { opacity: 0; transform: translate(-50%, -6px); } }
/* Notifications and the coach share the top-centre. While the coach is up the
   stack drops below it rather than landing on top of the instruction the
   player is in the middle of reading. tutorial.js publishes --coach-h on #hud
   because the card's height changes with its hint and callout lines, so a
   fixed offset was wrong every time it did. */
#hud:has(#coach:not([hidden])) #notify { top: calc(56px + var(--coach-h, 120px) + 10px); }

/* The highlight. The pulse is the accent at 55 % — paper, so it reads on the
   dark HUD without being mistaken for a faction or a semantic. */
.coach-hi {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  position: relative;
  z-index: 27;
  animation: coach-pulse 1.1s ease-in-out infinite;
  border-radius: var(--r-sm);
}
@keyframes coach-pulse {
  0%, 100% { box-shadow: 0 0 0 0 var(--accent-55); }
  50% { box-shadow: 0 0 0 9px rgba(244, 239, 228, 0); }
}
/* Buttons the current step did not name stay clickable, just quiet. */
.coach-dim .slot:not(.coach-hi) { opacity: 0.45; }

#brief { z-index: 72; background: rgba(4, 6, 9, 0.72); }
#brief > .sheet { width: 520px; max-width: 100%; display: flex; flex-wrap: wrap; gap: 10px; }
#briefTitle { width: 100%; font-size: 22px; font-weight: 700; }
#briefBody { width: 100%; font-size: 13.5px; line-height: 1.55; color: var(--ink-2); margin-bottom: 4px; }

#done { z-index: 80; background: rgba(4, 6, 9, 0.62); }
#done > .sheet { width: 560px; max-width: 100%; }
#doneTitle { font-size: 28px; font-weight: 800; color: var(--good); line-height: 1.1; }
#doneTitle.lost { color: var(--bad); }
#doneStats { margin-top: 8px; font-size: 13px; color: var(--ink-2); }
#doneNext { margin: 12px 0 16px; font-size: 12.5px; line-height: 1.5; }

/* -------------------------------------------------------------------- */
/* 11. Touch mode pill (shown only by touch.css)                         */
/* -------------------------------------------------------------------- */
#touchMode { display: none; }

/* -------------------------------------------------------------------- */
/* 12. The menu                                                          */
/* -------------------------------------------------------------------- */
#menu {
  position: fixed;
  inset: 0;
  overflow-y: auto;
  background: var(--bg);
  z-index: 90;
}
#menuBoard {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
}
/* The board is the game's own terrain painter; a wash keeps the type on it
   readable without hiding what it is. */
#menu::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  background:
    radial-gradient(1200px 620px at 22% 26%, rgba(14, 18, 20, 0.15), rgba(14, 18, 20, 0.82) 62%),
    linear-gradient(to bottom, rgba(14, 18, 20, 0.55), rgba(14, 18, 20, 0.9));
  pointer-events: none;
}
.menu-inner {
  position: relative;
  z-index: 1;
  max-width: 1180px;
  margin: 0 auto;
  padding: 52px 28px 64px;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 560px;
  align-items: start;
  gap: 34px 40px;
}

.hero { grid-column: 1; align-self: center; padding-top: 24px; }
#menu h1 {
  font-size: 56px;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1;
}
.tagline { margin-top: 12px; max-width: 44ch; font-size: 15px; line-height: 1.45; color: var(--ink-2); }
.facts { margin-top: 10px; font-size: 12px; color: var(--ink-2); letter-spacing: 0.02em; }
/* The hero sits on #menu's own terrain painter, not a solid panel, and the
   board underneath it is sometimes pale open ground — the same problem the
   map cursors solve with a halo stroke. A shadow does the same job for type. */
.hero h1, .hero .tagline, .hero .facts {
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7), 0 0 18px rgba(0, 0, 0, 0.55);
}
#menu h2 { font-size: 15px; font-weight: 600; margin-bottom: 9px; }
#menu h2 .dim { font-weight: 400; }

section.play {
  grid-column: 2;
  padding: 16px 16px 18px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--shadow), var(--inset);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
section.campaign, section.about { grid-column: 1 / -1; }

#tutorialCard {
  padding: 12px 13px 13px;
  margin-bottom: 18px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
}
#tutorialCard.first { border-color: var(--accent); }
#tutorialBtn { width: 100%; }
#tutorialSub { margin-top: 7px; font-size: 11.5px; }
/* Once the orientation is done the card is a receipt, not a pitch. */
#menu.done #tutorialCard, #tutorialCard.done { border-color: transparent; background: transparent; padding: 0 0 12px; }
#menu.done #tutorialBtn, #tutorialCard.done #tutorialBtn {
  width: auto;
  font-size: 12px;
  padding: 7px 12px;
  background: var(--panel-2);
  border-color: var(--line);
  color: var(--ink);
}
/* Half-finished: PLAY is the primary again and the orientation is an offer,
   not an instruction (01 §6.2). */
#menu.skipped #tutorialCard, #tutorialCard.skipped { border-color: var(--line); }
#menu.skipped #tutorialBtn, #tutorialCard.skipped #tutorialBtn {
  background: var(--panel-2);
  border-color: var(--line-strong);
  color: var(--ink);
  font-size: 12px;
  padding: 9px 14px;
}
/* On a first visit PLAY is the second thing offered, so it looks like it. */
#menu.first #playBtn { background: var(--panel-2); border-color: var(--line-strong); color: var(--ink); }

#factionRow {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
  margin-bottom: 14px;
}
.faction {
  position: relative;
  display: block;
  height: auto;
  min-height: 152px;
  padding: 9px 9px 8px;
  text-align: left;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--r);
  overflow: hidden;
  line-height: 1.25;
}
.faction:hover { border-color: var(--line-strong); }
/* 40 px, matching the SVG's own width/height attributes (menu.js) — a
   stylesheet width on `.emblem > svg` outranks them, so this is the number
   that actually ships. */
.faction > .emblem { display: block; width: 40px; height: 40px; color: var(--fc, var(--dim)); margin-bottom: 4px; }
.faction > .emblem > svg { width: 40px; height: 40px; display: block; stroke: currentColor; }
.faction > b { display: block; font-size: 13.5px; font-weight: 600; line-height: 1.15; color: var(--fc, var(--ink)); }
/* Two lines each, clamped: the identity and the weakness are both real
   sentences and a card that lets them run just clips them mid-word. The
   emblem is a `<span>` too (menu.js), so it has to be excluded by name or it
   picks up a line-clamp box model meant for text. */
.faction > span:not(.emblem) {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: 11.5px;
  line-height: 1.25;
  color: var(--ink);
  margin-top: 3px;
}
.faction > i {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: 10.5px;
  line-height: 1.25;
  color: var(--dim);
  margin-top: 4px;
}
.faction.on {
  border: 2px solid var(--fc, var(--accent));
  padding: 8px 8px 7px;
  background:
    linear-gradient(color-mix(in srgb, var(--fc, #fff) 10%, transparent), color-mix(in srgb, var(--fc, #fff) 10%, transparent)),
    var(--panel-2);
}
.faction.on > .emblem > svg { fill: color-mix(in srgb, var(--fc, #fff) 25%, transparent); }

section.play .row {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  align-items: end;
  gap: 8px;
  margin-bottom: 14px;
}
section.play label {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 4px;
  min-width: 0;
  height: 100%;
  font: 600 10px/1 var(--font);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--dim);
}
section.play label .dim { text-transform: none; letter-spacing: 0; font-weight: 400; }
/* menu.js dims the difficulty label when the mode has no director to tune. */
section.play label.off { opacity: 0.45; }
section.play input, section.play select { min-width: 0; width: 100%; }
#playBtn { width: 100%; height: 44px; padding: 0; }

#campaignList {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}
.mission {
  position: relative;
  display: block;
  height: 92px;
  padding: 10px 11px;
  text-align: left;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--r);
  line-height: 1.3;
}
.mission:hover { border-color: var(--line-strong); }
.mission > .num {
  display: block;
  font-size: 11px;
  color: var(--dim);
  letter-spacing: 0.08em;
}
.mission > b { display: block; font-size: 14px; font-weight: 600; margin-top: 2px; }
.mission > span { display: block; font-size: 11.5px; color: var(--dim); margin-top: 3px; }
.mission > .badge {
  position: absolute;
  top: 9px;
  right: 10px;
  padding: 2px 7px;
  border-radius: 10px;
  font: 600 10px/1.3 var(--font);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.mission > .badge.next { background: var(--accent); color: var(--accent-ink); }
/* A pulsing dot on the one card the player is meant to press next. */
.mission > .badge.next::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 5px;
  border-radius: 50%;
  background: var(--accent-ink);
  vertical-align: 0;
  animation: cell-pulse 1.4s ease-in-out infinite;
}
.mission > .badge.done { background: rgba(127, 201, 107, 0.12); color: var(--good); }
.mission > .badge.later, .mission > .badge.locked { background: var(--panel-3); color: var(--dim); }
/* Later missions are dimmed and still start: the server accepts any mission
   id, and a card that explains what it teaches and then refuses is worse than
   no card at all. */
.mission.later, .mission.locked { opacity: 0.8; }
.mission.done > b { color: var(--good); }
.mission.next { border-color: var(--accent); }

section.about ul { margin: 0; padding-left: 18px; columns: 2; column-gap: 34px; }
section.about li { margin-bottom: 7px; font-size: 12.5px; line-height: 1.45; color: var(--ink-2); break-inside: avoid; }
section.about li b { color: var(--ink); }
.notice {
  max-width: 84ch;
  margin-top: 14px;
  padding-left: 10px;
  border-left: 2px solid var(--line);
  font-size: 11.5px;
  color: var(--dim);
}
.notice:empty { display: none; }
/* A refused join is the one thing this line ever has to say URGENTLY, and it
   normally lives at the foot of the About list where nobody bounced back from
   a match is looking. As an error it leaves the section and becomes a banner
   over the menu; main.js clears it on the next start. */
.notice.bad {
  position: fixed;
  left: 50%;
  top: max(14px, env(safe-area-inset-top));
  transform: translateX(-50%);
  z-index: 60;
  max-width: min(520px, calc(100vw - 24px));
  margin: 0;
  padding: 9px 14px;
  background: var(--panel-solid);
  border: 1px solid var(--bad);
  border-left: 3px solid var(--bad);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow);
  font-size: 13px;
  color: var(--bad);
}
section.about > .dim { margin-top: 10px; font-size: 11.5px; }
section.about [data-account-open], section.about [data-account-showkey] { margin: 12px 8px 0 0; }

/* -------------------------------------------------------------------- */
/* 13. Loader                                                            */
/* -------------------------------------------------------------------- */
#loader {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: var(--bg);
  z-index: 100;
}
.loader-box { text-align: center; padding: 20px; }
.loader-box h1 { font-size: 40px; font-weight: 800; letter-spacing: -0.02em; margin-bottom: 8px; }
.loader-box p { margin-bottom: 10px; max-width: 46ch; font-size: 13px; }

/* -------------------------------------------------------------------- */
/* 14. Environment                                                       */
/* -------------------------------------------------------------------- */

/* A blur costs a full-screen composite every frame. On a phone that is the
   difference between 60 and 40 fps, and a phone GPU is already drawing the
   map. 03 §5.3: no backdrop-filter where there is no hover, and the panels
   go solid so they stay readable without one. */
@media (hover: none) {
  :root { --panel: #14181b; }
  #topbar, #mission, #tray, #minimap, #selpanel, #card, #coach, #matchover,
  #tip, #overlays, #tooltip, #notify > .note, section.play {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  #feed:hover { max-height: 90px; overflow: hidden; background: none; border: 0; padding: 0 2px; }
}

/* Motion here is only ever a signal — a pulse that means "low", a slide that
   means "new". Someone who has asked for less of it still gets the colour and
   the position, which carry the same information. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .coach-hi { animation: none; outline-width: 3px; }
  .cell.pulse, #tray > .chip.stalled { animation: none; }
  .cell.pulse > b { text-decoration: underline; text-decoration-style: dotted; }
}

/* The minimap shrinks before the selection panel does, because the panel holds
   words and the minimap holds a picture that survives being smaller. */
@media (max-width: 1280px) { :root { --mm: 200px; } }
@media (max-width: 1100px) { :root { --mm: 160px; } }

/* The top bar is the one row that cannot wrap and cannot scroll, so it is
   budgeted rather than trusted. At 1440 the cells, the strip, the 88 px the
   account pill sits in and the link add up to 1389 px. Below that the budget
   is spent in this order, cheapest information first. */
@media (max-width: 1400px) {
  /* The pill goes back where its own CSS puts it — bottom-left, over the
     minimap's corner — rather than costing the bar 88 px it has not got. */
  body.in-match .sga-floating { left: max(12px, env(safe-area-inset-left)); right: auto; top: auto; bottom: max(12px, env(safe-area-inset-bottom)); transform: scale(0.86); transform-origin: left bottom; }
  #topbar .link { margin-left: 12px; }
  #cellDom { min-width: 150px; }
}
@media (max-width: 1320px) {
  /* The kill chain is the first cell to go: the selection panel prints the
     same number for whatever is selected, and every other cell is a ceiling
     you cannot see any other way. */
  #cellChain { display: none; }
  .cell { min-width: 92px; }
  .cell.wide { min-width: 108px; }
  .cell.wider { min-width: 156px; }
  #cellSup, #cellCtl { min-width: 92px; }
  #cellSat { min-width: 108px; }
  #cellClock { min-width: 104px; }
  .cell > b { font-size: 15px; }
  #cellSat > b { font-size: 13px; }
  #topbar .strip button { width: 30px; }
  #topbar .strip #btnStrike { width: 42px; }
  /* The structure panel is the one state that spends its middle column on two
     lists; below 1320 it buys the width back from the stats, which are four
     short lines and the only column with slack. */
  #selpanel.structure { grid-template-columns: 106px minmax(140px, 1fr) minmax(0, 176px); }
}

@media (max-width: 1120px) {
  /* The last step before the phone layout takes over at 1100. */
  .cell { min-width: 84px; }
  .cell.wide { min-width: 100px; }
  .cell.wider { min-width: 140px; }
  #magBar { width: 120px; }
  #cellSup, #cellCtl { min-width: 84px; }
  #cellSat { min-width: 96px; }
  #cellClock { min-width: 92px; }
  #cellDom { min-width: 130px; }
  #topbar .link { margin-left: 6px; }
}

/* Section headings sit on the map board like the hero does, and the dim half
   of "Campaign — six missions, one mechanic each" vanished into a wood. Same
   shadow as the hero, and the subtitle one step up from --dim. */
#menu .menu-inner section > h2 {
  text-shadow: 0 1px 2px rgba(0, 0, 0, .7), 0 0 14px rgba(0, 0, 0, .5);
}
#menu .menu-inner section > h2 .dim { color: var(--ink-2); }
