:root {
  --bg: #1b211d;
  --panel: #242b25;
  --panel-2: #2c342d;
  --line: #46523f;
  --sand: #cfc7a8;
  --ink: #e9e4d2;
  --p1: #4fb8a6;
  --p2: #c1443c;
  --amber: #c99a3a;
  --gold: #ffd447;
  --lightblue: #7ec8e3;

  /* Rare-card category colors */
  --cat-attack: #ffd447;
  --cat-melee: #e2483d;
  --cat-movement: #5aa9e6;
  --cat-protection: #e6923c;
  /* Buffs (Aura Farming) — immediate-effect self-buffs, distinct from the
     "arms once, sits idle until triggered" standing auras below. Purple
     reads as "arcane/charged up," distinct from every other category
     color. */
  --cat-buff: #a86ee6;
  /* Standing auras (Dodge/Aura Pain/Faith/Hope/Duplicate) — cast once,
     produce no immediate effect, and sit armed indefinitely until whatever
     they're waiting for actually happens. Split out of Protection/Buff,
     which used to lump these in with genuine one-shot defensive/buff cards.
     Teal echoes the standing-aura ring color drawn on an armed player's
     token (see game.js's drawBoard), so the card and its live board
     indicator read as the same visual language. */
  --cat-aura: #4fd6c4;
}

* {
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: 'IBM Plex Mono', monospace;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* viewport-fit=cover draws edge-to-edge (fullscreen PWA / cutout area) —
     the env() insets keep content clear of the punch-hole camera and any
     overlaid system bar, and collapse to the plain 12px/6px elsewhere. testing 6px */
  padding: max(6px, env(safe-area-inset-top)) max(6px, env(safe-area-inset-right)) max(6px, env(safe-area-inset-bottom)) max(6px, env(safe-area-inset-left));
}

/* Kicked-elsewhere notice (see accountKickCheck in game.js) — fixed at the
   top so it reads over any lobby step or an active match alike, above
   .mute-controls (z-index 200) since it's a more urgent, one-off notice
   than the always-present settings gear. */
.kicked-banner {
  position: fixed;
  top: max(60px, env(safe-area-inset-top));
  left: 50%;
  transform: translateX(-50%);
  z-index: 300;
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: calc(100vw - 24px);
  background: #3a2420;
  border: 1px solid #c1443c;
  color: var(--ink);
  padding: 8px 10px 8px 14px;
  border-radius: 6px;
  font-size: 12px;
  line-height: 1.4;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}

.kicked-banner button {
  background: none;
  border: none;
  color: var(--ink);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 0 2px;
  flex-shrink: 0;
}

.kicked-banner button:hover {
  color: var(--amber);
}

/* In-game the board fills the viewport height (fitBoardHeight in game.js) —
   only the safe-area inset (0 in immersive fullscreen) stays reserved, so
   the frame reaches the very edge of the screen. */
body.in-game {
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

.wrap {
  width: 100%;
  max-width: 500px;
}

.logo {
  width: 260px;
  height: auto;
  margin: 4px 0 8px;
  overflow: visible; /* letters float above/below their own bbox, would otherwise clip */
}

.logo-lg {
  width: 320px;
  margin: 0 0 10px;
}

/* Wordmark's own two-layer look (see the SVG comment in index.html): cls-1
   is the darker "shadow" path sitting a few px below cls-2's "top" path,
   same drop-shadow trick the old 3-bar logo's ::after gradient crossfade
   was doing, just baked into the artwork's own two path layers instead of
   a hover-only overlay. Colors match the old idle bar gradient's overall
   tone (sand/khaki on dark) rather than the artwork's original grey. */
.logo .cls-1 {
  fill: #46523f;
}

.logo .cls-2 {
  fill: #cfc7a8;
}

/* Each letter floats independently — same bob as the old .logo .bar,
   just one <g>/<use> per letter instead of per bar, staggered across all
   8 (l-a-p-a-t-i-i-i) so the wordmark reads as a loose ripple rather than
   everything bobbing in lockstep. transform-box/transform-origin keep the
   float centered on each letter's own footprint (the SVG default origin
   is the whole viewBox, which would swing far-right letters in a wide
   arc instead of a straight bob). */
.logo-letter {
  animation: logo-hover 3.2s ease-in-out infinite;
  transform-box: fill-box;
  transform-origin: center;
  cursor: pointer;
}

.logo-letter:nth-child(1) { animation-delay: 0s; }
.logo-letter:nth-child(2) { animation-delay: .18s; }
.logo-letter:nth-child(3) { animation-delay: .36s; }
.logo-letter:nth-child(4) { animation-delay: .54s; }
.logo-letter:nth-child(5) { animation-delay: .72s; }
.logo-letter:nth-child(6) { animation-delay: .9s; }
.logo-letter:nth-child(7) { animation-delay: 1.08s; }
.logo-letter:nth-child(8) { animation-delay: 1.26s; }

@keyframes logo-hover {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-7px); }
}

@media (prefers-reduced-motion: reduce) {
  .logo-letter {
    animation: none;
  }
}

.subtitle {
  font-size: 10px;
  letter-spacing: .14em;
  color: #8a9280;
  text-transform: uppercase;
  margin-top: 14px;
  text-align: center;
  opacity: 0.2;
  transition: all 0.3s ease;
}

.subtitle:hover {
opacity: 1;
}

.board-frame {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 6px;
  /* padding: 6px; */
  width: 100%;
  position: relative;
  /* .turn-tag's round-start sweep travels well past either edge (up to
     150% of its own width) — without this, that off-screen travel still
     expands the page's actual scrollable width on mobile, even though
     nothing is visibly there. */
  overflow: hidden;
}

#board {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 3px;
  /* none, not manipulation — manipulation still lets the browser treat a
     vertical drag as a page-scroll gesture before our own pointermove
     handlers see it (see the drag-your-token-to-move feature), which read
     as the drag "only working in one direction" on touch. The board never
     needs to scroll/zoom on its own, so there's nothing native to preserve. */
  touch-action: none;
}

/* WebGL tile-backdrop layer (Phase 3, first cut, default ON — see
   tile-layer.js / WEBGL_MIGRATION_PLAN.md). The only overlay here that
   needs to sit BELOW #board rather than above it. #board itself is a plain
   (non-positioned) block box, and non-positioned boxes always paint above
   positioned descendants in the same stacking context regardless of DOM
   order — so position:absolute alone would put this ABOVE #board even
   though it comes first in the markup. z-index:-1 forces it into a
   stacking level behind #board's own (implicit level 0) to actually land
   underneath. */
#tileLayer {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: -1;
}

/* WebGL glow/bloom overlay (prototype — see glow.js) — sits exactly on top
   of #board (.board-frame is the shared position:relative ancestor both
   size against). mix-blend-mode:screen is what makes this layer's own
   additive-blend glow content ADD light onto #board underneath instead of
   covering/replacing it — same visual effect real bloom post-processing
   has, achieved here via CSS compositing between two stacked canvases
   rather than a single-canvas render-to-texture pass. pointer-events:none
   so every existing tap/drag handler on #board keeps working unchanged. */
/* WebGL token-sprite overlay (Phase 1 prototype, flag-gated — see
   sprite-layer.js / WEBGL_MIGRATION_PLAN.md). Normal alpha-blended sprite
   content (opaque tokens, not additive glow), so no mix-blend-mode — it
   just needs to sit visually where the 2D-drawn tokens normally would. */
#spriteLayer {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
/* WebGL particle overlay (Phase 2, see particles.js / WEBGL_MIGRATION_PLAN.md).
   Additive-blend content, same as glowLayer below — mix-blend-mode:screen
   so overlapping particles (dense fire/explosion clusters) brighten the
   board underneath instead of covering it. */
#particleLayer {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  mix-blend-mode: screen;
}
#glowLayer {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  mix-blend-mode: screen;
}
/* Topmost 2D layer — HP pips/action icon, always on top. Normal (not
   additive) content, same as spriteLayer — no mix-blend-mode. */
#topLayer {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.hand-overlay {
  position: absolute;
  left: 14px;
  right: 14px;
  bottom: 14px;
  display: flex;
  align-items: stretch;
  gap: 6px;
  z-index: 8;
  flex-direction:column;
  transform: translateY(0);
  transition: transform 0.392s ease, opacity 0.392s ease;
  opacity: 1;
}

/* Flies out through its own closest edge (the bottom) instead of a hard
   display:none — see .instruction.fly-hidden's identical reasoning. Used
   only for the resolution replay (renderHandOverlay's other hide cases —
   locked, mid-selection, dead, plan full — stay instant via .hidden, since
   there's nothing worth animating when the overlay is already empty). */
.hand-overlay.fly-hidden {
  transform: translateY(140%);
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .hand-overlay {
    transition: none;
  }
}

/* Basic cards and drawn specials sit in separate groups (the basic one
   carries the HP/AP reminder strip above it); flex-grow is set inline to
   each group's card count so every card ends up the same width, and the
   group gap matches the card gap so the row still reads as one hand. */
.hand-overlay .hand-group {
  flex-grow: 1;
  flex-basis: 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.hand-overlay .hand-cards {
  display: flex;
  gap: 6px;
}

/* HP/AP reminder and the separate lives reminder share the same pill
   chrome — .hand-group's own gap (see .hand-overlay .hand-group) keeps
   the two containers apart, since neither sets its own margin. */
.hp-ap-reminder,
.lives-reminder {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  align-items: center;
  background: rgba(31, 37, 31, 0.92);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 3px 6px;
  font-size: 9px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: #9aa192;
}

.hp-ap-reminder .stat,
.lives-reminder .stat {
  display: flex;
  align-items: center;
  gap: 3px;
}

/* The stats strip as its own full-width row under both card rows (see
   renderHandOverlay) — .hand-overlay's own align-items:center would
   otherwise shrink it to just its content's width like any other flex
   child; align-self:stretch overrides that for this one row specifically.
   justify-content:space-between (inherited from .hp-ap-reminder) then
   spreads AP/HP/Lives out across that full width instead of clumping in
   the middle. */
.hp-ap-reminder-full {
  align-self: stretch;
}

/* Same pip language as the board's tokens: green circles for HP, gold for
   AP. Lives uses the player's own seat color instead of a fixed color
   (set inline via style="" in game.js, seatColor(me)) — border/shadow
   still shared here so it doesn't need its own inline styling too. */
.hp-ap-reminder .hp-dot,
.hp-ap-reminder .ap-dot {
  display: block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
}

/* Same heart shape as .roster-dot below — two circles plus a rotated
   square forming the point, layered via box-shadow so a single element
   still works as an inline-colorable "dot" drop-in (background set via
   style="" in game.js, same as before). */
.lives-reminder .lives-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  clip-path: path('M3.5,6.8 C-2,3 0,-1 3.5,1.6 C7,-1 9,3 3.5,6.8 Z');
  border: none;
}

.hp-ap-reminder .hp-dot {
  background: #1eb101;
  border: 1px solid #1e4b00;
  box-shadow: 0 0 3px rgba(43, 255, 0, 0.69);
}

.lives-reminder .lives-dot {
  border: 1px solid rgba(0, 0, 0, 0.45);
}

.hp-ap-reminder .ap-dot {
  background: #ffd447;
  border: 1px solid #a34900;
  box-shadow: 0 0 3px hsla(30, 98%, 59%, 0.7);
}

.hand-overlay .hand-card {
  background: rgba(31, 37, 31, 0.94);
  backdrop-filter: blur(1px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}

/* Dead players' rock toss — a real member of #emoteOverlay's own row now
   (see renderTossRockOverlay in game.js), not a separately-positioned
   overlay, so it shares .emote-btn's exact circular-button chrome instead
   of its own near-duplicate copy of the same rules. Purely cosmetic, once
   per turn; greys out once spent. */
#tossRockBtn.emote-btn:disabled {
  opacity: 0.35;
  cursor: default;
}

#tossRockBtn.emote-btn.aiming {
  border-color: var(--amber);
  box-shadow: 0 0 8px rgba(230, 146, 60, 0.5);
}

/* Tapping your own token pops these up right over/under your body — a
   shortcut to the two most-used cards (and this turn's specials) without
   reaching down to the hand tray. Repositioned every refreshBoard() call
   (see renderBodyQuickCards in game.js) to track the token's own current/
   predicted screen position, since left/top are plain percentages of the
   canvas's own rendered size — the canvas scales responsively, so a
   percentage-based anchor stays correct across every viewport without any
   resize listener. Two independent elements now, each a fixed
   BODY_QUICK_CARDS_OFFSET_PX design-space gap from the token: #bodyQuickCards
   (Meditate/Shoot) sits BELOW, anchoring its own top edge to that point;
   #bodyQuickSpecialsRow sits ABOVE, anchoring its own bottom edge instead —
   hence the different translateY per id below. */
.body-quick-cards {
  position: absolute;
  transform: translate(-50%, 0);
  z-index: 7;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  animation: body-quick-cards-in .34s cubic-bezier(.22,.72,.32,1);
}

.body-quick-cards .hand-cards {
  display: flex;
  gap: 6px;
}

/* Same 60px width as the Meditate/Shoot row below (.body-quick-cards
   .hand-card) — kept as its own rule since the specials row may still want
   its own icon/label sizing independently. */
.body-quick-cards .body-quick-specials-row .hand-card {
  width: 60px;
  min-width: 60px;
  min-height: 44px;
}

.body-quick-cards .body-quick-specials-row .hand-card .icon {
  font-size: 15px;
}

.body-quick-cards .body-quick-specials-row .hand-card .label {
  font-size: 7px;
}

/* A soft fade+settle rather than lock-pop's snappier translate-up — this
   pops up from a tap mid-plan, so it reads better as a gentle reveal than
   a sharp arrival like Lock In's own bottom-of-screen pop. Keeps the same
   translate(-50%,0) base transform .body-quick-cards (the below-token
   #bodyQuickCards) already sits at (rather than reusing lock-pop, which
   bakes in its own translateX(-50%) with no vertical component, wrong for
   this element's own anchor point) and animates opacity + a small scale/
   rise on top of it. #bodyQuickSpecialsRow (above-token) uses the sibling
   -specials-in keyframes below instead, matching ITS OWN translate(-50%,
   -100%) base transform. */
@keyframes body-quick-cards-in {
  from {
    opacity: 0;
    transform: translate(-50%, 0) translateY(6px) scale(0.88);
  }

  to {
    opacity: 1;
    transform: translate(-50%, 0) translateY(0) scale(1);
  }
}

@keyframes body-quick-cards-specials-in {
  from {
    opacity: 0;
    transform: translate(-50%, -100%) translateY(6px) scale(0.88);
  }

  to {
    opacity: 1;
    transform: translate(-50%, -100%) translateY(0) scale(1);
  }
}

#bodyQuickSpecialsRow {
  transform: translate(-50%, -100%);
  animation-name: body-quick-cards-specials-in;
}

@media (prefers-reduced-motion: reduce) {
  .body-quick-cards {
    animation: none;
  }
}

