/* pvpTD — Game HUD & In-game Overlays */

/* Top bar HUD */
.game-hud {
  position: absolute;
  inset: 0;
  z-index: var(--z-hud);
  pointer-events: none;
  overflow: hidden;
}

.hud-top-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 16px;
  background: linear-gradient(180deg, rgba(14, 10, 26, 0.92), rgba(14, 10, 26, 0.65));
  pointer-events: auto;
}

.hud-gold {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 20px;
  font-weight: bold;
  min-width: 110px;
  padding: 4px 12px;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.08);
}
.hud-gold.defender { color: var(--color-defender); }
.hud-gold.attacker { color: var(--color-attacker); text-align: right; justify-content: flex-end; }

/* Whose purse is whose (P0 #9) */
.hud-gold .hud-gold-owner {
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 1px;
  opacity: 0.85;
  padding: 2px 5px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.1);
}
.hud-gold.mine { border-color: currentColor; box-shadow: 0 0 10px rgba(255, 255, 255, 0.08); }
.hud-gold.defender.mine .hud-gold-owner { background: var(--color-defender); color: #14101f; }
.hud-gold.attacker.mine .hud-gold-owner { background: var(--color-attacker); color: #14101f; }
.hud-gold.theirs { opacity: 0.62; transform: scale(0.92); }

/* Wave composer (Phase 2): stack a wave, send it as one decision */
.wave-composer {
  position: absolute;
  pointer-events: auto; /* the HUD root is pointer-transparent */
  bottom: 302px; /* clear of the 3-row send bar */
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: 80vw;
  padding: 8px 10px;
  border-radius: var(--radius-lg);
  background: rgba(20, 16, 31, 0.94);
  border: 1px solid var(--color-attacker);
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.5);
  z-index: 31;
}
.wave-composer.hidden { display: none; }

/* Synergy chips float above the composer (doc 08 Phase 2) */
.wave-synergies {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 6px;
  white-space: nowrap;
}
.wave-synergies.hidden { display: none; }
.synergy-chip {
  border: 1px solid rgba(240, 192, 64, 0.5);
  border-radius: 999px;
  background: rgba(20, 16, 31, 0.94);
  color: var(--color-gold, #f0c040);
  font-size: 11px;
  font-weight: 700;
  padding: 4px 12px;
  cursor: pointer;
  transition: transform 0.12s ease, background 0.12s ease;
}
.synergy-chip:hover { transform: translateY(-1px); background: rgba(40, 32, 58, 0.96); }
.synergy-chip.active {
  border-color: var(--color-gold, #f0c040);
  background: rgba(240, 192, 64, 0.16);
  animation: quest-glow 1.6s ease infinite;
  cursor: default;
}
.synergy-chip.hint { opacity: 0.85; border-style: dashed; }
.wave-chips { display: flex; flex-wrap: wrap; gap: 6px; max-width: 46vw; }
.wave-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 9px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.07);
  color: var(--color-text);
  font-size: 12px;
  cursor: pointer;
}
.wave-chip:hover { border-color: var(--color-attacker); }
.wave-chip-dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.wave-send-btn { white-space: nowrap; font-weight: 800; }
.wave-send-btn.too-expensive { opacity: 0.55; }
.wave-clear-btn { padding: 4px 9px; }

/* Incoming-wave banner (defender telegraph, Phase 2) */
.wave-banner {
  position: absolute;
  top: 64px;
  left: 50%;
  transform: translateX(-50%);
  padding: 8px 20px;
  border-radius: 999px;
  background: rgba(20, 16, 31, 0.92);
  border: 1px solid var(--color-attacker);
  color: var(--color-attacker);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.5px;
  pointer-events: none;
  z-index: 32;
}
.wave-banner.hidden { display: none; }
.wave-banner.pop { animation: wave-banner-pop 0.35s ease; }
@keyframes wave-banner-pop {
  0% { transform: translateX(-50%) scale(0.7); opacity: 0; }
  100% { transform: translateX(-50%) scale(1); opacity: 1; }
}

/* Moments engine banners (doc 08 Phase 2) — center-screen drama beats */
/* Job 060: this was `white-space: nowrap` at a fixed 26px, so on a 360px phone
   the ten-seconds-left banner ran off both edges — "not visible full". It also
   sat at 34% of the screen, right over the part of the board you are playing
   on. It now wraps inside the viewport, sits higher, and fades out of the way
   while you are choosing a tile. */
.moment-banner {
  position: absolute;
  top: 24%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 10px 24px;
  border-radius: 10px;
  background: rgba(20, 16, 31, 0.88);
  border: 2px solid var(--color-gold, #f0c040);
  color: var(--color-gold, #f0c040);
  font-size: clamp(16px, 5.2vw, 26px);
  font-weight: 900;
  letter-spacing: 1.5px;
  text-align: center;
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.7);
  pointer-events: none;
  z-index: 40;
  /* max-content first, so it only wraps when it genuinely doesn't fit —
     otherwise the shrink-to-fit box breaks "10 SECONDS LEFT" into a 72px
     column, which is a different kind of unreadable. */
  width: max-content;
  max-width: 92vw;
  white-space: normal;
  overflow-wrap: break-word;
}
.moment-banner.hidden { display: none; }
.moment-banner.pop { animation: moment-pop 0.45s cubic-bezier(0.2, 1.6, 0.4, 1); }
@keyframes moment-pop {
  0% { transform: translate(-50%, -50%) scale(0.4); opacity: 0; }
  60% { transform: translate(-50%, -50%) scale(1.12); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}
.moment-banner.moment-lastlife {
  border-color: var(--color-attacker, #e94560);
  color: var(--color-attacker, #e94560);
  animation: moment-pop 0.45s cubic-bezier(0.2, 1.6, 0.4, 1), moment-throb 0.9s ease 0.45s infinite;
}
@keyframes moment-throb {
  0%, 100% { transform: translate(-50%, -50%) scale(1); }
  50% { transform: translate(-50%, -50%) scale(1.05); }
}
.moment-banner.moment-comeback {
  border-color: var(--color-defender, #4ecca3);
  color: var(--color-defender, #4ecca3);
}
.moment-banner.moment-final {
  border-color: #fff;
  color: #fff;
}

/* Final-10-seconds timer urgency */
.hud-timer.final-ten {
  color: var(--color-attacker, #e94560) !important;
  animation: final-ten-pulse 1s ease infinite;
}
@keyframes final-ten-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.18); }
}

/* Affordability countdown: styled with the rest of the can't-afford treatment,
   further down (job 080). It used to live here as 9px in the card's corner. */

/* Placement-mode hint above the build bar */
.placement-hint {
  position: absolute;
  /* Clears the build bar: it is 132px tall sitting at bottom:10, so anything
     below 142 renders inside it. At 118 the hint had always been drawn on top
     of the tower cards — invisible as a bug until the matrix started measuring
     placement mode (job 051). */
  bottom: 152px;
  left: 50%;
  transform: translateX(-50%);
  padding: 6px 14px;
  border-radius: 999px;
  background: rgba(20, 16, 31, 0.92);
  border: 1px solid var(--color-defender);
  color: var(--color-defender);
  font-size: 12px;
  font-weight: 600;
  /* Tappable: "Esc cancels" is meaningless on a phone, and with no visible way
     out Dima was "getting stuck" with a selected tower (job 051). */
  pointer-events: auto;
  cursor: pointer;
  z-index: 30;
}
.placement-hint:hover { background: rgba(40, 32, 58, 0.96); }
.placement-hint .hint-cancel {
  margin-left: 8px;
  padding: 1px 8px;
  border-radius: 999px;
  background: rgba(233, 69, 96, 0.18);
  border: 1px solid rgba(233, 69, 96, 0.5);
  color: var(--color-attacker, #e94560);
  font-weight: 800;
}
.placement-hint.hidden { display: none; }

/* Confirm stage (job 054): a ghost is parked and one action away from being
   built. This is the stage that did not exist — the chip said "tap a tile" as
   if nothing had happened, so the player tapped a different tile and re-parked
   forever. Now the commit is a button you can see. */
.placement-hint.confirm {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px 6px 14px;
  cursor: default;
  border-color: var(--color-gold, #f5c542);
  color: var(--color-gold, #f5c542);
  animation: hint-confirm-in 160ms ease-out;
}
.placement-hint.confirm:hover { background: rgba(20, 16, 31, 0.92); }
.placement-hint .hint-text { white-space: nowrap; }
.placement-hint .hint-build {
  padding: 6px 14px;
  min-height: 32px;
  border-radius: 999px;
  background: var(--color-defender, #4ecca3);
  border: 1px solid var(--color-defender, #4ecca3);
  color: #0b1a14;
  font-size: 13px;
  font-weight: 800;
  cursor: pointer;
  white-space: nowrap;
}
.placement-hint .hint-build:active { transform: scale(0.96); }
.placement-hint.confirm .hint-cancel {
  margin-left: 0;
  padding: 6px 12px;
  min-height: 32px;
  cursor: pointer;
}
@keyframes hint-confirm-in {
  from { opacity: 0; transform: translateX(-50%) translateY(6px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}
/* Connection pill (job 055). Sits under the top bar, centred, and never covers
   a cell you might want to tap — it is only as wide as its text. */
.net-status {
  position: absolute;
  top: calc(var(--hud-top-h, 56px) + 8px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
  background: rgba(20, 16, 31, 0.94);
  border: 1px solid rgba(255, 255, 255, 0.15);
  color: #fff;
  z-index: 60;
  pointer-events: none; /* #ui-root is click-through; only the button opts in */
  max-width: 92vw;
}
.net-status.hidden { display: none; }
.net-status.warn { border-color: #f5c542; color: #f5c542; }
.net-status.ok   { border-color: var(--color-defender, #4ecca3); color: var(--color-defender, #4ecca3); }
.net-status.bad  { border-color: var(--color-attacker, #e94560); color: var(--color-attacker, #e94560); }
.net-status-btn {
  pointer-events: auto;
  padding: 6px 12px;
  min-height: 32px;
  border-radius: 999px;
  border: 1px solid currentColor;
  background: rgba(255, 255, 255, 0.08);
  color: inherit;
  font-weight: 800;
  font-size: 12px;
  cursor: pointer;
}
.net-status-btn.hidden { display: none; }
.net-status-btn:disabled { opacity: 0.5; cursor: default; }

/* Thumbs need 44px; a mouse does not. */
@media (hover: none) and (pointer: coarse) {
  .placement-hint .hint-build,
  .placement-hint.confirm .hint-cancel { min-height: 42px; padding-left: 16px; padding-right: 16px; }
}

.hud-leave-btn {
  background: rgba(233, 69, 96, 0.15);
  border: 1px solid rgba(233, 69, 96, 0.3);
  color: var(--color-danger);
  font-size: 14px;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.2s, border-color 0.2s;
  flex-shrink: 0;
}
.hud-leave-btn:hover {
  background: rgba(233, 69, 96, 0.3);
  border-color: var(--color-danger);
}
.hud-gold .gold-coin-icon { vertical-align: middle; flex-shrink: 0; }
.hud-gold .gold-coin-icon svg { width: 22px; height: 22px; }
.lives-icon { vertical-align: middle; flex-shrink: 0; }

.hud-gold.flash {
  animation: gold-bounce 0.35s var(--spring-bounce);
}

.hud-center {
  display: flex;
  gap: 20px;
  align-items: center;
  font-size: 16px;
}

.hud-lives {
  display: flex;
  align-items: center;
  gap: 5px;
  color: var(--color-lives);
  font-weight: bold;
  font-size: 18px;
  padding: 4px 12px;
  border-radius: 20px;
  background: rgba(255, 107, 107, 0.08);
  border: 1px solid rgba(255, 107, 107, 0.12);
}
.hud-lives.shake {
  animation: lives-damage 0.5s var(--spring-bounce);
}

.hud-timer {
  color: var(--color-timer);
  font-weight: bold;
  font-size: 22px;
  min-width: 70px;
  text-align: center;
  padding: 4px 14px;
  border-radius: 20px;
  background: rgba(255, 217, 61, 0.08);
  border: 1px solid rgba(255, 217, 61, 0.12);
}
.hud-timer.urgent {
  color: var(--color-danger);
  animation: pulse 0.8s ease infinite;
  border-color: rgba(233, 69, 96, 0.3);
  background: rgba(233, 69, 96, 0.1);
}

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

/* Build bar (defender) */
.build-bar {
  position: absolute;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
  z-index: var(--z-hud);
  display: flex;
  gap: 8px;
  padding: 10px 14px;
  background: rgba(14, 10, 26, 0.88);
  border: 2px solid rgba(78, 204, 163, 0.2);
  border-radius: 16px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
  pointer-events: auto;
  max-width: 95vw;
  overflow-x: auto;
}
.build-bar.hidden { display: none; }

/* Tower card */
.tower-card {
  /* pan-x, not none: `none` stopped the browser panning the bar, and since a
     finger always starts ON a card the seven towers were unreachable on a
     360px phone (job 051). pan-x lets the browser scroll horizontally while
     vertical gestures still reach pointermove for drag-from-card. */
  touch-action: pan-x;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  width: var(--card-width-tower);
  min-width: var(--card-width-tower);
  height: var(--card-height-tower);
  flex-shrink: 0;
  padding: 6px 4px 5px;
  /* No frame at rest (job 080). The frame is the SELECTION channel and nothing
     else — Dima: "the tower shouldn't have a frame at all — the one I'm
     selecting, the one getting the frame, the green one." The width stays so
     that gaining a frame never reflows the bar; only the colour appears. */
  border: var(--card-border-width) solid transparent;
  border-radius: var(--card-radius);
  background: linear-gradient(180deg, rgba(30, 20, 64, 0.95), rgba(20, 14, 44, 0.98));
  color: var(--color-text);
  cursor: pointer;
  transition: transform 0.2s var(--spring-bounce), box-shadow 0.2s ease, border-color 0.15s ease;
  position: relative;
  box-shadow: var(--card-shadow-idle);
  overflow: hidden;
}

/* Card shine overlay */
.tower-card::after {
  content: '';
  position: absolute;
  top: 0;
  left: -50%;
  width: 50%;
  height: 100%;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.06), transparent);
  pointer-events: none;
  transition: left 0.4s ease;
}
.tower-card:hover::after {
  left: 100%;
}

.tower-card:hover:not(:disabled):not(.cant-afford) {
  border-color: var(--color-defender);
  transform: translateY(-8px) scale(1.05);
  /* see the (hover: none) guard below — this lift is mouse-only */
  box-shadow: var(--card-shadow-hover);
  z-index: 2;
}
.tower-card:active:not(:disabled):not(.cant-afford) {
  transform: translateY(-4px) scale(0.97);
}
.tower-card:disabled {
  cursor: not-allowed;
}
/* The one card wearing a frame is the one you picked.
   Specificity is deliberate: `.tower-card.selected` (0,2,0) used to tie with
   `.tower-card.can-buy` (0,2,0) and lose on source order, so the green frame
   NEVER appeared on a tower you could actually afford — the border stayed gold
   and the glow stayed the gold ready-pulse. Selecting Arrow changed literally
   no pixel (job 080, measured). `.build-bar` in front makes it 0,3,0 so this
   can't be quietly outranked again. */
.build-bar .tower-card.selected,
.build-bar .tower-card.selected.cant-afford {
  border-color: var(--color-defender);
  --glow-color: rgba(78, 204, 163, 0.55);
  /* Glow only — the bounce scaled and lifted the card out from under the thumb
     that had just tapped it. Dima: "we're bumping it up a bit, but doesn't make
     much sense. Highlight is enough." (job 051) */
  animation: card-glow-pulse 1.8s ease infinite;
  z-index: 3;
}

/* Touch devices latch :hover on the last-tapped element, so the mouse lift
   stayed applied to the card the thumb had just used — which is the "bumping it
   up" Dima saw. Hover effects are for pointers that can actually hover. */
@media (hover: none) {
  .tower-card:hover:not(:disabled):not(.cant-afford),
  .mob-card:hover:not(:disabled):not(.cant-afford) {
    transform: none;
  }
}

.tower-card .tower-icon {
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 2px;
  /* Positioning + stacking context for `.afford-ring` (job 080). */
  position: relative;
  z-index: 0;
}

.tower-card .tower-name {
  font-size: 10px;
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
}
.tower-card .tower-cost {
  font-size: 11px;
  font-weight: 700;
  color: var(--color-gold);
  display: flex;
  align-items: center;
  gap: 2px;
}
.tower-card .hotkey {
  position: absolute;
  top: 3px;
  right: 3px;
}

/* Hover tooltip for tower and mob cards */
.btn-tooltip {
  display: none;
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
  min-width: 140px;
  max-width: 200px;
  padding: 8px 10px;
  background: rgba(14, 10, 26, 0.96);
  border: 1px solid var(--color-border-light);
  border-radius: var(--card-radius);
  box-shadow: var(--shadow-lg);
  font-size: 11px;
  line-height: 1.4;
  color: var(--color-text);
  text-align: center;
  z-index: calc(var(--z-panel) + 10);
  pointer-events: none;
  white-space: normal;
}
.btn-tooltip .tip-stats {
  font-size: 10px;
  color: var(--color-text-dim);
}
.tower-card:hover .btn-tooltip,
.mob-card:hover .btn-tooltip {
  display: block;
  animation: fade-in 0.15s ease;
}

/* Mob send bar (attacker) */
/* Centred with auto margins between left:0 and right:0, NOT with
   left:50% + translateX(-50%) (job 102). An absolutely positioned shrink-to-fit
   box only gets the width from its `left` offset to the containing block's
   right edge, so `left: 50%` handed this bar exactly half the screen — and
   because it WRAPS, it spent that half going tall: measured on the attacker HUD
   at 195px wide / 380px tall on a 390x844 phone (2 cards per row, 5 rows) and
   420x228 on an 840x760 foldable. That is Dima's "in folded device it's
   vertically aligned". The build bar never showed the same bug because it does
   not wrap: its min-content width is the whole row, so shrink-to-fit had
   nothing to squeeze.

   Job 112: it does not wrap any more either. A wrapping 10-mob roster was two
   rows on a phone — 219px against the build bar's 126 — and since job 083 fits
   the board to whatever the HUD leaves, the attacker played the same match on
   ~15% less board than the defender (575px canvas → 486px at 390x844). One
   scrolling row for both roles instead. `justify-content` is flex-start, not
   center: `width: max-content` already centres the whole bar between left:0 and
   right:0, and centring the CONTENT of a scroll container puts the overflow at
   both ends, where the leading half is unreachable. Overflowing rightwards also
   leaves a half-visible card at the edge, which is the only scroll affordance
   either bar has. */
.send-bar {
  position: absolute;
  bottom: 10px;
  left: 0;
  right: 0;
  width: max-content;
  margin-left: auto;
  margin-right: auto;
  z-index: var(--z-hud);
  display: flex;
  flex-wrap: nowrap;
  justify-content: flex-start;
  gap: 6px;
  padding: 10px 12px;
  background: rgba(14, 10, 26, 0.88);
  border: 2px solid rgba(233, 69, 96, 0.2);
  border-radius: 16px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
  pointer-events: auto;
  max-width: 95vw;
  overflow-x: auto;
}
.send-bar.hidden { display: none; }

/* Once the bar can use the width it has, the 10-mob roster wants 802px for a
   single row — and the tablet band caps it at 90vw, which is 46px short on an
   840px foldable, so the tenth card fell off the end (it wrapped, before job
   112 made the bar scroll; now it would simply be past the right edge).
   Matching on the id the HUD gives the bar (1,0,0) outranks that rule without
   editing it, and the effect stops at 641px, where the phone's own 100vw cap and
   the scrolling row take over. */
@media (min-width: 641px) {
  #send-bar { max-width: calc(100vw - 8px); }
}

/* Mob card */
.mob-card {
  /* The trap job 051 found on the build bar, which this bar inherited the day it
     started scrolling (job 112): a finger always starts ON a card, so without an
     explicit pan-x the send bar is unscrollable by thumb and half the roster is
     unreachable. `.tower-card` has carried this since 051. */
  touch-action: pan-x;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  width: var(--card-width-mob);
  min-width: var(--card-width-mob);
  height: var(--card-height-mob);
  flex-shrink: 0;
  padding: 5px 3px 4px;
  border: var(--card-border-width) solid var(--color-border);
  border-radius: var(--card-radius);
  background: linear-gradient(180deg, rgba(30, 20, 64, 0.95), rgba(20, 14, 44, 0.98));
  color: var(--color-text);
  cursor: pointer;
  transition: transform 0.2s var(--spring-bounce), box-shadow 0.2s ease, border-color 0.15s ease;
  position: relative;
  box-shadow: var(--card-shadow-idle);
  overflow: hidden;
}

/* Card shine overlay */
.mob-card::after {
  content: '';
  position: absolute;
  top: 0;
  left: -50%;
  width: 50%;
  height: 100%;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.06), transparent);
  pointer-events: none;
  transition: left 0.4s ease;
}
.mob-card:hover::after {
  left: 100%;
}

.mob-card:hover:not(:disabled):not(.cant-afford):not(.on-cooldown) {
  border-color: var(--color-attacker);
  transform: translateY(-8px) scale(1.05);
  box-shadow: var(--card-shadow-hover);
  z-index: 2;
}
.mob-card:active:not(:disabled):not(.cant-afford):not(.on-cooldown) {
  transform: translateY(-4px) scale(0.95);
}
.mob-card:disabled {
  cursor: not-allowed;
}
.mob-card.locked {
  cursor: not-allowed;
}

/* Mob tier badge */
.mob-card .mob-tier-badge {
  position: absolute;
  top: 3px;
  left: 3px;
  font-size: 8px;
  font-weight: 800;
  padding: 1px 4px;
  border-radius: 4px;
  line-height: 1.2;
}
.mob-card .mob-tier-badge.tier-1 { background: rgba(78, 204, 163, 0.3); color: #4ecca3; }
.mob-card .mob-tier-badge.tier-2 { background: rgba(240, 192, 64, 0.3); color: #f0c040; }
.mob-card .mob-tier-badge.tier-3 { background: rgba(233, 69, 96, 0.3); color: #e94560; }

.mob-card .mob-icon {
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 4px;
}
.mob-card .mob-name {
  font-size: 9px;
  font-weight: 700;
  line-height: 1.1;
  text-align: center;
}
.mob-card .mob-cost {
  font-size: 10px;
  font-weight: 700;
  color: var(--color-gold);
  display: flex;
  align-items: center;
  gap: 2px;
}
.mob-card .hotkey {
  position: absolute;
  top: 3px;
  right: 3px;
  font-size: 9px;
}

/* Recharge — the classic round shade wiping clockwise off the card.
   Tier gates reuse the same overlay as one long initial cooldown. */
.mob-card.on-cooldown {
  pointer-events: none;
}
.mob-card .cooldown-overlay {
  position: absolute;
  inset: 0;
  border-radius: var(--card-radius);
  overflow: hidden;
  z-index: 4;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.1s;
}
.mob-card.on-cooldown .cooldown-overlay,
.mob-card.locked .cooldown-overlay {
  opacity: 1;
}
.cooldown-overlay .cooldown-shade {
  --cd: 0;
  position: absolute;
  inset: 0;
  background: conic-gradient(
    rgba(22, 22, 32, 0.8) calc(var(--cd) * 1%),
    transparent calc(var(--cd) * 1%)
  );
}

/* Can't-afford styling — second pass (job 080; 062 was the first).
   062 answered "I need to see it with orange" with an orange BORDER. That
   failed for a reason that only shows up on the real bar: affordable cards were
   already wearing a gold `#f0c040` border and a gold pulse, so all seven cards
   were framed in a warm colour at all times and the state change was one amber
   swapped for a slightly different amber. Dima, second pass: "they remain
   orange... they should have this circle contrast switching which will explain
   me where we at."

   So affordability leaves the border entirely (the border is the selection
   channel now) and moves onto the round tower icon, which was carrying no
   information at all. Unaffordable = the circle drains to grey and a
   full-contrast orange arc around it fills toward the price. Two channels that
   nothing else on the card uses, and one of them is a quantity.

   The alpha stays high on purpose — 062's real finding is that dimming reads as
   "off" or as nothing, so the card recedes via actual colour and the parts that
   carry the message (price, ring, label) are at full strength.

   Job 089 then made this the whole client's sentence, not the build bar's:
   the send bar and the upgrade panel spoke 062's orange border until now, so a
   player learned one language for towers and a different one for mobs. Every
   rule below is written for all three surfaces at once, and the JS behind them
   is one function (`client/ui/affordability.js`) rather than three copies. */
.tower-card.cant-afford,
.mob-card.cant-afford,
.utp-upgrade-card.cant-afford {
  opacity: 1;
  filter: none;
  background: linear-gradient(180deg, rgba(11, 8, 20, 0.97), rgba(8, 6, 16, 0.99));
  box-shadow: inset 0 0 0 1px rgba(255, 159, 67, 0.18);
}
/* The border is not the message. It already means something else on each of the
   three: "selected" on a tower card, "ready to send" on a mob card, "this is an
   upgrade" on an upgrade card. That collision is exactly why 062 didn't land. */
.tower-card.cant-afford { border-color: transparent; }
.mob-card.cant-afford { border-color: var(--color-border); }
.tower-card.cant-afford .tower-cost,
.mob-card.cant-afford .mob-cost,
.utp-upgrade-card.cant-afford .utp-card-cost {
  color: var(--color-cant-afford, #ff9f43);
  font-weight: 800;
}
.tower-card.cant-afford .tower-name,
.mob-card.cant-afford .mob-name,
.utp-upgrade-card.cant-afford .utp-card-name { opacity: 0.7; }

/* The contrast switch. The silhouette survives — you are still choosing between
   towers while you save — but the colour does not, so "which of these can I
   build" is answerable without reading a single number.
   The filter goes on the icon's CONTENTS, not on `.tower-icon`: a filter on an
   ancestor applies to its whole rendered subtree and cannot be switched off by
   a descendant rule, so filtering the wrapper would have greyed out the orange
   ring inside it — the one part that has to stay loud. */
.tower-card.cant-afford .tower-icon .card-sprite,
.tower-card.cant-afford .tower-icon svg,
.mob-card.cant-afford .mob-icon .card-sprite,
.mob-card.cant-afford .mob-icon svg,
.utp-upgrade-card.cant-afford .utp-card-icon .utp-sprite,
.utp-upgrade-card.cant-afford .utp-card-icon .utp-dot {
  filter: grayscale(0.92) brightness(0.62) contrast(0.9);
  opacity: 0.6;
}

/* "Where we at": a conic ring around the icon, filled to gold / cost. Sized in
   percentages so it tracks the icon through every breakpoint (36px desktop,
   28px * --ui-scale portrait, 22px landscape-phone) without a media query.
   The mask turns the pie into a ring; where `mask` is unavailable it degrades
   to a filled pie sitting behind the sprite, which still reads as progress. */
.afford-ring {
  position: absolute;
  inset: -18%;
  border-radius: 50%;
  /* Behind the sprite, inside the icon's own stacking context (see the
     `position: relative; z-index: 0` on each icon wrapper) so a negative index
     cannot fall through the card's background. */
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s ease;
  background: conic-gradient(
    var(--color-cant-afford, #ff9f43) calc(var(--afford, 0) * 1%),
    rgba(255, 255, 255, 0.13) calc(var(--afford, 0) * 1%)
  );
  -webkit-mask: radial-gradient(circle closest-side, transparent 68%, #000 70%);
  mask: radial-gradient(circle closest-side, transparent 68%, #000 70%);
}
.cant-afford .afford-ring { opacity: 1; }
/* The reason job 080 left the send bar alone: a mob card's round overlay is
   already spoken for. A recharging or tier-locked card is drawing a conic sweep
   across the whole card, so the ring stands down rather than fighting it — one
   circular indicator at a time, and the nearer reason wins. */
.mob-card.on-cooldown .afford-ring,
.mob-card.locked .afford-ring,
.mob-card.on-cooldown .afford-eta,
.mob-card.locked .afford-eta { opacity: 0; }

/* The icon wrappers have to be stacking contexts for the ring's negative
   z-index. `.tower-card .tower-icon` declares its own above (job 080). */
.mob-card .mob-icon,
.utp-upgrade-card .utp-card-icon {
  position: relative;
  z-index: 0;
}

/* The label. It was 9px in the bottom-right corner and it blanked itself out
   above 120s — i.e. it was absent exactly when the tower was furthest out of
   reach. Now it is a chip across the foot of the card, which is the one place
   on all three cards that was empty. The radius matters on the upgrade card,
   which (unlike the two bars) is not `overflow: hidden`. */
.afford-eta {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  top: auto;
  padding: 2px 0 3px;
  background: var(--color-cant-afford, #ff9f43);
  color: #241000;
  font-size: 9px;
  font-weight: 800;
  letter-spacing: 0.2px;
  text-align: center;
  opacity: 1;
  z-index: 5;
  pointer-events: none;
  border-radius: 0 0 calc(var(--card-radius) - 2px) calc(var(--card-radius) - 2px);
}
/* The upgrade card has no spare foot — the price lives there — so its chip is
   in flow and bleeds out through the card's own padding instead of covering
   the number it is explaining. Same band, same colour, same words. */
.utp-upgrade-card .afford-eta {
  position: static;
  align-self: stretch;
  margin: 3px -10px -8px;
}

/* Job 096 — the answer to a tap you cannot pay for.
   `btn.disabled` on `cant-afford` means a real press produces no click at all
   (measured: mouse and touch both), so the card used to answer a thumb with
   nothing. The button stays disabled; the bar catches pointerdown/pointerup and
   the card flinches while its chip names the number that is missing. Placed
   after `.build-bar .tower-card.selected` on purpose — equal specificity, and a
   refusal on a selected card has to win the `animation` slot for its 0.34s. */
@keyframes card-refuse {
  0%, 100% { transform: translateX(0); }
  15% { transform: translateX(-5px); }
  38% { transform: translateX(5px); }
  60% { transform: translateX(-3px); }
  82% { transform: translateX(2px); }
}
.build-bar .tower-card.afford-refused,
.send-bar .mob-card.afford-refused,
.utp-upgrade-card.afford-refused {
  animation: card-refuse 0.34s ease;
  box-shadow: 0 0 0 2px var(--color-cant-afford, #ff9f43), var(--card-shadow-idle);
}
/* Without the shake there is still an answer: a 1.4s orange outline and the
   chip. The motion is the emphasis, not the message. */
@media (prefers-reduced-motion: reduce) {
  .build-bar .tower-card.afford-refused,
  .send-bar .mob-card.afford-refused,
  .utp-upgrade-card.afford-refused { animation: none; }
}
.afford-eta.need {
  background: #ff7a2f;
  color: #1a0800;
  letter-spacing: 0.3px;
}

/* Ready-to-buy glow: anything you can afford right now says so */
@keyframes card-ready-pulse {
  0%, 100% { box-shadow: 0 0 5px rgba(240, 192, 64, 0.28); }
  50% { box-shadow: 0 0 13px rgba(240, 192, 64, 0.55); }
}
/* Attacker bar only. A mob card is fire-and-forget — there is no selection
   state on it — so a "you can buy this" glow costs nothing there.
   On the build bar it cost everything: it framed every affordable tower in gold
   and outranked `.selected`, so the green selection frame was unreachable and
   seven of seven cards were always wearing a warm frame (job 080). An
   affordable tower now says so by being the one card that is neither framed
   nor drained. */
.mob-card.can-buy {
  border-color: var(--color-gold);
  animation: card-ready-pulse 2.2s ease-in-out infinite;
}

/* Floating gold text. It is absolutely positioned inside the gold pill, and
   with no offsets at all it landed on its static position — i.e. directly on
   top of the number it was reporting, so "+1g" sat across "371" in every
   screenshot (job 075). It belongs just above the pill, centred. */
.floating-gold {
  position: absolute;
  /* Below the pill, not above it: the pill sits within ~20px of the top and
     `.game-hud` is `overflow: hidden`, so anchoring above clipped the float
     away entirely — silence instead of overlap, which is the worse trade. */
  left: 50%;
  top: calc(100% + 4px);
  pointer-events: none;
  font-size: 16px;
  font-weight: 800;
  z-index: var(--z-toast);
  animation: float-gold-up 1s ease-out forwards;
  white-space: nowrap;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.8);
}
/* Drifts DOWN, away from the pill. The conventional float-up would carry it
   straight back over the number it is anchored under — measured overlapping at
   250ms before this was changed. It also carries the -50% centring through
   every keyframe; a bare translateY() drops it and snaps the text sideways. */
@keyframes float-gold-up {
  0%   { opacity: 0; transform: translate(-50%, 0) scale(0.8); }
  20%  { opacity: 1; transform: translate(-50%, 2px) scale(1.15); }
  55%  { opacity: 1; transform: translate(-50%, 8px) scale(1); }
  100% { opacity: 0; transform: translate(-50%, 20px) scale(0.9); }
}
.floating-gold.gain { color: #40f070; }
.floating-gold.spend { color: #e94560; }

/* Upgrade panel (legacy fallback) */
.upgrade-panel {
  position: absolute;
  z-index: var(--z-panel);
  min-width: 180px;
  max-width: 240px;
  padding: 12px;
  background: rgba(30, 20, 64, 0.95);
  border: 2px solid var(--color-defender);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  pointer-events: auto;
}
.upgrade-panel.hidden { display: none; }

/* Kill feed */
.kill-feed {
  position: absolute;
  top: 50px;
  right: 8px;
  z-index: var(--z-hud);
  width: 180px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  pointer-events: none;
}
.kill-feed-item {
  padding: 3px 8px;
  background: rgba(14, 10, 26, 0.7);
  border-radius: var(--radius-sm);
  font-size: 11px;
  color: var(--color-text-dim);
  animation: feed-slide-in 0.3s ease;
}
.kill-feed-item.fade-out {
  animation: feed-slide-out 0.3s ease forwards;
}

/* Countdown overlay */
.countdown-overlay {
  position: absolute;
  inset: 0;
  z-index: var(--z-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.5);
  pointer-events: none;
}
.countdown-overlay.hidden { display: none; }
.countdown-number {
  font-family: var(--font-display);
  font-size: 80px;
  font-weight: bold;
  color: var(--color-secondary);
  text-shadow: 0 0 30px rgba(240, 192, 64, 0.5);
}

/* Role select area (in lobby screen) */
.role-select {
  display: flex;
  gap: 12px;
  margin: 12px 0;
}
.role-card {
  flex: 1;
  padding: 14px 16px;
  border: 2px solid var(--color-border);
  border-radius: var(--radius-lg);
  background: var(--color-bg-panel);
  cursor: pointer;
  text-align: center;
  transition: all var(--transition-fast);
  min-width: 0;
}
.role-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}
.role-card.defender:hover,
.role-card.defender.selected {
  border-color: var(--color-defender);
  box-shadow: var(--shadow-glow-defender);
}
.role-card.attacker:hover,
.role-card.attacker.selected {
  border-color: var(--color-attacker);
  box-shadow: var(--shadow-glow-attacker);
}
.role-card.selected {
  transform: translateY(-2px);
}
.role-card .role-name {
  font-size: 18px;
  font-weight: bold;
  margin-bottom: 4px;
}
.role-card.defender .role-name { color: var(--color-defender); }
.role-card.attacker .role-name { color: var(--color-attacker); }
.role-card .role-desc {
  font-size: 11px;
  color: var(--color-text-dim);
}

/* Ready indicator */
.ready-status {
  display: flex;
  gap: 20px;
  justify-content: center;
  margin-top: 8px;
}
.ready-dot {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
}
.ready-dot .dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 2px solid var(--color-border);
  transition: all var(--transition-fast);
}
.ready-dot.ready .dot {
  background: var(--color-accent);
  border-color: var(--color-accent);
  box-shadow: 0 0 8px rgba(64, 240, 112, 0.5);
}

/* ===== Balance Lab Panel ===== */
.balance-toggle-btn {
  position: fixed;
  bottom: 10px;
  right: 120px;
  z-index: 9999;
  padding: 5px 12px;
  background: rgba(78, 204, 163, 0.15);
  border: 1px solid rgba(78, 204, 163, 0.4);
  border-radius: 12px;
  color: var(--color-defender);
  font-size: 11px;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.2s, border-color 0.2s;
  font-family: var(--font-main);
}
.balance-toggle-btn:hover {
  background: rgba(78, 204, 163, 0.3);
  border-color: var(--color-defender);
}

.balance-panel {
  position: fixed;
  top: 0;
  right: 0;
  width: 340px;
  height: 100vh;
  z-index: 9998;
  background: rgba(14, 10, 26, 0.96);
  border-left: 2px solid rgba(78, 204, 163, 0.25);
  box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5);
  display: flex;
  flex-direction: column;
  font-family: var(--font-main);
  color: var(--color-text);
  pointer-events: auto;
}
.balance-panel.hidden { display: none; }

.balance-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  flex-shrink: 0;
}
.balance-title {
  font-size: 16px;
  font-weight: 800;
  color: var(--color-defender);
}
.balance-close-btn {
  background: rgba(233, 69, 96, 0.15);
  border: 1px solid rgba(233, 69, 96, 0.3);
  color: var(--color-danger);
  width: 28px;
  height: 28px;
  border-radius: 50%;
  cursor: pointer;
  font-size: 12px;
  font-weight: bold;
  display: flex;
  align-items: center;
  justify-content: center;
}
.balance-close-btn:hover {
  background: rgba(233, 69, 96, 0.3);
}

.balance-body {
  flex: 1;
  overflow-y: auto;
  padding: 0 16px 16px;
}

.balance-section {
  margin-top: 14px;
}
.balance-section-title {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  color: var(--color-text-dim);
  margin-bottom: 6px;
  letter-spacing: 0.5px;
}

.balance-hash-display {
  font-family: monospace;
  font-size: 13px;
  color: var(--color-secondary);
  padding: 6px 10px;
  background: rgba(255, 255, 255, 0.04);
  border-radius: 6px;
  margin-bottom: 8px;
}

.balance-btn {
  padding: 6px 14px;
  background: rgba(78, 204, 163, 0.12);
  border: 1px solid rgba(78, 204, 163, 0.3);
  border-radius: 8px;
  color: var(--color-defender);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
  font-family: var(--font-main);
}
.balance-btn:hover {
  background: rgba(78, 204, 163, 0.25);
}
.balance-btn-sm {
  padding: 4px 10px;
  font-size: 10px;
}
.balance-btn.copied {
  background: rgba(64, 240, 112, 0.2);
  border-color: rgba(64, 240, 112, 0.4);
  color: #40f070;
}
.balance-btn-danger {
  background: rgba(233, 69, 96, 0.12);
  border-color: rgba(233, 69, 96, 0.3);
  color: var(--color-danger);
}
.balance-btn-danger:hover {
  background: rgba(233, 69, 96, 0.25);
}

.balance-autolog-label {
  font-size: 12px;
  color: var(--color-text);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 6px;
}
.balance-autolog-label input[type="checkbox"] {
  accent-color: var(--color-defender);
}

/* Snapshot list */
.balance-snap-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.balance-snap-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 8px;
  background: rgba(255, 255, 255, 0.03);
  border-radius: 6px;
  font-size: 12px;
}
.balance-snap-item input[type="radio"] {
  accent-color: var(--color-defender);
  flex-shrink: 0;
}
.balance-snap-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.balance-snap-hash {
  font-family: monospace;
  font-size: 10px;
  color: var(--color-text-dim);
  flex-shrink: 0;
}
.balance-snap-del {
  background: none;
  border: none;
  color: var(--color-danger);
  cursor: pointer;
  font-size: 12px;
  padding: 0 4px;
  opacity: 0.5;
  flex-shrink: 0;
}
.balance-snap-del:hover { opacity: 1; }

/* Log list */
.balance-log-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 400px;
  overflow-y: auto;
}
.balance-log-empty {
  font-size: 11px;
  color: var(--color-text-dim);
  text-align: center;
  padding: 12px;
}

/* Log entries — clickable to expand */
.balance-log-entry {
  padding: 6px 10px;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 8px;
  font-size: 11px;
  transition: border-color 0.15s;
}
.balance-log-entry:hover {
  border-color: rgba(255, 255, 255, 0.12);
}
.balance-log-entry.expanded {
  border-color: rgba(78, 204, 163, 0.3);
  background: rgba(78, 204, 163, 0.04);
}

.balance-log-header {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  user-select: none;
}
.balance-log-gameid {
  font-family: monospace;
  font-size: 11px;
  font-weight: 700;
  color: var(--color-secondary);
}
.balance-log-win-badge {
  font-size: 9px;
  font-weight: 800;
  padding: 1px 5px;
  border-radius: 4px;
  letter-spacing: 0.5px;
}
.balance-log-win-badge.defender {
  background: rgba(78, 204, 163, 0.2);
  color: var(--color-defender);
}
.balance-log-win-badge.attacker {
  background: rgba(233, 69, 96, 0.2);
  color: var(--color-attacker);
}
.balance-log-date {
  font-size: 10px;
  color: var(--color-text-dim);
  flex: 1;
  text-align: right;
}
.balance-log-expand-icon {
  font-size: 10px;
  color: var(--color-text-dim);
  width: 12px;
  text-align: center;
  flex-shrink: 0;
}

.balance-log-summary {
  font-size: 10px;
  color: var(--color-text-dim);
  margin-top: 3px;
  cursor: pointer;
  font-family: monospace;
}

/* Expanded log details */
.balance-log-details {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.balance-log-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 11px;
  margin-bottom: 8px;
}
.balance-log-table td {
  padding: 2px 0;
}
.balance-log-table td:first-child {
  color: var(--color-text-dim);
  width: 120px;
}
.balance-log-table td:last-child {
  font-weight: 600;
  text-align: right;
}
.balance-log-table td.defender { color: var(--color-defender); }
.balance-log-table td.attacker { color: var(--color-attacker); }
.balance-log-table code {
  font-size: 10px;
  background: rgba(255, 255, 255, 0.06);
  padding: 1px 4px;
  border-radius: 3px;
}
.balance-log-table tr.sep td {
  padding: 4px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}

.balance-log-raw {
  font-family: monospace;
  font-size: 9px;
  color: var(--color-text-dim);
  background: rgba(0, 0, 0, 0.3);
  padding: 8px;
  border-radius: 6px;
  margin-top: 6px;
  max-height: 200px;
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-all;
}
.balance-log-raw.hidden { display: none; }

/* Log action buttons */
.balance-log-actions {
  display: flex;
  gap: 6px;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid rgba(255, 255, 255, 0.04);
}

/* Event timeline */
.balance-log-timeline-section {
  margin-top: 8px;
}
.balance-log-timeline {
  max-height: 250px;
  overflow-y: auto;
  font-size: 10px;
  border: 1px solid rgba(255, 255, 255, 0.04);
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.15);
}
.balance-timeline-row {
  display: flex;
  gap: 8px;
  padding: 2px 8px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.02);
}
.balance-timeline-row:last-child { border-bottom: none; }
.balance-timeline-row:hover { background: rgba(255, 255, 255, 0.03); }
.balance-timeline-ts {
  font-family: monospace;
  color: var(--color-text-dim);
  min-width: 36px;
  flex-shrink: 0;
}
.balance-timeline-desc {
  flex: 1;
  color: var(--color-text);
}
.tl-action {
  font-weight: 700;
  font-size: 9px;
  padding: 0 4px;
  border-radius: 3px;
  margin-right: 4px;
  letter-spacing: 0.3px;
}
.tl-def { background: rgba(78, 204, 163, 0.2); color: var(--color-defender); }
.tl-atk { background: rgba(233, 69, 96, 0.2); color: var(--color-attacker); }
.tl-warn { background: rgba(240, 192, 64, 0.2); color: var(--color-secondary); }
.tl-dim { color: var(--color-text-dim); }
.tl-snap {
  font-family: monospace;
  color: var(--color-text-dim);
  font-style: italic;
}

/* Confirm dialog */
/* Themed confirm (job 041) — replaces the native confirm() on match exit.
   Fixed to the viewport and above the HUD, because it is mounted on <body>. */
.game-confirm-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.72);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9000;
  animation: fade-in 0.12s ease;
}
.game-confirm-box {
  background: linear-gradient(180deg, rgba(30, 20, 64, 0.99), rgba(20, 14, 44, 0.99));
  border: 2px solid var(--color-gold, #f0c040);
  border-radius: 14px;
  padding: 26px 28px;
  max-width: 380px;
  text-align: center;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.65);
}
.game-confirm-msg {
  font-size: 16px;
  color: var(--color-text);
  margin-bottom: 20px;
  line-height: 1.5;
}
.game-confirm-btns {
  display: flex;
  gap: 12px;
  justify-content: center;
}
.game-confirm-btns .btn { min-width: 116px; }
.game-confirm-danger {
  background: var(--color-attacker, #e94560);
  border-color: var(--color-attacker, #e94560);
  color: #fff;
}
.game-confirm-btns .btn:focus-visible {
  outline: 3px solid var(--color-gold, #f0c040);
  outline-offset: 2px;
}

.balance-confirm-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
}
.balance-confirm-box {
  background: rgba(20, 14, 44, 0.98);
  border: 2px solid rgba(233, 69, 96, 0.4);
  border-radius: 12px;
  padding: 20px;
  max-width: 280px;
  text-align: center;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
}
.balance-confirm-msg {
  font-size: 13px;
  color: var(--color-text);
  margin-bottom: 16px;
  line-height: 1.4;
}
.balance-confirm-btns {
  display: flex;
  gap: 10px;
  justify-content: center;
}

.balance-footer {
  display: flex;
  gap: 8px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid rgba(255, 255, 255, 0.06);
}

/* --- Ability bar (M2: abilities wired) --- */
/* A solid tray backing (job 026) so the rail reads as a deliberate HUD panel
   sitting in the left field gutter, never tangled with the spawn portal —
   belt-and-braces for narrow windows where the gutter is tightest. */
.ability-bar {
  position: absolute;
  /* The HUD root is pointer-events:none so clicks reach the board; every
     interactive overlay must opt back in. The ability rail never did, so the
     spells were visible but CLICK-THROUGH — only the Z/X/C/V hotkeys worked,
     which is why it survived to a phone, where there are no hotkeys. Dima:
     "the spells, I'm not sure that those are working" (job 051). */
  pointer-events: auto;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 9px 8px;
  border-radius: 18px;
  background: rgba(18, 12, 30, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  z-index: var(--z-hud);
}
.ability-bar.hidden { display: none; }
.ability-btn {
  position: relative;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  border: 2px solid var(--color-border-light);
  background: var(--color-bg-panel);
  color: var(--color-text);
  font-size: 22px;
  cursor: pointer;
  box-shadow: var(--shadow-md);
  transition: transform var(--transition-fast), border-color var(--transition-fast);
}
/* Painted card sprites (Tier 2): sprite when it exists, colored circle when not */
.tower-icon .card-sprite { width: 34px; height: 34px; object-fit: contain; }
.mob-icon .card-sprite { width: 26px; height: 26px; object-fit: contain; }
.tower-icon svg, .mob-icon svg { display: none; }
.tower-icon.no-sprite svg, .mob-icon.no-sprite svg { display: inline; }
.tower-icon.no-sprite .card-sprite, .mob-icon.no-sprite .card-sprite { display: none; }

.ability-icon-img {
  width: 30px;
  height: 30px;
  object-fit: contain;
  image-rendering: -webkit-optimize-contrast;
  pointer-events: none;
}
.ability-btn:hover { transform: scale(1.08); border-color: var(--color-secondary); }
.ability-btn:active { transform: scale(0.94); }
.ability-btn.cooling { filter: grayscale(0.8); cursor: default; }
.ability-key {
  position: absolute;
  bottom: -4px;
  right: -4px;
  font-size: 10px;
  font-weight: bold;
  background: var(--color-bg-dark);
  border: 1px solid var(--color-border);
  border-radius: 4px;
  padding: 1px 4px;
  color: var(--color-text-dim);
}
.ability-cd {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
  font-size: 18px;
  color: var(--color-timer);
  background: rgba(14, 10, 26, 0.55);
  border-radius: 50%;
  opacity: 0;
}

/* --- Spectator rail (job 135): both sides, read-only ------------------------
   A spectator has no side, so the rail carries all eight spells in two labelled
   columns. `pointer-events: none` is deliberate and is the whole reason the
   cells are divs, not buttons: nothing here is castable, so the rail must never
   eat a click meant for the board behind it. That is the opposite of the job 051
   fix above — which gave the PLAYER's rail its clicks back — and it is opposite
   on purpose. */
.ability-bar.spectating {
  pointer-events: none;
  flex-direction: row;
  align-items: flex-start;
  gap: 12px;
}
.ability-side {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
.ability-side-label {
  font-size: 10px;
  font-weight: bold;
  letter-spacing: 0.08em;
  color: var(--color-text-dim);
}
.ability-side-defender .ability-side-label { color: var(--color-defender); }
.ability-side-attacker .ability-side-label { color: var(--color-attacker); }
.ability-bar.spectating .ability-btn {
  width: 40px;
  height: 40px;
  font-size: 18px;
  cursor: default;
  display: flex;
  align-items: center;
  justify-content: center;
}
.ability-bar.spectating .ability-btn:hover {
  transform: none;
  border-color: var(--color-border-light);
}
.ability-bar.spectating .ability-icon-img { width: 24px; height: 24px; }
.ability-bar.spectating .ability-cd { font-size: 15px; }
/* A spell that is currently RUNNING, not cooling — the spectator's only cue
   that an effect is live, since they aren't the one who cast it. */
.ability-bar.spectating .ability-btn.live {
  border-color: var(--color-secondary);
  box-shadow: 0 0 10px rgba(240, 192, 64, 0.7);
}

/* --- Interactive tutorial highlight (M3.4) --- */
.tutorial-highlight {
  animation: tutorial-pulse 1.2s ease-in-out infinite;
  border-radius: var(--radius-md);
}
@keyframes tutorial-pulse {
  0%, 100% { box-shadow: 0 0 0 2px rgba(240, 192, 64, 0.9), 0 0 18px rgba(240, 192, 64, 0.5); }
  50% { box-shadow: 0 0 0 4px rgba(240, 192, 64, 0.5), 0 0 30px rgba(240, 192, 64, 0.8); }
}

/* Free Play build phase — a persistent countdown for the opening.
   The phase used to run silently, so a first-time player discovered it existed
   only when their lives started dropping. Deliberately not a flash banner:
   it has to be readable for the whole 32 seconds. */
/* Job 060: this was a three-line card ~230x150 sitting in the middle of the
   board for the whole build phase — the single biggest thing covering the
   playfield on a phone. It is a coach mark, so it belongs at the edge, on one
   line, and out of the way the moment you pick a tower. */
.build-phase {
  position: absolute;
  top: calc(var(--hud-top-h, 56px) + 6px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 7px 18px 9px;
  border-radius: 14px;
  background: rgba(20, 16, 31, 0.9);
  border: 1px solid var(--color-defender);
  color: var(--color-defender);
  pointer-events: none;
  z-index: 32;
  min-width: 210px;
  max-width: 92vw;
  text-align: center;
  transition: opacity 140ms ease;
}
body.placing .build-phase,
body.placing .moment-banner { opacity: 0.25; }
.build-phase b { font-size: 15px; font-weight: 800; letter-spacing: 0.4px; }
.build-phase span { font-size: 12px; color: var(--color-text-dim); }
.build-phase i {
  display: block;
  width: 100%;
  height: 3px;
  margin-top: 5px;
  border-radius: 2px;
  background: linear-gradient(to right, var(--color-defender) var(--p), rgba(255,255,255,0.12) var(--p));
  transition: background 0.3s linear;
}
.build-phase.urgent { border-color: var(--color-attacker); color: var(--color-attacker); }
.build-phase.urgent i { background: linear-gradient(to right, var(--color-attacker) var(--p), rgba(255,255,255,0.12) var(--p)); }
.build-phase.hidden { display: none; }

/* --- In-match settings (job 053) -------------------------------------------
   A modal over the board, not a screen: the lobby settings screen would hide
   the match. pointer-events is explicit because #ui-root is click-through and
   an overlay that forgets this is visible but dead (the job-051 ability-rail
   bug). */
.game-settings-overlay {
  position: fixed;
  inset: 0;
  z-index: 9500;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(0, 0, 0, 0.72);
  pointer-events: auto;
  animation: fade-in 0.12s ease;
}
.game-settings-overlay.hidden { display: none; }

.game-settings-panel {
  width: min(420px, 100%);
  max-height: min(86vh, 640px);
  overflow-y: auto;
  padding: 16px 18px 18px;
  border-radius: 14px;
  border: 2px solid var(--color-secondary, #f0c040);
  background: linear-gradient(180deg, rgba(30, 20, 64, 0.99), rgba(20, 14, 44, 0.99));
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.65);
}

.gs-head { display: flex; align-items: center; justify-content: space-between; }
.gs-title {
  font-family: Georgia, serif;
  font-size: 20px;
  font-weight: 800;
  color: var(--color-secondary, #f0c040);
}
.gs-close {
  min-width: 44px; min-height: 44px;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  background: rgba(255, 255, 255, 0.06);
  color: var(--color-text);
  font-size: 16px;
  cursor: pointer;
}
.gs-close:hover { border-color: var(--color-secondary); }
.gs-note { margin: 2px 0 12px; font-size: 11px; color: var(--color-text-dim); }

.gs-group { margin-bottom: 14px; }
.gs-group-title {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--color-text-dim);
  margin-bottom: 6px;
}
.gs-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 44px;   /* thumb target */
}
.gs-label { font-size: 14px; color: var(--color-text); }
.gs-slider { display: flex; align-items: center; gap: 8px; flex: 1; max-width: 62%; }
.gs-slider input[type="range"] { flex: 1; min-width: 0; }
.gs-value { font-size: 12px; color: var(--color-text-dim); min-width: 38px; text-align: right; }

.gs-toggle {
  width: 52px; height: 30px;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: rgba(255, 255, 255, 0.08);
  position: relative;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.gs-toggle .gs-knob {
  position: absolute; top: 3px; left: 3px;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--color-text-dim);
  transition: transform 0.15s ease, background 0.15s ease;
}
.gs-toggle.on { background: rgba(78, 204, 163, 0.28); border-color: var(--color-defender); }
.gs-toggle.on .gs-knob { transform: translateX(22px); background: var(--color-defender); }

.gs-select {
  min-height: 40px;
  padding: 6px 10px;
  border-radius: 8px;
  border: 1px solid var(--color-border);
  background: rgba(255, 255, 255, 0.06);
  color: var(--color-text);
  font-size: 14px;
}

.gs-actions { display: flex; gap: 10px; margin: 4px 0 10px; }
.gs-actions .btn { flex: 1; min-height: 44px; }
.gs-forfeit { border-color: rgba(233, 69, 96, 0.6); color: var(--color-attacker, #e94560); }
.gs-resume { width: 100%; min-height: 46px; }

/* Gear beside the leave button */
.hud-settings-btn {
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--color-border);
  color: var(--color-text);
  width: 32px; height: 32px;
  border-radius: 50%;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background 0.2s, border-color 0.2s;
  flex-shrink: 0;
  margin-right: 6px;
}
.hud-settings-btn:hover { border-color: var(--color-secondary); background: rgba(255, 255, 255, 0.12); }
@media (hover: none) and (pointer: coarse) {
  .hud-settings-btn, .hud-leave-btn { width: 40px; height: 40px; }
}


/* --- Abilities: explain themselves, no hotkeys on touch (job 067) --------- */
/* Dima: "The abilities not need hotkeys" and "I can't test abilities idk what
   those are doing." The badges are noise on a phone and the only explanation
   was a `title` tooltip, which touch never shows. */
@media (hover: none) and (pointer: coarse) {
  .ability-btn .ability-key { display: none; }
}

.ability-btn.explaining {
  border-color: var(--color-gold, #f0c040);
  box-shadow: 0 0 0 2px rgba(240, 192, 64, 0.35);
}

.ability-info {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  width: max-content;
  max-width: 88vw;
  padding: 10px 12px;
  border-radius: 12px;
  background: #150e2c;
  border: 1px solid var(--color-gold, #f0c040);
  box-shadow: 0 8px 26px rgba(0, 0, 0, 0.55);
  pointer-events: auto;   /* #ui-root is click-through */
  z-index: 46;
  text-align: left;
}
.ability-info.hidden { display: none; }
.ai-title { font-size: 14px; font-weight: 800; color: var(--color-gold, #f0c040); }
.ai-desc { font-size: 12px; color: var(--color-text); margin-top: 3px; max-width: 260px; }
.ai-meta { font-size: 11px; color: var(--color-text-dim); margin-top: 4px; }
.ai-actions { display: flex; gap: 8px; margin-top: 8px; }
.ai-use, .ai-close {
  flex: 1 1 auto;
  min-height: 40px;
  padding: 6px 14px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 800;
  cursor: pointer;
  white-space: nowrap;
}
.ai-use {
  background: var(--color-defender, #4ecca3);
  border: 1px solid var(--color-defender, #4ecca3);
  color: #0b1a14;
}
.ai-use:disabled { opacity: 0.5; cursor: default; }
.ai-close {
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--color-border);
  color: var(--color-text-dim);
}

/* The aim chip reuses the placement hint's shape but says what to point at. */
.aim-hint {
  border-color: var(--color-gold, #f0c040);
  color: var(--color-gold, #f0c040);
}


/* --- Chat overlays the board, it does not shrink it (job 092) -------------
   Dima, foldable playtest as attacker: "When I'm playing and I press Chat, the
   widget that appears with the 'Type message' section makes the game screen
   smaller. We expected here to cover the part of the game interface like Emote
   does now."

   Job 083 fits the board to the space the HUD leaves, measured live from the
   real rects. `.chat-panel` is bottom-anchored, so an expanded body pushed its
   own header upward and the board followed it down — 449px of board became
   251px at 840x760, 563 -> 415 at 390x844.

   The emote wheel never did that, and the reason is structural rather than a
   z-index: `.emote-wheel` is position:absolute inside `.emote-panel`, so it is
   OUT OF FLOW and the panel's own border box stays the size of its trigger.
   The chat body now works exactly the same way — it floats above a header that
   does not move — and carries `data-board-overlay` so measuredChrome() in
   game-renderer.js reserves only the header. Both halves are needed: the
   marker says "reserve nothing for me", the out-of-flow layout keeps the part
   that IS reserved from moving.

   These rules are `.chat-panel > x` (0,2,0) on purpose: the base chat styles
   live in screens.css, which is linked AFTER this file, so a bare `.chat-body`
   here would lose the tie and silently do nothing. */
.chat-panel > .chat-header {
  /* Its own rounded chip now that the body is detached above it, instead of the
     top half of a two-part box whose bottom half has floated away. */
  border-radius: var(--radius-md);
}
.chat-panel.expanded > .chat-header {
  border-color: var(--color-secondary, #f0c040);
}
.chat-panel > .chat-body {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(100% + 4px);
  /* Opaque: it sits over the playfield now, and a board legible THROUGH a panel
     is what job 048 recorded as reading like a rendering fault. */
  background: #150e2c;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.65);
  /* Guard only — the body's children are fixed-height, so this bites solely on
     a viewport too short to hold it at all. */
  max-height: min(56dvh, 320px);
  overflow: hidden;
}

/* --- Replay timeline: markers, playhead, drag target (job 072) ----------- */
.replay-track { position: relative; }
.replay-marks { position: absolute; inset: 0; pointer-events: none; }
.replay-mark {
  position: absolute;
  top: 50%;
  width: 3px;
  height: 12px;
  margin-left: -1.5px;
  transform: translateY(-50%);
  border-radius: 2px;
  opacity: 0.9;
}
.replay-mark.mark-leak  { background: var(--color-attacker, #e94560); height: 16px; }
.replay-mark.mark-build { background: var(--color-defender, #4ecca3); }
.replay-mark.mark-ability { background: var(--color-secondary, #f0c040); }
.replay-mark.mark-end   { background: #fff; height: 18px; width: 2px; }
.replay-head {
  position: absolute;
  top: 50%;
  width: 3px;
  height: 20px;
  margin-left: -1.5px;
  transform: translateY(-50%);
  border-radius: 2px;
  background: #fff;
  box-shadow: 0 0 6px rgba(255, 255, 255, 0.7);
  pointer-events: none;
}
/* A 4px line is not a scrub target on a phone: give the track real height and
   a comfortable hit area without making it look heavy. */
.replay-track {
  min-height: 24px;
  display: flex;
  align-items: center;
  cursor: pointer;
  touch-action: none;   /* the drag belongs to the scrubber, not the page */
}

/* --- Job 189/192/193: attacker pacing made visible ------------------------ */

/* Attacker income rate (job 192). Lives inside the attacker gold pill so the
   number and the rate that feeds it are one read. Pulses green when it RISES —
   which happens the moment a send lands (SEND_INCOME_RATIO), so the mechanic
   teaches itself. */
.hud-income-rate {
  font-size: 11px;
  font-weight: 700;
  opacity: 0.75;
  white-space: nowrap;
}
.hud-income-rate.rate-up {
  color: #4ecca3;
  opacity: 1;
  animation: rate-up-pulse 0.9s ease;
}
@keyframes rate-up-pulse {
  0% { transform: scale(1.4); }
  100% { transform: scale(1); }
}

/* Shared send-capacity pool (job 189 knob B). Only rendered while the server
   syncs `sendBudget`, i.e. ECONOMY.SEND_BUDGET_MAX > 0 — invisible at the
   committed defaults. Display-only: pointer-events stays OFF on purpose (it
   floats over the board above the send bar; a tap through it must still reach
   the board). Anchored above whichever bottom bar is live via the measured
   --build-h band (job 102), so it tracks the bar's real height on every
   breakpoint instead of guessing one. */
.send-budget {
  position: absolute;
  bottom: calc(var(--build-h, 110px) + 14px);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 3px 12px;
  border-radius: 999px;
  background: rgba(14, 10, 26, 0.88);
  border: 1px solid rgba(233, 69, 96, 0.35);
  z-index: var(--z-hud);
  pointer-events: none;
  font-size: 10px;
  font-weight: 700;
  color: var(--color-text);
  white-space: nowrap;
}
.send-budget.hidden { display: none; }
.send-budget-label {
  font-size: 8px;
  font-weight: 800;
  letter-spacing: 0.6px;
  opacity: 0.65;
}
.send-budget-track {
  width: 110px;
  height: 6px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.12);
  overflow: hidden;
}
.send-budget-fill {
  height: 100%;
  border-radius: 3px;
  background: var(--color-attacker, #e94560);
  transition: width 0.15s linear;
}
.send-budget.low .send-budget-fill { background: #f0c040; }
.send-budget-value { color: var(--color-gold, #f0c040); }
/* Knob D idle-grace pause (job 189 round 3): regen is frozen because the
   attacker went silent. The fill greys out and the label slot becomes the
   teaching line. The class is only applied below max (game-hud.js) — the
   server's flag can be true at a full pool (idle at max, nothing to regen),
   where "paused" would read as a malfunction. Placed AFTER .low on purpose:
   equal specificity, so source order lets the frozen grey beat the amber. */
.send-budget-hint {
  display: none;
  font-size: 8px;
  font-weight: 800;
  letter-spacing: 0.6px;
  color: #f0c040;
}
.send-budget.paused { border-color: rgba(255, 255, 255, 0.25); }
.send-budget.paused .send-budget-label { display: none; }
.send-budget.paused .send-budget-hint {
  display: inline;
  animation: budget-paused-hint 1.6s ease-in-out infinite;
}
.send-budget.paused .send-budget-fill {
  background: #6e7284;
  opacity: 0.7;
}
.send-budget.paused .send-budget-value { color: rgba(220, 220, 232, 0.75); }
@keyframes budget-paused-hint {
  50% { opacity: 0.5; }
}
@media (prefers-reduced-motion: reduce) {
  .send-budget.paused .send-budget-hint { animation: none; }
}
.send-budget.denied {
  border-color: #e94560;
  animation: budget-denied 0.45s ease 2;
}
@keyframes budget-denied {
  50% { box-shadow: 0 0 14px rgba(233, 69, 96, 0.85); }
}

/* Send-rejection toast (job 193). The kill feed is display:none below 641px,
   so this is the surface a phone actually sees when the server says no. Same
   non-interactive rule as the budget bar above it. */
.send-toast {
  position: absolute;
  bottom: calc(var(--build-h, 110px) + 46px);
  left: 50%;
  transform: translateX(-50%);
  padding: 6px 14px;
  border-radius: var(--radius-lg);
  background: rgba(20, 16, 31, 0.95);
  border: 1px solid var(--color-attacker, #e94560);
  color: var(--color-text);
  font-size: 12px;
  font-weight: 700;
  z-index: var(--z-toast);
  pointer-events: none;
  white-space: nowrap;
  max-width: 94vw;
  overflow: hidden;
  text-overflow: ellipsis;
}
.send-toast.hidden { display: none; }
.send-toast.pop { animation: rate-up-pulse 0.25s ease; }

/* A refused send press (job 193): the same shake the affordability refusal
   uses, in the attacker's red — cooldown, lock and server rejections all
   answer the finger instead of eating the press. */
.send-bar .mob-card.send-rejected {
  animation: card-refuse 0.34s ease;
  box-shadow: 0 0 0 2px var(--color-attacker, #e94560), var(--card-shadow-idle);
}
@media (prefers-reduced-motion: reduce) {
  .send-bar .mob-card.send-rejected { animation: none; }
}

/* Spend-gated tier lock (job 189 knob A): the locked card names the number.
   Sits inside .cooldown-overlay, so it shows exactly while the card is locked
   and never fights the recharge sweep (they are mutually exclusive states). */
.mob-card .gate-hint {
  position: absolute;
  left: 2px;
  right: 2px;
  bottom: 3px;
  font-size: 8px;
  font-weight: 800;
  text-align: center;
  color: var(--color-gold, #f0c040);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.95);
}
.mob-card .gate-hint.hidden { display: none; }
