/* ui/Hud — contract: ./Hud.spec.md. Tokens from ui/Theme. */

.sp-hud {
  --ship: var(--sp-accent);
  --banner-size: clamp(26px, 4.4vw, 54px); /* banner text size; the respawn overlay clears it */
  position: fixed;
  inset: 0;
  z-index: 10;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  /* top readouts · centre (banner, respawn) · minimap · bottom panels.
     The centre row shrinks first, so nothing is pushed off-screen. */
  grid-template-rows: auto minmax(0, 1fr) auto auto;
  gap: 16px 24px;
  padding: 20px 24px 22px;
  font-size: 13px;
  pointer-events: none; /* never block mouse aim… */
  user-select: none;
}

/* Identity colours for anything tagged with a ship or weapon id. */
.sp-hud,
.sp-hud [data-ship] { --ship: var(--sp-accent); }
.sp-hud[data-ship="laser"],
.sp-hud [data-ship="laser"] { --ship: var(--sp-ship-laser); }
.sp-hud[data-ship="tracerTank"],
.sp-hud [data-ship="tracerTank"] { --ship: var(--sp-ship-laser); }
.sp-hud[data-ship="plasma"],
.sp-hud [data-ship="plasma"] { --ship: var(--sp-ship-plasma); }
.sp-hud[data-ship="heTank"],
.sp-hud [data-ship="heTank"] { --ship: var(--sp-ship-plasma); }
.sp-hud[data-ship="rocket"],
.sp-hud [data-ship="rocket"] { --ship: var(--sp-ship-rocket); }

/* Streak tiers: cyan → violet → gold → white-hot singularity. */
.sp-hud [data-tier] { --tier: var(--sp-text-dim); }
.sp-hud [data-tier="1"],
.sp-hud [data-tier="2"] { --tier: var(--sp-accent-2); }
.sp-hud [data-tier="3"],
.sp-hud [data-tier="4"] { --tier: var(--sp-accent); }
.sp-hud [data-tier="5"],
.sp-hud [data-tier="6"] { --tier: var(--sp-rarity-legendary); }
.sp-hud [data-tier="7"] { --tier: var(--sp-text); }

/* Announcement kinds (after the ladder, so they win). A tier 0 banner reads in
   full text colour. Arena reveals are cyan, arena event warnings gold, and
   kinds from custom rules stay neutral whatever their tier. */
.sp-banner[data-tier="0"] { --tier: var(--sp-text); }
.sp-banner[data-kind]:not([data-kind="streak"], [data-kind="level"], [data-kind="arena"], [data-kind="event"]) { --tier: var(--sp-text); }
.sp-log__entry[data-kind]:not([data-kind="streak"], [data-kind="level"], [data-kind="arena"], [data-kind="event"]) { --tier: var(--sp-text-dim); }
.sp-banner[data-kind="arena"],
.sp-log__entry[data-kind="arena"] { --tier: var(--sp-accent-2); }
.sp-banner[data-kind="event"],
.sp-log__entry[data-kind="event"] { --tier: var(--sp-rarity-legendary); }
/* Daily Ops progress (Mission lines): the currency's colour, fainter until complete. */
.sp-log__entry[data-kind="mission"] { --tier: color-mix(in oklab, var(--sp-quanta) 75%, transparent); }
.sp-log__entry[data-kind="mission"]:not([data-tier="0"]) { --tier: var(--sp-quanta); }

/* ------------------------------------------------------ announcer log --- */

/* The top-left column: the latency pill, then the announcer log under it. The
   pill leads so its position never moves with the log's length (claims 91, 92). */
.sp-hud__left {
  grid-column: 1;
  grid-row: 1;
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: 8px;
  min-width: 0;
}

.sp-hud__announcer {
  display: flex;
  flex-direction: column;
  gap: 3px;
  max-width: 360px;
}

.sp-log__entry {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 5px 12px 5px 12px;
  background: linear-gradient(90deg, color-mix(in oklab, var(--sp-space-0) 72%, transparent), transparent);
  border-left: 2px solid var(--tier);
  animation: sp-hud-in-left var(--sp-dur) var(--sp-ease-out) both;
}

.sp-log__entry:nth-child(n + 4) {
  opacity: 0.5;
}

.sp-log__text {
  min-width: 0;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  /* One line in English; a longer language wraps inside the column rather than past it (docs/i18n.md §6). */
  overflow-wrap: break-word;
  color: var(--tier);
}

.sp-log__entry[data-tier="7"]:is(:not([data-kind]), [data-kind="streak"]) .sp-log__text {
  text-shadow: 0 0 12px var(--sp-accent);
}

.sp-log__subject {
  font-size: 12px;
  color: var(--sp-text-dim);
}

.sp-log__entry[data-local] .sp-log__subject {
  color: var(--sp-text);
}

/* ---------------------------------------------------------- kill feed --- */

/* Rows stay on one line. The feed fills its column (a size container, so it
   cannot size itself from its rows) and each row is capped to it, so when
   space runs out the names truncate; badges and weapon labels never give. */
.sp-hud__feed {
  grid-column: 3;
  grid-row: 1;
  justify-self: stretch;
  container-type: inline-size;
  container-name: sp-feed;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 4px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.sp-feed__entry {
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: 100%;
  padding: 5px 10px;
  background: var(--sp-glass);
  border: 1px solid var(--sp-line);
  border-radius: 7px;
  backdrop-filter: blur(6px);
  animation: sp-hud-in-right var(--sp-dur) var(--sp-ease-out) both;
}

.sp-feed__entry:has([data-local]) {
  border-color: color-mix(in oklab, var(--sp-accent-2) 45%, transparent);
}

.sp-feed__player {
  min-width: 0;
  max-width: 12em;
  overflow: hidden;
  /* Room below the text for the local player's underline, which the overflow
     would otherwise clip; the negative margin keeps the row height unchanged. */
  padding-bottom: 6px;
  margin-bottom: -6px;
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: color-mix(in oklab, var(--ship) 80%, var(--sp-text));
}

/* The same crew pair `.sp-match__team` uses, so the feed, the scoreline and the
   arena cannot disagree about which crew is which — claim 134 reads every
   [data-team] rule in this file against client/Renderer's TEAM_COLOR_KEYS. */
.sp-feed__player[data-team="0"] { --ship: var(--sp-accent-2); }

.sp-feed__player[data-team="1"] { --ship: var(--sp-rarity-legendary); }

.sp-feed__player[data-local] {
  color: var(--sp-text);
  text-decoration: underline;
  text-decoration-color: var(--ship);
  text-decoration-thickness: 2px;
  text-underline-offset: 4px;
}

.sp-feed__weapon {
  --weapon: var(--sp-text-dim);
  flex: none;
  padding: 3px 6px;
  font: 600 10px/1 var(--sp-font-mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--weapon);
  border: 1px solid color-mix(in oklab, var(--weapon) 45%, transparent);
  border-radius: 4px;
}

/* A blueprint gun's kill: its line's drawn mark in the line's colour, no word. */
.sp-feed__weapon--line {
  display: inline-grid;
  place-items: center;
  width: 30px;
  height: 18px;
  padding: 0;
}

.sp-feed__line {
  width: 22px;
  height: 14px;
  background: var(--weapon);
  filter: drop-shadow(0 0 3px color-mix(in oklab, var(--weapon) 60%, transparent));
  -webkit-mask: var(--icon) center / contain no-repeat;
  mask: var(--icon) center / contain no-repeat;
}

.sp-feed__weapon[data-line="blaster"] { --weapon: var(--sp-ship-laser); --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 12h15' stroke-width='3.2'/%3E%3Cpath d='M6 9v6' stroke-width='1.6'/%3E%3Ccircle cx='20' cy='12' r='2.4' fill='black' stroke='none'/%3E%3C/svg%3E"); }
.sp-feed__weapon[data-line="scatter"] { --weapon: var(--sp-ship-scatter); --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 12h9' stroke-width='3'/%3E%3Cpath d='M11 12l9-6M11 12h10M11 12l9 6' stroke-width='1.8'/%3E%3C/svg%3E"); }
.sp-feed__weapon[data-line="rocket"] { --weapon: var(--sp-ship-rocket); --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 8h12v8H2z' fill='black'/%3E%3Cpath d='M14 8l6 4-6 4z' fill='black'/%3E%3C/svg%3E"); }

.sp-feed__weapon[data-weapon="laser"] { --weapon: var(--sp-ship-laser); }
.sp-feed__weapon[data-weapon="plasma"] { --weapon: var(--sp-ship-plasma); }
.sp-feed__weapon[data-weapon="rocket"] { --weapon: var(--sp-ship-rocket); }

/* Tier pips: one filled diamond per evolution form, tucked against the name
   like the mastery badge. The name's own colour, dimmed; three (the Apex) in
   the legendary gold the Apex badge and banner use. Static, and whole at every
   feed width: three glyphs are the read (evolution-overhaul.md §2.7). */
.sp-feed__tier {
  position: relative; /* contains the visually hidden text */
  flex: none;
  margin-left: -6px; /* the badge's rule: the pips belong to their name */
  font: 700 12px/1 var(--sp-font-mono);
  letter-spacing: 0.02em;
  white-space: nowrap;
  color: color-mix(in oklab, var(--sp-text-dim) 60%, var(--sp-accent));
}

.sp-feed__tier[data-forms="3"] {
  color: var(--sp-rarity-legendary);
  text-shadow: 0 0 6px color-mix(in oklab, var(--sp-rarity-legendary) 55%, transparent);
}

/* Mastery badge: a small hexagon holding the pilot's level, tucked against the
   name it follows and tinted by tier. A tinted rim (::before) frames a dark
   face (::after); both are clipped to the hexagon. Static: no animation. */
.sp-feed__mastery {
  --mastery: var(--sp-rarity-common);
  --notch: 4px;
  position: relative; /* contains the visually hidden text */
  isolation: isolate;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 26px;
  height: 16px;
  margin-left: -5px; /* half the row gap: the badge belongs to its name */
  padding: 0 calc(var(--notch) + 2px);
  font: 700 10px/1 var(--sp-font-mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: color-mix(in oklab, var(--mastery) 68%, var(--sp-text));
}

.sp-feed__mastery::before,
.sp-feed__mastery::after {
  content: "";
  position: absolute;
  z-index: -1;
  clip-path: polygon(
    var(--notch) 0, calc(100% - var(--notch)) 0, 100% 50%,
    calc(100% - var(--notch)) 100%, var(--notch) 100%, 0 50%
  );
}

.sp-feed__mastery::before {
  inset: 0;
  background: color-mix(in oklab, var(--mastery) 75%, transparent);
}

.sp-feed__mastery::after {
  inset: 1px;
  background: color-mix(in oklab, var(--mastery) 16%, var(--sp-space-0));
}

.sp-feed__mastery[data-tier="common"] { --mastery: var(--sp-rarity-common); }
.sp-feed__mastery[data-tier="rare"] { --mastery: var(--sp-rarity-rare); }
.sp-feed__mastery[data-tier="epic"] { --mastery: var(--sp-rarity-epic); }
.sp-feed__mastery[data-tier="legendary"] {
  --mastery: var(--sp-rarity-legendary);
  filter: drop-shadow(0 0 3px color-mix(in oklab, var(--sp-rarity-legendary) 60%, transparent));
}

/* A very narrow feed column: even fully truncated names would leave no room,
   so badges step aside. Visually only, like .sp-visually-hidden: their
   ", mastery n" text stays for screen readers. */
@container sp-feed (width < 240px) {
  .sp-feed__mastery {
    position: absolute;
    width: 1px;
    min-width: 0;
    height: 1px;
    margin: 0;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
  }
}

/* ---------------------------------------------------- standing readout --- */

/* The top-centre stack: the standing readout, and the latency pill under it.
   The pill hangs off the panel's real bottom edge — this gap is its whole
   clearance — so no length here tracks the panel's height, and changing what
   the panel holds can never leave the pill stranded or overlapping. */
.sp-hud__top {
  grid-column: 2;
  grid-row: 1;
  justify-self: center;
  align-self: start;
  display: grid;
  justify-items: center;
  gap: 8px;
}

/* Where you stand is the whole point of the match, so the place is the biggest
   type in the HUD's top row; the clock, which no pilot can change, sits above
   it small, and the two gaps that decide the place sit under it.

   The width is fixed, not fluid: `ui/Settings` parks the gear 92px right of
   centre over the arena and this panel is centred, so 168px keeps 8px clear of
   it at every viewport. Long names truncate rather than push into the gear. */
.sp-hud__match {
  display: grid;
  justify-items: stretch;
  gap: 3px;
  width: 168px;
  padding: 8px 13px 9px;
  background: var(--sp-glass);
  border: 1px solid var(--sp-line);
  border-radius: var(--sp-radius);
  backdrop-filter: blur(8px);
  transition: border-color var(--sp-dur), box-shadow var(--sp-dur);
}

.sp-hud__match:has([data-warning]) {
  border-color: color-mix(in oklab, var(--sp-danger) 50%, transparent);
  box-shadow: 0 0 28px -14px var(--sp-danger);
}

/* Clock on the left, whoever is leading on the right. */
.sp-match__top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
}

.sp-match__clock {
  font-family: var(--sp-font-mono);
  font-size: 15px;
  font-weight: 500;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
  color: var(--sp-text-dim);
  transition: color var(--sp-dur), text-shadow var(--sp-dur);
}

.sp-match__clock[data-warning] {
  font-weight: 700;
  color: var(--sp-danger);
  text-shadow: 0 0 18px color-mix(in oklab, var(--sp-danger) 55%, transparent);
  animation: sp-clock-warn 1s ease-in-out infinite alternate;
}

.sp-match__leader {
  display: flex;
  align-items: baseline;
  gap: 4px;
  min-width: 0;
  font-size: 10px;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--sp-rarity-legendary);
}

.sp-match__leader::before {
  content: "★";
  flex: none;
  font-size: 9px;
  line-height: 1;
}

.sp-match__leader-name {
  min-width: 0;
  max-width: 7em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: color-mix(in oklab, var(--sp-rarity-legendary) 55%, var(--sp-text));
}

/* The place, then the field size over the kills beside it. `display: contents`
   on the rank wrapper puts all three in this one grid. */
.sp-match__standing {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  column-gap: 9px;
  margin: 1px 0 2px;
  line-height: 1;
  white-space: nowrap;
}

.sp-match__rank {
  display: contents;
}

.sp-match__place {
  grid-area: 1 / 1 / span 2 / auto;
  font-family: var(--sp-font-mono);
  font-size: 38px;
  font-weight: 300;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--sp-text);
  animation: sp-place-in 420ms var(--sp-ease-out) both;
}

/* First place is worth looking at. */
.sp-match__place[data-rank="1"] {
  color: var(--sp-rarity-legendary);
  text-shadow: 0 0 24px color-mix(in oklab, var(--sp-rarity-legendary) 45%, transparent);
}

.sp-match__suffix {
  font-size: 0.42em;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.sp-match__field,
.sp-match__scope,
.sp-match__kills {
  overflow: hidden;
  font-size: 10px;
  line-height: 1.3; /* its own box tall enough for its letters: the overflow clipped their feet */
  font-weight: 600;
  letter-spacing: 0.14em;
  text-overflow: ellipsis;
  text-transform: uppercase;
  color: var(--sp-text-faint);
}

.sp-match__field {
  grid-area: 1 / 2;
  align-self: end;
  padding-bottom: 2px;
}

.sp-match__kills {
  grid-area: 2 / 2;
  align-self: start;
  padding-top: 2px;
  font-family: var(--sp-font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--sp-text-dim);
}

/* A team mode measures the place inside the crew (Hud.spec.md, Modes), so a
   third small line names it and the place spans all three. The place keeps the
   column and the size it has in every other mode. */
.sp-match__scope {
  grid-area: 2 / 2;
  align-self: center;
  white-space: nowrap;
  color: color-mix(in oklab, var(--sp-text-faint) 70%, var(--sp-text-dim));
}

.sp-match__standing:has(.sp-match__scope) .sp-match__place {
  grid-row: 1 / span 3;
}

.sp-match__standing:has(.sp-match__scope) .sp-match__field {
  padding-bottom: 0;
}

.sp-match__standing:has(.sp-match__scope) .sp-match__kills {
  grid-area: 3 / 2;
  padding-top: 0;
}

/* What the mode is scored on, between the clock and the place (Hud.spec.md,
   Modes). Free-for-All renders none of this, so the panel is unchanged. */
.sp-match__mode {
  display: grid;
  justify-items: stretch;
  gap: 4px;
  width: 100%;
  min-width: 0;
  margin: 2px 0 1px;
  padding: 5px 0 0;
  border-top: 1px solid color-mix(in oklab, var(--sp-line) 85%, transparent);
}

.sp-match__mode-name {
  overflow: hidden;
  font-size: 9px;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: 0.06em; /* "Drużynowy deathmatch", "Командный бой" fit the 168 px panel */
  text-overflow: ellipsis;
  white-space: nowrap;
  text-transform: uppercase;
  color: var(--sp-text-faint);
}

/* A scoreline, not two boxes: at 168px a bordered chip spends its width on
   padding and truncates the crew name, which is the one word that has to read.
   Each crew takes half the panel and no more, so a long name truncates instead
   of pushing its score out. */
.sp-match__teams {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
}

.sp-match__team {
  --crew: var(--sp-text-dim);
  display: flex;
  align-items: baseline;
  gap: 5px;
  min-width: 0;
  font-size: 10px;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* The second crew reads from the right, so the two scores sit at the panel's
   edges and the eye finds the gap between them without reading either. */
.sp-match__team:last-child {
  flex-direction: row-reverse;
}

/* The two crews, in the tokens client/Renderer's TEAM_COLOR_KEYS names, so a
   chip here is the colour of the rim out there. A ui/* block cannot import a
   client/* one, so this pair is a second copy of that constant — CLAIM 134
   derives the pair from client/Renderer and compares it to what is written
   here, at every [data-team] rule in this file. Rename a token at either end
   and 134 fails; add a third crew colour and it fails too. */
.sp-match__team[data-team="0"] { --crew: var(--sp-accent-2); }

.sp-match__team[data-team="1"] { --crew: var(--sp-rarity-legendary); }

/* Yours, marked with a lozenge as well as with colour. */
.sp-match__team[data-local]::before {
  content: "◆";
  flex: none;
  align-self: center;
  font-size: 7px;
  color: var(--crew);
}

.sp-match__team[data-local] .sp-match__team-name {
  color: var(--crew);
}

.sp-match__team[data-leading] .sp-match__team-score {
  color: var(--crew);
  text-shadow: 0 0 12px color-mix(in oklab, var(--crew) 45%, transparent);
}

.sp-match__team-name {
  flex: 1;
  min-width: 0;
  letter-spacing: 0.02em; /* CYJAN, GOLD in translation: the name is the one word that has to read */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: color-mix(in oklab, var(--crew) 55%, var(--sp-text));
}

.sp-match__team-score {
  flex: none;
  font-family: var(--sp-font-mono);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--sp-text);
}

/* Who wears the crown, what is left of it, and a bar under both. */
.sp-match__crown {
  --crown: var(--sp-rarity-legendary);
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 4px 8px;
  width: 100%;
  min-width: 0;
  padding: 3px 6px 4px;
  border: 1px solid color-mix(in oklab, var(--crown) 30%, transparent);
  border-radius: var(--sp-radius-sm);
  background: color-mix(in oklab, var(--crown) 10%, transparent);
  font-size: 10px;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.sp-match__crown[data-local] {
  border-color: color-mix(in oklab, var(--crown) 65%, transparent);
  background: color-mix(in oklab, var(--crown) 20%, transparent);
}

.sp-match__crown-holder {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--sp-text);
}

/* Decorative: the holder is named in words either side of it. */
.sp-match__crown-holder::before {
  content: "♛ ";
  color: var(--crown);
}

.sp-match__crown-state {
  white-space: nowrap;
  font-size: 9px;
  color: color-mix(in oklab, var(--crown) 60%, var(--sp-text-dim));
}

.sp-match__crown-gauge {
  grid-column: 1 / -1;
  height: 3px;
  border-radius: 2px;
  background: color-mix(in oklab, var(--sp-line-strong) 70%, transparent);
  background-image: linear-gradient(to right, var(--crown) 0 calc(var(--fill, 0) * 100%), transparent calc(var(--fill, 0) * 100%));
  box-shadow: 0 0 10px -4px var(--crown);
  transition: background-image var(--sp-dur) linear;
}

/* The two gaps that decide the place: who is above, and who is closing in. */
/* Two rows' worth of height whether or not both rivals exist, so the panel
   (and the latency pill under it) never jumps as you take the lead or last. */
.sp-match__rivals {
  display: grid;
  gap: 2px;
  width: 100%;
  min-height: 36px; /* two rows, plus this list's own padding and rule */
  margin: 1px 0 0;
  padding: 6px 0 0;
  border-top: 1px solid color-mix(in oklab, var(--sp-line) 85%, transparent);
  list-style: none;
}

.sp-match__rival {
  --side: var(--sp-text-faint);
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-size: 10px;
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* Decorative: the words "ahead" and "behind" carry which side a rival is on. */
.sp-match__rival::before {
  content: "▲";
  flex: none;
  font-size: 8px;
  color: var(--side);
}

.sp-match__rival[data-side="ahead"] { --side: var(--sp-accent-2); }

.sp-match__rival[data-side="behind"] { --side: var(--sp-danger); }

.sp-match__rival[data-side="behind"]::before {
  content: "▼";
}

.sp-match__rival-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--sp-text-dim);
}

.sp-match__rival-gap {
  flex: none;
  font-family: var(--sp-font-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
  color: color-mix(in oklab, var(--side) 55%, var(--sp-text));
}

/* A place taken rises into its slot and flares; a place lost settles in from
   above, smaller and without the glow — the same asymmetry as the sound. */
.sp-match__place[data-move="up"] {
  animation-name: sp-place-up;
  animation-duration: 620ms;
}

.sp-match__place[data-move="down"] {
  animation-name: sp-place-down;
  animation-duration: 380ms;
}

@keyframes sp-clock-warn {
  to { opacity: 0.6; }
}

@keyframes sp-place-in {
  from { opacity: 0; }
}

@keyframes sp-place-up {
  0% { opacity: 0; transform: translateY(0.36em) scale(0.82); color: var(--sp-accent-2); text-shadow: 0 0 32px var(--sp-accent-2); }
  45% { opacity: 1; transform: translateY(-0.05em) scale(1.14); color: var(--sp-accent-2); text-shadow: 0 0 26px color-mix(in oklab, var(--sp-accent-2) 70%, transparent); }
  100% { opacity: 1; transform: none; }
}

@keyframes sp-place-down {
  0% { opacity: 0.15; transform: translateY(-0.22em); color: var(--sp-danger); }
  55% { color: color-mix(in oklab, var(--sp-danger) 60%, var(--sp-text)); }
  100% { opacity: 1; transform: none; }
}

/* ------------------------------------------------------------- banner --- */

.sp-hud__banner-slot {
  grid-column: 1 / -1;
  grid-row: 2;
  justify-self: center;
  align-self: start;
  margin-top: 8vh;
}

.sp-banner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  text-align: center;
  animation: sp-banner-in 720ms var(--sp-ease-out) both;
}

.sp-banner__text {
  font-size: var(--banner-size);
  font-weight: 300;
  line-height: 1;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  text-transform: uppercase;
  /* One line in English; a longer language breaks into balanced lines inside the screen (docs/i18n.md §6). */
  max-width: calc(100vw - 32px);
  text-wrap: balance;
  overflow-wrap: break-word;
  color: var(--tier);
  text-shadow: 0 0 28px color-mix(in oklab, var(--tier) 55%, transparent);
}

.sp-banner__subject {
  display: flex;
  align-items: center;
  gap: 14px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--sp-text-dim);
}

.sp-banner__subject::before,
.sp-banner__subject::after {
  content: "";
  width: 90px;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--tier));
  animation: sp-banner-rule 720ms var(--sp-ease-out) both;
}