/* Same token-anchored positioning as .body-quick-cards above, styled like a
   compact version of .lock-overlay's own amber button (see renderBodyLockButton
   in game.js) — purely additive, #lockOverlay keeps showing at the bottom too.
   Own entrance keyframes (not the shared body-quick-cards-in) — that one
   bakes in translate(-50%, 0), the BELOW-token anchor #bodyQuickCards uses,
   while this button sits ABOVE the token like #bodyQuickSpecialsRow
   (translate(-50%, -100%)). Reusing the wrong-anchor keyframes made the
   button render at the wrong vertical offset for the whole entrance
   animation, then visibly snap to its real position the instant the
   animation ended — same mismatch #bodyQuickSpecialsRow's own comment
   already called out and fixed for itself, just never applied here too. */
.body-lock-button {
  position: absolute;
  transform: translate(-50%, -100%);
  z-index: 7;
  padding-bottom: 10px; /* clears the token itself before the button starts */
  animation: body-lock-button-in .34s cubic-bezier(.22,.72,.32,1);
}

@keyframes body-lock-button-in {
  from {
    opacity: 0;
    transform: translate(-50%, -100%) translateY(6px) scale(0.88);
  }

  to {
    opacity: 1;
    transform: translate(-50%, -100%) translateY(0) scale(1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .body-lock-button {
    animation: none;
  }
}

.body-lock-button button {
  font-family: 'IBM Plex Mono', monospace;
  background: var(--amber);
  color: #1b211d;
  font-weight: 700;
  border: none;
  border-radius: 3px;
  padding: 8px 14px;
  font-size: 11px;
  letter-spacing: .06em;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
  white-space: nowrap;
}

.body-lock-button button:active {
  transform: scale(0.94);
}

/* Same token-anchored positioning idiom as .body-lock-button/.body-quick-
   cards above, but centered directly ON the token (translate -50%,-50%)
   rather than offset above/below it — this ring sits right over the
   player's own body, pointing at the tap/drag that opens the quick-tap
   popup (or starts a Move drag) during the matching tutorial step. See
   renderTutorialTokenRing in game.js. */
.tutorial-token-ring {
  position: absolute;
  transform: translate(-50%, -50%);
  z-index: 7;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  pointer-events: none;
  animation: tutorial-token-ring-pulse 1.1s ease-in-out infinite;
}

@keyframes tutorial-token-ring-pulse {
  0%, 100% { box-shadow: 0 0 0 2px rgba(255, 212, 71, 0.55), 0 0 10px 2px rgba(255, 212, 71, 0.45); }
  50% { box-shadow: 0 0 0 3px rgba(255, 242, 184, 0.9), 0 0 20px 6px rgba(255, 212, 71, 0.85); }
}

@media (prefers-reduced-motion: reduce) {
  .tutorial-token-ring {
    animation: none;
    box-shadow: 0 0 0 2px rgba(255, 212, 71, 0.7);
  }
}

/* Same shape/positioning as .tutorial-token-ring above, same idea (a
   pulsing "tap here" nudge over the player's own token) but a distinct
   soft white/teal tone rather than gold, so it never reads as the
   Training-only tutorial highlight — this one fires in every game mode
   after 5s of no pointer interaction at all (see renderIdleTokenRing in
   game.js). */
.idle-token-ring {
  position: absolute;
  transform: translate(-50%, -50%);
  z-index: 7;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  pointer-events: none;
  animation: idle-token-ring-pulse 1.4s ease-in-out infinite;
}

@keyframes idle-token-ring-pulse {
  0%, 100% { box-shadow: 0 0 0 2px rgba(214, 240, 235, 0.5), 0 0 10px 2px rgba(120, 200, 190, 0.4); }
  50% { box-shadow: 0 0 0 3px rgba(232, 250, 247, 0.85), 0 0 20px 6px rgba(120, 200, 190, 0.75); }
}

@media (prefers-reduced-motion: reduce) {
  .idle-token-ring {
    animation: none;
    box-shadow: 0 0 0 2px rgba(120, 200, 190, 0.65);
  }
}

.body-quick-cards .hand-card {
  width: 60px;
  min-width: 60px;
  min-height: 48px;
  background: rgba(31, 37, 31, 0.96);
  backdrop-filter: blur(1px);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}

.body-quick-cards .hand-card .icon {
  font-size: 17px;
}

#ascensionGameOverText{
  color: red;
  font-size: 14px;
  text-transform: uppercase;
}

/* Read-only-but-tappable info row for spectators and dead players: the
   turn's shared special cards, sitting directly above the emote row, built
   from real .hand-card elements (see renderTurnCardsOverlay) so a tap shows
   its own tooltip, same information a player's own tray would show for
   the same card — dead players, spectators, and active players all see
   identical read-only content here, since none of it affects gameflow.
   Needs pointer-events re-enabled (the board underneath still wants taps
   everywhere else, but this row itself is interactive). */
.turn-cards-overlay {
  position: absolute;
  left: 14px;
  right: 14px;
  bottom: 15px;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
}

.turn-cards-overlay .turn-cards-title {
  font-size: 9px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #9aa192;
  pointer-events: none;
}

.turn-cards-overlay .turn-cards-row {
  display: flex;
  justify-content: center;
  gap: 6px;
}

.emote-overlay {
  position: absolute;
  left: 14px;
  right: 14px;
  bottom: 150px;
  display: flex;
  justify-content: center;
  gap: 10px;
  z-index: 5;
}

.emote-overlay .emote-btn {
  background: rgba(31, 37, 31, 0.94);
  border: 1px solid var(--line);
  border-radius: 50%;
  width: 46px;
  height: 46px;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}

.emote-overlay .emote-btn:active {
  transform: scale(0.9);
}

.resolve-overlay {
  position: absolute;
  left: 50%;
  bottom: 70px;
  right: auto;
  top: auto;
  transform: translateX(-50%);
  z-index: 8;
  animation: lock-pop .32s cubic-bezier(.16,.84,.44,1);
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Replay transport bar (#replayControls) — same bottom-center slot as
   #resolveOverlay, just a persistent row of buttons instead of a single
   pop-in Showdown button (the two are never shown at once — replay mode
   never enters the live resolve flow). */
.replay-controls {
  position: absolute;
  left: 50%;
  bottom: 70px;
  transform: translateX(-50%);
  z-index: 8;
  display: flex;
  align-items: center;
  gap: 10px;
  background: rgba(20, 24, 20, 0.82);
  border-radius: 8px;
  padding: 8px 14px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
/* Killcam's own copy of .replay-controls (see #killcamControls in
   index.html) — pinned to the top of the board instead of the bottom, so
   it doesn't compete with winOverlayBottom's stats/rematch panel docked
   at the bottom of the screen during the killcam loop. */
.killcam-controls {
  position: absolute;
  left: 6px;
  top: 6px;
  bottom: auto;
  transform: none;
}
.replay-controls button {
  font-family: 'IBM Plex Mono', monospace;
  background: var(--amber);
  color: #1b211d;
  font-weight: 700;
  border: none;
  border-radius: 3px;
  padding: 8px 12px;
  font-size: 14px;
  cursor: pointer;
}
.replay-controls button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.replay-speed-btns {
  display: flex;
  gap: 4px;
}
.replay-speed-btns button {
  font-family: 'IBM Plex Mono', monospace;
  background: transparent;
  color: #e8e4d8;
  font-weight: 700;
  border: 1px solid var(--amber);
  border-radius: 3px;
  padding: 6px 8px;
  font-size: 12px;
  cursor: pointer;
}
.replay-speed-btns button.active {
  background: var(--amber);
  color: #1b211d;
}

.resolve-overlay button {
  font-family: 'IBM Plex Mono', monospace;
  background: var(--amber);
  color: #1b211d;
  font-weight: 700;
  border: none;
  border-radius: 3px;
  padding: 16px 28px;
  font-size: 15px;
  letter-spacing: .08em;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
  white-space: nowrap;
}

.resolve-overlay button {
  background: #c1443c;
  color: #242b25;
}

@keyframes lock-pop {
  from {
    transform: translateX(-50%) translateY(60px);
    opacity: 0;
  }

  to {
    transform: translateX(-50%) translateY(0);
    opacity: 1;
  }
}

/* Shared positioning for .plan-overlay + .reset-overlay (see the wrapper
   in index.html) — one right-edge-anchored column, plan chips above the
   reset buttons. Each child keeps its own show/hide animation (see
   .plan-overlay.fly-hidden/.reset-overlay.fly-hidden below); this wrapper
   only handles their shared position and vertical stacking, so the plan
   row's height doesn't need to be hardcoded against however many reset
   buttons currently show (2 vs 3). */
.bottom-right-stack {
  position: absolute;
  bottom: 165px;
  right: 6px;
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
}

.reset-overlay {
  display: flex;
  flex-direction: column;
  gap: 6px;
  transform: translateX(0);
  transition: transform 0.392s ease, opacity 0.392s ease;
  opacity: 1;
}

/* Flies out through its own closest edge (the right) instead of a hard
   display:none — see .instruction.fly-hidden's identical reasoning. */
.reset-overlay.fly-hidden {
  transform: translateX(140%);
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .reset-overlay {
    transition: none;
  }
}

.reset-overlay button {
  font-family: 'IBM Plex Mono', monospace;
  background: rgba(31, 37, 31, 0.92);
  color: var(--p2);
  font-weight: 700;
  border: 1px solid var(--p2);
  border-radius: 3px;
  padding: 8px 14px;
  font-size: 11px;
  letter-spacing: .05em;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}

.reset-overlay button:disabled {
  opacity: 0.4;
  cursor: default;
}

.reset-overlay #yourUndo {
  color: #9aa192;
  border-color: var(--line);
}

.reset-overlay #yourUnlock {
  color: var(--amber);
  border-color: var(--amber);
}

/* Planned actions as a bare column, stacked directly above the reset
   overlay (see .bottom-right-stack) — the chrome lives on the individual
   steps below, not the group. pointer-events:none because it sits where
   aiming taps land — it's read-only chrome. In-flow now (no position/
   transform of its own) since .bottom-right-stack's flex column already
   places it; align-items:flex-end matches the reset buttons' own right
   edge instead of each chip stretching full width. transform/transition
   here are for the show/hide fly animation only (see .fly-hidden below),
   not positioning. */
.plan-overlay {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 3px;
  pointer-events: none;
  transform: translateY(0);
  transition: transform 0.392s ease, opacity 0.392s ease;
  opacity: 1;
  margin-bottom: 12px; 
}

/* Flies out downward (toward the reset buttons it sits above) instead of
   a hard display:none — see .instruction.fly-hidden's identical
   reasoning. Replaces the old horizontal-row version's implicit "just
   part of the row sliding" — now its own explicit fly state, matching
   .reset-overlay's sibling treatment. */
.plan-overlay.fly-hidden {
  transform: translateY(140%);
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .plan-overlay {
    transition: none;
  }
}

/* Each planned step is an icon+label pill in the same visual language as
   the spectator/dead turn-cards row (.turn-card): dark chip, category-
   colored border/text for rares, neutral for basics. Eases in from above
   when planned (matching the column's own vertical stack direction, see
   .plan-overlay/.bottom-right-stack) — the animation runs once, on
   insertion: buildPlanner appends/removes entries instead of rebuilding
   the list, so existing entries never replay it. */
.plan-overlay div {
  display: flex;
  align-items: center;
  background: rgba(31, 37, 31, 0.94);
  border: 1px solid var(--line);
  color: #9aa192;
  border-radius: 3px;
  padding: 5px 8px;
  font-size: 9px;
  letter-spacing: .05em;
  text-transform: uppercase;
  white-space: nowrap;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
  animation: plan-step-in 0.35s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}

.plan-overlay div .icon {
  margin-right: 4px;
}

.plan-overlay div.cat-attack { border-color: var(--cat-attack); color: var(--cat-attack); }
.plan-overlay div.cat-melee { border-color: var(--cat-melee); color: var(--cat-melee); }
.plan-overlay div.cat-movement { border-color: var(--cat-movement); color: var(--cat-movement); }
.plan-overlay div.cat-protection { border-color: var(--cat-protection); color: var(--cat-protection); }
.plan-overlay div.cat-buff { border-color: var(--cat-buff); color: var(--cat-buff); }
.plan-overlay div.cat-aura { border-color: var(--cat-aura); color: var(--cat-aura); }

@keyframes plan-step-in {
  from { transform: translateY(-24px); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}

/* A chip the resolution replay has reached eases out to the right —
   sliding off toward the board edge as it's consumed, one chip at a time
   in step with the replay reaching that action (see markPlanStepPlayed).
   Fill mode forwards keeps it invisible afterwards; it keeps its layout
   slot so the remaining chips don't jump mid-replay, and buildPlanner
   clears the whole column once the new turn arrives. */
.plan-overlay div.played {
  animation: plan-step-out 0.35s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}

@keyframes plan-step-out {
  from { transform: translateX(0); opacity: 1; }
  to { transform: translateX(48px); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .plan-overlay div {
    animation: none;
  }
  .plan-overlay div.played {
    opacity: 0;
  }
}

/* Ranked's 15s planning countdown — a small persistent pill (NOT .turn-tag's
   huge sweep-animation styling; this stays put and just ticks its number
   down), positioned near the turn indicator but out of the way of the
   board itself. Turns red/pulses once low on time as a last-call cue. */
.ranked-turn-timer {
  position: absolute;
  top: 6px;
  right: 8px;
  z-index: 6;
  background: rgba(31, 37, 31, 0.92);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 3px 8px;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 12px;
  letter-spacing: .05em;
  color: var(--amber);
}
.ranked-turn-timer.ranked-turn-timer-low {
  color: #e2483d;
  border-color: #e2483d;
  animation: rankedTimerPulse 1s ease-in-out infinite;
}
@keyframes rankedTimerPulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

.turn-tag {
  position: absolute;
  top: 30%;
  left: 0;
  right: 0;
  transform: translateX(150%);
  z-index: 6;
  text-align: center;
  font-family: 'Oswald', sans-serif;
  letter-spacing: .1em;
  font-size: 5rem;
  color: white;
  text-transform: uppercase;
  white-space: nowrap;
  background: #413c23; /* jungle's own color — see .turn-tag.theme-jungle below, kept identical so a jungle match looks exactly as it always has */
  padding: 2rem;
  border-top: 1px;
  border-bottom: 1px;
}

/* Background follows the current map theme (see MAP_THEMES/currentMapTheme
   in game.js, MAP_THEME_TAG_CLASSES) — one class applied at a time by
   setTurnTagText, kept in the same dark/muted register as the original
   hardcoded jungle color so white label text + amber numbers (.round-emph)
   both stay legible against any of them, just tinted toward each theme's
   own palette (jungle's own olive-green, snow's cool blue-grey, autumn's
   warm rust, desert's dusty tan). */
.turn-tag.theme-jungle { background: #242b25; }
.turn-tag.theme-snow { background: #242b25; }
.turn-tag.theme-autumn { background: #242b25; }
.turn-tag.theme-desert { background: #242b25; }

/* New round: the tag eases in from off-screen left, holds centered on the
   board, then eases out to off-screen right — one continuous sweep, not a
   there-and-back — with the text swap (see game.js) landing right as it
   passes through center, so "Round 2" is already showing well before the
   exit starts. */
.turn-tag.round-fly {
  animation: turn-tag-round-fly 2600ms ease-in-out;
}
@keyframes turn-tag-round-fly {
  0% {
    transform: translateX(-150%);
    opacity: 0;
  }
  25% {
    transform: translateX(0);
    opacity: 1;
  }
  75% {
    transform: translateX(0);
    opacity: 1;
  }
  100% {
    transform: translateX(150%);
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .turn-tag.round-fly {
    animation: none;
  }
}

.instruction {
  position: absolute;
  right: 6px;
  top:6px;
  transform: translateX(0) translateY(0);
  transition: transform 0.392s ease, opacity 0.392s ease;
  opacity: 1;
  z-index: 5;
  /* background: rgba(31, 37, 31, 0.92);
  border: 1px solid var(--line); */
  border-radius: 4px;
  /* padding: 6px 14px; */
  text-align: center;
  font-size: 11px;
  line-height: 1.4;
  color: #9aa192;
  /* max-width: 65%; */
}

/* Headline (actions-remaining countdown, "Nice plan!", etc.) — much
   bolder/bigger than the box's old single small line, with the small hint
   line (below) demoted underneath it instead. */
.instruction-headline {
  font-size: 40px;
  font-family: 'Oswald', sans-serif;
  font-weight: 800;
  color: #e8e4d8;
  line-height: 1.25;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  text-transform: uppercase;
}

.instruction-hint {
  font-size: 10px;
  color: #9aa192;
  /* background: rgba(31, 37, 31, 0.92);
  border: 1px solid var(--line); */
  margin-top: 2px;
}
/* The leading digit inside the headline (see spinInstructionCount in
   game.js) — spun up/down on every change via instruction-spin-up/-down,
   re-triggered by toggling the class off then back on (a class added twice
   in a row doesn't restart a CSS animation on its own). Direction mirrors
   the actual count change: fewer actions left spins down, more (should
   never really happen mid-turn, but Undo can put a step back) spins up.
   overflow hidden + inline-block clips the roll instead of it spilling
   into neighboring text. */
.instruction-count {
  display: inline-block;
  overflow: hidden;
  color: var(--amber);
}
.instruction-count.spin-down { animation: instruction-spin-down 0.36s cubic-bezier(.2,.8,.3,1); }
.instruction-count.spin-up { animation: instruction-spin-up 0.36s cubic-bezier(.2,.8,.3,1); }
@keyframes instruction-spin-down {
  0%   { transform: translateY(-60%); opacity: 0; }
  60%  { transform: translateY(8%);  opacity: 1; }
  100% { transform: translateY(0);   opacity: 1; }
}
@keyframes instruction-spin-up {
  0%   { transform: translateY(60%); opacity: 0; }
  60%  { transform: translateY(-8%); opacity: 1; }
  100% { transform: translateY(0);   opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .instruction-count.spin-down, .instruction-count.spin-up { animation: none; }
}

/* Flies out through its own closest edge (the top) instead of a hard
   display:none — used during the resolution replay and while Tips is
   toggled off. Stays in the layout (no display:none) so the slide has
   something to animate; pointer-events off so it can't be tapped while
   off-screen mid-transition. */
.instruction.fly-hidden {
  transform: translateX(-50%) translateY(-140%);
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .instruction {
    transition: none;
  }
}

.log-panel {
  width: 100%;
  background: var(--panel);
  border: 1px solid var(--line);
  margin-top: 8px;
  padding: 8px 12px;
  font-size: 11px;
  line-height: 1.55;
  color: #b8b19a;
  height: 285px;
  overflow-y: auto;
  border-radius: 3px;
}

.log-panel .step-tag {
  color: var(--amber);
  font-weight: 600;
}

/* Combat-log modal — the match-long history behind the "Log" button.
   Sits above everything (mute controls included), dimmed + blurred
   backdrop, and a panel that slides up with a small fade. */
.combat-log-modal {
  position: fixed;
  inset: 0;
  background: rgba(10, 13, 9, 0.72);
  backdrop-filter: blur(3px);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  z-index: 80;
  padding: 20px;
}

.combat-log-panel {
  width: 100%;
  max-width: 420px;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
  overflow: hidden;
  animation: combat-log-in 0.18s ease-out;
  margin-top: 45px; 
}

@keyframes combat-log-in {
  from { opacity: 0; transform: translateY(14px) scale(0.98); }
  to   { opacity: 1; transform: none; }
}

.combat-log-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
  border-bottom: 1px solid var(--line);
  font-family: 'Oswald', sans-serif;
  text-transform: uppercase;
  letter-spacing: .12em;
  font-size: 13px;
  color: var(--amber);
}

.combat-log-head button {
  background: none;
  border: none;
  color: #9aa192;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  padding: 0 2px;
}

.combat-log-head button:hover {
  color: var(--amber);
}

.combat-log-body {
  overflow-y: auto;
  padding: 10px 14px;
  font-size: 11px;
  line-height: 1.55;
  color: #b8b19a;
  text-align: left;
}

.combat-log-body .step-tag {
  color: var(--amber);
  font-weight: 600;
}

.wardrobe-modal-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding-top: 16px;
  padding-bottom: 16px;
}

.build-modal-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 14px 10px 16px;
}

.build-modal-counter {
  font-size: 12px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--ink);
}

.build-modal-counter.build-modal-full {
  color: #4caf50;
}

.build-modal-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
  gap: 6px;
  width: 100%;
  max-height: 50vh;
  overflow-y: auto;
}

/* Online Ascension's own win-screen headline (winOverlay, see index.html's
   own comment on #ascensionRunOutcome) — gold for a run won (matches
   --gold's existing use as the Ascension accent color everywhere else,
   e.g. .ascension-rest-btn.selected below), a muted red for a party wipe
   (a real loss, not the neutral grey a draw would use elsewhere). */
.ascension-run-outcome {
  margin: 0 0 0.5rem;
  text-align: center;
  font-size: 1.3rem;
}
.ascension-run-outcome.run-won { color: var(--gold); }
.ascension-run-outcome.run-lost { color: #e05a5a; }

/* Online Co-Op's own normal-level reward body (the "choose a card, and
   rest" prompt + the 3-card row + camp/reflect) — plain block spacing
   otherwise stacked its children edge-to-edge, reading dense next to every
   other flex-gapped row in this overlay. */
#coopRewardNormalBody {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* Ascension's own reward-card row (renderAscensionRewardStep) — always
   exactly 3 cards, so a flex row that stretches them evenly full-width
   reads better than .build-modal-grid's auto-fill columns (which left-pack
   a short, fixed-length list instead of spreading it out). */
.ascension-reward-cards {
  display: flex;
  gap: 6px;
  width: 100%;
}
.ascension-reward-cards .hand-card {
  flex: 1;
}

/* Reuses .hand-card's own look (icon + label) so a picked card here reads
   as the exact same thing the in-match hand overlay shows — just toggled
   by click/tap instead of drawn. Shared by the Build modal's own grid and
   Ascension's reward-card row (renderAscensionRewardStep) — both are a
   "tap to pick" card list needing the same green highlight. */
.build-modal-grid .hand-card.selected,
.ascension-reward-cards .hand-card.selected {
  border-color: #4caf50;
  background: #1f2f21;
  box-shadow: 0 0 0 1px #4caf50 inset;
}

/* Camp/Reflect (renderAscensionRewardStep's rest-option row) — side by
   side now instead of stacked, each an equal-width button so the choice
   reads as one deliberate either/or rather than two minor stacked links. */
.ascension-rest-options {
  display: flex;
  gap: 6px;
  width: 100%;
  margin-top: 0.5rem;
}

/* Own button treatment (not .link-btn, which Read/Notes/the tutorial skip
   link all share as a bare text link elsewhere) — name emphasized on its
   own line, effect text smaller underneath. Matches .hand-card's own
   dimensions/border/radius/shadow exactly now (min-height, 4px radius,
   centered content, the same drop shadow the hand tray's own cards use),
   not just a loosely-similar approximation, so Camp/Reflect read as
   literal hand-cards with a two-line label instead of a separate button
   style that merely echoes the look. */
.ascension-rest-btn {
  flex: 1;
  min-height: 50px;
  min-width: 64px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 4px 4px;
  background: rgba(31, 37, 31, 0.94);
  backdrop-filter: blur(1px);
  border: 1px solid var(--line);
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
  cursor: pointer;
  font-family: 'IBM Plex Mono', monospace;
}

.ascension-rest-btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

.ascension-rest-btn .icon {
  font-size: 19px;
  line-height: 1;
  color: var(--amber);
}

.ascension-rest-btn.selected {
  border-color: #4caf50;
  background: #1f2f21;
  box-shadow: 0 0 0 1px #4caf50 inset;
}

.ascension-rest-btn-name {
  font-size: 8px;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--amber);
}

.ascension-rest-btn.selected .ascension-rest-btn-name,
.ascension-rest-btn.selected .icon {
  color: #4caf50;
}

.ascension-rest-btn-desc {
  max-width: 100%;
  overflow-wrap: break-word;
  text-align: center;
  line-height: 1.25;
  font-size: 8px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--ink);
}

/* Ascension map screen's player-vs-next-enemy row (renderAscensionMapStep/
   renderAscensionEnemyPreview) — two equal columns, each its own preview
   canvas + stat strip stacked underneath, side by side. */
.ascension-map-vs-row {
  display: flex;
  gap: 16px;
  width: 100%;
  justify-content: center;
  align-items: flex-start;
}

.ascension-map-vs-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  flex: 1;
  min-width: 0;
}

/* Ascension map screen's "Build" line (renderAscensionMapStep) — the run's
   unlocked cards as small icon+label pills instead of a comma-joined name
   list, same glanceable-card idea as a real hand-card just shrunk down. */
.ascension-build-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  margin-top: 4px;
  font-size: 11px;
}

.ascension-build-pill {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 2px 6px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: #1f251f;
  font-size: 10px;
  color: var(--sand);
}

.ascension-build-pill .icon {
  font-size: 11px;
  color: var(--gold);
}

.ascension-build-empty {
  font-size: 10px;
  color: #6f766a;
  font-style: italic;
}

/* Custom game settings' ban picker (renderCustomBanGrid in game.js) reuses
   this same grid/card look but needs the OPPOSITE color signal from a
   Build-modal pick: red for "excluded from this match," not green for
   "included in my deck" — .selected's green would read as the card being
   chosen/favored, which is backwards for a ban list. Also strikes the icon
   through so a banned card is unambiguous even without color (colorblind-
   safe, and reads clearly in any screenshot/GIF). */
.build-modal-grid .hand-card.banned {
  border-color: #c1443c;
  background: #331d1b;
  box-shadow: 0 0 0 1px #c1443c inset;
}
.build-modal-grid .hand-card.banned .icon {
  position: relative;
  opacity: 0.6;
}
.build-modal-grid .hand-card.banned .icon::after {
  content: '';
  position: absolute;
  left: -10%;
  right: -10%;
  top: 50%;
  height: 2px;
  background: #e2483d;
  transform: rotate(-18deg);
}

.build-modal-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 4px;
}

