/* Dice Brawl: Unfold the Dungeon - game screen: board, pieces, dice screen, control bar. */

:root {
  --cell: 24px;
  /* Player 1 builds red path, Player 2 builds blue path. */
  --path-0: rgba(229, 72, 77, 0.38);
  --path-1: rgba(59, 130, 246, 0.38);
  --tile-empty: #1a1d25;
  --tile-line: #262a35;
  --ghost-ok: rgba(80, 220, 130, 0.55);
  /* Striped, so an illegal ghost never looks like red path. */
  --ghost-bad: repeating-linear-gradient(45deg, rgba(240, 240, 240, 0.45) 0 3px, rgba(20, 20, 25, 0.6) 3px 6px);
  /* [ZeroLabs] 2026-10-27: orange (Zero; was sky blue #5bb8e8, the same as the Wind faction). */
  --crest-move: #f0903c;
  --crest-attack: #e0565b;
  --crest-defense: #8f9aa6;
  --crest-magic: #9b6ce0;
  --crest-trap: #4fa65a;
  --crest-summon: #e8c35b;
}

#screen-game {
  display: flex;
  flex-direction: column;
}

/* [ZeroLabs] 2026-11-01: the turn label is centered on the screen (Zero). Menu and Settings get the
   same width (renderHeader in app.js), and the label fills the middle with its text centered. Its
   column can shrink, and fitTextToWidth then makes its text smaller. (Equal 1fr side columns did not
   work: on a narrow screen the label took its width first and the side columns fell back to the
   buttons' own, different widths.) */
.game-header {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  padding: 8px 16px;
  border-bottom: 1px solid var(--border);
  flex: 0 0 auto;
}

.game-header #btn-menu {
  justify-self: start;
}

.game-header #btn-settings {
  justify-self: end;
}

.turn-label {
  min-width: 0;
  white-space: nowrap;
  text-align: center;
  font-weight: 600;
  font-size: 15px;
}

/* One column on every screen size: strip, stage, strip, control bar. */
.game-body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px 16px 10px;
}

/* The stage holds the board, the shape slider above it, and the dice screen that covers it. */
.stage {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.board-wrap {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* A zoomed-in board stays inside its area instead of covering the strips and the panel. */
  overflow: hidden;
  /* Two fingers twist the board here, so the browser must not pan or zoom. */
  touch-action: none;
}

/* The reason the dimension shape cannot go where it is, in the exact middle of the screen.
   It never takes taps, so dragging under it keeps working. */
.board-notice {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  max-width: calc(100vw - 32px);
  z-index: 20;
  /* Information only: no box, no border, and taps go straight through to the board. */
  pointer-events: none;
  background: none;
  color: rgba(255, 214, 216, 0.85);
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
  /* A soft dark outline keeps the text readable over any part of the board. */
  text-shadow: 0 0 3px rgba(0, 0, 0, 0.9), 0 0 6px rgba(0, 0, 0, 0.6);
}

.view-controls {
  position: absolute;
  right: 0;
  bottom: 0;
  display: flex;
  gap: 6px;
  z-index: 5;
}

.view-button {
  width: 44px;
  height: 44px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.view-button svg {
  width: 22px;
  height: 22px;
  fill: currentColor;
}

/* Turning the board animates. The figures turn with it, so they keep facing the player. */
.board,
.figure {
  transition: transform 0.35s ease;
}

/* No animation while measuring the board, or while two fingers twist it. */
.board-scene.no-transition .board,
.board-scene.no-transition .figure {
  transition: none;
}

/* The dice screen covers the whole stage, so the board is out of sight while dice are chosen
   and rolled, as in DDM. It scrolls on its own if a small screen needs it. */
.dice-screen {
  position: absolute;
  inset: 0;
  z-index: 10;
  overflow-y: auto;
  background: var(--surface);
  border-radius: 10px;
  box-shadow: var(--inset);
  padding: 16px;
  display: flex;
  flex-direction: column;
}

.dice-screen-inner {
  width: 100%;
  max-width: 520px;
  margin: auto;
}

/* The shape slider: one row of shape thumbnails along the top of the board. Swipe to see more. */
.shape-slider {
  --thumb-cell: 8px;
  flex: 0 0 auto;
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding: 2px 2px 6px;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
}

/* A fade on each edge that has more shapes past it, so the player can see the row continues.
   app.js sets the classes from the scroll position. */
.shape-slider.more-left {
  -webkit-mask-image: linear-gradient(to right, transparent 0, black 40px);
  mask-image: linear-gradient(to right, transparent 0, black 40px);
}

.shape-slider.more-right {
  -webkit-mask-image: linear-gradient(to left, transparent 0, black 40px);
  mask-image: linear-gradient(to left, transparent 0, black 40px);
}

.shape-slider.more-left.more-right {
  -webkit-mask-image: linear-gradient(to right, transparent 0, black 40px, black calc(100% - 40px), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, black 40px, black calc(100% - 40px), transparent 100%);
}

.shape-slider .shape-thumb {
  flex: 0 0 52px;
  height: 44px;
  scroll-snap-align: center;
}

/* The control bar at the bottom of the screen: placement controls, the bonus crest, End turn. */
.control-bar {
  flex: 0 0 auto;
  background: var(--surface);
  border-radius: 10px;
  box-shadow: var(--inset);
  padding: 8px;
}

.bar-row {
  display: flex;
  gap: 6px;
  align-items: stretch;
}

.bar-row .tool-button {
  flex: 1;
  width: auto;
  min-width: 0;
}

/* Always one line at a fixed height. A message that changes length must never change the bar's
   height, because that resizes the board under the player's finger. Long text shrinks to fit. */
.bar-message {
  font-size: 12px;
  line-height: 16px;
  height: 16px;
  color: var(--text-dim);
  margin: 0 0 6px;
  text-align: center;
  white-space: nowrap;
}

.bar-message.problem {
  color: var(--danger);
}

/* ---------- Player strips ---------- */

/* [ZeroLabs] 2026-10-27: a crest that the open window's cost uses bounces in the lit strip (Zero),
   the same bounce as ColorBrawl's dc-bounce: a 5px hop, a 3px hop, then a rest, every 2.4s. */
@keyframes crest-bounce {
  0%   { transform: translateY(0); }
  12%  { transform: translateY(-5px); }
  22%  { transform: translateY(0); }
  32%  { transform: translateY(-3px); }
  40%  { transform: translateY(0); }
  100% { transform: translateY(0); }
}

.crest-count.crest-bounce .crest-icon {
  animation: crest-bounce 2.4s ease-in-out infinite;
}

/* [ZeroLabs] 2026-10-27: the answering player's strip, bright above an open window's dark layer
   (z-index 50, shell.css). Taps fall through to the dark layer. See updateLitStrip in app.js. */
.player-strip.strip-lit {
  position: relative;
  z-index: 60;
  pointer-events: none;
}

/* [ZeroLabs] 2026-11-01: both strips in one bar at the top: Player 1 on the left, Player 2 (or the
   bot) on the right, on every screen (Zero). Each strip has two rows: the name and hearts, then the
   crest pool. Player 2's strip is the mirror of Player 1's: its color edge and contents on the right. */
.strip-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 6px;
  order: 0;
}

.game-body .stage {
  order: 1;
}

.game-body .control-bar {
  order: 3;
}

.player-strip {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  white-space: nowrap;
  font-size: 13px;
  min-width: 0;
  padding: 4px 8px;
  border-radius: 6px;
  background: var(--surface);
  box-shadow: var(--inset);
  border-left: 4px solid transparent;
}

.player-strip.player-0 {
  border-left-color: var(--player-0);
}

.player-strip.player-1 {
  align-items: flex-end;
  border-left: none;
  border-right: 4px solid var(--player-1);
}

.strip-top {
  display: flex;
  align-items: center;
  /* [ZeroLabs] 2026-11-01: in em, so it shrinks with the text. */
  gap: 0.6em;
}

.player-strip.player-1 .strip-top {
  flex-direction: row-reverse;
}

.player-strip.active {
  outline: 1px solid var(--accent);
}

.strip-name {
  font-weight: 600;
}

/* The heart colors come from .heart, so lost hearts show grey here too. */
.strip-hearts {
  letter-spacing: 1px;
}

.strip-hearts .heart {
  display: inline-block;
}

/* [ZeroLabs] 2026-11-01: the crest pool is the strip's second row (no longer pushed to the right).
   The gap is in em, so it shrinks with the text (fitStripsTogether in app.js); the row never wraps. */
.strip-crests {
  display: flex;
  flex-wrap: nowrap;
  gap: 0.6em;
}

.crest-count {
  font-variant-numeric: tabular-nums;
}

/* ---------- Board (2.5D tabletop) ----------
   The grid tilts back with rotateX. Each raised tile is a "block": a top face lifted with translateZ,
   plus side faces folded down to the floor. Figures (pieces, chests, Kings) are upright cards that
   undo the tilt, so they stand on top of the blocks and face the player. */

:root {
  /* --tilt leans the board back. --spin turns it, so the view comes from a corner and figures
     do not stand straight in front of the tiles behind them. app.js sets --spin: the default
     corner view (BASE_SPIN_DEGREES) plus however far the player has turned the board. */
  --tilt: 42deg;
  --spin: -12deg;
  --block-height: calc(var(--cell) * 0.42);
  --dais-height: calc(var(--cell) * 0.6);
  --floor: #232733;
  --floor-line: rgba(0, 0, 0, 0.45);
  --block-0-top: #cf4a4f;
  --block-0-front: #8c2a2e;
  --block-0-side: #a8373b;
  --block-1-top: #3f82e3;
  --block-1-front: #234f92;
  --block-1-side: #2e65b5;
  --dais-top: #9aa0ad;
  --dais-front: #5d626d;
  --dais-side: #787e8a;
}

/* The scene carries the player's zoom and pan. app.js sets the three variables. */
.board-scene {
  --zoom: 1;
  --pan-x: 0px;
  --pan-y: 0px;
  perspective: calc(var(--cell) * 70);
  perspective-origin: 50% 35%;
  transform: translate(var(--pan-x), var(--pan-y)) scale(var(--zoom));
  transition: transform 0.35s ease;
}

.board-scene.no-transition {
  transition: none;
}

.board {
  display: grid;
  grid-template-columns: repeat(13, var(--cell));
  grid-template-rows: repeat(19, var(--cell));
  /* The board lies on a leaned-back table. --offset-x/y is where its center sits on the table,
     measured from the point the camera looks at, and --spin turns it. app.js sets all three. */
  transform: rotateX(var(--tilt)) translate(var(--offset-x, 0px), var(--offset-y, 0px)) rotateZ(var(--spin));
  transform-style: preserve-3d;
  background: var(--floor);
  box-shadow: 0 0 0 calc(var(--cell) * 0.25) #191c24;
  /* The finger drags the shape on the board, so the browser must not pan or zoom here. */
  touch-action: none;
  user-select: none;
  /* The tiles lie in the same plane as the board, so the board must not take taps meant for them.
     Taps on tiles still bubble up to the board's listeners. */
  pointer-events: none;
}

.tile {
  position: relative;
  transform-style: preserve-3d;
  background: var(--floor);
  box-shadow: inset 0 0 0 0.5px var(--floor-line);
  pointer-events: auto;
}

/* While the player drags the dimension shape, only the floor tiles answer "what is under the
   finger". Raised blocks and figures stand in front of the tiles behind them and would give a
   different tile from one moment to the next. */
.board.dragging .block,
.board.dragging .figure {
  pointer-events: none;
}

/* A raised block. Its own element is the top face. */
.block {
  position: absolute;
  inset: 0;
  transform: translateZ(var(--block-height));
  transform-style: preserve-3d;
}

.block.dais {
  transform: translateZ(var(--dais-height));
}

.block-side {
  position: absolute;
}

/* The face toward the player, folded down from the bottom edge of the top face. */
.block-side.front {
  left: 0;
  top: 100%;
  width: 100%;
  height: var(--block-height);
  transform-origin: top;
  transform: rotateX(-90deg);
}

/* The far side, folded down from the top edge of the top face. */
.block-side.back {
  left: 0;
  bottom: 100%;
  width: 100%;
  height: var(--block-height);
  transform-origin: bottom;
  transform: rotateX(90deg);
}

.block-side.east {
  top: 0;
  left: 100%;
  width: var(--block-height);
  height: 100%;
  transform-origin: left;
  transform: rotateY(90deg);
}

.block-side.west {
  top: 0;
  right: 100%;
  width: var(--block-height);
  height: 100%;
  transform-origin: right;
  transform: rotateY(-90deg);
}

.block.dais .block-side.front,
.block.dais .block-side.back {
  height: var(--dais-height);
}

.block.dais .block-side.east,
.block.dais .block-side.west {
  width: var(--dais-height);
}

.block.owner-0 { background: var(--block-0-top); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25); }
.block.owner-0 .front,
.block.owner-0 .back { background: var(--block-0-front); }
.block.owner-0 .east,
.block.owner-0 .west { background: var(--block-0-side); }