.sp-banner__subject::after {
  transform: scaleX(-1);
}

/* Streak tiers 5+ get heavier type; the singularity collapses inward with a
   halo. Other kinds never take these treatments. */
.sp-banner[data-tier="5"]:is(:not([data-kind]), [data-kind="streak"]) .sp-banner__text,
.sp-banner[data-tier="6"]:is(:not([data-kind]), [data-kind="streak"]) .sp-banner__text {
  font-weight: 400;
}

.sp-banner[data-tier="7"]:is(:not([data-kind]), [data-kind="streak"]) .sp-banner__text {
  font-weight: 200;
  letter-spacing: 0.44em;
  text-indent: 0.44em;
  text-shadow:
    0 0 18px var(--sp-accent),
    0 0 60px var(--sp-accent);
}

.sp-banner[data-tier="7"]:is(:not([data-kind]), [data-kind="streak"]) {
  animation-name: sp-banner-collapse;
  animation-duration: 900ms;
}

/* Arena reveal: calm. Light type, very wide tracking, a slow blur-in. */
.sp-banner[data-kind="arena"] {
  animation: sp-banner-reveal 1600ms var(--sp-ease-out) both;
}

.sp-banner[data-kind="arena"] .sp-banner__text {
  font-size: calc(var(--banner-size) * 0.8);
  font-weight: 200;
  letter-spacing: 0.56em;
  text-indent: 0.56em;
  text-shadow: 0 0 36px color-mix(in oklab, var(--tier) 45%, transparent);
}

/* A banner's explanation line. Sentence case and readable rather than the
   spaced small-caps a subject gets, because this one is meant to be *read*:
   the mode's name says which game it is and only this says what winning it
   means. */
.sp-banner__detail {
  max-width: 34ch;
  margin-top: 2px;
  font-size: 15px;
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0.01em;
  text-align: center;
  text-wrap: balance;
  color: var(--sp-text-dim);
}

@media (max-width: 720px) {
  .sp-banner__detail { max-width: 26ch; font-size: 13px; }
}

/* Longer rules, should an arena announcement ever carry a subject. */
.sp-banner[data-kind="arena"] .sp-banner__subject::before,
.sp-banner[data-kind="arena"] .sp-banner__subject::after {
  width: 150px;
}

/* Arena event warning: heavier, smaller gold type that pulses gently once it
   has landed. The words ("… incoming") carry the warning; colour reinforces. */
.sp-banner[data-kind="event"] .sp-banner__text {
  font-size: calc(var(--banner-size) * 0.72);
  font-weight: 400;
  letter-spacing: 0.24em;
  text-indent: 0.24em;
  text-shadow: 0 0 26px color-mix(in oklab, var(--tier) 50%, transparent);
  animation: sp-event-pulse 1200ms ease-in-out 720ms infinite alternate;
}

@keyframes sp-banner-in {
  0% { opacity: 0; transform: scale(1.22); filter: blur(10px); }
  55% { opacity: 1; filter: blur(0); }
  100% { opacity: 1; transform: scale(1); }
}

@keyframes sp-banner-collapse {
  0% { opacity: 0; transform: scale(2.2); filter: blur(18px); }
  60% { opacity: 1; transform: scale(0.96); filter: blur(0); }
  100% { opacity: 1; transform: scale(1); }
}

@keyframes sp-banner-rule {
  from { width: 0; opacity: 0; }
}

@keyframes sp-banner-reveal {
  0% { opacity: 0; transform: scale(1.04); filter: blur(14px); }
  100% { opacity: 1; transform: scale(1); filter: blur(0); }
}

@keyframes sp-event-pulse {
  to { opacity: 0.62; }
}

/* ------------------------------------------------------------ respawn --- */

/* Shares the banner's row and sits clear below the tallest banner, so the two
   never overlap. No transform on this element: its ::before is fixed to the
   viewport. */
.sp-respawn {
  grid-column: 1 / -1;
  grid-row: 2;
  justify-self: center;
  align-self: start;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  margin-top: calc(8vh + var(--banner-size) + 84px);
  padding: 26px 64px;
  text-align: center;
  /* A soft dark halo keeps the text legible over busy arena effects. */
  background: radial-gradient(closest-side, color-mix(in oklab, var(--sp-space-0) 78%, transparent), transparent);
  animation: sp-respawn-fade 600ms ease-out both;
}

/* A faint danger vignette while destroyed, behind every HUD region. */
.sp-respawn::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background: radial-gradient(
    ellipse 75% 70% at 50% 50%,
    transparent 45%,
    color-mix(in oklab, var(--sp-danger) 9%, color-mix(in oklab, var(--sp-space-0) 55%, transparent))
  );
  pointer-events: none;
}

.sp-respawn__title {
  margin: 0;
  font-size: clamp(18px, 2.2vw, 28px);
  font-weight: 200;
  line-height: 1;
  letter-spacing: 0.34em;
  text-indent: 0.34em; /* balances the trailing letter-spacing */
  text-transform: uppercase;
  /* Wraps, balanced, only when a language runs past the screen (docs/i18n.md §6). */
  max-width: calc(100vw - 32px);
  text-wrap: balance;
  overflow-wrap: break-word;
  color: var(--sp-text);
  text-shadow: 0 0 24px color-mix(in oklab, var(--sp-space-0) 80%, transparent);
}

.sp-respawn__killer {
  font-weight: 400;
  color: color-mix(in oklab, var(--sp-danger) 70%, var(--sp-text));
}