.build-modal-error {
  font-size: 11px;
  color: #e05a4e;
}

.wardrobe-preview-canvas {
  width: 160px;
  height: 160px;
  border: 1px solid var(--line);
  border-radius: 4px;
}

.combat-log-empty {
  color: #6f766a;
  text-align: center;
  padding: 18px 0;
}

.chat-box {
  width: 100%;
  background: var(--panel);
  border: 1px solid var(--line);
  margin-top: 8px;
  border-radius: 3px;
  display: flex;
  flex-direction: column;
  text-align: left; /* .overlay sets text-align:center — don't let that leak in */
}

/* #chatBox lives permanently inside the chat modal now (see its own comment
   in index.html) and sits inside .combat-log-body's own padding/border
   already — its own border/margin would double up into a frame-within-a-
   frame, so strip those back to flush/borderless here. */
.chat-modal-body {
  padding: 0;
}

.friends-modal-body {
  padding: 10px 14px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.friends-search-row {
  margin-bottom: 4px;
}

.friends-search-input {
  width: 100%;
  box-sizing: border-box;
  background: #2c342d;
  border: 1px solid var(--line);
  border-radius: 3px;
  color: var(--ink);
  padding: 6px 8px;
  font-size: 12px;
}

.friends-list {
  display: flex;
  flex-direction: column;
}

.friends-list:not(:empty) {
  margin-bottom: 10px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
}

.friends-list-label {
  font-size: 9px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #6f766a;
  margin-bottom: 4px;
}

.friends-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 0;
  font-size: 12px;
}

.friends-row-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink);
}

.friends-status-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex-shrink: 0;
  background: #5a6055; /* offline default */
}

