/* ════════════════════════════════════════════════════════════
   COSMO ARENA HUD — standalone stylesheet, the ONLY place HUD
   styling lives. Two independent layouts keyed on body.touch:
   no responsive breakpoints, no legacy cascade.
   ════════════════════════════════════════════════════════════ */
:root {
  /* Defaults only — game.js overwrites these from CFG.hud at boot, which is
     what lets the Control Room re-skin the interface without a stylesheet
     edit. Anything added here needs a matching entry in applyHudTheme(). */
  --hud-radius: 8px;
  --hud-border-w: 1px;
  --hud-line: rgba(56, 178, 230, .55);
  --hud-line-hi: rgba(120, 214, 255, .8);
  --hud-glow: rgba(56, 178, 230, .22);
  --hud-glass-a: rgba(8, 16, 27, .93);
  --hud-glass-b: rgba(3, 8, 15, .96);
  --hud-gold: #eab543;
  --hud-red: rgba(232, 92, 62, .6);
}

/* ─────────────────────────── shared primitives ─────────────────────────── */
.hudPlate {
  border: var(--hud-border-w) solid var(--hud-line);
  border-top-color: var(--hud-line-hi);
  border-radius: var(--hud-radius);
  background:
    radial-gradient(130% 80% at 50% 0%, rgba(40, 90, 140, .18), transparent 55%),
    linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 12px var(--hud-glow),
              inset 0 1px 0 rgba(140, 210, 250, .18);

}

/* upgrade + refit + ability icons (inline SVG) */
.ico { display: inline-block; width: 18px; height: 18px; background-size: contain; background-position: center; background-repeat: no-repeat; }
/* Custom art dropped in icons/ — the image itself comes from an inline
   background-image, this just gives it the same box and fit as a built-in. */
/* Custom art, letterboxed inside the normal glyph box. */
.icoCustom { background-size: contain; background-repeat: no-repeat; background-position: center; }

/* ...or bled edge to edge across the whole tile ("fill" mode). The art becomes
   the tile's background and the level / cost / name labels ride on top of it,
   so nothing the player needs to read gets covered. */
/* NO overflow:hidden here — the tooltips are positioned outside these tiles and
   clipping the tile would clip them too. The art rounds its own corners
   instead. */
.upgTile, .cmd { position: relative; }
/* !important because touch has an ID-selector rule further down
   (#commandCard .cmd .aico { width: 15px }) that otherwise outranks three
   classes and was shrinking every "fill" icon on mobile back down to glyph
   size — the icon looked tiny with a big empty tile around it. */
.ico.icoCustom.icoFill {
  position: absolute !important; inset: 0 !important;
  width: auto !important; height: auto !important;
  background-size: cover; background-position: center;
  border-radius: inherit; z-index: 0; pointer-events: none;
}
/* keep the text legible over busy art */
.upgTile > b, .cmd > b, .cmd .cName, .cmd .cCost, .cmd .cLvl {
  position: relative; z-index: 1;
}
.upgTile:has(.icoFill) > b, .cmd:has(.icoFill) > b,
.cmd:has(.icoFill) .cName, .cmd:has(.icoFill) .cCost, .cmd:has(.icoFill) .cLvl {
  text-shadow: 0 1px 3px rgba(0, 0, 0, .95), 0 0 8px rgba(0, 0, 0, .8);
}
/* the tip must escape the tile even though the tile now clips its art */
.upgTile .tip, .cmd.ability .tip, .cmd.upg .tip { z-index: 60; }

/* ---- Decorative tile frame -------------------------------------------------
   A single image stretched over the whole tile, drawn ABOVE the icon art but
   BELOW the labels. When a frame is in use the tile drops its own border and
   background so the art isn't sitting inside two frames at once. */
.tileFrame {
  position: absolute; inset: 0;
  background-repeat: no-repeat; background-position: center;
  background-size: 100% 100%;
  pointer-events: none; z-index: 2;
}
.framed {
  border-color: transparent !important;
  background: none !important;
  box-shadow: none !important;
}
/* labels ride above the frame */
.framed > b, .framed .cName, .framed .cCost, .framed .cLvl { z-index: 3; }
/* the icon art pulls in slightly so the frame's bevel doesn't cover it */
.framed .ico.icoCustom.icoFill { inset: 9% !important; border-radius: 2px; }
.ico-weapons{ background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23ffd27f" stroke-width="1.9" stroke-linecap="round"><circle cx="12" cy="12" r="7"/><path d="M12 1v4M12 19v4M1 12h4M19 12h4"/><circle cx="12" cy="12" r="1.6" fill="%23ffd27f"/></svg>'); }
.ico-missiles { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23ff9a6a" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2c2.5 2 4 5 4 8.5 0 2.4-.6 4.6-1.6 6.5h-4.8C8.6 15.1 8 12.9 8 10.5 8 7 9.5 4 12 2z"/><circle cx="12" cy="9" r="1.6"/><path d="M9.6 17l-2 4M14.4 17l2 4M12 17v4"/></svg>'); }
.ico-hull { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%236aff8a" stroke-width="1.9" stroke-linejoin="round"><path d="M12 2l8 3.5v6c0 5-3.4 9.2-8 10.5-4.6-1.3-8-5.5-8-10.5v-6z"/><path d="M8.5 12l2.5 2.5 4.5-5" stroke-linecap="round"/></svg>'); }
.ico-shields { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%237fd4ff" stroke-width="1.9"><circle cx="12" cy="12" r="8.5"/><circle cx="12" cy="12" r="4.5" opacity=".65"/></svg>'); }
.ico-range { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23c9a6ff" stroke-width="1.9" stroke-linecap="round"><path d="M3 12h18"/><path d="M17 7l5 5-5 5"/><path d="M7 8v8" opacity=".7"/></svg>'); }
.ico-engines { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23ff9a6a" stroke-width="1.9" stroke-linejoin="round" stroke-linecap="round"><path d="M14 3l-3 8h5l-4 10 1.5-7H8z"/></svg>'); }
.ico-reactor { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%2366e6c8" stroke-width="1.8"><circle cx="12" cy="12" r="2.2" fill="%2366e6c8"/><ellipse cx="12" cy="12" rx="10" ry="4.4"/><ellipse cx="12" cy="12" rx="10" ry="4.4" transform="rotate(60 12 12)"/><ellipse cx="12" cy="12" rx="10" ry="4.4" transform="rotate(120 12 12)"/></svg>'); }
.aico { display: block; width: 28px; height: 28px; margin: 0 auto; background-size: contain; background-position: center; background-repeat: no-repeat; }
.aico-target  { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%234fc7f5" stroke-width="1.7"><circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="3.2"/><path d="M12 1.5v4M12 18.5v4M1.5 12h4M18.5 12h4" stroke-linecap="round"/></svg>'); }
.aico-rockets { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%234fc7f5" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M7 3c1.4 1.2 2 3 2 5l-.6 6H5.6L5 8c0-2 .6-3.8 2-5z"/><path d="M17 3c1.4 1.2 2 3 2 5l-.6 6h-2.8L15 8c0-2 .6-3.8 2-5z"/><path d="M6 17l-1.4 3.4M8 17l1.4 3.4M16 17l-1.4 3.4M18 17l1.4 3.4"/></svg>'); }
.aico-shield  { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%234fc7f5" stroke-width="1.7" stroke-linejoin="round"><path d="M12 2l8 3.4v6.2c0 5-3.4 9.1-8 10.4-4.6-1.3-8-5.4-8-10.4V5.4z"/></svg>'); }
.aico-boost   { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23eab543" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 20l7-6 7 6M5 13l7-6 7 6"/></svg>'); }
.aico-plus    { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%235df08e" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>'); }

/* ─────────────────────────── frame chrome ─────────────────────────── */
/* SAFE AREA.
   index.html asks for viewport-fit=cover, which is right -- the starfield
   should run edge to edge under the notch. But nothing then inset the
   CONTROLS, so on a phone with gesture navigation the bottom row of the HUD
   sat inside the system gesture strip, and taps there go to the OS instead of
   the page. That is why the abilities button sometimes needed a second press:
   the first one was caught by the gesture bar, not missed by the button.

   Insetting the HUD CONTAINER moves every absolutely-positioned panel inside
   it at once, so the map, the console, the refit drawer and the ship card all
   come clear of it together. The canvas behind is untouched and still covers
   the whole screen. */
#hud {
  position: fixed;
  /* BOTTOM ONLY.
     The gesture bar along the bottom edge genuinely eats taps, which is what
     this was added for. The left and right insets are the camera cutout, and
     insetting for those pushed the whole HUD inward -- so "top-left corner"
     came out as a fat margin down the left-hand side. The corner is the
     corner. */
  /* NO bottom inset.

     This reserved the home-indicator strip so the gesture bar could not eat
     taps. On an iPhone in landscape that is ~21px of empty screen under the
     ship card, and it read as a gap nobody asked for -- twice, at full and at
     half. The panels along the bottom are large, and their CONTENT sits well
     inside them, so the few pixels the bar actually claims land on panel
     chrome rather than on a button.

     Measured, not assumed: canvas and window both report 844x390 on the
     device, so the canvas covers the screen and this inset was the only thing
     holding the HUD up. */
  inset: 0;
  pointer-events: none; z-index: 10; font-family: inherit;
}
#hud > * { pointer-events: auto; }
#hud.hidden { display: none; }

/* top bar: minerals only, centred */
#topBar { position: absolute; top: 8px; left: 0; right: 0; display: flex; justify-content: center; pointer-events: none; }
.stationBar { display: none; }
#minerals {
  pointer-events: auto;
  border: 1px solid var(--hud-line); border-top-color: var(--hud-line-hi); border-radius: 8px;
  background: linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 12px var(--hud-glow);
  padding: 6px 22px;
  font-size: 15px; font-weight: 800; letter-spacing: 1px;
  color: #cfeaff; text-shadow: 0 0 10px rgba(80, 200, 255, .45);
}

/* ══ THE FARM STREAK ═══════════════════════════════════════════════════════
   Kills in a row without being hit, and the mineral multiplier it buys.
   Bare text over the field: it lived in a box in the top bar first and read
   as furniture. Centred high, where the sky is -- your own hull sits at the
   centre of the screen, so this is above the fight, not on it. */
body.layoutEdit #streak { pointer-events: auto !important; cursor: move; }
#streak {
  /* CENTRED WITHOUT A TRANSFORM.
     This used to be left:50% + translateX(-50%), which breaks the moment the
     part can be resized: the individual `scale` property is applied BEFORE
     `transform` in the chain, so the -50% got multiplied by the scale too and
     the counter slid left as it grew -- 27px of drift at 1.6x, measured.
     Auto margins on a shrink-wrapped box centre it with no transform at all,
     leaving translate and scale entirely to the layout editor. */
  position: fixed; left: 0; right: 0; top: 16vh;
  width: max-content; margin-inline: auto;
  z-index: 66; pointer-events: none;
  display: flex; flex-direction: column; align-items: center;
  font-family: inherit;
  color: #8dffbe;
  text-shadow: 0 0 10px rgba(90, 255, 170, .6), 0 0 34px rgba(60, 220, 140, .35);
  -webkit-user-select: none; user-select: none;
}
#streak > b {
  display: block;
  font-size: 56px; line-height: 1; font-weight: 800; letter-spacing: 2px;
  font-variant-numeric: tabular-nums;
  will-change: transform;
}
#streak > span {
  font-size: 13px; letter-spacing: 5px; font-weight: 700; margin-top: 4px; opacity: .85;
}
#streak > span::before { content: 'STREAK '; opacity: .6; }

/* AT RISK: hit once, the next one takes it. Amber, and the number cannot hold
   still. Declared BEFORE the kill punch below, so a kill landed while at risk
   still punches. */
#streak.atRisk {
  color: #ffb063;
  text-shadow: 0 0 12px rgba(255, 170, 70, .75), 0 0 36px rgba(255, 120, 40, .4);
}
#streak.atRisk > b { animation: streakJitter .09s steps(2, end) infinite; }
@keyframes streakJitter {
  0%   { transform: translate(-2px, 1px); }
  50%  { transform: translate(2px, -1px); }
  100% { transform: translate(-1px, -2px); }
}

/* Every kill: the number LANDS. Scaled up and dropped back with a flash of
   white at the top. 300ms, because it happens often. */
#streak.hit > b { animation: streakHit .3s cubic-bezier(.2, 1.6, .4, 1); }
@keyframes streakHit {
  0%   { transform: scale(1.55); color: #fff;
         text-shadow: 0 0 26px rgba(160, 255, 210, .95), 0 0 60px rgba(90, 255, 170, .6); }
  100% { transform: scale(1); }
}

/* Every milestone: a ring goes out and the multiplier flares. */
#streak.pulse::after {
  content: ''; position: absolute; left: 50%; top: 42%;
  width: 90px; height: 90px; margin: -45px 0 0 -45px;
  border-radius: 50%; border: 3px solid currentColor;
  animation: streakRing .7s ease-out forwards;
}
@keyframes streakRing {
  0%   { transform: scale(.4); opacity: .9; }
  100% { transform: scale(3.2); opacity: 0; }
}
#streak.pulse > span { animation: streakFlare .7s ease-out; }
@keyframes streakFlare {
  0%   { color: #fff; letter-spacing: 9px; }
  100% { letter-spacing: 5px; }
}

/* And the moment it goes: the number comes apart where it stood. */
/* THE LOSS, on the counter itself -- so it shatters wherever the number was
   actually standing, including after the part has been dragged elsewhere. */
#streak.lost {
  color: #ff7a62;
  text-shadow: 0 0 14px rgba(255, 90, 60, .7), 0 0 40px rgba(255, 60, 30, .35);
  animation: streakLostFade 1.5s ease-out forwards;
}
#streak.lost > b { animation: streakShatter 1.5s cubic-bezier(.3, .1, .7, 1) forwards; }
@keyframes streakShatter {
  0%   { transform: scale(1); letter-spacing: 2px; filter: blur(0); }
  18%  { transform: scale(1.25); color: #fff; }
  100% { transform: scale(1.1) translateY(48px); letter-spacing: 26px; filter: blur(6px); }
}
@keyframes streakLostFade {
  0%, 55% { opacity: 1; }
  100%    { opacity: 0; }
}

@media (max-width: 760px) {
  /* Clear of the layout bar's top band -- the counter is the one HUD part
     that starts centred at the top, which is exactly where that bar lives.
     Only a default; the editor moves it wherever you like. */
  #streak { top: 22vh; }
  #streak > b { font-size: 34px; }
  #streak > span { font-size: 10px; letter-spacing: 3px; }
  #streak.pulse::after { width: 60px; height: 60px; margin: -30px 0 0 -30px; }
}

/* corner buttons */
#hudToggle, #adminToggle, #fsToggle, #btnSelf, #btnCancel {
  position: absolute;
  min-width: 40px; min-height: 34px; padding: 6px 12px;
  border: 1px solid var(--hud-line); border-radius: 8px;
  background: linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
  color: #cfe8fa; font-size: 13px; font-weight: 800; letter-spacing: 1.4px;
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 10px var(--hud-glow);
  cursor: pointer;
}
#hudToggle { top: 8px; right: 8px; font-size: 16px; }
#adminToggle { top: 8px; right: 58px; }
#fsToggle { top: 8px; right: 108px; }
#btnSelf { top: 8px; left: 8px; }
#btnCancel { top: 8px; left: 110px; display: none; }
body.targeting #btnCancel { display: block; }
#mapToggle { display: none; }
#hudToggle:hover, #adminToggle:hover, #fsToggle:hover, #btnSelf:hover { border-color: var(--hud-line-hi); color: #fff; }
#touchBtns { position: static; }

/* messages */
#respawnMsg {
  position: absolute; top: 38%; left: 0; right: 0; text-align: center;
  font-size: 24px; letter-spacing: 4px; color: #ff8a7a;
  text-shadow: 0 0 20px rgba(255, 80, 60, .5); pointer-events: none;
}
#respawnMsg .spectate { color: #9fd0ff; }
#targetHint {
  position: absolute; top: 54px; left: 0; right: 0; text-align: center;
  font-size: 12px; letter-spacing: 1.6px; color: #ffd27f; pointer-events: none;
  text-shadow: 0 1px 3px #000;
}
#netBadge {
  position: absolute; top: 8px; left: 50%; transform: translateX(-50%); margin-top: 40px;
  font-size: 9px; letter-spacing: 1.4px; color: #7fb8dc; pointer-events: none;
}
.netDot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: #35e07a; margin-right: 5px; }
#netPlayers { display: none; }

/* minimap — always visible, bottom-left */
#minimapPanel {
  position: absolute; left: 8px; bottom: 8px; padding: 5px;
  border: 1px solid var(--hud-line); border-top-color: var(--hud-line-hi); border-radius: 8px;
  background: linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 12px var(--hud-glow);
}
#minimapPanel .panelLabel { display: none; }
#minimap { display: block; border-radius: 5px; background: #02050a; cursor: pointer; }

/* ── expand button ───────────────────────────────────────────────────────
   Top-right corner of the map. PHONES ONLY: on a desktop the map is already
   big enough and the screen has room for it, so the button is only in the way.
   The panel is pointer-events:none in places (see below), so this claims its
   own back. */
#mmExpand { display: none; }
body.touch #mmExpand {
  position: absolute; top: 7px; right: 7px; z-index: 3;
  width: 15px; height: 15px; padding: 0;
  display: grid; place-items: center;
  border: 1px solid var(--hud-line); border-radius: 3px;
  background: rgba(4, 10, 18, .78);
  color: var(--hud-line-hi);
  cursor: pointer; pointer-events: auto;
  opacity: .72; transition: opacity .12s ease, background .12s ease;
}
#mmExpand svg { width: 9px; height: 9px; display: block; }
#mmExpand:hover { opacity: 1; background: rgba(12, 30, 48, .92); }
#mmExpand:focus-visible { outline: 2px solid var(--hud-line-hi); outline-offset: 2px; opacity: 1; }
/* Open, the icon means "put it back", so it flips to the inward arrows. */
body.mmBig #mmExpand svg { transform: rotate(180deg); }

/* ── expanded map ────────────────────────────────────────────────────────
   Its own overlay, anchored to the right. The DOCKED map is untouched and
   stays in its corner, so the button that opened this is still there to
   close it. Sized in JS to fit the viewport — a phone in portrait has very
   little width to give, and guessing here is what put it off the screen. */
#minimapBigWrap {
  position: fixed; right: 8px; top: 50%; transform: translateY(-50%);
  z-index: 60; padding: 5px;
  border: 1px solid var(--hud-line); border-top-color: var(--hud-line-hi);
  border-radius: 8px;
  background: linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 12px var(--hud-glow);
  pointer-events: auto;
}
#minimapBigWrap[hidden] { display: none !important; }
#minimapBig { display: block; border-radius: 5px; background: #02050a; cursor: pointer; }
/* Its own way out, for when the expanded map covers the docked one. */
#mmShrink {
  position: absolute; top: 7px; right: 7px; z-index: 2;
  width: 15px; height: 15px; padding: 0;
  display: grid; place-items: center;
  border: 1px solid var(--hud-line); border-radius: 3px;
  background: rgba(4, 10, 18, .82);
  color: var(--hud-line-hi);
  cursor: pointer; opacity: .8;
}
#mmShrink svg { width: 9px; height: 9px; display: block; }
#mmShrink:focus-visible { outline: 2px solid var(--hud-line-hi); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { #mmExpand { transition: none; } }

/* ─────────────────────────── SHIP CARD ───────────────────────────
   StarCraft layout — NO bars:

     ┌──────────┬──────────────────────────────┐
     │ portrait │        Battlecruiser         │
     │          │  Move Speed: 55 (Normal)     │
     │ 300/300  │  Kills: 12 (240◆)            │
     │ 1100/1100│  [▢][▢][▢][▢][▢][▢][▢]       │
     │ 200/200  │     ARMORED · CAPITAL        │
     └──────────┴──────────────────────────────┘                     */
#infoPanel {
  position: absolute; bottom: 8px; left: 226px; right: auto;
  width: max-content; max-width: calc(100vw - 570px);
  height: auto; box-sizing: border-box;
  display: flex; align-items: center; gap: 28px;
  padding: 16px 30px;
  border: 1px solid var(--hud-line); border-top-color: var(--hud-line-hi); border-radius: 8px;
  background:
    radial-gradient(130% 80% at 50% 0%, rgba(40, 90, 140, .18), transparent 55%),
    linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 12px var(--hud-glow),
              inset 0 1px 0 rgba(140, 210, 250, .18);

}
body:not(.hasSelection) #infoPanel { display: none; }

/* ── left column: portrait, then the three vitals numbers ── */
#infoLeft {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  flex: 0 0 auto;
}
#portrait {
  position: relative; width: 76px; height: 76px; flex: 0 0 auto;
  border: 1px solid var(--hud-line); border-radius: 6px;
  background: radial-gradient(circle at 50% 32%, rgba(28, 56, 88, .8), rgba(3, 7, 13, .97) 74%);
  box-shadow: inset 0 0 18px rgba(0, 0, 0, .9);
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
#portrait.hostile { border-color: var(--hud-red); }
#portraitArt {
  width: 64px; height: 64px;
  background-size: contain; background-position: center; background-repeat: no-repeat;
  transition: filter .3s ease;
}
/* Push uploaded art onto a known hue first (grayscale+sepia), then rotate to
   the band colour — that way the result does not depend on what colour the
   art happened to be. Healthy art is left completely untouched. */
/* Silhouette mode. The art becomes a mask and the band colour is the paint,
   so the result is the exact colour with none of the art's detail lost. */
#portraitArt.maskArt {
  background-color: var(--hpCol, #5aff8a);
  -webkit-mask-image: var(--art); mask-image: var(--art);
  -webkit-mask-size: contain;     mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center;  mask-position: center;
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--hpCol, #5aff8a) 55%, transparent));
  transition: background-color .3s ease;
}
#portraitArt.hpWarn { filter: grayscale(1) sepia(1) saturate(7) hue-rotate(3deg) brightness(1.06); }
#portraitArt.hpCrit { filter: grayscale(1) sepia(1) saturate(9) hue-rotate(-38deg) brightness(1.04); }
/* The frame reads the same bands, so damage is legible even on art that
   resists tinting. */
#portrait.hpWarn { border-color: #ffd24a !important; box-shadow: 0 0 12px rgba(255, 210, 74, .35); }
#portrait.hpCrit {
  border-color: #ff5a4a !important; box-shadow: 0 0 14px rgba(255, 90, 74, .45);
  animation: hpCritPulse 1.1s ease-in-out infinite;
}
@keyframes hpCritPulse {
  0%, 100% { box-shadow: 0 0 10px rgba(255, 90, 74, .35); }
  50%      { box-shadow: 0 0 20px rgba(255, 90, 74, .75); }
}
#portrait[data-tier]:not([data-tier=''])::after {
  content: attr(data-tier);
  position: absolute; top: 0; left: 0; width: 24px; height: 24px;
  background: linear-gradient(160deg, var(--hud-gold), #9a6d1c);
  clip-path: polygon(0 0, 100% 0, 0 100%);
  color: #141005; font-size: 11px; font-weight: 800;
  padding: 2px 0 0 4px; box-sizing: border-box;
}
#portraitVitals {
  display: flex !important; flex-direction: column; align-items: center;
  line-height: 1.45; font-weight: 800; letter-spacing: .3px;
}
#portraitVitals .vSh { font-size: 14px; color: #6fc9ff; }
#portraitVitals .vHp { font-size: 14px; color: #5df08e; }
#portraitVitals .vEn { font-size: 13px; color: #c9a0ff; }

/* bars are not part of the StarCraft card */
#unitBars { display: none !important; }
#unitSub { display: none; }

/* ── right column: name, stat lines, tiles, tags ── */
#infoMain {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  flex: 0 0 auto;
  /* WAS min-width: 380px, and that was the empty space in the unit card.
     The panel is width:max-content, so it can never shrink below whatever its
     inner column insists on -- 380px held the card open past its own text and
     the surplus showed as blank board on the right.

     Content width instead. Numbers are tabular (below) so a speed or a kill
     count changing digits does not make the card breathe. */
  min-width: 0;
  gap: 6px;
}
#unitName {
  font-size: 21px; font-weight: 800; letter-spacing: 1.4px; color: #57c9f7;
  padding-top: 2px;
  text-shadow: 0 0 14px rgba(70, 190, 250, .45), 0 1px 2px #000;
  white-space: nowrap; text-align: center; margin: 0;
}
#unitName .tag, #unitName .youTag {
  font-size: 7px; font-weight: 800; letter-spacing: 1.2px;
  padding: 2px 7px; margin-left: 6px; border-radius: 9px; vertical-align: 3px;
}
#unitName .tag { border: 1px solid currentColor; }
#unitName .youTag { background: linear-gradient(90deg, #3ba7dc, #1e6a96); color: #eaf6ff; }

#unitStats { display: flex; flex-direction: column; align-items: center; width: 100%; }
#unitStats .scCard { display: flex; flex-direction: column; align-items: center; gap: 5px; width: 100%; }
#unitStats .scLine {
  font-variant-numeric: tabular-nums;
  font-size: 13.5px; line-height: 1.45; letter-spacing: .2px; color: #cfe4f4; white-space: nowrap;
}
#unitStats .scLine b { color: #fff; font-weight: 700; }
#unitStats .scLine em { font-style: normal; color: #8fb2cc; }
.scTier { color: #6fe08a !important; }
#unitStats .scIcons {
  display: flex; flex-wrap: nowrap; gap: 7px; justify-content: center; margin-top: 5px;
}
#unitStats .scIco { display: none; }
#unitStats .scTags {
  font-size: 11px; font-weight: 700; letter-spacing: 1.4px;
  color: #57c9f7; text-align: center; margin-top: 7px; padding-bottom: 2px;
}