.block.owner-1 { background: var(--block-1-top); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25); }
.block.owner-1 .front,
.block.owner-1 .back { background: var(--block-1-front); }
.block.owner-1 .east,
.block.owner-1 .west { background: var(--block-1-side); }

.block.dais { background: var(--dais-top); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.3); }
.block.dais .front,
.block.dais .back { background: var(--dais-front); }
.block.dais .east,
.block.dais .west { background: var(--dais-side); }

/* The King's Heart Points, drawn on the side of its dais that faces the player.
   app.js sets data-facing on the board as the board turns. */
.dais-hearts {
  display: none;
}

.board[data-facing="front"] .block-side.front .dais-hearts,
.board[data-facing="back"] .block-side.back .dais-hearts,
.board[data-facing="east"] .block-side.east .dais-hearts,
.board[data-facing="west"] .block-side.west .dais-hearts {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0;
  /* The same size on all 4 sides: 3 square slots of 0.95em fit the length of a side (one tile). */
  font-size: calc(var(--cell) * 0.33);
  line-height: 1;
}

/* The east and west sides are folded down on their side, so their hearts line up along the edge
   (a column in the side's own frame) and each heart is turned back upright. */
.board[data-facing] .block-side.east .dais-hearts,
.board[data-facing] .block-side.west .dais-hearts {
  flex-direction: column;
}

/* Each heart sits in a square slot that turns it upright on its side of the dais. A heart's own box
   is taller than it is wide, and turning it does not change the space it takes, so a square slot
   makes all 4 sides use the same space. The fade animation scales the heart inside the slot. */
.heart-slot {
  width: 0.95em;
  height: 0.95em;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.heart-slot .heart {
  display: inline-block;
}

.block-side.east .heart-slot {
  transform: rotate(-90deg);
}

.block-side.west .heart-slot {
  transform: rotate(90deg);
}

/* The far side is folded the other way, so its hearts would show upside down. */
.block-side.back .heart-slot {
  transform: rotate(180deg);
}

/* Hearts, on the King's dais and in the player strips. A lost heart fades to grey. */
.heart {
  color: #ff5a6a;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);
}

.heart.lost {
  color: #6b7080;
  text-shadow: none;
}

.heart.fading {
  color: #6b7080;
  text-shadow: none;
  animation: heart-fade 1.2s ease both;
}

@keyframes heart-fade {
  from {
    color: #ff5a6a;
    transform: scale(1.25);
  }
  to {
    color: #6b7080;
    transform: scale(1);
  }
}

/* ---------- Unit actions on the board ---------- */

/* The unit the player chose in the unit menu.
   [ZeroLabs] 2026-10-26: the whole square tile under the unit is lit (Zero), not a white outline,
   so it never looks like the white "chosen tile" mark.
   [ZeroLabs] 2026-10-26: the same color as the move tiles (Zero), now off-white. */
.block.selected::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(240, 236, 222, 0.7);
}

/* [ZeroLabs] 2026-10-26: every tile highlight fills the whole square tile in its color (Zero: no
   rounded corners, and the whole tile, not only its edge). */