.friends-status-dot.online { background: #5cb85c; box-shadow: 0 0 3px rgba(92, 184, 92, 0.7); }
.friends-status-dot.in_game { background: var(--amber); box-shadow: 0 0 3px hsla(30, 98%, 59%, 0.7); }
.friends-status-dot.in_lobby { background: #4aa8ff; box-shadow: 0 0 3px rgba(74, 168, 255, 0.7); }

.friends-row-status {
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: #9aa192;
  flex-shrink: 0;
}

.friends-row-lastseen {
  text-transform: none;
  letter-spacing: normal;
  opacity: .8;
}

.friends-row button {
  background: none;
  border: 1px solid var(--line);
  border-radius: 3px;
  color: #9aa192;
  font-size: 10px;
  padding: 3px 7px;
  cursor: pointer;
  flex-shrink: 0;
}

.friends-row button:hover {
  border-color: var(--amber);
  color: var(--amber);
}

.friends-row button.friends-accept-btn {
  border-color: #5cb85c;
  color: #5cb85c;
}

.friends-row button.friends-remove-btn:hover {
  border-color: #c0392b;
  color: #c0392b;
}

.friends-row button.friends-disabled-btn {
  cursor: default;
  opacity: 0.6;
}

.friends-row button.friends-disabled-btn:hover {
  border-color: var(--line);
  color: #9aa192;
}

.friends-empty {
  color: #6f766a;
  text-align: center;
  padding: 10px 0;
  font-size: 11px;
}

/* Small red dot on the Friends button, same positioning as chatToggleBtn's
   own unread pip (.mute-btn is already position:relative) — flags an
   incoming request without needing the modal open. Red rather than
   chat-unread-pip's green so the two read as distinct kinds of "new". */
.friends-unread-pip {
  position: absolute;
  top: -3px;
  right: -3px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #c0392b;
  border: 1px solid #5e1810;
}

.chat-modal-body .chat-box {
  border: none;
  margin-top: 0;
  border-radius: 0;
}

.chat-modal-body .chat-messages {
  height: 220px;
  padding: 10px 14px;
}

.chat-modal-body .chat-input-row {
  padding: 10px 14px;
}

.chat-messages {
  height: 110px;
  overflow-y: auto;
  padding: 8px 12px;
  font-size: 11px;
  line-height: 1.55;
  color: #b8b19a;
}

.chat-messages .chat-msg .chat-ts {
  color: #6f6a5a;
  font-size: 9px;
  margin-right: 4px;
}

.chat-messages .chat-msg .chat-name {
  color: var(--amber);
  font-weight: 600;
}

.chat-messages .chat-msg.chat-mine .chat-name {
  color: var(--p1);
}

.chat-input-row {
  display: flex;
  gap: 6px;
  padding: 8px;
  border-top: 1px solid var(--line);
}

.chat-input-row input {
  flex: 1;
  min-width: 0;
  background: #1f251f;
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 8px 10px;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 12px;
  color: var(--ink);
}

.chat-input-row input::placeholder {
  color: #5a6055;
}

.chat-input-row button {
  font-family: 'IBM Plex Mono', monospace;
  background: var(--amber);
  color: #1b211d;
  font-weight: 700;
  border: none;
  border-radius: 3px;
  padding: 8px 14px;
  font-size: 11px;
  letter-spacing: .05em;
  text-transform: uppercase;
  cursor: pointer;
}

.hand-row {
  display: flex;
  gap: 8px;
  margin-bottom: 8px;
}

.hand-card {
  flex: 1;
  min-width: 0; /* without this, flex items refuse to shrink below their
    label's unwrapped width, and a row of 6 cards pushes past the canvas
    edge on narrow screens instead of actually shrinking to fit */
  min-height: 50px;
  min-width: 64px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: #1f251f;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  cursor: pointer;
  padding: 4px 4px;
  font-size: 8px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--ink);
}

.hand-card .icon {
  font-size: 19px;
  line-height: 1;
}

/* On the narrowest phones, a row of 6 cards leaves very little width each —
   shrinking the label and letting go of the letter-spacing buys enough
   room that most labels wrap at a word boundary instead of splitting
   mid-word ("SHOOT" -> "SHOO"/"T"). */
@media (max-width: 380px) {
  .hand-card {
    font-size: 8px;
    letter-spacing: 0;
    padding: 6px 2px;
  }

  .hand-card .icon {
    font-size: 16px;
  }
}

.hand-card .label {
  max-width: 100%;
  overflow-wrap: break-word;
  text-align: center;
  line-height: 1.25;
}

/* Meditate's +2 AP payoff, drawn as the same gold pips the board uses
   for action points (see drawPips in game.js). */
.hand-card .ap-gain {
  display: flex;
  align-items: center;
  gap: 3px;
  color: var(--gold);
  font-size: 9px;
  line-height: 1;
}

.hand-card .ap-pip {
  display: block;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #ffd447;
  border: 1px solid #a34900;
  box-shadow: 0 0 3px hsla(30, 98%, 59%, 0.7);
}

/* Every special (rare) card costs 1 AP — a single pip pinned to the top
   corner shows the price. Needs the card to anchor it. */
.hand-card {
  position: relative;
}

.hand-card .ap-cost {
  position: absolute;
  top: 4px;
  right: 4px;
}

.hand-card.rare {
  border: 2px solid var(--gold);
  box-shadow: 0 0 6px rgba(255, 212, 71, 0.35);
  color: var(--gold);
}

.hand-card.rare .icon {
  color: var(--gold);
}

/* Category color overrides — attack keeps the default gold above */
.hand-card.rare.cat-melee {
  border-color: var(--cat-melee);
  box-shadow: 0 0 6px rgba(226, 72, 61, 0.35);
  color: var(--cat-melee);
}
.hand-card.rare.cat-melee .icon { color: var(--cat-melee); }

.hand-card.rare.cat-movement {
  border-color: var(--cat-movement);
  box-shadow: 0 0 6px rgba(90, 169, 230, 0.35);
  color: var(--cat-movement);
}
.hand-card.rare.cat-movement .icon { color: var(--cat-movement); }

.hand-card.rare.cat-protection {
  border-color: var(--cat-protection);
  box-shadow: 0 0 6px rgba(230, 146, 60, 0.35);
  color: var(--cat-protection);
}
.hand-card.rare.cat-protection .icon { color: var(--cat-protection); }

.hand-card.rare.cat-buff {
  border-color: var(--cat-buff);
  box-shadow: 0 0 6px rgba(168, 110, 230, 0.35);
  color: var(--cat-buff);
}
.hand-card.rare.cat-buff .icon { color: var(--cat-buff); }

.hand-card.rare.cat-aura {
  border-color: var(--cat-aura);
  box-shadow: 0 0 6px rgba(79, 214, 196, 0.35);
  color: var(--cat-aura);
}
.hand-card.rare.cat-aura .icon { color: var(--cat-aura); }

/* .selected needs to win over EVERY category color above (same 3-class
   specificity as .hand-card.rare.cat-X, so whichever came later in the
   file used to win by source order alone — a picked Melee/Movement/
   Protection card in Ascension's reward screen kept its red/blue/orange
   border instead of ever showing picked, since .ascension-reward-cards
   .hand-card.selected's green lost that tie). Repeating .selected on each
   cat-X selector below (4 classes) beats the 3-class rules above
   unambiguously, and the highlight itself is stronger than a plain border
   swap — a thicker ring plus a wider, brighter glow — so "picked" reads
   clearly even against an already-saturated category color. */
.hand-card.rare.cat-melee.selected,
.hand-card.rare.cat-movement.selected,
.hand-card.rare.cat-protection.selected,
.hand-card.rare.cat-buff.selected,
.hand-card.rare.cat-aura.selected {
  border-color: #4caf50;
  box-shadow: 0 0 0 2px #4caf50, 0 0 10px rgba(76, 175, 80, 0.6);
}

.hand-card.pending {
  background: var(--amber);
  color: #1b211d;
  border-color: var(--amber);
  font-weight: 700;
}

.hand-card.pending.rare {
  color: #1b211d;
}

.hand-card.pending.rare .icon {
  color: #1b211d;
}

.hand-card.empty {
  visibility: hidden;
}

.hand-card.disabled {
  opacity: 0.35;
  cursor: default;
  border-color: var(--line);
  box-shadow: none;
  filter: grayscale(60%);
}

/* The one card a tutorial step wants tapped next — a pulsing gold ring so
   it reads as "tap this" without needing an arrow/pointer graphic. Applied
   on top of whatever rare/category classes the card already has; disabled
   cards never get this (a taught card is always the one clickable one). */
.hand-card.tutorial-target {
  animation: tutorial-card-pulse 1.1s ease-in-out infinite;
}

@keyframes tutorial-card-pulse {
  0%, 100% { box-shadow: 0 0 6px 1px rgba(255, 212, 71, 0.55); border-color: var(--gold); }
  50% { box-shadow: 0 0 14px 4px rgba(255, 212, 71, 0.95); border-color: #fff2b8; }
}

/* Step-by-step Training caption — a centered banner above the board
   (rather than sharing #instruction's top-right corner, which still shows
   its own generic per-step text) so the lesson text reads as the primary
   thing to look at during onboarding. */
/* Centered on the canvas itself (board-frame's only other child besides
   the 6px padding, close enough to call this "on the canvas") rather than
   pinned to the top corner like #instruction — the lesson text is the
   main thing to read each step, so it sits where the eye already is.
   pointer-events:none on the shell (see #tutorialSkip below) since it now
   visually overlaps the board tiles the player is being told to tap;
   without this the caption would block the very tap it's teaching. */
.tutorial-overlay {
  position: absolute;
  left: 50%;
  top: 15%;
  transform: translate(-50%, -50%);
  transition: transform 0.392s ease, opacity 0.392s ease;
  opacity: 1;
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  min-width: 92%;
  pointer-events: none;
}

.tutorial-overlay.hidden {
  display: none;
}

/* Flies out upward off the top edge, same convention as
   .instruction.fly-hidden — used during the resolution replay so it
   doesn't sit over the combat animation between the tutorial's two
   teaching turns. Travels from its own top:24% anchor, so -180% (rather
   than a dead-center start's -220%) is enough to clear the canvas at the
   same visual speed. */
.tutorial-overlay.fly-hidden {
  transform: translate(-50%, -180%);
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  .tutorial-overlay {
    transition: none;
  }
}

.tutorial-caption {
  background: rgba(31, 37, 31, 0.95);
  border: 1px solid var(--gold);
  box-shadow: 0 0 8px rgba(255, 212, 71, 0.35);
  border-radius: 4px;
  padding: 8px 16px;
  text-align: center;
  font-size: 12px;
  line-height: 1.45;
  color: var(--ink);
  /* Content-width, capped so long captions wrap into a box rather than
     stretching edge-to-edge — was 280px, widened 50px per request. */
  max-width: 330px;
}

/* Discard instructions + Exit tutorial side by side, instead of each
   stacking as its own full row in .tutorial-overlay's own column layout —
   both are dismiss-style actions for the overlay itself, so they read as a
   pair rather than two separate steps. */
.tutorial-overlay-actions {
  display: flex;
  flex-direction: row;
  gap: 8px;
}

#tutorialSkip {
  background: none;
  border: 1px solid var(--line);
  border-radius: 4px;
  color: #9aa192;
  font-size: 9px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 3px 10px;
  cursor: pointer;
  pointer-events: auto; /* the shell disables clicks (see .tutorial-overlay) so board taps pass through; this button opts back in */
}

#tutorialSkip:hover {
  color: var(--ink);
  border-color: var(--ink);
}

/* Same shell as #tutorialSkip (same pointer-events opt-in reasoning — the
   .tutorial-overlay shell disables clicks so board taps pass through) but
   red-tinted, since this one actually leaves the match (forfeit + return
   to the lobby) rather than just dismissing the lesson overlay. */
.tutorial-exit-btn {
  background: none;
  border: 1px solid #6b2a26;
  border-radius: 4px;
  color: #e0645a;
  font-size: 9px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 3px 10px;
  cursor: pointer;
  pointer-events: auto;
}

.tutorial-exit-btn:hover {
  color: #ff8478;
  border-color: #e0645a;
}

/* Lives inside .hand-overlay now (re-parented by renderHandOverlay, right
   above the action-tooltip), not floating separately over the board —
   a normal flex-column row like every other .hand-overlay child, rather
   than the absolutely-positioned standalone popup it used to be. */
.cancel-row {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  width: 100%;
}

.cancel-buttons {
  display: flex;
  align-items: center;
  gap: 6px;
  padding:12px;
  border-radius: 6px;
  background: #202521;
  border:1px solid var(--line);
}

.action-tooltip {
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: rgba(31, 37, 31, 0.92);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 6px 14px;
  text-align: left;
  font-size: 11px;
  line-height: 1.4;
  color: #9aa192;
  cursor: pointer; /* tap anywhere on it to dismiss — see game.js's click handler */
  transform: translateY(0);
  transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.28s ease;
  opacity: 1;
  /* Always an in-flow row inside its container (the hand tray or the
     spectator/dead-player turn-cards row, see renderActionTooltip/
     renderHandOverlay/renderTurnCardsOverlay in game.js) — never
     absolutely positioned anymore, since the old .board-frame "home"
     position only existed to support the live-aiming case, which is gone.
     Same width as the HP/AP/Lives reminder strip below it
     (.hp-ap-reminder-full) — stretches to fill the container's own inset
     width, NOT a full-bleed breakout to .board-frame's edges (no negative
     margins here, unlike an earlier version of this rule). align-self:
     stretch overrides the container's own align-items:center, which would
     otherwise shrink this to the text's own width like any other flex
     child. */
  align-self: stretch;
}

/* .tooltip-hidden, not .hidden — .hidden is display:none !important
   (unanimatable, and wins mid-transition), which is why the tooltip used
   to just snap in/out with no motion. This eases upward on the way in
   (see the plain .action-tooltip state above, which is what it eases TO)
   and back down on the way out, mirroring .plan-overlay's own vertical
   fly language. Keeps its layout slot (no display:none) so the row above
   it doesn't jump — same reasoning as .fly-hidden elsewhere in this file.
   pointer-events:none while hidden so a leftover empty strip near the top
   of the tray can't eat taps meant for the cards below it during the ease-
   out. */
.action-tooltip.tooltip-hidden {
  transform: translateY(18px);
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .action-tooltip {
    transition: none;
  }
}

/* #cardStatsRow lives inside the tooltip now (above the text) so the stat
   pills travel with it to every home — starts hidden via its own .hidden
   class (see renderCardStatsRow in game.js), same as before. */
.action-tooltip .card-stats-row {
  margin: 0;
}

.card-stats-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: left;
}

.card-stats-row .stat-pill {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  background: rgba(31, 37, 31, 0.92);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 3px 8px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .03em;
}

.card-stats-row .stat-pill.stat-pill-dmg {
  color: #e6c94a;
  border-color: #6b5f2a;
}

.card-stats-row .stat-pill.stat-pill-heal {
  color: #8ec98a;
  border-color: #3a5a34;
}

.card-stats-row .stat-pill.stat-pill-range {
  color: #d8dbd2;
  border-color: var(--line);
}

.card-stats-row .stat-pill.stat-pill-self {
  color: #e0645a;
  border-color: #6b2a26;
}

.card-stats-row .stat-pill.stat-pill-push {
  color: var(--cat-movement);
  border-color: #2c5578;
}

.card-stats-row .stat-pill.stat-pill-ap {
  color: var(--amber);
  border-color: #6b5326;
}

.card-stats-row .stat-pill.stat-pill-reflect {
  color: var(--cat-protection);
  border-color: #7a4f1f;
}

/* Plain descriptive UI tags (lobbed/projectile/aura — see CARD_STATS' own
   comment) rather than a stat with its own dedicated meaning like dmg/heal —
   one shared muted style instead of a per-tag color. */
.card-stats-row .stat-pill.stat-pill-tag {
  color: #9aa192;
  border-color: var(--line);
  text-transform: lowercase;
}

/* Leads the row, category-tinted the same way .hand-card.rare.cat-X is —
   this is the card's own name, not one of its stat numbers, so it borrows
   the card's own color language instead of the neutral stat-pill palette
   the dmg/heal/range/etc. pills above use. */
.card-stats-row .stat-pill.stat-pill-name.cat-attack { color: var(--cat-attack); border-color: var(--cat-attack); }
.card-stats-row .stat-pill.stat-pill-name.cat-melee { color: var(--cat-melee); border-color: var(--cat-melee); }
.card-stats-row .stat-pill.stat-pill-name.cat-movement { color: var(--cat-movement); border-color: var(--cat-movement); }
.card-stats-row .stat-pill.stat-pill-name.cat-protection { color: var(--cat-protection); border-color: var(--cat-protection); }
.card-stats-row .stat-pill.stat-pill-name.cat-buff { color: var(--cat-buff); border-color: var(--cat-buff); }
.card-stats-row .stat-pill.stat-pill-name.cat-aura { color: var(--cat-aura); border-color: var(--cat-aura); }