.sp-respawn__rule {
  display: block;
  width: min(380px, 70vw);
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--sp-danger) 50%, transparent);
  filter: drop-shadow(0 0 5px var(--sp-danger));
  animation: sp-banner-rule 720ms var(--sp-ease-out) both;
}

.sp-respawn__countdown {
  margin: 0;
  font-size: 11px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.26em;
  text-indent: 0.26em;
  text-transform: uppercase;
  color: var(--sp-text-dim);
}

.sp-respawn__seconds {
  display: inline-block;
  min-width: 1.2em;
  font: 500 15px/1 var(--sp-font-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  color: var(--sp-text);
}

/* The ship is gone: its own panels step back. */
.sp-hud:has(> .sp-respawn) .sp-hud__vitals,
.sp-hud:has(> .sp-respawn) .sp-hud__special {
  opacity: 0.55;
}

@keyframes sp-respawn-fade {
  from { opacity: 0; }
}

/* --------------------------------------------------------- connection --- */

/* Latency readout: a small pill under the standing readout, in the top-centre
   stack, so it reads as part of it and stays clear of the announcer log and
   kill feed on either side. It is placed by following the panel in that stack
   and by nothing else: with no readout above it, it simply moves up. */
.sp-hud__net {
  --net: var(--sp-danger);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 11px 5px 10px;
  font: 500 11px/1 var(--sp-font-mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--sp-text-dim);
  background: var(--sp-glass);
  border: 1px solid var(--sp-line);
  border-radius: 999px;
  backdrop-filter: blur(8px);
}

.sp-hud__net[data-quality="good"] { --net: var(--sp-accent-2); }
.sp-hud__net[data-quality="fair"] { --net: var(--sp-rarity-legendary); }
.sp-hud__net[data-quality="poor"] { --net: var(--sp-danger); }

/* Three ascending bars. How many are lit says the quality without colour. */
.sp-net__bars {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 10px;
}

.sp-net__bar {
  width: 3px;
  height: 40%;
  background: color-mix(in oklab, var(--sp-text) 16%, transparent);
  border-radius: 1px;
}

.sp-net__bar:nth-child(2) { height: 70%; }
.sp-net__bar:nth-child(3) { height: 100%; }

.sp-net__bar[data-lit] {
  background: var(--net);
  box-shadow: 0 0 6px color-mix(in oklab, var(--net) 55%, transparent);
}

/* Step H: the server's city leads the number ("Singapore · 62 ms"), in the
   pill's own face, capped so a long name never widens the pill. */
.sp-net__region {
  display: inline-block;
  max-width: 14ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--sp-text);
}

.sp-net__sep {
  color: var(--sp-text-faint);
}

.sp-hud__net[data-quality="poor"] .sp-net__latency {
  color: color-mix(in oklab, var(--sp-danger) 55%, var(--sp-text));
}

.sp-hud__net[data-state="reconnecting"] .sp-net__bars {
  animation: sp-clock-warn 800ms ease-in-out infinite alternate;
}

/* Reconnecting: a glass panel in the banner's place over a faint veil, short
   enough to clear the respawn overlay beneath it. The glass lives on the inner
   status element: a backdrop-filter here would trap the fixed ::before veil. */
.sp-reconnecting {
  grid-column: 1 / -1;
  grid-row: 2;
  justify-self: center;
  align-self: start;
  margin-top: 8vh;
  animation: sp-respawn-fade 400ms ease-out both;
}

/* The world is on hold: dim the whole arena, behind every HUD region. */
.sp-reconnecting::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background: color-mix(in oklab, var(--sp-space-0) 45%, transparent);
  pointer-events: none;
}

.sp-reconnecting__status {
  display: grid;
  justify-items: center;
  gap: 10px;
  padding: 16px 32px 17px;
  text-align: center;
  background: var(--sp-glass);
  border: 1px solid var(--sp-line-strong);
  border-radius: var(--sp-radius);
  backdrop-filter: blur(10px);
}

.sp-reconnecting__title {
  margin: 0;
  font-size: clamp(16px, 1.7vw, 21px);
  font-weight: 300;
  line-height: 1;
  letter-spacing: 0.34em;
  text-indent: 0.34em; /* balances the trailing letter-spacing */
  text-transform: uppercase;
  max-width: calc(100vw - 32px);
  text-wrap: balance;
  overflow-wrap: break-word;
  color: var(--sp-text);
}

/* Indeterminate progress: a bright segment sweeping along a hairline. */
.sp-reconnecting__scan {
  position: relative;
  display: block;
  width: min(240px, 60vw);
  height: 2px;
  overflow: hidden;
  background: var(--sp-line);
  border-radius: 1px;
}

.sp-reconnecting__scan::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 36%;
  background: linear-gradient(90deg, transparent, var(--sp-accent-2), transparent);
  animation: sp-reconnect-scan 1300ms ease-in-out infinite;
}

.sp-reconnecting__hint {
  margin: 0;
  font-size: 12px;
  line-height: 1.3;
  color: var(--sp-text-dim);
}

/* While reconnecting the overlay takes the banner's place (visually only: the
   banner is aria-hidden anyway), and the ship's own panels step back. */
.sp-hud:has(> .sp-reconnecting) .sp-hud__banner-slot {
  visibility: hidden;
}

.sp-hud:has(> .sp-reconnecting) .sp-hud__vitals,
.sp-hud:has(> .sp-reconnecting) .sp-hud__special {
  opacity: 0.55;
}

@keyframes sp-reconnect-scan {
  from { transform: translateX(-100%); }
  to { transform: translateX(280%); }
}

/* ------------------------------------------------------------ minimap --- */

/* Its own grid row, directly above the vitals. The app sizes the canvas's
   backing store and draws into it; only its CSS size is set here. */
.sp-hud__minimap {
  --minimap-size: 176px;
  grid-column: 1;
  grid-row: 3;
  align-self: end;
  justify-self: start;
  display: grid;
  gap: 10px;
  padding: 12px 14px 14px;
}

.sp-minimap__name {
  max-width: var(--minimap-size);
  overflow: hidden;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.2em;
  text-overflow: ellipsis;
  text-transform: uppercase;
  white-space: nowrap;
  color: color-mix(in oklab, var(--sp-accent-2) 75%, var(--sp-text));
}

.sp-minimap__canvas {
  display: block;
  width: var(--minimap-size);
  height: var(--minimap-size);
  background: color-mix(in oklab, var(--sp-space-0) 60%, transparent);
  border-radius: var(--sp-radius-sm);
}

/* --------------------------------------------------- first-match hint --- */

/* **The camera is locked to the player, so their ship is at the middle of the
   screen on every frame.** The strip is therefore placed by its distance from
   that middle — not by which grid cell happens to be free — and it is taken
   out of the grid entirely, so no later change to the rows can slide it back
   under the ship. Every placement below is written as `50% ± the clearance`
   inside a `max()`, which can only ever push it further out.

   It takes no clicks either, so on a phone it cannot swallow a thumb meant for
   a stick. */
.sp-hud__hint {
  /* The nearest the strip may ever come to the middle of the screen. It scales
     with the viewport so a short window keeps the ship clear too. */
  --hint-clearance: max(120px, 21vh);
  /* The vitals and special row, plus the strip's own height and a gap. */
  --hint-floor: 168px;
  position: fixed;
  left: 50%;
  /* As low as the bottom panels allow, and never nearer the middle than the
     clearance: `max()` only ever moves it further down. */
  top: max(calc(50% + var(--hint-clearance)), calc(100% - var(--hint-floor)));
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px 18px;
  max-width: calc(100% - 32px);
  padding: 8px 16px;
  background: var(--sp-glass);
  border: 1px solid var(--sp-line);
  border-radius: 999px;
  backdrop-filter: blur(8px);
  pointer-events: none;
  opacity: 1;
  transform: translateX(-50%);
  /* The parent sets data-leaving on the first input, or after a few seconds,
     and drops the strip once this has run. */
  transition: opacity 420ms var(--sp-ease-out);
  animation: sp-hint-in 520ms var(--sp-ease-out) both;
}

.sp-hud__hint[data-leaving] {
  opacity: 0;
}

.sp-hint__row {
  display: flex;
  align-items: center;
  gap: 7px;
}

.sp-hint__label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--sp-text-dim);
}

@keyframes sp-hint-in {
  from { opacity: 0; transform: translate(-50%, 10px); }
}

/* The tip line (client/Tips): a one-time tip, or the ship card's three lines.
   It lives in the hint slot and is placed by the strip's own rule — measured
   from the middle of the screen, never from a grid cell — and when the control
   hint shows too it goes *under* the strip: `--tip-step` is the strip's height
   and a gap. Wide, the strip steps up and the tip takes the band above the
   vitals; above the centre (stacked, landscape) the tip keeps the clearance and
   the strip steps further out; on touch the tip steps down. Type never goes
   under 12px, and it wraps inside the screen's width. */
.sp-hud__hint,
.sp-hud__tip {
  --tip-step: 52px;
}

.sp-hud__tip {
  --hint-clearance: max(120px, 21vh);
  /* The vitals and special row, plus a three-line ship card and a gap. */
  --tip-floor: 200px;
  position: fixed;
  left: 50%;
  top: max(calc(50% + var(--hint-clearance)), calc(100% - var(--tip-floor)));
  display: grid;
  gap: 2px;
  width: max-content;
  max-width: min(440px, calc(100% - 32px));
  padding: 8px 16px;
  background: var(--sp-glass);
  border: 1px solid var(--sp-line);
  border-radius: 14px;
  backdrop-filter: blur(8px);
  text-align: center;
  pointer-events: none;
  opacity: 1;
  transform: translateX(-50%);
  transition: opacity 420ms var(--sp-ease-out);
  animation: sp-hint-in 520ms var(--sp-ease-out) both;
}

.sp-hud__tip[data-leaving] {
  opacity: 0;
}

.sp-tip__line {
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--sp-text);
  overflow-wrap: anywhere;
}

/* The ship card's name line. */
.sp-tip__line:first-child:not(:last-child) {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.14em;
  color: var(--sp-text-dim);
}

/* Both at once, wide: the strip steps up by the tip's step, the tip under it. */
.sp-hud:has(> .sp-hud__tip) > .sp-hud__hint {
  top: max(calc(50% + var(--hint-clearance)), calc(100% - var(--tip-floor, 200px) - var(--tip-step)));
}
.sp-hud__hint ~ .sp-hud__tip {
  top: max(calc(50% + var(--hint-clearance) + var(--tip-step)), calc(100% - var(--tip-floor)));
}

/* The storm line (client/HudModel fieldView): the tone is said in the words; the edge is its echo. */
.sp-hud__tip[data-tone="danger"] {
  border-color: color-mix(in oklab, var(--sp-danger) 70%, var(--sp-line));
  box-shadow: 0 0 18px color-mix(in oklab, var(--sp-danger) 30%, transparent);
}

.sp-hud__tip[data-tone="safe"] {
  border-color: color-mix(in oklab, var(--sp-accent-2) 70%, var(--sp-line));
  box-shadow: 0 0 18px color-mix(in oklab, var(--sp-accent-2) 24%, transparent);
}

/* ------------------------------------------------------------- vitals --- */

.sp-hud__minimap,
.sp-hud__vitals,
.sp-hud__special,
.sp-hud__superweapon {
  background: var(--sp-glass);
  border: 1px solid var(--sp-line);
  border-radius: var(--sp-radius);
  backdrop-filter: blur(8px);
}

.sp-hud__vitals {
  position: relative; /* the comeback tag and the level toast sit on its edges */
  grid-column: 1;
  grid-row: 4;
  align-self: end;
  justify-self: start;
  display: grid;
  gap: 9px;
  width: min(360px, 100%);
  padding: 14px 16px 15px;
}

.sp-hud__ship {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ship);
}

.sp-hud__row {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
}

.sp-hud__row-label {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.16em;
  white-space: nowrap;
  text-transform: uppercase;
  color: var(--sp-text-faint);
}

/* With a keyboard the two rows share one label column as wide as the longer
   label ("Прочность", "Kadłub", "Poz. 9"), so both bars and the run strip
   under them still start together. Where subgrid is missing each row keeps
   its own 40 px column. */
.sp-hud:not([data-touch="true"]) .sp-hud__vitals {
  grid-template-columns: minmax(40px, max-content) minmax(0, 1fr) auto;
  column-gap: 12px;
}

.sp-hud:not([data-touch="true"]) .sp-hud__vitals > * {
  grid-column: 1 / -1;
}

.sp-hud:not([data-touch="true"]) .sp-hud__vitals > .sp-hud__row {
  grid-template-columns: subgrid;
}

.sp-hud:not([data-touch="true"]) .sp-hud__vitals > .sp-hud__run {
  grid-column: 2;
  margin-left: 0;
}

.sp-hud__row-value {
  min-width: 92px;
  font: 500 12px/1 var(--sp-font-mono);
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--sp-text-dim);
}

/* The XP bar's next-module badge hangs 11 px past the bar's end: a long value
   ("4,830 / 4,830 XP") keeps clear of it rather than touching its ring. */
.sp-meter[data-kind="xp"] + .sp-hud__row-value {
  padding-left: 8px;
}

/* -------------------------------------------------------------- meter --- */

.sp-meter {
  --fill: 0;
  /* How much of the box the live maximum still fills. 1 everywhere but a
     crowned hull bar, so every other meter is drawn exactly as it was. */
  --track: 1;
  --crown: 0;
  /* One second of repair, laid on the track at the fill's own end: where it
     starts, how wide it is, and how loudly it is drawn. The hull's own colour
     and never the crown's gold — the crown lends hull and takes it back, this
     is hull coming back, and two facts do not share a colour. */
  --regen-start: 0;
  --regen: 0;
  --regen-tint: var(--sp-hull);
  --regen-alpha: 0;
  --meter: var(--sp-text);
  position: relative;
  height: 8px;
  overflow: hidden;
  background: color-mix(in oklab, var(--sp-text) 8%, transparent);
  border-radius: 4px;
}

.sp-meter__fill {
  position: absolute;
  inset: 0;
  background: var(--meter);
  border-radius: inherit;
  transform: scaleX(calc(var(--fill) * var(--track)));
  transform-origin: left center;
  transition: transform var(--sp-dur) var(--sp-ease-out), background-color var(--sp-dur);
}

/* --------------------------------------------------- the crown's share --- */

/* In Juggernaut the crown lends hull and takes it back over 45 seconds, and
   the simulation clamps the live hull down with it. Drawn as one number over
   another that reads as damage nobody dealt, so the box here means the hull
   the crown gave at the take: the bar recedes along a gold rail that says
   where it used to reach. Gold is the crown's own token — the band, the crown
   block in the standing readout and the armour on the ship are one fact. */
.sp-meter[data-crown] { --crown-tint: var(--sp-rarity-legendary); }