/* Tiles the selected unit can move to (raised path tiles and flat tiles alike). */
.highlight-move::after {
  content: "";
  position: absolute;
  inset: 0;
  /* [ZeroLabs] 2026-10-26: off-white, strong enough that the tile color under it does not muddy
     it (Zero; was green). Move tiles do not blink; the see-through copy blinks instead. */
  background: rgba(240, 236, 222, 0.7);
}

/* Targets the selected unit can attack. */
.highlight-target::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(255, 154, 60, 0.5);
  animation: highlight-pulse 0.9s ease-in-out infinite;
}

/* Chests the selected unit can open. */
.highlight-chest::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(243, 197, 74, 0.5);
  animation: highlight-pulse 1.2s ease-in-out infinite;
}

/* [ZeroLabs] 2026-10-25: units an Effect can be used on. */
.highlight-effect::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(180, 140, 255, 0.5);
  animation: highlight-pulse 1.1s ease-in-out infinite;
}

/* [ZeroLabs] 2026-10-25: the answers to a pick: warp exits, or enemy monsters to swap with. */
.highlight-pick::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(69, 214, 230, 0.5);
  animation: highlight-pulse 1.1s ease-in-out infinite;
}

/* [ZeroLabs] 2026-10-25: an open Warp Vortex, lying flat on its tile and turning slowly. */
.vortex-swirl {
  position: absolute;
  inset: 12%;
  border-radius: 50%;
  background:
    radial-gradient(circle, #0b0d12 0 18%, transparent 19%),
    conic-gradient(#45d6e6, #7a4dff, #45d6e6, #7a4dff, #45d6e6);
  box-shadow: 0 0 6px rgba(69, 214, 230, 0.8);
  animation: vortex-turn 3s linear infinite;
  pointer-events: none;
}

@keyframes vortex-turn {
  to { transform: rotate(360deg); }
}

/* The tile the player picked, waiting for Confirm.
   [ZeroLabs] 2026-10-26: a solid off-white (Zero). The old see-through white turned salmon over the
   pink tiles. It is more solid than the off-white move tiles around it. */
.highlight-chosen::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(250, 248, 240, 1);
  animation: none;
}

/* The see-through copy of a unit on its proposed tile.
   [ZeroLabs] 2026-10-26: it blinks, so the player sees where the unit will go (Zero). */
.figure-preview {
  opacity: 0.5;
  pointer-events: none;
  /* [ZeroLabs] 2026-10-26: slower (Zero; was 1.2s). */
  animation: preview-pulse 2.4s ease-in-out infinite;
}

@keyframes preview-pulse {
  /* [ZeroLabs] 2026-10-26: a smaller blink (Zero; was 0.75 to 0.2). */
  0%, 100% { opacity: 0.75; }
  50% { opacity: 0.45; }
}

/* While choosing a move or a target, no figure takes taps, so a tap always reaches the tile the
   player sees under the finger. A press on the selected unit is found by its position instead
   (see isPressOnSelectedUnit in app.js), so the unit can still be dragged. */
.board.choosing .figure {
  pointer-events: none;
}

@keyframes highlight-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}

/* The dimension preview: a see-through block. */
.block.ghost-ok,
.block.ghost-ok .block-side {
  background: var(--ghost-ok);
}

.block.ghost-bad,
.block.ghost-bad .block-side {
  background: var(--ghost-bad);
}

.block.ghost-summon::after {
  content: "";
  position: absolute;
  inset: 18%;
  border: 2px dashed rgba(255, 255, 255, 0.9);
  border-radius: 50%;
}

/* An upright figure standing on the middle of a block. */
.figure {
  position: absolute;
  left: -10%;
  right: -10%;
  bottom: 45%;
  height: 130%;
  transform-origin: bottom center;
  /* Undo the board's turn and tilt, in reverse order, so the figure faces the player. */
  transform: rotateZ(calc(var(--spin) * -1)) rotateX(calc(var(--tilt) * -1));
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

/* [ZeroLabs] 2026-10-26: a unit stands ON its ringed base, not inside it. The base's top layer is
   3px above the tile (.mini-base-layer.layer-2), so the unit is lifted the same 3px first. Before,
   the ring cut off the bottom of the icon and the unit looked like it floated (Zero). */
/* [ZeroLabs] 2026-10-26: a chest stands on its ringed base the same way (Zero). */
.figure.figure-monster,
.figure.figure-chest {
  /* [ZeroLabs] 2026-10-26: translateY slides the feet toward the viewer, onto the front half of the
     ring, so more of the body shows over it (Zero). It comes after the turn is undone, so the
     slide always points at the viewer, at every board angle. The feet stay on the ring's top. */
  transform: translateZ(3px) rotateZ(calc(var(--spin) * -1)) translateY(calc(var(--cell) * 0.15)) rotateX(calc(var(--tilt) * -1));
  /* [ZeroLabs] 2026-10-26: the feet stand on the exact center of the tile and the ring (was 45%).
     The unit turns around its feet when the board turns, so an off-center point made it look
     higher at some angles and lower at others (Zero). */
  bottom: 50%;
}

.figure-glyph {
  font-size: calc(var(--cell) * 1.05);
  line-height: 1;
  text-shadow: 0 2px 2px rgba(0, 0, 0, 0.6);
}

.figure-king .figure-glyph {
  font-size: calc(var(--cell) * 0.9);
}

/* [ZeroLabs] 2026-10-26: the King lies flat on its tile, like a picture on the platform, and does
   not stand up like the units (Zero). It is fixed to the tile: it turns with the board, the same
   way the tile does (Zero). */
.figure.figure-king {
  inset: 0;
  height: auto;
  transform-origin: center;
  transform: none;
  align-items: center;
}

/* [ZeroLabs] 2026-10-26: the second player's King (the top of the board) is turned around, so each
   crown faces its own side (Zero). */
.figure.figure-king.owner-1 {
  transform: rotate(180deg);
}

.figure-king .figure-glyph.symbol-glyph svg {
  /* A flat picture has no height, so a smaller shadow right under it. */
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
}

.figure-king.owner-0 .figure-glyph {
  color: #ff6a6f;
  /* [ZeroLabs] 2026-10-26: the crown icon is filled with --faction, so the King uses its owner's color. */
  --faction: #ff6a6f;
}

.figure-king.owner-1 .figure-glyph {
  color: #6aa6ff;
  --faction: #6aa6ff;
}

/* ---------- Miniatures ---------- */

/* The round base lies flat on the block top. Its layers are lifted a little each, so it has thickness.
   The top layer's ring shows the owner. */
.mini-base {
  position: absolute;
  inset: 10%;
  transform-style: preserve-3d;
}

.mini-base-layer {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: #15171d;
}

.mini-base-layer.layer-0 { transform: translateZ(1px); }
.mini-base-layer.layer-1 { transform: translateZ(2px); }

.mini-base-layer.layer-2 {
  transform: translateZ(3px);
  background: #2a2e38;
  border: 2px solid transparent;
}

.mini-base.owner-0 .layer-2 { border-color: #ffb3b6; }
.mini-base.owner-1 .layer-2 { border-color: #b3cdff; }

/* The figure is drawn in the faction color, with one thin darker edge and a light drop shadow for depth. */
.mini-glyph {
  color: var(--faction);
  -webkit-text-stroke: 0.5px rgba(0, 0, 0, 0.45);
  text-shadow:
    1px 1px 0 color-mix(in srgb, var(--faction) 70%, black),
    0 2px 2px rgba(0, 0, 0, 0.35);
}

.figure-king .figure-glyph {
  -webkit-text-stroke: 0.6px rgba(0, 0, 0, 0.55);
}

/* [ZeroLabs] 2026-10-26: a chest on the board: the treasure-chest-outline icon (Zero). */
.figure-chest .chest-icon {
  /* [ZeroLabs] 2026-10-26: 80% of the old size (Zero; was 70%). */
  width: 56%;
  /* [ZeroLabs] 2026-10-26: 2px lower, onto the ring (Zero; was 2%). */
  margin-bottom: calc(2% - 2px);
}

/* The chest icon, on the board and in the portrait.
   [ZeroLabs] 2026-10-26: brown wood in the lid and body (Zero: they were see-through), inside a
   gold frame with a thin dark edge. */
.chest-icon svg {
  display: block;
  width: 100%;
  height: auto;
  filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.5));
}

.chest-icon .chest-wood {
  fill: #7a5126;
}

.chest-icon .chest-frame {
  fill: #c9a35a;
  stroke: #4a3115;
  stroke-width: 0.6;
  paint-order: stroke;
}

/* ---------- Panel content ---------- */

.panel-title {
  font-weight: 700;
  font-size: 15px;
  margin: 2px 0 8px;
  white-space: nowrap;
}

.panel-hint {
  color: var(--text-dim);
  font-size: 13px;
  margin: 0 0 10px;
}

.panel-error {
  color: var(--danger);
  font-size: 13px;
  margin: 6px 0;
  min-height: 1em;
}

.panel-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 10px;
}