/* Mini read-only copy of the actual selected hand-card, sitting inline in
   the cancel-buttons row — reuses .hand-card's own border/background/icon/
   rare+category coloring (see the JS that builds this element's markup)
   instead of a separate pill style, just shrunk to fit next to Cancel/
   Confirm and stripped of the pointer cursor since it isn't clickable. */
.selected-card-tag {
  flex: none;
  min-width: 0;
  min-height: 0;
  width: 65px;
  height: 55px;
  padding: 3px 2px;
  font-size: 7px;
  cursor: default;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}

.selected-card-tag .icon {
  font-size: 14px;
}

.cancel-row button {
  background: rgba(31, 37, 31, 0.94);
  color: #9aa192;
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 6px 14px;
  font-size: 10px;
  letter-spacing: .05em;
  text-transform: uppercase;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
  white-space: nowrap;
}

.cancel-row #yourConfirmAim {
  color: #5cb85c;
  border-color: #5cb85c;
}

.cancel-row #yourCancel {
  color: #d9822b;
  border-color: #d9822b;
}

.lock-row {
  display: flex;
  justify-content: flex-end;
  margin-top: 6px;
}

.lock-row button {
  font-family: 'IBM Plex Mono', monospace;
  background: var(--amber);
  color: #1b211d;
  font-weight: 700;
  border: none;
  border-radius: 3px;
  padding: 10px 20px;
  font-size: 12px;
  letter-spacing: .05em;
  cursor: pointer;
}

.lock-row button:disabled {
  background: #3a3a30;
  color: #777;
}

.overlay {
  position: fixed;
  inset: 0;
  background: #12160f;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 50;
  text-align: center;
  padding: 20px;
}

/* Win screen only — no background of its own, no top banner at all (see
   startPostGameBoardLoop in game.js, which loops the actual final turn's
   replay on the real board underneath, visible everywhere above the
   bottom scoreboard). .win-overlay-bottom is the only panel with any
   fill — a flat solid color clamped tight to its own content, not a
   gradient bleeding upward, so there's no grey wash creeping over the
   replay above it. */
.win-overlay-split {
  background: transparent;
  justify-content: flex-end;
  padding: 0;
  /* .overlay is position:fixed;inset:0 (full viewport, z-index:50) — this
     variant's own transparent top region (everything except
     winOverlayBottom below) was silently intercepting every click over the
     whole screen, including #killcamControls sitting in .board-frame
     underneath (a DOM sibling, but stacked below z-index:50 regardless of
     nesting) — reported as "can't click the speed controls during killcam."
     none here, re-enabled on winOverlayBottom itself so ITS OWN buttons
     (Rematch/Leave/etc.) keep working. */
  pointer-events: none;
}
.win-overlay-bottom {
  width: 100%;
  max-width: 500px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 14px 20px 20px;
  background: rgba(18, 22, 15, 0.92);
  pointer-events: auto;
}

/* Background art behind every lobby step except stepName (see
   showLobbyStep in game.js, which toggles this class off whenever stepId
   is 'stepName') — layered on top of .overlay's own flat #12160f so a slow
   image load never leaves a blank flash, and dimmed/darkened so the step
   panels' own text stays readable over it. #lobbyOverlay only (not
   #winOverlay, .overlay's other user) — scoped by id, not the shared
   class, so the win screen never picks this up. Starts at the bottom
   position (see .lobby-backdrop-0 below) the instant the backdrop first
   shows — showLobbyStep applies that class in the same synchronous pass
   as .lobby-overlay-backdrop itself, so there's no unstyled/mid-transition
   frame where the position is still whatever CSS's own initial 0% 0% is. */
#lobbyOverlay.lobby-overlay-backdrop {
  background-image: linear-gradient(rgba(18, 22, 15, 0.72), rgba(18, 22, 15, 0.72)), url('images/lobby-background.png');
  /* NOT background-size:cover — the source art's own aspect ratio happens
     to be very close to a typical phone viewport's, so cover leaves almost
     zero vertical overflow to pan through (the pan looked like it "wasn't
     moving" on real devices, even though the position was genuinely
     changing). auto 150% instead forces the image to always render 1.5x
     the viewport's own height, guaranteeing real vertical travel room for
     background-position regardless of the source image's ratio or the
     device's screen ratio. */
  background-size: auto 150%;
  transition: background-position 0.6s ease;
}

/* Three fixed vertical stops (see lobbyBackdropStop in game.js, a 0-2
   index into these three classes) instead of the old two-position
   alternation — forward navigation (a regular "next step" click) steps
   toward .lobby-backdrop-2 (top), Back steps toward .lobby-backdrop-0
   (bottom), clamped at either end rather than wrapping, so the image pans
   deliberately up through the flow and back down again on the way out
   instead of just oscillating between two spots. */
#lobbyOverlay.lobby-overlay-backdrop.lobby-backdrop-0 {
  background-position: center 100%;
}

#lobbyOverlay.lobby-overlay-backdrop.lobby-backdrop-1 {
  background-position: center 50%;
}

#lobbyOverlay.lobby-overlay-backdrop.lobby-backdrop-2 {
  background-position: center 0%;
}

/* Applied for one frame by showLobbyStep (game.js) only on the very first
   backdrop appearance, so that initial snap-to-bottom doesn't itself pan
   in from wherever background-position's own CSS-initial value sits —
   removed again immediately after, so every subsequent stop change still
   transitions normally. */
#lobbyOverlay.lobby-backdrop-no-transition {
  transition: none;
}

/* No prefers-reduced-motion opt-out here (unlike most other transitions in
   this file) — a slow background-position pan behind static text is a
   subtle decorative shift, not the kind of large/fast motion that setting
   exists to suppress, and disabling it made the backdrop look like it
   "doesn't move" at all on devices with that OS setting on (position still
   changed step to step, just instantly, easy to miss entirely). */

.overlay h2 {
  font-family: 'Oswald', sans-serif;
  text-transform: uppercase;
  letter-spacing: .1em;
  font-size: 20px;
}

.overlay p {
  color: #9aa192;
  max-width: 340px;
  font-size: 13px;
  line-height: 1.6;
}

/* Menu buttons: flat face with a chunky 4px bottom edge — the old
   two-layer gradient crossfade (::before/::after) is gone. Pressing sinks
   the button like a real key: the bottom edge collapses to 1px while the
   button translates down by the same 3px, and margin-bottom takes up the
   freed height so the button's bottom edge (and everything laid out below
   it) stays put mid-press. */
/* .back-link and .exit-btn opt out entirely — they keep their own
   !important styling and get no hover effect at all. */
.overlay button:not(.back-link):not(.exit-btn):not(#chatSendBtn):not(#rematchBtn):not(#copyCodeOnlyBtn):not(#copyCodeBtn):not(#startGameBtn):not(.roster-remove-bot):not(.play-btn):not(.body-option):not(.recent-game-row){
  padding: 14px 12px;
  font-size: 13px;
  font-family: 'IBM Plex Mono', monospace;
  background: #cfc7a8;
  color: #12160f;
  border: 0px solid #878067;
  border-bottom-width: 4px;
  border-radius: 6px;
  font-weight: 700;
  text-transform: uppercase;
  width: 100%;
  cursor: pointer;
  transition: background 0.3s ease;
}

.overlay button:not(.back-link):not(.exit-btn):not(#chatSendBtn):not(#rematchBtn):not(#copyCodeOnlyBtn):not(#copyCodeBtn):not(#startGameBtn):not(.roster-remove-bot):not(.play-btn):not(.body-option):not(.recent-game-row):hover {
  background: var(--amber);
}

.overlay button:not(.back-link):not(.exit-btn):not(#chatSendBtn):not(#rematchBtn):not(#copyCodeOnlyBtn):not(#copyCodeBtn):not(#startGameBtn):not(.roster-remove-bot):not(.play-btn):not(.body-option):not(.recent-game-row):active {
  border-bottom-width: 1px;
  margin-bottom: 3px;
  transform: translateY(3px);
}

/* Not enough players yet: a plain red-text call to action, no fill, no
   border — reads as "waiting/blocked" rather than a normal button. Once
   .ready is toggled on (see showLobbyRoster() in game.js), it flips to a
   solid amber fill with black text — the same "go" signal every other
   ready/confirm action in the app uses. */
/* "Play" — the fast path: join whatever open lobby exists, or host a new
   one if none do (see playBtn's onclick in game.js). Styled as a solid
   gold CTA, same "go" language as #startGameBtn.ready, so it reads as the
   obvious first thing to press rather than one option among equals. */
.play-btn {
  width: 100%;
  padding: 14px 24px;
  /* font-size: 15px; */
  font-family: 'IBM Plex Mono', monospace;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .05em;
  border-radius: 6px;
  border: none;
  background: var(--gold);
  color: #12160f;
  cursor: pointer;
  box-shadow: 0 0 12px rgba(255, 212, 71, 0.35);
  transition: transform 0.15s ease, box-shadow 0.3s ease;
  margin-bottom: 6px;
  border-bottom: 4px solid #b18700;
}

.play-btn:hover {
  box-shadow: 0 0 18px rgba(255, 212, 71, 0.55);
}

.play-btn:active {
  transform: scale(0.97);
}

.play-btn:disabled {
  opacity: 0.6;
  cursor: default;
  box-shadow: none;
}

/* Ascension's entry point on stepChoice — same shape/weight as .play-btn
   (the obvious-CTA look) but light-blue instead of gold, so it doesn't
   compete with Play as the primary action while still reading as a real
   button rather than a plain link. */
.play-btn-blue {
  background: var(--lightblue);
  color: #12160f;
  box-shadow: 0 0 12px rgba(126, 200, 227, 0.35);
  border-bottom: 4px solid #418199;
}

.play-btn-blue:hover {
  box-shadow: 0 0 18px rgba(126, 200, 227, 0.55);
}

/* Register/Log in on stepChoice's guest card — same shape/weight as
   .play-btn but green, distinguishing it from Play's gold and Ascension's
   blue as its own distinct CTA. */
.play-btn-green {
  background: #5cb85c;
  color: #12160f;
  box-shadow: 0 0 12px rgba(92, 184, 92, 0.35);
    border-bottom: 4px solid #378b37;
}

.play-btn-green:hover {
  box-shadow: 0 0 18px rgba(92, 184, 92, 0.55);
}

/* Draft Mode's entry point on stepChoice — same shape/weight as .play-btn
   but red, distinguishing it from Play's gold/Ascension's blue/Register's
   green as its own distinct CTA. */
.play-btn-red {
  background: #d9534f;
  color: #12160f;
  box-shadow: 0 0 12px rgba(217, 83, 79, 0.35);
    border-bottom: 4px solid #93302d;
}

.play-btn-red:hover {
  box-shadow: 0 0 18px rgba(217, 83, 79, 0.55);
}

#startGameBtn {
  padding: 12px 24px;
  font-size: 13px;
  font-family: 'IBM Plex Mono', monospace;
  font-weight: 700;
  text-transform: uppercase;
  width: 100%;
  border-radius: 6px;
  background: transparent;
  color: var(--p2);
  border: none;
  transition: background 0.3s ease, color 0.3s ease;
}

#startGameBtn.ready {
  width: 100%;
  padding: 14px 24px;
  /* font-size: 15px; */
  font-family: 'IBM Plex Mono', monospace;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .05em;
  border-radius: 6px;
  border: none;
  background: var(--gold);
  color: #12160f;
  cursor: pointer;
  box-shadow: 0 0 12px rgba(255, 212, 71, 0.35);
  transition: transform 0.15s ease, box-shadow 0.3s ease;
  margin-bottom: 6px;
  border-bottom: 4px solid #b18700;
}


.hidden {
  display: none !important;
}

.mute-controls {
  /* position: relative; */
  width: 100%;
  /* top: 10px; */
  left: 10px;
  margin-bottom: 6px;
  z-index: 100;
  display: flex;
  gap: 6px;
  justify-content: flex-start;
  max-width: 500px;
}

.mute-btn {
  position: relative;
  font-family: 'IBM Plex Mono', monospace;
  background: var(--panel);
  color: #9aa192;
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 6px 10px;
  font-size: 10px;
  letter-spacing: .05em;
  cursor: pointer;
}

/* Ranked queue status pill — gold, so it stands out from the plain
   Log/Chat/Friends buttons it sits beside in the same top bar (it's the
   one persistent "something is happening in the background" indicator,
   see startRankedQueuePoll in game.js). Timer counts up in a slightly
   dimmer gold beside the label. */
.ranked-queue-btn {
  background: rgba(255, 212, 71, 0.14);
  color: var(--gold);
  border-color: var(--gold);
}
.ranked-queue-btn-timer {
  margin-left: 6px;
  color: var(--amber);
  font-variant-numeric: tabular-nums;
}

/* Options — Exit/audio folded behind one ⚙ button so the in-game top
   bar stays Log/Tips/Chat only. Opens as the same centered combat-log-modal
   overlay every other in-game popup uses (see #configMenu in index.html),
   not its own anchored dropdown — .config-menu-wrap now only needs to
   position the ⚙ button itself within the top bar. */
.config-menu-wrap {
  margin-right: auto; /* ⚙ stays left; Friends/Log/Chat group aligns right */
}

.config-menu-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.config-menu-body .mute-btn {
  text-align: left;
  white-space: nowrap;
  font-size: 1rem;
}

/* Toggle buttons (mute, music, autoconfirm, tips) carry two
   spans — a static label (♪ SFX, 🎵 Music, ...) pinned left and a diamond
   checkbox mark pinned right, space-between so the mark reads as a status
   column instead of running into the label text. */
.toggle-btn {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
}

.toggle-btn-label {
  color: #9aa192;
}

/* Diamond checkbox mark — same rotated-square shape/fill as the volume
   sliders' own thumb (see .volume-slider::-webkit-slider-thumb below),
   reused here so "on" reads as a filled diamond and "off" as just its
   outline, one consistent checked/unchecked language across every
   settings control in this menu rather than mixing slider thumbs with
   ON/OFF text. toggle-on/toggle-off are set alongside each other by
   setToggleBtnState in game.js so the mark and the button's own state can
   never drift apart. */
.toggle-btn-state {
  display: inline-block;
  width: 12px;
  height: 12px;
  flex-shrink: 0;
  border: 1.5px solid var(--p2);
  border-radius: 2px;
  transform: rotate(45deg);
  box-sizing: border-box;
}

.toggle-btn.toggle-on .toggle-btn-state {
  background: #cfc7a8;
  border-color: #cfc7a8;
}

.toggle-btn.toggle-off .toggle-btn-state {
  background: transparent;
}

/* Volume sliders, one under each of SFX/Music's own ON/OFF toggle — styled
   as a thin track + small round thumb to match the config menu's dark
   compact panel rather than the browser's default slider chrome. The
   slider's own box is at least 24px tall (a real touch target), even
   though the visible track drawn inside it stays a thin 4px line. */