.sp-meter__crown,
.sp-meter__spent {
  position: absolute;
  top: 0;
  bottom: 0;
  pointer-events: none;
  transition: left var(--sp-dur) var(--sp-ease-out), width var(--sp-dur) var(--sp-ease-out);
}

/* Still lent: laid over the fill, so a crowned bar reads as hull with a gold
   top end rather than as two bars. The band marks the crown's part of the
   *range*, so only the part of it the ship still has is painted bright — a
   solid band over an empty track would read as hull that is not there. */
.sp-meter__crown {
  --crown-alpha: 0.26;
  left: calc((var(--track) - var(--crown)) * 100%);
  width: calc(var(--crown) * 100%);
  background: color-mix(in oklab, var(--crown-tint) calc(var(--crown-alpha) * 100%), transparent);
  border-radius: inherit;
  box-shadow: inset 1px 0 0 0 var(--crown-tint);
}

/* The filled part of the band: where the hull reaches into the crown's range. */
.sp-meter__crown::after {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: clamp(0%, calc((var(--fill) * var(--track) - var(--track) + var(--crown)) / var(--crown) * 100%), 100%);
  background: color-mix(in oklab, var(--crown-tint) 72%, transparent);
  border-radius: inherit;
  transition: width var(--sp-dur) var(--sp-ease-out);
}

/* Already given back: the rail beyond the live bar's end. */
.sp-meter__spent {
  --crown-alpha: 0.16;
  left: calc(var(--track) * 100%);
  right: 0;
  background: color-mix(in oklab, var(--crown-tint) calc(var(--crown-alpha) * 100%), transparent);
  border-left: 1px solid color-mix(in oklab, var(--crown-tint) 55%, transparent);
}

/* ------------------------------------------------------ the repair lead --- */

/* Waiting: the band grows out of the fill over the delay, which is the
   countdown. Running: it holds its length and travels with the fill, which is
   the direction. Fainter while waiting, so the moment repair engages shows
   without anything moving suddenly. */
.sp-meter[data-regen="waiting"] { --regen-alpha: 0.34; }
.sp-meter[data-regen="active"] { --regen-alpha: 0.68; }

.sp-meter__regen {
  position: absolute;
  /* Inset from the bar's edges: a mark on the track, not hull the ship has. */
  top: 2px;
  bottom: 2px;
  left: calc(var(--regen-start) * 100%);
  width: calc(var(--regen) * 100%);
  pointer-events: none;
  border-radius: 0 1px 1px 0;
  background: color-mix(in oklab, var(--regen-tint) calc(var(--regen-alpha) * 100%), transparent);
  /* The target tick: where the hull reaches a second from now. */
  box-shadow: inset -1px 0 0 0 color-mix(in oklab, var(--regen-tint) calc(var(--regen-alpha) * 130%), transparent);
  transition:
    left var(--sp-dur) var(--sp-ease-out),
    width var(--sp-dur) var(--sp-ease-out),
    background-color var(--sp-dur);
}

/* The number and the bar are one fact, so the value picks up the same rising
   mark while the hull is actually climbing. Its room is reserved whether or not
   it shows, so the number never jumps as repair starts and stops. */
.sp-meter[data-kind="hull"] + .sp-hud__row-value::after {
  content: " ▴";
  color: var(--regen-tint);
  opacity: 0;
  transition: opacity var(--sp-dur);
}

.sp-meter[data-regen="active"] + .sp-hud__row-value::after { opacity: 1; }

.sp-meter[data-kind="hull"] { --meter: var(--sp-hull); }

.sp-meter[data-kind="hull"][data-low] {
  --meter: var(--sp-danger);
  animation: sp-low-hull 850ms ease-in-out infinite alternate;
}

/* In a standing storm: the bar is ringed in the storm's colour and breathes (held still by ui/Theme's reduced-motion rule). */
.sp-meter[data-kind="hull"][data-storm] {
  --storm: var(--sp-accent-2);
  outline: 1px solid var(--storm);
  outline-offset: 2px;
  box-shadow: 0 0 12px color-mix(in oklab, var(--storm) 55%, transparent);
  animation: sp-storm-hull 700ms ease-in-out infinite alternate;
}

.sp-meter[data-kind="hull"][data-storm="edge"] { --storm: var(--sp-danger); }

@keyframes sp-storm-hull {
  from { outline-color: color-mix(in oklab, var(--storm) 35%, transparent); }
  to { outline-color: var(--storm); }
}

.sp-meter[data-kind="xp"] .sp-meter__fill {
  background: linear-gradient(90deg, var(--sp-accent), var(--sp-xp));
}

.sp-meter[data-kind="special"] {
  height: 5px;
}

.sp-meter[data-kind="special"] { --meter: var(--ship); }

/* ------------------------------------------------ XP bar and the run --- */

/* The glyph at the bar's end: the next level is a milestone (◆ form, ★ Keystone).
   It straddles the end, so the XP meter does not clip: its fill is a plain
   scaled bar with nothing laid over it that needs the box's edge. */
.sp-meter[data-kind="xp"] { overflow: visible; }

.sp-meter__milestone {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translate(50%, -50%);
  font-size: 12px;
  line-height: 1;
  color: var(--sp-accent-2);
  text-shadow: 0 0 8px color-mix(in oklab, currentColor 60%, transparent);
  pointer-events: none;
}

.sp-meter__milestone[data-kind="keystone"] { color: var(--sp-rarity-legendary); }

/* A blueprint's next module at the bar's end (docs/blueprints.md §6): the kind drawn as a small
   mark in a dark socket, a gun in its line's colour; a full frame's tier-up mark breathes. */
.sp-meter__module {
  position: absolute;
  right: 0;
  top: 50%;
  width: 18px;
  height: 18px;
  transform: translate(60%, -50%);
  color: var(--sp-accent);
  background: color-mix(in oklab, var(--sp-space-0) 86%, transparent);
  border: 1px solid color-mix(in oklab, currentColor 55%, transparent);
  border-radius: 50%;
  box-shadow: 0 0 8px color-mix(in oklab, currentColor 45%, transparent);
  pointer-events: none;
}

.sp-meter__module::after {
  content: '';
  position: absolute;
  inset: 3px;
  background: currentColor;
  -webkit-mask: var(--icon) center / contain no-repeat;
  mask: var(--icon) center / contain no-repeat;
}