.upgTile {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; border-radius: 5px;
  border: 1px solid var(--hud-line); border-top-color: var(--hud-line-hi);
  background:
    radial-gradient(90% 60% at 50% 0%, rgba(60, 140, 210, .25), transparent 60%),
    linear-gradient(180deg, #16283c, #0a1421);
  box-shadow: inset 0 1px 0 rgba(150, 215, 255, .22), 0 2px 5px rgba(0, 0, 0, .5);
  cursor: help; flex: 0 0 auto;
}
.upgTile.tipOpen { border-color: var(--hud-line-hi); }
.upgTile .ico { width: 24px; height: 24px; }
.upgTile > b {
  position: absolute; right: 3px; bottom: 1px;
  font-size: 12px; font-weight: 800; color: #f2f9ff;
  text-shadow: 0 0 4px #000, 0 1px 2px #000;
}

/* SC2 tooltip: cyan title, colour-coded values */
.tip {
  position: absolute; bottom: calc(100% + 10px); left: 50%; transform: translateX(-50%);
  display: flex; flex-direction: column; gap: 3px;
  min-width: 206px; padding: 11px 14px 12px;
  text-align: left; white-space: nowrap;
  background: linear-gradient(180deg, rgba(14, 34, 58, .98), rgba(6, 16, 30, .99));
  border: 1px solid #3ba7dc; border-radius: 5px;
  box-shadow: 0 8px 26px rgba(0, 0, 0, .8), 0 0 18px rgba(59, 167, 220, .3);
  opacity: 0; visibility: hidden; transition: opacity .12s;
  pointer-events: none; z-index: 60;
}
/* Hover is DESKTOP ONLY. A touch WebView leaves :hover latched on the last
   thing tapped, and the canvas calls preventDefault() on touchstart so the
   browser never gets the signal to clear it — the tooltip stayed visible no
   matter where you tapped next, whatever the .tipOpen class said. On touch,
   .tipOpen is the only thing that shows a tip. */
body:not(.touch) .upgTile:hover .tip, .upgTile.tipOpen .tip { opacity: 1; visibility: visible; }
body:not(.touch) .upgTile:hover { border-color: var(--hud-line-hi); }

/* Refit tiles lost their tooltips in the redesign: the hover rule above still
   said .upgTile while the tiles became .cmd.upg, so the CSS sat orphaned and
   nothing ever showed. Same treatment as ability tiles. */
.cmd.upg { position: relative; overflow: visible; }
.cmd.upg .tip {
  left: auto; right: 0; bottom: calc(100% + 8px); top: auto;
  transform: none; text-align: left; white-space: normal; min-width: 210px;
}
body:not(.touch) .cmd.upg:hover .tip, .cmd.upg.tipOpen .tip { opacity: 1; visibility: visible; }

/* Ability tiles get the same explain-yourself tooltip as the refit icons. They
   sit in the bottom-right command card, so the tip opens up and to the LEFT to
   stay on screen. */
.cmd.ability { position: relative; overflow: visible; }
.cmd.ability .tip {
  left: auto; right: 0; bottom: calc(100% + 8px); top: auto;
  transform: none; text-align: left; white-space: normal; min-width: 210px;
}
body:not(.touch) .cmd.ability:hover .tip, .cmd.ability.tipOpen .tip { opacity: 1; visibility: visible; }
@media (hover: hover) and (pointer: fine) {
  .upgTile:hover .tip,
  .cmd.upg:hover .tip,
  .cmd.ability:hover .tip { opacity: 1; visibility: visible; }
}
.tip em { font-style: normal; font-size: 13px; font-weight: 800; color: #57c9f7; text-shadow: 0 0 10px rgba(70, 190, 250, .45); margin-bottom: 5px; }
.tipRow { font-size: 11px; font-weight: 600; color: #b9d3e8; }
.tipRow b { font-weight: 700; color: #fff; }
.tipRow b.v-att { color: #ff9a6a; } .tipRow b.v-hp { color: #5df08e; }
.tipRow b.v-sh { color: #6fc9ff; } .tipRow b.v-en { color: #c9a0ff; }
.tipRow b.v-spd { color: #ffd069; } .tipRow b.v-rng { color: #a9b6ff; }
.tipNext { margin-top: 6px; padding-top: 6px; font-size: 10px; color: #86a8c6; border-top: 1px solid rgba(59, 167, 220, .25); }
/* The price, first and unmissable: mineral-gold, its own row under the
   title. Buried at the bottom of the tip it was the last thing read on a
   panel whose whole question is "can I afford this". */
.tipCost { display: block; margin: 3px 0 5px; font-size: 12px; font-weight: 700;
           color: #ffd257; letter-spacing: .4px; }
.tipCost.maxed { color: #86a8c6; font-weight: 600; }

/* ─────────────────────────── REFIT (docked) ─────────────────────────── */
#upgPanel {
  position: absolute; right: 8px; bottom: 122px; padding: 8px 10px 10px;
  border: 1px solid var(--hud-line); border-top-color: var(--hud-line-hi); border-radius: 8px;
  background: linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 12px var(--hud-glow);
  display: none;
}
body.atBase #upgPanel { display: block; }
.upgHead {
  font-size: 8.5px; font-weight: 800; letter-spacing: 2.6px; text-align: center;
  color: #57e0a0; text-shadow: 0 0 8px rgba(60, 220, 140, .4); margin-bottom: 7px;
}
/* BOTH CONSOLES ARE THE SAME FIXED SIZE.

   Same height was not enough: the refit console and the abilities console swap
   into the same corner, so if their WIDTHS differ the panels around them move
   every time you dock. Sizing either one from its contents guarantees that,
   because the two never hold the same number of tiles -- and it varies by hull
   on top of that.

   So both are a fixed grid, six by two, and short ones simply leave cells
   empty. A console that is always the same size is what makes the layout
   stationary; nothing else does. */
#upgradeGrid { display: grid; grid-template-columns: repeat(6, 62px); gap: 5px; }

/* command tiles (refit + abilities share .cmd) */
.cmd {
  position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center;
  height: 58px; padding: 4px 2px; border-radius: 6px;
  border: 1px solid var(--hud-line); border-top-color: var(--hud-line-hi);
  background:
    radial-gradient(90% 60% at 50% 0%, rgba(60, 140, 210, .22), transparent 60%),
    linear-gradient(180deg, #122133, #081120);
  box-shadow: inset 0 1px 0 rgba(150, 215, 255, .2), 0 2px 5px rgba(0, 0, 0, .5);
  color: #cfe6fa; cursor: pointer; font-family: inherit;
}
.cmd:hover:not(:disabled) { border-color: #6fe0a0; box-shadow: inset 0 0 8px rgba(80, 220, 140, .18), 0 0 10px rgba(60, 220, 140, .25); }
.cmd:disabled { opacity: .38; cursor: default; }
.cmd > b {
  /* the SC2 read: a BIG hotkey in the corner you can see without leaning in */
  position: absolute; top: 1px; left: 4px;
  font-size: 14px; font-weight: 800; color: #dfeeff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .95), 0 0 6px rgba(0, 0, 0, .7);
}
.cmd .ico { width: 17px; height: 17px; }
.cmd .cName { font-size: 8px; font-weight: 700; letter-spacing: .4px; margin-top: 2px; white-space: nowrap; max-width: 58px; overflow: hidden; text-overflow: ellipsis; }
.cmd .cCost { font-size: 8.5px; font-weight: 800; color: #ffd76e; }
/* refit tiles: the corner number is the owned level, pinned bottom-right */
.cmd.upg .cCost {
  position: absolute; right: 5px; bottom: 3px;
  font-size: 13px; color: #ffffff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .95);
}
.cmd.upg.maxed .cCost { color: #ffd76e; }
.cmd .cLvl { font-size: 7.5px; color: #8fb4d4; letter-spacing: 1px; }

/* ─────────────────────────── ABILITIES ─────────────────────────── */
#cmdPanel { position: absolute; right: 8px; bottom: 8px; }
#abilityToggle {
  display: none;
  padding: 12px 20px; border-radius: 9px;
  border: 1px solid var(--hud-gold);
  background: linear-gradient(180deg, rgba(50, 38, 12, .95), rgba(24, 18, 6, .97));
  color: #ffe2a0; font-size: 12px; font-weight: 800; letter-spacing: 2px;
  box-shadow: 0 0 0 1px rgba(14, 10, 2, .9), 0 0 14px rgba(234, 181, 67, .3);
  cursor: pointer; font-family: inherit;
}
#commandCard { display: flex; gap: 8px; }
#commandCard .cmd { width: auto; height: 62px; }
#commandCard .cmd .cName { max-width: 68px; }
.cmd.ability:not(:disabled) {
  border-color: var(--hud-gold);
  background:
    radial-gradient(90% 60% at 50% 20%, rgba(240, 181, 69, .2), transparent 65%),
    linear-gradient(180deg, rgba(30, 24, 9, .96), rgba(12, 9, 3, .97));
  box-shadow: inset 0 1px 0 rgba(255, 226, 160, .3), 0 0 12px rgba(234, 181, 67, .35);
}
.cmd.ability:not(:disabled) .cName { color: #ffe2a0; }
.cmd.ability.research .cCost { color: #7fffc0; }
.cmd.ability.research::after {
  content: 'RESEARCH'; font-size: 6px; letter-spacing: 1.6px; color: #57e0a0; margin-top: 1px;
}
.cmd.ability.targeting { border-color: #fff; box-shadow: 0 0 14px rgba(255, 255, 255, .4); }

/* HUD visibility states: full → hidden → minimal (map + own vitals) */
/* THE REFIT GRID IS NOT FURNITURE EITHER.
   These three were meant to read the same way: hide the console when the HUD
   is minimised, EXCEPT the panel you have deliberately opened. The first two
   carry :not(.cmdAbilities) so the abilities drawer still comes up. #upgPanel
   did not -- so minimised, the upgrades were hidden with !important on every
   tab, and there was no tab to switch to that brought them back. Docked and
   minimised, "buy an upgrade" was simply not reachable.
   Now it mirrors #cmdPanel exactly: hidden while the OTHER tab is showing. */
body.hudCollapsed:not(.cmdAbilities) #infoPanel,
body.hudCollapsed:not(.cmdAbilities) #cmdPanel, body.hudCollapsed.cmdAbilities #upgPanel,
/* The comms stack too. It sits directly above the minimap, so leaving it
   behind put a PING button floating in empty space over the battlefield --
   which is the opposite of what "hide the HUD" is asked for. */
body.hudCollapsed #chatWrap,
body.hudCollapsed #netBadge { display: none !important; }
/* ...except the top bar. Minerals and the match clock are what you are
   watching the whole game — how close the next upgrade is, how long you have
   been in. Hiding the console is about clearing the MAP, and those two are a
   single centred strip that covers nothing. */
/* ...except the abilities button. Hiding the console should clear the view,
   not disarm you — a desktop still has the hotkeys, a phone has nothing else
   to press.

   The BUTTON, not the grid. The grid is three rows of tiles, which is most of
   the screen you just asked to have back; a hidden HUD that replaces the
   console with a slightly smaller console has not hidden anything. One small
   button in the corner, and the row comes up on a tap.

   It sits INSIDE #cmdPanel, so the panel cannot be display:none or the button
   goes with it however the button itself is styled. The panel stays, stripped
   of its frame and its contents, as a mount for the one control. */
body.touch.hudCollapsed #cmdPanel {
  display: block !important;
  background: none !important; border: none !important; box-shadow: none !important;
  pointer-events: none;
}
/* ANCHORED TO THE SCREEN, NOT TO THE BAND. #cmdPanel is `position: absolute;
   right: 8px; bottom: 8px`, which is right while #console is the positioned
   band along the bottom -- but that band rule is `:not(.hudCollapsed)`, so the
   moment the HUD minimised, #console lost its positioning and the panel's
   right/bottom resolved against an unpositioned box sitting up beside the
   trackpad. That is the undocked abilities row appearing bottom-LEFT when
   minimised. Fixed to the corner it belongs in, the same way the drawer
   button already is. */
body.hudCollapsed #cmdPanel {
  position: fixed;
  right: 14px; left: auto; top: auto;
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  z-index: 40;
}
body.touch.hudCollapsed #cmdPanel > #commandCard { display: none; }
/* Desktop gets the drawer button too when the HUD is hidden.
   It normally has no button at all -- the whole card is on screen, so there is
   nothing to open. Minimised there is no card either, which left a desktop
   player with their abilities simply gone and no way back short of restoring
   the HUD. The rules below were all .touch-only for that reason; the hidden
   HUD is the one case where a desktop wants exactly what a phone wants. */
body.hudCollapsed #abilityToggle,
body.touch.hudCollapsed #abilityToggle {
  display: flex !important;
  /* fixed, so the #hud inset above does not apply -- it carries its own. */
  position: fixed;
  right: 14px;
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  z-index: 40;
  pointer-events: auto;
  /* The drawer button is full-width inside the console panel, which it is not
     in any more — fixed to the corner it has to size to its own label or it
     stretches the whole screen. */
  width: auto; padding: 10px 16px;
}
/* Tapping it brings the icon row back; it is never up on its own. */
body.touch.hudCollapsed.abilitiesOpen #cmdPanel { pointer-events: auto; }
body.hudCollapsed.abilitiesOpen #cmdPanel { pointer-events: auto; display: block !important; }
body.hudCollapsed:not(.abilitiesOpen) #cmdPanel {
  display: block !important;
  background: none !important; border: none !important; box-shadow: none !important;
  pointer-events: none;
}
body.hudCollapsed:not(.abilitiesOpen) #commandCard { display: none !important; }
body.hudCollapsed.abilitiesOpen #commandCard { display: grid !important; }
/* A minimised HUD shows ONE panel: the abilities card. An earlier attempt at
   "let them research while minimised" brought the refit console up beside it,
   which put two framed boxes in the same corner. Ability research is done from
   the ability tiles in the card, so nothing was gained by it. */

/* ════════════════════════════════════════════════════════════════════════
   MINIMISED HUD: the map STAYS.
   Hiding it outright gave up the one thing on screen you cannot reconstruct
   by looking at the battlefield -- where everyone else is. So it is kept and
   pushed back instead: no plate, no frame, three-quarter size and half
   transparent, so it reads as a glance rather than as a panel.
   Only the map surface takes input; the box around it must not, for the same
   reason it must not when the HUD is up.
   ════════════════════════════════════════════════════════════════════════ */
body.hudCollapsed #minimapPanel {
  display: block !important;
  background: none !important;
  border: none !important;
  border-image: none !important;
  box-shadow: none !important;
  padding: 0 !important;
  opacity: var(--mini-alpha, .6);
  /* No CSS shrink. The canvas is sized in JS (applyMoveMode), and scaling it
     down again here fought that -- raising the size setting grew the canvas
     and then this shaved 9% straight back off. */
  transform-origin: bottom left;
  pointer-events: none;
}
body.hudCollapsed #minimapPanel #minimap {
  pointer-events: auto;
  border-radius: 4px;
  /* Without the plate behind it the map needs its own faint edge, or it
     dissolves into the starfield and stops reading as a map at all. */
  box-shadow: inset 0 0 0 1px rgba(120, 190, 250, .22);
}
body.hudCollapsed #minimapPanel::before,
body.hudCollapsed #minimapPanel::after { display: none !important; }
body.touch.hudCollapsed.abilitiesOpen #cmdPanel > #commandCard { display: grid; }

/* ════════════════════════════════════════════════════════════
   TOUCH LAYOUT — its own numbers, not breakpoints.
   ════════════════════════════════════════════════════════════ */
body.touch #minerals { font-size: 12px; padding: 4px 16px; }
body.touch #minimapPanel { padding: 4px; }
body.touch #infoPanel { left: 150px; min-width: 0; max-width: 460px; padding: 7px 14px 6px; gap: 12px; }
body.touch.abilitiesOpen #infoPanel { right: 348px; }
body.touch #portrait { width: 58px; height: 58px; }
body.touch #portraitArt { width: 46px; height: 46px; }
body.touch #infoMain { width: 216px; }
body.touch #unitName { font-size: 11px; margin-bottom: 4px; }
body.touch .uBar { height: 9px; padding: 0 52px 0 16px; }
body.touch .uBar > b { width: 12px; height: 12px; }
body.touch .uBar::after { left: 16px; right: 52px; }
body.touch .uBar > i { left: 17px; max-width: calc(100% - 70px); }
body.touch .uBar > span { width: 48px; font-size: 9px; line-height: 12px; }
body.touch #unitStats .scLine { font-size: 8.5px; line-height: 1.6; }
body.touch #unitStats .scIcons { grid-template-columns: repeat(4, 30px); gap: 4px; }
body.touch .upgTile { width: 30px; height: 30px; }
body.touch .upgTile .ico { width: 16px; height: 16px; }
body.touch .upgTile > b { font-size: 8px; }
body.touch .tip { bottom: auto; top: calc(100% + 8px); }

/* touch abilities: collapsed behind the gold button */
/* #cmdPanel is z-index 20 and #upgPanel is 80 on touch, and both are
   positioned -- so each opens its own stacking context and the button, being
   INSIDE #cmdPanel, could not out-rank the refit grid no matter what z-index
   it carried. The panel has to win first, then the button within it. */
body.touch #abilityToggle { display: block; }
/* (Showing #commandCard on its own was not enough: about eight rules strip
    #cmdPanel back to a bare button while the drawer is shut, so the grid
    appeared inside a button-sized panel with no plate. The drawer is opened
    on undocking instead -- see setAbilitiesOpen in game.js -- which is one
    switch rather than a fight with all of them.) */
body.touch #commandCard { display: none; }
body.touch.abilitiesOpen #commandCard { display: flex; }
/* The button does NOT vanish when the drawer opens -- see the pinned-corner
   block at the end of this file. Hiding it left the closing tap with nothing
   to land on. */
body.touch #commandCard .cmd { width: auto; height: 56px; }
body.touch #upgPanel { bottom: 110px; }
body.touch #upgradeGrid, body.touch #commandCard { grid-template-columns: repeat(6, 58px); }
body.touch .cmd { height: 54px; }
body.touch .cmd > b { display: none; }

/* ════════════════════════════════════════════════════════════
   TOUCH CARD — two columns, wide enough that nothing is cut off.

     ┌─────────────┬──────────────────────────┐
     │ Battlecruiser│ Move Speed: 55 (Normal) │
     │  300 / 300   │ Kills: 13 (124◆)        │
     │ 1100 / 1100  │ ▢ ▢ ▢ ▢ ▢ ▢ ▢           │
     │  200 / 200   │                          │
     └─────────────┴──────────────────────────┘
   Children of #infoLeft / #infoMain / #unitStats are hoisted with
   display:contents so one grid can lay them all out.
   ════════════════════════════════════════════════════════════ */
body.touch #infoPanel {
  left: 150px; right: auto; width: max-content;
  max-width: calc(100vw - 176px);
  height: auto; padding: 9px 14px;
  display: grid;
  /* portrait · vitals · stats */
  grid-template-columns: max-content max-content max-content;
  align-items: center; justify-items: start;
  column-gap: 16px; row-gap: 1px;
}
body.touch #infoLeft,
body.touch #infoMain,
body.touch #unitStats,
body.touch #unitStats .scCard { display: contents; }

/* The wireframe DOES fit: the compact panel is laid out as a grid and simply
   never gave it a column. It gets one of its own on the left, with the text
   columns shifted right, so a phone shows the same hull silhouette a desktop
   does rather than an unexplained gap where it should be. */
body.touch #portrait {
  display: flex;                       /* NOT block — flex is what centres the art */
  align-items: center; justify-content: center;
  grid-column: 1; grid-row: 1 / span 3;
  width: 54px; height: 54px; align-self: center; justify-self: center;
}
body.touch #portraitArt { width: 44px; height: 44px; }
body.touch #unitStats .scTags { display: none; }     /* no ARMORED · CAPITAL */

/* left column */
body.touch #unitName {
  grid-column: 2; grid-row: 1;
  font-size: 13px; padding: 0 0 3px; margin: 0; justify-self: center;
}
body.touch #unitName .tag, body.touch #unitName .youTag {
  font-size: 6.5px; padding: 1px 5px; margin-left: 4px; vertical-align: 2px;
}
body.touch #portraitVitals {
  grid-column: 2; grid-row: 2 / span 2;
  align-items: center; justify-self: center; line-height: 1.35;
}
body.touch #portraitVitals .vSh,
body.touch #portraitVitals .vHp { font-size: 13.2px; }
body.touch #portraitVitals .vEn { font-size: 12.1px; }

/* right column */
body.touch #unitStats .scLine { font-size: 10.5px; line-height: 1.4; grid-column: 3; }
body.touch #unitStats .scLine:nth-of-type(1) { grid-row: 1; }
body.touch #unitStats .scLine:nth-of-type(2) { grid-row: 2; }
body.touch #unitStats .scIcons {
  grid-column: 3; grid-row: 3;
  display: flex; gap: 5px; margin-top: 4px;
}
body.touch .upgTile { width: 32px; height: 32px; }
body.touch .upgTile .ico { width: 17px; height: 17px; }
body.touch .upgTile > b { font-size: 9px; }

/* tooltip opens UPWARD on touch — the card sits at the bottom of the screen */
body.touch .tip { bottom: calc(100% + 8px); top: auto; min-width: 186px; }

/* refit becomes a centred popup over everything while docked */
body.touch #upgPanel {
  position: fixed; left: 50%; top: 46%; right: auto; bottom: auto;
  transform: translate(-50%, -50%);
  z-index: 80; padding: 14px 18px 16px;
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 24px rgba(56, 178, 230, .35),
              0 18px 60px rgba(0, 0, 0, .8);
}
body.touch #upgPanel .upgHead { font-size: 9.5px; margin-bottom: 10px; }
body.touch #upgradeGrid, body.touch #commandCard { grid-template-columns: repeat(6, 66px); gap: 7px; }
body.touch #upgPanel .cmd { height: 60px; }

/* ════════════════════════════════════════════════════════════
   COMMAND CARD — one panel, bottom-right, two pages.
     page 1 (docked)  : upgrades + [ABILITIES] tile
     page 2           : abilities + [BACK] tile (back only when docked)
   Nothing ever sits in the middle of the screen.
   ════════════════════════════════════════════════════════════ */
.aico-back { background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23a9c6de" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 5l-7 7 7 7"/></svg>'); }

#upgPanel, #cmdPanel {
  position: absolute; right: 8px; bottom: 8px; left: auto; top: auto;
  transform: none; z-index: 20;
  padding: 9px 11px 11px;
  border: 1px solid var(--hud-line); border-top-color: var(--hud-line-hi);
  border-radius: 8px;
  background:
    radial-gradient(130% 80% at 50% 0%, rgba(40, 90, 140, .18), transparent 55%),
    linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 12px var(--hud-glow),
              inset 0 1px 0 rgba(140, 210, 250, .18);

}

/* which page shows */
body.atBase:not(.cmdAbilities) #upgPanel { display: block; }
body.atBase:not(.cmdAbilities) #cmdPanel { display: none; }
body.atBase.cmdAbilities #upgPanel { display: none; }
body.atBase.cmdAbilities #cmdPanel { display: block; }
body:not(.atBase) #upgPanel { display: none; }
body:not(.atBase) #cmdPanel { display: block; }
body:not(.hasSelection) #upgPanel, body:not(.hasSelection) #cmdPanel { display: none; }

/* the old gold pill is replaced by the in-grid nav tile */

/* both pages use the same 4-wide grid so tiles never jump around */
#upgradeGrid, #commandCard {
  display: grid !important;
  grid-template-columns: repeat(4, 68px);
  gap: 6px;
}
#upgradeGrid, #commandCard { grid-template-columns: repeat(6, 68px); }
#upgradeGrid .cmd, #commandCard .cmd { width: auto; height: 62px; }
/* Abilities run one tile per slot plus nav, so they get 10% less each to buy
   back room on narrow phones. Refit keeps full size (fewer tiles since the
   Range and Reactor tracks went away). */
#commandCard { grid-template-columns: repeat(4, 61px); }
#commandCard .cmd { height: 56px; }

/* back tile is only meaningful while docked (otherwise there is no page 1) */
#toUpgrades { display: none; }
body.atBase #toUpgrades { display: flex; }

/* ══════════════════════════════════════════════════════════════════════════
   MOBILE HUD CORRECTIONS
   ══════════════════════════════════════════════════════════════════════════ */

/* The minimap PANEL is 161px around a 127px map -- a 17px ring of frame and
   padding. It carried pointer-events:auto with no handler of its own, so a
   drag starting anywhere on that ring was swallowed and the camera did not
   move. That is a dead border all the way around the map, in the corner a
   left thumb naturally rests. Only the map surface should be interactive. */
#minimapPanel { pointer-events: none; }
#minimap { pointer-events: auto; }

/* Fullscreen is a desktop affordance. On a phone the app is already
   fullscreen and the button does nothing worth a slot in the top bar. */
/* Hidden on touch because a phone browser has no fullscreen API worth the
   button -- but a tablet does, and that is where the browser chrome costs the
   most screen. Shown wherever the API actually exists, which the boot code
   feature-detects into .fsCapable, and never in the installed app where there
   is no chrome to escape. */
body.touch #fsToggle { display: none !important; }
body.touch.fsCapable:not(.nativeApp) #fsToggle { display: block !important; }
/* Rotating can make a mobile browser drop fullscreen. In that state the way
   back must be a labelled recovery control, not a tiny anonymous glyph that
   can disappear among the other HUD buttons. */
body.touch.fsCapable:not(.nativeApp).fullscreenDropped #fsToggle {
  display: flex !important;
  position: fixed !important;
  z-index: 59 !important;
  top: calc(8px + env(safe-area-inset-top, 0px)) !important;
  right: calc(8px + env(safe-area-inset-right, 0px)) !important;
  width: auto !important;
  align-items: center;
}
body.touch.fsCapable:not(.nativeApp).fullscreenDropped #fsToggle::after {
  content: ' FULLSCREEN';
  margin-left: 6px;
  font-size: 9px;
  letter-spacing: 1.2px;
}

/* The minimised abilities drawer is a BUTTON, not a panel.
   The FRAME is what had to go -- a plate and a border wrapped around a single
   control. The label stays: hiding it left the button free to wrap its bolt
   and its word onto two lines, which looked far worse than the frame ever
   did. nowrap keeps "⚡ ABILITIES" on one line, the way it always read. */
body.touch:not(.abilitiesOpen):not(.cmdAbilities) #cmdPanel {
  /* Sit on the same baseline as the ship card. The panel's own 6px inset left
     the button 2px lower than the card beside it -- six device pixels on a 3x
     screen, which is exactly enough to read as crooked. */
  bottom: 8px !important;
  right: 8px !important;
  background: none !important;
  border: none !important;
  border-image: none !important;
  box-shadow: none !important;
  padding: 0 !important;
  /* right-align the lone button to the panel edge, or it floats off-centre in
     a box that no longer has visible sides */
  display: flex; justify-content: flex-end; width: auto;
}
/* #cmdPanel::before prints its own 'ABILITIES' heading above the contents.
   Right when the drawer is OPEN and full of tiles. Collapsed, the only thing
   under it is a button that already says ABILITIES -- so the word appeared
   twice, the second time as a stray grey caption. */
body.touch:not(.abilitiesOpen):not(.cmdAbilities) #cmdPanel::before { display: none !important; }
body.touch:not(.abilitiesOpen) #abilityToggle {
  white-space: nowrap;
  width: auto; min-width: 0;
  padding: 9px 15px;
}

/* nav tiles read as chrome, not as purchases */
.navTile {
  border-color: rgba(234, 181, 67, .6) !important;
  background:
    radial-gradient(90% 60% at 50% 0%, rgba(240, 181, 69, .18), transparent 60%),
    linear-gradient(180deg, rgba(26, 20, 8, .96), rgba(10, 7, 3, .97)) !important;
}
.navTile .cName { color: #ffe2a0 !important; font-size: 8.5px; letter-spacing: 1.2px; }
.navTile:hover { border-color: var(--hud-gold) !important; box-shadow: 0 0 12px rgba(234, 181, 67, .35) !important; }
#toUpgrades { border-color: var(--hud-line) !important; background: linear-gradient(180deg, #122133, #081120) !important; }
#toUpgrades .cName { color: #cfe6fa !important; }

.upgHead {
  font-size: 8.5px; font-weight: 800; letter-spacing: 2.4px; text-align: center;
  color: #57e0a0; text-shadow: 0 0 8px rgba(60, 220, 140, .4); margin-bottom: 8px;
}
#cmdPanel::before {
  content: 'ABILITIES'; display: block;
  font-size: 8.5px; font-weight: 800; letter-spacing: 2.4px; text-align: center;
  color: #8fc0e0; margin-bottom: 8px;
}

/* touch: same card, finger-sized, still bottom-right (never centred) */
body.touch #upgPanel, body.touch #cmdPanel {
  position: absolute; right: 6px; bottom: 6px; left: auto; top: auto;
  transform: none; padding: 6px 8px 8px;
}
body.touch #upgradeGrid, body.touch #commandCard { grid-template-columns: repeat(6, 53px); gap: 4px; }
body.touch #upgPanel .cmd, body.touch #cmdPanel .cmd { height: 48px; }
body.touch #commandCard { grid-template-columns: repeat(4, 48px); }
body.touch #cmdPanel .cmd { height: 43px; }
body.touch .cmd > b { display: none; }
/* the card owns the bottom-right, so the ship strip stops short of it */
body.touch #infoPanel { max-width: calc(100vw - 448px); }

/* ════════════════════════════════════════════════════════════
   Kill feed · Leaderboard · Cast bar
   ════════════════════════════════════════════════════════════ */
#killFeed {
  position: absolute; top: 54px; right: 12px;
  display: flex; flex-direction: column; align-items: flex-end; gap: 4px;
  pointer-events: none; z-index: 30;
}
.kfRow {
  padding: 5px 11px; border-radius: 5px;
  background: linear-gradient(180deg, rgba(8, 16, 27, .92), rgba(3, 8, 15, .95));
  border: 1px solid var(--hud-line);
  font-size: 13.75px; font-weight: 600; letter-spacing: .3px; color: #a9c6de;
  white-space: nowrap;
  opacity: 1; transition: opacity .8s;
}
.kfRow.kfOut { opacity: 0; }
.kfRow b { font-weight: 800; }
.kfMine { color: #6fc9ff; }
.kfTheirs { color: #ff7a5c; }

#leaderboard {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  min-width: 300px; padding: 12px 16px 14px; z-index: 70;
  border: 1px solid var(--hud-line); border-top-color: var(--hud-line-hi);
  border-radius: 8px;
  background: linear-gradient(180deg, rgba(8, 16, 27, .97), rgba(3, 8, 15, .98));
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 26px var(--hud-glow), 0 18px 60px rgba(0, 0, 0, .8);
}
#leaderboard.hidden { display: none; }
.lbHead {
  display: flex; justify-content: space-between;
  padding-bottom: 7px; margin-bottom: 7px;
  border-bottom: 1px solid rgba(56, 178, 230, .3);
  font-size: 12px; font-weight: 800; letter-spacing: 1.4px; color: #cfe6fa;
}
.lbRow {
  display: grid; grid-template-columns: 1fr auto 34px;
  align-items: center; gap: 10px;
  padding: 4px 2px; font-size: 12.5px; font-weight: 700;
}
.lbRow.lbMe { background: rgba(56, 178, 230, .12); border-radius: 4px; }
.lbName.lbBlue { color: #6fc9ff; }
.lbName.lbRed  { color: #ff7a5c; }
.lbScore { color: #eaf4ff; font-weight: 800; }
.lbKills { color: #ffd069; font-weight: 800; text-align: right; }
.lbRow.lbDead { opacity: .38; }
.lbRow.lbDead .lbName { text-decoration: line-through; }

#castBar {
  position: absolute; left: 50%; bottom: 132px; transform: translateX(-50%);
  width: 260px; padding: 7px 10px 8px; z-index: 40; pointer-events: none;
  border: 1px solid rgba(255, 201, 74, .6); border-radius: 6px;
  background: linear-gradient(180deg, rgba(26, 20, 8, .95), rgba(10, 7, 3, .97));
  box-shadow: 0 0 16px rgba(255, 201, 74, .3);
}
#castBar.hidden { display: none; }
.castLabel {
  font-size: 9px; font-weight: 800; letter-spacing: 2px; color: #ffd98f;
  text-align: center; margin-bottom: 5px;
}
.castTrack { height: 8px; border-radius: 2px; background: rgba(8, 20, 32, .95); overflow: hidden; }
.castTrack > i {
  display: block; height: 100%; width: 0;
  background: linear-gradient(90deg, #c08a1e, #ffd069);
  box-shadow: 0 0 8px rgba(255, 208, 105, .7);
}
body.touch #killFeed { top: 50px; font-size: 12.5px; }
body.touch .kfRow { font-size: 12.5px; padding: 5px 11px; }
body.touch #castBar { bottom: 112px; width: 220px; }

/* ── touch card ─────────────────────────────────────────────────────────────
   Left-justified. Width is set at runtime (layoutTouchCard) so it always stops
   short of the command card. Icons on TWO rows, kept short by tight leading.

     Battlecruiser        Move Speed: 55 (Normal)
     300 / 300            Kills: 0 (0◆)
     1100 / 1100          ▢ ▢ ▢ ▢
     200 / 200            ▢ ▢ ▢                                              */
body.touch #infoPanel {
  padding: 5px 13px;
  column-gap: 16px; row-gap: 0;
  align-items: center;
  justify-content: flex-start;
}
body.touch #infoLeft { align-items: flex-start; }
body.touch #unitName {
  font-size: 12.7px; margin: 0 0 1px; padding: 0;
  text-align: left; justify-self: start;
}
body.touch #portraitVitals {
  align-items: flex-start;          /* left-justified, not centred */
  line-height: 1.08; margin: 0;
}
body.touch #portraitVitals .vSh,
body.touch #portraitVitals .vHp { font-size: 12px; }
body.touch #portraitVitals .vEn { font-size: 11px; }

body.touch #infoMain { align-items: flex-start; }
body.touch #unitStats { align-items: flex-start; }
body.touch #unitStats .scCard { align-items: flex-start; }
body.touch #unitStats .scLine { font-size: 10.5px; line-height: 1.15; text-align: left; }
/* The portrait column is defined further up; these later overrides restyle
   the same panel and must not drop the column assignments with them. */
body.touch #portrait { grid-column: 1; grid-row: 1 / span 3; }
body.touch #unitName, body.touch #portraitVitals { grid-column: 2; }
body.touch #unitStats .scLine, body.touch #unitStats .scIcons { grid-column: 3; }

/* two rows of four */
body.touch #unitStats .scIcons {
  display: flex; flex-wrap: nowrap;
  gap: 3px; margin-top: 2px; justify-content: flex-start;
}
/* ...and the card's weapon tiles come down with them, so the card narrows by
   about 20px without losing a single tile off the row. */
body.touch .upgTile { width: 26px; height: 26px; }
body.touch .upgTile .ico { width: 13px; height: 13px; }
body.touch .upgTile > b { font-size: 9px; right: 2px; }

/* leaderboard button, beside MY SHIP (no Tab key on a phone) */
#btnLb { display: none; }
body.touch #btnLb {
  display: block; position: absolute; top: 8px; left: 95px;
  min-width: 42px; min-height: 34px; padding: 6px 10px;
  border: 1px solid var(--hud-line); border-radius: 8px;
  background: linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
  color: #cfe8fa; font-size: 16px; line-height: 1; text-align: center;
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 10px var(--hud-glow);
  cursor: pointer; z-index: 25;
}
body.touch #btnLb:active { border-color: var(--hud-line-hi); }
body.touch #btnCancel { left: 191px; }

/* ── command tiles: no level tag, names that fit ────────────────────────────
   "L0" was the upgrade level and just added noise. Ability names were also
   overflowing their tiles and colliding, so they now wrap to two tight lines
   and clip cleanly instead. */
.cmd .cLvl { display: none !important; }

.cmd .cName {
  white-space: normal;
  line-height: 1.08;
  max-width: 100%;
  overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
#commandCard .cmd .cName { font-size: 7.5px; letter-spacing: 0; }
#upgradeGrid .cmd .cName { font-size: 8px; letter-spacing: .2px; }

/* the green cost already says "not researched yet" — drop the extra label */
.cmd.ability.research::after { display: none !important; }

/* ── tile layout: name across the top, price pinned bottom-right ─────────────
   The icon owns the middle of the tile, so the two labels are lifted out of the
   flex flow and parked in corners instead of stacking under it. */
.cmd .cName {
  position: absolute; top: 3px; left: 3px; right: 3px;
  text-align: center; margin-top: 0;
}
.cmd .cCost {
  position: absolute; bottom: 2px; right: 4px;
  text-align: right; line-height: 1;
}
/* the icon no longer has to share vertical space with the labels */
.cmd .ico, .cmd .aico { margin-top: 4px; }

/* PHONE-SIZED, not tablet-sized. These were 58px tiles at 52px tall, which
   made the refit console about 400px wide -- and with a 420px unit card and
   the trackpad's corner, that is the whole width of a handset with 35px left
   over. The character had nowhere to stand, so it was being hidden.

   48px tiles give the console back about 45px and the row somewhere to put
   the character. The names still fit on two lines at this size. */
body.touch #upgradeGrid, body.touch #commandCard {
  grid-template-columns: repeat(4, 48px); gap: 3px;
}
body.touch #upgPanel .cmd, body.touch #cmdPanel .cmd { height: 44px; padding: 2px 2px; }
body.touch #commandCard .cmd .aico, body.touch #upgradeGrid .cmd .ico { width: 15px; height: 15px; }
body.touch .cmd .cCost { font-size: 8px; }


/* ── card: no tag line, centred on screen ───────────────────────────────────
   ARMORED · CAPITAL removed and speed/kills on one line, so the card gets
   shorter on its own. Centring is done between the map and the command card
   so it sits in the real gap, not the middle of the window. */
#unitStats .scTags { display: none !important; }

@media (min-width: 901px) and (min-height: 621px) {
  /* HALF A RULE IS WORSE THAN NONE.
     This centres the card by pairing left:50% with a -50% shift. Written
     unscoped it also matched TABLETS, where `body.touch #infoPanel` sets
     left:150px and wins on specificity -- but nothing competed with the
     transform, so the card kept the 150px anchor AND took the shift, and
     slid half its own width off the left edge of an iPad. Both halves are
     now set together for each case. */
  /* CENTRED WITH AUTO MARGINS, not with left:50% and a -50% shift.

     Two reasons, both learned the hard way here.

     The percentage pair is TWO declarations that have to travel together, and
     every fix in this file has turned up another rule setting one without the
     other -- an anchor with the shift still applied drags the card off the
     left edge. Auto margins are a single declaration that cannot half-apply.

     And a percentage resolves against the CONTAINING BLOCK, which on iPadOS
     Safari is not the viewport: left:50% there computed to 211px, half of 422
     rather than half of 1133, putting the card off-screen from a rule that
     reads as obviously correct. Auto margins centre in whatever box actually
     contains the element, so they cannot disagree with it.

     Both selectors are spelled out so this matches the specificity of the
     `body.touch #infoPanel` rule it has to beat; a bare #infoPanel loses to
     it and the card stays anchored at 150px. */
  body.touch #infoPanel,
  body:not(.touch) #infoPanel {
    left: 0; right: 0; transform: none; margin-inline: auto; width: max-content;
  }
  /* ROOM FOR THE PORTRAIT. The card is centred between the map and the
     abilities; with a character in that gap it has to shift left by the width
     the character took, or the two overlap. Auto margins centre within what is
     LEFT of the container, so an asymmetric margin moves it -- no left or
     transform juggling, which is what has bitten this panel three times. */
  body.hasPortrait #infoPanel { margin-right: var(--portrait-gap, 250px); }
}