.volume-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
}
.volume-slider {
  -webkit-appearance: none;
  appearance: none;
  flex: 1;
  min-width: 0;
  height: 24px;
  background: transparent;
  outline: none;
  cursor: pointer;
}
.volume-slider::-webkit-slider-runnable-track {
  height: 4px;
  background: var(--line);
  border-radius: 2px;
}
.volume-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 14px;
  height: 14px;
  margin-top: -5px; /* centers the 14px thumb on the 4px track within the 24px box */
  border-radius: 3px;
  background: #cfc7a8;
  transform: rotate(45deg);
  cursor: pointer;
}
.volume-slider::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border: none;
  border-radius: 3px;
  background: #cfc7a8;
  transform: rotate(45deg);
  cursor: pointer;
}
.volume-slider::-moz-range-track {
  height: 4px;
  background: var(--line);
  border-radius: 2px;
}
.volume-value {
  min-width: 2.4em;
  text-align: right;
  font-size: 10px;
  color: #9aa192;
  font-variant-numeric: tabular-nums;
}

/* Exit Game sits among plain toggles (audio/tips) but is destructive
   and one-way — a red tint sets it apart so it doesn't get tapped on
   reflex the way a mute toggle would. */
#exitGameBtn {
  background: var(--p2);
  color: var(--ink);
  border-color: var(--p2);
  margin-top: 40px;
  text-align: center;
}

/* Unread-chat pip on #chatToggleBtn — same green as the board's HP dots,
   shown while a new message has arrived and the chat modal is closed;
   cleared the moment it's opened (see chatToggleBtn's onclick). */
.chat-unread-pip {
  position: absolute;
  top: -3px;
  right: -3px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #1eb101;
  border: 1px solid #1e4b00;
  box-shadow: 0 0 3px rgba(43, 255, 0, 0.69);
}

/* Opted out of the generic .overlay button rule above (dark text on a
   dark gradient card — near-unreadable, and easy to mistake for a
   disabled button) in favor of a bold amber CTA, since this is the one
   button on the win screen you actually want to draw the eye. */
#rematchBtn {
  width: 100%;
  margin-top: 12px;
  padding: 12px 24px;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  background: var(--amber);
  color: #1b211d;
  border: none;
  border-radius: 6px;
  cursor: pointer;
}
#rematchBtn:disabled {
  opacity: 0.5;
  cursor: default;
}
  

.lobby-step {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  border-radius: 6px;
  border-top: 6px solid #22291e;
  border-bottom: 6px solid #22291e;
  padding:1rem;
  background-color:#1a1e18;
  opacity: 1;
  transform: none;
  transition: opacity 0.18s ease, transform 0.18s ease;
   width: 100%;
  max-width: 500px;
}

/* The ban-cards grid (customBanGrid, auto-fill columns of >=64px) needs
   more room than the narrower lobby steps to avoid collapsing to a single
   cramped column. */
#stepCustomSettings {
  min-width: 340px;
}
#customBanGrid {
  max-height: 30vh;
}

/* Applied by showLobbyStep (game.js) for one frame right after a step is
   un-hidden, then removed on the next frame — the removal is what actually
   fires the transition (going from one of these "entering" states back to
   .lobby-step's own resting opacity/transform above), same "flip a class,
   let CSS animate the delta" approach as every other transition in this
   file. Only applied on a genuine step change, not on showLobbyRoster's
   every-poll re-call of showLobbyStep('lobbyRoster') while already sitting
   there — see lastShownLobbyStepId in game.js. Two variants instead of one:
   forward navigation (deeper into the flow) slides in from the right,
   Back/logout/leave navigation slides in from the left — showLobbyStep's
   own `direction` argument picks which. */
.lobby-step-entering-forward {
  opacity: 0;
  transform: translateX(24px);
  transition: none;
}

.lobby-step-entering-back {
  opacity: 0;
  transform: translateX(-24px);
  transition: none;
}

#lobbyRoster {
  /* Without an explicit width, this shrinks to whatever its narrowest
     child needs, and full-width children (chat-box, Start Game) then
     resolve against that cramped basis instead of a page-like column —
     matching .wrap's own sizing keeps the re-parented chat box (and this
     screen generally) consistent with the in-game view. */
  width: 100%;
  max-width: 500px;
}

#lobbyroster {
  border-top: 6px solid #22291e;
    border-bottom: 6px solid #22291e;
    padding: 1rem;
}

.back-link {
  /* margin-top: 16px; */
  background: transparent;
  color: #8a9280;
  border: none;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 13px;
  letter-spacing: .05em;
  text-transform: uppercase;
  cursor: pointer;
  background: transparent !important;
  border: 1px solid #8a9280!important;
  padding: 8px 12px;
    border-radius: 6px;
}

.link-btn {
  margin-top: 6px;
  background: transparent;
  color: #8a9280;
  border: none;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 11px;
  letter-spacing: .05em;
  text-transform: uppercase;
  cursor: pointer;
}

.link-btn:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

.link-btn.selected {
  color: #ffd447;
  outline: 1px solid #ffd447;
  border-radius: 4px;
}

/* League badge (#profileModal) — tier name + points, colored per tier via
   data-tier (set by renderProfile in game.js). Sits above the Stats panel,
   same max-width so the two align as one visual column. */
.league-badge {
  width: 100%;
  max-width: 380px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 10px 16px;
  margin-bottom: 4px;
}
.league-badge-tier {
  font-family: 'Oswald', sans-serif;
  letter-spacing: .08em;
  text-transform: uppercase;
  font-size: 15px;
  font-weight: 600;
  color: var(--sand);
}
.league-badge-points {
  font-family: 'IBM Plex Mono', monospace;
  font-size: 12px;
  color: #8a9280;
}
.league-badge[data-tier="Wood"]     .league-badge-tier { color: #a8815a; }
.league-badge[data-tier="Bronze"]   .league-badge-tier { color: #c1743c; }
.league-badge[data-tier="Silver"]   .league-badge-tier { color: #b9c2c9; }
.league-badge[data-tier="Gold"]     .league-badge-tier { color: #d9c93c; }
.league-badge[data-tier="Platinum"] .league-badge-tier { color: #6fd9c9; }
.league-badge[data-tier="Diamond"]  .league-badge-tier { color: #5aa9e6; }
.league-badge[data-tier="Master"]   .league-badge-tier { color: #e05a9e; }

.learn-panel {
  width: 100%;
  max-width: 380px;
  max-height: 60vh;
  overflow-y: auto;
  text-align: left;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 14px 16px;
}

.learn-panel h3 {
  font-family: 'Oswald', sans-serif;
  letter-spacing: .08em;
  text-transform: uppercase;
  font-size: 13px;
  color: var(--amber);
  margin: 14px 0 4px;
}

.learn-panel h3:first-child {
  margin-top: 0;
}

.learn-panel p {
  font-size: 12px;
  line-height: 1.6;
  color: #b8b19a;
  margin: 0 0 6px;
  max-width: none;
}

.learn-panel b {
  color: var(--sand);
}

.learn-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 11px;
}

.learn-table th {
  text-align: left;
  color: #8a9280;
  text-transform: uppercase;
  letter-spacing: .05em;
  font-size: 10px;
  padding: 4px 6px;
  border-bottom: 1px solid var(--line);
}

.learn-table td {
  padding: 6px 6px;
  border-bottom: 1px solid #333c30;
  color: #b8b19a;
  line-height: 1.4;
  vertical-align: top;
}

.learn-table td:first-child {
  color: var(--gold);
  font-weight: 600;
  white-space: nowrap;
}

/* Top-10 favorite cards bar chart (#profileTopCards, profile stats modal) —
   one row per card, label | horizontal bar | raw count. Bar width is
   relative to the #1 card's own count (set inline per row by
   renderProfileTopCards), not a fixed scale. */
.profile-top-cards {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 8px;
}
.profile-bar-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
}
.profile-bar-label {
  flex: 0 0 96px;
  color: #b8b19a;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.profile-bar-track {
  flex: 1 1 auto;
  height: 10px;
  background: #333c30;
  border-radius: 3px;
  overflow: hidden;
}
.profile-bar-fill {
  height: 100%;
  background: var(--amber);
  border-radius: 3px;
  transition: width 0.4s ease;
}
.profile-bar-count {
  flex: 0 0 auto;
  min-width: 1.6em;
  text-align: right;
  color: #8a9280;
  font-family: 'IBM Plex Mono', monospace;
}

/* Card icons injected by renderLearnCardIcons (game.js) into
   #learnCardsTable's first cell, ahead of the name text. */
#learnCardsTable .icon {
  margin-right: 5px;
}

.stat-dmg, .stat-heal {
  display: inline-block;
  font-weight: 700;
  font-size: 10px;
  white-space: nowrap;
}

.stat-dmg {
  color: #e08a6e;
}

.stat-heal {
  color: #8ec98a;
}

/* Profile modal (#profileModal) — reuses .learn-panel/.learn-table wholesale
   for both the summary block and the match-history list; these three
   classes just override .learn-table td:first-child's default gold for
   the history table's outcome cell, so a win/loss/draw reads at a glance. */
#profileHistoryTable td:first-child {
  font-weight: 700;
  white-space: nowrap;
}
.profile-outcome-win { color: var(--amber) !important; }
.profile-outcome-loss { color: #8a9280 !important; }
.profile-outcome-draw { color: var(--sand) !important; }
.profile-replay-link {
  color: var(--amber);
  text-decoration: underline;
  white-space: nowrap;
}

#profileSummaryPanel, #profileHistoryPanel {
  margin-bottom: 12px;
}

.exit-btn {
  /* width: 100%; */
  margin-top: 14px;
  padding: 12px;
  background: transparent;
  color: #8a9280;
  border: 1px solid var(--line);
  border-radius: 3px;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 13px;
  letter-spacing: .05em;
  text-transform: uppercase;
  cursor: pointer;
  color: #404b3a !important;
    background: transparent !important;
    border: 1px solid #3f4b3a !important;
}

.lobby-divider {
  color: #8a9280;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .1em;
  margin: 12px 0;
}

.lobby-join-row {
  display: flex;
  gap: 8px;
}

.lobby-join-row input {
  font-family: 'IBM Plex Mono', monospace;
  background: #1f251f;
  border: 1px solid var(--line);
  color: var(--ink);
  border-radius: 3px;
  padding: 12px 14px;
  font-size: 14px;
  letter-spacing: .1em;
  text-transform: uppercase;
  width: 130px;
  text-align: center;
}

.lobby-join-row button {
  margin-top: 0;
}

.lobby-error {
  color: var(--p2);
  font-size: 12px;
  margin: 12px;
  max-width: 300px;
}

.recent-games {
  width:100%;
  margin-top: 8px;
}

/* Live games (#recentGamesList), open lobbies (#openLobbiesList), and
   finished-match replays (#replayListItems) — the three .recent-games rows
   lists (stepJoinCode/stepSpectateCode). Capped and scrollable rather than
   left to grow with the list, same idiom as .learn-panel above — .lobby-step
   has no height limit of its own and #lobbyOverlay never scrolls (see its
   own comment), so an unbounded list here used to grow the whole step past
   the viewport and push accountNavBar's Back button off-screen with no way
   to reach it. The "Live :"/"Finished matches :" divider label stays
   outside this rule (on the shared .recent-games wrapper) so it stays
   pinned above the scrolling rows instead of scrolling away with them. */
#recentGamesList, #openLobbiesList, #replayListItems {
  max-height: 42vh;
  overflow-y: auto;
}

/* Live/Replay category switch on the Watch screen (#stepSpectateCode) —
   two .link-btn.selected toggles side by side rather than stacked, same
   visual language (gold outline when active) as every other .link-btn,
   just laid out as a row. */
.spectate-category-row {
  display: flex;
  gap: 12px;
  justify-content: center;
  width: 100%;
  margin-top: 4px;
}

.recent-games .recent-game-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  background: #1f251f;
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 8px 12px;
  margin-bottom: 6px;
  cursor: pointer;
  font-family: 'IBM Plex Mono', monospace;
  color: var(--ink);
  text-align: left;
  background: #33392f;
    padding: 10px;
    border: none;
    margin-bottom: 6px;
    border-radius: 3px;
}

.recent-game-row::before{
    background: #1f251f!important;
    border: 1px solid #757575;
}

.recent-games .recent-game-row:hover {
  border-color: var(--amber);
  background: #1f251f;
}

.recent-game-row .recent-game-code {
  font-weight: 700;
  letter-spacing: .1em;
  color: var(--amber);
}

.recent-game-row .recent-game-info {
  flex: 1;
  font-size: 11px;
  color: #9aa192;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Mode badge + border color-coding for the open-lobbies list (Join screen)
   — yellow for normal, red for Draft, so a draft-mode lobby (which needs a
   saved 10-card build to join, see join_game.php) is obvious before
   clicking in. Border color piggybacks a modifier class on the same row
   .recent-game-row already uses, rather than a separate element. */
.recent-game-row.mode-normal {
  border-color: var(--gold);
}

.recent-game-row.mode-draft {
  border-color: #d9534f;
}

/* Custom (see matchDisplayMode() server-side — a plain mode='normal' game
   with non-default starting_lives/banned_cards_json) — blue, same accent
   Custom/Turbo's own buttons already use on stepMultiplayer. */
.recent-game-row.mode-custom {
  border-color: #5aa9e6;
}

/* Ranked — only ever appears in the Watch list (recent_games.php), never
   Join (a ranked match is never joinable by code, see
   list_open_lobbies.php's own comment) — a distinct pink/magenta (the
   league badge's own Master-tier color) since normal/draft/custom already
   claim gold/red/blue. */
.recent-game-row.mode-ranked {
  border-color: #e05a9e;
}

/* Online Ascension (see matchDisplayMode() server-side — mode='ascension'
   passes straight through, never bucketed into 'custom' despite also
   setting a non-default banned_cards_json) — violet, distinct from the 4
   colors above so a co-op lobby reads apart from every adversarial mode
   at a glance. */
.recent-game-row.mode-ascension {
  border-color: #9b6fe0;
}

.recent-game-row .recent-game-mode-badge {
  font-size: 9px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: 3px;
  flex-shrink: 0;
}

.recent-game-mode-badge.mode-normal {
  background: rgba(255, 212, 71, 0.18);
  color: var(--gold);
}

.recent-game-mode-badge.mode-draft {
  background: rgba(217, 83, 79, 0.18);
  color: #d9534f;
}

.recent-game-mode-badge.mode-custom {
  background: rgba(90, 169, 230, 0.18);
  color: #5aa9e6;
}

.recent-game-mode-badge.mode-ranked {
  background: rgba(224, 90, 158, 0.18);
  color: #e05a9e;
}

.recent-game-mode-badge.mode-ascension {
  background: rgba(155, 111, 224, 0.18);
  color: #9b6fe0;
}

.room-code-row {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 12px;
  justify-content: center;
  width: 100%;
}

.room-code {
  /* font-size: 20px;
  letter-spacing: 0.15em;
  font-weight: 700; */
  color: var(--amber);
  border: 2px solid var(--amber);
  border-radius: 5px;
  padding: 5px;
}

.room-created {
  /* text-align: center;
  color: #857f6c;
  margin: 2px 0 8px; */
  font-size: 10px;
}

#copyCodeBtn,
#copyCodeOnlyBtn {
  margin-top: 0;
  padding: 10px 16px;
  font-size: 12px;
  background: transparent;
  color: var(--sand);
  border: 1px solid var(--line);
  border-radius: 3px;
}

#copyCodeBtn.copied,
#copyCodeOnlyBtn.copied {
  background: var(--sand);
  color: #1b211d;
  border-color: var(--sand);
}