.die-faces {
  display: flex;
  gap: 3px;
}

/* ---------- Crest symbols ---------- */

.crest-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.4em;
  height: 1.4em;
  border-radius: 4px;
  color: #0b0d12;
  /* [ZeroLabs] 2026-10-27: centered on the text line (Zero: "middle" sat too low next to text).
     -0.3em puts the middle of the 1.4em icon level with the middle of the capital letters. */
  vertical-align: -0.3em;
  flex: 0 0 auto;
}

.crest-icon svg {
  width: 78%;
  height: 78%;
  fill: currentColor;
}

.crest-icon.crest-SUMMON { background: var(--crest-summon); color: rgba(11, 13, 18, 0.8); }
.crest-icon.crest-MOVE { background: var(--crest-move); }
.crest-icon.crest-ATTACK { background: var(--crest-attack); }
.crest-icon.crest-DEFENSE { background: var(--crest-defense); }
.crest-icon.crest-MAGIC { background: var(--crest-magic); }
.crest-icon.crest-TRAP { background: var(--crest-trap); }

/* The summon level sits on top of the die symbol. */
.crest-level {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.7em;
  font-weight: 800;
  color: var(--crest-summon);
  line-height: 1;
}

/* "x2" or "x3" on a multi-crest face, as in the GBA game: inside the symbol's bottom-right corner,
   on a solid background that covers that corner. Its z-index keeps it above every gold or silver
   glow, including the glow of the face next to it. It scales with the symbol, so on a small phone
   it stays in the corner; the text never gets smaller than 6px. */
.crest-count-badge {
  position: absolute;
  right: 0;
  bottom: 0;
  z-index: 2;
  font-size: max(0.42em, 6px);
  font-weight: 800;
  line-height: 1;
  padding: 0.04em 0.1em;
  border-radius: 0.3em 0 0.3em 0;
  letter-spacing: -0.02em;
  background: #0b0d12;
  color: #ffffff;
  white-space: nowrap;
  pointer-events: none;
}

.crest-icon.rolled {
  outline: 2px solid #ffffff;
  outline-offset: 1px;
}

.die-faces .crest-icon {
  width: 22px;
  height: 22px;
  font-size: 15px;
}