.sp-meter__module[data-kind="gun"] { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2v12' stroke-width='3.4'/%3E%3Ccircle cx='12' cy='17' r='4.2' fill='black'/%3E%3C/svg%3E"); }
.sp-meter__module[data-kind="cannon"] { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2v11' stroke-width='6'/%3E%3Ccircle cx='12' cy='17' r='5' fill='black'/%3E%3C/svg%3E"); }
.sp-meter__module[data-kind="spar"] { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 2v20M16 2v20'/%3E%3Cpath d='M8 4l8 4-8 4 8 4-8 4' stroke-width='1.4'/%3E%3C/svg%3E"); }
.sp-meter__module[data-kind="magazine"] { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='7' r='4.2'/%3E%3Ccircle cx='12' cy='17' r='4.2'/%3E%3Ccircle cx='12' cy='7' r='1.2' fill='black'/%3E%3Ccircle cx='12' cy='17' r='1.2' fill='black'/%3E%3C/svg%3E"); }
.sp-meter__module[data-kind="engine"] { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 3h12v9H6z' fill='black'/%3E%3Cpath d='M9 12l-2 4h10l-2-4' fill='black'/%3E%3Cpath d='M10 18l2 4 2-4' stroke-width='1.6'/%3E%3C/svg%3E"); }
.sp-meter__module[data-kind="armour"] { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 3h10l4 4v10l-4 4H7l-4-4V7z' fill='black'/%3E%3C/svg%3E"); }
.sp-meter__module[data-kind="keystone"] { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.5 3h7L21 8.5v7L15.5 21h-7L3 15.5v-7z'/%3E%3Cpath d='M12 7l1.5 3.5L17 12l-3.5 1.5L12 17l-1.5-3.5L7 12l3.5-1.5z' fill='black'/%3E%3C/svg%3E"); }
.sp-meter__module[data-kind="tierup"] { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12l7-7 7 7M5 20l7-7 7 7' stroke-width='2.6'/%3E%3C/svg%3E"); }

.sp-meter__module[data-weapon="blaster"] { color: var(--sp-ship-laser); }
.sp-meter__module[data-weapon="scatter"] { color: var(--sp-ship-scatter); }
.sp-meter__module[data-weapon="rocket"] { color: var(--sp-ship-rocket); }
.sp-meter__module[data-kind="armour"] { color: var(--sp-text-dim); }
.sp-meter__module[data-kind="magazine"],
.sp-meter__module[data-kind="keystone"] { color: var(--sp-accent-2); }
.sp-meter__module[data-kind="tierup"] { color: var(--sp-rarity-legendary); }

.sp-meter[data-kind="xp"][data-full] .sp-meter__fill {
  background: var(--sp-rarity-legendary);
}

@media (prefers-reduced-motion: no-preference) {
  .sp-meter__module[data-kind="tierup"] { animation: sp-tierup 1.1s var(--sp-ease-out) infinite alternate; }
}

@keyframes sp-tierup {
  from { opacity: 0.55; }
  to { opacity: 1; }
}

/* The tier and the stage's five pips, in the level's place. */
.sp-hud__tier {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.sp-hud__stage {
  display: inline-flex;
  gap: 2px;
}

.sp-hud__stage-pip {
  width: 4px;
  height: 4px;
  transform: rotate(45deg);
  background: var(--sp-line);
}

.sp-hud__stage-pip[data-on="true"] {
  background: var(--sp-accent-2);
  box-shadow: 0 0 4px color-mix(in oklab, var(--sp-accent-2) 70%, transparent);
}

/* The last minute: kill XP ×1.5. The bar breathes gold; the announcer said why. */
.sp-meter[data-kind="xp"][data-final] {
  box-shadow: 0 0 0 1px color-mix(in oklab, var(--sp-rarity-legendary) 55%, transparent);
  animation: sp-final-minute 1.2s ease-in-out infinite alternate;
}

@keyframes sp-final-minute {
  from { box-shadow: 0 0 0 1px color-mix(in oklab, var(--sp-rarity-legendary) 35%, transparent); }
  to { box-shadow: 0 0 12px 1px color-mix(in oklab, var(--sp-rarity-legendary) 70%, transparent); }
}

/* The whole climb on one thin strip, under the XP row. Decoration: the bar is the fact. */
.sp-hud__run {
  position: relative;
  height: 3px;
  margin: -3px 0 0 52px; /* under the bar, past the "Lv" label's column and gap */
  border-radius: 2px;
  background: color-mix(in oklab, var(--sp-text) 10%, transparent);
}

.sp-hud__run-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: calc(var(--fill) * 100%);
  border-radius: inherit;
  background: color-mix(in oklab, var(--sp-xp) 70%, transparent);
  transition: width var(--sp-dur) var(--sp-ease-out);
}

.sp-hud__run-mark {
  position: absolute;
  top: -3px;
  left: calc(var(--at) * 100%);
  width: 3px;
  height: 9px;
  border-radius: 1px;
  transform: translateX(-50%);
  background: var(--sp-accent-2);
  opacity: 0.45;
}

.sp-hud__run-mark[data-kind="keystone"] { background: var(--sp-rarity-legendary); }
.sp-hud__run-mark[data-reached="true"] { opacity: 1; }

/* Earning the comeback multiplier: said once, small, at the panel's top right. */
.sp-hud__comeback {
  position: absolute;
  top: 12px;
  right: 16px;
  font-size: 12px;
  font-weight: 600;
  color: var(--sp-accent-2);
}

/* What an automatic level gave, one line just above the panel. */
.sp-hud__toast {
  position: absolute;
  left: 0;
  bottom: calc(100% + 8px);
  max-width: 100%;
  padding: 6px 12px;
  border: 1px solid color-mix(in oklab, var(--sp-xp) 60%, var(--sp-line));
  border-radius: 8px;
  background: var(--sp-glass);
  backdrop-filter: blur(8px);
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--sp-text);
  pointer-events: none;
  animation: sp-toast-in 180ms var(--sp-ease-out);
}

.sp-hud__toast[data-kind="keystone"] {
  border-color: var(--sp-rarity-legendary);
  color: var(--sp-rarity-legendary);
}

@keyframes sp-toast-in {
  from { opacity: 0; transform: translateY(6px); }
}

@media (prefers-reduced-motion: reduce) {
  .sp-meter[data-kind="xp"][data-final] { animation: none; box-shadow: 0 0 0 1px var(--sp-rarity-legendary); }
  .sp-hud__toast { animation: none; }
}

@keyframes sp-low-hull {
  from { box-shadow: 0 0 0 0 transparent; }
  to { box-shadow: 0 0 14px 1px color-mix(in oklab, var(--sp-danger) 70%, transparent); }
}

/* ------------------------------------------------------------ special --- */

.sp-hud__special {
  grid-column: 2;
  grid-row: 4;
  align-self: end;
  display: grid;
  gap: 10px;
  width: 240px;
  padding: 12px 14px;
  transition: border-color var(--sp-dur), box-shadow var(--sp-dur);
}

.sp-hud__special[data-state="ready"] {
  border-color: color-mix(in oklab, var(--ship) 55%, transparent);
  box-shadow: 0 0 26px -14px var(--ship);
}

.sp-hud__special[data-state="active"] {
  border-color: var(--ship);
}

.sp-hud__special-head,
.sp-hud__special-body {
  display: flex;
  align-items: center;
  gap: 10px;
}

.sp-hud__special-body .sp-meter {
  flex: 1;
}

.sp-hud__special-name {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.sp-hud__special-status {
  margin-left: auto;
  font: 600 11px/1 var(--sp-font-mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--sp-text-dim);
}

.sp-hud__special[data-state="cooldown"] .sp-hud__special-status {
  text-transform: none; /* "4s", not "4S" */
}

.sp-hud__special[data-state="ready"] .sp-hud__special-status,
.sp-hud__special[data-state="active"] .sp-hud__special-status {
  color: var(--ship);
}

/* -------------------------------------------------------- superweapon --- */

/* A second ability slot, directly above the special and never in its cell: the
   two have separate buttons, and a pilot who confuses them wastes the rarest
   thing in the match. Only on screen while one is carried.

   It is the special panel's box, laid out to the special panel's rhythm — the
   glass, radius, padding and row gap above are literally the same rule — so
   the two read as one family. What it does not borrow is the meter. */
.sp-hud__superweapon {
  --superweapon: var(--sp-rarity-legendary);
  grid-column: 2;
  grid-row: 3;
  align-self: end;
  justify-self: center;
  display: grid;
  gap: 10px;
  width: 240px;
  padding: 12px 14px;
  border-color: color-mix(in oklab, var(--superweapon) 60%, transparent);
  box-shadow: 0 0 30px -12px var(--superweapon);
  animation: sp-superweapon-in 420ms var(--sp-ease-out) both;
}

/* The five read apart at a glance, in the colours the arena draws them in
   (client/Renderer SUPERWEAPON_COLOR_KEYS). */
.sp-hud__superweapon[data-superweapon="singularity"] { --superweapon: var(--sp-paint-crimson); }
.sp-hud__superweapon[data-superweapon="lance"] { --superweapon: var(--sp-accent-2); }
.sp-hud__superweapon[data-superweapon="nova"] { --superweapon: var(--sp-paint-void); }
.sp-hud__superweapon[data-superweapon="bombardment"] { --superweapon: var(--sp-paint-solar); }
.sp-hud__superweapon[data-superweapon="overload"] { --superweapon: var(--sp-paint-magenta); }
/* A tank arena's own three (docs/superweapons.md §8). */
.sp-hud__superweapon[data-superweapon="railgun"] { --superweapon: var(--sp-paint-ice); }
.sp-hud__superweapon[data-superweapon="emp"] { --superweapon: var(--sp-paint-aurora); }
.sp-hud__superweapon[data-superweapon="airstrike"] { --superweapon: var(--sp-paint-ember); }

/* An Overload running: the one meter this slot holds, and it only drains. */
.sp-hud__superweapon-timer {
  flex: 1 1 auto;
  height: 4px;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--superweapon) calc(var(--left, 0) * 100%), color-mix(in oklab, var(--superweapon) 18%, transparent) 0);
}
.sp-hud__superweapon[data-active] {
  box-shadow: 0 0 36px -8px var(--superweapon);
}

.sp-hud__superweapon-head,
.sp-hud__superweapon-body {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* "Right click" is two words and one key: it never breaks across lines. */
.sp-hud__superweapon .sp-kbd {
  white-space: nowrap;
}

/* The heading, where the special panel puts "Speed Burst". The weapon's own
   colour, so one weapon reads as one colour from the arena to the HUD. */
.sp-hud__superweapon-name {
  min-width: 0;
  overflow: hidden;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-overflow: ellipsis;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--superweapon);
}

/* The special panel's second row is a meter creeping back up to full. This one
   is where the whole difference between the two lives: there is no meter,
   because there is nothing to wait for. The words take the gauge's place, and
   push the keycap to the same right edge the special's sits on. */
.sp-hud__superweapon-status {
  margin-right: auto;
  font: 600 11px/1 var(--sp-font-mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--superweapon);
}

@keyframes sp-superweapon-in {
  from { opacity: 0; transform: translateY(8px); }
}

/* ------------------------------------------------- superweapon: firing ---

   On a phone there is no right mouse button and client/TouchControls has no
   second button to spare, so the slot itself becomes the trigger: `data-fire`
   wraps the two rows in a button and the box a pilot is already reading is the
   box they press. It is on screen only while one is carried, which is the
   guard a one-use weapon needs — there is nothing to mis-press when there is
   nothing to spend.

   ui/Hud claims 139–141. */
.sp-hud__superweapon[data-fire] {
  /* The screen a thumb owns at each bottom corner. 120px is
     client/TouchControls' own `buttonInset + buttonRadius` — the reach that
     block already claims for the special button, and so the only measure of a
     corner that is not invented here. It grows with the viewport; `max()` can
     only ever hold more of the corner clear. */
  --thumb-corner: max(120px, 30vw);
  justify-self: center;
  /* Never wider than the screen left between the two corners. */
  width: min(240px, calc(100% - 2 * var(--thumb-corner)));
  /* Pressable, and pressed. The slot already carries the weapon's colour, so
     the affordance is depth, not a second hue. */
  box-shadow: 0 0 30px -10px var(--superweapon), inset 0 0 0 1px color-mix(in oklab, var(--superweapon) 28%, transparent);
}

/* The button *is* the box's contents: the glass, border and padding stay on
   the slot, so the fire control and the readout are the same object drawn the
   same way. The HUD takes no pointer events; this puts them back, and the
   browser then hands the tap here instead of to the canvas underneath —
   exactly as it already does for the upgrade tray. */
.sp-hud__superweapon-fire {
  display: grid;
  gap: inherit;
  width: 100%;
  min-width: 0;
  min-height: 44px; /* a thumb's target, whatever the type does */
  margin: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  background: none;
  border: 0;
  border-radius: inherit;
  cursor: pointer;
  pointer-events: auto;
  -webkit-tap-highlight-color: transparent;
}

.sp-hud__superweapon-fire:focus-visible {
  outline: 2px solid var(--superweapon);
  outline-offset: 3px;
}

.sp-hud__superweapon-fire:active {
  transform: translateY(1px);
}

/* Very narrow: the corners still get their 120px, so the box gives way instead.
   The status keeps `white-space: nowrap` (claim 99) and the keycap drops to its
   own line rather than pushing out of the slot. */
.sp-hud__superweapon[data-fire] .sp-hud__superweapon-body {
  flex-wrap: wrap;
}

@media (prefers-reduced-motion: reduce) {
  .sp-hud__superweapon-fire:active { transform: none; }
}

/* --------------------------------------------------------------- tray --- */

/* The column is `minmax(0, 1fr)` and not `auto` here and on the card list
   below, because an `auto` track is sized to its widest item and then overflows
   a narrower container outward: the cards would set the tray's width instead of
   fitting into it, and at 520px that put `+n more`, `WEAPON` and the Details
   button off the screen. The cell is the budget. */
.sp-hud__tray {
  grid-column: 3;
  grid-row: 3 / 5; /* level with the minimap and vitals, bottom-aligned */
  justify-self: end;
  align-self: end;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 6px;
  width: min(320px, 100%);
  pointer-events: auto; /* …except the tray, which is clickable */
  animation: sp-hud-in-right 420ms var(--sp-ease-out) both;
}

.sp-tray__header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 2px 2px;
}

.sp-tray__title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--sp-rarity-legendary);
}

.sp-tray__title::before {
  content: "";
  width: 6px;
  height: 6px;
  background: currentColor;
  border-radius: 50%;
  box-shadow: 0 0 10px currentColor;
  animation: sp-tray-pulse 1.4s ease-in-out infinite;
}

.sp-tray__more {
  margin-left: auto;
  font: 600 11px/1 var(--sp-font-mono);
  color: var(--sp-text-dim);
}

.sp-tray__cards {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 5px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.sp-tray__details {
  justify-self: end;
  margin-top: 2px;
  background: var(--sp-glass);
}

@keyframes sp-tray-pulse {
  50% { opacity: 0.35; }
}

/* ------------------------------------------------------------ shared --- */

@keyframes sp-hud-in-left {
  from { opacity: 0; transform: translateX(-14px); }
}

@keyframes sp-hud-in-right {
  from { opacity: 0; transform: translateX(14px); }
}

/* ------------------------------------------------------------ board slot --- */

/* The endless arena's live board (ui/RunBoard). A readout: the HUD places it and
   never turns its pointer events on. With a pointer it heads the right-hand
   column and the kill feed takes the row under it, so the board holds still as
   kills come and go (spec: The board slot). */
.sp-hud__board {
  min-width: 0;
  max-width: 100%;
}

.sp-hud:not([data-touch="true"]) > .sp-hud__board {
  grid-column: 3;
  grid-row: 1;
  justify-self: end;
  align-self: start;
}

.sp-hud:not([data-touch="true"]):has(> .sp-hud__board) > .sp-hud__feed {
  grid-row: 2;
  align-self: start;
}

/* The board names the leader and carries the pilot's own place: the readout
   stops saying both (hidden the way .sp-visually-hidden hides), and keeps the
   clock and the kills. */
.sp-hud:not([data-touch="true"]):has(> .sp-hud__board) .sp-match__place,
.sp-hud:not([data-touch="true"]):has(> .sp-hud__board) .sp-match__leader {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.sp-hud:not([data-touch="true"]):has(> .sp-hud__board) .sp-match__kills {
  grid-area: 1 / 1 / auto / -1;
  padding-top: 0;
}

/* Small viewports: the clock gets its own row, and the left column stacks
   minimap, vitals and special beside the tray. */
@media (max-width: 860px) {
  .sp-hud {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-rows: auto auto minmax(0, 1fr) auto auto auto auto;
  }

  .sp-hud__top { grid-column: 1 / -1; grid-row: 1; }
  .sp-hud__left { grid-row: 2; }
  .sp-hud__feed { grid-column: 2; grid-row: 2; }
  /* The live board takes the feed's cell, and the feed the row under it. */
  .sp-hud:not([data-touch="true"]) > .sp-hud__board { grid-column: 2; grid-row: 2; }
  .sp-hud:not([data-touch="true"]):has(> .sp-hud__board) > .sp-hud__feed { grid-row: 3; }
  .sp-hud__banner-slot,
  .sp-respawn,
  .sp-reconnecting { grid-row: 3; }

  /* Stacked, the minimap, vitals and special own the whole lower half of the
     screen, so there is no band below the ship to sit in: the strip goes the
     same clearance *above* the middle instead, between the top readouts and
     the banner. */
  .sp-hud__hint { top: auto; bottom: calc(50% + var(--hint-clearance)); }
  /* The tip keeps the clearance and the strip steps further out, above it. */
  .sp-hud__hint, .sp-hud__tip { --tip-step: 80px; }
  .sp-hud__tip,
  .sp-hud__hint ~ .sp-hud__tip { top: auto; bottom: calc(50% + var(--hint-clearance)); }
  .sp-hud:has(> .sp-hud__tip) > .sp-hud__hint { top: auto; bottom: calc(50% + var(--hint-clearance) + var(--tip-step)); }
  .sp-hud__minimap { grid-row: 4; --minimap-size: 128px; }
  .sp-hud__vitals { grid-row: 5; }
  /* Stacked, the slot gets a row of its own above the special rather than
     sharing one: at this width they would sit on top of each other. */
  .sp-hud__superweapon { grid-column: 1; grid-row: 6; justify-self: stretch; width: auto; }
  .sp-hud__special { grid-column: 1; grid-row: 7; width: auto; }
  .sp-hud__tray { grid-column: 2; grid-row: 4 / 8; }

  /* A fire control cannot stay in that left-column row: at 520 x 800 it is
     exactly where a left thumb lands on the move stick, and an interactive box
     there swallows it. It takes a row of its own under everything instead,
     centred on the bottom edge — the placement the landscape phone below has
     always used — with both bottom corners left to the thumbs. */
  .sp-hud__superweapon[data-fire] { grid-column: 1 / -1; grid-row: 8; justify-self: center; }
}

/* Very narrow, or narrow and short: centred text is wider than the gap beside
   the minimap, or the centre row has little height, so the minimap, banner and
   respawn type all compact to stay clear of each other. */
@media (max-width: 700px), (max-width: 860px) and (max-height: 760px) {
  .sp-hud { --banner-size: 22px; }
  .sp-hud__minimap { --minimap-size: 104px; }

  .sp-minimap__name {
    font-size: 9px;
    letter-spacing: 0.12em;
  }

  .sp-respawn {
    margin-top: calc(8vh + var(--banner-size) + 56px);
    padding: 12px 16px;
  }

  .sp-respawn__title {
    font-size: 15px;
    letter-spacing: 0.2em;
    text-indent: 0.2em;
  }

  /* One line of hint is more than a narrow screen has room for: it wraps to
     two, and the glyphs and words step down with everything else. */
  .sp-hud__hint {
    gap: 6px 12px;
    padding: 7px 12px;
  }

  .sp-hint__label {
    font-size: 10px;
    letter-spacing: 0.1em;
  }

  /* Short enough to stay clear of the compacted respawn overlay. */
  .sp-reconnecting__status {
    gap: 7px;
    padding: 10px 16px 11px;
  }

  .sp-reconnecting__title {
    font-size: 14px;
    letter-spacing: 0.2em;
    text-indent: 0.2em;
  }

  .sp-reconnecting__hint {
    font-size: 11px;
  }
}

/* A landscape phone: wide enough for the three-column layout, but too short
   for the stacked one, and its bottom corners belong to the player's thumbs
   (client/TouchControls draws its sticks there). So the HUD stays in three
   columns, compacts hard, and keeps the minimap off the left corner. */
@media (max-height: 520px) and (min-width: 560px) {
  .sp-hud {
    --banner-size: 20px;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr) auto;
    gap: 6px 12px;
    padding: 8px 12px 10px;
    font-size: 11px;
  }

  .sp-hud__left { grid-column: 1; grid-row: 1; }
  .sp-hud__top { grid-column: 2; grid-row: 1; }
  .sp-hud__feed { grid-column: 3; grid-row: 1; }
  .sp-hud:not([data-touch="true"]) > .sp-hud__board { grid-column: 3; grid-row: 1; }
  .sp-hud:not([data-touch="true"]):has(> .sp-hud__board) > .sp-hud__feed { grid-row: 2; }
  .sp-hud__banner-slot,
  .sp-respawn,
  .sp-reconnecting { grid-column: 1 / -1; grid-row: 2; }

  /* A landscape phone has no band under the ship either — the vitals and
     special flank the centre a few pixels off the bottom edge. */
  .sp-hud__hint { top: auto; bottom: calc(50% + var(--hint-clearance)); }
  .sp-hud__hint, .sp-hud__tip { --tip-step: 44px; }
  .sp-hud__tip,
  .sp-hud__hint ~ .sp-hud__tip { top: auto; bottom: calc(50% + var(--hint-clearance)); }
  .sp-hud:has(> .sp-hud__tip) > .sp-hud__hint { top: auto; bottom: calc(50% + var(--hint-clearance) + var(--tip-step)); }

  /* Both bottom corners are a thumb, so nothing may sit in one. The minimap
     goes up to the top-left, and the two panels a player reads mid-fight flank
     the centre, inboard of where the sticks are drawn. */
  .sp-hud__minimap { --minimap-size: 76px; grid-column: 1; grid-row: 1; justify-self: start; align-self: start; }
  .sp-hud__left { grid-column: 1; grid-row: 2; align-self: start; }
  .sp-hud__vitals { grid-column: 1; grid-row: 3; justify-self: end; }
  /* The middle of the bottom edge: between the two flanking panels, clear of
     both thumbs and of the special it must never be mistaken for. */
  .sp-hud__superweapon { grid-column: 2; grid-row: 3; justify-self: center; width: auto; }
  .sp-hud__special { grid-column: 3; grid-row: 3; justify-self: start; width: auto; }
  .sp-hud__tray { grid-column: 3; grid-row: 2; justify-self: end; align-self: end; }

  .sp-minimap__name { font-size: 8px; letter-spacing: 0.1em; }
  .sp-hud__hint { gap: 5px 10px; padding: 5px 10px; }
  .sp-hint__label { font-size: 9px; letter-spacing: 0.08em; }
  .sp-respawn { margin-top: 0; padding: 8px 14px; }
  .sp-respawn__title { font-size: 14px; letter-spacing: 0.18em; text-indent: 0.18em; }
}

/* The invite button: the same pill as the latency readout, a line below it, so
   the corner reads as one column of room information rather than two widgets.
   It is a control, so it takes the accent and answers to hover and focus. */
.sp-hud__invite {
  align-self: flex-start;
  padding: 5px 12px;
  font: 600 11px/1 var(--sp-font-mono);
  letter-spacing: 0.06em;
  color: var(--sp-accent-2);
  background: var(--sp-glass);
  border: 1px solid color-mix(in srgb, var(--sp-accent-2) 40%, var(--sp-line));
  border-radius: 999px;
  backdrop-filter: blur(8px);
  cursor: pointer;
  pointer-events: auto;
  transition: color 140ms ease, border-color 140ms ease, background-color 140ms ease;
}

.sp-hud__invite:hover,
.sp-hud__invite:focus-visible {
  color: var(--sp-text);
  border-color: var(--sp-accent-2);
  background: color-mix(in srgb, var(--sp-accent-2) 16%, var(--sp-glass));
}

/* Copying is silent, so the button says what it did. */
.sp-hud__invite[data-copied="true"] {
  color: var(--sp-text);
  border-color: color-mix(in srgb, var(--sp-accent-2) 70%, transparent);
}

/* The quick chat button (docs/networking.md §8.8): the invite button's pill, a
   line below it, with a speech bubble and its key. Claim 202. */
.sp-hud__chat {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  gap: 7px;
  padding: 4px 6px 4px 10px;
  font: 600 12px/1 var(--sp-font-ui);
  letter-spacing: 0.02em;
  color: var(--sp-text-dim);
  background: var(--sp-glass);
  border: 1px solid var(--sp-line);
  border-radius: 999px;
  backdrop-filter: blur(8px);
  cursor: pointer;
  pointer-events: auto;
  transition: color 140ms ease, border-color 140ms ease, background-color 140ms ease;
}

.sp-hud__chat:hover,
.sp-hud__chat:focus-visible,
.sp-hud__chat[aria-expanded="true"] {
  color: var(--sp-text);
  border-color: var(--sp-accent-2);
  background: color-mix(in srgb, var(--sp-accent-2) 14%, var(--sp-glass));
}

.sp-hud__chat-glyph {
  flex: none;
  width: 14px;
  height: 14px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4 3h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H11l-5 4v-4H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4 3h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H11l-5 4v-4H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z'/%3E%3C/svg%3E") center / contain no-repeat;
}

.sp-hud__chat-key { font-size: 12px; padding: 1px 5px; }

/* The panel it opens (ui/QuickChat): left of the ship and level with it, where
   the arena is empty and the mouse is near. Claim 204. */
.sp-hud__quick-chat {
  position: absolute;
  z-index: 3;
  left: max(24px, calc(50% - 380px));
  top: 50%;
  transform: translateY(-50%);
}

/* ------------------------------------------------------- upgrade badge ---

   On a phone the tray, its card rows and its Details button sat where the
   thumbs go, and a thumb that lands on DOM never reaches client/TouchControls.
   So on touch one pill stands in for all of it, in the top band: the parent
   points it at the picker, the evolution choice or the stat sheet. Claim 154. */
.sp-hud__badge {
  --badge: var(--sp-rarity-legendary);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 44px;
  min-height: 44px;
  margin: 0;
  padding: 0 7px 0 12px;
  font: 700 12px/1 var(--sp-font-ui);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--sp-text);
  background: color-mix(in oklab, var(--badge) 16%, var(--sp-glass));
  border: 1px solid color-mix(in oklab, var(--badge) 70%, transparent);
  border-radius: 999px;
  backdrop-filter: blur(8px);
  box-shadow: 0 0 18px -8px var(--badge);
  cursor: pointer;
  pointer-events: auto;
  -webkit-tap-highlight-color: transparent;
  animation: sp-badge-pulse 1.6s ease-in-out infinite;
}

.sp-hud__badge[data-kind="evolve"] { --badge: var(--sp-accent); }
.sp-hud__badge[data-kind="stats"] { --badge: var(--sp-accent-2); }

/* The third evolution: gold, and a brighter, quicker pulse than a card level's
   (evolution-overhaul.md §5.6). The word says Apex whether or not it moves. */
.sp-hud__badge[data-kind="apex"] {
  --badge: var(--sp-rarity-legendary);
  animation: sp-badge-apex 1.1s ease-in-out infinite;
}

.sp-badge__count {
  display: inline-grid;
  place-items: center;
  min-width: 24px;
  height: 24px;
  padding: 0 4px;
  font: 700 13px/1 var(--sp-font-mono);
  letter-spacing: 0;
  color: var(--sp-space-0);
  background: var(--badge);
  border-radius: 999px;
}

.sp-hud__badge:active { transform: translateY(1px); }

.sp-hud__badge:focus-visible {
  outline: 2px solid var(--badge);
  outline-offset: 3px;
}

@keyframes sp-badge-pulse {
  50% { box-shadow: 0 0 24px -2px var(--badge); }
}

@keyframes sp-badge-apex {
  50% {
    box-shadow: 0 0 28px 0 var(--badge), 0 0 0 2px color-mix(in oklab, var(--badge) 55%, transparent);
    border-color: var(--badge);
  }
}

@media (prefers-reduced-motion: reduce) {
  .sp-hud__badge { animation: none; }
  .sp-hud__badge[data-kind="apex"] { animation: none; }
  .sp-hud__badge:active { transform: none; }
}

/* ------------------------------------------------------- the phone HUD ---

   `touch`: the thumbs are the controls, and client/TouchControls listens on
   the canvas, so any DOM that takes a pointer below the top band swallows a
   stick. Everything interactive goes into one band along the top; below it the
   screen is the sticks' and the special disc's. Chosen by touch and
   orientation, never by width: a phone held either way gets this, and a narrow
   desktop window never does. Remove before shrinking; what stays is 12px or
   more. See the spec, The phone layout. Claims 145, 152–158. */
.sp-hud[data-touch="true"] {
  /* ui/Settings' gear: 44px, 14px in from the top-right corner. */
  --gear-reserve: 52px;
  --banner-size: 22px;
  gap: 6px 8px;
  padding:
    max(8px, env(safe-area-inset-top))
    max(8px, env(safe-area-inset-right))
    max(8px, env(safe-area-inset-bottom))
    max(8px, env(safe-area-inset-left));
  font-size: 12px;
}

/* The stacks dissolve: each piece takes its own cell of the band or the rows
   under it. */
.sp-hud[data-touch="true"] .sp-hud__left,
.sp-hud[data-touch="true"] .sp-hud__top {
  display: contents;
}

/* Hidden visually, the way .sp-visually-hidden hides: a screen reader still
   hears every one of them. */
.sp-hud[data-touch="true"] .sp-hud__special,
.sp-hud[data-touch="true"] .sp-hud__ship,
.sp-hud[data-touch="true"] .sp-hud__row-label[aria-hidden="true"],
.sp-hud[data-touch="true"] .sp-minimap__name,
.sp-hud[data-touch="true"] .sp-meter[data-kind="xp"] + .sp-hud__row-value,
.sp-hud[data-touch="true"] .sp-match__leader,
.sp-hud[data-touch="true"] .sp-match__field,
.sp-hud[data-touch="true"] .sp-match__scope,
.sp-hud[data-touch="true"] .sp-match__rivals,
.sp-hud[data-touch="true"] .sp-match__mode-name,
.sp-hud[data-touch="true"] .sp-match__crown-gauge,
.sp-hud[data-touch="true"] .sp-match__crown-state {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The minimap: a 64px canvas and nothing else. */
.sp-hud[data-touch="true"] .sp-hud__minimap {
  --minimap-size: 64px;
  justify-self: start;
  align-self: start;
  gap: 0;
  padding: 0;
}

/* The match pill: clock, place, kills, on one line. A crew mode keeps its
   scores and a crown mode its holder, in the same line when they fit. */
.sp-hud[data-touch="true"] .sp-hud__match {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px 10px;
  justify-self: start;
  align-self: start;
  width: auto;
  min-width: 0;
  max-width: 100%;
  padding: 3px 14px;
  border-radius: 999px;
}

.sp-hud[data-touch="true"] .sp-match__top { display: contents; }
.sp-hud[data-touch="true"] .sp-match__clock { font-size: 12px; }

.sp-hud[data-touch="true"] .sp-match__standing {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 0;
}

.sp-hud[data-touch="true"] .sp-match__place { font-size: 24px; }
.sp-hud[data-touch="true"] .sp-match__suffix { font-size: 12px; }

.sp-hud[data-touch="true"] .sp-match__kills {
  padding: 0;
  font-size: 12px;
  letter-spacing: 0.08em;
}

.sp-hud[data-touch="true"] .sp-match__mode {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 0;
  border: 0;
}

/* A crew mode's scoreline, as one tight line that reads left to right —
   "◆ CYAN 3 – 1 GOLD" — rather than the pointer layout's two half-panel
   columns, which on a phone left "CYAN" stranded at one end, the two scores
   touching in the middle and the lozenge alone at the other. Nothing grows:
   the mode block is only as wide as its line, so it shares the clock's row
   when the pill has room and wraps whole when it does not. */
.sp-hud[data-touch="true"] .sp-match__mode { width: auto; }

.sp-hud[data-touch="true"] .sp-match__teams {
  display: flex;
  align-items: baseline;
  gap: 6px;
  width: auto;
}

.sp-hud[data-touch="true"] .sp-match__team { flex: none; gap: 4px; }
.sp-hud[data-touch="true"] .sp-match__team-name { flex: none; overflow: visible; }

.sp-hud[data-touch="true"] .sp-match__team:first-child::after {
  content: "–";
  flex: none;
  margin-left: 2px;
  color: var(--sp-text-faint);
}

.sp-hud[data-touch="true"] .sp-match__team,
.sp-hud[data-touch="true"] .sp-match__crown { font-size: 12px; }

/* Hull and XP: two thin bars, out of the bottom-left corner the move stick
   owns. */
.sp-hud[data-touch="true"] .sp-hud__vitals {
  justify-self: stretch;
  align-self: start;
  gap: 4px;
  width: auto;
  min-width: 0;
  padding: 5px 10px;
}

/* Flex rather than the pointer layout's three columns: the hull row's word
   "Hull" is hidden (the bar and its number say it), and the XP row's numbers
   are, so each row gives the bar whatever its words leave. */
.sp-hud[data-touch="true"] .sp-hud__row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.sp-hud[data-touch="true"] .sp-hud__row .sp-meter { flex: 1 1 auto; min-width: 40px; }

.sp-hud[data-touch="true"] .sp-hud__row-label {
  font-size: 12px;
  letter-spacing: 0.06em;
}

.sp-hud[data-touch="true"] .sp-hud__row-value {
  min-width: 0;
  font-size: 12px;
}

.sp-hud[data-touch="true"] .sp-meter { height: 6px; }
.sp-hud[data-touch="true"] .sp-meter[data-kind="xp"] { height: 4px; }
.sp-hud[data-touch="true"] .sp-hud__run { margin-left: 0; }
.sp-hud[data-touch="true"] .sp-hud__comeback { top: 8px; right: 12px; }
/* A phone's vitals sit in the top band, under the standing readout: the toast
   drops below the panel instead of over the readout. */
.sp-hud[data-touch="true"] .sp-hud__toast { top: calc(100% + 6px); bottom: auto; font-size: 12px; padding: 4px 10px; }

/* The controls. The latency pill and the invite button are the ones the
   pointer layouts put in the top-left column. */
.sp-hud[data-touch="true"] .sp-hud__net { font-size: 12px; }

.sp-hud[data-touch="true"] .sp-hud__invite {
  min-height: 44px;
  font-size: 12px;
  letter-spacing: 0;
}

/* The chat button: one round 44 px target, the bubble alone. */
.sp-hud[data-touch="true"] .sp-hud__chat {
  justify-content: center;
  width: 44px;
  min-width: 44px;
  height: 44px;
  min-height: 44px;
  padding: 0;
}

.sp-hud[data-touch="true"] .sp-hud__chat-glyph { width: 20px; height: 20px; }

/* The panel: centred under the band, big tap targets (ui/QuickChat). */
.sp-hud[data-touch="true"] .sp-hud__quick-chat {
  left: 50%;
  top: calc(max(8px, env(safe-area-inset-top)) + 196px);
  transform: translateX(-50%);
}

/* The superweapon: the same slot, a thumb-sized button, in the band. */
.sp-hud[data-touch="true"] .sp-hud__superweapon {
  width: auto;
  min-width: 0;
  gap: 2px;
  padding: 6px 10px;
}

.sp-hud[data-touch="true"] .sp-hud__superweapon[data-fire] { padding: 0; }

.sp-hud[data-touch="true"] .sp-hud__superweapon-fire {
  gap: 3px;
  align-content: center;
  min-height: 48px;
  padding: 4px 10px;
}

.sp-hud[data-touch="true"] .sp-hud__superweapon-body { flex-wrap: nowrap; gap: 8px; }
.sp-hud[data-touch="true"] .sp-hud__superweapon-status { font-size: 12px; }
.sp-hud[data-touch="true"] .sp-hud__superweapon .sp-kbd { font-size: 12px; }

/* Under the band: one log line and two feed rows, then the banner. */
.sp-hud[data-touch="true"] .sp-hud__announcer {
  grid-column: 1 / -1;
  justify-self: center;
  align-items: center;
  min-width: 0;
  max-width: 100%;
}

.sp-hud[data-touch="true"] .sp-log__entry {
  min-width: 0;
  max-width: 100%;
  padding: 4px 12px;
  background: color-mix(in oklab, var(--sp-space-0) 60%, transparent);
  border-radius: 999px;
  border-left: 0;
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--tier) 40%, transparent);
}

.sp-hud[data-touch="true"] .sp-log__text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sp-hud[data-touch="true"] .sp-hud__feed {
  grid-column: 1 / -1;
  align-self: start;
  gap: 3px;
}

.sp-hud[data-touch="true"] .sp-feed__entry { padding: 3px 8px; gap: 2px 8px; flex-wrap: wrap; justify-content: flex-end; }
/* On a phone a line may take two rows (the killer and the weapon, then the victim): a callsign
   is never cut to "Ke…" for a translated weapon's sake (2026-10-01). */
.sp-hud[data-touch="true"] .sp-feed__player { max-width: 100%; overflow: visible; text-overflow: clip; }
.sp-hud[data-touch="true"] .sp-feed__weapon { font-size: 12px; padding: 2px 5px; }
.sp-hud[data-touch="true"] .sp-feed__mastery { height: 18px; font-size: 12px; }
.sp-hud[data-touch="true"] .sp-feed__tier { font-size: 12px; }

.sp-hud[data-touch="true"] .sp-hud__banner-slot,
.sp-hud[data-touch="true"] .sp-reconnecting {
  grid-column: 1 / -1;
  justify-self: center;
  align-self: start;
  margin-top: 4px;
}

.sp-hud[data-touch="true"] .sp-banner { gap: 8px; }
.sp-hud[data-touch="true"] .sp-banner__subject { font-size: 12px; gap: 10px; }
.sp-hud[data-touch="true"] .sp-banner__subject::before,
.sp-hud[data-touch="true"] .sp-banner__subject::after { width: 48px; }
.sp-hud[data-touch="true"] .sp-banner__detail { font-size: 13px; max-width: 26ch; }
.sp-hud[data-touch="true"] .sp-reconnecting__hint { font-size: 12px; }

/* Dead: the overlay takes the middle of the whole screen, where the ship was,
   and nothing else is there — the banner is not rendered while it shows. */
.sp-hud[data-touch="true"] .sp-respawn {
  grid-column: 1 / -1;
  grid-row: 1 / -1;
  justify-self: center;
  align-self: center;
  gap: 8px;
  margin: 0;
  padding: 12px 20px;
}

.sp-hud[data-touch="true"] .sp-respawn__title { font-size: 15px; letter-spacing: 0.2em; text-indent: 0.2em; }
.sp-hud[data-touch="true"] .sp-respawn__rule { width: min(260px, 60vw); }
.sp-hud[data-touch="true"] .sp-respawn__countdown { font-size: 12px; }

/* While reconnecting as well, the reconnecting panel is under the band, so
   the countdown drops toward the bottom: controls are paused either way. */
.sp-hud[data-touch="true"]:has(> .sp-reconnecting) .sp-respawn {
  align-self: end;
  margin-bottom: 12vh;
}

/* The first-match hint names thumbs, and the thumbs are below the centre line:
   the same clearance, on that side. */
.sp-hud[data-touch="true"] .sp-hud__hint {
  top: calc(50% + var(--hint-clearance));
  bottom: auto;
  /* Pinned at 50% from the left, shrink-to-fit would give it half the screen
     and stack the rows; it takes its own width, up to the screen's. */
  width: max-content;
  gap: 6px 14px;
  padding: 6px 14px;
}

.sp-hud[data-touch="true"] .sp-hint__label { font-size: 12px; letter-spacing: 0.08em; }

/* The tip names thumbs too: below the centre, under the strip when both show. */
.sp-hud[data-touch="true"] .sp-hud__hint,
.sp-hud[data-touch="true"] .sp-hud__tip { --tip-step: 64px; }
.sp-hud[data-touch="true"] .sp-hud__tip { top: calc(50% + var(--hint-clearance)); bottom: auto; padding: 6px 14px; }
.sp-hud[data-touch="true"] .sp-hud__hint ~ .sp-hud__tip { top: calc(50% + var(--hint-clearance) + var(--tip-step)); bottom: auto; }
.sp-hud[data-touch="true"]:has(> .sp-hud__tip) > .sp-hud__hint { top: calc(50% + var(--hint-clearance)); bottom: auto; }
.sp-hud[data-touch="true"] .sp-tip__line { font-size: 13px; }
.sp-hud[data-touch="true"] .sp-tip__line:first-child:not(:last-child) { font-size: 12px; }
.sp-hud[data-touch="true"] .sp-hint__keys .sp-kbd { font-size: 12px; }

/* A phone has no room below the centre for both — landscape is too short, and
   upright the strip wraps onto three rows over the special: the strip keeps the
   thumbs' side and the tip takes the same clearance above the middle. */
@media (max-height: 520px) and (min-width: 560px), (max-width: 599px) {
  /* The least clearance the ship allows, so the tip sits under the opening banner. */
  .sp-hud[data-touch="true"] .sp-hud__tip { --hint-clearance: 120px; }
  .sp-hud[data-touch="true"] .sp-hud__tip,
  .sp-hud[data-touch="true"] .sp-hud__hint ~ .sp-hud__tip { top: auto; bottom: calc(50% + var(--hint-clearance)); }
}

/* A phone on its side is 375 to 390 px tall: the clearance over the centre is the top band's own
   height, so the tip and the band share it. The tip takes the band's empty middle, from the bars'
   right edge to the gear, instead of the screen's centre (where it ran under the bars and over the
   board); the board steps out while a tip shows, as it does for the offer; and while the offer
   is up, which fills that middle, the tip waits out of sight under it. */
@media (orientation: landscape) and (max-height: 520px) {
  .sp-hud[data-touch="true"] .sp-hud__tip {
    left: calc(max(12px, env(safe-area-inset-left)) + 276px);
    max-width: calc(100% - 276px - var(--gear-reserve, 56px) - 2 * max(12px, env(safe-area-inset-left)) - 8px);
    text-align: left;
    transform: none;
    animation: none;
  }
}

/* The same two steps on a short upright phone (360x640): the clearance over the centre lands the
   tip on the board's row and on the offer's, so the board steps out while a tip shows and the tip
   waits while the offer is up. A taller phone (844) has the room and keeps all three. */
@media (orientation: landscape) and (max-height: 520px), (orientation: portrait) and (max-width: 599px) and (max-height: 720px) {
  .sp-hud[data-touch="true"]:has(> .sp-hud__tip) .sp-hud__board {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  .sp-hud[data-touch="true"]:has(> .sp-hud__parts) > .sp-hud__tip { visibility: hidden; }
}

/* Portrait: the minimap beside the pill and the bars, then a row of controls
   under them — latency on the left, the superweapon and the badge on the
   right, under the gear, whose bottom edge this row is already below. The
   invite button, online only, a row lower. Then the log, the feed and the
   banner, centred. */
@media (orientation: portrait) {
  .sp-hud[data-touch="true"] {
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    grid-template-rows: auto auto auto auto auto auto minmax(0, 1fr);
  }

  .sp-hud[data-touch="true"] .sp-hud__minimap { grid-column: 1; grid-row: 1 / 3; }

  .sp-hud[data-touch="true"] .sp-hud__match,
  .sp-hud[data-touch="true"] .sp-hud__vitals {
    grid-column: 2 / -1;
    margin-right: calc(var(--gear-reserve) + 8px);
  }

  .sp-hud[data-touch="true"] .sp-hud__match { grid-row: 1; }
  .sp-hud[data-touch="true"] .sp-hud__vitals { grid-row: 2; }

  .sp-hud[data-touch="true"] .sp-hud__net { grid-column: 1; grid-row: 3; justify-self: start; align-self: center; }
  .sp-hud[data-touch="true"] .sp-hud__superweapon { grid-column: 3; grid-row: 3; justify-self: end; align-self: center; }
  .sp-hud[data-touch="true"] .sp-hud__badge { grid-column: 4; grid-row: 3; justify-self: end; align-self: center; }
  .sp-hud[data-touch="true"] .sp-hud__invite { grid-column: 1 / -1; grid-row: 4; justify-self: start; align-self: start; }
  .sp-hud[data-touch="true"] .sp-hud__chat { grid-column: 2; grid-row: 3; justify-self: start; align-self: center; }

  .sp-hud[data-touch="true"] .sp-hud__announcer { grid-row: 5; }
  .sp-hud[data-touch="true"] .sp-hud__feed { grid-row: 6; justify-self: stretch; align-items: center; }
  .sp-hud[data-touch="true"] .sp-hud__banner-slot,
  .sp-hud[data-touch="true"] .sp-reconnecting { grid-row: 7; }

  /* The modular level-up strip (ui/PartPicker): its own row under the controls,
     above everything transient (spec: The parts slot). */
  .sp-hud[data-touch="true"]:has(> .sp-hud__parts) {
    grid-template-rows: auto auto auto auto auto auto auto minmax(0, 1fr);
  }
  .sp-hud[data-touch="true"] .sp-hud__parts { grid-column: 1 / -1; grid-row: 5; }
  .sp-hud[data-touch="true"]:has(> .sp-hud__parts) .sp-hud__announcer { grid-row: 6; }
  .sp-hud[data-touch="true"]:has(> .sp-hud__parts) .sp-hud__feed { grid-row: 7; }
  .sp-hud[data-touch="true"]:has(> .sp-hud__parts) .sp-hud__banner-slot,
  .sp-hud[data-touch="true"]:has(> .sp-hud__parts) .sp-reconnecting { grid-row: 8; }

  /* The live board (ui/RunBoard): its own row under the controls, at the right,
     above the level-up strip and everything transient (spec: The board slot). */
  .sp-hud[data-touch="true"]:has(> .sp-hud__board) {
    grid-template-rows: auto auto auto auto auto auto auto minmax(0, 1fr);
  }
  .sp-hud[data-touch="true"] .sp-hud__board { grid-column: 1 / -1; grid-row: 5; justify-self: end; }
  .sp-hud[data-touch="true"]:has(> .sp-hud__board) .sp-hud__announcer { grid-row: 6; }
  .sp-hud[data-touch="true"]:has(> .sp-hud__board) .sp-hud__feed { grid-row: 7; }
  .sp-hud[data-touch="true"]:has(> .sp-hud__board) .sp-hud__banner-slot,
  .sp-hud[data-touch="true"]:has(> .sp-hud__board) .sp-reconnecting { grid-row: 8; }
  .sp-hud[data-touch="true"]:has(> .sp-hud__board):has(> .sp-hud__parts) {
    grid-template-rows: auto auto auto auto auto auto auto auto minmax(0, 1fr);
  }
  .sp-hud[data-touch="true"]:has(> .sp-hud__board) .sp-hud__parts { grid-row: 6; }
  .sp-hud[data-touch="true"]:has(> .sp-hud__board):has(> .sp-hud__parts) .sp-hud__announcer { grid-row: 7; }
  .sp-hud[data-touch="true"]:has(> .sp-hud__board):has(> .sp-hud__parts) .sp-hud__feed { grid-row: 8; }
  .sp-hud[data-touch="true"]:has(> .sp-hud__board):has(> .sp-hud__parts) .sp-hud__banner-slot,
  .sp-hud[data-touch="true"]:has(> .sp-hud__board):has(> .sp-hud__parts) .sp-reconnecting { grid-row: 9; }
  /* The board carries the place: the pill keeps the clock and the kills. */
  .sp-hud[data-touch="true"]:has(> .sp-hud__board) .sp-match__place {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}

/* Landscape: the band is one strip the minimap's height — the minimap, the
   pill over the bars, a spacer, then the latency pill (the invite button under
   it), the superweapon and the badge — and it ends in the gear's empty column.
   The log is centred under it; the feed takes the right edge and spans the
   banner's row too, so two feed rows never push the banner onto the ship. */
@media (orientation: landscape) {
  /* The controls strip keeps out of both thumbs' corners, so it never sits under the special
     button: it wraps onto more lines between them instead. */
  .sp-hud[data-touch="true"] .sp-hud__hint {
    max-width: calc(100% - 2 * max(190px, 24vw));
    top: auto;
    bottom: max(10px, env(safe-area-inset-bottom));
  }
  .sp-hud[data-touch="true"] {
    --banner-size: 18px;
    grid-template-columns: auto auto minmax(0, 1fr) auto auto auto var(--gear-reserve);
    grid-template-rows: auto auto auto minmax(0, 1fr);
  }

  .sp-hud[data-touch="true"] .sp-hud__minimap { grid-column: 1; grid-row: 1 / 3; }
  .sp-hud[data-touch="true"] .sp-hud__match { grid-column: 2; grid-row: 1; }
  .sp-hud[data-touch="true"] .sp-hud__vitals { grid-column: 2; grid-row: 2; }
  .sp-hud[data-touch="true"] .sp-hud__net { grid-column: 4; grid-row: 1 / 3; justify-self: start; align-self: start; }
  .sp-hud[data-touch="true"] .sp-hud__invite { grid-column: 4; grid-row: 1 / 3; justify-self: start; align-self: end; }
  .sp-hud[data-touch="true"] .sp-hud__chat { grid-column: 3; grid-row: 1 / 3; justify-self: end; align-self: start; }
  .sp-hud[data-touch="true"] .sp-hud__quick-chat { top: calc(max(8px, env(safe-area-inset-top)) + 92px); }
  .sp-hud[data-touch="true"] .sp-hud__superweapon { grid-column: 5; grid-row: 1 / 3; justify-self: start; align-self: start; }
  .sp-hud[data-touch="true"] .sp-hud__badge { grid-column: 6; grid-row: 1 / 3; justify-self: start; align-self: start; }
  /* The modular level-up strip: the band's empty middle column, beside the bars. */
  .sp-hud[data-touch="true"] .sp-hud__parts { grid-column: 3; grid-row: 1 / 3; align-self: start; min-width: 0; }

  .sp-hud[data-touch="true"] .sp-hud__announcer { grid-row: 3; max-width: 40%; }
  .sp-hud[data-touch="true"] .sp-hud__feed {
    grid-row: 3 / 5;
    justify-self: end;
    align-items: flex-end;
    width: min(30%, 280px);
  }

  .sp-hud[data-touch="true"] .sp-hud__banner-slot,
  .sp-hud[data-touch="true"] .sp-reconnecting { grid-row: 4; }

  /* The live board (ui/RunBoard): the band's empty middle column, at its right-hand
     end, a step in from the chat button while there is one. The level-up strip
     wants the same column: while both show the board steps out of sight (remove
     before shrinking) and the pill keeps its place (spec: The board slot). */
  .sp-hud[data-touch="true"] .sp-hud__board { grid-column: 3; grid-row: 1 / 3; justify-self: end; align-self: start; }
  .sp-hud[data-touch="true"]:has(.sp-hud__chat) .sp-hud__board { margin-right: 52px; }
  .sp-hud[data-touch="true"]:has(> .sp-hud__parts) .sp-hud__board,
  .sp-hud[data-touch="true"]:has(> .sp-hud__board):not(:has(> .sp-hud__parts)) .sp-match__place {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}

/* ---------------------------------------------------- damage feedback --- */

/* The chip a hit leaves on the track: what it took, where it took it. */
.sp-meter__chip {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--chip-start) * 100%);
  width: calc(var(--chip) * 100%);
  pointer-events: none;
  background: color-mix(in oklab, var(--sp-danger) 55%, transparent);
  border-radius: 0 2px 2px 0;
}

/* One notch per recent hit, fading as the parent lowers --alpha. */
.sp-meter__tick {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--at) * 100%);
  width: 1px;
  pointer-events: none;
  background: var(--sp-danger);
  opacity: var(--alpha);
}

/* Shield hit points laid past the fill. */
.sp-meter__shield {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--shield-start) * 100%);
  width: calc(var(--shield) * 100%);
  pointer-events: none;
  background: color-mix(in oklab, var(--sp-accent-2) 80%, transparent);
  box-shadow: 0 0 8px color-mix(in oklab, var(--sp-accent-2) 60%, transparent);
  border-radius: 2px;
}