/* ════════════════════════════════════════════════════════════
   In-game ship selector — overlays the live match
   ════════════════════════════════════════════════════════════ */
#shipPicker {
  position: fixed; inset: 0; z-index: 90;
  display: flex; align-items: center; justify-content: center;
  background: rgba(2, 6, 12, .45);
  pointer-events: auto;
}
#shipPicker.hidden { display: none; }

.spPanel {
  width: min(880px, 94vw);
  border: 1px solid var(--hud-line); border-top-color: var(--hud-line-hi);
  border-radius: 10px;
  background:
    radial-gradient(120% 70% at 50% 0%, rgba(40, 90, 140, .22), transparent 55%),
    linear-gradient(180deg, rgba(8, 18, 30, .97), rgba(3, 8, 15, .98));
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 30px var(--hud-glow), 0 24px 70px rgba(0, 0, 0, .8);
  padding: 14px 18px 16px;
}

.spHead {
  display: flex; align-items: baseline; justify-content: center; position: relative;
  padding-bottom: 10px; margin-bottom: 12px;
  border-bottom: 1px solid rgba(56, 178, 230, .28);
}
.spTitle { font-size: 20px; font-weight: 800; letter-spacing: 1px; color: #cfe8fa; }
.spTimer {
  position: absolute; right: 0; top: -2px;
  font-size: 22px; font-weight: 800; color: #eaf4ff;
}

.spBody { display: grid; grid-template-columns: auto 1fr; gap: 20px; align-items: start; }

/* left: hull tiles. Two columns rather than four — the art is the thing being
   chosen, so the tiles are bigger and there is room for it to read. */
.spGrid { display: grid; grid-template-columns: repeat(2, 92px); gap: 8px; }
.spTile {
  position: relative; width: 92px; height: 82px;
  border: 1px solid rgba(56, 178, 230, .45); border-radius: 5px;
  background: linear-gradient(180deg, #14243a, #08111f);
  cursor: pointer; padding: 0; overflow: hidden;
}
.spTile.on {
  border-color: #7fd4ff;
  box-shadow: 0 0 0 1px #7fd4ff, 0 0 14px rgba(90, 200, 255, .5);
}
.spTile > b {
  position: absolute; top: 2px; left: 4px; z-index: 2;
  font-size: 11px; font-weight: 800; color: #cfe8fa; text-shadow: 0 1px 3px #000;
}
.spTileArt {
  display: block; width: 100%; height: 100%;
  background-size: 78%; background-position: center; background-repeat: no-repeat;
  opacity: .95;
}
/* A hull you have not earned. Still shown -- knowing it exists is the point
   of a reward -- but visibly not yours to take. */
.spTile.locked { cursor: not-allowed; border-color: rgba(120, 140, 160, .35); }
.spTile.locked .spTileArt { opacity: .3; filter: grayscale(1); }
.spTile.locked > b { color: #8fa2b4; }
.spLock {
  position: absolute; inset: 0; z-index: 3; display: grid; place-items: center;
  font-size: 20px; text-decoration: none; text-shadow: 0 2px 6px #000;
}

/* right: detail */
.spDetail { min-width: 0; }
.spName { font-size: 19px; font-weight: 800; color: var(--hud-gold); margin-bottom: 8px; }
.spArt {
  height: 200px; margin-bottom: 10px;
  border: 1px solid rgba(56, 178, 230, .4); border-radius: 5px;
  background: radial-gradient(circle at 50% 45%, rgba(24, 52, 84, .6), rgba(3, 8, 15, .95) 72%);
  overflow: hidden;
}
#spCanvas { display: block; width: 100%; height: 100%; }
.spRole, .spVitals, .spSpeed { font-size: 13px; color: #a9c6de; line-height: 1.65; }
.spRole b, .spSpeed b { color: #eaf4ff; }
.spHull { color: #5df08e; }
.spShield { color: #6fc9ff; }
.spDesc { font-size: 12.5px; color: #8fb2cc; line-height: 1.5; margin: 8px 0 12px; }
.spAbilTitle { font-size: 12px; color: #a9c6de; margin-bottom: 6px; }
.spAbils { display: flex; flex-wrap: wrap; gap: 6px; }
.spAbil {
  width: 44px; height: 44px; border-radius: 5px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid rgba(56, 178, 230, .45);
  background: linear-gradient(180deg, #16283c, #0a1421);
}
.spAbil .aico { width: 26px; height: 26px; }

.spConfirm {
  display: block; width: 100%; margin-top: 14px; padding: 12px 0;
  border: 1px solid #7fd4ff; border-radius: 6px;
  background: linear-gradient(180deg, #2f7fc4, #1b4c7d);
  color: #f2faff; font-size: 15px; font-weight: 800; letter-spacing: 1.4px;
  font-family: inherit; cursor: pointer;
  box-shadow: 0 6px 20px rgba(30, 110, 190, .35);
}
.spConfirm:hover { background: linear-gradient(180deg, #3a95e0, #22598f); }

/* ── TOUCH LAYOUT ────────────────────────────────────────────────────────
   A phone in landscape is ~390px tall and ~850px wide: the desktop picker's
   two-column 'tiles beside a detail sheet' shape wastes the width and runs
   out of height. So on touch the panel is rebuilt rather than shrunk — one
   full-width row of large hull tiles, the numbers and the ability icons side
   by side beneath them, and Confirm as a full-width bar.

   The old rules also had a real bug: .spGrid declared 44px columns while
   .spTile was 62px wide, so the tiles overflowed their own column and sat on
   top of the ship name and stats. Sizing the track from the tile is what
   stops that, so the two are defined together from here on. */
body.touch .spPanel { width: 96vw; padding: 10px 14px 12px; }
body.touch .spHead { padding-bottom: 6px; margin-bottom: 8px; }
body.touch .spTitle { font-size: 15px; }
body.touch .spTimer { font-size: 16px; }

/* one column: the hull row on top, everything else under it */
body.touch .spBody { display: block; }
body.touch .spGrid {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-bottom: 10px;
}
body.touch .spTile { flex: 0 0 auto; width: 96px; height: 66px; }
body.touch .spTile > b { font-size: 12px; }

/* numbers on the left, kit on the right — both readable, neither scrolling */
/* Explicit placement, because the detail sheet is a flat list of siblings:
   left the read-me-now numbers, right the loadout, and the name across the
   top. Left to auto-flow they alternated columns and the stats ended up
   scattered across four half-empty rows. */
body.touch .spDetail {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto auto auto auto;
  align-items: center;
  column-gap: 16px;
  row-gap: 1px;
}
body.touch .spName   { grid-area: 1 / 1 / 2 / 3; font-size: 15px; margin-bottom: 2px; }
body.touch .spRole   { grid-area: 2 / 1 / 3 / 2; }
body.touch .spVitals { grid-area: 3 / 1 / 4 / 2; }
body.touch .spSpeed  { grid-area: 4 / 1 / 5 / 2; }
body.touch .spAbils  { grid-area: 2 / 2 / 5 / 3; align-content: center; }
body.touch .spArt { display: none; }          /* the tiles already show the hull */
body.touch .spDesc { display: none; }         /* prose nobody reads under a timer */
body.touch .spAbilTitle { display: none; }
body.touch .spRole, body.touch .spVitals, body.touch .spSpeed {
  font-size: 11.5px; line-height: 1.5;
}
body.touch .spAbils { gap: 5px; justify-content: flex-end; max-width: 320px; }
body.touch .spAbil { width: 34px; height: 34px; }
body.touch .spAbil .aico { width: 21px; height: 21px; }
body.touch .spConfirm { padding: 11px 0; font-size: 14px; margin-top: 10px; }

/* Very short viewports (a notched phone in landscape is ~360px): the same
   shape, one size down. Nothing is removed that was not already gone. */
@media (max-height: 420px) {
  body.touch .spPanel { padding: 7px 12px 9px; }
  body.touch .spHead { padding-bottom: 4px; margin-bottom: 6px; }
  body.touch .spTitle { font-size: 13px; }
  body.touch .spTimer { font-size: 14px; }
  body.touch .spGrid { gap: 6px; margin-bottom: 7px; }
  body.touch .spTile { width: 84px; height: 56px; }
  body.touch .spName { font-size: 13px; }
  body.touch .spRole, body.touch .spVitals, body.touch .spSpeed { font-size: 10.5px; line-height: 1.4; }
  body.touch .spAbil { width: 29px; height: 29px; }
  body.touch .spAbil .aico { width: 18px; height: 18px; }
  body.touch .spConfirm { padding: 9px 0; font-size: 13px; margin-top: 7px; }
}

/* picker preview: give the canvas real pixels and lift the stage lighting */
.spArt { position: relative; background: radial-gradient(circle at 50% 45%, rgba(30, 64, 102, .75), rgba(3, 8, 15, .96) 74%); }
#spCanvas { width: 100%; height: 100%; display: block; }
.spTileArt { background-size: 82%; }
.spTile > b { background: rgba(3, 8, 15, .75); padding: 0 3px; border-radius: 3px; }

/* spawn status: never leave the player staring at a silent black screen */
#spawnErr {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  z-index: 95; display: none; padding: 14px 22px;
  border: 1px solid var(--hud-line); border-radius: 8px;
  background: linear-gradient(180deg, rgba(8,16,27,.96), rgba(3,8,15,.97));
  box-shadow: 0 0 24px var(--hud-glow);
  color: #ffd27f; font-size: 14px; font-weight: 700; letter-spacing: .6px;
}

/* ============================================================
   Touch: collapse the ability grid behind its button.

   The grid is nine tiles. On a phone that is most of the screen, and it sits
   there permanently even though you only touch it to cast. The drawer exists
   to buy that room back. A later "in-grid nav tile" redesign had disabled the
   button and forced the card to `display: grid !important`, so on touch it
   could never close; these rules restore it. Desktop keeps the always-open
   grid, where there is room for it — and a hidden HUD skips the drawer
   entirely, handled above.
   ============================================================ */
body.touch #abilityToggle {
  display: block !important;
  width: 100%; padding: 9px 14px; white-space: nowrap;
}
body.touch #commandCard { display: none !important; }
body.touch.abilitiesOpen #commandCard { display: grid !important; }
body.touch.hudCollapsed:not(.abilitiesOpen) #commandCard { display: none !important; }
/* collapsed drawer should hug its button rather than reserve the grid's box */
body.touch:not(.abilitiesOpen):not(.cmdAbilities) #cmdPanel { width: auto; padding: 6px; }
/* A hidden HUD is the drawer too, so it keeps hugging its button. */
body.touch.hudCollapsed:not(.abilitiesOpen):not(.cmdAbilities) #cmdPanel { width: auto; padding: 6px; }

/* ============================================================
   Multiplayer comms — touch layout.

   Three separate problems on a phone, all fixed here because hud.css is the
   last stylesheet and `#hud > * { pointer-events: auto }` above outranks
   anything style.css says about these elements.
   ============================================================ */

/* 1. The wrapper was swallowing left-thumb drags.
      #chatWrap is 54vw wide on a phone — an invisible 460px slab sitting over
      the bottom-left of the map, i.e. exactly where a left thumb pans. The
      blanket rule above had turned it into a touch target even though it is
      just a container. Only the actual controls should take input. */
#hud > #chatWrap { pointer-events: none; }
#hud > #chatWrap #chatLog { pointer-events: none; }
/* ...and the ROWS inside it are containers too.
   This is the third time this exact bug has been fixed in this file, so it is
   worth naming: a flex/grid container that only holds small controls still has
   a full-width BOX, and pointer-events:auto makes that whole box a touch
   target. #chatQuick holds two small buttons but spans the entire 54vw chat
   column, so it swallowed every drag across the middle of the screen.
   Measured on a Galaxy S24: touches at 50%,48% -- dead centre -- were landing
   on #chatQuick instead of the canvas.
   The rule is containers NONE, controls AUTO, all the way down. */
#hud > #chatWrap #chatQuick,
#hud > #chatWrap #pingToggle,
#hud > #chatWrap #quickList,
#hud > #chatWrap #chatEntry { pointer-events: none; }
#hud > #chatWrap #chatQuick > *,
#hud > #chatWrap #quickList > *,
#hud > #chatWrap #chatEntry > * { pointer-events: auto; }

/* 2. Mobile quick pings are the controls themselves. There is no drawer to
      open in combat: four separated icon targets remain visible at the saved
      Ping HUD position. A vertical rail matches the familiar mobile-MOBA
      pattern, stays narrow, and leaves the battlefield around it tappable. */
body.touch #pingToggle,
body.mobilePings #pingToggle {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
/* A player who has arranged Ping already receives an inline translate from
   applyHudLayout and keeps the old bottom-left coordinate system exactly.
   With no saved placement, the four-button rail needs a useful home of its
   own: the old drawer anchor was low enough that a 222px rail clipped its top
   two actions off-screen on a landscape phone. */
body.touch #pingToggle:not([style*="translate"]),
body.mobilePings #pingToggle:not([style*="translate"]) {
  position: fixed;
  z-index: 45;
  top: calc(60px + env(safe-area-inset-top, 0px));
  right: calc(12px + env(safe-area-inset-right, 0px));
}
body.touch #quickList,
body.touch #quickList.hidden,
body.mobilePings #quickList,
body.mobilePings #quickList.hidden {
  display: flex !important;
  position: static !important;
  flex-direction: column;
  gap: 5px;
  width: auto;
  max-height: none;
  margin: 0;
  overflow: visible;
}
body.touch #quickList button,
body.mobilePings #quickList button {
  justify-content: center;
  width: 24px;
  height: 24px;
  min-width: 24px;
  padding: 0;
  border-radius: 4px;
  background: rgba(7, 20, 35, .86);
  box-shadow: 0 2px 10px rgba(0, 0, 0, .38), inset 0 0 12px rgba(74, 154, 210, .08);
}
body.touch #quickList button span,
body.mobilePings #quickList button span { display: none; }
body.touch #quickList button em,
body.mobilePings #quickList button em {
  width: auto;
  font-size: 12.5px;
  line-height: 1;
  filter: drop-shadow(0 0 2.5px currentColor);
}
body.touch #pingMenuToggle,
body.mobilePings #pingMenuToggle { display: none !important; }
body.touch #chatQuick,
body.mobilePings #chatQuick { justify-content: center; }
body.touch #chatOpenBtn {
  width: 42px;
  height: 34px;
  padding: 0;
}

/* keep the log short so it never pushes the menu off-screen either */
body.touch #chatLog { max-height: 58px; }
body.touch .chatLine { font-size: 10px; }

/* 3. Typing on a phone.
      The desktop entry is a thin inline strip with "Tab = team, Esc = cancel"
      hints that mean nothing without a keyboard. On touch it becomes a proper
      bar across the bottom with tappable Send / scope / close, clear of the
      on-screen keyboard. */
body.touch #chatEntry {
  position: fixed; z-index: 92;
  left: 8px;
  right: 8px;
  bottom: calc(8px + env(safe-area-inset-bottom, 0px));
  gap: 6px; padding: 7px; border-radius: 8px;
  background: rgba(4, 10, 20, .97); border: 1px solid #2a4a7a;
}
body.touch #chatInput { font-size: 15px; padding: 9px 10px; }  /* 16px-ish stops iOS zoom */
body.touch #chatSend, body.touch #chatClose, body.touch #chatScope {
  flex: 0 0 auto; padding: 9px 12px; font-size: 11px; letter-spacing: 1.2px;
}
#chatScope {
  border: none; cursor: pointer;
  font-size: 9px; letter-spacing: 1.5px; padding: 4px 7px; border-radius: 3px;
  color: #04070e; background: #7fd4ff; font-weight: 800;
}
#chatScope.team { background: #6aff8a; }
#chatSend {
  background: linear-gradient(180deg, #2f7fc4, #1b4c7d);
  border: 1px solid rgba(127,212,255,.5); color: #f2faff; font-weight: 700;
}
#chatClose { background: rgba(20,40,75,.85); color: #9fc3e6; }
/* desktop keeps the compact inline strip */
body:not(.touch) #chatSend, body:not(.touch) #chatClose { display: none; }

/* ============================================================
   Ability card — desktop
   ============================================================ */

/* A dimmed tile must not drag its tooltip down with it.
   `opacity` applies to the whole subtree, and the tip is a CHILD of the tile,
   so every unresearched/unaffordable ability rendered its tooltip at 38%
   opacity — unreadable over a bright battlefield. Dim the tile's own contents
   instead and leave the tip alone. */
.cmd:disabled { opacity: 1; cursor: default; }
.cmd:disabled > *:not(.tip) { opacity: .38; }


body:not(.touch) #commandCard .cmd { height: 68px; }
/* Icons carry the meaning; the hotkey and cost stay, the name goes — it was
   the thing forcing narrow tiles and three rows. The tooltip still names it. */
body:not(.touch) #commandCard .cmd .cName { display: none; }

/* ════════════════════════════════════════════════════════════════════════
   MINIMISED HUD: a SELECTED unit still reports.
   Long-pressing an enemy brings its card up, and minimised that card was
   simply gone -- so the gesture did nothing and there was no way to read
   anything about a ship without restoring the whole console. Kept to what is
   worth a glance: the stat icons and the move speed, no portrait, no name
   plate, and pushed back the same way the map is -- no frame, no plate,
   translucent. Only while something is selected; nothing is shown otherwise.
   ════════════════════════════════════════════════════════════════════════ */
body.hudCollapsed.hasSelection #infoPanel {
  display: flex !important;
  background: none !important;
  border: none !important;
  border-image: none !important;
  box-shadow: none !important;
  padding: 0 !important;
  opacity: .72;
  /* The PANEL takes no input -- it must not eat taps meant for the map behind
     it -- but the stat icons inside it do, or their tooltips are unreachable
     and the strip becomes a row of symbols with no way to ask what they mean. */
  pointer-events: none;
}
body.hudCollapsed:not(.cmdAbilities) #infoPanel .upgTile,
body.hudCollapsed:not(.cmdAbilities) #infoPanel .bfChip,
body.hudCollapsed:not(.cmdAbilities) #infoPanel .tip { pointer-events: auto; }

/* Centred on a phone. Anchored left it sat under the thumb that flies the
   ship, and read as a stray caption rather than as a readout. */
/* (The screen-centring rule that lived here is gone: it outranked the gap rule below and put the strip back under the icons.) */
body.touch.hudCollapsed #unitStats,
body.touch.hudCollapsed #unitStats .scCard { justify-items: center; text-align: center; }
body.touch.hudCollapsed #unitStats .scIcons { justify-content: center; }
body.hudCollapsed:not(.cmdAbilities) #infoPanel::before,
body.hudCollapsed:not(.cmdAbilities) #infoPanel::after { display: none !important; }
/* Hide the name plate, bars and portrait -- but NOT #infoMain, which is the
   box #unitStats lives in. Hiding the parent took the numbers with it. */
body.hudCollapsed #infoLeft,
body.hudCollapsed #unitName,
body.hudCollapsed #unitSub,
body.hudCollapsed #unitBars,
body.hudCollapsed #unitStats .scTags,
body.hudCollapsed #unitStats .scLine:not(:first-of-type) { display: none !important; }
body.hudCollapsed #infoMain { display: block !important; width: auto !important; }
body.hudCollapsed #unitStats .scCard {
  background: none !important;
  border: none !important;
  box-shadow: none !important;
  padding: 0 !important;
}
/* Same on the refit grid: the icon carries it on a desktop, where there is a
   tooltip a mouse can reach. Touch keeps the captions -- there is no hover
   there, so the word IS the label. */
body:not(.touch) #upgradeGrid .cmd .cName { display: none; }
body:not(.touch) #commandCard .cmd .aico,
body:not(.touch) #commandCard .cmd .ico { width: 34px; height: 34px; }

/* An ability that can be upgraded RIGHT NOW (docked, levels left). Gold pulse
   so it reads at a glance, same language as the ABILITIES button. */
.cmd.ability.canUp { border-color: #ffc94a; }
.cmd.ability.canUp .cCost { color: #ffc94a; font-weight: 800; }
.cmd.ability.canUp:not(:disabled) {
  animation: upPulse 1.4s ease-in-out infinite;
}
@keyframes upPulse {
  0%, 100% { box-shadow: 0 0 4px rgba(255, 201, 74, .35); }
  50%      { box-shadow: 0 0 13px rgba(255, 201, 74, .8); }
}

/* Match clock. Sits beside the mineral counter in the top bar, deliberately
   quieter than it — you glance at the time, you read your bank. */
#matchClock {
  align-self: center; margin-left: 8px; padding: 5px 12px;
  font-size: 12px; font-weight: 700; letter-spacing: 1.6px;
  color: #8fb4d4; background: rgba(8, 16, 28, .72);
  border: 1px solid rgba(90, 140, 190, .35);
  font-variant-numeric: tabular-nums;   /* stops the width jittering each second */
}
body.touch #matchClock { font-size: 10.5px; padding: 3px 8px; letter-spacing: 1px; }

/* Leaderboard button on desktop too. It was touch-only on the reasoning that
   a keyboard has Tab — but Tab is undiscoverable, and the trophy sitting next
   to MY SHIP costs nothing. Same slot on both, so muscle memory carries. */
#btnLb {
  display: block;
  position: absolute;
  top: 8px;
  left: 95px;
  min-width: 42px;
  min-height: 34px;
  padding: 6px 10px;
  border: 1px solid var(--hud-line);
  border-radius: 8px;
  background: linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
  color: #cfe8fa;
  font-size: 16px;
  line-height: 1;
  text-align: center;
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 10px var(--hud-glow);
  cursor: pointer;
  z-index: 25;
}
#btnLb:hover { border-color: var(--hud-line-hi); color: #fff; }

/* Camera follow. On a keyboard this is Space; a phone has no Space, so the
   toggle is a button on both — and having it visible is what tells a desktop
   player the mode exists at all. Lit while the camera is locked on. */
#btnFollow {
  display: block;
  position: absolute;
  top: 8px;
  left: 143px;
  min-width: 42px;
  min-height: 34px;
  padding: 6px 10px;
  border: 1px solid var(--hud-line);
  border-radius: 8px;
  background: linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
  color: #cfe8fa;
  font-size: 16px;
  line-height: 1;
  text-align: center;
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 10px var(--hud-glow);
  cursor: pointer;
  z-index: 25;
}
#btnFollow:hover { border-color: var(--hud-line-hi); color: #fff; }
#btnFollow.on {
  border-color: #6aff8a;
  color: #6aff8a;
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 12px rgba(106, 255, 138, .55);
}

/* On a phone the top strip is full: the right-hand toggles reach left to 227px
   on a 375px screen and the follow button sat under the fullscreen one. Drop it
   to its own row rather than shrinking the whole strip. */
@media (max-width: 700px) {
  #btnFollow { top: 8px; left: 143px; }
}
/* Cancel moves along on both, not just touch. */
#btnCancel { left: 191px; }

/* Floating bounty numbers on a kill. */
.bountyPop {
  position: absolute;
  pointer-events: none;
  z-index: 24;
  font: 800 15px/1 'Chakra Petch', system-ui, sans-serif;
  color: #9fffc0;
  text-shadow: 0 0 8px rgba(60, 255, 150, .55), 0 1px 2px rgba(0, 0, 0, .9);
  animation: bountyRise 1.05s ease-out forwards;
}
@keyframes bountyRise {
  0%   { opacity: 0; transform: translate(-50%, 0) scale(0.8); }
  18%  { opacity: 1; transform: translate(-50%, -10px) scale(1.06); }
  100% { opacity: 0; transform: translate(-50%, -46px) scale(1); }
}

/* Hover card on a leaderboard name — ladder standing without leaving the board.
   The numbers ride along with the scoreboard, so this never waits on a fetch. */
