:root{
  --ink:#0E1417;
  --ink-2:#141C21;
  --panel:#18232A;
  --line:#26343C;
  --line-soft:#1E2A31;
  --sq-a:#1B262C;
  --sq-b:#1F2B32;
  --text:#D6E0E4;
  --dim:#7C8F99;
  /* The two sides wear the colours of the physical tiles: green and beige.
     --p0/--p1 are the light UI versions for text and rings on the dark panels;
     the --tile-* values are sampled from the token art itself so the CSS-drawn
     tiles (infantry, bases, unclaimed hulls) sit beside the PNG ones. */
  --p0:#A9C27A;         /* green side */
  --p0-deep:#3E4B2A;
  --p1:#DCCBA5;         /* beige side */
  --p1-deep:#4A3F26;
  --tile-g:#5A6B3C;--tile-g-ink:#EEF0E2;
  --tile-b:#D9C9A3;--tile-b-ink:#333615;
  /* A vehicle nobody crews is black. The tile is the dark hull's counter and
     its ink has to lift off it, so the ink is the LIGHT half of this pair -
     the reverse of the two sides, whose tiles are light and inked dark. */
  --tile-n:#15181B;--tile-n-ink:#8A9096;
  --board-frame:#42483A;--board-edge:#2A2F24;--axis:#C9CFB8;
  --alert:#FF6B57;
  --ok:#7BD88F;
  --warn:#E3B341;   /* the opponent's last move */
  --cell:30px;
  --display:"Barlow Condensed","Oswald","Arial Narrow",system-ui,sans-serif;
  --mono:"IBM Plex Mono",ui-monospace,"SF Mono",Menlo,monospace;
}