.sp-hud__shield-value { color: var(--sp-accent-2); }

/* Critical: the value in danger, and a still vignette round the screen. */
.sp-hud[data-critical="true"] [data-part="hull-value"] { color: var(--sp-danger); }
.sp-hud[data-critical="true"]::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  box-shadow: inset 0 0 120px color-mix(in oklab, var(--sp-danger) 22%, transparent);
}

/* The feed's Quanta chip: what the local player earned by this kill. */
.sp-feed__quanta {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font: 500 12px var(--sp-font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--sp-quanta);
  text-shadow: 0 0 8px color-mix(in oklab, var(--sp-quanta) 60%, transparent);
}

/* ------------------------------------------------------- build sheet --- */

/* The way into the build sheet (ui/BuildSheet): the ship's name, with its key
   beside it. On a phone the name is hidden and the button covers the whole
   hull panel, which sits in the top band, out of every stick zone. */
.sp-hud__build-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  justify-self: start;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  pointer-events: auto;
}

.sp-hud__build-toggle:hover .sp-hud__ship,
.sp-hud__build-toggle[aria-expanded="true"] .sp-hud__ship {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.sp-hud__build-toggle:focus-visible {
  outline: 2px solid var(--ship);
  outline-offset: 3px;
  border-radius: 4px;
}

.sp-hud__build-key { font-size: 12px; opacity: 0.75; }

.sp-hud[data-touch="true"] .sp-hud__build-toggle {
  position: absolute;
  inset: 0;
  /* An absolute grid item still honours its self-alignment: stretch it, or it
     shrinks to its (hidden) contents. */
  justify-self: stretch;
  align-self: stretch;
  z-index: 1;
  border-radius: inherit;
}

/* ------------------------------------ superweapon call and feed icons --- */

/* The five colours the arena draws the superweapons in (client/Renderer's
   SUPERWEAPON_COLOR_KEYS), for the call strip, the feed chip and the icon. */
.sp-hud__call,
.sp-feed__weapon[data-superweapon],
.sp-sw-icon { --superweapon: var(--sp-text); }
.sp-hud__call[data-superweapon="singularity"],
.sp-feed__weapon[data-superweapon="singularity"],
.sp-sw-icon[data-superweapon="singularity"] { --superweapon: var(--sp-paint-crimson); }
.sp-hud__call[data-superweapon="lance"],
.sp-feed__weapon[data-superweapon="lance"],
.sp-sw-icon[data-superweapon="lance"] { --superweapon: var(--sp-accent-2); }
.sp-hud__call[data-superweapon="nova"],
.sp-feed__weapon[data-superweapon="nova"],
.sp-sw-icon[data-superweapon="nova"] { --superweapon: var(--sp-paint-void); }
.sp-hud__call[data-superweapon="bombardment"],
.sp-feed__weapon[data-superweapon="bombardment"],
.sp-sw-icon[data-superweapon="bombardment"] { --superweapon: var(--sp-paint-solar); }
.sp-hud__call[data-superweapon="overload"],
.sp-feed__weapon[data-superweapon="overload"],
.sp-sw-icon[data-superweapon="overload"] { --superweapon: var(--sp-paint-magenta); }
.sp-hud__call[data-superweapon="railgun"],
.sp-feed__weapon[data-superweapon="railgun"],
.sp-sw-icon[data-superweapon="railgun"] { --superweapon: var(--sp-paint-ice); }
.sp-hud__call[data-superweapon="emp"],
.sp-feed__weapon[data-superweapon="emp"],
.sp-sw-icon[data-superweapon="emp"] { --superweapon: var(--sp-paint-aurora); }
.sp-hud__call[data-superweapon="airstrike"],
.sp-feed__weapon[data-superweapon="airstrike"],
.sp-sw-icon[data-superweapon="airstrike"] { --superweapon: var(--sp-paint-ember); }

/* The call: "NOVA — Vex", under the standing readout, for everyone. It fades
   out over the Announcer's SUPERWEAPON_CALL_MS, keyed per fire, so a second
   call replays its entrance. */
.sp-hud__call {
  display: flex;
  align-items: center;
  gap: 10px;
  width: fit-content;
  max-width: min(92vw, 440px);
  min-width: 0;
  padding: 7px 16px;
  font: 700 13px/1 var(--sp-font-mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--superweapon);
  background: color-mix(in oklab, var(--sp-space-0) 74%, transparent);
  border: 1px solid color-mix(in oklab, var(--superweapon) 65%, transparent);
  border-radius: 999px;
  box-shadow: 0 0 26px -6px var(--superweapon), inset 0 0 14px -8px var(--superweapon);
  backdrop-filter: blur(6px);
  animation: sp-call 2600ms var(--sp-ease-out) both;
}

.sp-hud__call .sp-sw-icon { --size: 18px; }

.sp-call__name {
  flex: none;
  font-size: 16px;
  letter-spacing: 0.22em;
  text-shadow: 0 0 12px color-mix(in oklab, var(--superweapon) 70%, transparent);
}

.sp-call__dash { flex: none; opacity: 0.65; }

.sp-call__pilot {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  letter-spacing: 0.08em;
  color: var(--sp-text);
}

.sp-hud__call[data-local="true"] .sp-call__pilot { color: var(--sp-accent-2); }

@keyframes sp-call {
  0% { opacity: 0; transform: translateY(-8px) scale(0.94); }
  7% { opacity: 1; transform: none; }
  82% { opacity: 1; transform: none; }
  100% { opacity: 0; transform: none; }
}

/* A phone: the call heads the banner's slot, under the band, never on the ship. */
.sp-hud[data-touch="true"] .sp-hud__call {
  margin: 0 auto 6px;
  padding: 5px 12px;
  gap: 8px;
  font-size: 12px;
}
.sp-hud[data-touch="true"] .sp-call__name { font-size: 13px; letter-spacing: 0.18em; }
.sp-hud[data-touch="true"] .sp-hud__call .sp-sw-icon { --size: 15px; }

/* The feed chip of a kill a superweapon scored: its icon leads the label, and
   a superweapon's own kill takes its colour. An Overload kill keeps the ship
   weapon's chip and adds the Overload's icon. */
.sp-feed__weapon[data-superweapon] {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}
.sp-feed__weapon[data-superweapon]:not([data-superweapon="overload"]) {
  --weapon: var(--superweapon);
  box-shadow: 0 0 10px -3px var(--superweapon);
}
.sp-feed__weapon .sp-sw-icon { --size: 13px; }

/* The icons: each is the silhouette its pickup marker is drawn in — a hole in
   rings, two rails round a needle, a core in blooming rings, a reticle, a
   spiked star — so a colour-blind player tells them apart by shape. */
.sp-sw-icon {
  --size: 14px;
  position: relative;
  display: inline-block;
  flex: none;
  width: var(--size);
  height: var(--size);
  color: var(--superweapon);
  filter: drop-shadow(0 0 3px color-mix(in oklab, var(--superweapon) 70%, transparent));
}

.sp-sw-icon__shape {
  position: absolute;
  inset: 0;
  box-sizing: border-box;
}

.sp-sw-icon[data-superweapon="singularity"] .sp-sw-icon__shape {
  border-radius: 50%;
  background: radial-gradient(circle, var(--sp-space-0) 0 36%, currentColor 39% 56%, transparent 59% 70%, currentColor 73% 86%, transparent 89%);
}

.sp-sw-icon[data-superweapon="lance"] .sp-sw-icon__shape {
  inset: -8% 20%;
  transform: rotate(45deg);
  background: linear-gradient(90deg, currentColor 0 22%, transparent 22% 39%, var(--sp-text) 39% 61%, transparent 61% 78%, currentColor 78%);
}

.sp-sw-icon[data-superweapon="nova"] .sp-sw-icon__shape {
  border-radius: 50%;
  background: radial-gradient(circle, currentColor 0 24%, transparent 27% 44%, currentColor 47% 60%, transparent 63% 78%, currentColor 81% 94%, transparent 97%);
}

.sp-sw-icon[data-superweapon="bombardment"] .sp-sw-icon__shape {
  border: 2px solid currentColor;
  border-radius: 50%;
  background:
    radial-gradient(circle, currentColor 0 17%, transparent 20%),
    linear-gradient(currentColor, currentColor) center top / 2px 30% no-repeat,
    linear-gradient(currentColor, currentColor) center bottom / 2px 30% no-repeat,
    linear-gradient(currentColor, currentColor) left center / 30% 2px no-repeat,
    linear-gradient(currentColor, currentColor) right center / 30% 2px no-repeat;
}

.sp-sw-icon[data-superweapon="overload"] .sp-sw-icon__shape {
  background: currentColor;
  clip-path: polygon(50% 0, 61% 30%, 85% 15%, 70% 39%, 100% 50%, 70% 61%, 85% 85%, 61% 70%, 50% 100%, 39% 70%, 15% 85%, 30% 61%, 0 50%, 30% 39%, 15% 15%, 39% 30%);
}

/* A tank arena's own: a slug on two rails, a core in a ring, and a run of chevrons. */
.sp-sw-icon[data-superweapon="railgun"] .sp-sw-icon__shape {
  inset: -8% 20%;
  transform: rotate(45deg);
  background:
    linear-gradient(currentColor, currentColor) center / 36% 34% no-repeat,
    linear-gradient(90deg, currentColor 0 18%, transparent 18% 82%, currentColor 82%);
}

.sp-sw-icon[data-superweapon="emp"] .sp-sw-icon__shape {
  border-radius: 50%;
  border: 2px solid currentColor;
  background:
    radial-gradient(circle, currentColor 0 30%, transparent 33%),
    linear-gradient(45deg, transparent 46%, currentColor 46% 54%, transparent 54%),
    linear-gradient(-45deg, transparent 46%, currentColor 46% 54%, transparent 54%);
}

.sp-sw-icon[data-superweapon="airstrike"] .sp-sw-icon__shape {
  background: currentColor;
  clip-path: polygon(0 0, 50% 22%, 100% 0, 100% 22%, 50% 44%, 0 22%, 0 39%, 50% 61%, 100% 39%, 100% 61%, 50% 83%, 0 61%);
}

@media (prefers-reduced-motion: reduce) {
  .sp-hud__call { animation: sp-call-still 2600ms linear both; }
}

@keyframes sp-call-still {
  0%, 85% { opacity: 1; }
  100% { opacity: 0; }
}

/* A quick chat line in the feed (claim 201): the kill row's glass, the name in
   its pilot's colour, the words in the text colour, a touch quieter. */
.sp-feed__entry--chat {
  gap: 6px;
  border-color: color-mix(in oklab, var(--sp-accent-2) 22%, var(--sp-line));
}

.sp-feed__entry--chat .sp-feed__player::after { content: ":"; color: var(--sp-text-dim); }

.sp-feed__chat-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--sp-text);
}