.lbName { position: relative; }
.lbGuest {
  margin-left: 6px; padding: 1px 5px; border-radius: 4px; font-style: normal;
  font-size: 8px; letter-spacing: 1px;
  background: rgba(255, 190, 110, .16); color: #ffcf95;
}
.lbCard {
  display: none;
  position: absolute;
  left: 100%;
  top: -6px;
  margin-left: 10px;
  z-index: 40;
  min-width: 156px;
  padding: 8px 10px;
  border: 1px solid rgba(120, 200, 255, .45);
  border-radius: 8px;
  background: linear-gradient(180deg, rgba(10, 24, 42, .97), rgba(6, 14, 26, .97));
  box-shadow: 0 8px 26px rgba(0, 0, 0, .6);
  white-space: nowrap;
  pointer-events: none;
}
.lbName:hover .lbCard, .lbName.lbOpen .lbCard { display: block; }
@media (max-width: 820px) {
  .lbCard { left: auto; right: 0; top: 100%; margin: 4px 0 0; }
}
.lbCard > b {
  display: block;
  margin-bottom: 5px;
  color: #eaf6ff;
  font-size: 13px;
  letter-spacing: .06em;
}
.lbCard > span {
  display: block;
  color: #8fb4d8;
  font-size: 11px;
  line-height: 1.55;
}
.lbCard i { color: #cfe8fa; font-style: normal; font-weight: 700; }

/* The after value in a refit tooltip: green, because it is what you are buying. */
.tip .tipUp { color: #6aff8a; }

/* A settings row the active mode overrules — the value shown is the default,
   not what the match will run on. */
#admBody .modeForced > span:first-child::after {
  content: ' (mode overrides)';
  color: #ffd75e;
  font-size: 9px;
  letter-spacing: 0;
}
#admBody .modeForced input { border-color: #7a5a12; }

/* The Juggernaut autocannon chip — same treatment as the wing chip:
   an armament, not a refit track. */
.upgTile.cannonTile { border-color: #7a4a2a; }
.upgTile.cannonTile > b { color: #ffb27a; }

/* Secondary figure inside a tooltip row — the "+160 vs capital" that qualifies
   the number before it without competing with it. */
.tip .tipDim { font-style: normal; color: #8fa8c4; font-size: 0.92em; }

/* Solo: the ping bar stays, the typed chat goes. Pings are orders the AI
   fleet acts on; a chat box with nobody on the other end is furniture. */
body.soloComms #chatOpenBtn,
body.soloComms #chatEntry { display: none; }

/* ── UPLOADED PANEL FRAME ─────────────────────────────────────────────────
   When a frame image is set, the panels wear it as a border-image instead of
   the plain line. `hudFramed` goes on <body> from applyHudTheme, so none of
   this is evaluated when no frame is chosen.

   Two things have to happen beyond swapping the border, and missing either is
   what produced "a black square around my PNG":

     - the panel's own dark fill paints under the BORDER box by default, so it
       showed through every transparent pixel of the frame art. Clipping it to
       the padding box keeps the fill inside the frame where it belongs.
     - the 1px near-black ring these panels carry as a box-shadow drew a hard
       square immediately outside the art. It goes.

   The selector list is every real panel. #cmdPanel is the abilities drawer
   itself — an earlier version targeted #commandCard, the grid INSIDE it, which
   is why the abilities panel was the one without a frame. */
body.hudFramed #minimapPanel,
body.hudFramed #infoPanel,
body.hudFramed #upgPanel,
body.hudFramed #cmdPanel,
body.hudFramed #hudChar,
body.hudFramed .hudPlate {
  border-style: solid;
  border-width: var(--hud-frame-width, 14px);
  border-image-source: var(--hud-frame);
  border-image-slice: var(--hud-frame-slice, 24) fill;
  border-image-repeat: stretch;
  /* the art carries its own corners; CSS rounding would clip them to arcs */
  border-radius: 0;
  /* keep the fill inside the frame, not behind its transparent parts */
  background-clip: padding-box;
  /* and drop the hard 1px ring that framed the frame */
  box-shadow: none;
}

/* The minimap canvas has its own corner rounding, which reads as a mismatch
   inside a square frame. */
body.hudFramed #minimap { border-radius: 0; }

/* ══════════════════════════════════════════════════════════════════════════
   ACHIEVEMENT BANNER

   StarCraft's shape and, more importantly, its THEATRE: the plate does not
   simply appear, it arrives. Four things happen at once and then stop, which
   is what separates a celebration from a notification —

     1. the plate slides in and overshoots slightly, then settles
     2. a white flash blows out across it on arrival and fades
     3. a specular sheen sweeps left to right, once
     4. the border glow pulses twice, then holds

   All of it is composited transform/opacity work on a handful of elements, so
   it costs nothing in the middle of a firefight.
   ══════════════════════════════════════════════════════════════════════════ */

#achBanners {
  position: fixed;
  top: 64px;
  right: 0;
  z-index: 70;
  display: flex;
  flex-direction: column;
  gap: 10px;
  pointer-events: none;
}

.achBanner {
  position: relative;
  width: 430px;
  max-width: 82vw;
  /* off-stage until it is told to arrive */
  transform: translateX(112%);
  opacity: 0;
  transition: transform 0.55s cubic-bezier(0.13, 1.5, 0.4, 1), opacity 0.35s ease;
}
.achBanner.in { transform: none; opacity: 1; }

/* ── the glow that bleeds outside the plate ─────────────────────────────── */
.achGlow {
  position: absolute;
  inset: -14px -6px -14px -18px;
  border-radius: 14px;
  background:
    radial-gradient(60% 120% at 12% 50%, rgba(90, 190, 255, 0.55), transparent 70%),
    radial-gradient(70% 140% at 90% 50%, rgba(255, 190, 90, 0.28), transparent 70%);
  filter: blur(9px);
  opacity: 0;
}
.achBanner.in .achGlow { animation: achGlowPulse 2.4s ease-out 0.15s 2; }
@keyframes achGlowPulse {
  0%   { opacity: 0; }
  18%  { opacity: 1; }
  100% { opacity: 0.32; }
}

/* ── the plate ──────────────────────────────────────────────────────────── */
.achPlate {
  position: relative;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 18px 12px 12px;
  overflow: hidden;
  /* the angled left edge is what makes it read as sci-fi rather than a toast */
  clip-path: polygon(22px 0, 100% 0, 100% 100%, 0 100%, 0 22px);
  border: 1px solid rgba(130, 210, 255, 0.75);
  border-right: 0;
  background:
    linear-gradient(180deg, rgba(30, 78, 128, 0.97) 0%, rgba(12, 34, 62, 0.97) 42%,
                            rgba(7, 20, 38, 0.98) 100%),
    radial-gradient(120% 100% at 0% 0%, rgba(120, 200, 255, 0.32), transparent 60%);
  box-shadow:
    inset 0 1px 0 rgba(170, 230, 255, 0.55),
    inset 0 -1px 0 rgba(0, 0, 0, 0.5),
    0 10px 30px rgba(0, 0, 0, 0.65);
}

/* a hairline of light down the angled edge */
.achPlate::before {
  content: '';
  position: absolute;
  left: 0; top: 22px; bottom: 0;
  width: 2px;
  background: linear-gradient(180deg, rgba(150, 225, 255, 0.9), rgba(150, 225, 255, 0));
}

/* ── the specular sweep ─────────────────────────────────────────────────── */
.achSheen {
  position: absolute;
  top: 0; bottom: 0;
  left: -60%;
  width: 45%;
  background: linear-gradient(105deg,
    transparent 0%, rgba(255, 255, 255, 0.14) 35%,
    rgba(255, 255, 255, 0.45) 50%, rgba(255, 255, 255, 0.14) 65%, transparent 100%);
  transform: skewX(-18deg);
  opacity: 0;
}
.achBanner.in .achSheen { animation: achSweep 1.15s cubic-bezier(0.3, 0, 0.2, 1) 0.28s 1; }
@keyframes achSweep {
  0%   { left: -60%; opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { left: 115%; opacity: 0; }
}

/* ── the arrival flash ──────────────────────────────────────────────────── */
.achFlash {
  position: absolute;
  inset: 0;
  background: radial-gradient(80% 140% at 14% 50%, rgba(220, 245, 255, 0.85), transparent 65%);
  opacity: 0;
  mix-blend-mode: screen;
}
.achBanner.in .achFlash { animation: achFlash 0.75s ease-out 1; }
@keyframes achFlash {
  0%   { opacity: 0.95; }
  100% { opacity: 0; }
}

/* ── icon, in a lit frame ───────────────────────────────────────────────── */
.achIconWrap {
  position: relative;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 54px; height: 54px;
  border: 1px solid rgba(150, 225, 255, 0.7);
  border-radius: 7px;
  background: linear-gradient(180deg, #1d4d7d, #0a1c30);
  box-shadow: inset 0 1px 0 rgba(190, 240, 255, 0.5), 0 0 16px rgba(90, 190, 255, 0.4);
}
/* a ring that expands out of the icon once, like an impact */
.achIconRing {
  position: absolute;
  inset: 0;
  border: 2px solid rgba(160, 225, 255, 0.9);
  border-radius: 7px;
  opacity: 0;
}
.achBanner.in .achIconRing { animation: achRing 0.9s ease-out 0.1s 1; }
@keyframes achRing {
  0%   { transform: scale(1); opacity: 0.9; }
  100% { transform: scale(1.75); opacity: 0; }
}
.achIcon { width: 32px; height: 32px; }
.achIcon.icoCustom {
  width: 100%; height: 100%;
  background-size: contain; background-repeat: no-repeat; background-position: center;
}

/* ── text ───────────────────────────────────────────────────────────────── */
.achText { flex: 1 1 auto; min-width: 0; }
.achEyebrow {
  margin-bottom: 1px;
  color: #7fc4ee;
  font: 700 8.5px/1 'Chakra Petch', system-ui, sans-serif;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  opacity: 0.85;
}
.achName {
  font: 700 17px/1.15 'Chakra Petch', system-ui, sans-serif;
  letter-spacing: 0.02em;
  color: #f2fbff;
  text-shadow: 0 0 14px rgba(120, 200, 255, 0.6);
}
.achDesc {
  margin-top: 2px;
  font: 400 12px/1.35 'Chakra Petch', system-ui, sans-serif;
  color: #a9cde8;
}

/* ── points ─────────────────────────────────────────────────────────────── */
.achPtsWrap { flex: 0 0 auto; text-align: center; padding-left: 8px; }
.achPts {
  font: 800 30px/0.95 'Chakra Petch', system-ui, sans-serif;
  color: #ffd76e;
  text-shadow: 0 0 18px rgba(255, 190, 80, 0.85), 0 2px 0 rgba(90, 50, 0, 0.6);
}
.achBanner.in .achPts { animation: achPop 0.55s cubic-bezier(0.2, 1.8, 0.4, 1) 0.2s 1; }
@keyframes achPop {
  0%   { transform: scale(0.4); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}
.achPtsLabel {
  margin-top: 1px;
  color: #b98f3e;
  font: 700 8px/1 'Chakra Petch', system-ui, sans-serif;
  letter-spacing: 0.2em;
}

@media (max-width: 900px), (pointer: coarse) {
  #achBanners { top: 44px; gap: 7px; }
  .achBanner { width: 320px; }
  .achPlate { gap: 10px; padding: 8px 12px 8px 8px; clip-path: polygon(16px 0, 100% 0, 100% 100%, 0 100%, 0 16px); }
  .achIconWrap { width: 40px; height: 40px; }
  .achIcon { width: 24px; height: 24px; }
  .achName { font-size: 13.5px; }
  .achDesc { font-size: 10px; }
  .achEyebrow { font-size: 7.5px; }
  .achPts { font-size: 22px; }
}

/* Someone who does not want motion gets the plate without the theatre. */
@media (prefers-reduced-motion: reduce) {
  .achBanner, .achBanner.in .achGlow, .achBanner.in .achSheen,
  .achBanner.in .achFlash, .achBanner.in .achIconRing, .achBanner.in .achPts {
    animation: none !important;
    transition-duration: 0.2s !important;
  }
  .achBanner.in .achGlow { opacity: 0.32; }
}

/* ── ACHIEVEMENTS ON THE PROFILE ──────────────────────────────────────────
   A summary that opens the board. The card cannot hold fifty rows, and a
   count with a progress bar is what anyone actually wants at a glance. */

.profAchSummary {
  display: block;
  width: 100%;
  padding: 12px 14px 11px;
  border: 1px solid rgba(110, 190, 255, 0.28);
  border-radius: 9px;
  background: linear-gradient(180deg, rgba(20, 44, 76, 0.5), rgba(10, 22, 40, 0.5));
  color: inherit;
  text-align: left;
  cursor: pointer;
  font: inherit;
}
.profAchSummary:hover { border-color: rgba(150, 215, 255, 0.55); background: rgba(30, 66, 108, 0.55); }

.paCount {
  display: block;
  font: 800 22px/1 'Chakra Petch', system-ui, sans-serif;
  color: #8fd3ff;
}
.paMeta {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  margin-top: 3px;
  color: #8ba4c0; font-size: 12px;
}
.paGo { color: #6fc0ff; font-weight: 600; }
.paBar {
  display: block; height: 5px; margin-top: 9px;
  border-radius: 3px; background: rgba(2, 8, 18, 0.8); overflow: hidden;
}
.paBar > i { display: block; height: 100%; width: 0;
  background: linear-gradient(90deg, #2f8fc4, #7fd4ff); transition: width 0.5s ease; }

.profAchRecent { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 9px; }
.paChip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 9px 4px 6px;
  border: 1px solid rgba(110, 190, 255, 0.25);
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.04);
  color: #cfe3f7; font-size: 11.5px;
}
.paChipIcon { width: 16px; height: 16px; }
.paChipIcon.icoCustom { background-size: contain; background-repeat: no-repeat; background-position: center; }
.paNone { color: #6e88a8; font-size: 12px; font-style: italic; }

/* ── the full board ───────────────────────────────────────────────────── */

.achBoard {
  position: fixed; inset: 0; z-index: 9200;
  display: flex; align-items: center; justify-content: center;
  padding: 16px;
  background: rgba(2, 5, 14, 0.78);
  backdrop-filter: blur(3px);
}
.abCard {
  position: relative;
  width: min(880px, 100%);
  max-height: min(88vh, 760px);
  display: flex; flex-direction: column;
  padding: 20px 22px 16px;
  border: 1px solid rgba(110, 190, 255, 0.3);
  border-radius: 12px;
  background: linear-gradient(180deg, #0b1426, #070c18);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.65);
}
.abClose {
  position: absolute; top: 8px; right: 10px;
  width: 30px; height: 30px;
  border: 0; border-radius: 6px; background: transparent;
  color: #6e88a8; font-size: 16px; cursor: pointer;
}
.abClose:hover { background: rgba(255, 255, 255, 0.06); color: #cfe3f7; }

.abHead { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; }
.abTitle {
  color: #8fd3ff; font: 700 15px/1 'Chakra Petch', system-ui, sans-serif;
  letter-spacing: 0.1em; text-transform: uppercase;
}
.abScore { color: #a8c4dc; font-size: 13px; }
.abScore b { color: #eaf6ff; }

.abFilters { display: flex; gap: 7px; margin: 14px 0 12px; }
.abFilter {
  padding: 5px 13px;
  border: 1px solid rgba(110, 190, 255, 0.25);
  border-radius: 20px;
  background: transparent; color: #8ba4c0;
  font: 600 11.5px/1 system-ui, sans-serif; cursor: pointer;
}
.abFilter.on { background: rgba(60, 140, 210, 0.28); color: #eaf6ff; border-color: #7fd4ff; }

.abGrid {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 9px;
  padding-right: 4px;
}
.abItem {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 12px;
  border: 1px solid rgba(110, 190, 255, 0.16);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.03);
}
.abItem.abGot { border-left: 3px solid #6fc0ff; background: rgba(30, 70, 116, 0.3); }
/* Locked ones are the whole point of a board — dimmed, not hidden. */
.abItem.abLocked { opacity: 0.42; }
.abItem.abSecret .abName { font-style: italic; }

.abIcon {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 38px; height: 38px;
  border: 1px solid rgba(110, 190, 255, 0.25); border-radius: 6px;
  background: linear-gradient(180deg, #17395e, #0b1c30);
}
.abIcon .paChipIcon { width: 22px; height: 22px; }
.abText { flex: 1 1 auto; min-width: 0; }
.abName { color: #eaf6ff; font: 700 13.5px/1.2 'Chakra Petch', system-ui, sans-serif; }
.abDesc { margin-top: 2px; color: #9dbdd8; font-size: 11.5px; line-height: 1.35; }
.abWhen { margin-top: 3px; color: #6e88a8; font-size: 10.5px; }
.abPts {
  flex: 0 0 auto;
  color: #ffd76e; font: 800 19px/1 'Chakra Petch', system-ui, sans-serif;
}
.abItem.abLocked .abPts { color: #7b7460; }
.abEmpty { grid-column: 1 / -1; color: #6e88a8; text-align: center; padding: 30px 0; }

@media (max-width: 700px), (pointer: coarse) {
  .abGrid { grid-template-columns: 1fr; }
  .abCard { padding: 14px 14px 10px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE TRACKPAD

   Bottom-left, where a left thumb already rests. It is deliberately large and
   deliberately faint: a control you steer by feel wants to be findable
   without looking, and invisible enough not to compete with the battlefield
   it sits on top of.
   ══════════════════════════════════════════════════════════════════════════ */
#moveStick {
  position: fixed;
  left: 14px;
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  width: 132px; height: 132px;
  z-index: 36;
  display: grid; place-items: center;
  touch-action: none;
  pointer-events: auto;
}
#moveStick.hidden { display: none; }
body:not(.layoutEdit) #moveStick:not(.stickHeld) { opacity: 1; pointer-events: auto; }
body:not(.layoutEdit) #moveStick.stickHeld {
  left: var(--stick-x) !important;
  top: var(--stick-y) !important;
  bottom: auto !important;
  transform: translate(-50%, -50%) !important;
  pointer-events: none;
}
.bountyPopLarge {
  color: #65ff9c;
  font-size: 23px;
  white-space: nowrap;
  text-shadow: 0 0 12px rgba(60, 255, 150, .8), 0 2px 3px #001c0c;
  animation-duration: 1.5s;
}

/* Actual laser/missile damage dealt to a ship. Compact, red and outlined so
   it remains legible over both bright explosions and black space. */
.damagePop {
  position: absolute;
  pointer-events: none;
  z-index: 25;
  font: 900 12.6px/1 'Chakra Petch', system-ui, sans-serif;
  color: #ff5b55;
  -webkit-text-stroke: .9px rgba(45, 0, 0, .9);
  paint-order: stroke fill;
  text-shadow: 0 0 6.3px rgba(255, 35, 30, .72), 0 1px 1.8px #000;
  animation: damageRise .9s ease-out forwards;
}
@keyframes damageRise {
  0%   { opacity: 0; transform: translate(-50%, 2px) scale(.82); }
  14%  { opacity: 1; transform: translate(-50%, -7px) scale(1.08); }
  72%  { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -38px) scale(.96); }
}
.stickRing {
  position: absolute; inset: 0;
  border-radius: 50%;
  border: 2px solid rgba(120, 190, 250, .30);
  background: radial-gradient(circle at 50% 50%, rgba(20, 42, 70, .34), rgba(8, 16, 28, .22));
  box-shadow: inset 0 0 22px rgba(80, 160, 230, .12);
  transition: border-color .15s ease, box-shadow .15s ease;
}
.stickKnob {
  position: relative;
  width: 52px; height: 52px;
  border-radius: 50%;
  border: 2px solid rgba(140, 210, 255, .55);
  background: radial-gradient(circle at 50% 35%, rgba(120, 200, 255, .55), rgba(20, 50, 84, .75));
  box-shadow: 0 2px 10px rgba(0, 0, 0, .5);
  /* No transition on transform: the knob must track the finger exactly, and
     easing it puts the ship a frame behind the thumb. */
}
#moveStick.stickHeld .stickRing {
  border-color: rgba(160, 220, 255, .6);
  box-shadow: inset 0 0 26px rgba(120, 200, 255, .22);
}

/* ── the top row, on a phone ────────────────────────────────────────────────
   MY SHIP, the leaderboard and the camera-follow toggle used to sit top-LEFT,
   which is the one piece of screen the map wants. They are all occasional
   controls -- press once, go back to flying -- so they belong with the other
   occasional controls at the top right, beside settings.

   Laid out leftward from the slot fullscreen vacated. The centred mineral
   counter sits well clear of the innermost of them. */
/* ONLY in trackpad mode. Tap-to-move keeps the layout it always had: the map
   stays bottom-left and these stay top-left, because nothing is competing for
   that corner. They move only when the stick takes the bottom-left. */
/* THE FULLSCREEN BUTTON LIVES AT right:108px. These three move into the
   top-right when the stick takes the bottom-left corner, and #btnFollow used
   to land on exactly that slot -- so on a tablet in trackpad mode the button
   was rendered, reported display:block, and was completely invisible because
   the follow control was sitting on top of it. It gets its own slot past the
   end of the row instead. */
body.touch.stickMove #btnFollow { left: auto; right: 108px; }
body.touch.stickMove #fsToggle  { right: 262px; }
body.touch.stickMove #btnLb     { left: auto; right: 158px; }
body.touch.stickMove #btnSelf {
  left: auto; right: 210px;
  /* An icon, not the words. "MY SHIP" is 79px of label on a row that also has
     to hold the mineral count and the match clock, and on a 667-wide handset
     it collided with them. The other two controls beside it are already
     glyphs; a lone word among them looked like a placeholder because it read
     like one. */
  font-size: 0;
  width: 44px; min-width: 44px; padding: 0;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2.5 20 20.5 12 16.2 4 20.5Z' fill='%23cfe8fa'/%3E%3C/svg%3E"),
    linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
  background-repeat: no-repeat, no-repeat;
  background-position: center, center;
  background-size: 19px 19px, cover;
}
/* Selected: the glyph lights up with the border rather than staying grey. */
body.touch.stickMove #btnSelf:active {
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2.5 20 20.5 12 16.2 4 20.5Z' fill='%23ffffff'/%3E%3C/svg%3E"),
    linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
}
/* Cancel-targeting stays top-left. It appears only while you are aiming, and
   the corner it now has to itself is the clearest place for a way out. */

/* With the stick in the bottom-left corner, the map cannot also live there.
   It takes the top-left instead -- vacated by the buttons above, so it gets
   the whole corner rather than hiding under them -- and gives up some size.
   The stick is the control you use constantly; the map is the one you glance
   at. */
body.touch.stickMove #minimapPanel {
  top: 8px;
  bottom: auto;
  left: 8px;
}

/* ============================================================
   Battlefield cover.

   Shown only when a match starts before the station and outposts have
   finished downloading — 72MB that the loading screen deliberately no
   longer waits for. Without this the player arrives to flat procedural
   stand-ins where the biggest objects on the board should be. The pause
   is the same length either way; this puts it on a covered screen
   instead of on the battlefield.
   ============================================================ */
#bfCover {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #04060f;
  transition: opacity .35s ease;
}
#bfCover.hidden { opacity: 0; pointer-events: none; }
.bfCoverTitle {
  font: 600 clamp(14px, 2.4vw, 22px)/1.2 'Rajdhani', system-ui, sans-serif;
  letter-spacing: .28em;
  color: #cfe6ff;
  text-align: center;
}
.bfCoverSub {
  margin-top: .7em;
  font: 400 clamp(11px, 1.5vw, 14px)/1.4 'Rajdhani', system-ui, sans-serif;
  letter-spacing: .12em;
  color: #6f8ba8;
  text-align: center;
}
#bfCoverTip {
  width: min(520px, 76vw);
  margin: 20px auto 0;
  padding-top: 14px;
  border-top: 1px solid rgba(110, 165, 230, .18);
  text-align: center;
}

/* ============================================================
   Mobile browser guard.

   A phone browser entering a match crashes the tab — the renderer process is
   killed for memory, which no script can catch, so the only thing that helps
   is saying so beforehand. Deliberately a warning with a way through rather
   than a block: some phones cope, and refusing a device that would have worked
   is its own kind of wrong.
   ============================================================ */
#mobileWarn {
  position: fixed; inset: 0; z-index: 9500; display: flex;
  align-items: center; justify-content: center; padding: 20px;
  background: rgba(2, 5, 12, .93); backdrop-filter: blur(3px);
}
.mwBox {
  max-width: 460px; width: 100%; border: 1px solid #2a4a7a; border-radius: 12px;
  background: linear-gradient(180deg, #0d1b2e, #070d18); padding: 22px 22px 18px;
  font-family: 'Rajdhani', system-ui, sans-serif; color: #cfe6ff;
}
.mwBox h2 {
  margin: 0 0 12px; font-size: 17px; letter-spacing: .1em; color: #ffc46a;
  text-transform: uppercase; font-weight: 700; line-height: 1.3;
}
.mwBox p { margin: 0 0 11px; font-size: 13px; line-height: 1.55; color: #b7cde8; }
.mwBox p.mwFix { color: #cfe6ff; }
.mwBox b { color: #ff9a8a; }
.mwRow { display: flex; gap: 10px; margin: 16px 0 12px; flex-wrap: wrap; }
.mwGet, .mwGo {
  flex: 1 1 150px; text-align: center; padding: 11px 14px; border-radius: 7px;
  font-family: inherit; font-size: 13px; letter-spacing: .1em; cursor: pointer;
  text-decoration: none; text-transform: uppercase; font-weight: 700;
}
.mwGet { background: #ffc46a; color: #141414; border: 1px solid #ffc46a; }
.mwGo { background: #0c1526; color: #cfe6ff; border: 1px solid #2a4a7a; }
.mwGo:hover { background: #13223c; }
.mwChk {
  display: flex; align-items: center; gap: 8px;
  font-size: 11px; color: #6f8ba8; cursor: pointer;
}

/* ============================================================
   The abilities button must not vanish with the selection.

   #abilityToggle lives INSIDE #cmdPanel, and #cmdPanel is display:none
   whenever nothing is selected (and at base, unless the abilities tab is
   showing). On a phone that button is the ONLY way into the drawer, so losing
   the selection silently took the control away -- which reads as a button that
   needs pressing four times, because the presses that land are the ones where
   something happened to be selected.

   The panel stays; only its CARD is hidden. Desktop is untouched -- it has the
   whole drawer on screen and never depended on this button.
   ============================================================ */
body.touch:not(.hasSelection) #cmdPanel { display: flex !important; }
body.touch:not(.hasSelection) #commandCard { display: none !important; }
body.touch.atBase:not(.cmdAbilities) #cmdPanel { display: flex !important; }

/* The diagnostics panel must not become the thing it is diagnosing.
   It is a readout; only its Copy button needs to take input. Without this it
   sat over the left of the screen and blocked the very touches it was there
   to record. */
#diagPanel { pointer-events: none; }
#diagPanel #diagCopy { pointer-events: auto; }


/* ════════════════════════════════════════════════════════════════════════
   THE ABILITIES BUTTON MUST NOT HIDE UNDER ITS OWN DRAWER.

   The drawer opened by hiding the button and letting #commandCard take the
   corner it had been occupying. So the first tap worked and every tap after
   it landed on the card, or on a tile's <span> -- measured on the phone as

       91%,93% -> #commandCard (BLOCKED)
       91%,93% -> span (BLOCKED)

   with #abilityToggle never once the hit target. There was no way to close
   the drawer by pressing where the button had just been, and the closer
   deliberately spares #cmdPanel, so pressing inside it did nothing either.
   That is the "six taps before anything happens" report: not a missed tap, a
   covered control.

   The button now STAYS, ordered last inside a panel that is anchored to the
   bottom, so it keeps the same corner whether the drawer is open or shut and
   the card stacks above it. One spot, one control, toggles both ways.
   ════════════════════════════════════════════════════════════════════════ */
/* THE DRAWER REPLACES THE BUTTON. IT DOES NOT SIT NEXT TO IT.

   Open, the panel prints its own ABILITIES heading and fills the corner with
   tiles -- a second button under it is redundant, and while the HUD is
   minimised the two occupy the same corner outright, so the button covered
   the last row of abilities (System Lock and Teleport went under it).

   So the button hides while the drawer is open, and tapping anywhere off the
   drawer closes it and brings the button back.

   That was always the intended shape, and it was never what made the button
   hard to press: the toggle was bound more than once, each binding with its
   own private repeat-guard, so one press toggled twice and landed back where
   it started. That is fixed in game.js with a window-scoped guard, which is
   what lets this go back to the simple arrangement. */
body.touch.abilitiesOpen #abilityToggle { display: none !important; }
/* ...AND WHEN THE DOCKED CONSOLE IS ALREADY SHOWING ABILITIES.

   Two different classes mean 'abilities are on screen': .abilitiesOpen is the
   minimised drawer, .cmdAbilities is the docked console's abilities page. Only
   the first hid the button, so docking and pressing ABILITIES left the button
   sitting on top of the panel it had just opened -- offering to do the thing
   it had already done. */
body.touch.cmdAbilities #abilityToggle { display: none !important; }

/* The refit grid reaches into this corner on a short screen and swallowed the
   taps meant for the button -- the "#upgradeGrid (BLOCKED)" line in the very
   first readout. The grid is a container, so it takes no input of its own and
   leaves that to its tiles; the same rule the chat and diagnostics panels
   already follow. */
body.touch #upgPanel, body.touch #upgradeGrid { pointer-events: none; }
body.touch #upgPanel .cmd,
body.touch #upgPanel button,
body.touch #upgPanel .upgTile { pointer-events: auto; }

/* ════════════════════════════════════════════════════════════════════════
   AT A STATION WITH THE HUD HIDDEN: the console comes back as ONE panel.
   Upgrades are bought on the refit page, and a hidden HUD had no way to reach
   it at all -- the drawer only ever showed abilities. Bringing BOTH panels up
   was the earlier attempt, and it put two framed boxes in the same corner.
   This shows whichever tab is active, exactly as the full HUD does, with the
   ABILITIES / BACK nav tiles to move between them.
   ════════════════════════════════════════════════════════════════════════ */
/* ONE DRAWER, DOCKED OR NOT. The three rules that were here swapped the
   drawer's contents when the ship was docked: #upgPanel in place of
   #commandCard. Two different elements, two different widths -- and the strip
   beside it reserves space from its neighbour's edge, so the whole bottom row
   re-laid itself on docking. That is the last of the "two HUDs".
   Reaching the refit page while docked does not need this: the REFIT button in
   the top corner (#rfOpen, further down) is exactly that, and it is there in
   both states. So the drawer is always the ability icons, and the refit panel
   stays out of it. */
body.hudCollapsed.abilitiesOpen #upgPanel { display: none !important; }

/* ════════════════════════════════════════════════════════════════════════
   ACTIVE EFFECTS on the selected hull.
   Watching your own health climb with nothing on screen saying why is the
   thing these fix — a Warden repairing you, venom eating you, an EMP holding
   your abilities down. One chip per effect; the tooltip carries the rate and
   the time left. Reuses .tip, so it behaves like every other tooltip here.
   ════════════════════════════════════════════════════════════════════════ */
/* Pinned to the TOP RIGHT of the portrait rather than stacked under it, so
   the effects read as badges ON the ship rather than as another stat row. */
#infoLeft { position: relative; }
#unitBuffs {
  position: absolute; top: -3px; right: -3px; z-index: 6;
  display: flex; flex-direction: column; gap: 3px; margin: 0;
}
#unitBuffs:empty { display: none; }
.bfChip {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px;
  font-size: 12px; line-height: 1;
  border: 1px solid currentColor; border-radius: 4px;
  background: rgba(4, 10, 18, .78);
  cursor: help;
}
.bfChip .tip {
  /* .tip is hidden by default and revealed on hover/tipOpen, same as the
     upgrade chips — see the hover rules further up this file.
     BELOW and to the LEFT: these sit in the panel top-right corner, where a
     tooltip placed above or centred runs off the top of the screen. */
  right: 0; left: auto; transform: none;
  top: calc(100% + 8px); bottom: auto;
  width: max-content; max-width: 230px;
}
/* ── ON A PHONE THE CHIPS LIVE IN THE CARD, NOT ON THE SCREEN ────────────
   They were pinned to the top-right corner of the WHOLE CONSOLE, three
   hundred pixels from the ship they describe and directly under the ability
   circles, which is where they went missing.

   Nobody put them there. `#unitBuffs` is absolutely positioned against
   `#infoLeft`, and on touch `#infoLeft` is `display: contents` -- so it has
   no box, cannot be a containing block, and its `position: relative` does
   nothing at all. The chips fell through to the next positioned ancestor,
   which is #console. Measured on a 760x360 handset viewport: the card sits at
   x=125 and the chips landed at x=736, hard against the right edge.

   So on touch they stop floating and take a cell in the card's own grid.
   The right-hand column runs speed on row 1 and the stat icons on row 3;
   row 2 is empty, which is where they now sit -- beside the vitals, on the
   ship they belong to, and nowhere near a thumb button. */
body.touch #unitBuffs {
  position: static;
  grid-column: 3; grid-row: 2;
  flex-direction: row; gap: 4px;
  align-self: center; justify-self: start;
}
body.touch .bfChip { width: 18px; height: 18px; font-size: 11px; }
/* The card is at the bottom of the screen, so a tip hanging below it is a tip
   off the edge of the phone. Opens upward and to the right instead. */
body.touch .bfChip .tip {
  left: 0; right: auto;
  bottom: calc(100% + 8px); top: auto;
}

body:not(.touch) .bfChip:hover .tip { opacity: 1; visibility: visible; }
.bfChip.tipOpen .tip { opacity: 1; visibility: visible; }
@media (hover: hover) and (pointer: fine) {
  .bfChip:hover .tip { opacity: 1; visibility: visible; }
}

/* ════════════════════════════════════════════════════════════════════════
   MINIMISED STRIP: speed ABOVE the icons, and energy in place of kills.

   `body.touch #unitStats .scCard { display: contents }` flattens the card into
   its parent's row, which is why the speed line sat to the LEFT of the stat
   icons rather than over them. Collapsed, the strip lays itself out as a
   column instead.

   Kills are a scoreboard number and belong on the full console. Minimised, the
   glance you want off your own hull is how much energy you have to spend, so
   the two swap. Both are always in the markup; only one is ever shown.
   ════════════════════════════════════════════════════════════════════════ */
body.hudCollapsed #unitStats {
  display: flex !important;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
body.hudCollapsed #unitStats .scCard { display: flex !important; flex-direction: column; align-items: center; gap: 4px; }
.scEnergy { display: none; }
body.hudCollapsed .scKills { display: none; }
body.hudCollapsed .scEnergy { display: inline; }

/* iOS CAMERA HOUSING.

   Only on iOS, and only the LEFT edge. env(safe-area-inset-left) on its own is
   not an iOS signal -- Android reports it in landscape too, which pushed this
   panel well inward there for no reason.

   The BOTTOM is deliberately untouched: #hud is already inset by
   safe-area-inset-bottom, so adding it again here double-counted and lifted the
   map off the line the rest of the HUD sits on. */
body.iosApp #minimapPanel { left: calc(8px + env(safe-area-inset-left, 0px)); }

/* ===================== ROGUE RUN ===================== */
/* The ladder strip. Top-left under the match chrome, small enough to ignore
   while fighting and readable at a glance between shots. */
#rogueHud {
  position: fixed; left: 12px; top: 76px; z-index: 40;
  background: rgba(6, 14, 28, .82); border: 1px solid rgba(120, 200, 255, .35);
  border-radius: 10px; padding: 7px 10px; min-width: 168px;
  font-size: 12px; color: #cfe6ff; pointer-events: auto;
  backdrop-filter: blur(3px);
}
.rogueHudTop { display: flex; align-items: center; gap: 8px; }
.rogueHudTop b { color: #ffd257; letter-spacing: 1px; font-size: 11px; }
.rogueHudTop span { font-weight: 700; font-variant-numeric: tabular-nums; }
.rogueHudTop em { color: #86a8c6; font-style: normal; font-size: 11px; }
#rogueUpsToggle {
  margin-left: auto; background: none; border: 0; color: #86a8c6;
  cursor: pointer; font-size: 13px; line-height: 1; padding: 0 2px;
}
/* The upgrade list is opt-in: collapsed by default so the strip stays a strip. */
#rogueHudUps { display: none; margin: 6px 0 0; padding: 0 0 0 14px; }
#rogueHud.showUps #rogueHudUps { display: block; }
#rogueHudUps li { font-size: 11px; color: #9fd0a0; margin: 2px 0; }
#rogueHudUps li.rogueNone { color: #6b8299; }

/* Draft */
.rogueDraft { text-align: center; max-width: 900px; padding: 0 16px; }
.rogueUpTitle { color: #7fd4ff; font-size: 15px; letter-spacing: 2px; }
.rogueUpSub { color: #ffd257; font-size: 24px; letter-spacing: 3px; margin: 6px 0 18px; font-weight: 700; }
.rogueCards { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
.rogueCard {
  width: 220px; min-height: 150px; padding: 14px; cursor: pointer; text-align: left;
  background: linear-gradient(180deg, rgba(14,30,54,.95), rgba(8,18,34,.95));
  border: 1px solid rgba(120, 200, 255, .35); border-radius: 12px;
  color: #cfe6ff; display: flex; flex-direction: column; gap: 8px;
  transition: transform .12s ease, border-color .12s ease;
}
.rogueCard:hover { transform: translateY(-4px); border-color: #7fd4ff; }
.rogueCard em { font-style: normal; font-size: 10px; letter-spacing: 2px; text-transform: uppercase; color: #86a8c6; }
.rogueCard b { font-size: 16px; color: #fff; }
.rogueCard span { font-size: 12px; color: #9fb4cc; line-height: 1.45; }
/* Rarity reads as colour, so the eye sorts the three cards before reading. */
.rogueCard.r-common { border-color: rgba(120, 200, 255, .3); }
.rogueCard.r-rare { border-color: rgba(122, 162, 255, .6); }
.rogueCard.r-rare em { color: #7aa2ff; }
.rogueCard.r-epic { border-color: rgba(200, 130, 255, .7); }
.rogueCard.r-epic em { color: #c882ff; }
.rogueCard.r-epic b { color: #f0dcff; }

/* Summary */
.rogueEndBox {
  background: rgba(6, 14, 28, .95); border: 1px solid rgba(120, 200, 255, .35);
  border-radius: 14px; padding: 26px 30px; min-width: 380px; max-width: 480px;
}
.rogueEndTitle { font-size: 26px; letter-spacing: 3px; text-align: center; margin-bottom: 18px; font-weight: 700; }
.rogueEndStats { display: flex; flex-direction: column; gap: 7px; }
.rogueStat { display: flex; justify-content: space-between; font-size: 13px; color: #9fb4cc; }
.rogueStat b { color: #cfe6ff; font-variant-numeric: tabular-nums; }
.rogueEndUpsWrap { margin-top: 18px; padding-top: 14px; border-top: 1px solid rgba(120,200,255,.2); }
.rogueEndUpsHead { font-size: 10px; letter-spacing: 2px; color: #86a8c6; margin-bottom: 6px; }
#rogueEndUps { margin: 0; padding-left: 16px; }
#rogueEndUps li { font-size: 12px; color: #9fd0a0; margin: 3px 0; }
.rogueEndBtns { display: flex; gap: 10px; margin-top: 20px; }
.rogueEndBtns button { flex: 1; }
@media (max-width: 700px) {
  .rogueCard { width: 100%; max-width: 320px; min-height: 0; }
  #rogueHud { top: 62px; font-size: 11px; min-width: 140px; }
}


/* Encounter countdown. Big, central, and gone in five seconds — it exists to
   tell you what is coming and give you a beat to breathe before it does. */
.rogueGoBox { text-align: center; }
.rogueGoWave { font-size: 15px; letter-spacing: 5px; color: #ffd257; font-weight: 700; }
.rogueGoType { font-size: 30px; letter-spacing: 6px; color: #eaf6ff; margin-top: 4px; font-weight: 700; }
.rogueGoNum {
  font-size: 92px; line-height: 1; font-weight: 700; color: #7fd4ff;
  margin: 10px 0 6px; font-variant-numeric: tabular-nums;
  text-shadow: 0 0 26px rgba(127, 212, 255, .55);
}
.rogueGoNum.pop { animation: rogueGoPop .42s ease-out; }
@keyframes rogueGoPop {
  from { transform: scale(1.5); opacity: .25; }
  to   { transform: scale(1);   opacity: 1; }
}
.rogueGoBlurb { font-size: 13px; color: #9fb4cc; max-width: 420px; margin: 0 auto; }

/* The build so far, under the draft — evidence that earlier picks are still
   working, which the cards alone never gave. */
.rogueHeld { margin-top: 22px; display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; align-items: center; }
.rogueHeldHead { font-size: 10px; letter-spacing: 2px; color: #86a8c6; margin-right: 4px; }
.rogueHeldChip {
  font-size: 11px; padding: 3px 9px; border-radius: 999px;
  background: rgba(10, 22, 40, .8); border: 1px solid rgba(120, 200, 255, .3); color: #cfe6ff;
}
.rogueHeldChip.r-rare { border-color: rgba(122, 162, 255, .6); color: #cfe0ff; }
.rogueHeldChip.r-epic { border-color: rgba(200, 130, 255, .7); color: #f0dcff; }

/* Ability keys on the countdown. Unresearched ones are dimmed and show what
   they cost, so the panel doubles as "here is what you can still buy". */
.rogueGoKeys { margin-top: 18px; display: flex; flex-wrap: wrap; gap: 7px; justify-content: center; max-width: 640px; }
.rgKey {
  display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px 4px 5px;
  font-size: 12px; color: #cfe6ff; background: rgba(10, 22, 40, .8);
  border: 1px solid rgba(120, 200, 255, .3); border-radius: 999px;
}
.rgKey b {
  display: inline-grid; place-items: center; min-width: 20px; height: 20px;
  border-radius: 5px; background: rgba(127, 212, 255, .18);
  color: #eaf6ff; font-size: 11px; border: 1px solid rgba(127, 212, 255, .45);
}
.rgKey.locked { color: #7f93ab; border-color: rgba(90, 150, 220, .18); }
.rgKey.locked b { background: rgba(120, 150, 180, .12); border-color: rgba(120, 150, 180, .3); color: #9fb4cc; }
.rgKey i { font-style: normal; color: #ffd257; }

/* An unresearched refit tile is LOCKED, not just empty: dimmed art, its price
   in the corner instead of a level, and the same green the ability bar uses
   for "you can buy this". Without it a tile you had never bought looked the
   same as one you owned at level zero. */
.cmd.upg.research { opacity: .55; }
.cmd.upg.research .ico { filter: grayscale(1) brightness(.8); }
.cmd.upg.research .cCost { color: #7fffc0; }
.cmd.upg.research .cLvl { color: #7fffc0; letter-spacing: .5px; }
.cmd.upg.research:not(:disabled):hover { opacity: .85; }

/* ============ Rogue Run draft, dressed ============
   The first version was three plain boxes of text. Same data, now wearing the
   game's own visual language: a rarity-lit frame, corner brackets, a big
   emblem and a glow, so which card is the rare one reads instantly. Rarity is
   carried by currentColor, so one value paints frame, brackets, ring, emblem
   and wash together. */
.rogueUpTitle {
  color: #7fd4ff; font-size: 14px; letter-spacing: 6px;
  text-shadow: 0 0 18px rgba(127, 212, 255, .55);
}
.rogueUpSub {
  color: #ffd257; font-size: 30px; letter-spacing: 7px; font-weight: 700;
  margin: 8px 0 26px; text-shadow: 0 0 26px rgba(255, 210, 87, .45);
}
.rogueUpSub::before, .rogueUpSub::after {
  content: '—'; color: #ffd257; opacity: .5; margin: 0 16px; font-weight: 400;
}
.rogueCards { gap: 20px; }
.rogueCard {
  position: relative; overflow: hidden; width: 236px; min-height: 300px;
  padding: 18px 16px 16px; border-width: 2px; border-radius: 6px;
  background:
    radial-gradient(120% 80% at 50% 118%, rgba(120,200,255,.20), transparent 62%),
    linear-gradient(180deg, rgba(10,26,48,.96), rgba(6,14,28,.98));
  box-shadow: 0 0 0 1px rgba(0,0,0,.5), 0 10px 30px rgba(0,0,0,.45);
  transition: transform .14s ease, box-shadow .14s ease, border-color .14s ease;
}
.rogueCard::before, .rogueCard::after {
  content: ''; position: absolute; width: 22px; height: 22px; pointer-events: none;
  border: 2px solid currentColor; opacity: .85;
}
.rogueCard::before { top: 6px; left: 6px; border-right: 0; border-bottom: 0; }
.rogueCard::after { bottom: 6px; right: 6px; border-left: 0; border-top: 0; }
.rogueCard em { font-style: normal; font-size: 10px; letter-spacing: 3px; text-transform: uppercase; }
.rogueCard b { display: block; font-size: 19px; color: #fff; margin: 8px 0 6px; line-height: 1.15; }
.rcDesc { display: block; font-size: 12px; color: #9fb4cc; line-height: 1.5; }
.rcArt {
  position: absolute; left: 50%; bottom: 26px; transform: translateX(-50%);
  width: 108px; height: 108px; display: grid; place-items: center;
  border-radius: 50%; border: 1px solid currentColor;
  background: radial-gradient(circle, rgba(255,255,255,.10), transparent 68%);
}
.rcArt::after {
  content: ''; position: absolute; inset: -12px; border-radius: 50%;
  border: 1px solid currentColor; opacity: .28;
}
.rcArt .ico { width: 54px; height: 54px; background-size: contain; filter: drop-shadow(0 0 10px currentColor); }
.rcGlow {
  position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(90% 60% at 50% 108%, currentColor, transparent 60%);
  opacity: .16;
}
.rogueCard:hover { transform: translateY(-6px); box-shadow: 0 0 0 1px rgba(0,0,0,.5), 0 16px 40px rgba(0,0,0,.55); }
.rogueCard.r-common { color: #7fd4ff; border-color: rgba(127,212,255,.55); }
.rogueCard.r-common em { color: #7fd4ff; }
.rogueCard.r-rare {
  color: #7aa2ff; border-color: rgba(122,162,255,.75);
  box-shadow: 0 0 0 1px rgba(0,0,0,.5), 0 0 22px rgba(122,162,255,.25), 0 10px 30px rgba(0,0,0,.45);
}
.rogueCard.r-rare em { color: #9db8ff; }
.rogueCard.r-epic {
  color: #d07bff; border-color: rgba(208,123,255,.85);
  box-shadow: 0 0 0 1px rgba(0,0,0,.5), 0 0 34px rgba(208,123,255,.35), 0 10px 30px rgba(0,0,0,.45);
}
.rogueCard.r-epic em { color: #e0a6ff; }
@media (max-width: 760px) {
  .rogueCard { width: 100%; max-width: 320px; min-height: 210px; }
  .rcArt { width: 76px; height: 76px; bottom: 16px; }
  .rcArt .ico { width: 38px; height: 38px; }
}

/* Collapsed HUD: your BUILD, not the catalogue.
   The strip is a glance, so it carries only refits actually bought — an
   unbought track at level 0 is noise there, while the full console still
   lists every track so you can see what the hull can fit. */
/* The refit tiles STAY, at zero or not. Hiding the empty ones emptied the
   whole strip for a fresh ship and took away the four icons that told you
   what your hull can fit. A zero on a tile is information; a missing tile
   is a bug report. */
/* ...and with the empty ones gone the row should hug the abilities button
   rather than sitting centred over a gap. */
body.hudCollapsed #unitStats .scIcons { justify-content: flex-end; }


/* ===========================================================================
   MINIMISED HUD: THE ABILITIES YOU RESEARCHED, IN THE CORNER.
   ---------------------------------------------------------------------------
   There is no drawer any more. A minimised HUD used to collapse every ability
   behind a gold ABILITIES button, so using one cost two taps and the panel it
   opened was mostly things you had not bought -- a menu standing between you
   and your own ship.

   These are the SAME .cmd.ability buttons as the full console, moved rather
   than recreated: they keep their click handlers, hotkey badges, cooldown
   sweeps and energy gating for free. Unresearched (.research) and passive
   (.passiveAbil) ones are hidden, so what is left is exactly the set that
   does something when pressed.

   Late in the file, and !important throughout, because the rules that hid
   these behind .abilitiesOpen are themselves !important.
   ========================================================================= */
body.hudCollapsed:not(.cmdAbilities) #cmdPanel,
body.touch.hudCollapsed:not(.cmdAbilities) #cmdPanel,
/* ANCHORED WHETHER THE DRAWER IS 'OPEN' OR NOT.

   This carried :not(.abilitiesOpen), which was fine while that class only ever
   meant 'the drawer is showing'. It now also means 'the ability bar is out
   because you are flying' -- so the selector stopped matching, the panel lost
   its fixed bottom-right anchor, and the ability tiles floated off into the
   middle of the screen on a minimised HUD.

   Minimised, the ability row belongs in the corner regardless of why it is on
   screen. .cmdAbilities is still excluded: that is the docked console, which
   is a different thing in a different place. */
body.hudCollapsed:not(.cmdAbilities) #cmdPanel {
  display: block !important;
  position: fixed;
  right: 14px;
  bottom: calc(14px + env(safe-area-inset-bottom, 0px));
  left: auto; top: auto;
  width: auto; max-width: min(62vw, 520px);
  padding: 0; margin: 0;
  background: none; border: 0; box-shadow: none;
  pointer-events: auto;
  z-index: 41;
}
/* the panel chrome: title bar, corner brackets */
body.hudCollapsed:not(.cmdAbilities) #cmdPanel::before,
body.hudCollapsed:not(.cmdAbilities) #cmdPanel::after { display: none !important; }

body.hudCollapsed:not(.cmdAbilities) #cmdPanel > #commandCard,
body.touch.hudCollapsed:not(.cmdAbilities) #cmdPanel > #commandCard,
body.hudCollapsed:not(.cmdAbilities):not(.abilitiesOpen) #commandCard,
body.touch.hudCollapsed:not(.cmdAbilities):not(.abilitiesOpen) #commandCard,
body.hudCollapsed:not(.cmdAbilities).abilitiesOpen #commandCard {
  display: flex !important;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 6px;
  padding: 0; margin: 0;
  background: none; border: 0; box-shadow: none;
}

/* Nothing here that cannot be pressed. */
/* ...but at the station, with ABILITIES chosen on the refit console, the
   whole kit has to be visible -- that screen is where unresearched abilities
   are BOUGHT, so hiding them there hid the thing the player came for. */
body.hudCollapsed:not(.cmdAbilities) #commandCard .cmd.ability.research,
body.hudCollapsed:not(.cmdAbilities) #commandCard .cmd.ability.passiveAbil { display: none !important; }

/* The drawer button has nothing left to open. */
body.hudCollapsed #abilityToggle,
body.touch.hudCollapsed #abilityToggle,
body.touch.hudCollapsed:not(.abilitiesOpen) #abilityToggle { display: none !important; }

/* The name label is what makes these tiles wide; in the corner the icon and
   hotkey carry it, and the tooltip still has the full text. */
body.hudCollapsed:not(.cmdAbilities) #commandCard .cmd.ability .cName { display: none !important; }
body.hudCollapsed:not(.cmdAbilities) #commandCard .cmd.ability {
  width: 54px; height: 54px; min-width: 0; padding: 4px;
}
/* Smaller than the console tiles, not bigger. These sit over the playfield in
   the corner of a phone screen, where they were eating the bottom-right of the
   map; 44px is still comfortably above the 40px minimum tap target. */
body.touch.hudCollapsed:not(.cmdAbilities) #commandCard .cmd.ability { width: 44px; height: 44px; padding: 3px; }
body.touch.hudCollapsed #commandCard .cmd.ability .aico,
body.touch.hudCollapsed #commandCard .cmd.ability .ico { width: 26px; height: 26px; }
body.touch.hudCollapsed #commandCard .cmd.ability b { font-size: 9px; }
body.touch.hudCollapsed #commandCard { gap: 5px; }

/* An ability that is currently aiming says so, because pressing it again is
   now what cancels it. */
body.hudCollapsed:not(.cmdAbilities) #commandCard .cmd.ability.targeting {
  outline: 2px solid var(--hud-gold);
  outline-offset: -2px;
}

/* Speed on the full console, vitals on the collapsed strip -- never both. */
.scVitals { display: none; }
body.hudCollapsed .scVitals { display: inline; }
body.hudCollapsed .scSpeed { display: none; }


/* ════════════════════════════════════════════════════════════
   THE CAMERA CUTOUT
   ────────────────────────────────────────────────────────────
   In landscape the notch eats a strip down one side, and iOS reports it as
   safe-area-inset-left. Only the minimap was accounting for it, so every
   other left-anchored control -- the self button, the cancel button, the unit
   card, the comms stack -- still started at a flat 8px and sat under the
   cutout. iosApp only: env() resolves to 0 elsewhere anyway, but the class
   keeps the rule out of desktop layouts entirely.
   ════════════════════════════════════════════════════════════ */
body.iosApp #btnSelf { left: calc(8px + env(safe-area-inset-left, 0px)); }
body.iosApp #btnCancel { left: calc(110px + env(safe-area-inset-left, 0px)); }
/* Only where the card is ANCHORED LEFT. At tablet sizes it is centred instead
   (left:50% with a -50% shift), and this rule -- later in the file and more
   specific -- put the 150px anchor back while leaving the shift in place,
   which is precisely the bug that dragged the card off the left edge of an
   iPad. Same failure as before: half a positioning rule. */
@media (max-width: 900px), (max-height: 620px) {
  body.iosApp.touch #infoPanel { left: calc(150px + env(safe-area-inset-left, 0px)); }
}
/* The chat block (and the ping button inside it) sat 150px in on iOS and 12px
   in everywhere else, so ping ended up near the middle of the screen on an
   iPhone and in the corner on Android. The 150 was meant to clear the trackpad
   in a layout that no longer exists, and the safe-area inset then added ~44px
   more in landscape on top of it. Same left edge as every other platform now,
   plus whatever the notch genuinely needs. */
body.iosApp.touch #chatWrap { left: calc(12px + min(env(safe-area-inset-left, 0px), 10px)); }
body.iosApp #moveStick { left: calc(8px + env(safe-area-inset-left, 0px)); }


/* ════════════════════════════════════════════════════════════
   HULL PORTRAIT
   ────────────────────────────────────────────────────────────
   Sits between the unit card and the abilities on the FULL hud only. Hidden
   when the hud is minimised: that state exists to give the battlefield back,
   and a character animation is the first thing that should go.
   pointer-events off throughout -- it is decoration, and it must never eat a
   click meant for the console behind it.
   ════════════════════════════════════════════════════════════ */
#hudChar {
  position: absolute;
  /* 8px on the row; raised above the unit card when the row is too tight (applyPortrait) */
  bottom: var(--portrait-bottom, 8px);
  /* Follows whichever console panel is on screen -- see applyPortrait. */
  right: var(--portrait-right, 372px);
  height: var(--portrait-h, 190px);
  /* The FRAME's shape, not the clip's. Width comes from the aspect ratio and
     object-fit crops the video into it, so a 9:16 recording can sit in a 2:3
     hole without being squashed or shrunk to nothing. */
  aspect-ratio: var(--portrait-ar, 2 / 3);
  width: auto;
  object-position: var(--portrait-focus, 50% 22%);
  pointer-events: none;
  z-index: 6;
  /* No plate behind it: a clip with a transparent background should sit on
     the starfield, and one without already brings its own frame. */
  background: none;
  filter: drop-shadow(0 0 12px rgba(0, 0, 0, .75));
}
/* THE SAME PLATE AS EVERY OTHER PANEL. On its own the clip was a bare
   rectangle floating in the starfield -- the only thing on the HUD without an
   edge, which read as a video dropped on top of the game rather than part of
   it. Frame art (hudFramed) overrides these, exactly as it does for the unit
   card. */
#hudChar {
  border: var(--hud-border-w, 1px) solid var(--hud-line);
  border-radius: var(--hud-radius, 8px);
  background: linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 12px var(--hud-glow),
              inset 0 1px 0 rgba(140, 210, 250, .18);
  object-fit: cover;
}
#hudChar.hidden { display: none !important; }
body.hudCollapsed #hudChar { display: none !important; }
/* SHOWN ON PHONES TOO.

   This was display:none below 900px on the reasoning that there was no room
   beside the abilities -- but the position and the size are now yours to set
   in the HUD layout editor, per device type, so 'no room' is a judgement the
   editor makes better than a hard-coded breakpoint. It still hides with the
   HUD when minimised, and it is still admin-gated by the config. */
@media (max-width: 900px) {
  #hudChar { max-width: 40vw; }
}


/* ════════════════════════════════════════════════════════════
   HUD LAYOUT EDIT MODE
   ────────────────────────────────────────────────────────────
   Every movable panel is outlined and named, and the whole HUD stops being
   interactive -- in this mode the panels are furniture, so a press has to
   reach the drag handler rather than the button it landed on.
   ════════════════════════════════════════════════════════════ */
/* ARRANGE WHAT IS ACTUALLY ON SCREEN.

   Forcing every panel visible was tried and reverted: a panel the game has
   hidden has nothing in it, so edit mode filled the screen with an empty
   ability grid, a card reading NO UNIT SELECTED and a refit console with no
   stock -- all overlapping, none of it representing what the panel looks like
   in use. Arranging a panel by its empty shell is worse than not arranging it.

   So: dock to place the refit console, select something to place the unit
   card. What you drag is the real panel with its real contents at its real
   size, which is the only way the result means anything. */
body.layoutEdit #minimapPanel,
body.layoutEdit #infoPanel,
body.layoutEdit #cmdPanel,
body.layoutEdit #upgPanel,
body.layoutEdit #hudChar,
body.layoutEdit #tutBox,
body.layoutEdit #moveStick,
body.layoutEdit #pingToggle,
body.layoutEdit #killBanners,
body.layoutEdit #streak {
  outline: 2px dashed rgba(120, 220, 255, .85);
  outline-offset: 3px;
  cursor: move;
  pointer-events: auto !important;
}
body.layoutEdit #minimapPanel *,
body.layoutEdit #infoPanel *,
body.layoutEdit #cmdPanel *,
body.layoutEdit #upgPanel *,
body.layoutEdit #hudChar *,
body.layoutEdit #tutBox *,
body.layoutEdit #moveStick *,
body.layoutEdit #pingToggle *,
body.layoutEdit #killBanners *,
body.layoutEdit #streak * { pointer-events: none !important; }
/* THE SLATE IS USUALLY EMPTY, and an empty box cannot be grabbed. While the
   editor is open it keeps a banner-sized footprint so there is something to
   take hold of and the dashed outline has an area to draw around. */
body.layoutEdit #killBanners {
  min-width: 200px;
  min-height: 46px;
}
body.layoutEdit #killBanners::before {
  content: 'KILL BANNERS';
  font: 700 11px/46px ui-monospace, monospace;
  letter-spacing: 2px;
  color: rgba(150, 225, 255, .55);
}
body.layoutEdit [data-layout-label]::after {
  content: attr(data-layout-label);
  position: absolute; top: -20px; left: 0;
  font: 700 10px/1 ui-monospace, monospace; letter-spacing: 1px;
  color: #071018; background: rgba(120, 220, 255, .9);
  padding: 3px 6px; border-radius: 3px; white-space: nowrap;
}
#layoutBar {
  position: fixed; top: 10px; left: 50%; translate: -50% 0;
  z-index: 9500; display: flex; gap: 10px; align-items: center;
  padding: 8px 14px; border-radius: 8px;
  background: linear-gradient(180deg, rgba(10,20,32,.97), rgba(4,10,18,.98));
  border: 1px solid var(--hud-line-hi, #6cc);
  box-shadow: 0 6px 24px rgba(0,0,0,.6);
  color: #cfe9ff; font: 600 12px ui-monospace, monospace; letter-spacing: 1px;
}
#layoutBar.hidden { display: none !important; }
#layoutBar .lbHint { color: #7fa0b8; font-weight: 400; letter-spacing: 0; }
/* Which layout is being written. The editor writes to a different set per HUD
   state, and that has to be visible or a save looks like it went nowhere. */
#layoutBar .lbState {
  color: #071018; background: var(--hud-gold, #eab543);
  padding: 3px 8px; border-radius: 4px; font-size: 11px; letter-spacing: 1px;
}
#layoutBar button {
  background: #0e1b28; color: #cfe9ff; border: 1px solid #24455e;
  padding: 5px 11px; border-radius: 5px; font: inherit; cursor: pointer;
}
#layoutBar button:hover { border-color: var(--hud-line-hi, #6cc); color: #fff; }

/* ══════════════════════════════════════════════════════════════════════════
   THE EDITOR HAS TO WORK ON THE PHONE

   It is the device whose layout most needs arranging, and it was the one
   device the editor could not be used on. Two reasons, both here:

   1. The bar is one flex row of eight controls with no wrap, pinned to the
      middle. On a 375px handset it is far wider than the screen, so Save,
      Reset all and Done were simply off the edge with no way to reach them.
   2. Nothing set touch-action on the panels being dragged. A pointerdown on
      a touchscreen is provisional -- the browser takes the gesture for its
      own panning and CANCELS the pointer stream -- so drags died partway.
   ══════════════════════════════════════════════════════════════════════════ */
#layoutBar {
  flex-wrap: wrap;
  justify-content: center;
  max-width: calc(100vw - 16px);
  box-sizing: border-box;
}
body.touch #layoutBar {
  /* Below the top row of HUD buttons rather than under them, and tap targets
     a finger can actually hit. */
  top: calc(8px + env(safe-area-inset-top, 0px));
  gap: 6px;
  padding: 8px 10px;
  font-size: 11px;
}
body.touch #layoutBar button { padding: 9px 13px; font-size: 12px; }
/* The hint text is the first thing to go when room is short -- the buttons
   are the part you cannot do without. */
@media (max-width: 560px) { #layoutBar .lbHint { display: none; } }

/* A SHORT SCREEN IS THE ONE WHERE THIS MATTERS MOST.
   Measured on a 760x360 handset: eleven wrapped buttons made the bar 132px
   tall -- more than a third of the display, sitting on top of the very panels
   it exists to arrange. The streak counter was inside it end to end and could
   not be grabbed at all. Landscape phones get the compact set. */
@media (max-height: 460px) {
  #layoutBar, body.touch #layoutBar {
    gap: 4px; padding: 4px 6px; font-size: 9px; letter-spacing: 0;
    top: calc(4px + env(safe-area-inset-top, 0px));
  }
  #layoutBar > b { font-size: 9px; }
  #layoutBar .lbHint, #layoutBar .lbState { display: none; }
  #layoutBar button, body.touch #layoutBar button { padding: 4px 7px; font-size: 10px; }
}

/* THE DRAG ITSELF. Without this the browser pans instead and the pointer
   stream is cancelled mid-gesture, which reads as a panel that will not move.
   Scoped to edit mode so normal play keeps its scrolling and its taps. */
body.layoutEdit #minimapPanel,
body.layoutEdit #infoPanel,
body.layoutEdit #cmdPanel,
body.layoutEdit #upgPanel,
body.layoutEdit #hudChar,
body.layoutEdit #tutBox,
body.layoutEdit #moveStick,
body.layoutEdit #pingToggle,
body.layoutEdit #streak { touch-action: none !important; }



/* ════════════════════════════════════════════════════════════
   THE TWO CONSOLES ARE ONE SIZE
   ────────────────────────────────────────────────────────────
   The refit console and the abilities console occupy the same corner and swap
   as you dock. Matching their GRIDS was not enough -- they carry different
   padding and different headers, so the panels still came out 438 and 485 wide
   and everything beside them moved on every dock.

   Both panels are now one fixed width. Whichever is showing occupies exactly
   the same rectangle, so docking changes nothing about the layout and a saved
   arrangement is correct in every state. That is what makes the HUD
   stationary; matching heights alone never could.
   ════════════════════════════════════════════════════════════ */


/* ══════════════════════════════════════════════════════════════════════════
   THE TWO CONSOLES ARE ONE BOX

   Refit (#upgPanel) and Abilities (#cmdPanel) are two elements that have to
   look and sit like a single unchanging box, because docking swaps which one
   is on screen and ANY difference between them reads as the HUD jumping.

   They differed because their size was set in ELEVEN places scattered through
   this file -- 62px, 68px, 61px, 53px, 58px, 66px, 75px -- and the two panels
   did not land on the same ones. #upgPanel also carried its own bottom (122px
   against 8px) and its own padding. Every previous attempt at this added a
   twelfth rule to the end of the pile instead of removing the pile. Those
   rules are deleted now and this is the only place either console is sized.

   FOR ANYONE EDITING THIS LATER: never write a selector that sizes one of
   these two and not the other. Change the variables.
   ══════════════════════════════════════════════════════════════════════════ */

body:not(.touch) {
  /* SQUARE. The icons are square, so a 75x62 tile stretched them -- which is
     what happens when you set a width without looking at the height that was
     already there. */
  --tile-w: 68px;
  --tile-h: 68px;
  --tile-gap: 6px;
  --console-pad-x: 11px;
  --console-head: 20px;   /* fixed, so two unlike headers cost the same room */
  /* Derived from the tiles rather than guessed: six columns, two rows.
     This is the CONTENT box only -- see box-sizing below. */
  --console-w: calc(6 * var(--tile-w) + 5 * var(--tile-gap));
  --console-h: calc(2 * var(--tile-h) + var(--tile-gap) + var(--console-head));
}
/* Touch is NOT sized here. Mobile keeps the rules it has always had,
   further up this file -- this box is a desktop change only. */

/* ...and NOT when the HUD is minimised. This forces a two-row console box
   (--console-h) with !important, which is why the minimised panel kept coming
   out 126px tall with one icon sitting at the top of it. Minimised is one row
   of icons; it is not this box. */
body:not(.touch):not(.hudCollapsed) #cmdPanel,
body:not(.touch):not(.hudCollapsed) #upgPanel {
  position: absolute !important;
  right: 8px !important; bottom: 8px !important; left: auto !important; top: auto !important;
  width: var(--console-w) !important;
  height: var(--console-h) !important;
  padding: 9px var(--console-pad-x) 11px !important;
  /* CONTENT-BOX, and this is not a style preference.

     body.hudFramed draws these panels with a 12.8px picture frame. Under
     border-box a fixed 460px width has to pay for that frame plus the padding
     out of its own 460, which left 412 for the tiles -- six 68px columns need
     438, so the sixth wrapped away and the console looked a size smaller than
     it was asked to be. Measured, not assumed: computed border came back as
     12.8px per side.

     Sizing the CONTENT instead means the frame and the padding are added
     outside the number, so the tile area is exactly what it says. The two
     panels still match because they take their border from the same hudFramed
     rule and their padding from the line above. */
  box-sizing: content-box !important;
}

/* Both headings take the same strip, whether they are a real element
   (.upgHead) or a pseudo-element (#cmdPanel::before). */
body:not(.touch) .upgHead, body:not(.touch) #cmdPanel::before {
  height: var(--console-head) !important;
  line-height: var(--console-head) !important;
  margin: 0 0 var(--tile-gap) !important;
  padding: 0 !important;
  box-sizing: border-box !important;
}

body:not(.touch) #upgradeGrid, body:not(.touch) #commandCard {
  display: grid !important;
  /* auto-fill wraps a surplus tile onto the next row instead of slicing it in
     half, which is what a hard column count did. */
  grid-template-columns: repeat(auto-fill, var(--tile-w)) !important;
  grid-auto-rows: var(--tile-h) !important;
  gap: var(--tile-gap) !important;
  justify-content: start !important;
  align-content: start !important;
  /* visible: a selected tile draws its glow outside its own box */
  overflow: visible !important;
}

body:not(.touch) #upgradeGrid .cmd, body:not(.touch) #commandCard .cmd {
  width: var(--tile-w) !important;
  height: var(--tile-h) !important;
  min-width: 0 !important;
  box-sizing: border-box !important;
}

/* The collapsed dock is a bare row of buttons, not a console -- the one place
   either panel is allowed to be a different shape. */
body:not(.touch).hudCollapsed:not(.cmdAbilities) #cmdPanel { width: auto !important; height: auto !important; }

/* ══════════════════════════════════════════════════════════════════════════
   DOCKED -> ABILITIES ON A PHONE

   Pressing ABILITIES in the docked console added .cmdAbilities, which hid
   #upgPanel and showed #cmdPanel -- but on touch the GRID inside it,
   #commandCard, is display:none !important unless .abilitiesOpen is set,
   and that class belongs to the minimised drawer, not the docked console.
   So the panel appeared and was empty: from the outside, the refit box just
   vanished. The class that shows the panel and the class that fills it were
   two different switches and only one of them was being thrown.
   ══════════════════════════════════════════════════════════════════════════ */
body.touch.atBase.cmdAbilities #cmdPanel { display: block !important; }
body.touch.atBase.cmdAbilities #commandCard { display: grid !important; }
body.touch.atBase.cmdAbilities #cmdPanel > #commandCard { display: grid !important; }
/* and the way back to refit */
body.touch.atBase.cmdAbilities #toUpgrades { display: flex !important; }

/* ══════════════════════════════════════════════════════════════════════════
   ONE ABILITIES PANEL AT A TIME, AND IT ALWAYS HAS ITS BOX

   #cmdPanel plays two parts on a phone: a lone button when the abilities are
   put away, and a full console when they are out. The rules that strip it back
   to a button asked only whether the minimised DRAWER was open
   (:not(.abilitiesOpen)) -- so the DOCKED abilities page, which is
   .cmdAbilities, was served as a button: no plate, no border, no heading, with
   a grid of tiles floating on the battlefield. Those rules now exclude both
   ways of showing abilities (above).

   The other half: while docked on the REFIT page the panel was forced visible
   so its button stayed reachable, and it sat behind the refit console as an
   empty framed box with a stray ABILITIES heading. It is not needed there --
   the refit grid carries its own ABILITIES tile, which is the way in.
   ══════════════════════════════════════════════════════════════════════════ */
body.touch.atBase.hasSelection:not(.cmdAbilities):not(.hudCollapsed) #cmdPanel {
  display: none !important;
}

/* ══════════════════════════════════════════════════════════════════════════
   THE SAME TREATMENT ON A PHONE: TWO ROWS, ONE SIZE

   Desktop got this earlier; mobile was deliberately left on its old rules
   after a revert. Now both consoles are one unchanging box here too.

   FIVE columns, not four. Refit carries eight upgrades plus its ABILITIES
   tile -- nine -- and at four across that is three rows, which is what made
   the docked box taller than the abilities box. Five across is 5+4 for refit
   and 5+3 for abilities: two rows each, the same box.

   5 x 53 + 4 x 4 = 281px of tiles, comfortably inside a 375px handset.

   Scoped to the states where the panel IS a console. #cmdPanel is a bare
   button when the abilities are put away, and it must stay one.
   ══════════════════════════════════════════════════════════════════════════ */
body.touch {
  --tile-w: 53px;
  --tile-h: 53px;
  --tile-gap: 4px;
  --console-pad-x: 8px;
  --console-head: 16px;
  --console-w: calc(5 * var(--tile-w) + 4 * var(--tile-gap));
  --console-h: calc(2 * var(--tile-h) + var(--tile-gap) + var(--console-head));
}
body.touch #upgPanel,
body.touch.cmdAbilities #cmdPanel,
body.touch.abilitiesOpen #cmdPanel {
  width: var(--console-w) !important;
  height: var(--console-h) !important;
  padding: 6px var(--console-pad-x) 8px !important;
  /* content-box for the same reason as desktop: the hudFramed border is
     12.8px a side and would otherwise eat the tile area. */
  box-sizing: content-box !important;
}
body.touch .upgHead,
body.touch #cmdPanel::before {
  height: var(--console-head) !important;
  line-height: var(--console-head) !important;
  margin: 0 0 var(--tile-gap) !important;
  padding: 0 !important;
  box-sizing: border-box !important;
}
body.touch #upgradeGrid,
body.touch #commandCard {
  grid-template-columns: repeat(auto-fill, var(--tile-w)) !important;
  grid-auto-rows: var(--tile-h) !important;
  gap: var(--tile-gap) !important;
  justify-content: start !important;
  align-content: start !important;
  overflow: visible !important;
}
body.touch #upgradeGrid .cmd,
body.touch #commandCard .cmd {
  width: var(--tile-w) !important;
  height: var(--tile-h) !important;
  min-width: 0 !important;
  box-sizing: border-box !important;
}
/* The put-away drawer keeps hugging its button -- it is not a console. */
body.touch:not(.abilitiesOpen):not(.cmdAbilities) #cmdPanel {
  width: auto !important;
  height: auto !important;
}

/* ══════════════════════════════════════════════════════════════════════════
   THE UNIT CARD KEEPS ITS WIDTH ON DESKTOP

   Trimming it to its contents was asked for on MOBILE, where the strip is
   fought over. On a desktop there is nothing else competing for that space
   and the wider plate reads better -- the stat block and the weapon tiles
   have room to breathe instead of being crushed against the hull thumbnail.

   Touch keeps the content width: there, every pixel is contested and the
   runtime layout hands the surplus to the console and the portrait.
   ══════════════════════════════════════════════════════════════════════════ */
body:not(.touch) #infoMain { min-width: 380px; }

/* ══════════════════════════════════════════════════════════════════════════
   THE PHONE'S STAT LINE IS SHORTER

   'Move Speed: 60 (Slow) · Kills: 0 (0◆)' barely fits the card on a handset.
   The words that survive are the ones carrying the number; the qualifier and
   the earnings are detail the desktop has room for and the phone does not.

   Hidden here rather than written differently in the builder, so there is one
   line of markup and no second copy to drift out of step with this one.
   ══════════════════════════════════════════════════════════════════════════ */
body.touch #unitStats .scSpeed .scMove,
body.touch #unitStats .scSpeed em,
body.touch #unitStats .scKills em { display: none !important; }

/* ══════════════════════════════════════════════════════════════════════════
   THE STACKED CARD IS COMPACT, NOT WIDE

   When the card cannot fit beside the console it goes above it -- and up
   there it had the whole width to itself, so max-content took all of it: a
   333px card on a 375px screen, three columns in one long row.

   Above the console the scarce thing is width, not height, so the weapon
   tiles drop to their own row under the text. Same information, about half
   the width, and the battlefield gets the rest back.
   ══════════════════════════════════════════════════════════════════════════ */
body.touch.cardStacked #infoPanel {
  grid-template-columns: max-content max-content !important;
  row-gap: 3px !important;
}
/* The third column has to be RE-PLACED, not just removed from the template.
   Two items are pinned to `grid-column: 3`, and an explicit placement creates
   the track whatever the template says -- which is why cutting the template to
   two columns still measured three. */
body.touch.cardStacked #unitStats .scLine {
  grid-column: 2 !important;
  grid-row: 3 !important;
}
body.touch.cardStacked #unitStats .scIcons {
  grid-column: 1 / -1 !important;
  grid-row: 4 !important;
}

/* ══════════════════════════════════════════════════════════════════════════
   PING SURVIVES THE MINIMISED HUD

   #chatWrap is hidden wholesale when the HUD is minimised, and the ping button
   lives inside it, so minimising took the ping away too. The original reason
   was sound -- a PING floating in empty space over the battlefield is the
   opposite of hiding the HUD -- but it is a placeable part now, so where it
   sits is your decision rather than an accident.

   The WRAP comes back, not its contents: the log, the quick-phrase list and
   the entry box stay hidden, and of the two buttons only PING is shown. A
   hidden parent cannot show a visible child, which is why this has to reach
   in rather than simply un-hiding the button.
   ══════════════════════════════════════════════════════════════════════════ */
body.hudCollapsed #chatWrap {
  display: block !important;
  pointer-events: none;   /* the wrap is a 54vw slab; it must not eat taps */
}
/* ...and #quickList stays, because it IS the ping menu. Hiding everything but
   the button left PING opening a list that was display:none -- pressing it did
   nothing at all, on desktop and phone alike. The list has its own .hidden
   class, toggled by setQuickOpen, which is what decides whether it shows. */
body.hudCollapsed #chatWrap > *:not(#pingToggle) { display: none !important; }
body.hudCollapsed #pingToggle { display: block !important; pointer-events: none; }
body.hudCollapsed #quickList:not(.hidden) { display: flex !important; pointer-events: auto; }
body.touch.hudCollapsed #quickList,
body.touch.hudCollapsed #quickList.hidden,
body.mobilePings.hudCollapsed #quickList,
body.mobilePings.hudCollapsed #quickList.hidden { display: flex !important; pointer-events: none; }
body.hudCollapsed #chatQuick {
  display: flex !important;
  pointer-events: none;
}
body.hudCollapsed #chatQuick > *:not(#pingMenuToggle) { display: none !important; }

/* The docked button is outside #hud. The hangar owns the whole screen, so it
   is never valid there even if a stale class somehow survives another exit. */
body.hangarLive #rfOpen,
body.hangarLive #refitScreen { display: none !important; }

/* The effect chips take taps in their own right, whatever a parent has said
   about pointer-events -- on a phone this is the only way to ask what a
   temporary effect is. */
.bfChip { pointer-events: auto; }

/* Current enemy target: the bars that matter during combat. */
#targetBar {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 2px 0; border-radius: 0;
  border: 0;
  background: transparent;
  font: 700 10px ui-monospace, monospace; letter-spacing: 1px; color: #cfe9ff;
}
#targetBar.hidden { display: none !important; }
#targetName { max-width: 104px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #ffb0a8; }
#targetBar .targetVital {
  position: relative; display: block; width: 112px; height: 13px; border-radius: 1px;
  background: rgba(3,8,14,.9); overflow: hidden; border: 1px solid rgba(160,205,235,.22);
}
#targetBar .targetVital > span { display:block; height:100%; width:0; transition:width .18s linear; }
#targetHullFill { background:linear-gradient(90deg,#a91818,#ff5a4f); }
#targetShieldFill { background:linear-gradient(90deg,#176fbd,#52c8ff); }
#targetBar .targetVital b {
  position:absolute; inset:0; display:grid; place-items:center; color:#fff;
  font:700 7px/1 ui-monospace,monospace; letter-spacing:0; text-shadow:0 1px 2px #000;
}
body.touch #targetBar { padding:1px 0; font-size:9px; gap:2px; }
body.touch #targetBar .targetVital { width:82px; height:11px; }
body.touch #targetName { max-width:72px; }

/* ══════════════════════════════════════════════════════════════════════════
   LEVEL UP
   Centre of the screen, three seconds, gone. Everything is transform and
   opacity so it costs nothing mid-fight. pointer-events none throughout: it
   is a celebration, and it must never eat a tap meant for the battle.
   ══════════════════════════════════════════════════════════════════════════ */
#lvlUp {
  position: fixed; inset: 0; z-index: 95; pointer-events: none;
  display: grid; place-items: center; overflow: hidden;
  opacity: 0; transition: opacity .25s ease;
}
#lvlUp.in { opacity: 1; }
#lvlUp.out { opacity: 0; transition: opacity .6s ease; }
/* a white-gold blink over the whole screen, over in a quarter second */
#lvlUp .luFlash {
  position: absolute; inset: 0;
  background: radial-gradient(60% 60% at 50% 50%, rgba(255, 236, 170, .55), rgba(255, 200, 90, .18) 40%, transparent 70%);
  opacity: 0;
}
#lvlUp.in .luFlash { animation: luFlash .55s ease-out 1; }
@keyframes luFlash { 0% { opacity: 0; } 12% { opacity: 1; } 100% { opacity: 0; } }
/* god-rays turning slowly behind the card */
#lvlUp .luRays {
  position: absolute; width: 140vmax; height: 140vmax; border-radius: 50%;
  background: repeating-conic-gradient(from 0deg, rgba(255, 215, 120, .16) 0deg 6deg, transparent 6deg 18deg);
  -webkit-mask: radial-gradient(circle, #000 0%, rgba(0,0,0,.6) 22%, transparent 48%);
  mask: radial-gradient(circle, #000 0%, rgba(0,0,0,.6) 22%, transparent 48%);
  opacity: 0; transform: scale(.6) rotate(0deg);
}
#lvlUp.in .luRays { animation: luRays 3.6s ease-out 1 forwards; }
@keyframes luRays {
  0%   { opacity: 0; transform: scale(.6) rotate(0deg); }
  15%  { opacity: 1; }
  100% { opacity: .55; transform: scale(1) rotate(28deg); }
}
/* two shock rings racing out */
#lvlUp .luRing {
  position: absolute; width: 120px; height: 120px; border-radius: 50%;
  border: 3px solid rgba(255, 220, 130, .95);
  box-shadow: 0 0 24px rgba(255, 200, 90, .8), inset 0 0 18px rgba(255, 200, 90, .5);
  opacity: 0; transform: scale(.2);
}
#lvlUp.in .luRing { animation: luRing 1.1s cubic-bezier(.16, .84, .3, 1) .05s 1; }
#lvlUp.in .luRing2 { animation-delay: .22s; border-color: rgba(120, 200, 255, .9); box-shadow: 0 0 24px rgba(90, 180, 255, .8); }
@keyframes luRing { 0% { opacity: 0; transform: scale(.2); } 10% { opacity: 1; } 100% { opacity: 0; transform: scale(9); } }
/* sparks: streaks flung outward, each on its own heading */
#lvlUp .luSparks { position: absolute; width: 0; height: 0; }
#lvlUp .luSparks i {
  position: absolute; left: 0; top: -1.5px; height: 3px; width: var(--w, 24px); border-radius: 2px;
  background: linear-gradient(90deg, transparent, #ffe6a8 60%, #fff);
  box-shadow: 0 0 8px rgba(255, 210, 110, .9);
  transform-origin: 0 50%;
  transform: rotate(var(--a)) translateX(0) scaleX(.2);
  opacity: 0;
}
#lvlUp.in .luSparks i { animation: luSpark 1s cubic-bezier(.1, .8, .3, 1) var(--t, 0s) 1; }
@keyframes luSpark {
  0%   { opacity: 0; transform: rotate(var(--a)) translateX(30px) scaleX(.2); }
  12%  { opacity: 1; }
  100% { opacity: 0; transform: rotate(var(--a)) translateX(var(--d)) scaleX(1); }
}
/* the card: kicker, the big number, a bar, what's next */
#lvlUp .luCard {
  position: relative; text-align: center; padding: 26px 54px 24px;
  border-radius: 16px;
  border: 1px solid rgba(255, 215, 120, .55);
  background: linear-gradient(180deg, rgba(20, 26, 44, .92), rgba(6, 9, 18, .96));
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 60px rgba(255, 190, 80, .35), 0 24px 80px rgba(0, 0, 0, .6);
  transform: scale(.6) translateY(30px); opacity: 0;
}
#lvlUp.in .luCard { animation: luCard .7s cubic-bezier(.2, 1.4, .3, 1) .08s 1 forwards; }
@keyframes luCard { 0% { transform: scale(.6) translateY(30px); opacity: 0; } 100% { transform: none; opacity: 1; } }
#lvlUp .luKicker {
  font: 800 12px/1 ui-monospace, monospace; letter-spacing: 6px; color: #ffd98a;
  text-shadow: 0 0 12px rgba(255, 200, 90, .7);
}
#lvlUp .luLevel { display: flex; align-items: baseline; justify-content: center; gap: 10px; margin: 10px 0 8px; }
#lvlUp .luLv { font: 800 26px/1 ui-monospace, monospace; letter-spacing: 4px; color: #cfe9ff; opacity: .8; }
#lvlUp .luNum {
  font: 900 104px/1 ui-monospace, monospace; letter-spacing: -2px; color: #fff;
  background: linear-gradient(180deg, #fff 0%, #ffe9b0 45%, #ffb545 100%);
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 0 18px rgba(255, 200, 90, .85)) drop-shadow(0 4px 0 rgba(120, 70, 0, .5));
  transform: scale(.4); opacity: 0;
}
#lvlUp.in .luNum { animation: luNum .8s cubic-bezier(.2, 1.6, .3, 1) .22s 1 forwards; }
@keyframes luNum { 0% { transform: scale(.4); opacity: 0; } 60% { opacity: 1; } 100% { transform: none; opacity: 1; } }
#lvlUp .luBar { width: 260px; height: 6px; margin: 4px auto 10px; border-radius: 3px; background: rgba(120, 190, 250, .18); overflow: hidden; }
#lvlUp .luBar span { display: block; height: 100%; width: 0; background: linear-gradient(90deg, #3aa0ff, #ffe6a8); box-shadow: 0 0 10px rgba(255, 220, 140, .8); }
#lvlUp.in .luBar span { animation: luBar 1s ease-out .35s 1 forwards; }
@keyframes luBar { 0% { width: 0; } 100% { width: 100%; } }
#lvlUp .luNext { font: 600 12px/1.2 ui-monospace, monospace; letter-spacing: 1px; color: #9fc0d8; }
body.touch #lvlUp .luCard { padding: 18px 30px 16px; }
body.touch #lvlUp .luNum { font-size: 76px; }
body.touch #lvlUp .luBar { width: 200px; }

/* ══════════════════════════════════════════════════════════════════════════
   MOVEMENT INTRO -- full screen, once, before a touch player's first launch.
   Two cards that demonstrate as well as describe; the whole card is the
   button. Nothing else on screen competes with it.
   ══════════════════════════════════════════════════════════════════════════ */
#moveIntro {
  position: fixed; inset: 0; z-index: 120;
  display: grid; place-items: center; padding: 18px;
  padding-top: max(18px, env(safe-area-inset-top)); padding-bottom: max(18px, env(safe-area-inset-bottom));
  background: radial-gradient(120% 90% at 50% 0%, rgba(20, 40, 80, .85), rgba(4, 7, 14, .97) 60%);
  backdrop-filter: blur(6px);
  overflow-y: auto;
}
#moveIntro.hidden { display: none !important; }
#moveIntro .miInner { width: min(920px, 100%); text-align: center; }
#moveIntro .miKicker { font: 800 11px/1 ui-monospace, monospace; letter-spacing: 5px; color: #7fd4ff; }
#moveIntro .miTitle { margin: 10px 0 6px; font-size: clamp(22px, 4.5vw, 34px); letter-spacing: 1px; color: #fff; }
#moveIntro .miSub { margin: 0 0 18px; color: #9fc0d8; font-size: 13px; }
#moveIntro .miCards { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; }
#moveIntro .miCard {
  display: flex; flex-direction: column; gap: 10px; text-align: left;
  padding: 16px 16px 14px; border-radius: 14px; cursor: pointer; font: inherit; color: inherit;
  border: 1px solid rgba(120, 190, 250, .35);
  background: linear-gradient(180deg, rgba(14, 24, 40, .92), rgba(6, 10, 18, .96));
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 12px 40px rgba(0, 0, 0, .5);
  transition: transform .15s ease, border-color .15s ease, box-shadow .15s ease;
}
#moveIntro .miCard:hover, #moveIntro .miCard:focus-visible { transform: translateY(-2px); border-color: #ffd98a; box-shadow: 0 0 0 1px rgba(2,7,14,.9), 0 0 28px rgba(255, 200, 90, .35); outline: none; }
#moveIntro .miDemo { border-radius: 10px; background: #060a12; border: 1px solid rgba(120, 190, 250, .18); overflow: hidden; }
#moveIntro .miDemo svg { display: block; width: 100%; height: auto; }
#moveIntro .miName { font: 800 18px/1.1 ui-monospace, monospace; letter-spacing: 1.5px; color: #fff; }
#moveIntro .miText { font-size: 13px; line-height: 1.45; color: #b9d2e6; min-height: 3.8em; }
#moveIntro .miPick {
  align-self: flex-start; margin-top: 2px; padding: 8px 14px; border-radius: 6px;
  font: 800 12px/1 ui-monospace, monospace; letter-spacing: 2px; color: #071018;
  background: linear-gradient(180deg, #ffe6a8, #eab543);
}
/* ---- the demonstrations. Loops; purely decorative. ---- */
@keyframes miTapRing { 0%, 15% { r: 6; opacity: 0; } 20% { opacity: 1; } 60% { r: 26; opacity: 0; } 100% { r: 26; opacity: 0; } }
#moveIntro .miRing { animation: miTapRing 2.6s ease-out infinite; }
#moveIntro .miRing2 { animation-delay: .25s; }
@keyframes miFinger { 0%, 12% { transform: translateY(-18px); opacity: 0; } 18% { transform: none; opacity: 1; } 30% { opacity: 1; } 40%, 100% { transform: translateY(-18px); opacity: 0; } }
#moveIntro .miFinger { animation: miFinger 2.6s ease-in-out infinite; }
@keyframes miShipTap { 0%, 22% { offset-distance: 0%; } 85%, 100% { offset-distance: 100%; } }
#moveIntro .miShipTap { offset-path: path('M40 100 Q120 40 196 62'); offset-rotate: auto; animation: miShipTap 2.6s cubic-bezier(.3, .1, .3, 1) infinite; }
@keyframes miKnob { 0%, 15% { transform: translate(0, 0); } 30% { transform: translate(16px, -14px); } 60% { transform: translate(18px, -18px); } 80%, 100% { transform: translate(0, 0); } }
#moveIntro .miKnob { animation: miKnob 2.6s ease-in-out infinite; }
@keyframes miShipStick { 0%, 15% { offset-distance: 0%; } 70% { offset-distance: 100%; } 100% { offset-distance: 100%; } }
#moveIntro .miShipStick { offset-path: path('M120 110 C 150 60, 180 60, 214 40'); offset-rotate: auto; animation: miShipStick 2.6s ease-in-out infinite; }
@media (max-width: 560px) { #moveIntro .miText { min-height: 0; } #moveIntro .miCards { gap: 12px; } }

/* The movement intro on a SHORT screen (a phone in landscape, or a browser
   with its bars still up): the demos shrink, the copy tightens, and the two
   cards sit side by side so both choices are on screen at once. */
#moveIntro { min-height: 100dvh; align-content: center; }
@media (max-height: 560px) {
  #moveIntro { padding: 10px; place-items: start center; }
  #moveIntro .miKicker { font-size: 9px; letter-spacing: 3px; }
  #moveIntro .miTitle { margin: 6px 0 2px; font-size: clamp(18px, 3.6vw, 24px); }
  #moveIntro .miSub { margin: 0 0 10px; font-size: 12px; }
  #moveIntro .miCards { grid-template-columns: 1fr 1fr; gap: 10px; }
  #moveIntro .miCard { padding: 10px 12px 10px; gap: 6px; }
  #moveIntro .miDemo svg { max-height: 88px; width: auto; margin: 0 auto; }
  #moveIntro .miName { font-size: 15px; }
  #moveIntro .miText { font-size: 11.5px; line-height: 1.35; min-height: 0; }
  #moveIntro .miPick { padding: 7px 12px; font-size: 11px; }
}

/* ==========================================================================
   ONE HUD ROW  (touch only -- the desktop layout is untouched)

   The unit card, the character and the console used to be three absolutely
   positioned boxes, each computing its own edges from a measurement of the
   others. That is a circular dependency dressed up as a layout, and it
   showed exactly as you would expect: the character drawn over the refit
   panel, sizes that breathed as the drawer opened, and an arrangement that
   was right in a desktop emulator and wrong on a real handset.

   They are now one flex row. Overlap is not prevented, it is impossible.

   The console is the exception: it is PINNED to the right-hand end and the
   row reserves its width as padding. That is deliberate -- the refit panel
   and the abilities drawer are different widths, and if the console were a
   normal flex item the character would slide sideways every time you docked.
   Reserving the widest of the two keeps everything still.
   ========================================================================== */
body.touch:not(.hudCollapsed) #console {
  position: absolute;
  /* SAFE-AREA INSETS, CAPPED.

     A phone with a display cutout reports a 35px side inset in landscape, and
     honouring it in full pushed the whole row 35px in from that edge -- a
     visible band beside the console with nothing in it, and on the other side
     a gap beside the minimap. The row sits along the very bottom of the
     screen, below where a cutout physically is, so the full inset buys nothing
     there; 10px is enough to clear a rounded corner, which is the part that
     does reach the bottom.

     The BOTTOM inset is honoured in full: that one is the home indicator, and
     it really is where the row wants to be. */
  left: max(6px, min(env(safe-area-inset-left, 0px), 10px));
  right: max(6px, min(env(safe-area-inset-right, 0px), 10px));
  bottom: max(6px, env(safe-area-inset-bottom, 0px));
  display: flex;
  align-items: flex-end;
  gap: 8px;
  /* the band the console occupies, published by layoutTouchCard */
  /* NOT --console-w: that name is already the console tile grid's own width
     (see the tile-sizing block above), and borrowing it made this padding read
     265px of tiles instead of the panel's outer width -- which is exactly how
     the character ended up 41px inside the refit panel. */
  /* NO RESERVED BANDS. Every member is in the flow, so there is nothing
     outside it to reserve space for -- and reserving space by hand is what
     broke the row twice: the console was pinned, then the character was, and
     each time the one guarantee the row exists for was thrown away. */
  /* the row is a container, not a target: only its panels take input, or it
     would swallow every tap along the bottom of the battlefield */
  pointer-events: none;
  z-index: 20;
}
/* WHATEVER IS BOTTOM-LEFT, MEASURED. In tap-to-move that is the minimap; in
   trackpad mode the map moves to the top and the stick takes the corner.
   This was 152px hard-coded, and the real minimap on a 915px phone reaches
   175px -- so the unit card was drawn 17px into it. Neither obstacle is a
   member of the row; the row simply starts after whichever one is there.
   Published by layoutTouchCard from the same bounds the old layout used. */
body.touch:not(.hudCollapsed) #console { padding-left: var(--row-pad-left, 152px); }

/* THE ROW POSITIONS ITS MEMBERS -- SAVED OFFSETS DO NOT.

   The layout editor stores a drag as `translate`, which moves a panel when
   it is PAINTED but not where it sits in the flow. That was the whole
   positioning mechanism in the old absolute layout, and it is incompatible
   with a row: the card carries dx -42 from an earlier session, so flexbox
   placed it correctly and the translate then slid it 42px left, straight
   over the minimap. Measured: 27px into the map, 10px into the character.
   Every 'still overlapping' after the row was built traces back to this.

   Inside the row, position belongs to the row. Size does not -- the editor's
   scale and width still apply, so the panels remain yours to tune. */
body.touch:not(.hudCollapsed) #console > #infoPanel,
body.touch:not(.hudCollapsed) #console > #hudChar,
body.touch:not(.hudCollapsed) #console > #upgPanel,
body.touch:not(.hudCollapsed) #console > #cmdPanel { translate: none !important; }

/* THE TWO MEMBERS. Every absolute position they carried is dropped here, in
   one place, rather than fought rule by rule further up the file. */
body.touch:not(.hudCollapsed) #console > #infoPanel,
body.touch:not(.hudCollapsed) #console > #hudChar {
  position: static !important;
  /* AND NO TRANSFORM. The minimised strip is centred with
     left: 50% plus translateX(-50%). As a flex item the left is ignored but
     the translate is not, so the card was shifted half its own width to the
     left -- straight over the minimap. Flexbox centres it instead (see the
     minimised rule at the end). The layout editor is unaffected: it uses the
     separate translate/scale properties, not transform. */
  transform: none !important;
  left: auto !important; right: auto !important;
  top: auto !important; bottom: auto !important;
}
/* THE ORDER OF YIELDING.

   On a 915px phone the four things want about 970px, so something has to
   give -- and saying so explicitly is the whole point. Before, nothing
   yielded and the row simply overflowed to the right, which is how the
   character ended up drawn 41px into the refit panel.

   The card yields FIRST and by a long way (shrink factor 8): its text can
   wrap and it is still a card. The character yields only after the card has
   nothing left to give, and it shrinks in PROPORTION -- width and height
   together, via aspect-ratio -- so it is never squashed, only smaller. The
   console yields never: it is pinned and its band is reserved. */
body.touch:not(.hudCollapsed) #console > #infoPanel {
  margin: 0 !important;
  width: auto !important;
  max-width: none !important;
  /* THE CARD DOES NOT YIELD.

     Every width I squeezed it to made it worse: at 267px the stat line broke
     over two lines, at 199px the weapon tiles stacked into a column, at 94px
     it stopped being a card at all. The squeezing came with a train of hacks
     to survive it -- a shrinkable stats column, wrapping tiles, wrapping
     text -- and every one of them was a symptom. It is drawn at its designed
     size, full stop. */
  /* THE CARD TAKES UP THE SLACK.

     It grows into whatever the row has spare, so the strip runs from the
     minimap to the character with no dead gap in the middle, and it shrinks
     first when the row is tight -- its grid reflows, and it stops at a floor
     where it is still readable. Growing is safe here in a way a free offset
     never was: flexbox knows about it, so the panels stay side by side.
     Nothing else grows, so the character and console keep the size you set. */
  flex: 1 1 auto;
  min-width: min(190px, 100%);
  pointer-events: auto;
}

body.touch:not(.hudCollapsed) #console > #hudChar {
  /* IN THE FLOW, AND IT SHRINKS WITHOUT LOSING ITS SHAPE.

     WIDTH is the flexed dimension and height is auto, so aspect-ratio derives
     the height from whatever width flexbox settles on. Shrinking therefore
     makes the frame smaller, never a different shape.

     Both of the earlier attempts are recorded here because both looked
     reasonable and both were wrong:

       - a fixed HEIGHT with the width from the aspect. When anything capped
         the height, the width did not follow, and a 4:5 frame rendered at
         0.92.
       - pinning it out of the flow so nothing could push it. That removed the
         one guarantee the row exists for: anything outside the flow can be
         overlapped by anything inside it, and on a narrow phone this ended up
         behind the console -- in the DOM, invisible on screen.

     It is a flex item like everything else now. It yields after the card and
     stops at a floor where it is still a face rather than a sliver. */
  position: static !important;
  margin: 0 !important;
  left: auto !important; right: auto !important;
  top: auto !important; bottom: auto !important;
  transform: none !important;
  flex: 0 1 auto;
  width: var(--portrait-w, 152px);
  height: auto !important;
  max-height: none !important;
  aspect-ratio: var(--portrait-ar, 4 / 5);
  min-width: 56px;
  pointer-events: none;
}
/* THE CONSOLE IS A MEMBER OF THE ROW, NOT AN EXCEPTION TO IT.

   It was pinned here to stop it being pushed off the right edge -- and that
   single exception is what re-created the whole problem the row was built to
   end. Anything outside the flow can be overlapped by anything inside it, and
   on a narrow phone the character duly ended up behind this panel.

   In the flow it cannot be overlapped, and it cannot be pushed off either:
   it never shrinks, so the card and the character give way around it. The
   auto left margin holds it against the right-hand end when there is room to
   spare, and collapses to nothing when there is not. */
body.touch:not(.hudCollapsed) #console > #upgPanel,
body.touch:not(.hudCollapsed) #console > #cmdPanel {
  position: static !important;
  left: auto !important; right: auto !important;
  top: auto !important; bottom: auto !important;
  margin: 0 0 0 auto !important;
  flex: 0 0 auto;
  pointer-events: auto;
}

/* ---- THE TOP BAR STOPS AT THE CORNER BUTTONS ---------------------------
   #topBar is centred across the whole width, so on a phone the right-hand
   end of the group -- the match clock -- ran underneath the button cluster
   and was covered. Reserving the cluster real width shifts the centre left
   instead, and the clock is readable again. */
body.touch #topBar {
  /* THE SAME RESERVE ON BOTH SIDES, so the group lands on the screen's centre
     line and still clears the buttons.

     Reserving only the right pushed it left by exactly that reserve -- which
     is why the minerals, level and clock sat off-centre. Reserving each side
     its own cluster's width was better but still not centred: the clusters
     are different widths, so the group settled 19px off. Taking the WIDER of
     the two for both sides is symmetric, so the midpoint is the screen's
     midpoint, and the wider cluster is cleared by construction. */
  padding-left: var(--topbar-pad-l, 0px);
  padding-right: var(--topbar-pad-r, 360px);
  box-sizing: border-box;
}

/* (The row does not apply at all when the HUD is minimised -- see the
    :not(.hudCollapsed) on every rule above. The minimised layout has its own
    fixed corners and its own centring, and the row was overriding both with
    position: static !important, which is why the ability tiles floated into
    the middle of the screen and the strip stopped centring.) */

/* Set by layoutTouchCard only on a screen too narrow to hold a readable card
   and the character at once -- around 730px, well below any phone in
   landscape. A card squeezed past that point grows upward instead of just
   getting narrower, and climbs into the minimap. */
/* (There was a rule here that hid the character whenever the row was full.
    It is gone. Hiding it was never the answer to a row that is too tight --
    making the row fit is, and that is what the smaller console and card tiles
    above do. The character is always drawn.) */



/* THE MINIMAP LEAVES THE ROW when the row cannot hold it, a readable card
   and the character together (layoutTouchCard decides, from widths alone).
   It is the right panel to move: it is a map, it reads the same in a corner,
   and in trackpad mode it already lives up there. The alternative was
   shrinking the card into illegibility or hiding the character, and both of
   those are worse than a map that moved. */
/* (The minimap used to be moved to the top corner here when the row got
    tight. It is gone: a map that changes corners is not a fix, and the
    character shrinking a little is.) */

/* ==========================================================================
   WING ORDERS

   Sits directly above the command console, because it is the same kind of
   thing: buttons you press to order something about. Only shown when the
   hull has craft to order.
   ========================================================================== */
#wingBar {
  /* FIXED TO THE VIEWPORT, not to #console.

     bottom:100% inside #console works on touch, where the row is positioned --
     and on desktop #console is static, so the containing block became #hud and
     the bar was placed above the top of the screen. Invisible, with nothing to
     suggest why. The runtime measures where the console's top edge actually is
     and the bar sits just above it, which is the same place on every layout. */
  position: fixed;
  right: max(12px, min(env(safe-area-inset-right, 0px), 10px));
  bottom: var(--wing-bar-bottom, 200px);
  display: flex; align-items: center; gap: 6px;
  padding: 5px 7px; z-index: 21;
  border: 1px solid var(--hud-line); border-radius: 8px;
  background: linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
  box-shadow: 0 0 0 1px rgba(2, 7, 14, .9), 0 0 12px var(--hud-glow);
  pointer-events: auto;
}
#wingBar.hidden { display: none !important; }
#wingCount {
  font-size: 9px; font-weight: 800; letter-spacing: 1.2px;
  color: #7fb8dc; padding-right: 2px;
}
#wingBar button {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 4px 8px; cursor: pointer;
  border: 1px solid var(--hud-line); border-radius: 6px;
  background: linear-gradient(180deg, rgba(30,60,90,.55), rgba(10,20,34,.7));
  color: #cfe8fa; font: inherit; font-size: 10px; font-weight: 800; letter-spacing: 1px;
}
#wingBar button > b { color: var(--hud-gold, #eab543); font-size: 9px; }
#wingBar button:hover { border-color: var(--hud-line-hi); color: #fff; }
/* The order being aimed right now. */
#wingBar button.arming {
  border-color: var(--hud-gold, #eab543);
  box-shadow: 0 0 10px rgba(234, 181, 67, .5);
  color: #fff;
}
#wingBar button:disabled { opacity: .4; cursor: default; }
/* On a phone the console is already tight; keep the bar compact. */
body.touch #wingBar { padding: 4px 5px; gap: 4px; }
body.touch #wingBar button { padding: 3px 6px; font-size: 9px; }
/* No hotkey letters on a phone: there is no keyboard to press them on, and
   they were costing width on the tightest layout there is. */