*{box-sizing:border-box}
html,body{height:100%}
body{
  margin:0;background:
    radial-gradient(1200px 700px at 20% -10%, #1A272E 0%, transparent 60%),
    var(--ink);
  color:var(--text);
  font-family:var(--display);
  font-size:16px;
  -webkit-font-smoothing:antialiased;
}
.hidden{display:none !important}
button{font-family:inherit}

/* ---------- lobby ---------- */
.screen{min-height:100vh}
#lobby{display:grid;place-items:center;padding:24px}
.lobby-card{max-width:520px;width:100%;text-align:left}
.eyebrow{
  font-family:var(--mono);font-size:11px;letter-spacing:.24em;text-transform:uppercase;
  color:var(--dim);margin:0 0 14px;
}
.lobby-card h1{
  font-size:clamp(52px,11vw,88px);line-height:.86;margin:0 0 18px;
  font-weight:600;letter-spacing:-.01em;text-transform:uppercase;
}
.lobby-card h1 span{display:block;color:var(--p0)}
.lede{color:var(--dim);font-size:18px;line-height:1.5;margin:0 0 28px;max-width:44ch}
.lede a{color:var(--p0);text-decoration:underline;text-underline-offset:3px}
.lede a:hover{color:#fff}
.lobby-actions{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
.join{display:flex;gap:8px}
#join-code{
  width:120px;padding:11px 14px;background:var(--ink-2);border:1px solid var(--line);
  color:var(--text);font-family:var(--mono);font-size:16px;letter-spacing:.3em;text-transform:uppercase;
  border-radius:2px;
}
#join-code:focus{outline:2px solid var(--p0);outline-offset:1px}

.btn{
  padding:11px 18px;background:transparent;border:1px solid var(--line);
  color:var(--text);font-size:15px;letter-spacing:.06em;text-transform:uppercase;
  cursor:pointer;border-radius:2px;transition:border-color .12s,background .12s,color .12s;
}
.btn:hover{border-color:var(--p0);color:#fff}
.btn:focus-visible{outline:2px solid var(--p0);outline-offset:2px}
.btn:disabled{opacity:.35;cursor:not-allowed}
.btn-primary{background:var(--p0);border-color:var(--p0);color:#1A2210;font-weight:600}
.btn-primary:hover{background:#C3D69A;color:#1A2210}
.btn-ghost{color:var(--dim)}
.wide{width:100%;margin-top:10px}
.msg{color:var(--alert);font-family:var(--mono);font-size:12px;min-height:16px;margin:14px 0 0}

/* the list of games anyone may watch */
.games{margin-top:32px}
.games-title{
  font-family:var(--mono);font-size:11px;letter-spacing:.24em;text-transform:uppercase;
  color:var(--dim);margin:0 0 10px;font-weight:400;
}
.games-list{list-style:none;padding:0;margin:0 0 10px;display:grid;gap:8px}
.games-list li{
  display:flex;align-items:center;gap:14px;padding:10px 12px;
  border:1px solid var(--line);border-radius:2px;font-family:var(--mono);font-size:12px;
}
.games-list .g-code{letter-spacing:.3em;color:var(--text)}
.games-list .g-meta{color:var(--dim);flex:1}

/* ---------- game shell ---------- */
.topbar{
  display:flex;align-items:center;gap:16px;padding:12px 20px;
  border-bottom:1px solid var(--line-soft);
}
.brand{text-transform:uppercase;letter-spacing:.16em;font-size:14px;color:var(--dim)}
.code-chip{font-family:var(--mono);font-size:12px;color:var(--dim);letter-spacing:.1em}
.code-chip b{color:var(--text);letter-spacing:.3em}
.turn-banner{
  margin-left:auto;font-size:15px;text-transform:uppercase;letter-spacing:.1em;
  padding:6px 14px;border:1px solid var(--line);border-radius:2px;
}
.turn-banner.you{border-color:var(--p0);color:var(--p0)}
.turn-banner.them{color:var(--dim)}
.btn-mini{padding:4px 10px;font-size:11px}

/* The clock sits between the code and the banner. It is the one thing on the
   bar that changes every second, so it is set in the mono face and kept small:
   a number to glance at, not a banner to read. */
.clock{
  margin-left:auto;display:flex;align-items:baseline;gap:8px;
  font-family:var(--mono);font-size:13px;color:var(--dim);letter-spacing:.06em;
  padding:6px 12px;border:1px solid var(--line);border-radius:2px;
}
.clock+.turn-banner{margin-left:0}
.clock b{font-size:18px;color:var(--text);font-weight:500;min-width:3.2em;text-align:right}
.clock.you b{color:var(--p0)}
.clock.low b{color:var(--alert)}
.clock .clock-who{text-transform:uppercase;font-size:11px}
.clock .clock-allow{font-size:11px}
.claim-bar{
  display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px;
  padding:10px 14px;border:1px solid var(--warn);border-radius:2px;color:var(--warn);
  font-size:14px;letter-spacing:.02em;
}
.claim-bar span{flex:1 1 auto}
/* The ask bar is the claim bar in the player's own colour: an offer, not a warning. */
.claim-bar.ask{border-color:var(--p0);color:var(--p0)}

.layout{display:flex;gap:24px;padding:20px;align-items:flex-start;flex-wrap:wrap}
/* The wrap is pinned to the board's own width. Without min-width:0 a long hint
   is treated as an unbreakable contribution to the flex item's size, so the wrap
   grows past the board and shoves the side panel off the row. */
/* `flex:0 0 auto` resolves the flex base to the item's MAX-CONTENT width, and
   max-content measures the hint paragraph as one unbroken line — so a long hint
   sized this column to the sentence rather than to the board and shoved the side
   panel off the row. `min-width` doesn't help: it constrains the minimum, not
   the content-derived base. `width:min-content` is the fix. The board is a
   `width:max-content` grid and therefore rigid, so min-content on the column
   resolves to exactly the board's width, and every other child — the hint, the
   animation note — is then a percentage of that and wraps inside it. */
.board-wrap{flex:0 0 auto;width:min-content;min-width:0;max-width:100%;position:relative}
.hint{
  font-family:var(--mono);font-size:12px;color:var(--dim);margin:12px 2px 0;min-height:16px;
  /* Wrap inside the board's width and never push anything: the hint is advice,
     not layout. overflow-wrap catches the occasional long unbroken token. */
  width:100%;max-width:100%;box-sizing:border-box;
  white-space:normal;overflow-wrap:anywhere;word-break:break-word;line-height:1.5;
}
/* Same reasoning: the note is a flex row and would otherwise measure its text as
   one line and re-widen the column the hint no longer does. */
.anim-note{width:100%;max-width:100%;box-sizing:border-box;flex-wrap:wrap}

/* ---------- board ---------- */
.board{
  /* Fixed tracks, not `auto`. An `auto` track sizes to its content AND stretches
     to fill spare space in the container, so the moment the board sat in a
     flex row wider than itself the 21 columns grew unevenly and the squares
     drifted apart. Pinning every track to --cell and letting the grid be
     max-content keeps the squares locked whatever the container does. */
  /* The sprite layer is an absolutely-positioned child, so the board has to be
     the positioning context it measures against. Out-of-flow children create no
     grid tracks, so this does not disturb the 21 columns. */
  position:relative;
  display:grid;
  grid-template-columns:repeat(21,var(--cell));
  grid-auto-rows:var(--cell);
  width:max-content;
  justify-content:start;align-content:start;
  background:var(--board-frame);border:1px solid var(--board-edge);padding:6px;border-radius:3px;
  user-select:none;
}
/* The terrain. One image under the whole 20x20 playing area rather than a
   background per square: the art is a single painted board whose dark grid
   lines fall exactly one --cell apart, so it is laid at the offset of the first
   square (the padding plus the axis column) and sized to twenty cells, and every
   DOM cell then sits over its own painted square. The cells themselves stay
   transparent; everything that used to be a cell background is an overlay now. */
.board::before{
  content:'';position:absolute;pointer-events:none;
  left:calc(6px + var(--cell));top:calc(6px + var(--cell));
  width:calc(var(--cell) * 20);height:calc(var(--cell) * 20);
  background:url(/assets/board.webp) center/100% 100% no-repeat;
}
/* Player 2 sees the board with the rows reversed, so the terrain is turned over
   with it — otherwise the painted grid would still line up but the ground under
   square A1 would change with the seat. Purely decorative either way. */
.board.flipped::before{transform:scaleY(-1)}
.axis{
  width:100%;height:var(--cell);display:grid;place-items:center;overflow:hidden;
  font-family:var(--mono);font-size:10px;color:var(--axis);
}
.cell{
  width:100%;height:var(--cell);position:relative;
  background:transparent;border:1px solid transparent;cursor:default;
  display:grid;place-items:center;
}
/* Every state below is drawn OVER the terrain: translucent fills and rings, never
   an opaque background, so the painted square stays visible underneath. */
/* NO z-index, opacity, filter or transform on a cell. Any of them makes the
   cell a stacking context and traps its piece inside it, and a piece drawn a
   square higher then paints over the head of the one below. The pieces stack by
   row — later in the DOM is lower on the screen and paints on top — and that
   only holds while every cell stays out of the stacking order. */
.cell.sel{outline:2px solid #fff;outline-offset:-2px;
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.5)}
.cell.dest{cursor:pointer}
.cell.dest::before{
  content:"";position:absolute;width:8px;height:8px;border-radius:50%;
  background:#2FBF5A;box-shadow:0 0 0 1.5px #fff,0 1px 2px rgba(0,0,0,.5);
}
.cell.dest.occupied::before{
  width:100%;height:100%;border-radius:0;background:transparent;
  box-shadow:inset 0 0 0 2px var(--alert);
}
.cell.place{cursor:pointer;box-shadow:inset 0 0 0 2px rgba(242,167,59,.9);background:rgba(242,167,59,.18)}
.cell.pick{cursor:pointer;box-shadow:inset 0 0 0 2px rgba(47,191,90,.9)}
.cell.picked{box-shadow:inset 0 0 0 2px #2FBF5A;background:rgba(47,191,90,.35)}
/* A drone target is an enemy vehicle to shoot, not a square to build on. */
.cell.target{box-shadow:inset 0 0 0 2px var(--alert);background:rgba(255,70,50,.32);cursor:crosshair}
.cell.brushable{cursor:crosshair}
.cell:hover.brushable,.cell:hover.dest,.cell:hover.place,.cell:hover.pick{
  background-image:linear-gradient(rgba(255,255,255,.22),rgba(255,255,255,.22))}

.glyph{width:100%;height:100%;display:block}
.p0{color:var(--p0)}
.p1{color:var(--p1)}
.pn{color:var(--dim)}

/* ---------- tiles ----------
   Everything that stands on a square is a tile: a round punch-out counter in
   its side's colour, like a coin sitting on the square. Hazards and the crew
   numbers are the token art itself (PNG, clipped to the circle); vehicles,
   infantry, bases and unclaimed hulls are drawn in CSS to the same pattern — a
   flat colour with the glyph in the counter's ink — so the board reads as one
   set of pieces. The rim is a pale inner ring and a soft drop shadow, which is
   what makes a flat disc read as a punched counter rather than a dot. */
.tile{
  position:absolute;inset:1px;border-radius:50%;overflow:hidden;
  background-size:100% 100%;background-repeat:no-repeat;
  box-shadow:0 0 0 1px rgba(0,0,0,.32),inset 0 0 0 1.5px rgba(255,255,255,.22),
    inset 0 -1px 2px rgba(0,0,0,.25),0 1.5px 2px rgba(0,0,0,.5);
  display:grid;place-items:center;
}
.tile .glyph{width:74%;height:74%}
.tile.ctile.p0{background-color:var(--tile-g);color:var(--tile-g-ink)}
.tile.ctile.p1{background-color:var(--tile-b);color:var(--tile-b-ink)}
/* A vehicle nobody is crewing belongs to nobody, and must not wear either
   player's colour. Under the vehicle win condition, telling at a glance which
   vehicles are anyone's and which are there for the taking is the single most
   important thing on the board — an unclaimed one is a square either player can
   win the game by walking into. Grey, dashed, no side. */
.tile.ctile.pn{background-color:var(--tile-n);color:var(--tile-n-ink)}
.tile.ctile.pn::after{content:'';position:absolute;inset:2px;border:1px dashed currentColor;
  opacity:.75;border-radius:1px;pointer-events:none}
/* ---------- the pieces ----------
   The units are rendered 3D models (tools/build-models.py) standing on the
   board, not sitting in a coin. They are drawn LARGER than a square and may
   overhang their neighbours; what fixes them to a square is the anchor. Every
   render puts the model's ground point at the same spot in its image — centred,
   30% up from the bottom — and the image is laid so that spot is the centre of
   the cell. A soft shadow under the ground point marks the square itself.
   Pieces lower on the screen are later in the DOM, so they paint over the ones
   behind them, which is the right way round for a view from above. */
.foot{
  position:absolute;left:12%;right:12%;top:42%;bottom:12%;border-radius:50%;
  background:radial-gradient(ellipse at center,rgba(0,0,0,.5) 0%,rgba(0,0,0,.28) 45%,transparent 72%);
  pointer-events:none;
}
.glyph.unit{
  position:absolute;
  width:calc(var(--cell) * 2.09);height:calc(var(--cell) * 2.09);
  left:50%;top:50%;
  margin-left:calc(var(--cell) * -1.045);
  margin-top:calc(var(--cell) * -1.463);   /* 70% of the height above the anchor */
  background-size:contain;background-position:center;background-repeat:no-repeat;
  pointer-events:none;z-index:3;
  filter:drop-shadow(0 1px 1px rgba(0,0,0,.35));
}
.glyph.model{background-size:contain;background-position:center;background-repeat:no-repeat}
.m-inf-g{background-image:url(/assets/model-inf-g.png)}
.m-inf-b{background-image:url(/assets/model-inf-b.png)}
.m-inf-n{background-image:url(/assets/model-inf-n.png)}
.m-inf-nd{background-image:url(/assets/model-inf-nd.png)}
.m-tank-g{background-image:url(/assets/model-tank-g.png)}
.m-tank-b{background-image:url(/assets/model-tank-b.png)}
.m-tank-n{background-image:url(/assets/model-tank-n.png)}
.m-tank-nd{background-image:url(/assets/model-tank-nd.png)}
.m-plane-g{background-image:url(/assets/model-plane-g.png)}
.m-plane-b{background-image:url(/assets/model-plane-b.png)}
.m-plane-n{background-image:url(/assets/model-plane-n.png)}
.m-plane-nd{background-image:url(/assets/model-plane-nd.png)}
/* A Base is a miniature compound on the square. It is ground, so it sits under
   any unit standing on it, and it is never turned: it has no front. */
.m-base-g{background-image:url(/assets/model-base-g.png)}
.m-base-b{background-image:url(/assets/model-base-b.png)}
.glyph.unit.base{z-index:2}
/* A crewed vehicle carries its side's NUMBERED counter — the number is how many
   men are aboard — as a small coin at the top-left corner of its square, set
   just outside it over the grid line so it clears the model. It is pinned to
   the square, not the model, and drawn above it. */
.tile.nbadge{inset:-20% auto auto -20%;width:50%;height:50%;z-index:4}
.t-g1{background-image:url(/assets/tile-green-1.png)}
.t-g2{background-image:url(/assets/tile-green-2.png)}
.t-g3{background-image:url(/assets/tile-green-3.png)}
.t-g4{background-image:url(/assets/tile-green-4.png)}
.t-b1{background-image:url(/assets/tile-beige-1.png)}
.t-b2{background-image:url(/assets/tile-beige-2.png)}
.t-b3{background-image:url(/assets/tile-beige-3.png)}
.t-b4{background-image:url(/assets/tile-beige-4.png)}
/* Hazards: the gear is a Mine, the rocket is Anti-Air. Laid on an empty square
   the token fills it; with a unit standing on it the token drops to a badge in
   the bottom-right corner, so both the piece and what it is standing on stay
   readable. The crew number lives at the opposite corner. */
.h-gm{background-image:url(/assets/haz-green-mine.png)}
.h-ga{background-image:url(/assets/haz-green-aa.png)}
.h-bm{background-image:url(/assets/haz-beige-mine.png)}
.h-ba{background-image:url(/assets/haz-beige-aa.png)}
.tile.haz.under{inset:auto 0 0 auto;width:48%;height:48%;z-index:4}
/* Ground a Drone tore up: the crater counter, laid on the square. */
.tile.crater{background-image:url(/assets/crater.png)}
/* A Base is ground rather than a piece. Empty, it is a tile with the base
   glyph; with a unit on it, it is a ring around that unit in the owner's colour. */
.base-ring{position:absolute;inset:-1px;border:2px solid currentColor;border-radius:50%;
  pointer-events:none;z-index:4;box-shadow:inset 0 0 0 1px rgba(0,0,0,.35),0 0 0 1px rgba(0,0,0,.35)}
/* Tiles used as icons in the side panel rather than on a square. */
.tile.icon{position:relative;inset:auto;width:24px;height:24px;flex:0 0 24px}
.glyph.model.icon{position:relative;width:28px;height:28px;flex:0 0 28px;margin:-2px 0}

/* ---------- panel ---------- */
.panel{flex:1 1 320px;min-width:300px;max-width:420px;display:flex;flex-direction:column;gap:14px}
.pane{background:var(--panel);border:1px solid var(--line-soft);border-radius:3px;padding:14px 16px}
.pane.grow{flex:1 1 auto;min-height:180px;display:flex;flex-direction:column}
.pane h2{
  margin:0 0 8px;font-size:13px;letter-spacing:.18em;text-transform:uppercase;color:var(--dim);font-weight:500;
}
.pane-note{margin:0 0 12px;font-size:14px;color:var(--dim);line-height:1.45}
.tag{font-family:var(--mono);font-size:11px;color:var(--p1);letter-spacing:0}

.brushes{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.brush{
  display:flex;align-items:center;gap:10px;padding:9px 10px;background:var(--ink-2);
  border:1px solid var(--line);border-radius:2px;color:var(--text);cursor:pointer;text-align:left;
}
.brush:hover{border-color:var(--p0)}
.brush[aria-pressed="true"]{border-color:var(--p0);background:#2B3520}
.brush:disabled{opacity:.3;cursor:not-allowed}
.brush svg{width:22px;height:22px;flex:0 0 22px}
.brush .bn{font-size:14px;line-height:1.1}
.brush .bc{font-family:var(--mono);font-size:11px;color:var(--dim);margin-left:auto}

.row-btns{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
.row-btns .btn{flex:1 1 auto;padding:9px 12px;font-size:13px}
.sel-info{font-size:14px;color:var(--text);line-height:1.5;min-height:44px}
.sel-info em{color:var(--dim);font-style:normal}

.counts{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:12px}
.counts th{text-align:left;color:var(--dim);font-weight:400;padding:3px 0}
.counts td{padding:3px 0;text-align:right}
.counts td.you{color:var(--p0)}
.counts td.them{color:var(--p1)}

.log{list-style:none;margin:0;padding:0;overflow-y:auto;flex:1 1 auto;max-height:260px;font-size:14px}
.log li{padding:4px 0;border-bottom:1px solid var(--line-soft);color:var(--dim);line-height:1.4}
.log li.turn{color:var(--text);text-transform:uppercase;letter-spacing:.1em;font-size:12px;margin-top:6px}
/* The three parts of a turn, banded. The mark names the band and the rule down
   the left carries it over the lines beneath, so the log is read as three groups
   under a turn rather than as one undifferentiated column. */
.log li.stepmark{color:var(--dim);text-transform:uppercase;letter-spacing:.14em;font-size:10px;
  padding:6px 0 2px;border-bottom:0;opacity:.75}
.log li.step-strike,.log li.stepmark.step-strike{border-left:2px solid var(--alert);padding-left:8px}
.log li.step-action,.log li.stepmark.step-action{border-left:2px solid var(--line-soft);padding-left:8px}
.log li.step-build,.log li.stepmark.step-build{border-left:2px solid var(--p0);padding-left:8px}
.log li.boom{color:var(--alert)}
.log li.win{color:var(--ok)}

/* ---------- overlay / toast ---------- */
.overlay{position:fixed;inset:0;background:rgba(8,12,14,.88);display:grid;place-items:center;z-index:50}
.over-card{text-align:center;padding:36px}
/* A request for the bot's seat sits above the result card: a finished bot game
   is exactly when the player is free to answer it. */
#challenge{z-index:55}
/* The toss: the hazard token spun in 3D and settled face up. Both faces are
   the real tile art, back to back; .coin.aa ends a half-turn further round so
   the Anti-Air face is the one showing. */
.coin-wrap{perspective:700px;width:120px;height:120px;margin:0 auto 22px;
  animation:coin-hop 2.2s ease-in-out forwards}
/* ONE SPIN, ONE SLOWING. A timing function applies to every keyframe segment
   separately, so a spin written as two segments eased out twice: it settled on
   a face at the top of the hop and then spun up again to land on another. The
   spin is a single segment now, and the hop is its own animation on the wrap. */
.coin{position:relative;width:100%;height:100%;transform-style:preserve-3d;
  animation:coin-flip 2.2s cubic-bezier(.15,.6,.2,1) forwards}
.coin.aa{animation-name:coin-flip-aa}
.coin-face{position:absolute;inset:0;backface-visibility:hidden}
.coin-face.face-aa{transform:rotateY(180deg)}
@keyframes coin-flip{from{transform:rotateY(0)}to{transform:rotateY(1800deg)}}
@keyframes coin-flip-aa{from{transform:rotateY(0)}to{transform:rotateY(1980deg)}}
@keyframes coin-hop{from{transform:translateY(0)}40%{transform:translateY(-60px)}to{transform:translateY(0)}}
@media (prefers-reduced-motion:reduce){.coin,.coin-wrap{animation-duration:.01s}}
.over-card h2{font-size:clamp(36px,7vw,64px);margin:0 0 12px;text-transform:uppercase;line-height:1}
.toast{
  position:fixed;left:50%;bottom:26px;transform:translateX(-50%);
  background:var(--alert);color:#210904;padding:10px 18px;border-radius:2px;
  font-size:14px;letter-spacing:.03em;z-index:60;max-width:80vw;
}

@media (max-width:900px){:root{--cell:20px}.layout{padding:12px;gap:14px}.tile.icon{width:20px;height:20px;flex-basis:20px}}
@media (prefers-reduced-motion:reduce){*{transition:none !important;animation:none !important}}

/* origin square of a queued infantry step */
.cell.ordered{box-shadow:inset 0 0 0 2px rgba(47,191,90,.6)}
.cell.ordered .unit,.cell.ordered .foot{opacity:.7}

/* the opponent's most recent turn */
.cell.lastmove{outline:2px solid var(--warn);outline-offset:-2px}
.cell.lastmove::after{
  content:'';position:absolute;inset:0;
  background:rgba(227,179,65,.22);pointer-events:none;
}
/* A DRONE OF MINE IS COMING DOWN HERE at the start of my next turn. Only ever my
   own — the view carries no square for the opponent's. Dashed rather than solid
   so it reads as a thing that has not happened yet, and it must not shout over
   .anim-hit, which is the same square a beat later once it actually lands. */
/* AIMING, FIRST CLICK: the men who could put a Drone up, and the one you have
   picked. Deliberately not the strike colour - these are your own men being
   offered, and painting them in the target red would read as squares to hit. */
.cell.spotter-pick{box-shadow:inset 0 0 0 2px var(--p0);background:rgba(169,194,122,.18);cursor:pointer}
.cell.spotter-picked{box-shadow:inset 0 0 0 3px var(--p0);background:rgba(169,194,122,.34)}

/* A man of yours with a Drone already in the air. He has to be alive when it
   lands or it is lost with him, so he is marked for the whole of its flight and
   not only while you are aiming. Solid, against the target's dashed outline:
   one is a square you are threatening, the other a man you are protecting. */
.cell.drone-spotter{outline:2px solid var(--p0);outline-offset:-3px}

.cell.drone-inbound{outline:2px dashed var(--alert);outline-offset:-3px}
.cell.drone-inbound::after{
  content:'';position:absolute;inset:0;
  background:repeating-linear-gradient(45deg,
    rgba(214,90,72,.16) 0 4px, transparent 4px 8px);
  pointer-events:none;
}

/* Ground a Drone tore up this turn. Impassable to everything, including flight,
   until the turn passes. The crater counter (.tile.crater) is what shows it;
   the class is kept on the cell for the playback marks and the tests. */
.cell.cratered{background:rgba(0,0,0,.12)}

/* the half you cannot see into while deploying */
.cell.concealed{background:rgba(10,14,12,.66)}


/* ---------- deployment presets ---------- */
.sel{
  flex:1 1 auto;min-width:0;background:var(--ink-2);color:var(--text);
  border:1px solid var(--line);border-radius:6px;padding:7px 8px;
  font-family:var(--mono);font-size:12px;
}
.sel:focus{outline:none;border-color:var(--p0)}


/* ---------- turn playback ---------- */
/* The opponent's finished turn is replayed a beat at a time. These mark the
   squares the current beat is about; the board underneath is the pre-turn one
   until the story finishes and the real state settles in. */
.cell.anim-trail{background:rgba(242,167,59,.38);box-shadow:inset 0 0 0 1px #F2A73B}
/* The trail fades back along the route, so the DIRECTION of a move reads at a
   glance instead of every square on the path looking equally recent. */
.cell.anim-trail1{background:rgba(242,167,59,.28);box-shadow:inset 0 0 0 1px rgba(242,167,59,.62)}
.cell.anim-trail2{background:rgba(242,167,59,.18);box-shadow:inset 0 0 0 1px rgba(242,167,59,.34)}
.cell.anim-trail3{background:rgba(242,167,59,.09);box-shadow:inset 0 0 0 1px rgba(242,167,59,.16)}
.cell.anim-moveTo{background:rgba(242,167,59,.45);box-shadow:inset 0 0 0 2px #F2A73B}
.cell.anim-from{box-shadow:inset 0 0 0 1px rgba(242,167,59,.6)}
.cell.anim-build{background:rgba(47,191,90,.4);box-shadow:inset 0 0 0 2px #2FBF5A;animation:buildpop .45s ease-out}
.cell.anim-hit{background:rgba(255,70,50,.45);box-shadow:inset 0 0 0 2px var(--alert);animation:hitpulse .5s ease-out}
@keyframes buildpop{
  from{filter:brightness(1.8)}
  to{filter:none}
}

/* ---------- the moving piece ----------
   The board is a grid of fixed --cell squares, so a sprite can be positioned
   over it arithmetically and tweened with `transform` on its own compositor
   layer — no reflow, and the grid underneath never has to be touched. */
.sprite-layer{position:absolute;inset:0;pointer-events:none;z-index:6;overflow:hidden}
.sprite{
  position:absolute;width:var(--cell);height:var(--cell);
  left:0;top:0;will-change:transform;
  display:grid;place-items:center;
  filter:drop-shadow(0 0 6px rgba(0,0,0,.7));
}
.sprite .tile{inset:1px}
.sprite{overflow:visible}
.sprite.dying{animation:spritedie .34s ease-in forwards}
@keyframes spritedie{
  from{opacity:1;transform-origin:center}
  to{opacity:0}
}
/* ---------- the drone ----------
   Smaller than a piece and lit rather than solid, so it reads as something over
   the board instead of something on it. It is drawn above the unit sprites
   because it is the only thing that flies. */
.drone-sprite{
  width:calc(var(--cell) * .72);height:calc(var(--cell) * .72);
  margin:calc(var(--cell) * .14);
  z-index:2;opacity:.96;color:#fff;
  filter:drop-shadow(0 2px 5px rgba(0,0,0,.85));
}
.drone-sprite .glyph{stroke-width:2.2}
/* Held over the target between the strike beat and the kill. The wobble is
   small on purpose — it should look like a machine station-keeping, not like a
   loose icon. */
.drone-sprite.hovering{animation:dronehover .55s ease-in-out infinite alternate}
@keyframes dronehover{
  from{filter:drop-shadow(0 2px 5px rgba(0,0,0,.85)) brightness(1)}
  to{filter:drop-shadow(0 4px 7px rgba(0,0,0,.85)) brightness(1.35)}
}
.drone-sprite.drone-boom{animation:droneboom .42s ease-out forwards}
@keyframes droneboom{
  0%{opacity:1}
  30%{opacity:1;filter:brightness(2.6)}
  100%{opacity:0}
}
/* The detonation itself: a ring over the square, expanding and fading. */
.drone-blast{
  position:absolute;left:0;top:0;width:var(--cell);height:var(--cell);
  border-radius:50%;pointer-events:none;z-index:3;
  box-shadow:0 0 0 2px var(--alert) inset, 0 0 14px 2px var(--alert);
  animation:droneblast .5s ease-out forwards;
}
/* The position of the ring is an inline `transform`, so the expansion is done
   with the shadow rather than with `scale` — animating transform here would
   throw the ring off its square. */
@keyframes droneblast{
  from{opacity:.95;box-shadow:0 0 0 2px var(--alert) inset, 0 0 6px 0 var(--alert)}
  to{opacity:0;box-shadow:0 0 0 10px rgba(0,0,0,0) inset, 0 0 26px 10px rgba(0,0,0,0)}
}

@keyframes hitpulse{
  from{transform:scale(1.14);filter:brightness(1.7)}
  to{transform:scale(1);filter:none}
}
.anim-note{
  display:flex;align-items:center;gap:8px;margin:10px 2px 0;
  font-family:var(--mono);font-size:12px;color:var(--p1);min-height:16px;
}

/* DRONES ARE IN THE AIR. Shown for as long as any are, and pulsed at the top of
   every turn — the count is the only thing either player is told about a strike,
   so it has to arrive rather than sit there waiting to be noticed. */
.drone-warning{
  display:flex;align-items:center;gap:8px;margin:10px 2px 0;padding:6px 10px;
  font-family:var(--mono);font-size:12px;letter-spacing:.04em;text-transform:uppercase;
  color:var(--alert);border:1px solid var(--alert);border-radius:4px;
  background:rgba(214,90,72,.08);
}
.drone-warning::before{content:'▲';font-size:11px;line-height:1}
.drone-warning.flash{animation:droneWarn 1.1s ease-out}
@keyframes droneWarn{
  0%   {transform:scale(.96);background:rgba(214,90,72,.45);border-color:#fff;color:#fff}
  35%  {transform:scale(1.02);background:rgba(214,90,72,.28)}
  100% {transform:scale(1);background:rgba(214,90,72,.08);border-color:var(--alert);color:var(--alert)}
}
@media (prefers-reduced-motion: reduce){
  .drone-warning.flash{animation:none}
}
.anim-note button{
  background:none;border:1px solid var(--line);color:var(--dim);
  border-radius:5px;padding:2px 8px;font:inherit;cursor:pointer;
}
.anim-note button:hover{color:var(--text);border-color:var(--p0)}
@media (prefers-reduced-motion: reduce){
  .cell.anim-hit{animation:none}
  .drone-sprite.hovering,.drone-blast{animation:none}
}


/* ---------- in-game replay ---------- */
.scrub-row{display:flex;align-items:center;gap:8px;margin-top:10px}
.scrub-row input[type=range]{flex:1 1 auto;min-width:0;accent-color:var(--p0)}
.scrub-row .btn{padding:2px 10px;line-height:1.4}
.viewing-past{box-shadow:inset 0 0 0 2px var(--p1)}

/* Deployment: the two centre rows are no man's land. Hazards may be laid there,
   but no unit starts on the line, so the strip is drawn as ground you cannot
   put men on rather than left to be discovered by a refused click. */
.cell.noman::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(135deg,
    transparent 0 5px, rgba(0, 0, 0, .2) 5px 10px);
}

/* The result overlay used to be a dead end: the Save replay button lived in the
   panel underneath it and became reachable and unreachable at the same moment.
   The overlay carries its own now, and can be dismissed to get at the board. */
.over-actions { justify-content: center; margin: 0 0 14px; }
.over-rematch { color: var(--dim); margin: 0 0 10px; min-height: 1.2em; }
#btn-over-new { margin-left: 8px; }
.overlay.peeking { pointer-events: none; opacity: 0; }
.over-reopen {
  position: fixed; right: 18px; bottom: 18px; z-index: 60;
}

.btn-danger { background: var(--alert); color: #fff; border-color: var(--alert); }

/* ---------- the record ---------- */
.history-link{color:var(--p0);text-decoration:underline;text-underline-offset:3px}
.history-link:hover{color:#fff}