/* A ship that left through the edge storm alive: a line of its own, in the Quanta colour. */
.sp-feed__entry--extracted {
  gap: 6px;
  border-color: color-mix(in oklab, var(--sp-accent-2) 40%, var(--sp-line));
}

.sp-feed__extracted {
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--sp-accent-2);
}

.sp-feed__extracted-tier {
  color: var(--sp-text-dim);
  font-variant-numeric: tabular-nums;
}

/* ------------------------------------------------------------ parts slot --- */

/* The modular level-up strip (ui/PartPicker), with a pointer: centred, docked above
   the bottom row (the ability panel's), so it never sits on the ship or the vitals. */
.sp-hud {
  --parts-lift: 124px;
}

.sp-hud:not([data-touch="true"]) .sp-hud__parts {
  position: absolute;
  left: 50%;
  bottom: var(--parts-lift);
  z-index: 1;
  transform: translateX(-50%);
  max-width: calc(100% - 48px);
}

/* The off-screen warning arrows (docs/modular-ships.md §10b): a picture at the screen's edge. */
.sp-hud__threats {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 6; /* over the panels: an arrow at a corner is still read */
}

.sp-threat {
  position: absolute;
  /* Shifted by its own share of the screen: at the left edge it hangs right of its place,
     at the right edge left of it, so the label is always on the screen. */
  transform: translate(calc(var(--threat-x, 0.5) * -100%), calc(var(--threat-y, 0.5) * -100%));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  color: var(--sp-rarity-legendary);
  pointer-events: none;
}