body.touch #wingBar button > b { display: none; }
/* THE ORDERS SURVIVE A MINIMISED HUD.

   Minimising gives the battlefield back; it does not mean giving up control
   of the wing. The bar is four small buttons and it is the only way to command
   the craft on a phone, so hiding it there took the feature away exactly when
   the screen was clearest to fly in. */


/* (Every minimised-HUD override added during the flex-row work was removed
    here. Each one fixed the case in front of me and broke another: the
    docked console folded into a vertical strip, the stat line was clipped
    mid-word, the tiles could not be resized. The minimised HUD had its own
    working layout further up this file and did not need any of it -- the
    only thing it needed was for the flex row to leave it alone, which the
    :not(.hudCollapsed) guards above now do.) */

/* THE TILES PACK TO THE RIGHT-HAND END OF THE ROW.

   The panel is pinned to the corner and shrink-wraps, but the grid inside it
   is repeat(auto-fill, tile) with justify-content:start -- it builds as many
   columns as it is given and puts the first tile in the leftmost one. With a
   single researched ability that is 216px from the corner, measured, which
   reads as an icon floating in open space.

   Only the packing changes. Position, plate, size and the docked console are
   all left exactly as they were. */
body.hudCollapsed:not(.cmdAbilities) #cmdPanel > #commandCard {
  justify-content: flex-end !important;
  justify-items: end !important;
  /* ONE LINE, GROWING LEFT FROM THE CORNER.

     The panel is capped at min(62vw, 520px), and six icons exceed that: at
     max-content the row ran off the screen, and wrapping put a second line
     above the first. Neither is a row along the bottom.

     The panel is pinned to the right, so it can simply be allowed to grow",
     leftwards as far as the screen: one line, right-aligned, always on screen,
     however many abilities are researched. */
  width: auto !important;
  max-width: 100% !important;
  flex-wrap: nowrap !important;
  margin-left: auto !important;
}