.lobby-waiting-text {
  color: var(--amber) !important;
}

.name-input {
  font-family: 'IBM Plex Mono', monospace;
  background: #1f251f;
  border: 1px solid var(--line);
  color: var(--ink);
  border-radius: 3px;
  padding: 12px 14px;
  font-size: 14px;
  letter-spacing: .05em;
  text-align: center;
  margin-bottom: 8px;
  margin-top: 8px;
}

#accountLoggedIn {
  display: flex;
  align-items: center;
  gap: 8px;
  justify-content: center;
  font-size: 12px;
  color: var(--ink);
}

#stepAuth #authPasswordInput {
  text-transform: none; /* a password shouldn't visually uppercase as you type it */
}

.auth-panel-buttons {
  display: flex;
  gap: 8px;
}

.auth-panel-buttons button {
  flex: 1;
}

/* Account card at the top of stepChoice — a framed panel instead of the
   old bare welcome-text + underlined links. Always visible: logged-in
   players get Profile / Log out, guests get a Register button (stepAuth).
   Boxed like .lobby-step itself (same border/background/radius/padding) so
   it reads as its own nested step-like container rather than bare content
   floating at the top of the step. */
/* Wraps the single shared account card outside/above the stepped content
   (see #accountCardWrap's own comment in index.html) — width/centering
   used to come for free from whichever .lobby-step the card sat inside;
   now that it's a sibling of the steps instead, this owns that sizing
   itself. Hidden/shown by showLobbyStep (game.js) depending on the
   current step. */
.account-card-wrap {
  width: 100%;
  max-width: 500px;
}

/* Shared Back/Log out bar — one copy outside/below the stepped content
   instead of a "backFromX" button duplicated on every step (see
   #accountNavBar's own comment in index.html). Same sizing idea as
   .account-card-wrap above. */
.account-nav-bar {
  width: 100%;
  max-width: 500px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}

.account-card {
  /* Wide enough that the logged-in row's buttons fit without the labels
     clipping. */
  width: 260px;
  max-width: 100%;
  border-radius: 6px;
  border-top: 6px solid #22291e;
  border-bottom: 6px solid #22291e;
  padding: 1rem;
  background-color: #1a1e18;
  margin: 6px 0 12px;
  width: 100%;
  /* Smoothly eases every property below between the plain and .collapsed
     states (toggleAccountCardExpanded in game.js) — border/padding/
     background here, max-height/opacity on the actions column below. */
  transition: border-top-width 0.2s ease, border-bottom-width 0.2s ease, padding 0.2s ease, background-color 0.2s ease, margin 0.2s ease;
}

/* Collapsed is the default state (see toggleAccountCardExpanded in
   game.js) — no background/border/padding, just the current preview
   canvas centered; every button row (.account-card-body-row's actions
   column, Register) fades/collapses shut until expanded again. Re-
   collapses on every step change. max-height + opacity, not display:none —
   display:none can't be transitioned (see .action-tooltip.tooltip-hidden's
   own comment for the same reasoning), so the actions column instead
   collapses its own box down to 0 while fading out. */
.account-card.collapsed {
  border-top-width: 0;
  border-bottom-width: 0;
  padding: 0;
  background-color: transparent;
  margin: 6px 0;
  width: auto;
  display: flex;
  justify-content: center;
}
.account-card.collapsed .account-card-body-row {
  width: auto;
  gap: 0;
}
.account-card-actions-col,
.account-card-name {
  max-height: 200px;
  opacity: 1;
  overflow: hidden;
  transition: max-height 0.2s ease, opacity 0.2s ease, margin-top 0.2s ease, flex-basis 0.2s ease;
}
.account-card.collapsed .account-card-actions-col,
.account-card.collapsed .account-card-name {
  max-height: 0;
  opacity: 0;
  margin-top: 0;
  pointer-events: none;
  /* .account-card-actions-col is flex:1 in the expanded row — without
     zeroing its basis/growth too, it still claimed the row's leftover
     horizontal width (and the row's own gap) even at max-height:0, pushing
     the preview canvas off-center instead of leaving it alone in the row. */
  flex: 0 0 0;
  width: 0;
}
.account-card.collapsed .player-preview-canvas {
  cursor: pointer;
}

@media (prefers-reduced-motion: reduce) {
  .account-card,
  .account-card-actions-col,
  .account-card-name {
    transition: none;
  }
}

.account-card-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.account-card-name {
  font-size: 12px;
  color: var(--ink);
  letter-spacing: .05em;
}

.account-card-actions {
  display: flex;
  gap: 8px;
  width: 100%;
}

/* Wraps the account-card's button rows alongside playerPreviewCanvas — the
   canvas sits on the left, the two .account-card-actions rows stack in the
   remaining space on the right, same "small read-only token preview" idea
   as .wardrobe-preview-canvas but without any of the wardrobe modal's
   carousel/color-picker chrome. */
.account-card-body-row {
  display: flex;
  gap: 10px;
  width: 100%;
  align-items: center;
}

.account-card-actions-col {
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 1;
  min-width: 0;
}

.player-preview-canvas {
  width: 100px;
  height: 100px;
  /* border: 1px solid var(--line); */
  border-radius: 4px;
  flex-shrink: 0;
}

.account-card-btn {
  flex: 1;
  background: var(--panel-2);
  color: var(--sand);
  border: 1px solid var(--line);
  border-radius: 6px;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 10px;
  letter-spacing: .05em;
  text-transform: uppercase;
  padding: 7px 6px;
  cursor: pointer;
}

/* "Looks disabled" but stays a real, clickable button — used instead of the
   disabled attribute wherever a click should pop the disabledReasonModal
   explaining why (see game.js's disabled-button explainer). A genuinely
   disabled <button> never fires a click event in any browser, so intercepting
   the tap requires keeping it enabled and faking the dimmed look here instead.
   aria-disabled (set alongside this class, not the disabled attribute) keeps
   assistive tech honest about its state without blocking the click JS needs. */
.btn-disabled {
  opacity: 0.6;
  cursor: default;
}
.account-card-btn.btn-disabled {
  cursor: default;
}

/* Achievements modal body — one block per server/lib/unlocks.php entry,
   rendered by renderAchievementsList in game.js: label + have/count on
   one line, the condition text below it, then a fill bar showing progress
   toward the requirement. Met unlocks show a gold checkmark/label and a
   fully-filled bar; unmet ones fill proportionally, so the list doesn't
   jump around as entries flip from locked to unlocked, it just fills in. */
.achievements-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
}

.achievement-row {
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-size: 10px;
  letter-spacing: .03em;
  color: var(--sand);
}

.achievement-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
}

.achievement-row.met .achievement-label {
  color: var(--gold);
}

.achievement-condition {
  opacity: 0.75;
  font-size: 9px;
}

.achievement-progress {
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  opacity: 0.85;
}

.achievement-row.met .achievement-progress {
  color: var(--gold);
}

.achievement-bar {
  width: 100%;
  height: 6px;
  border-radius: 3px;
  background: var(--panel-2);
  border: 1px solid var(--line);
  overflow: hidden;
}

.achievement-bar-fill {
  height: 100%;
  background: var(--amber);
  border-radius: 2px;
  transition: width 0.3s ease;
}

.achievement-row.met .achievement-bar-fill {
  background: var(--gold);
}

/* Lobby color pool — one swatch per palette slot; free ones are tappable,
   taken ones dim out, and yours wears a white ring. */
/* The .body-picker row inside the room lobby's Customize modal (see
   showLobbyRoster in game.js, which fills #lobbyBodyPicker via
   buildBodyOptionButtons) — smaller than the wardrobe modal's version
   since it's a secondary "restyle after seeing everyone else" control, and
   centered to match the color pool below it. */
.lobby-body-picker {
  justify-content: center;
  flex-wrap: nowrap;
  margin: 10px 0 2px;
}

.lobby-body-picker .body-option {
  width: 34px;
  height: 34px;
  flex: 0 0 auto;
  padding: 4px;
}

.lobby-body-picker .body-swatch {
  transform: scale(0.62);
}

.color-pool {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin: 10px 0 2px;
  flex-wrap: wrap;
  max-width: 240px;
}

.color-pool .color-swatch {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}

.color-pool .color-swatch.taken {
  opacity: 0.28;
  cursor: default;
}

.color-pool .color-swatch.mine {
  opacity: 1;
  border-color: #fff;
  box-shadow: 0 0 7px rgba(255, 255, 255, 0.45);
}

.roster-list {
  width: 100%;
  margin: 6px 0 16px;
  text-align: left;
}

.roster-list .roster-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 4px;
  font-size: 12px;
  /* border-bottom: 1px solid var(--line); */
}

.roster-list .roster-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
}

.roster-list .roster-name {
  flex: 1;
  color: var(--ink);
}

.roster-list .roster-name-link {
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: transparent;
}
.roster-list .roster-name-link:hover {
  text-decoration-color: currentColor;
}

.roster-list .roster-tag {
  color: #8a9280;
  font-size: 10px;
  text-transform: uppercase;
}

.roster-list .roster-tag.bot-tag {
  color: var(--amber);
  border: 1px solid var(--amber);
  border-radius: 3px;
  padding: 1px 4px;
}

.roster-list .roster-remove-bot {
  background: red!important;
  border: 1px solid var(--amber);
  color: #ffffff;
  border-radius: 4px;
  width: 20px!important;
  height: 20px!important;
  line-height: 1;
  font-size: 13px;
  cursor: pointer;
  flex-shrink: 0;
}

.roster-list .roster-remove-bot:hover {
  border-color: #c1443c;
  color: #c1443c;
}

/* Host-only lobby control: pick a personality, add a bot to fill an
   empty seat. Sits right under the roster list, above Leave. */
.add-bot-row {
  display: flex;
  gap: 8px;
  width: 100%;
  margin: 4px 0 ;
}

/* .add-bot-row button {
  background: var(--amber);
  color: #1b211d;
  border: none;
  border-radius: 4px;
  padding: 8px 14px;
  font-family: 'IBM Plex Mono', monospace;
  font-weight: 700;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: .03em;
  cursor: pointer;
  white-space: nowrap;
  flex:1;
}

.add-bot-row button:disabled {
  opacity: 0.5;
  cursor: default;  
} */

.roster-overlay {
  position: absolute;
  top: 6px;
  left: 9px;
  z-index: 6;
  max-width: 150px;
  /* background: rgba(31, 37, 31, 0.92);
  border: 1px solid var(--line); */
  border-radius: 4px;
  /* padding: 6px 10px; */
  font-size: 10px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  transform: translateX(0);
  transition: transform 0.392s ease, opacity 0.392s ease;
  opacity: 1;
}

/* Flies out through its own closest edge (the left) instead of a hard
   display:none — see .instruction.fly-hidden's identical reasoning. */
.roster-overlay.fly-hidden {
  transform: translateX(-140%);
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .roster-overlay {
    transition: none;
  }
}

.roster-overlay .roster-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 0;
  font-size: 10px;
  white-space: nowrap;
}

.roster-overlay .roster-dot {
  width: 8px;
  height: 8px;
  clip-path: path('M4,7.8 C-2.3,3.4 -0.3,-1.1 4,1.8 C8.3,-1.1 10.3,3.4 4,7.8 Z');
  flex-shrink: 0;
}

.roster-overlay .roster-name {
  color: var(--ink);
  cursor: default;
}

.roster-overlay .roster-name.dead {
  color: #5a6055;
  text-decoration: line-through;
}

.roster-overlay .roster-lives {
  display: flex;
  align-items: center;
  gap: 3px;
  flex-shrink: 0;
}

.roster-overlay .roster-hp {
  display: flex;
  align-items: center;
  gap: 2px;
  flex-shrink: 0;
}

.roster-overlay .roster-hp-pip {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: #5cb85c;
  flex-shrink: 0;
}

.roster-overlay .roster-progress {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 3px;
  flex-shrink: 0;
}

.roster-overlay .roster-pip {
  width: 6px;
  height: 6px;
  border-radius: 0;
  background: #3a4235;
  border: 1px solid var(--line);
}

.roster-overlay .roster-pip.filled {
  background: var(--amber);
  border-color: var(--amber);
}

.roster-overlay .roster-pip.filled.locked {
  background: var(--p2);
  border-color: var(--p2);
}


/* Slides the whole win card up from the bottom of the screen as it fades
   in, instead of just popping in place — applied via JS right when the
   .hidden class is removed (see endGame() in game.js). */
#winOverlay.win-enter {
  animation: win-overlay-enter 500ms ease-out;
}

@keyframes win-overlay-enter {
  from { transform: translateY(60px); opacity: 0; }
  to   { transform: translateY(0); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  #winOverlay.win-enter {
    animation: none;
  }
}


/* End-of-match stats/ranking table — reuses the log-panel's dark-panel
   treatment so it reads as part of the same win card, not a bolted-on
   widget. One row per seat, ranked by survival (winner first). */
.stats-panel {
  width: 100%;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 8px 10px;
  font-size: 11px;
}

/* Ranked's own points-delta line under the stats panel — green for a gain,
   red for a loss, same win/loss color language the rest of the app already
   uses (e.g. .profile-outcome-win/-loss). */
.ranked-points-delta {
  width: 100%;
  text-align: center;
  font-family: 'Oswald', sans-serif;
  letter-spacing: .05em;
  font-size: 14px;
  padding: 4px 0;
}
.ranked-points-delta.ranked-points-gain { color: #5cb85c; }
.ranked-points-delta.ranked-points-loss { color: #e2483d; }

.stats-panel .stats-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 0;
  border-bottom: 1px solid var(--line);
}

.stats-panel .stats-row:last-child {
  border-bottom: none;
}

.stats-panel .stats-row.winner-row {
  color: var(--amber);
  font-weight: 600;
}

.stats-panel .stats-rank {
  width: 1.4em;
  text-align: right;
  color: #9aa192;
  flex-shrink: 0;
}

.stats-panel .stats-color-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex-shrink: 0;
}

.stats-panel .stats-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.stats-panel .stats-fav-spell {
  display: flex;
  align-items: center;
  gap: 3px;
  color: #b8b19a;
  flex-shrink: 0;
}

.stats-panel .stats-numbers {
  display: flex;
  gap: 10px;
  color: #b8b19a;
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}

.stats-panel .stats-numbers .stat-label {
  color: #6f7568;
  margin-right: 2px;
}

/* Body wardrobe picker on the name screen — one button per shape in
   game.js's BODY_SHAPES; the swatch spans are pure-CSS silhouettes. */