.btn-icon {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

/* ---------- Dice selection: Goal, Backup, slots ---------- */

.goal-row {
  display: flex;
  gap: 8px;
  align-items: stretch;
  margin-bottom: 10px;
}

.goal-box {
  flex: 1;
  min-width: 0;
  font: inherit;
  color: var(--text);
  text-align: left;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 6px 8px;
  cursor: pointer;
  box-shadow: var(--inset);
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.goal-box-label {
  font-size: 11px;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.goal-box-value {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  white-space: nowrap;
}

.slot-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 4px;
}

/* A roll slot: a hexagon die (see .hex-die) with the die's name under it. The slot itself has no
   box; the hexagon is the shape the player sees and taps. */
.slot {
  font: inherit;
  color: var(--text);
  background: none;
  border: none;
  padding: 4px 0;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

/* What the Goal (gold) and the Backup (silver) are aiming at.
   Both glows have the same width; only the color differs, and the silver is a little dimmer. */
.goal-box.glow-goal {
  border-color: #ffd34d;
  box-shadow: var(--inset), 0 0 0 1px #ffd34d, 0 0 12px rgba(255, 211, 77, 0.55);
}

.goal-box.glow-backup {
  border-color: rgba(214, 220, 230, 0.85);
  box-shadow: var(--inset), 0 0 0 1px rgba(214, 220, 230, 0.85), 0 0 12px rgba(214, 220, 230, 0.4);
}

/* The Goal and Backup labels take their box's color too. */
.goal-box.glow-goal .goal-box-label {
  color: #ffd34d;
}

.goal-box.glow-backup .goal-box-label {
  color: rgba(214, 220, 230, 0.95);
}

.crest-icon.glow-goal {
  box-shadow: 0 0 0 2px #ffd34d, 0 0 8px rgba(255, 211, 77, 0.9);
}

.crest-icon.glow-backup {
  box-shadow: 0 0 0 2px rgba(214, 220, 230, 0.85), 0 0 8px rgba(214, 220, 230, 0.6);
}

/* The goal chance line, with room between it and the dice slots above. */
.goal-chance {
  margin-top: 16px;
}

.slot-empty-text {
  color: var(--text-dim);
  font-size: 13px;
}

.slot-name {
  display: block;
  width: 100%;
  text-align: center;
  font-size: 12px;
  white-space: nowrap;
}

.goal-box-text {
  min-width: 0;
  flex: 1;
}

/* ---------- The hexagon die ----------
   A die seen from above: the monster or chest in the middle, its six faces at the six corners.
   --hex sets everything, so the same markup works in a roll slot and in the die browser.
   In a slot the hexagon takes the slot's width, up to 160px. The box has the hexagon's own shape
   (a pointy-top hexagon is 0.866 times as wide as it is tall), so its flat sides fill the width.
   cqw below is a share of this width. The miniature and the faces are sized on their own, so a
   bigger hexagon gives them more room instead of making them bigger. */
.hex-die {
  --hex: min(100%, 160px);
  position: relative;
  width: var(--hex);
  aspect-ratio: 0.866;
  flex: 0 0 auto;
  container-type: inline-size;
}

/* In the die browser: 260px, or less when the window is narrower, so it never forces a scroll. */
/* [ZeroLabs] 2026-11-01: the roll slots' turning 3D die in place of the hexagon, the same size.
   The Goal and Backup glows show as a soft light around the die. */
.die3d {
  width: min(100%, 160px);
  aspect-ratio: 1;
  margin: 0 auto;
}

.die3d.die3d-large {
  width: min(260px, 100%);
}

.die3d canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* [ZeroLabs] 2026-11-01: every chosen die in a roll slot glows warm (Zero), on the hexagon and on
   the 3D die alike. */
.hex-die.glow-chosen .hex-outline .hex-outer {
  stroke: #ffc94d;
  stroke-width: 2.5;
  filter: drop-shadow(0 0 5px rgba(255, 190, 70, 0.85));
}

.die3d.glow-chosen canvas {
  filter: drop-shadow(0 0 12px rgba(255, 190, 70, 0.8));
}

.die3d.glow-goal canvas {
  filter: drop-shadow(0 0 10px rgba(255, 211, 77, 0.75));
}

.die3d.glow-backup canvas {
  filter: drop-shadow(0 0 10px rgba(214, 220, 230, 0.6));
}

.hex-die.hex-large {
  --hex: min(260px, 100%);
  width: min(260px, 100%);
}

/* The text under the large hexagon lines up with the hexagon's left and right edges. */
.hex-browser > :not(.hex-die) {
  width: min(260px, 100%);
}

.hex-outline {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* The outer edge, the six ring pieces (one per face), and the middle hexagon. */
.hex-outline .hex-outer {
  fill: none;
  stroke: var(--border);
  stroke-width: 1.5;
}

.hex-outline .hex-piece {
  fill: #2c3140;
  stroke: var(--border);
  stroke-width: 1;
}

.hex-outline .hex-inner {
  fill: var(--surface-raised);
  stroke: var(--border);
  stroke-width: 1;
}

.hex-die.hex-empty .hex-outline polygon {
  fill: none;
  stroke-dasharray: 3 2;
}

/* The die glows along its outer edge: gold for the Goal, silver for the Backup, same width. */
.hex-die.glow-goal .hex-outline .hex-outer {
  stroke: #ffd34d;
  stroke-width: 2.5;
  filter: drop-shadow(0 0 4px rgba(255, 211, 77, 0.8));
}

.hex-die.glow-backup .hex-outline .hex-outer {
  stroke: rgba(214, 220, 230, 0.85);
  stroke-width: 2.5;
  filter: drop-shadow(0 0 4px rgba(214, 220, 230, 0.6));
}

/* Each ring piece is filled with its crest's color, with a dark border between pieces. */
.hex-outline .hex-piece.crest-SUMMON { fill: var(--crest-summon); }
.hex-outline .hex-piece.crest-MOVE { fill: var(--crest-move); }
.hex-outline .hex-piece.crest-ATTACK { fill: var(--crest-attack); }
.hex-outline .hex-piece.crest-DEFENSE { fill: var(--crest-defense); }
.hex-outline .hex-piece.crest-MAGIC { fill: var(--crest-magic); }
.hex-outline .hex-piece.crest-TRAP { fill: var(--crest-trap); }

.hex-outline .hex-piece[class*="crest-"] {
  stroke: #12141a;
  stroke-width: 1.4;
}

/* A ring piece whose crest a crest goal wants: outlined and glowing in the goal's color.
   It is drawn on top of its neighbors so the whole outline shows. */
.hex-outline .hex-piece.glow-goal {
  stroke: #ffd34d;
  stroke-width: 2;
  filter: drop-shadow(0 0 2.5px rgba(255, 211, 77, 0.95));
}

.hex-outline .hex-piece.glow-backup {
  stroke: rgba(214, 220, 230, 0.95);
  stroke-width: 2;
  filter: drop-shadow(0 0 2.5px rgba(214, 220, 230, 0.8));
}

/* In a ring piece, the symbol sits straight on the piece's color, with no square of its own. */
.hex-die .hex-face {
  background: none;
}

/* The miniature or chest in the middle hexagon. cqw is a share of the hexagon's width. */
.hex-die .portrait-hex {
  --size: 36cqw;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

/* One face, in the middle of its ring piece. JavaScript sets left and top. */
.hex-die .hex-face {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 14.5cqw;
  height: 14.5cqw;
  font-size: 10.5cqw;
  border-radius: 3px;
}

/* ---------- Portraits ---------- */

/* A portrait is the board miniature drawn flat: the shaded figure standing on an oval base.
   --size sets everything, so the same markup works at every size. */
.portrait {
  --size: 40px;
  position: relative;
  width: var(--size);
  height: var(--size);
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  line-height: 1;
}

/* [ZeroLabs] 2026-11-02: a portrait's 3D unit (die-viewer.js createUnit) fills the portrait, its feet
   on the ringed base; the unit stands in front of the base. */
.portrait-3d .unit3d {
  position: absolute;
  left: -10%;
  right: -10%;
  /* [ZeroLabs] 2026-11-02: 6% -> 0%: no base under it any more (below). */
  bottom: 0;
  height: 120%;
  z-index: 1;
  pointer-events: none;
}

/* [ZeroLabs] 2026-11-02: no round base under a 3D unit (Zero: not needed any more). */
.portrait-3d .portrait-base {
  display: none;
}

/* [ZeroLabs] 2026-11-02: (Zero: keep the owner's ring for a unit on the board) the unit menu's summary,
   opened on a unit on the board, keeps the ringed base in the owner's color, with the unit on it. */
.unit-summary .portrait-3d .portrait-base {
  display: block;
}

.unit-summary .portrait-3d .unit3d {
  bottom: 6%;
}

.portrait-3d .unit3d canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* [ZeroLabs] 2026-11-02: (Zero: too large on the die details page) in the hexagon's center the unit
   stays inside its portrait (about 70% of the size it has elsewhere). */
/* [ZeroLabs] 2026-11-02: (Zero: the head was cut off, and the unit started at the middle) in the
   hexagon, the 3D portrait fills the inner hexagon (56% of the die's width, was the 36% glyph slot)
   and the whole unit fits inside it, centered on the die. */
.hex-die .portrait-hex.portrait-3d {
  /* [ZeroLabs] 2026-11-02: 56 -> 46: inside the inner hexagon (Zero: way too big). */
  /* [ZeroLabs] 2026-11-02: 46 -> 54: larger again (Zero), now that the head always fits its picture. */
  --size: 54cqw;
}

/* [ZeroLabs] 2026-11-02: a centered portrait (the summon cards, Zero) gets the hexagon's frame: the
   picture fills the portrait exactly, not 120% of it from the bottom. */
.portrait-centered .unit3d,
.hex-die .portrait-3d .unit3d {
  left: 0;
  right: 0;
  bottom: 0;
  height: 100%;
  /* [ZeroLabs] 2026-11-02: the unit in the hexagon takes taps and drags (it spins, Zero). */
  pointer-events: auto;
}

/* [ZeroLabs] 2026-11-02: a summon card's unit does not spin, so taps go to the card. */
.summon-card .portrait-centered .unit3d {
  pointer-events: none;
}

/* [ZeroLabs] 2026-11-02: an item's chest gets the units' treatment (Zero): no round base under it; the
   chest is centered instead of pulled down onto the base. The tiny Goal icon keeps its base. */
.portrait-item:not(.portrait-tiny) .portrait-base {
  display: none;
}

.portrait-item:not(.portrait-tiny) {
  justify-content: center;
}

.portrait-item:not(.portrait-tiny) .portrait-chest {
  margin-bottom: 0;
}

.portrait-tiny { --size: 22px; }
.portrait-small { --size: 44px; }
.portrait-large { --size: 104px; }

.portrait-glyph {
  position: relative;
  z-index: 1;
  font-size: calc(var(--size) * 0.82);
  /* [ZeroLabs] 2026-10-26: lowered twice (was -0.12) so the figure stands on its ringed base
     instead of floating over it (Zero). About 9px lower in the large portrait. */
  margin-bottom: calc(var(--size) * -0.21);
}

/* The base: an oval with a darker lower edge for thickness, ringed in the owner's color. */
.portrait-base {
  width: 84%;
  height: 26%;
  border-radius: 50%;
  background: #2a2e38;
  border: 2px solid transparent;
  box-shadow: 0 calc(var(--size) * 0.05) 0 #15171d, 0 calc(var(--size) * 0.08) calc(var(--size) * 0.06) rgba(0, 0, 0, 0.55);
}

.portrait-base.owner-0 { border-color: #ffb3b6; }
.portrait-base.owner-1 { border-color: #b3cdff; }

.portrait-tiny .portrait-base {
  border-width: 1px;
}

/* [ZeroLabs] 2026-10-26: the chest in a portrait is the same icon as on the board, standing on the
   same ringed base as a unit (Zero). The icon has an empty band under the chest, so it is pulled
   down onto the base (was -0.06, over a shadow). */
.portrait-chest {
  position: relative;
  z-index: 1;
  /* [ZeroLabs] 2026-10-26: 80% of the old size (Zero; was 74%), and pulled down by 80% as much. */
  width: 59%;
  margin-bottom: calc(var(--size) * -0.145);
}

/* ---------- Unit menu ---------- */

.menu-option {
  font: inherit;
  color: var(--text);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
  cursor: pointer;
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 2px;
  box-shadow: var(--inset);
}

.menu-option:hover:not(:disabled) {
  border-color: var(--accent);
}

.menu-option:disabled {
  opacity: 0.5;
  cursor: default;
}

.menu-option-label {
  font-size: 15px;
  font-weight: 600;
}

/* Why a greyed-out option cannot be used right now. */
.menu-option-reason {
  display: block;
  width: 100%;
  font-size: 12px;
  color: var(--text-dim);
  white-space: nowrap;
}

.unit-summary {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 6px;
}

.unit-summary .portrait-large {
  --size: 56px;
}

.unit-summary-lines {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.unit-summary .details-line {
  text-align: left;
}

/* A checkbox with its label, for settings like automatic defense. */
/* [ZeroLabs] 2026-10-27: the Shield Wall "guarded" shield, at the top right of a guarded unit on
   the board, and before the explanation in the unit details. */
.guard-badge {
  position: absolute;
  top: 18%;
  right: 16%;
  width: calc(var(--cell) * 0.32);
  height: calc(var(--cell) * 0.32);
}

.guard-badge svg,
.guard-badge-inline svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: #e8eef7;
  stroke: #2a3a55;
  stroke-width: 1.5;
  paint-order: stroke;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.6));
}

.guard-badge-inline {
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
}

/* [ZeroLabs] 2026-10-27: a row of crest icons centered in its window (the Crest Up confirmation). */
.crest-row-centered {
  justify-content: center;
}

/* [ZeroLabs] 2026-10-26: TEST ONLY: a faction name over its units in the Test summon list. */
.test-summon-heading {
  margin-top: 8px;
  font-weight: 700;
  color: var(--text-dim);
  text-align: left;
}

.toggle-row {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 14px;
  color: var(--text);
  text-align: left;
  cursor: pointer;
  margin-top: 8px;
}

.toggle-row input {
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
}

/* The bonus crest buttons in their window. */
#bonus-box .tool-button {
  height: 72px;
}

#bonus-box .tool-button .crest-icon {
  font-size: 24px;
}

/* ---------- Picker (Goal list, die browser) ---------- */

.overlay-dim {
  background: rgba(8, 9, 13, 0.75);
}

/* Pickers use the phone's width: little padding around and inside the window. */
.overlay-dim {
  padding: 8px;
}

.picker-box {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-height: 90vh;
  /* [ZeroLabs] 2026-10-27: never taller than the space left beside a lit strip (updateLitStrip). */
  max-height: min(90vh, 100%);
  padding: 16px 12px;
}

/* [ZeroLabs] 2026-10-26: the picker title, with Previous and Next on both sides in the die
   browser. The title shrinks its text to fit between them (fitTextToWidth), and never wraps. */
.picker-title-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.picker-title-row .overlay-title {
  flex: 1;
  min-width: 0;
}

.picker-title-nav {
  flex: none;
}

/* [ZeroLabs] 2026-10-26: the die browser keeps one height for every die, so a long text does not
   make the box jump while the player moves through the dice (Zero). The details scroll inside it,
   and the footer (Select, Close) stays at the bottom. dvh follows the phone's visible height. */
.picker-box.picker-fixed {
  height: 90vh;
  height: 90dvh;
}

.picker-box.picker-fixed .picker-body {
  flex: 1;
  min-height: 0;
}

.picker-footer:empty {
  display: none;
}

.picker-footer .panel-actions {
  margin-top: 0;
}

.picker-footer {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.picker-body {
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.picker-option {
  font: inherit;
  font-size: 14px;
  color: var(--text);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 10px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 10px;
  text-align: left;
  white-space: nowrap;
}

.picker-option.selected {
  border-color: var(--accent);
  outline: 2px solid var(--accent);
}

.browser-view {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.browser-name {
  font-size: 18px;
  font-weight: 700;
  white-space: nowrap;
}

.browser-detail {
  font-size: 13px;
  color: var(--text-dim);
  text-align: center;
  white-space: pre-wrap;
}

.browser-view .die-faces .crest-icon {
  width: 30px;
  height: 30px;
  font-size: 20px;
}

.browser-nav {
  justify-content: center;
}

/* [ZeroLabs] 2026-10-26: buttons that share the full width of the row equally (the die browser's
   Select and Close). */
.split-row .btn {
  flex: 1;
}

/* ---------- Roll results ---------- */

.roll-results {
  display: flex;
  justify-content: center;
  gap: 14px;
  margin: 4px 0 10px;
}

.roll-results .crest-icon {
  width: 56px;
  height: 56px;
  font-size: 38px;
  border-radius: 10px;
  box-shadow: var(--inset), 0 3px 0 rgba(0, 0, 0, 0.35);
}

.summon-choices {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}

.summon-card {
  font: inherit;
  color: var(--text);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px 4px 8px;
  cursor: pointer;
  box-shadow: var(--inset);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.summon-card.highlighted {
  border-color: var(--accent);
  outline: 2px solid var(--accent);
}

.summon-card .portrait-large {
  --size: 76px;
}

/* [ZeroLabs] 2026-11-02: (Zero: the glass dice replace the cards) a summon choice has no card box,
   like a roll slot (.slot): the glass die is what the player sees and taps. The highlighted one glows
   warm, like a chosen die in the roll slots (.die3d.glow-chosen). */
.summon-card.summon-die {
  background: none;
  border: none;
  box-shadow: none;
  padding: 4px 0;
}

.summon-card.summon-die.highlighted {
  outline: none;
}

.summon-die.highlighted .die3d canvas {
  filter: drop-shadow(0 0 12px rgba(255, 190, 70, 0.8));
}

/* The details of the highlighted card, shown once under all the cards. */
.summon-details,
.browser-view {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
  min-width: 0;
}

.details-name {
  font-size: 16px;
  font-weight: 700;
  white-space: nowrap;
  width: 100%;
  text-align: center;
}

.details-line {
  font-size: 13px;
  color: var(--text-dim);
  white-space: pre;
  width: 100%;
  text-align: center;
}

/* [ZeroLabs] 2026-10-27: everything below the ATK/DEF/HP line is left-aligned (Zero): the passive
   traits, the skills, the item text, the status line, and the Guarded line. */
.details-boon {
  font-size: 13px;
  color: var(--text-dim);
  text-align: left;
  width: 100%;
}

.details-boon-name {
  color: var(--text);
  font-weight: 600;
}

/* [ZeroLabs] 2026-10-25: step 3. A stat raised by a buff (Fortify, War Banner, Fool's Blessing, Feast) is green. */
.stat-buffed {
  color: #5ee08a;
  font-weight: 700;
}

/* [ZeroLabs] 2026-10-27: a stat lowered by a debuff (Sunder) is red (Zero). */
.stat-debuffed {
  color: #ff6b6b;
  font-weight: 700;
}

/* A unit's lasting conditions (Snared, Fortified, ...). */
/* [ZeroLabs] 2026-10-27: one skill on its own, in a confirmation window, is centered (Zero). */
.details-boon.details-boon-centered {
  text-align: center;
}

.details-status {
  color: #f3c54a;
  /* [ZeroLabs] 2026-10-27: left-aligned, below the stat line (Zero). */
  text-align: left;
}

/* The crest cost after a boon's text: small crest symbols inside the line of text. */
.boon-cost {
  white-space: nowrap;
}

/* [ZeroLabs] 2026-10-27: the crest in a cost is the height of the text (Zero: it was bigger and hung
   below the line), raised so its middle is level with the middle of the text (Zero: -0.15em was
   still low). */
.boon-cost .crest-icon {
  width: 1em;
  height: 1em;
  margin: 0 1px;
  vertical-align: -0.05em;
}

/* [ZeroLabs] 2026-10-25: "Not built yet." after a skill whose rule comes in a later build part. */
.boon-not-built {
  color: #e8904a;
  font-style: italic;
}

/* What an opened chest held and did, in the middle of the screen for both players. It disappears
   by itself after a few seconds, or when tapped. */
.item-notice {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(320px, calc(100vw - 32px));
  z-index: 40;
  padding: 14px 16px;
  border-radius: 10px;
  background: rgba(12, 14, 20, 0.92);
  border: 2px solid #f3c54a;
  box-shadow: 0 0 18px rgba(243, 197, 74, 0.45);
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 6px;
  cursor: pointer;
}

.item-notice-title {
  font-size: 18px;
  font-weight: 700;
  color: #f3c54a;
  white-space: nowrap;
}

.item-notice-text {
  font-size: 13px;
  color: var(--text-dim);
}

.item-notice-result {
  font-size: 14px;
  color: var(--text);
}

/* [ZeroLabs] 2026-10-26: a rank icon (Material Design Icons), sized and shaded like the old chess
   glyphs. Every rank and the King use one now. */
.symbol-glyph svg {
  display: block;
  width: 0.9em;
  height: 0.9em;
  fill: var(--faction);
  stroke: rgba(0, 0, 0, 0.5);
  stroke-width: 0.6;
  filter:
    drop-shadow(1px 1px 0 color-mix(in srgb, var(--faction) 70%, black))
    drop-shadow(0 2px 2px rgba(0, 0, 0, 0.35));
}

/* [ZeroLabs] 2026-10-26: the General's tank, the same treatment as the chest (Zero): 80% of the
   size, and lowered onto the ring. The tank icon has an empty band under its tracks (5 of its 24
   units), so the icon is pulled down by that much. */
.symbol-glyph.rank-general svg {
  width: 0.72em;
  height: 0.72em;
  margin-bottom: -0.15em;
}

/* [ZeroLabs] 2026-10-26: the Assassin's knife, the same treatment (Zero): 80% of the size, and
   lowered onto the ring by its empty band (2 of its 24 units). */
.symbol-glyph.rank-assassin svg {
  width: 0.72em;
  height: 0.72em;
  margin-bottom: -0.06em;
}

/* ---------- Dimension controls ---------- */

.shape-thumb {
  padding: 0;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--inset);
}

.shape-thumb.selected {
  border-color: var(--accent);
  outline: 2px solid var(--accent);
}

.shape-thumb-grid {
  display: grid;
  gap: 1px;
}

.shape-thumb-cell {
  background: var(--text-dim);
  border-radius: 1px;
}

.shape-thumb-cell.summon {
  background: var(--crest-summon);
}

.tool-button {
  width: 58px;
  height: 58px;
  padding: 4px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
}

.tool-button svg {
  width: 24px;
  height: 24px;
  fill: currentColor;
}

/* A block with a width, so fitTextToWidth can shrink a long label to fit the button. */
.tool-label {
  display: block;
  width: 100%;
  text-align: center;
  font-size: 11px;
  white-space: nowrap;
}

.tool-button .crest-icon {
  font-size: 18px;
}

/* [ZeroLabs] 2026-10-28: Vs Bot (build step 4). The toast while the bot plays, in ColorBrawl's style
   (its spinner and border), at the top of the stage so it does not cover the bot's moves. */
.bot-toast {
  position: absolute;
  top: 8px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 10px;
  width: max-content;
  max-width: calc(100% - 16px);
  padding: 8px 10px 8px 14px;
  border-radius: 14px;
  background: var(--surface-raised);
  border: 1px solid rgba(108, 140, 255, 0.5);
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.55), var(--inset);
  color: var(--text);
}

.bot-toast-label {
  font-weight: 700;
  white-space: nowrap;
}

.bot-spinner {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 2.5px solid rgba(108, 140, 255, 0.25);
  border-top-color: var(--accent);
  animation: bot-spin 0.7s linear infinite;
}

@keyframes bot-spin {
  to { transform: rotate(360deg); }
}

/* While the bot plays, the person's controls do nothing. They stay visible, so the person can
   watch the bot's roll. Taps on the board only open details (app.js). */
.bot-busy #dice-screen,
.bot-busy #control-bar,
.bot-busy #shape-slider {
  pointer-events: none;
}

.bot-busy #dice-screen .btn,
.bot-busy #control-bar .btn {
  opacity: 0.55;
}

/* The pass card between turns (ColorBrawl's pass card). */
.pass-box {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.pass-line {
  margin: 0;
  color: var(--text);
}

/* [ZeroLabs] 2026-11-02: (Zero) a pass card line with its picture on the left: a still image of the
   unit, the chest icon, or a heart. */
.pass-line-row {
  display: flex;
  align-items: center;
  gap: 10px;
  text-align: left;
}

.pass-image {
  flex: none;
  width: 48px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.pass-image .unit3d,
.pass-image .unit3d canvas {
  display: block;
  width: 100%;
  height: 100%;
}

.pass-image .mini-glyph {
  font-size: 30px;
}

.pass-chest {
  width: 34px;
  height: 34px;
}

.pass-chest svg {
  width: 100%;
  height: 100%;
}

.pass-heart {
  width: 34px;
  height: 34px;
}

.pass-heart-lost path:first-child {
  fill: #e0565b;
}

.pass-heart-gained path:first-child {
  fill: #ff7a9a;
}

.pass-heart-crack {
  fill: none;
  stroke: var(--surface);
  stroke-width: 1.6;
  stroke-linejoin: round;
}

.pass-replay {
  position: absolute;
  top: 10px;
  right: 10px;
}

/* The title keeps clear of the Replay button on both sides, so it stays centered. */
.pass-box .overlay-title {
  padding: 0 64px;
}

/* [ZeroLabs] 2026-10-29: the 3D board (js/three/board3d.js) fills the board area; the CSS board is
   hidden once it runs. The view buttons and the notice stay on top of it. */
.board3d-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.board3d-on .board-scene {
  display: none;
}

/* [ZeroLabs] 2026-10-29: the King cut-in (js/three/king-cutin.js): its own full-screen layer above
   the whole game, and the black layer for its fades above that. The fade never takes taps. */
.cutin-canvas {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  z-index: 200;
  display: block;
}

.cutin-fade {
  position: fixed;
  inset: 0;
  background: #000;
  opacity: 0;
  pointer-events: none;
  z-index: 201;
}

/* [ZeroLabs] 2026-10-29: the dice screen fades away while the dice are thrown on the board, and
   fades back in with the results (Zero: the hard cut was rough). */
.dice-screen {
  transition: opacity 0.25s ease;
}

.dice-screen.fade-away {
  opacity: 0;
  pointer-events: none;
}

/* [ZeroLabs] 2026-10-29: the sideways fade to black when a roll starts (Zero). A wide band, black in
   the middle and soft at both edges, sweeps in from the right, covers the screen, and sweeps off to
   the left. The board throws the dice only after it is gone (ROLL_START_DELAY_SECONDS in
   board3d.js). It never takes taps. */
.roll-wipe {
  position: fixed;
  top: 0;
  left: 0;
  width: 180vw;
  height: 100vh;
  background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #000 15%, #000 85%, rgba(0, 0, 0, 0) 100%);
  transform: translateX(56%);
  pointer-events: none;
  z-index: 150;
}

.roll-wipe.playing {
  /* [ZeroLabs] 2026-10-29: slower (Zero: 0.75 s was too quick). */
  animation: roll-wipe 1.4s ease-in-out forwards;
}

/* [ZeroLabs] 2026-10-29: from right to left (Zero: flipped). */
@keyframes roll-wipe {
  0% { transform: translateX(56%); }
  45% { transform: translateX(-22%); }
  60% { transform: translateX(-22%); }
  100% { transform: translateX(-100%); }
}

/* [ZeroLabs] 2026-10-31: "Dice Roll!" in an action font right before the dice are thrown (Zero).
   It slams in while the sweep covers the screen and bursts away as the throw starts; it lasts as
   long as the sweep (1.4 s). The size follows the screen width, so it always fits. */
.roll-title {
  position: fixed;
  top: 50%;
  left: 50%;
  font-family: 'Bangers', 'Impact', sans-serif;
  font-size: min(18vw, 150px);
  letter-spacing: 0.04em;
  white-space: nowrap;
  color: #ffd84a;
  -webkit-text-stroke: 0.04em #3a1300;
  paint-order: stroke fill;
  /* [ZeroLabs] 2026-11-01: no blurred glow (Zero: glitchy). A blur on huge scaled text is slow to
     paint on every frame. The title gets its own layer (will-change). */
  text-shadow: 0.05em 0.06em 0 #b3280e;
  transform: translate(-50%, -50%) rotate(-6deg) scale(0);
  opacity: 0;
  will-change: transform, opacity;
  pointer-events: none;
  z-index: 151;
}

.roll-title.playing {
  animation: roll-title 1.4s linear forwards;
}

@keyframes roll-title {
  0% { transform: translate(-50%, -50%) rotate(-6deg) scale(2.6); opacity: 0; }
  30% { transform: translate(-50%, -50%) rotate(-6deg) scale(2.6); opacity: 0; animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1.2); }
  42% { transform: translate(-50%, -50%) rotate(-6deg) scale(1); opacity: 1; }
  80% { transform: translate(-50%, -50%) rotate(-4deg) scale(1.08); opacity: 1; animation-timing-function: ease-in; }
  100% { transform: translate(-50%, -50%) rotate(-4deg) scale(1.6); opacity: 0; }
}

/* [ZeroLabs] 2026-11-02: (Zero) the end title, in the "Dice Roll!" style: it pops in, holds while
   the winners jump, then fades; the result window comes after it (app.js END_TITLE_MS = 2.4 s).
   Gold for a win, cold blue for a defeat in a bot match. */
.roll-title.end-title.playing {
  animation: end-title 2.4s linear forwards;
}

.roll-title.end-title.defeat {
  color: #a9c2ee;
  -webkit-text-stroke: 0.04em #0b1630;
  text-shadow: 0.05em 0.06em 0 #2a3f73;
}

@keyframes end-title {
  0% { transform: translate(-50%, -50%) rotate(-6deg) scale(2.6); opacity: 0; animation-timing-function: cubic-bezier(0.2, 0.8, 0.3, 1.2); }
  14% { transform: translate(-50%, -50%) rotate(-6deg) scale(1); opacity: 1; }
  80% { transform: translate(-50%, -50%) rotate(-4deg) scale(1.06); opacity: 1; animation-timing-function: ease-in; }
  100% { transform: translate(-50%, -50%) rotate(-4deg) scale(1.4); opacity: 0; }
}

/* [ZeroLabs] 2026-10-29: the results panel without a summon is centered (Zero). */
/* [ZeroLabs] 2026-11-01: the skill demo's panel (?demo, js/demo.js): the dropdown, Play, and a line
   that says what to look for. It sits over the bottom of the board. */
/* [ZeroLabs] 2026-11-02: the Vs Bot opponent list (app.js openVsBotStart): one full-width button per
   bot, its name over its two faction colors and names, and a Tough tag. */
.bot-choices {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 8px;
}

.bot-choice {
  font: inherit;
  color: var(--text);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 9px 12px;
  cursor: pointer;
  box-shadow: var(--inset);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  text-align: left;
}

.bot-choice:hover {
  border-color: var(--accent);
}

.bot-choice-name {
  font-size: 16px;
  font-weight: 700;
}

.bot-choice-details {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 13px;
  color: var(--text-dim);
}

.bot-choice-swatch {
  width: 12px;
  height: 12px;
  border-radius: 3px;
}

.bot-choice-tough {
  font-size: 11px;
  font-weight: 700;
  padding: 1px 6px;
  border-radius: 4px;
  color: #2a1200;
  background: #ffb347;
}

/* [ZeroLabs] 2026-11-02: the bot difficulty buttons (app.js openWhoGoesFirst): one row, the picked one
   in the accent color. */
.difficulty-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  margin-bottom: 8px;
}

.difficulty-button.selected {
  background: var(--accent);
  border-color: var(--accent);
  color: #0b0d12;
  font-weight: 600;
}

/* [ZeroLabs] 2026-11-02: the King attack demo's timing slider, on its own row of the demo panel. */
.demo-tuning {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.demo-tuning input {
  flex: 1;
  min-width: 0;
}

.demo-tuning-label {
  flex: none;
  font-size: 13px;
  color: var(--text);
}

.demo-panel {
  position: fixed;
  left: 50%;
  bottom: 12px;
  transform: translateX(-50%);
  width: min(560px, calc(100vw - 32px));
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px;
  padding: 10px 12px;
  border-radius: 12px;
  background: rgba(20, 22, 30, 0.88);
  border: 1px solid var(--border);
  z-index: 140;
}

.demo-select {
  min-width: 0;
  padding: 6px;
  border-radius: 8px;
  background: var(--surface-raised);
  color: var(--text);
  border: 1px solid var(--border);
  font: inherit;
}

/* [ZeroLabs] 2026-11-01: the selected skill's description (Zero), above the note. */
.demo-description {
  grid-column: 1 / -1;
  color: var(--text);
  font-size: 13px;
}

.demo-description-name {
  font-weight: 700;
}

.demo-message {
  grid-column: 1 / -1;
  color: var(--text-dim);
  font-size: 13px;
}

.demo-message:empty {
  display: none;
}

/* [ZeroLabs] 2026-11-01: "You rolled" and the rolled crests in one row, the crests a little smaller
   (56 px -> 42 px) (Zero). "You rolled" stays on the left and the crests in the middle of the panel
   (Zero): two equal side columns keep the middle one centered. */
.rolled-header {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 10px;
  /* [ZeroLabs] 2026-11-01: 10 -> 18 px (Zero: a little more room above the unit cards). */
  margin-bottom: 18px;
}

.rolled-header .panel-title {
  margin: 0;
  justify-self: start;
  text-align: left;
}

.rolled-header .roll-results {
  gap: 10px;
  margin: 0;
}

.rolled-header .roll-results .crest-icon {
  width: 42px;
  height: 42px;
  font-size: 28px;
  border-radius: 8px;
  /* [ZeroLabs] 2026-11-01: the warm glow of the chosen dice in the roll slots (.die3d.glow-chosen),
     under the icon's own inset and drop shadows (Zero). */
  box-shadow: var(--inset), 0 3px 0 rgba(0, 0, 0, 0.35), 0 0 12px rgba(255, 190, 70, 0.8);
}

/* [ZeroLabs] 2026-11-01: the summon choice after a roll (Zero): the hint and the unit cards centered,
   and Summon and Skip summon side by side, each half of one row, at the bottom of the dice screen. */
.dice-screen-inner.summon-panel {
  flex: 1;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
}

.summon-panel .panel-hint {
  text-align: center;
  /* [ZeroLabs] 2026-11-01: it sits under the cards now, with room above it. */
  margin: 10px 0 0;
}

.summon-panel .summon-choices {
  display: flex;
  justify-content: center;
}

/* Each card keeps a third of the row, as in the old 3-column grid, so 2 cards stay the same size. */
.summon-panel .summon-card {
  flex: 0 1 calc((100% - 16px) / 3);
  min-width: 0;
}

.summon-panel .panel-actions {
  margin-top: auto;
  padding-top: 12px;
  flex-wrap: nowrap;
}

.summon-panel .panel-actions .btn {
  flex: 1 1 0;
  min-width: 0;
}

.dice-screen-inner.no-summon,
.no-summon .panel-title,
.no-summon .panel-hint {
  text-align: center;
}

.no-summon .panel-actions {
  justify-content: center;
}

/* [ZeroLabs] 2026-10-29: a crest flying from the roll results into the crest pool, and the bump of
   the pool's count when it lands. */
.crest-flyer {
  position: fixed;
  z-index: 140;
  pointer-events: none;
  transform-origin: center;
  margin: 0;
}

.crest-count.crest-gain .crest-icon {
  animation: crest-gain 0.4s ease-out;
}

@keyframes crest-gain {
  0% { transform: scale(1); }
  40% { transform: scale(1.45); }
  100% { transform: scale(1); }
}

/* [ZeroLabs] 2026-10-29: "-N" floating up off a crest pool count when it drops (moves, and every
   cost). */
.crest-drop {
  position: fixed;
  z-index: 141;
  pointer-events: none;
  color: #ff6b6b;
  font-weight: 800;
  font-size: 15px;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
  animation: crest-drop 0.9s ease-out forwards;
}

/* [ZeroLabs] 2026-11-02: it floats down from under the icon (the pools are at the top, Zero). */
@keyframes crest-drop {
  0% { transform: translateY(0); opacity: 0; }
  15% { opacity: 1; }
  100% { transform: translateY(22px); opacity: 0; }
}