/* NO BOX. THE ICONS ARE THE WHOLE THING.

   The container was still reserving the console shape -- two tile rows and a
   header -- so a single icon sat at the top of a 140px-tall invisible box
   pinned to the corner. Nothing drew it, which is why it read as an icon
   floating in space rather than as a panel in the wrong place.

   Its height is now its contents, and its contents are the icons. */
body.hudCollapsed:not(.cmdAbilities) #cmdPanel,
body.hudCollapsed:not(.cmdAbilities) #cmdPanel > #commandCard {
  height: max-content !important;
  min-height: 0 !important;
  max-height: none !important;
  grid-auto-rows: max-content !important;
  align-content: flex-end !important;
  align-items: flex-end !important;
}

/* ...and the panel is EXACTLY its icons wide.

   Raising the cap without shrink-wrapping it let the box stretch to whatever
   room was available -- a wide empty rectangle around a single icon. It takes
   its width from its contents; the cap only stops a long row leaving the
   screen. */
body.hudCollapsed:not(.cmdAbilities) #cmdPanel {
  width: max-content !important;
  max-width: calc(100vw - 28px) !important;
}
body.hudCollapsed:not(.cmdAbilities) #cmdPanel > #commandCard {
  width: max-content !important;
  max-width: 100% !important;
}