.body-picker {
  display: flex;
  flex-wrap:wrap;
  gap: 8px;
}
.body-option {
  font-family: 'IBM Plex Mono', monospace;
  background: #1f251f;
  border: 1px solid var(--line);
  border-radius: 3px;
  width: 48px;
  height: 48px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 6px;
  cursor: pointer;
  flex: 1;
}
.body-option.selected {
  border-color: var(--amber);
  box-shadow: 0 0 0 1px var(--amber);
}
.body-swatch {
  display: block;
  background: #9aa192;
  border: 2px solid #12160f;
}
.body-option.selected .body-swatch { background: var(--amber); }
.body-swatch-circle { width: 24px; height: 24px; border-radius: 50%; }
.body-swatch-block  { width: 24px; height: 24px; border-radius: 5px; }
.body-swatch-tower  { width: 20px; height: 28px; border-radius: 6px; }
.body-swatch-hexagon { width: 26px; height: 24px; border: none; clip-path: polygon(25% 0, 75% 0, 100% 50%, 75% 100%, 25% 100%, 0 50%); }
.body-swatch-tomb   { width: 20px; height: 26px; border-radius: 10px 10px 2px 2px; }
.body-swatch-cup    { width: 24px; height: 22px; border-radius: 2px 2px 45% 45%; }
.body-swatch-triangle { width: 26px; height: 26px; border: none; clip-path: polygon(50% 0, 100% 100%, 0 100%); }
.body-swatch-trapezoid { width: 26px; height: 22px; border: none; clip-path: polygon(0 0, 100% 0, 80% 100%, 20% 100%); }
.body-swatch-triangleDown { width: 26px; height: 26px; border: none; clip-path: polygon(0 0, 100% 0, 50% 100%); }
.body-swatch-trapezoidDown { width: 26px; height: 22px; border: none; clip-path: polygon(20% 0, 80% 0, 100% 100%, 0 100%); }
.body-swatch-octagon { width: 26px; height: 26px; border: none; clip-path: polygon(30% 0, 70% 0, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0 70%, 0 30%); }
.body-swatch-pentagon { width: 26px; height: 26px; border: none; clip-path: polygon(50% 0, 100% 38%, 82% 100%, 18% 100%, 0 38%); }
.body-swatch-emerald { width: 24px; height: 30px; border: none; clip-path: polygon(20% 0, 80% 0, 100% 18%, 100% 82%, 80% 100%, 20% 100%, 0 82%, 0 18%); }
.body-swatch-star { width: 28px; height: 28px; border: none; clip-path: polygon(50% 0%, 63% 35%, 100% 35%, 69% 57%, 82% 91%, 50% 70%, 18% 91%, 31% 57%, 0% 35%, 37% 35%); }
.body-swatch-heart { width: 26px; height: 24px; border: none; clip-path: path("M13,24 C2,15 0,8 0,5 C0,1 4,-1 8,2 C10,3.5 12,6 13,8 C14,6 16,3.5 18,2 C22,-1 26,1 26,5 C26,8 24,15 13,24 Z"); }
.body-swatch-berry { width: 26px; height: 24px; border-radius: 50%; }
.body-swatch-teardrop { width: 22px; height: 28px; border: none; clip-path: path("M11,0 C17,10 22,16 22,20 C22,25 17,28 11,28 C5,28 0,25 0,20 C0,16 5,10 11,0 Z"); }
.body-swatch-pear { width: 22px; height: 28px; border: none; clip-path: path("M11,0 C15,0 15,6 15,8 C20,10 22,16 22,20 C22,25 17,28 11,28 C5,28 0,25 0,20 C0,16 2,10 7,8 C7,6 7,0 11,0 Z"); }
.body-swatch-bulb { width: 22px; height: 28px; border: none; clip-path: path("M11,0 C17,0 22,4 22,8 C22,12 20,15 15,18 C15,20 15,25 11,28 C7,25 7,20 7,18 C2,15 0,12 0,8 C0,4 5,0 11,0 Z"); }
.body-swatch-gourd { width: 22px; height: 28px; border: none; clip-path: path("M11,0 C17,0 21,4 21,8 C21,11 19,13 15,14 C19,15 21,17 21,20 C21,24 17,28 11,28 C5,28 1,24 1,20 C1,17 3,15 7,14 C3,13 1,11 1,8 C1,4 5,0 11,0 Z"); }
/* A square rotated 45° — same clip-path technique as the other angular
   swatches above, just a 4-point diamond polygon instead of block's plain
   border-radius square. */
.body-swatch-diamond { width: 26px; height: 26px; border: none; clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
.body-swatch-pill { width: 18px; height: 28px; border-radius: 9px; }
.body-swatch-shortpill { width: 28px; height: 18px; border-radius: 9px; }

/* Hat picker — plain-text option pills (game.js's HAT_OPTIONS/
   buildHatOptionButtons) rather than icon swatches: most hat silhouettes
   (the cowboy brim, the halo ring) don't read clearly at swatch size. */
.hat-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
}
.hat-option {
  font-family: 'IBM Plex Mono', monospace;
  font-size: 11px;
  letter-spacing: .05em;
  text-transform: uppercase;
  background: #1f251f;
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 8px 12px;
  cursor: pointer;
}
.hat-option.selected {
  border-color: var(--amber);
  color: var(--amber);
  box-shadow: 0 0 0 1px var(--amber);
}

/* The turn-tag's own number (e.g. "Round 3", "Level 2") — .turn-tag itself
   is white now (used to be the reverse: amber base text, white emphasis),
   so this is the one part of the tag that stands out. Inline now, not its
   own block/smaller line — it used to also carry a secondary "Turn N" line
   underneath the round number, which no longer exists (see game.js's own
   turnTagText/spectateTagText — the turn count was dropped entirely). */
.round-emph {
  color: var(--amber);
}

/* Wardrobe modal's body/hat/marking/tone pickers — left/right carousels
   (one option visible at a time) instead of the full button grid used
   elsewhere (.body-picker/.hat-picker, still used by the lobby roster's
   own re-pick row). */
.carousel-field {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 100%;
}

/* Two .carousel-field entries side by side (currently Hat + Body) instead
   of each taking the full row — each field keeps its own 100%-width
   layout, just squeezed into half the space via flex. */
.wardrobe-field-row {
  display: flex;
  gap: 10px;
  width: 100%;
}

.wardrobe-field-row .carousel-field {
  flex: 1;
  min-width: 0;
}

/* Same compact-carousel treatment .wardrobe-bling-row already uses — two
   fields sharing one row have half the width of a standalone
   .carousel-field, so the label needs to wrap and the fixed min-width has
   to go, or a long body-shape name (e.g. "Trapezoid Down") would clip. */
.wardrobe-field-row .wardrobe-carousel {
  gap: 6px;
}

.wardrobe-field-row .carousel-current {
  /* Half-width field, so the standalone 96px box would overflow — shrink
     it but keep it fixed (not min-width:0) so it still holds the arrows
     a constant distance apart regardless of label length. */
  width: 64px;
}

.wardrobe-field-row .carousel-label {
  font-size: 10px;
  white-space: nowrap;
  text-align: center;
  line-height: 1.15;
}

.carousel-field-label {
  font-size: 9px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #6f766a;
}

/* A bling slot's own carousel + its "in front" checkbox, stacked tighter
   than the standalone .carousel-field rows above it since this is one
   logical row (picker + toggle) rather than two separate fields. */
.wardrobe-bling-row {
  gap: 2px;
}

/* Body's and Soul's own arrow-only carousels flank the canvas left/right
   (see .wardrobe-carousel-bare below) — this row is what centers the
   canvas between them, same margin:0-auto centering .wardrobe-canvas-wrap
   used to do alone before Body/Soul moved in beside it. */
.wardrobe-canvas-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
}

/* Each side of the canvas: Body's single arrow stacked 20px above Soul's
   own (left column = both ‹, right column = both ›) — see
   .wardrobe-carousel-bare for the individual arrow containers themselves. */
.wardrobe-canvas-arrow-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  flex-shrink: 0;
}

/* Positioning context for the 4 corner FRONT/BEHIND toggles — sized/placed
   purely off the canvas's own box, so the toggles track it directly rather
   than needing their own layout math. Leads the panel now (see
   .wardrobe-modal-body), centered by its row (.wardrobe-canvas-row) rather
   than its own margin now that Body/Soul's arrows flank it. */
.wardrobe-canvas-wrap {
  position: relative;
  width: 160px;
  height: 160px;
  /* Both .wardrobe-canvas-row and its ancestor .wardrobe-modal-body are
     flex containers, so without this the wrap is a flex item twice over
     and gets shrunk below its own 160px height whenever the panel's
     content is taller than its max-height (72vh) — on shorter mobile
     viewports it visibly squashed down to ~97px, with the
     absolutely-positioned <canvas> inside (still 160px, unaffected by the
     shrink) spilling out of the wrap's now-shorter box. flex-shrink:0
     keeps it fixed size; any real overflow scrolls via the body's own
     overflow-y:auto instead. */
  flex-shrink: 0;
}

.wardrobe-canvas-wrap .wardrobe-preview-canvas {
  position: absolute;
  top: 0;
  left: 0;
}

/* Each bling slot's own front/behind toggle, pinned directly onto the
   canvas at the same corner it decorates (see BLING_SLOTS in game.js:
   TL/BL at the body's left edge, TR/BR at its right) — reads as "which
   layer of THIS corner" rather than a generic option living beside its
   carousel. Compact pill button rather than a checkbox: click cycles
   FRONT/BEHIND, current state IS the label so no separate indicator is
   needed. Half-transparent by default so it doesn't obscure the preview
   more than necessary, full opacity on hover/focus for legibility while
   interacting with it. */
.wardrobe-front-toggle {
  position: absolute;
  font-family: 'IBM Plex Mono', monospace;
  font-size: 8px;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  padding: 2px 5px;
  border-radius: 3px;
  border: 1px solid var(--line);
  background: rgba(31, 37, 31, 0.65);
  color: var(--sand);
  cursor: pointer;
  z-index: 2;
}

.wardrobe-front-toggle:hover,
.wardrobe-front-toggle:focus-visible {
  background: rgba(31, 37, 31, 0.92);
  border-color: var(--amber);
  color: var(--amber);
}

.wardrobe-front-toggle.is-front {
  border-color: var(--gold);
  color: var(--gold);
}

.wardrobe-front-toggle.corner-tl { top: 4px; left: 4px; }
.wardrobe-front-toggle.corner-tr { top: 4px; right: 4px; }
.wardrobe-front-toggle.corner-bl { bottom: 4px; left: 4px; }
.wardrobe-front-toggle.corner-br { bottom: 4px; right: 4px; }

/* Each bling slot's own color picker — a real <select> (native dropdown
   still works for keyboard/tap) styled as a small filled circle showing
   the currently-picked color as its background, rather than a text/arrow
   box. Positioned directly under (top corners) or above (bottom corners)
   that same corner's own FRONT/BEHIND toggle, over the canvas — same
   "lives at the corner it decorates" idea as the toggle itself. Text/arrow
   suppressed via appearance:none + a fixed small box so only the color
   shows; color: transparent hides the <option> text that would otherwise
   peek through on some browsers' native control rendering. */
.wardrobe-bling-color-select {
  position: absolute;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 1px solid var(--line);
  padding: 0;
  cursor: pointer;
  z-index: 2;
  appearance: none;
  -webkit-appearance: none;
  color: transparent;
  background-repeat: no-repeat;
}

.wardrobe-bling-color-select:hover,
.wardrobe-bling-color-select:focus-visible {
  border-color: var(--amber);
}

.wardrobe-bling-color-select.corner-tl { top: 26px; left: 6px; }
.wardrobe-bling-color-select.corner-tr { top: 26px; right: 6px; }
.wardrobe-bling-color-select.corner-bl { bottom: 26px; left: 6px; }
.wardrobe-bling-color-select.corner-br { bottom: 26px; right: 6px; }

.wardrobe-carousel {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
}

/* Set by buildCarouselPicker's own `disabled` param (e.g. Tone while
   Marking is 'none') — greys the whole picker out instead of hiding the
   field, so its row stays put rather than the layout shifting. */
.wardrobe-carousel.carousel-disabled {
  opacity: 0.4;
}

/* Body/Soul's own carousels (buildCarouselPicker's `bare` param — arrow
   buttons only, no .carousel-current/label span in between) flanking the
   canvas in .wardrobe-canvas-row. No fixed-width current box to size
   around here, so this just collapses the row gap the base
   .wardrobe-carousel rule would otherwise leave for one. */
.wardrobe-carousel-bare {
  display: flex;
  align-items: center;
  gap: 0;
  width: auto;
}

.carousel-arrow:disabled {
  cursor: default;
  opacity: 0.6;
}

.carousel-arrow {
  font-family: 'IBM Plex Mono', monospace;
  background: #1f251f;
  color: var(--sand);
  border: 1px solid var(--line);
  border-radius: 3px;
  width: 32px;
  height: 32px;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  flex-shrink: 0;
}

.carousel-arrow:hover {
  border-color: var(--amber);
  color: var(--amber);
}

.carousel-current {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  /* Fixed width, not just a min — a long label must never grow this box,
     or the arrows (laid out beside it in .wardrobe-carousel's flex row)
     drift outward with it instead of sitting a fixed distance from center.
     Overflow is cropped (see .carousel-label's own ellipsis) rather than
     wrapping or widening. */
  width: 96px;
  flex-shrink: 0;
}

.carousel-label {
  font-family: 'IBM Plex Mono', monospace;
  font-size: 12px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--amber);
  white-space: nowrap;
  /* Crop instead of letting long option names (e.g. "Trapezoid Down")
     push the carousel's fixed-width box wider than its arrows expect. */
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

.carousel-current .body-swatch {
  background: var(--amber);
}

.wardrobe-bling-row .carousel-current {
  /* Same fixed-width-over-min-width fix as .wardrobe-field-row above —
     keeps this row's own arrows a constant distance apart too. */
  width: 64px;
}

.wardrobe-bling-row .carousel-label {
  font-size: 10px;
  white-space: nowrap;
  text-align: center;
  line-height: 1.15;
}

/* 2x2 block of Bling pickers below the canvas/Hat+Body/etc. rows, instead
   of a pair of narrow columns flanking the canvas — each cell now has the
   same width as any other .wardrobe-field-row field, so the bling
   carousels no longer need their old cramped stacked-arrow treatment. */
.wardrobe-bling-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  width: 100%;
}

/* A still-locked wardrobe option (see buildCarouselPicker's getLockInfo) —
   browsable via the carousel arrows same as any unlocked one, just dimmed
   and non-committal: picking it is a no-op until the account meets the
   requirement shown in the hint below the label. */
.carousel-current.locked .carousel-label {
  color: #9aa192;
}

.carousel-lock-hint {
  font-family: 'IBM Plex Mono', monospace;
  font-size: 9px;
  letter-spacing: .03em;
  text-transform: none;
  color: #6f766a;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