.sp-threat[data-kind="ship"] {
  color: var(--sp-danger);
}

.sp-threat__arrow {
  display: block;
  font-size: 28px;
  line-height: 1;
  transform: rotate(var(--threat-angle, 0rad));
  text-shadow: 0 0 8px currentColor;
}

.sp-threat__label {
  font-family: var(--sp-font-mono);
  font-size: 12px;
  line-height: 1.2;
  white-space: nowrap;
  padding: 1px 6px;
  border-radius: var(--sp-radius-sm);
  background: var(--sp-glass);
  color: var(--sp-text);
  border: 1px solid currentColor;
}

@media (prefers-reduced-motion: no-preference) {
  .sp-threat__arrow {
    animation: sp-threat-pulse 900ms var(--sp-ease-out) infinite alternate;
  }
}

@keyframes sp-threat-pulse {
  from { opacity: 1; }
  to { opacity: 0.55; }
}

/* With a pointer, the level-up strip takes the band above the vitals where the tip and the
   control hint sit: while it shows they go above the ship instead, the clearance kept. */
.sp-hud:not([data-touch="true"]):has(> .sp-hud__parts) > .sp-hud__tip {
  top: auto;
  bottom: calc(50% + var(--hint-clearance));
}
.sp-hud:not([data-touch="true"]):has(> .sp-hud__parts) > .sp-hud__hint {
  top: auto;
  bottom: calc(50% + var(--hint-clearance));
}
.sp-hud:not([data-touch="true"]):has(> .sp-hud__parts):has(> .sp-hud__tip) > .sp-hud__hint {
  bottom: calc(50% + var(--hint-clearance) + var(--tip-step));
}

/* Desktop (2026-10-09): the HUD's sizes were set for phones and read tiny on a monitor. With a
   mouse on a large enough window it scales up whole (zoom keeps every proportion and hit area),
   and is sized down by the same factor so the layer still covers the viewport exactly. */
@media (pointer: fine) and (min-width: 1000px) and (min-height: 640px) {
  .sp-hud {
    --hud-zoom: 1.15;
    zoom: var(--hud-zoom);
    inset: 0 auto auto 0;
    width: calc(100vw / var(--hud-zoom));
    height: calc(100vh / var(--hud-zoom));
  }
}
@media (pointer: fine) and (min-width: 1400px) and (min-height: 860px) {
  .sp-hud { --hud-zoom: 1.3; }
}
@media (pointer: fine) and (min-width: 1900px) and (min-height: 1050px) {
  .sp-hud { --hud-zoom: 1.5; }
}