/* NOTHING OVERLAPS. The strip stops short of the icons.

   The icon row grows leftwards from the corner; the stat strip is centred on
   the screen. Left alone they meet in the middle -- measured, the row ran
   over "Energy: 200 / 200".

   game.js publishes the row’s RENDERED width as --min-abil-w (rendered, so
   it tracks the +/- scale). Reserved on BOTH sides so the strip keeps the
   screen’s centre line rather than being shoved off it, and the stat LINE
   ellipsises if it is squeezed -- an earlier version clipped the panel and
   cut "Shields" to "hields" mid-word. */

body.hudCollapsed #infoPanel #unitStats .scLine {
  max-width: 100% !important;
  overflow: hidden !important;
  text-overflow: ellipsis !important;
  white-space: nowrap !important;
}

/* ONE ROW. The strip fills the gap between the minimap and the icon row --
   same baseline as both -- and is SIZED to that gap: left edge from the map,
   right edge from the icons, both published by game.js. Overlap is not
   something to prevent here; there is no width at which it can happen.
   The contents centre within that gap, and the stat line ellipsises if the
   gap is narrow. */
body.hudCollapsed #infoPanel {
  position: fixed !important;
  bottom: calc(14px + env(safe-area-inset-bottom, 0px)) !important;
  left: max(var(--min-left, 12px), calc(12px + env(safe-area-inset-left, 0px))) !important;
  right: calc(var(--min-abil-w, 0px) + 14px) !important;
  width: auto !important;
  max-width: none !important;
  transform: none !important;
  /* translate is NOT locked off here any more. It was, which is why the one
     panel most worth moving on a minimised HUD could be dragged and snapped
     straight back -- an inline offset loses to an !important. applyHudLayout
     owns it now and writes it from the strip's OWN saved entry (infoPanelMin),
     so a nudge here cannot follow the full-size card around. */
  justify-content: center !important;
  scale: none !important; zoom: 1 !important;
  /* VISIBLE, so a tooltip can leave the strip. Clipping here cut every tip
     off at the strip's edge -- the box is only as wide as the gap it sits in,
     and a tip is wider than that by design. The contents are kept inside the
     gap by --min-strip-scale and the stat line's own ellipsis instead. */
  overflow: visible !important;
  /* THE BOX CANNOT COLLAPSE.
     Its width is whatever is left between the corners -- and when the ability
     row is wide that remainder reaches ZERO, measured. A zero-width box does
     not hide its contents; they are CENTRED in it, so a 112px row of upgrade
     icons drew from 56px to the LEFT of the box and landed on the trackpad.
     That is the bug reported five times: the box was always where it should
     be, and the icons were never in the box.
     min-width wins over a width derived from left+right, and the box grows
     RIGHTWARDS from its left edge -- away from the pad, never toward it. */
  min-width: min(240px, calc(100vw - var(--min-left, 158px) - 16px)) !important;
}
body.hudCollapsed #infoPanel #infoMain, body.hudCollapsed #infoPanel #unitStats { max-width: 100%; min-width: 0; }
/* +/- in the HUD editor resizes the strip's CONTENT (game.js publishes the
   scale, capped to the gap) -- the box stays exactly the gap. */
body.hudCollapsed #infoPanel #infoMain { scale: var(--min-strip-scale, 1); transform-origin: center bottom; }
body.hudCollapsed #infoPanel #unitStats .scLine { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* DOCKED AND UNDOCKED ARE THE SAME HUD. NOTHING APPEARS OR VANISHES.
   This used to hide the ability row whenever the ship was docked and the HUD
   minimised, on the grounds that the docked refit console's own ABILITIES
   button sat underneath it. On touch that console is not on screen at all --
   `body.touch.atBase #upgPanel { display: none !important }` further up
   replaces it with the REFIT button in the top corner -- so there has been
   nothing to overlap for a while, and all the rule did was delete a panel on
   docking and put it back on undocking. That is the HUD visibly rearranging
   itself every time you dock, which is not something a HUD should ever do.
   The row stays put in both states. */

/* FLYING, THE DRAWER HAS NO HEADING. Undocked it is a strip of icons, and a
   caption over a strip of icons is just a row it could have used. Docked the
   console still says which page it is on. */
body:not(.atBase) #cmdPanel::before { content: none !important; display: none !important; }

/* THE FLYING DRAWER IS AS TALL AS ITS ROWS. Undocked it borrowed the docked
   console's fixed height (--console-h: header + two tile rows), so a kit that
   fits on one and a bit rows sat at the top of a frame with a blank band
   under it. Docked, the console keeps its fixed height for the refit grid. */
body:not(.touch):not(.hudCollapsed):not(.atBase) #cmdPanel {
  height: auto !important; min-height: 0 !important;
}

/* ...and on TOUCH too. The open drawer took the docked console's fixed height
   (--console-h) whether or not it was docked, so flying, a kit that fits two
   rows sat over a blank third. Docked keeps the fixed height; flying fits. */
body.touch.abilitiesOpen:not(.atBase) #cmdPanel { height: auto !important; min-height: 0 !important; }

/* ── PREVIEWING ANOTHER UNIT ──────────────────────────────────────────────
   Their card and their pilot, and nothing of yours. The console, the wing bar
   and the drawer button all belong to the ship you are flying, and stacking
   them beside somebody else's stats is what made an inspection read as a
   broken HUD. Selecting your own ship again brings all of it straight back;
   hotkeys never stopped working. */
body.previewing #cmdPanel,
body.previewing #upgPanel,
body.previewing #wingBar,
body.previewing #abilityToggle { display: none !important; }

/* ══ KILL SLATES ═══════════════════════════════════════════════════════════
   With permadeath a kill ends somebody's match, and there are only six in a
   whole game. They used to scroll past as small text in the corner, carrying
   the same weight as a docking hint. These sit at the bottom centre -- where
   the eye already is during a fight, and clear of the console on both sides.
   Built to the same recipe as the achievement banner: a glow that bleeds past
   the plate, a sheen that sweeps once, and type big enough to read without
   looking away from what you are doing. */
#killBanners {
  position: fixed;
  left: 50%;
  /* Above the ability bar, and above the phone's home indicator. */
  /* CLEAR OF THE CONSOLE, NOT ON IT. 150px put the slate on top of the unit
     card and the refit panel -- the two things you read while deciding what to
     do about the kill it is announcing. Just under halfway up puts it in empty
     sky above the whole HUD stack, where nothing has to be covered. */
  bottom: 46%;
  transform: translateX(-50%);
  z-index: 68;
  display: flex;
  flex-direction: column-reverse;   /* newest nearest the middle of the screen */
  align-items: center;
  gap: 8px;
  pointer-events: none;
}
body.hudCollapsed #killBanners { bottom: 40%; }

.kbCard {
  position: relative;
  opacity: 0;
  transform: translateY(16px) scale(0.94);
  transition: transform .42s cubic-bezier(.13, 1.5, .4, 1), opacity .28s ease;
}
.kbCard.in { opacity: 1; transform: none; }

.kbGlow {
  position: absolute;
  inset: -26px -40px;
  border-radius: 16px;
  background:
    radial-gradient(52% 120% at 50% 50%, var(--kb-glow), transparent 70%),
    radial-gradient(90% 200% at 50% 50%, var(--kb-glow2), transparent 78%);
  filter: blur(18px);
  opacity: 0;
}
.kbCard.in .kbGlow { animation: kbPulse 2.2s ease-out .1s 1 forwards; }
@keyframes kbPulse {
  0% { opacity: 0; } 14% { opacity: 1; } 55% { opacity: .62; } 100% { opacity: .5; }
}

.kbPlate {
  position: relative;
  overflow: hidden;
  padding: 9px 30px 11px;
  border: 1px solid var(--kb-line);
  border-radius: 10px;
  background:
    linear-gradient(180deg, rgba(10, 20, 34, .96), rgba(3, 8, 15, .97));
  box-shadow:
    0 10px 30px rgba(0, 0, 0, .55),
    0 0 26px var(--kb-glow),
    inset 0 0 22px var(--kb-inner),
    inset 0 1px 0 rgba(255, 255, 255, .08);
  text-align: center;
}
/* One sweep of light across the plate as it lands. */
.kbSheen {
  position: absolute; inset: 0;
  background: linear-gradient(105deg, transparent 34%, rgba(255, 255, 255, .16) 50%, transparent 66%);
  transform: translateX(-120%);
}
.kbCard.in .kbSheen { animation: kbSweep 1.05s cubic-bezier(.3, 0, .2, 1) .24s 1; }
@keyframes kbSweep { to { transform: translateX(120%); } }

.kbHead {
  font: 800 21px/1.05 ui-monospace, "SF Mono", monospace;
  letter-spacing: 6px;
  text-indent: 6px;                  /* letter-spacing pads the right, not the left */
  color: var(--kb-head);
  text-shadow: 0 0 10px var(--kb-glow), 0 0 26px var(--kb-glow), 0 0 46px var(--kb-glow2);
}
.kbLine {
  margin-top: 3px;
  font: 500 12.5px/1.3 ui-monospace, monospace;
  letter-spacing: .6px;
  color: #91adc4;
  display: flex; gap: 7px; justify-content: center; align-items: baseline;
}
.kbLine i { font-style: normal; opacity: .72; }
.kbLine b { font-weight: 700; }
.kbMine   { color: #7fd4ff; }
.kbTheirs { color: #ff8a76; }

/* Tone carries who it was good for, without a word being spent on it. */
.kb-good { --kb-glow: rgba(70, 190, 255, .62); --kb-glow2: rgba(40, 130, 255, .34); --kb-inner: rgba(60, 150, 230, .16); --kb-line: rgba(110, 200, 255, .55); --kb-head: #dff1ff; }
.kb-bad  { --kb-glow: rgba(255, 90, 70, .58);  --kb-glow2: rgba(200, 40, 30, .32);  --kb-inner: rgba(220, 70, 50, .16);  --kb-line: rgba(255, 120, 100, .5);  --kb-head: #ffe3db; }
.kb-gold { --kb-glow: rgba(255, 196, 80, .7);  --kb-glow2: rgba(255, 140, 30, .38);  --kb-inner: rgba(255, 180, 70, .18);  --kb-line: rgba(255, 205, 120, .68); --kb-head: #fff0c6; }

/* A phone has neither the width nor the vertical room, and the slate must
   never cover the ability bar it is sitting above. */
@media (max-width: 760px) {
  .kbPlate { padding: 7px 18px 9px; }
  .kbHead { font-size: 16px; letter-spacing: 4px; text-indent: 4px; }
  .kbLine { font-size: 11px; gap: 5px; }
  #killBanners { bottom: 48%; max-width: 94vw; }
}

/* ══ OBJECTIVE ═════════════════════════════════════════════════════════════
   The buff readout. Directly under the match clock, because it is the same
   kind of fact -- a number counting down that both teams are playing around.
   A timed advantage nobody can see the end of is one nobody plays around. */
#buffBar {
  position: fixed;
  top: 46px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 66;
  display: flex;
  gap: 10px;
  pointer-events: none;
  font: 700 11px/1 ui-monospace, monospace;
  letter-spacing: 1.4px;
}
#buffBar span {
  padding: 4px 10px;
  border-radius: 5px;
  border: 1px solid currentColor;
  background: rgba(4, 10, 18, .82);
}
#buffBar .bbMine   { color: #ffd98f; box-shadow: 0 0 14px rgba(255, 196, 80, .3); }
#buffBar .bbTheirs { color: #ff9a8c; box-shadow: 0 0 14px rgba(255, 90, 70, .25); }

/* The line on the objective slate that says what winning it bought. */
.kbBuff {
  margin-top: 5px;
  font: 700 11px/1 ui-monospace, monospace;
  letter-spacing: 2px;
  color: #ffd98f;
  text-shadow: 0 0 12px var(--kb-glow);
}
@media (max-width: 760px) {
  #buffBar { top: 40px; font-size: 10px; }
  .kbBuff { font-size: 10px; letter-spacing: 1.2px; }
}

/* ══ COLLAPSED MINIMAP ON A PHONE — OUT OF THE THUMB'S WAY ══════════════════
   Minimised, the map is a bare canvas at the bottom-left of the screen, which
   on a touch device is exactly where a right-handed player taps to move and
   where the left thumb rests. Every stray tap landed on the map and jumped the
   camera instead of ordering a move -- an input that steals the order you were
   giving, at the moment you were giving it.

   Top-left instead: still the corner the eye already checks for it, and clear
   of both the move area and the ability bar. Desktop keeps the bottom-left
   position, where a mouse has no such problem.

   transform-origin follows it, or the HUD editor's scale would grow the map
   downward from a corner it no longer sits in. */
body.touch.hudCollapsed #minimapPanel {
  top: calc(8px + env(safe-area-inset-top, 0px));
  bottom: auto;
  transform-origin: top left;
}

/* ══ THE TOP-LEFT CLUSTER IS A ROW, NOT THREE GUESSES ══════════════════════
   MY SHIP, the trophy and the follow toggle were placed at hard-coded left
   offsets -- 8px, 95px, 143px -- which only works while every button is
   exactly the width the person who typed those numbers assumed. "MY SHIP" is
   wider than 87px in iOS Safari's font metrics, so the trophy was drawn on top
   of it. No amount of re-guessing the numbers fixes that class of bug on the
   next device with different metrics.

   They are already siblings inside #touchBtns, so that becomes the row: the
   buttons lay themselves out end to end with a real gap and CANNOT overlap,
   whatever any of them measures. Moving the cluster is then moving one box.

   MY SHIP also becomes its glyph on every touch layout rather than only with
   the trackpad. It is the widest thing up here by a factor of two, the two
   controls beside it are already glyphs, and the existing stickMove rules made
   exactly this change for exactly this reason. */
body.touch #touchBtns {
  position: fixed;
  top: calc(8px + env(safe-area-inset-top, 0px));
  left: calc(8px + env(safe-area-inset-left, 0px));
  display: flex;
  align-items: center;
  gap: 8px;
  z-index: 26;
  pointer-events: none;            /* the gaps are not a wall over the map */
}
body.touch #touchBtns > button {
  position: static !important;
  top: auto !important; left: auto !important;
  right: auto !important; bottom: auto !important;
  flex: 0 0 auto;
  pointer-events: auto;
}

/* THE CORNER BELONGS TO WHATEVER IS IN IT. With the trackpad down there, or
   the map up here when the HUD is minimised, the cluster moves out as one --
   clear of the three toggles that live along the right edge (8/58/108). */
body.touch.stickMove #touchBtns,
body.touch.hudCollapsed #touchBtns {
  left: auto;
  right: calc(160px + env(safe-area-inset-right, 0px));
}

/* The glyph. Same art and the same 44px box the trackpad layout already used,
   so the row has a predictable width in every state. */
body.touch #touchBtns > #btnSelf {
  font-size: 0;
  width: 44px; min-width: 44px; padding: 0;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2.5 20 20.5 12 16.2 4 20.5Z' fill='%23cfe8fa'/%3E%3C/svg%3E"),
    linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
  background-repeat: no-repeat, no-repeat;
  background-position: center, center;
  background-size: 19px 19px, cover;
}
body.touch #touchBtns > #btnSelf:active {
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2.5 20 20.5 12 16.2 4 20.5Z' fill='%23ffffff'/%3E%3C/svg%3E"),
    linear-gradient(180deg, var(--hud-glass-a), var(--hud-glass-b));
}

/* ══ RADIAL ABILITIES — FREE PLACEMENT IN AN INVISIBLE FRAME ═══════════════
   The first version of this was a flex row with a few canned nudges, which is
   not what was asked for and does not work: a row decides where its children
   go, so "move that one over there" is not a thing you can express. Every
   button is now ABSOLUTELY POSITIONED inside a frame that covers the screen,
   so each has its own place and its own size and nothing lays anything out.

   The frame is invisible and click-through; only the buttons themselves take
   a press, so the map, the card and the battlefield underneath are all still
   reachable through the gaps.

   Positions are stored as FRACTIONS of the viewport, so an arrangement made in
   one orientation is still in proportion in the other. */
body.radialAbilities.radialAbilities.radialAbilities #console > #cmdPanel {
  position: fixed !important;
  inset: 0 !important;
  width: auto !important; height: auto !important;
  max-width: none !important; max-height: none !important;
  margin: 0 !important; padding: 0 !important;
  background: none !important;
  border: none !important;
  box-shadow: none !important;
  overflow: visible !important;
  pointer-events: none !important;      /* the frame is a coordinate space, not a lid */
  /* ABOVE THE CONSOLE. The circles are placed anywhere on the screen, and
     "anywhere" includes over the refit panel -- which is where a bottom-right
     cluster naturally lands. Below it, half the arrangement disappears the
     moment you dock. Still under the kill slates and the layout bar. */
  z-index: 120;   /* the refit panel is 80; the circles must clear it */
  /* NO ZOOM, NO SCALE, NO TRANSLATE ON THE FRAME.

     The abilities panel is a HUD part, so the layout editor gives it a size --
     applied as `zoom` inside the touch row. Zoom rescales the COORDINATE
     SYSTEM of everything inside it, and this frame is a coordinate system:
     a circle at left:702px was drawn at 650, the reachable right edge shrank
     by the same 7.4%, and a drag that computes screen pixels and writes them
     as zoomed pixels moves the circle slower than the cursor and drifts. That
     is the phantom safe zone and the terrible movement, one cause.

     A full-screen frame has no size to tune -- the circles carry their own --
     so every transform the editor may have left on it is cancelled here. */
  zoom: 1 !important;
  scale: none !important;
  translate: none !important;
  transform: none !important;
}
body.radialAbilities.radialAbilities.radialAbilities #console > #cmdPanel::before,
body.radialAbilities.radialAbilities.radialAbilities #console > #cmdPanel::after { content: none !important; }

/* ALWAYS ON SCREEN, DOCKED OR NOT.

   The abilities panel normally shares the console with the refit grid, so it
   is hidden while docked and behind the ABILITIES tab otherwise -- which is
   why the circles only turned up after minimising and back. In this layout the
   cluster is a floating overlay that competes with nothing, so there is no
   reason to hide it: the abilities you can cast are the abilities you can see,
   at all times. */
body.radialAbilities.radialAbilities.radialAbilities #console > #cmdPanel {
  display: block !important;
  visibility: visible !important;
}
body.radialAbilities #console #cmdPanel > #commandCard {
  display: block !important;
  visibility: visible !important;
  position: absolute !important;
  inset: 0 !important;
  width: auto !important; height: auto !important;
  max-width: none !important;
  grid-template-columns: none !important;
  gap: 0 !important;
  padding: 0 !important;
  background: none !important;
  border: none !important;
  pointer-events: none !important;
}

/* Each button owns a point on the screen. left/top come from the saved
   fraction; the size is the saved scale times the base diameter. */
/* DISPLAY IS NOT MINE TO FORCE.

   A hull carries a different number of abilities, and the game hides the spare
   buttons with an INLINE display:none. A stylesheet rule marked !important
   beats an inline one that is not -- so 'display: grid !important' here dragged
   every empty slot back into view as a blank circle with a price on it. The
   display property is left alone: the game says which buttons exist, this only
   says where they go and how big they are. */
body.radialAbilities #console #cmdPanel > #commandCard .cmd.ability[data-slot] {
  position: absolute !important;
  margin: 0 !important;
  /* The press fires the ability, so the browser must not be able to reinterpret
     it as a pan or a double-tap zoom half a gesture later -- that is what makes
     a cast land while the other thumb is steering. */
  touch-action: none;
  width: var(--rb-size, 64px) !important;
  height: var(--rb-size, 64px) !important;
  min-width: 0 !important; min-height: 0 !important;
  padding: 0 !important;
  border-radius: 50% !important;
  display: grid;
  place-items: center;
  pointer-events: auto;
  background:
    radial-gradient(120% 120% at 50% 15%, rgba(38, 74, 112, .95), rgba(8, 18, 30, .96));
  border: 2px solid rgba(120, 190, 250, .5);
  box-shadow: 0 3px 10px rgba(0, 0, 0, .55), inset 0 0 12px rgba(90, 170, 240, .18);
  /* overflow stays VISIBLE: the tooltip is a child of the button, and
     overflow:hidden on a 64px circle clipped it to nothing. The icon sits
     inside the circle on its own, so nothing needs clipping. */
  overflow: visible;
  /* Lighter than the console behind it -- the field shows through, which is
     the point of putting the buttons on the field. Full opacity is kept for
     the press, the drag, and the one being edited. */
  opacity: .8;
}
body.radialAbilities #console #cmdPanel > #commandCard .cmd.ability[data-slot]:active,
body.layoutEdit.radialAbilities #console #cmdPanel > #commandCard .cmd.ability[data-slot] { opacity: 1; }
body.radialAbilities #console #cmdPanel > #commandCard .cmd.ability[data-slot] i.ico,
body.radialAbilities #console #cmdPanel > #commandCard .cmd.ability[data-slot] i.aico {
  width: 74%; height: 74%;
  background-size: contain; background-position: center; background-repeat: no-repeat;
  margin: 0;
}
/* No room for a name at this size, and no keyboard to press the hotkey on. */
body.radialAbilities #console #cmdPanel > #commandCard .cmd.ability[data-slot] .cName,
body.radialAbilities #console #cmdPanel > #commandCard .cmd.ability[data-slot] > b { display: none !important; }
/* The cost stays: it is the one number that decides whether a press will work. */
body.radialAbilities #console #cmdPanel > #commandCard .cmd.ability[data-slot] .cCost {
  position: absolute; left: 0; right: 0; bottom: 3px;
  text-align: center;
  font: 700 10px/1 ui-monospace, monospace;
  color: #ffd98f;
  text-shadow: 0 1px 3px #000;
}
body.radialAbilities #console #cmdPanel > #commandCard .cmd.ability[data-slot]:disabled,
body.radialAbilities #console #cmdPanel > #commandCard .cmd.ability[data-slot].cd {
  opacity: .45; border-color: rgba(120, 150, 180, .35);
}
body.radialAbilities #console #cmdPanel > #commandCard .cmd.ability[data-slot]:active {
  border-color: #9fd8ff;
  box-shadow: 0 0 18px rgba(120, 200, 255, .5), inset 0 0 16px rgba(120, 200, 255, .3);
}

/* The ABILITIES toggle keeps a fixed corner of its own. */
body.radialAbilities.radialAbilities.radialAbilities #console > #cmdPanel > #abilityToggle {
  position: fixed !important;
  right: calc(10px + env(safe-area-inset-right, 0px)) !important;
  bottom: calc(10px + env(safe-area-inset-bottom, 0px)) !important;
  left: auto !important; top: auto !important;
  pointer-events: auto;
}

/* While editing, the frame shows its edges so the space being arranged in is
   visible, and a dragged button is never dimmed by a cooldown. */
body.radialEdit #console > #cmdPanel {
  outline: 1px dashed rgba(120, 200, 255, .35);
  outline-offset: -1px;
}
/* A DRAG IS NOT A SCROLL.
   Without touch-action:none the browser owns the gesture: it waits to see
   whether the finger is panning, swallows the first several pointermoves, and
   then hands over what is left. That delay and the jump when it releases is
   what "stiff" was. Set for BOTH editors -- it was only on the radial-only one,
   and the HUD layout editor is the one being used. */
body.radialEdit #console #cmdPanel > #commandCard .cmd.ability[data-slot],
body.layoutEdit #console #cmdPanel > #commandCard .cmd.ability[data-slot] {
  opacity: 1 !important;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  cursor: grab;
}
body.layoutEdit.radialAbilities #console > #cmdPanel { touch-action: none; }
body.radialEdit #console #cmdPanel > #commandCard .cmd.ability[data-slot].rbPick {
  outline: 3px solid #ffc24f; outline-offset: 3px;
}

/* ── the radial layout editor ─────────────────────────────────────────────
   Its own bar, deliberately unlike the HUD layout editor's: they can both be
   reachable, and confusing which one is open would mean editing the wrong
   arrangement. The selected button is ringed so there is never a question
   about what the + and − apply to. */
#radialBar {
  position: fixed;
  left: 50%; transform: translateX(-50%);
  top: calc(8px + env(safe-area-inset-top, 0px));
  z-index: 200;
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  justify-content: center;
  max-width: 96vw;
  padding: 7px 10px;
  border: 1px solid rgba(255, 196, 80, .5); border-radius: 9px;
  background: rgba(6, 12, 20, .95);
  box-shadow: 0 6px 24px rgba(0, 0, 0, .6);
  font: 700 11px/1 ui-monospace, monospace; letter-spacing: 1px;
  color: #ffd98f;
}
#radialBar.hidden { display: none; }
#radialBar b { letter-spacing: 2px; }
#radialBar #rbWho {
  color: #cfe9ff; font-weight: 400; letter-spacing: 0;
  max-width: 34vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#radialBar button {
  padding: 6px 9px; border-radius: 6px; cursor: pointer;
  border: 1px solid rgba(110, 165, 230, .45);
  background: #12283d; color: #cfe9ff; font: inherit;
}
#radialBar button:hover { border-color: #4aa3e0; color: #fff; }
#radialBar #rbSmall, #radialBar #rbBig { min-width: 34px; font-size: 15px; }

/* While editing, the buttons are furniture: no cooldown dimming to read
   through, and the one being edited is obvious. */
body.radialEdit #console #cmdPanel > #commandCard .cmd.ability[data-slot] {
  opacity: 1 !important;
  touch-action: none;          /* a drag must not scroll or pan the page */
}
body.radialEdit #console #cmdPanel > #commandCard .cmd.ability[data-slot]:focus,
body.radialEdit #console #cmdPanel > #commandCard .cmd.ability[data-slot].rbPick {
  outline: 2px solid #ffc24f; outline-offset: 3px;
}

/* ══ THE CIRCLES STAY GRABBABLE IN THE LAYOUT EDITOR ═══════════════════════
   The editor makes every child of a panel inert -- 'body.layoutEdit #cmdPanel *
   { pointer-events: none }' -- so that dragging a panel by its middle grabs the
   PANEL and not the button under your finger. Exactly right for a panel of
   buttons; wrong here, because in this layout the circles are not contents of
   a panel, they ARE the panels. A press could not reach one at all, which is
   why dragging did nothing on a real mouse or a real finger. (It worked when I
   dispatched a synthetic event, because that skips hit-testing -- which is how
   I convinced myself it was fine when it was not.)

   The frame stays click-through for the opposite reason: it covers the whole
   screen at a high z-index, and the editor would otherwise give it
   pointer-events:auto and put an invisible sheet over every other panel you
   were trying to drag. */
body.layoutEdit.radialAbilities #console > #cmdPanel,
body.radialEdit.radialAbilities #console > #cmdPanel {
  pointer-events: none !important;
  outline: none !important;
  cursor: default !important;
}
body.layoutEdit.radialAbilities #cmdPanel #commandCard,
body.radialEdit.radialAbilities #cmdPanel #commandCard {
  pointer-events: none !important;
}
body.layoutEdit.radialAbilities #cmdPanel #commandCard .cmd.ability[data-slot],
body.radialEdit.radialAbilities #cmdPanel #commandCard .cmd.ability[data-slot] {
  pointer-events: auto !important;
  cursor: grab;
}
/* ...and their contents must not eat the press either. */
body.layoutEdit.radialAbilities #cmdPanel #commandCard .cmd.ability[data-slot] *,
body.radialEdit.radialAbilities #cmdPanel #commandCard .cmd.ability[data-slot] * {
  pointer-events: none !important;
}

/* ONLY WHAT YOU CAN PRESS. The cluster is a set of things to cast: an
   unresearched ability is not castable and a passive never is. Both stay on
   the full console, where the list describes the hull; out here they were
   circles that did nothing when pressed, which is the one thing a thumb
   cluster must never contain. */
body.radialAbilities #console #cmdPanel > #commandCard .cmd.ability[data-slot].research,
body.radialAbilities #console #cmdPanel > #commandCard .cmd.ability[data-slot].passiveAbil {
  display: none !important;
}


/* ── THE BLOOD FRAME ────────────────────────────────────────────────────────
   A red glow hugging the edge of the screen while the hull is burning.

   Built as two nested elements on purpose. The OUTER one carries --danger,
   which game.js writes (quantised, only when it changes) for how bad it is;
   the INNER one carries the pulse. Nested opacities multiply, so the pulse
   scales with the severity without JS touching a keyframe.

   Only OPACITY is animated. A pulsing box-shadow or background-size repaints
   a full-screen layer every frame, which is exactly the kind of constant cost
   the phone cannot afford; an opacity animation is composited. */
#dangerFrame {
  position: absolute; inset: 0;
  pointer-events: none;
  opacity: var(--danger, 0);
  transition: opacity .3s linear;
}
/* INSET SHADOWS, not a radial gradient, for the rim itself. A radial has
   to be sized in percentages of the box, so the strong end of the ramp lands
   wherever the aspect ratio puts it -- at 16:9 the first draft's hot stops
   fell outside the visible area entirely and the whole thing read as a faint
   pink smudge. An inset shadow hugs all four edges by definition, at any
   shape of screen. Painted ONCE into the layer; only opacity animates. */
#dangerFrame i {
  position: absolute; inset: 0; display: block;
  box-shadow:
    inset 0 0 52px 4px rgba(255, 40, 40, .40),
    inset 0 0 130px 22px rgba(214, 12, 16, .68),
    inset 0 0 300px 70px rgba(120, 0, 6, .60);
  /* A little extra weight in the corners, where the eye catches it first. */
  background: radial-gradient(ellipse 62% 58% at 50% 50%, transparent 62%, rgba(110, 0, 5, .38) 100%);
  animation: dangerPulse 1.5s ease-in-out infinite;
}
@keyframes dangerPulse { 0%, 100% { opacity: .6; } 50% { opacity: 1; } }
/* A warning that throbs is a problem for anyone who asked the system for less
   motion. It stays visible -- it is information -- it just holds still. */
@media (prefers-reduced-motion: reduce) {
  #dangerFrame i { animation: none; opacity: .85; }
}


/* ── THE REFIT SCREEN (touch only) ──────────────────────────────────────────
   Near-full-screen, landscape: title bar, a grid of everything this hull can
   buy, and a detail bar pinned to the bottom. Desktop keeps the console it has
   always had -- this is display:none there.

   PROPORTIONS ARE TAKEN FROM THE DESIGN, NOT FROM THE SPACE LEFT OVER.
   The card is 7:6, the art is everything between the name and the price, and
   the detail bar is sized to be read. Measured against the reference: a
   137x118 card carrying a ~65px icon.

   Which means the grid SCROLLS on a phone, and that is the trade being made
   deliberately. A landscape phone is 844x390 -- a fifth of the height of the
   design it is copying -- so the choice is three rows of postage stamps or two
   rows at full size with the third a thumb-flick away. Two full-size rows. */
#refitScreen { display: none; }
body.touch #refitScreen:not(.hidden) {
  display: flex; flex-direction: column;
  position: fixed; z-index: 70;
  /* A WINDOW, not the whole screen. Percentages so the margin reads the
     same on any handset, and margin:auto with all four insets set keeps it
     centred once the max sizes bite on a tablet. Vertical is the tighter of
     the two on purpose: height is what the card rows are competing for. */
  top: calc(3.5% + env(safe-area-inset-top, 0px));
  bottom: calc(3.5% + env(safe-area-inset-bottom, 0px));
  left: calc(5% + env(safe-area-inset-left, 0px));
  right: calc(5% + env(safe-area-inset-right, 0px));
  max-width: 1040px; max-height: 660px; margin: auto;
  background: linear-gradient(180deg, rgba(9, 20, 36, .97), rgba(4, 10, 20, .98));
  border: 1px solid var(--hg-line, rgba(56, 178, 230, .55));
  border-radius: 8px;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .6), 0 18px 60px rgba(0, 0, 0, .7),
              inset 0 1px 0 rgba(120, 214, 255, .28);
  overflow: hidden;
}
/* The console is the SAME controls in miniature -- two of them on screen is
   two places to tap for one thing. */
body.touch.refitOpen #console { display: none !important; }
/* AND THE CORNER BUTTONS GO WITH IT.
   The refit window is inset 5%/3.5%, which puts its close X straight over the
   ⚙ and ▾ in the top-right corner -- measured at 19-53px down against their
   8-42px band. They are unreachable behind a modal anyway, and leaving them
   there means every press of the X is a press on top of the settings menu. */
body.touch.refitOpen #hudToggle,
body.touch.refitOpen #adminToggle,
body.touch.fsCapable.refitOpen:not(.nativeApp) #fsToggle { display: none !important; }

#rfHead {
  display: flex; align-items: center; gap: 10px;
  padding: 5px 8px 5px 12px;
  background: linear-gradient(180deg, rgba(14, 34, 60, .95), rgba(7, 17, 31, .92));
  border-bottom: 1px solid rgba(56, 178, 230, .38);
  flex: 0 0 auto;
}
.rfTitles { flex: 1; min-width: 0; }
.rfTitle {
  font-size: 13px; font-weight: 700; letter-spacing: 2px;
  color: #7fd4ff; text-transform: uppercase; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.rfSub { font-size: 8.5px; letter-spacing: .6px; color: #7f9bb4; margin-top: 1px; }
#rfBank { font-size: 14px; font-weight: 700; color: var(--hud-gold, #eab543); white-space: nowrap; }
/* A SQUARE BUTTON WITH THE GLYPH IN THE MIDDLE OF IT.
   It was 30x28 -- not square -- with a 14px glyph sitting on a 1-unit line box
   inside it, so the cross was small, off-centre and crammed against a hard red
   border. Flexed to centre the glyph, squared off, and the red toned down to a
   frame rather than a fill: it is the way out of a panel, not a warning. */
#rfClose {
  width: 34px; height: 34px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  background: rgba(120, 22, 26, .32); color: #ffcfcf;
  border: 1px solid rgba(226, 70, 70, .45); border-radius: 8px;
  font-size: 17px; line-height: 1; font-weight: 400;
  cursor: pointer; padding: 0;
  transition: background .12s ease, border-color .12s ease, color .12s ease;
  -webkit-tap-highlight-color: transparent;
}
#rfClose:hover { background: rgba(160, 30, 34, .5); border-color: rgba(255, 110, 110, .75); color: #fff; }
#rfClose:active { background: rgba(190, 34, 38, .8); border-color: rgba(255, 140, 140, .9); color: #fff; }

/* EVERY UPGRADE ON ONE SCREEN, no scrolling.
   grid-auto-rows: 1fr makes the rows divide whatever height is left after the
   header and the detail bar, so the whole list is always visible and the cards
   simply get shorter when a hull carries more of them. Sizing the card in
   pixels instead is what put a third row below the fold. */
#rfGrid {
  flex: 1 1 auto; min-height: 0; overflow: hidden;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
  grid-auto-rows: minmax(0, 1fr);
  gap: 5px; padding: 5px;
}
/* The card fills its row. Its height is the grid's to give, not its own to
   ask for -- which is what stops the list ever running past the bottom. */
.rfCard {
  position: relative; display: flex; flex-direction: column; align-items: center;
  height: 100%; min-height: 0; gap: 2px; padding: 5px 5px 4px;
  justify-content: space-between; overflow: hidden;
  background: linear-gradient(180deg, rgba(16, 34, 58, .9), rgba(7, 16, 29, .92));
  border: 1px solid rgba(56, 178, 230, .34); border-radius: 6px;
  color: #cfe6ff; font-family: inherit; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.rfName {
  position: relative; z-index: 1; text-shadow: 0 1px 3px rgba(0, 0, 0, .95);
  font-size: 10.5px; font-weight: 600; letter-spacing: 0; line-height: 1.3;
  text-align: center; color: #e2eefb; width: 100%; flex: 0 0 auto;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
/* The art gets EVERY pixel between the name and the price. Contained art
   letterboxes to the smaller side of its box, so growing the ICON does
   nothing -- the BOX has to grow, which is why the card is sized by ratio
   and this is flex:1 rather than a fixed 38px. */
/* THE ART BLEEDS EDGE TO EDGE across this band, exactly as it does on the
   console tiles it came from. Letterboxing it (background-size: contain) is
   what produced "a black square on a blue background": the art files carry
   their own dark backgrounds, so a contained icon draws its own rectangle
   inside a wider card. Filling the band leaves no rectangle to see -- and
   because the band is a sibling of the name and the price rather than the
   whole card, it still cannot cover either of them. */
/* THE ART IS THE TILE. Not a picture in a slot on the tile -- the whole tile,
   edge to edge, with the name and the price riding on top of it behind a
   scrim. That is how these same files are drawn on the console tiles this
   screen replaced, and it is the only arrangement that never shows the dark
   rectangle the art files carry baked into them. */
.rfArt { position: absolute; inset: 0; overflow: hidden; }
.rfArt::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(180deg,
    rgba(4, 10, 20, .88) 0%, rgba(4, 10, 20, .18) 30%,
    rgba(4, 10, 20, .22) 66%, rgba(4, 10, 20, .92) 100%);
}
/* TWO KINDS OF ICON, AND THEY DO NOT WANT THE SAME TREATMENT.
   A built-in is a glyph -- several are CSS masks rather than plain images, so
   stretching one to the tile does not enlarge it, it erases it. They stay a
   centred badge at a fixed size, which is how the console tiles have always
   drawn them.
   Uploaded art carries its own baked background, so it bleeds to the edges:
   contained, it draws its own dark rectangle inside a wider card, which is
   the "black square on a blue background". */
#rfGrid .rfCard i.ico:not(.icoCustom) {
  /* top/left carry !important too: `inset` is a shorthand for all four, so an
     `inset: auto !important` beat the plain top/left that were meant to centre
     this and pinned every glyph to the corner. */
  position: absolute !important;
  top: 50% !important; left: 50% !important;
  right: auto !important; bottom: auto !important;
  transform: translate(-50%, -50%);
  width: 54px !important; height: 54px !important;
  background-size: contain !important; background-repeat: no-repeat;
  background-position: center;
}
#rfGrid .rfCard i.ico.icoCustom {
  position: absolute !important; inset: 0 !important;
  width: 100% !important; height: 100% !important;
  background-size: cover !important; background-repeat: no-repeat;
  background-position: center; border-radius: 0;
}
/* Beside the art, vertically centred, exactly as the design has it. */
.rfLvl {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%); z-index: 2;
  font-size: 10px; font-weight: 700; color: #9fe4ff; letter-spacing: .2px;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .9);
}
.rfCost {
  position: relative; z-index: 1; text-shadow: 0 1px 3px rgba(0, 0, 0, .95);
  font-size: 12px; font-weight: 700; letter-spacing: .3px; flex: 0 0 auto;
  color: var(--hud-gold, #eab543);
}
.rfCard.sel {
  border-color: var(--hud-gold, #eab543);
  box-shadow: 0 0 0 1px rgba(234, 181, 67, .5), 0 0 16px rgba(234, 181, 67, .3);
  background: linear-gradient(180deg, rgba(46, 40, 18, .92), rgba(22, 17, 6, .94));
}
/* Cannot afford it YET. Dimmed, not hidden: you still need to read what you
   are saving for. */
.rfCard.poor { opacity: .5; }
.rfCard.researchLocked .rfArt { filter: grayscale(1); opacity: .35; }
.rfCard.researchLocked .rfName, .rfCard.researchLocked .rfCost { color: #718294; }
.rfCard.poor .rfCost { color: #c96b6b; }
.rfCard.owned .rfCost, .rfCard.maxed .rfCost { color: #6fbf87; }

/* ── the detail bar ──
   This is where the tooltip went. A phone has no hover, so what one more level
   costs and buys has to be readable sitting still. */
#rfDetail {
  flex: 0 0 auto;
  display: flex; align-items: flex-start; gap: 11px;
  padding: 6px 9px; min-height: 0;
  max-height: 55%; overflow-y: auto;
  background: linear-gradient(180deg, rgba(6, 14, 26, .96), rgba(10, 22, 38, .96));
  border-top: 1px solid rgba(56, 178, 230, .38);
}
.rfdIcon {
  width: 50px !important; height: 50px !important; flex: 0 0 auto;
  background-size: contain; background-repeat: no-repeat; background-position: center;
  border: 1px solid rgba(56, 178, 230, .3); border-radius: 5px;
  background-color: rgba(12, 26, 44, .7);
}
.rfdMain { flex: 1 1 auto; min-width: 0; }
.rfdName {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  font-size: 16px; font-weight: 700; color: #9fe4ff; letter-spacing: .3px;
}
.rfdChip {
  font-style: normal; font-size: 10.5px; font-weight: 700; letter-spacing: .4px;
  padding: 2px 8px; border-radius: 4px; color: #cfe6ff;
  background: rgba(30, 58, 92, .9); border: 1px solid rgba(56, 178, 230, .45);
}
.rfdChip.next { color: #2a1f08; background: linear-gradient(180deg, #f2c65a, #d79a22); border-color: #f6dd9a; }
.rfdChip.max { color: #8fe0b0; border-color: rgba(90, 200, 140, .5); }
.rfdChip.new { color: #ffd9a0; border-color: rgba(234, 181, 67, .5); }
.rfdArrow { font-style: normal; font-size: 12px; color: #6f8ba4; }
.rfdDesc {
  font-size: 10.5px; color: #a4bed5; line-height: 1.35; margin-top: 3px;
  display: block; white-space: normal; overflow-wrap: anywhere;
}
/* Highlight the visible energy readout as well as the floating ship bar. */
body.tutEnergy.ownSelected:not(.hudCollapsed) #portraitVitals .vEn,
body.tutEnergy.ownSelected.hudCollapsed .scEnergy {
  outline: 2px solid #ffe08a; outline-offset: 2px;
  border-radius: 2px; background: rgba(80, 57, 9, .8);
  box-shadow: 0 0 12px rgba(255, 212, 90, .7);
  color: #ffe8ac;
}
.rfdStats {
  flex: 0 0 auto; display: flex; flex-direction: column; justify-content: center;
  gap: 3px; max-width: 40%;
}
.rfdStat {
  display: grid; grid-template-columns: auto auto auto auto; align-items: baseline;
  gap: 6px; font-size: 12px; white-space: nowrap;
}
.rfdStat span { color: #8faac2; }
.rfdStat b { color: #dfeeff; font-weight: 700; }
.rfdStat i { font-style: normal; color: #6f8ba4; }
.rfdStat b.up { color: #7fe08a; }
.rfdBuy { flex: 0 0 auto; display: flex; flex-direction: column; align-items: stretch; gap: 5px; }
.rfdCost { font-size: 12px; color: #9fbdd6; text-align: center; }
.rfdCost b { color: var(--hud-gold, #eab543); font-weight: 700; }
#rfBuy {
  min-width: 142px; padding: 10px 16px;
  font-family: inherit; font-size: 15px; font-weight: 700; letter-spacing: 1.8px;
  color: #23180a; cursor: pointer;
  background: linear-gradient(180deg, #f2c65a, #d79a22);
  border: 1px solid #f6dd9a; border-radius: 6px;
}
#rfBuy:active { background: linear-gradient(180deg, #d79a22, #b87d16); }
#rfBuy:disabled {
  background: rgba(30, 46, 66, .8); color: #6c8299;
  border-color: rgba(56, 178, 230, .28); cursor: default;
}
.rfdEmpty { align-self: center; font-size: 12px; color: #7f9bb4; letter-spacing: .5px; }

/* A landscape phone: drop the subtitle and tighten the chrome, so the height
   that buys goes to the cards rather than to decoration. */
@media (max-height: 430px) {
  .rfSub { display: none; }
  #rfHead { padding: 3px 8px 3px 12px; }
  .rfTitle { font-size: 12px; }
  #rfGrid { padding: 4px; gap: 4px; }
  .rfName { font-size: 10px; }
  #rfDetail { padding: 5px 9px; }
  .rfdIcon { width: 44px !important; height: 44px !important; }
}
/* Narrow phones give the description its own row above the purchase controls. */
@media (max-width: 600px) {
  #rfDetail { display: grid; grid-template-columns: 44px minmax(0, 1fr); }
  .rfdMain { grid-column: 2; }
  .rfdStats { grid-column: 1 / -1; max-width: none; flex-direction: row; flex-wrap: wrap; gap: 6px 14px; }
  .rfdBuy { grid-column: 1 / -1; flex-direction: row; align-items: center; justify-content: flex-end; }
}
/* Room to breathe on a tablet. */
@media (min-height: 560px) {
  #rfGrid { grid-template-columns: repeat(auto-fill, minmax(108px, 1fr)); gap: 8px; padding: 9px; }
  .rfName { font-size: 12.5px; }
  .rfCost { font-size: 13px; }
  #rfDetail { min-height: 92px; }
  .rfdName { font-size: 20px; }
}

/* ── THE DOCKED BUTTON (touch only) ─────────────────────────────────────────
   The console's refit page is gone on a phone -- everything it sold is on the
   refit screen, and two copies of one shop is two places to tap. What is left
   is this: docked, and the screen closed, you get one button back. */
body.touch.atBase #upgPanel { display: none !important; }
#rfOpen { display: none; }
body.touch.atBase:not(.refitOpen) #rfOpen {
  display: flex; align-items: center; gap: 6px;
  position: fixed; z-index: 45;
  top: calc(52px + env(safe-area-inset-top, 0px));
  right: calc(10px + env(safe-area-inset-right, 0px));
  padding: 9px 15px;
  font-family: inherit; font-size: 12px; font-weight: 700; letter-spacing: 1.6px;
  color: #23180a; cursor: pointer;
  background: linear-gradient(180deg, #f2c65a, #d79a22);
  border: 1px solid #f6dd9a; border-radius: 6px;
  box-shadow: 0 3px 14px rgba(0, 0, 0, .5), 0 0 18px rgba(234, 181, 67, .3);
  -webkit-tap-highlight-color: transparent;
}
body.touch.atBase:not(.refitOpen) #rfOpen:active {
  background: linear-gradient(180deg, #d79a22, #b87d16);
}


/* ── THE PILOT, MINIMISED: A TRANSMISSION ───────────────────────────────────
   `body.hudCollapsed #hudChar { display: none }` above is the rule: no avatar
   on the minimised HUD. This is the exception, and only while she is actually
   saying something -- the class is on for exactly the length of the line (see
   applyPortrait). Two body classes beat the one above it, so the !important
   there does not need fighting.

   Top centre, clear of the corner buttons and of the ability row, framed so it
   reads as an incoming call rather than as a panel that has come loose. */
   IN THE LAYOUT EDITOR it is shown whether or not she is talking, because a
   panel that only exists for the length of a spoken line is not a panel anyone
   can drag. Same box, same anchor, so what is arranged here is what appears.  */
body.hudCollapsed.pilotSpeaking #hudChar,
body.hudCollapsed.layoutEdit #hudChar {
  display: block !important;
  position: fixed !important;
  top: calc(46px + env(safe-area-inset-top, 0px)) !important;
  left: 50% !important; right: auto !important; bottom: auto !important;
  /* NO `translate` HERE. It is the property the layout editor writes an offset
     into, and an !important one meant a drag was thrown away on the next apply
     -- the same thing that made the minimised stat strip undraggable. Centring
     lives in `transform`, which is a separate property and composes with it:
     the stylesheet still centres the card, the saved offset moves it from
     there. */
  transform: translateX(-50%) !important;
  width: min(168px, 34vw) !important; height: auto !important;
  max-width: none !important; max-height: 30vh !important;
  z-index: 44 !important;
  border: 1px solid rgba(120, 214, 255, .55) !important;
  border-radius: 6px;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .7), 0 6px 26px rgba(0, 0, 0, .6),
              0 0 22px rgba(56, 178, 230, .28);
  background: rgba(4, 10, 20, .9);
  pointer-events: none;
  animation: pilotHail .28s ease-out;
}
/* Grabbable, and still, while it is being placed. Every pixel of the HUD is
   furniture in edit mode; a card that ignores the pointer cannot be picked up,
   and one that replays its arrival animation on every apply fights the drag. */
body.hudCollapsed.layoutEdit #hudChar {
  pointer-events: auto !important;
  animation: none !important;
}
@keyframes pilotHail {
  from { opacity: 0; transform: translateX(-50%) translateY(-8px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  body.hudCollapsed.pilotSpeaking #hudChar { animation: none; }
}


/* ── THE MINIMISED STRIP CLEARS THE TRACKPAD, MEASURED OR NOT ───────────────
   --min-left is published by game.js from the RENDERED box of whatever sits in
   the bottom-left corner. That works right up until the corner is empty at the
   moment it looks: open the ability drawer and the pad is display:none, so the
   measurement comes back 0, the fallback is 12px from the screen edge, and the
   strip slides under a pad that is about to come back. The minimap cannot save
   it either -- minimised, the map is in the TOP corner and is excluded on
   purpose.

   So the reserve is also stated outright. The pad is a known 132px box at a
   known offset (#moveStick above), and body.stickMove says the player chose
   it, whether or not it happens to be on screen this frame. */
body.hudCollapsed.stickMove #infoPanel {
  left: max(
    var(--min-left, 12px),
    calc(158px + env(safe-area-inset-left, 0px))
  ) !important;
}

/* ── KILL BANNERS: THE LIGHT, NOT THE BOX ───────────────────────────────────
   The slate had a plate behind it -- border, fill, drop shadow -- which is a
   panel, and a panel in the middle of the screen reads as UI to dismiss rather
   than as a thing that just happened. The type carries it instead: a hard
   glow, a light streak that opens out through the line, and an arc either
   side. Smaller than the plate version, because with nothing behind it the
   type does not have to fight a box for attention. */
.kbPlate {
  overflow: visible;
  padding: 4px 34px 5px;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
}
/* The arcs. Two half-capsules, drawn from the border so they stay crisp at
   any size and cost nothing to animate. */
.kbPlate::before, .kbPlate::after {
  content: '';
  position: absolute; top: 50%; translate: 0 -50%;
  width: 22px; height: 78%;
  border: 2px solid var(--kb-line);
  opacity: .85;
  filter: drop-shadow(0 0 6px var(--kb-glow));
}
.kbPlate::before { left: -4px;  border-right: 0; border-radius: 22px 0 0 22px; }
.kbPlate::after  { right: -4px; border-left: 0;  border-radius: 0 22px 22px 0; }

/* The streak. Was a diagonal sheen sweeping across a plate; with the plate
   gone it is the flash of light the words arrive on -- opening out from the
   centre through the whole line and settling as a thin rule. */
.kbSheen {
  position: absolute; left: -6%; right: -6%; top: 50%;
  height: 2px; inset-block-end: auto;
  transform: translateY(-50%) scaleX(0);
  background: linear-gradient(90deg,
    transparent 0%, var(--kb-glow) 15%,
    rgba(255, 255, 255, .95) 50%,
    var(--kb-glow) 85%, transparent 100%);
  box-shadow: 0 0 14px var(--kb-glow), 0 0 30px var(--kb-glow2);
  opacity: 0;
}
.kbCard.in .kbSheen { animation: kbBeam .85s cubic-bezier(.16, 1, .3, 1) .04s forwards; }
@keyframes kbBeam {
  0%   { transform: translateY(-50%) scaleX(0);   opacity: 0; }
  22%  { opacity: 1; }
  100% { transform: translateY(-50%) scaleX(1);   opacity: .32; }
}
/* Smaller, and lit from inside rather than sitting on a lit box. */
.kbHead {
  font: 800 17px/1.06 ui-monospace, "SF Mono", monospace;
  letter-spacing: 4px;
  text-indent: 4px;
  text-shadow:
    0 0 6px var(--kb-glow), 0 0 16px var(--kb-glow),
    0 0 34px var(--kb-glow2), 0 2px 4px rgba(0, 0, 0, .9);
}
.kbLine {
  margin-top: 2px;
  font: 500 11px/1.3 ui-monospace, monospace;
  text-shadow: 0 1px 4px rgba(0, 0, 0, .95);
}
.kbGlow { inset: -20px -34px; filter: blur(20px); }
@media (prefers-reduced-motion: reduce) {
  .kbCard.in .kbSheen { animation: none; opacity: .32; transform: translateY(-50%) scaleX(1); }
}


/* ── THE STRIP CANNOT PAINT OUTSIDE ITS GAP ─────────────────────────────────
   The box is the gap between the corners and it is positioned correctly -- but
   its contents are CENTRED in it with overflow:visible, so a row wider than
   the gap spills out of BOTH ends and the icons land on the trackpad anyway.
   Moving the box never fixed that, because the box was never where the icons
   were: measured at 158px with the icons drawn from 106.

   overflow:hidden is not the answer (it was tried, and it cuts the tile
   tooltips off at the strip edge -- a tip is wider than the gap by design).
   clip-path can be one-sided: the left and right edges clip at the box, while
   top and bottom are opened up far enough that a tooltip still escapes
   upwards. */
/* NO CLIP. This clipped the strip to its box, and on a phone where the gap
   is narrow -- notch insets each side plus a wide ability row -- the centred
   contents are wider than the gap, so clipping them removed most of the strip.
   That is the bottom HUD "disappearing" on a minimised phone. A clip can only
   ever hide; the spill is fixed by where the content STARTS instead. */
/* CENTRED. It was left-aligned for one build, to stop centred content that
   had outgrown its box spilling onto the trackpad -- but the box cannot
   collapse any more (min-width, above), so there is nothing to spill, and
   left-aligning a strip in a wide gap just looks wrong. Which it did. */
body.hudCollapsed #infoPanel { justify-content: center !important; }

/* ════════════════════════════════════════════════════════════════════════
   MINIMISED, THE ABILITY ROW IS ALWAYS ITS ICONS WIDE.

   The shrink-wrap rules above are all written `body.hudCollapsed:not(.cmdAbilities)`.
   `.cmdAbilities` says which TAB of the docked console is showing -- a docked
   idea, meaningless on a minimised HUD, and set while flying. So undocked
   every one of those rules switched itself off and #cmdPanel fell back to a
   full-width box: measured 780px wide at left 0 on a 780px screen, for about
   70px of icons. Docked the flag was clear, the rules applied, and the same
   panel measured 69px.

   That is what made the bottom row re-lay itself on docking. --min-abil-w is
   the distance from the right edge to the neighbour's content, and the stat
   strip's right edge is calc(--min-abil-w + 14px) -- so a neighbour that
   claims the whole screen pushes the strip's right edge off the left of it,
   the width goes negative, and min-width:240px takes over. 240 undocked
   against 520 docked, which is the two HUDs, reported five times.

   Minimised, the row is the row. No tab flag in the selector.
   ════════════════════════════════════════════════════════════════════════ */
/* NOT IN RADIAL MODE. There the panel is a full-screen frame and the card
   places its buttons around a circle inside it -- shrink-wrapping either one
   collapses that frame and the buttons fall back to an ordinary grid, which is
   the radial ring turning into a row of squares. The block below carries this
   same guard; this one was written first and missed it. */
body.hudCollapsed:not(.radialAbilities) #cmdPanel {
  width: max-content !important;
  max-width: calc(100vw - 28px) !important;
}
body.hudCollapsed:not(.radialAbilities) #cmdPanel > #commandCard {
  width: max-content !important;
  max-width: 100% !important;
}

/* ════════════════════════════════════════════════════════════════════════
   THE MINIMISED ABILITY ROW IS A CORNER BOX. ALWAYS. NO EXCEPTIONS.

   Specificity, ranked, for `width` on a minimised #cmdPanel:
     1,1,1  body.hudCollapsed #cmdPanel                              max-content !important
     1,3,1  body.hudCollapsed:not(.cmdAbilities) #cmdPanel           max-content !important
     1,5,1  body:not(.touch).hudCollapsed:not(.cmdAbilities) #cmdPanel   auto !important

   The last one outranks the other two, and its `:not(.cmdAbilities)` is the
   whole problem: .cmdAbilities is a DOCKED flag, so the rule is excluded when
   docked and applies when flying. Docked, the panel shrink-wrapped to 53px;
   undocked, `width: auto !important` won and it became a full-width box at
   left 0, 780px across on a 780px screen.

   That box is what --min-abil-w measures, and the stat strip's right edge is
   calc(--min-abil-w + 14px) -- so undocked the strip's right edge was pushed
   off the left of the screen, its width went negative, and min-width:240px
   took over. 240 undocked against 534 docked: the two HUDs.

   Matched at 1,5,1 so it outranks all three, and position/left/right are
   pinned as well because the measurement also showed left:0 -- the corner
   anchor at `body.hudCollapsed #cmdPanel` (1,1,1, not important) was losing
   too. Nothing about a corner box depends on a docked tab flag.
   ════════════════════════════════════════════════════════════════════════ */
body.hudCollapsed:not(.radialAbilities) #cmdPanel,
body:not(.touch).hudCollapsed:not(.cmdAbilities):not(.radialAbilities) #cmdPanel,
body.touch.hudCollapsed:not(.abilitiesOpen):not(.cmdAbilities):not(.radialAbilities) #cmdPanel {
  position: fixed !important;
  left: auto !important;
  right: 14px !important;
  width: max-content !important;
  max-width: calc(100vw - 28px) !important;
}

/* ...and the last one that outranked it: `body.touch.cmdAbilities #cmdPanel`
   sets `width: var(--console-w) !important` at 1,2 for the DOCKED console,
   which is right when the console is on screen and wrong when the HUD is
   minimised and the panel is a corner box. Matched here with .hudCollapsed
   added so this wins only while minimised, and the docked console keeps its
   width everywhere else. */
body.touch.hudCollapsed.cmdAbilities:not(.radialAbilities) #cmdPanel,
body.hudCollapsed.cmdAbilities:not(.radialAbilities) #cmdPanel {
  position: fixed !important;
  left: auto !important;
  right: 14px !important;
  width: max-content !important;
  max-width: calc(100vw - 28px) !important;
}

/* NO BACK TILE ON TOUCH.
   #toUpgrades switches the docked console from its abilities tab to its refit
   tab. On touch there is no refit tab to switch to -- `body.touch.atBase
   #upgPanel { display: none !important }` takes it off the screen and the
   REFIT button in the top corner (#rfOpen) is how the refit page is reached.
   So the tile navigated nowhere and drew a lone box captioned BACK. */
body.touch #toUpgrades { display: none !important; }


/* ════════════════════════════════════════════════════════════════════════
   THE STEP CARD STANDS DOWN WHILE THE SHOP IS OPEN.

   Three attempts at sharing the screen with the refit panel, all wrong:
   behind it (unreadable), on top of it (a lid over the tiles), and above it in
   its own strip (squeezed the grid and still looked like clutter). On a
   landscape handset that panel IS the screen; there is no arrangement where
   both fit and neither is in the way.

   So it does not share. The step is SPOKEN -- the pilot reads it aloud -- so
   nothing is lost by taking the card off the screen while the player is
   actually shopping, and everything is gained: the full grid, every tile
   visible, nothing intercepting a tap. It comes back the moment the panel is
   closed.
   ════════════════════════════════════════════════════════════════════════ */
body.touch.refitOpen #tutBox { display: none !important; }


/* THE NEXT PRESS, MADE OBVIOUS.
   Selecting a tile fills the detail bar and buys nothing -- the purchase is a
   second press on this button. Nothing said so, and "I assumed clicking it
   just upgraded the ability" is the only reasonable reading of a shop where
   the tiles are the biggest thing on screen. It pulses for four seconds after
   every pick, then settles. */
@keyframes rfBuyPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 205, 90, .85), 0 2px 10px rgba(0,0,0,.45); transform: scale(1); }
  50%      { box-shadow: 0 0 0 7px rgba(255, 205, 90, 0), 0 2px 10px rgba(0,0,0,.45); transform: scale(1.045); }
}
#rfBuy.rfPulse {
  animation: rfBuyPulse 1.15s ease-in-out infinite;
  outline: 2px solid rgba(255, 226, 150, .95);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  #rfBuy.rfPulse { animation: none; }
}

/* Supporting kill credit stays subordinate and wraps on narrow screens. */
.kbAssists, .kfAssists { margin-top: 4px; color: #90a8ba; font-size: 10.5px; line-height: 1.4; font-weight: 500; letter-spacing: .2px; white-space: normal; overflow-wrap: anywhere; }
.kbAssists { max-width: 340px; margin-left: auto; margin-right: auto; }
.kfRow { max-width: min(420px, calc(100vw - 24px)); box-sizing: border-box; white-space: normal; overflow-wrap: anywhere; }
.kbCard { max-width: calc(100vw - 24px); }
.kbLine { flex-wrap: wrap; }

#unitStats .scLance { color:#c4a0ed; font-size:11px; letter-spacing:.07em; margin:4px 0; }
#unitStats .scLance b { color:#efdfff; margin-left:8px; }
body.touch #unitStats .scLance { grid-column:1 / -1; font-size:10px; margin:2px 0; }

/* Mobile refit window: 80% of its previous overall dimensions. */
body.touch #refitScreen:not(.hidden) { transform: scale(.8); transform-origin: center; }

.cmd.ability[aria-disabled="true"] > *:not(.tip) { opacity: .38; }
/* Readable combat feedback in both the full console and compact ability row. */
#outpostAttackAlert {
  position:fixed;top:72px;right:18px;z-index:55;pointer-events:none;
  color:#ffd092;font-size:12px;font-weight:800;letter-spacing:1px;
  max-width:35vw;text-align:right;text-shadow:0 2px 4px #000,0 0 8px #000;
  background:none;border:0;box-shadow:none;
}
#outpostAttackAlert[hidden] { display:none; }
.cmd.ability .abilityCountdown {
  position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  z-index:5;pointer-events:none;font-size:clamp(23px,3vw,36px);font-weight:900;
  line-height:1;color:#fff;text-shadow:0 2px 4px #000,1px 0 2px #000;
  background:rgba(0,0,0,.3);border-radius:inherit;
}
.cmd.ability .abilityCountdown:empty { display:none; }
.cmd.ability.isCooling .cCost { visibility:hidden; }
.cmd.ability.isCooling {opacity:1 !important;filter:none !important;}
.cmd.ability.isCooling .aico {opacity:.48;}
.cmd.ability.isCooling > .abilityCountdown {opacity:1;}
.cmd.ability.abilityReadyFlash { animation:abilityReady .65s ease-out; }
@keyframes abilityReady {
  0% {box-shadow:inset 0 0 25px #fff,0 0 18px #8ef1ff;filter:brightness(1.65);}
  100% {box-shadow:inset 0 0 0 transparent,0 0 0 transparent;filter:brightness(1);}
}
body.hudCollapsed #infoPanel > #unitBuffs {
  position:absolute;display:flex;flex-direction:row;flex-wrap:wrap;gap:4px;
  top:auto;bottom:calc(100% + 8px);right:auto;left:50%;transform:translateX(-50%);max-width:min(340px,65vw);
  pointer-events:auto;
}
body.hudCollapsed #infoPanel > #unitBuffs:empty {display:none;}
body.hudCollapsed #unitBuffs .bfChip .tip {top:auto;bottom:calc(100% + 8px);}
@media (pointer:coarse) {#outpostAttackAlert {top:66px;right:12px;font-size:10px;max-width:42vw;}}
@media (prefers-reduced-motion:reduce) {.cmd.ability.abilityReadyFlash {animation:none;box-shadow:0 0 8px #8ef1ff;}}
