:root {
  --bg: #1b1e26;
  --panel: #262b36;
  --panel2: #2f3542;
  --panel3: #363d4d;
  --border: #3a4152;
  --text: #e8eaf0;
  --muted: #9aa3b5;
  --accent: #5aa9ff;
  --accent-dim: #3a6ea8;
  --good: #3bb54a;
  --mid: #e0a52c;
  --bad: #d9453d;
  --armor: #7c8aa5;
  --danger-bg: #3a2323;
  --danger-border: #6b3f3f;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: "Segoe UI", Arial, sans-serif;
  height: 100%;
}

h1, h2, h3, h4 { margin: 0; }

button, select, input {
  font-family: inherit;
  color: var(--text);
}

/* ---------- layout ---------- */

#topbar {
  padding: 12px 20px;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 6px;
}
#topbar h1 { font-size: 19px; }
#topbar .sub { color: var(--muted); font-size: 13px; }

#app {
  display: grid;
  grid-template-columns: 420px 1fr;
  gap: 16px;
  padding: 16px;
  align-items: start;
}

/* PLAY BATTLE FULL PAGE (added 2026-09-27, per Aiden - "make the whole
   play battle a separate page entirely... its own tab that takes up the
   whole screen... it removes everything else"). Covers the ENTIRE
   viewport (not just where #app used to be) - #topbar included - while
   shown; app.js's enterPlayFullPage()/exitPlayFullPage() toggle this
   against #topbar+#app wholesale, never both visible at once. */
#play-fullpage {
  position: fixed;
  inset: 0;
  z-index: 50;
  background: var(--bg);
  overflow: auto;
  padding: 16px;
}
#play-fullpage-bar {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 14px; gap: 12px;
}
#play-fullpage-title { font-size: 15px; font-weight: 700; color: var(--muted); }
/* Fullscreen (the real browser Fullscreen API, toggled on #play-fullpage
   itself so only this content fills the screen edge-to-edge) needs no
   special CSS of its own - the browser already stretches the fullscreened
   element to fill the display; :fullscreen here just drops the fixed
   inset/z-index concerns since there's nothing left to layer over. */
#play-fullpage:fullscreen { padding: 20px; }
@media (max-width: 980px) {
  #app { grid-template-columns: 1fr; }
}

.card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px;
}
.card + .card { margin-top: 14px; }

.card h2 {
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin-bottom: 10px;
}

/* ---------- setup: placement board ---------- */

.board-wrap { display: flex; flex-direction: column; gap: 8px; }
.board-caption { font-size: 12px; color: var(--muted); }

.board-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  grid-auto-rows: 1fr;
  gap: 6px;
}

.tile-box {
  aspect-ratio: 1 / 1;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel2);
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 4px;
  overflow: hidden;
  transition: background-color 0.15s, box-shadow 0.15s;
}
.tile-box.placement-empty { background: var(--panel); border-style: dashed; }
.tile-box .tile-tag {
  position: absolute; top: 3px; left: 5px; font-size: 9px; color: var(--muted); font-weight: 600;
}
.tile-box select {
  width: 100%;
  font-size: 10.5px;
  background: var(--panel3);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 3px 2px;
  margin-top: auto;
}
.tile-box .unit-swatch {
  width: 60%; aspect-ratio: 1/1; border-radius: 8px; margin-top: 8px;
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 13px; color: #10131a;
}
/* ICON SPRITE (added 2026-09-27, per Aiden - "the icon ones can be used
   when we select our squad") - swatch keeps its background/text styling
   for the initials fallback (no colored background needed once a real
   icon image fills the box). */
.tile-box .unit-swatch img { width: 100%; height: 100%; object-fit: contain; }

#unit-count-banner {
  font-size: 12.5px; padding: 6px 8px; border-radius: 6px; margin-top: 4px;
}
#unit-count-banner.ok { color: var(--good); }
#unit-count-banner.bad { color: var(--bad); background: var(--danger-bg); border: 1px solid var(--danger-border); }

/* ---------- setup: controls ---------- */

.field-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
.field-row label { font-size: 13px; color: var(--muted); min-width: 76px; }
.field-row select, .field-row input[type=number] {
  background: var(--panel2); border: 1px solid var(--border); border-radius: 6px;
  padding: 6px 8px; font-size: 13px;
}
.field-row input[type=number] { width: 80px; }

.btn {
  background: var(--panel3); color: var(--text); border: 1px solid var(--border);
  border-radius: 7px; padding: 9px 14px; font-size: 13.5px; font-weight: 600; cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.btn:hover:not(:disabled) { background: var(--accent); color: #0d0f14; border-color: var(--accent); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn.primary { background: var(--accent-dim); border-color: var(--accent); }
.btn.primary:hover:not(:disabled) { background: var(--accent); }
.btn-row { display: flex; gap: 10px; margin-top: 6px; flex-wrap: wrap; }

#progress-wrap { margin-top: 10px; display: none; }
#progress-wrap.active { display: block; }
.progress-track { height: 10px; background: var(--panel2); border-radius: 5px; overflow: hidden; border: 1px solid var(--border); }
.progress-fill { height: 100%; background: var(--accent); width: 0%; transition: width 0.2s ease; }
#progress-label { font-size: 12px; color: var(--muted); margin-top: 4px; }

#status-msg { font-size: 12.5px; margin-top: 8px; padding: 7px 9px; border-radius: 6px; display: none; }
#status-msg.show { display: block; }
#status-msg.error { background: var(--danger-bg); border: 1px solid var(--danger-border); color: #ff9d9d; }
#status-msg.info { background: #22303a; border: 1px solid #33566b; color: #9fd6ff; }

/* ---------- enemy priority ---------- */

.priority-list { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
@media (max-width: 520px) { .priority-list { grid-template-columns: 1fr; } }
.priority-row {
  display: flex; align-items: center; gap: 8px;
  background: var(--panel2); border: 1px solid var(--border); border-radius: 7px;
  padding: 6px 8px;
}
.priority-row .sprite-thumb { width: 30px; height: 30px; object-fit: contain; flex-shrink: 0; }
.priority-row .sprite-placeholder {
  width: 30px; height: 30px; border-radius: 5px; background: var(--panel3);
  display: flex; align-items: center; justify-content: center; font-size: 13px; color: var(--muted); flex-shrink: 0;
}
.priority-row .p-name { flex: 1; font-size: 12.5px; }
.priority-stepper { display: flex; align-items: center; gap: 4px; }
.priority-stepper button {
  width: 22px; height: 22px; border-radius: 5px; border: 1px solid var(--border);
  background: var(--panel3); color: var(--text); cursor: pointer; font-size: 13px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
}
.priority-stepper button:hover { background: var(--accent); color: #0d0f14; }
.priority-stepper input {
  width: 32px; text-align: center; background: var(--panel3); border: 1px solid var(--border);
  border-radius: 5px; padding: 3px 0; font-size: 12.5px;
}
.priority-hint { font-size: 11.5px; color: var(--muted); margin-top: 8px; line-height: 1.5; }

/* AI BEHAVIOR SLIDERS (added 2026-09-27) */
.slider-hint { font-size: 11.5px; color: var(--muted); margin-bottom: 10px; line-height: 1.5; }
.slider-row input[type=range] { flex: 1 1 auto; min-width: 100px; accent-color: var(--accent); }
.slider-value { font-size: 12.5px; color: var(--text); min-width: 32px; text-align: right; font-variant-numeric: tabular-nums; }
.slider-caption { display: flex; justify-content: space-between; font-size: 10.5px; color: var(--muted); margin: -4px 0 12px; }

/* ---------- results tabs ---------- */

.tabs { display: flex; gap: 4px; margin-bottom: 12px; border-bottom: 1px solid var(--border); }
.tab-btn {
  background: none; border: none; color: var(--muted); padding: 9px 16px; font-size: 13.5px;
  cursor: pointer; border-bottom: 2px solid transparent; font-weight: 600;
}
.tab-btn:hover { color: var(--text); }
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
.tab-pane { display: none; }
.tab-pane.active { display: block; }

.empty-state { color: var(--muted); font-size: 13.5px; padding: 30px 10px; text-align: center; }

.summary-strip { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; }
.stat-tile {
  background: var(--panel2); border: 1px solid var(--border); border-radius: 8px;
  padding: 10px 16px; min-width: 120px;
}
.stat-tile .stat-value { font-size: 22px; font-weight: 700; }
.stat-tile .stat-label { font-size: 11.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em; }

/* ORIENTATION MATCHES THE SQUAD SELECTOR (fixed 2026-09-26, per Aiden -
   "make this the same orientation as the squad selector part"). Same
   5-column grid as .board-grid, with each card pinned to its own tile's
   real gridColumn/gridRow in JS (renderResults()) exactly like
   buildPlacementGrid() does for #placement-grid - so a Mauler on B4 in
   the overview sits in the same visual slot as B4 in the squad selector,
   instead of wherever plain auto-fill happened to flow it. */
.unit-risk-grid {
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; margin-bottom: 18px;
}
.unit-risk-card {
  border-radius: 8px; padding: 9px 10px; border: 1px solid var(--border);
  display: flex; flex-direction: column; justify-content: center;
}
.unit-risk-card .r-tile { font-size: 10.5px; color: rgba(255,255,255,0.65); }
.unit-risk-card .r-name { font-size: 13px; font-weight: 700; margin: 2px 0 4px; display: flex; align-items: center; gap: 5px; }
.unit-risk-card .r-name img { width: 18px; height: 18px; object-fit: contain; flex-shrink: 0; }
.unit-risk-card .r-name .row-icon-ph { width: 18px; height: 18px; border-radius: 3px; background: var(--panel3); display: inline-flex; align-items: center; justify-content: center; font-size: 8px; color: var(--muted); flex-shrink: 0; }
.unit-risk-grid.compact .r-name img,
.unit-risk-grid.compact .r-name .row-icon-ph { width: 14px; height: 14px; }
.unit-risk-card .r-rate { font-size: 12px; }
.risk-safe { background: #1e3a22; border-color: #2f6b38; }
.risk-mid { background: #3a3220; border-color: #6b5a2f; }
.risk-high { background: #3a2323; border-color: #6b3f3f; }

/* COMPACT VARIANT (added 2026-09-27, per Aiden - "All (find best)" ranking
   table now tucks one of these grids under each of the 10 battle rows, so
   it needs to take up noticeably less room than the one full-size grid at
   the top of the page - see app.js's renderUnitRiskGrid(). */
.unit-risk-grid.compact { gap: 5px; margin-bottom: 0; max-width: 480px; }
.unit-risk-grid.compact .unit-risk-card { padding: 5px 7px; border-radius: 6px; }
.unit-risk-grid.compact .r-tile { font-size: 9px; }
.unit-risk-grid.compact .r-name { font-size: 11px; margin: 1px 0 2px; }
.unit-risk-grid.compact .r-rate { font-size: 10px; line-height: 1.3; }

table.stat-table { width: 100%; border-collapse: collapse; font-size: 12.5px; margin-bottom: 18px; }
table.stat-table th {
  text-align: left; color: var(--muted); font-weight: 600; font-size: 11px; text-transform: uppercase;
  padding: 6px 8px; border-bottom: 1px solid var(--border);
}
table.stat-table td { padding: 6px 8px; border-bottom: 1px solid #ffffff0a; }
table.stat-table tr:hover td { background: #ffffff08; }
table.stat-table .row-name { display: flex; align-items: center; gap: 7px; }
table.stat-table .row-name img { width: 22px; height: 22px; object-fit: contain; }
table.stat-table .row-name .row-icon-ph {
  width: 22px; height: 22px; border-radius: 4px; background: var(--panel3);
  display: inline-flex; align-items: center; justify-content: center; font-size: 10px; color: var(--muted);
}

.matchup-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 700px) { .matchup-cols { grid-template-columns: 1fr; } }

.arrow-sep { color: var(--muted); margin: 0 4px; }

/* ---------- watch battle board ---------- */

.watch-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 10px; }
.watch-toolbar select { background: var(--panel2); border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px; font-size: 13px; }

#watch-statusline { color: var(--muted); font-size: 12.5px; margin-bottom: 10px; }

/* TURN BANNER (added 2026-09-26, per Aiden - "make it clear what is the
   enemy turn and what is our turn when watching"). Sits right under the
   statusline, big and color-coded so it reads at a glance while stepping/
   playing through a battle: blue+"YOUR TURN" for a friendly action,
   red+"ENEMY TURN" for an enemy one, and a plain neutral state (no color)
   for the non-action steps (wave start/end, battle end) where "whose
   turn" doesn't apply. See app.js's updateTurnBanner(). */
#turn-banner {
  display: inline-block; font-size: 13px; font-weight: 700; letter-spacing: 0.02em;
  padding: 5px 12px; border-radius: 6px; margin-bottom: 10px; border: 1px solid var(--border);
  background: var(--panel2); color: var(--muted);
}
#turn-banner.turn-friendly { background: #1c2f45; border-color: var(--accent-dim); color: #bfe0ff; }
#turn-banner.turn-enemy { background: var(--danger-bg); border-color: var(--danger-border); color: #ffbcbc; }

/* Matching highlight on whichever board is currently acting, so the
   banner's color-coding is echoed on the board itself. */
.battle-grid.active-turn-friendly { box-shadow: 0 0 0 2px var(--accent) inset; }
.battle-grid.active-turn-enemy { box-shadow: 0 0 0 2px var(--bad) inset; }

/* LAYOUT (matched to Play Battle's 2026-09-26, per Aiden - "make the watch
   battle the same layout as play battle but still fit in the existing
   buttons up the top of the watch battle"). Same two-column grid shape as
   .play-layout below (boards stacked on the left, battle log stretched to
   match their full height on the right - see .play-boards-col's own big
   comment for why a flex column with an explicit height, not width-derived
   aspect-ratio boards, is what actually keeps both boards on screen at once
   without the log panel being some unrelated fixed height next to them).
   Watch keeps its OWN top toolbar (statusline/turn-banner/restart-prev-
   play-next-speed row/scrubber - untouched, still above this grid) rather
   than Play's statusline+banner, so .watch-boards-col's height subtracts
   more page chrome than .play-boards-col's does - see that class below. */
.watch-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: 16px;
  align-items: stretch;
}
@media (max-width: 900px) { .watch-layout { grid-template-columns: 1fr; } }

.watch-boards-col {
  display: flex; flex-direction: column; gap: 8px; min-width: 0;
  /* Empirically measured (real headless-browser render, 1920x1080 and
     1366x768, 10-unit squad/13-enemy wave - same method as .play-boards-col
     below) - accounts for Watch's taller top chrome (statusline + turn
     banner + toolbar row + scrubber) on top of the same topbar/tabs/card
     padding .play-boards-col already subtracts for. */
  height: calc(100vh - 345px);
  min-height: 320px;
}
.watch-grid {
  width: 100%;
  flex: 1 1 0;
  min-height: 0;
}
.watch-grid .cell { aspect-ratio: unset; overflow: hidden; }

.watch-log-col { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.watch-log-col #log-panel { flex: 1; min-height: 200px; height: auto; }
@media (max-width: 900px) {
  .watch-log-col #log-panel { flex: none; height: 260px; }
  .watch-boards-col { height: auto; min-height: 0; }
  .watch-grid { flex: none; height: 42vh; }
}

.board-title { font-size: 12px; color: var(--muted); margin: 2px 2px 6px; }

/* BOARD MODE TOGGLE (2026-09-28) - see app.js's playSetBoardMode. */
.board-mode-toggle { display: flex; gap: 6px; margin-bottom: 6px; }
.board-mode-toggle .mode-btn { font-size: 12px; padding: 5px 10px; }
.board-mode-toggle .mode-btn.chosen { background: var(--accent); color: #0d0f14; border-color: var(--accent); }

/* BATTLE BACKDROP (added 2026-09-27, per Aiden - "the background for the
   infected strike as well"). Confirmed the real background for this
   specific encounter from the actual game data (battle_encounters.json's
   armies "636"-"645", all ten Boss Strike 15 battles): bg_image =
   "battle_map_marin" - same wiki source every other sprite in this
   project already comes from. Applied as a dark-overlaid backdrop on
   .battle-grid itself (shared by Watch AND Play boards) rather than a
   plain color - a linear-gradient darkening layer UNDER which the real
   image still shows through, so it reads as a battle scene instead of a
   flat panel, while every .cell keeps its own OPAQUE background
   (var(--panel3), unchanged) - unit info stays exactly as readable as
   before, the art only shows in the grid's own gap/padding area around
   the cells, never behind the text/bars themselves. */
.battle-grid {
  display: grid; grid-template-columns: repeat(5, 1fr); grid-auto-rows: 1fr; gap: 6px;
  /* 2026-09-28: swapped to the authentic in-game battle_map_marin texture
     (extracted from the Steam install's own battlebackgrounds bundle, not
     the wiki) - same treatment, just the real source art. Old wiki file
     left on disk untouched as a fallback. */
  background: linear-gradient(rgba(13,15,20,0.72), rgba(13,15,20,0.72)), url("/sprites/bg_battle_map_marin_game.jpg");
  background-size: cover; background-position: center;
  border: 1px solid var(--border); border-radius: 8px; padding: 8px;
}

.cell {
  aspect-ratio: 1 / 1;
  border: 1px solid var(--border); border-radius: 8px; background: var(--panel3);
  position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 3px; overflow: visible; transition: background-color 0.2s;
}
.cell.empty { background: transparent; border-style: dashed; opacity: 0.3; }
.cell.dead { opacity: 0.4; filter: grayscale(75%); }
.cell .tile-label { position: absolute; top: 2px; right: 4px; font-size: 9px; color: var(--muted); }
/* FIX 2026-09-26, per Aiden - "fix the apex colossus sprite to make it fit
   like the others. Its too big" (screenshot showed it bleeding down into
   the row below it). Root cause: .content was never given its own
   height - it's a plain block div, sized only by its children - so
   .sprite-img's `height: 68%` below had no real number to compute
   against and (per spec) falls back to the image's OWN native size
   instead of being constrained. Every sprite was technically exposed to
   this, it just went unnoticed because most of the sprite PNGs happen to
   be proportioned closely enough to look fine anyway; Apex Colossus's
   asset is tall enough that it visibly overflowed the (deliberately
   overflow:visible, for the damage-number floaters) cell. Making
   .content an actual flex child that fills the cell (flex:1 1 auto;
   min-height:0) gives it a real height, so height:68% below now
   genuinely bounds every sprite regardless of that PNG's own dimensions -
   not just Apex Colossus, any future oversized asset too. */
.cell .content {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  width: 100%; flex: 1 1 auto; min-height: 0; overflow: hidden;
}
/* ENLARGED 2026-09-26, per Aiden - "the sprites are smaller than the boxes
   they are sitting when watching. the box sizes are fine, just enlarge the
   sprites." Box (.cell) size itself is untouched (still aspect-ratio 1/1,
   sized purely by the grid) - only the sprite's own footprint inside it
   grew (62%/46% -> 84%/68%), with the name/bars/hp-text below trimmed
   slightly (font sizes, bar height, margins) so the bigger sprite still
   leaves room for all of that without spilling out of the cell. */
.cell .sprite-img { width: 84%; height: 68%; object-fit: contain; margin-top: 1px; }
.cell .sprite-fallback {
  width: 84%; height: 68%; border-radius: 8px; margin-top: 1px;
  display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 20px; color: #10131a;
}
.cell .unit-name { font-size: 9.5px; font-weight: 700; margin-top: 1px; text-align: center; line-height: 1.1; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cell .bar { width: 88%; height: 4px; border-radius: 3px; background: #1a1d24; margin-top: 1px; overflow: hidden; }
.cell .bar-fill { height: 100%; transition: width 0.35s ease; }
.cell .armor-bar .bar-fill { background: var(--armor); }
.cell .hp-text { font-size: 8px; color: var(--muted); margin-top: 0; }
.cell .dead-mark { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 22px; color: var(--bad); background: rgba(0,0,0,0.35); border-radius: 8px; }

.cell.flash-hit { background-color: #5a2a2a; }
.cell.flash-miss { background-color: #3a3a2a; }
.cell.flash-kill { background-color: #6a1414; }
.cell.flash-attack { box-shadow: 0 0 0 2px var(--accent) inset; }

/* STUN/FREEZE (added 2026-09-26, per Aiden - "stunning is a real feature
   that should be implemented with visual queues"). Colors match the game's
   own status_effect_families.json: family 1 se_stun = #0fe4f7 (cyan),
   family 3 se_frozen = #cc99ff (light purple). The persistent ring stays on
   the cell for as long as the unit is disabled (driven by u.status in
   playRenderBoard/the Watch Battle board renderer); flash-stun/flash-frozen
   are the one-shot pulse on the turn the effect actually lands, same pattern
   as flash-hit/flash-kill above. */
.cell.status-stun { box-shadow: 0 0 0 3px #0fe4f7 inset; }
.cell.status-frozen { box-shadow: 0 0 0 3px #cc99ff inset; }
.cell.flash-stun { background-color: #0fe4f733; }
.cell.flash-frozen { background-color: #cc99ff33; }
.status-badge {
  position: absolute; top: 2px; left: 4px; font-size: 12px; line-height: 1;
  display: flex; align-items: center; gap: 2px; z-index: 4;
  text-shadow: 0 1px 2px #000;
}
.status-badge-stun { color: #0fe4f7; }
.status-badge-frozen { color: #cc99ff; }
.status-badge-turns { font-size: 8px; font-weight: 700; color: inherit; }

.floater {
  position: absolute; left: 50%; top: 8%; transform: translateX(-50%); font-weight: 700; font-size: 12.5px;
  pointer-events: none; animation: floatUp 0.9s ease-out forwards; z-index: 5; text-shadow: 0 1px 2px #000;
}
.floater.dmg { color: #ff8a8a; }
.floater.miss { color: #cfd6e6; }
.floater.kill { color: #ffd24a; }
/* CRIT (added 2026-09-26, per Aiden - "I want the damage numbers on the
   board to show if it was crit"). Bigger/bolder and a distinct orange so a
   crit floater reads as more emphatic than a normal hit at a glance,
   whether or not it also happened to be a kill. */
.floater.crit { color: #ff9d2e; font-size: 14.5px; font-weight: 800; text-shadow: 0 0 5px rgba(255, 140, 20, 0.85), 0 1px 2px #000; }
/* Offset below the dmg/crit floater (also at top:8%) so a hit that both
   damages AND lands stun/freeze in the same shot shows two readable lines
   instead of overlapping text. */
.floater.status-stun, .floater.status-frozen { top: 24%; }
.floater.status-stun { color: #0fe4f7; font-size: 13px; font-weight: 800; text-shadow: 0 0 5px rgba(15, 228, 247, 0.85), 0 1px 2px #000; }
.floater.status-frozen { color: #cc99ff; font-size: 13px; font-weight: 800; text-shadow: 0 0 5px rgba(204, 153, 255, 0.85), 0 1px 2px #000; }
/* DOT TICK (added 2026-09-26, per Aiden's spread-over-time DoT fix) -
   same floater mechanism as crit/status above, distinct colors so a tick
   reads differently from a direct hit at a glance. */
.floater.dot-fire { color: #ff6a3d; font-size: 13px; font-weight: 800; text-shadow: 0 0 5px rgba(255, 106, 61, 0.85), 0 1px 2px #000; }
.floater.dot-poison { color: #7CFC6E; font-size: 13px; font-weight: 800; text-shadow: 0 0 5px rgba(124, 252, 110, 0.85), 0 1px 2px #000; }
@keyframes floatUp { 0% { opacity: 1; transform: translate(-50%, 0); } 100% { opacity: 0; transform: translate(-50%, -20px); } }

#summary-banner { display: none; background: #223322; border: 1px solid #3f6b3f; border-radius: 8px; padding: 10px 12px; margin-top: 12px; font-size: 13px; }
#summary-banner.fail { background: #332222; border-color: #6b3f3f; }

/* height comes from .watch-log-col #log-panel above (flex:1, stretched to
   match the boards column) - no longer a fixed 460px independent of how
   tall the boards actually are, that mismatch was the bug Aiden reported. */
#log-panel { overflow-y: auto; font-size: 11.5px; line-height: 1.5; font-family: Consolas, monospace; }
.log-line { padding: 1px 0; border-bottom: 1px solid #ffffff08; }
.log-line.log-wave_start { color: #7fd0ff; font-weight: 700; margin-top: 6px; }
.log-line.log-wave_end { color: #ffd24a; font-weight: 700; margin-bottom: 6px; }
.log-line.log-battle_end { color: #7fffb0; font-weight: 700; }
.log-line.log-shift { color: #c9a6ff; font-style: italic; }
/* CRIT (added 2026-09-26, per Aiden - "show in battle logs if a crit
   happened"). Applies on top of an action line's own type color. */
.log-line.log-crit { color: #ff9d2e; font-weight: 700; }
/* STUN/FREEZE (added 2026-09-26, per Aiden - "stunning is a real feature
   that should be implemented with visual queues"). Same colors as the
   board badge/ring. */
.log-line.log-stun { color: #0fe4f7; font-weight: 700; }
.log-line.log-frozen { color: #cc99ff; font-weight: 700; }
/* PASS (added 2026-09-27, per Aiden - "make sure that if enemy has no
   valid attacks it passes its turn"). Dimmed/italic - a forfeited turn is
   real information (confirms the round genuinely moved on) but shouldn't
   visually compete with an actual hit/kill/status line. */
.log-line.log-pass { color: var(--muted); font-style: italic; }

#scrubber { width: 100%; margin-top: 4px; }

/* ---------- play battle (interactive) ---------- */

/* LAYOUT (reworked 2026-09-26, per Aiden - "enemy formation on top and our
   friendly board beneath them, our attack options should be in between the
   two boards. The logs can be displayed on the right side"). Two-column
   grid: left column stacks enemy grid -> weapon bar -> friendly grid
   top-to-bottom; right column is the battle log, stretched (align-items:
   stretch) to match the left column's full height so it never overlaps or
   gets squashed by the boards. Collapses to a single column (log below the
   boards, with its own fixed scroll height) once the window is too narrow
   for both columns side by side. */
.play-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: 16px;
  align-items: stretch;
}
@media (max-width: 900px) {
  .play-layout { grid-template-columns: 1fr; }
}

/* BOTH BOARDS VISIBLE WITHOUT SCROLLING, TAKE 2 (reworked 2026-09-26, per
   Aiden - the first version ("cap the grid's WIDTH in vh, let aspect-ratio
   derive height from that") looked broken/overlapping in real use: a
   friendly unit's longer name ("Portable Electric Fence") plus a 2nd armor
   bar made that cell's CONTENT taller than its aspect-ratio-implied height,
   and since .cell had overflow:visible with no hard height cap, the cell
   (and its whole row) just grew past the intended size - worse on the
   friendly board than the enemy board since more friendly units carry
   armor, which is exactly the "overlapping/uneven boards" Aiden saw. Fixed
   properly this time: .play-boards-col gets an EXPLICIT height (viewport
   height minus the page chrome above it - topbar/tabs/statusline/padding,
   measured empirically), the two boards+weapon-bar share that height via
   flexbox (flex:1 1 0 on each grid, weapon bar fixed), which gives each
   grid a firm computed height that flows into battle-grid's own
   grid-auto-rows:1fr as REAL row tracks instead of a width-derived guess -
   and .play-grid .cell drops aspect-ratio entirely and clips overflow, so
   a too-long name/extra bar can never push a row taller than its track;
   text still ellipsizes via .unit-name's own rule. Verified end to end
   with a real headless-browser render of the live page (10-unit squad,
   13-enemy wave, Battle 5) at 1920x1040 and 1366x768 - zero page scroll,
   no cell overflow, at both sizes. */
.play-boards-col {
  display: flex; flex-direction: column; gap: 8px; min-width: 0;
  /* CHROME HEIGHT RE-ESTIMATED (2026-09-27, per Aiden - "make the whole
     play battle a separate page"). The 230px this used to subtract was
     empirically measured (real headless-browser render) for the OLD
     context - inside #app, under #topbar + .tabs + .card padding. Play
     Battle is now its own full-page view instead (#play-fullpage, no
     topbar/tabs/card - see index.html), with a smaller #play-fullpage-bar
     toolbar replacing that chrome, so less is subtracted now. This value
     is an ESTIMATE, not re-verified against a real browser render the way
     the original 230px was (no headless-browser tooling available this
     session) - deliberately kept on the higher/safer side (under-using
     available height, not risking overflow) rather than tuned tight.
     Check in a real browser and adjust if there's noticeably wasted space
     below the boards, or (less likely, given the conservative estimate)
     any scrolling. */
  height: calc(100vh - 190px);
  min-height: 320px;
}

.play-grid {
  width: 100%;
  flex: 1 1 0;
  min-height: 0;
}
.play-grid .cell { aspect-ratio: unset; overflow: hidden; }

/* ISOMETRIC PLAY BOARD (2026-09-28, per Aiden - "try find the grid that the
   friendly and enemy units sit in", then "Yes" to building an authentic-
   proportion overlay once no such texture existed anywhere in the game's
   own bundles). Replaces .play-grid's flat CSS-grid rectangular cells with
   the real board's own proportions for Play Battle specifically (see
   app.js's matching ISOMETRIC PLAY BOARD header for the geometry source/
   derivation, row-letter remap and column-order reasoning). .play-grid
   above is left completely in place, unused by Play now but still there to
   revert to if this ever needs rolling back - Watch Battle still uses it
   as-is, untouched.
   #play-enemy-grid/#play-friendly-grid are now just .iso-layer wrappers
   (position:absolute;inset:0) inside this one #play-stage - .iso-tile
   keeps the .cell class too, so every existing state rule above
   (.empty/.dead/.selectable/.targetable/.flash-.../.status-...) still
   applies unchanged; only shape/position/sizing are overridden here. */
/* RESPONSIVE FIT (2026-09-28, per Aiden - "units seem a bit bunched up...
   needs to scale to whatever screen it's sitting on"). Root cause (found by
   comparing #play-stage's real getBoundingClientRect() aspect against
   ISO_LAYOUT.W/H, not assumed): .play-stage used to be width:100% AND
   flex:1 1 0 (both dimensions fixed by its flex parent) with `aspect-ratio`
   just set as an inline style - with both dimensions already fixed,
   aspect-ratio has nothing left to derive and is ignored, so x% and y%
   ended up representing DIFFERENT physical pixel distances (the stage's
   real aspect never matched W:H). That's what "bunched up" was - not a
   sizing choice, a genuine distortion bug that also got worse the wider
   the window (matches Aiden's "meant for 1080x720" read - a modern
   ultrawide-ish viewport made the mismatch bigger). Fixed with JS instead
   of a pure-CSS trick (isoFitStage in app.js, via ResizeObserver): the
   wrap centers/letterboxes .play-stage at an EXPLICIT pixel width/height
   computed to the real W:H ratio, so every %-based child position (tiles,
   sprites, labels, floaters) is finally undistorted at any window size. */
#play-stage-wrap {
  flex: 1 1 0; min-height: 0; display: flex; align-items: center; justify-content: center;
}
.play-stage {
  position: relative;
  border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
  background: linear-gradient(rgba(13,15,20,0.72), rgba(13,15,20,0.72)), url("/sprites/bg_battle_map_marin_game.jpg");
  background-size: cover; background-position: center;
  /* touch-action:none (2026-09-28) - stops the browser treating a drag
     across the board as a page-scroll gesture before pointermove ever
     fires, needed for the targeting-ring hover tracking. */
  touch-action: none;
}
.iso-layer { position: absolute; inset: 0; pointer-events: none; }
.cell.iso-tile {
  position: absolute; transform: translate(-50%, -50%); aspect-ratio: auto;
  clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
  opacity: 0.4; pointer-events: auto; padding: 0; border: none; border-radius: 0;
}
.cell.iso-tile.empty { opacity: 0.12; }
.cell.iso-tile .tile-label { font-size: 8px; text-shadow: 0 1px 2px #000; }

/* SELECTION/STATUS RINGS DON'T SURVIVE clip-path (found via screenshot,
   2026-09-28) - .selectable/.targetable/.status-stun/.status-frozen are all
   inset box-shadows on the base .cell, but clip-path cuts away everything
   outside the diamond, including a shadow that conceptually starts at the
   box's own rectangular edges - what's left is 4 barely-visible slivers at
   the edge midpoints instead of a visible ring. flash-hit/flash-kill/etc
   don't have this problem (background-color paints INSIDE the shape it
   belongs to, nothing to clip) - only these box-shadow-based ones needed a
   different treatment: a solid diamond fill instead of a ring. Also see
   playFlash's floater-placement comment for the matching fix on damage-
   number text, which has the same root cause. */
.cell.iso-tile.selectable { box-shadow: none; background: var(--accent); opacity: 0.55; }
.cell.iso-tile.selected-attacker { box-shadow: none; background: #ffd24a; opacity: 0.6; }
.cell.iso-tile.targetable { box-shadow: none; background: var(--bad); opacity: 0.55; }
.cell.iso-tile.targetable:hover { background: #ff6a6a; }
.cell.iso-tile.status-stun { box-shadow: none; background: #0fe4f7; opacity: 0.4; }
.cell.iso-tile.status-frozen { box-shadow: none; background: #cc99ff; opacity: 0.4; }

/* Sprites + their HP/name/status label are a SEPARATE absolutely-positioned
   layer (.iso-unit, one per side per tile - see playBuildIsoUnits), not
   inside .iso-tile, so a friendly and an enemy sprite share one z-index
   stacking context regardless of which side's .iso-layer they logically
   belong to (painter's algorithm across BOTH sides at once). unitEl itself
   spans the whole stage (inset:0) purely so its children's percentage
   left/top/width resolve against the real stage size, same basis the tile
   diamonds use - only its children are actually positioned per-unit. */
.iso-unit { position: absolute; inset: 0; pointer-events: none; }
.iso-unit.dead .iso-sprite, .iso-unit.dead .iso-fallback { filter: grayscale(85%) brightness(0.6); }
/* pointer-events:none (2026-09-28, per Aiden) - clicks always go to the
   .iso-tile diamond underneath, never the sprite art itself. See
   playBuildIsoUnits' CLICK HANDLING comment in app.js for why. */
.iso-sprite, .iso-fallback { position: absolute; pointer-events: none; }
.iso-fallback {
  width: 56px; height: 56px; margin-left: -28px; margin-top: -54px;
  border-radius: 8px; display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 18px; color: #10131a;
}
/* Attack poses / hit VFX / the aoe_target ring (2026-09-28) - stepped grid
   sprite sheets, positioned in real CSS px (not %, see playSheetAnim in
   app.js for why), background-position moved frame by frame on a timer. */
.iso-sheet-anim {
  position: absolute; pointer-events: none; background-repeat: no-repeat;
  image-rendering: -webkit-optimize-contrast;
}
/* --iso-k (2026-09-28, set by isoFitStage() in app.js) is the board's own
   px-per-native-unit scale, roughly 1 at a comfortable desktop size and
   smaller on a cramped/phone viewport - clamp() keeps label text/bars
   readable at the small end and not oversized at the large end, instead of
   scaling 1:1 with the board the way sprites/tiles correctly do. */
/* Hangs DOWN from the unit's own foot point (2026-09-28, per Aiden - "the
   health bars needs to be underneath the enemies") - translate(-50%, 0)
   instead of the old (-50%, -100%) that sat above the sprite's head. */
.iso-label {
  position: absolute; transform: translate(-50%, 6px); pointer-events: none;
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  background: rgba(13,15,20,0.78); border: 1px solid var(--border); border-radius: 6px;
  padding: 2px 5px 3px; z-index: 1; white-space: nowrap;
}
.iso-label .unit-name { font-size: clamp(8px, calc(var(--iso-k, 1) * 11px), 12px); font-weight: 700; text-align: center; line-height: 1.1; max-width: 96px; overflow: hidden; text-overflow: ellipsis; }
.iso-label .bar { width: clamp(36px, calc(var(--iso-k, 1) * 64px), 72px); height: 4px; border-radius: 3px; background: #1a1d24; margin-top: 1px; overflow: hidden; }
.iso-label .bar-fill { height: 100%; transition: width 0.35s ease; }
.iso-label .armor-bar .bar-fill { background: var(--armor); }
.iso-label .hp-text { font-size: clamp(7px, calc(var(--iso-k, 1) * 9px), 10px); color: var(--muted); }
.iso-label .status-badge { position: static; margin-top: 1px; text-shadow: none; }
.iso-label .dead-mark { position: static; font-size: 13px; color: var(--bad); background: none; }

.play-weapon-bar { flex: 0 0 auto; min-height: 20px; }

.play-log-col { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.play-log-col #play-log-panel { flex: 1; min-height: 200px; }
@media (max-width: 900px) {
  .play-log-col #play-log-panel { flex: none; height: 260px; }
  .play-boards-col { height: auto; min-height: 0; }
  .play-grid { flex: none; height: 42vh; }
  /* .play-stage-wrap needs the same explicit-height treatment as .play-grid
     above once .play-boards-col drops its fixed height on mobile - flex:1 1
     0 has nothing to flex FROM otherwise (2026-09-28, phone-use fix). */
  #play-stage-wrap { flex: none; height: 42vh; }
}

.cell.selectable { cursor: pointer; box-shadow: 0 0 0 2px var(--accent) inset; animation: selectablePulse 1.4s ease-in-out infinite; }
.cell.selected-attacker { box-shadow: 0 0 0 3px #ffd24a inset; }
.cell.targetable { cursor: pointer; box-shadow: 0 0 0 2px var(--bad) inset; }
.cell.targetable:hover { background-color: #4a2020; }
@keyframes selectablePulse {
  0%, 100% { box-shadow: 0 0 0 2px var(--accent) inset; }
  50% { box-shadow: 0 0 0 2px var(--accent) inset, 0 0 10px 1px var(--accent); }
}

#play-statusline { color: var(--muted); font-size: 12.5px; margin-bottom: 6px; }
#play-friendly-title { transition: color 0.2s; }
#play-friendly-title.awaiting-weapon, #play-friendly-title.awaiting-target { color: var(--accent); font-weight: 700; }

#play-banner {
  display: none; font-size: 13px; font-weight: 700; letter-spacing: 0.02em;
  padding: 5px 12px; border-radius: 6px; margin-bottom: 8px; border: 1px solid var(--border);
  background: var(--panel2); color: var(--muted);
}
#play-banner.show { display: inline-block; }
#play-banner.turn-friendly { background: #1c2f45; border-color: var(--accent-dim); color: #bfe0ff; }
#play-banner.turn-enemy { background: var(--danger-bg); border-color: var(--danger-border); color: #ffbcbc; }

.play-weapon-bar { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; align-items: center; }
.play-weapon-bar .btn.weapon-btn { font-size: 12.5px; padding: 7px 12px; }
.play-weapon-bar .btn.weapon-btn.chosen { background: var(--accent); color: #0d0f14; border-color: var(--accent); }
.play-weapon-bar .hint { font-size: 12px; color: var(--muted); align-self: center; }

/* BLOCKED WEAPONS (added 2026-09-26, per Aiden's cooldown report - see
   renderPlayWeaponBar()'s own comment). Shown disabled/dimmed alongside the
   usable weapons so a unit's on-cooldown attack is visibly counting down
   ("cooldown (2 turns left)") instead of just silently missing from the
   list with no explanation. */
.play-weapon-bar .btn.weapon-btn.blocked {
  opacity: 0.55; cursor: not-allowed; background: var(--panel2); font-style: italic;
}
.play-weapon-bar .btn.weapon-btn.blocked:hover { background: var(--panel2); color: var(--text); border-color: var(--border); }

#play-result-banner { display: none; background: #223322; border: 1px solid #3f6b3f; border-radius: 8px; padding: 10px 12px; margin-top: 4px; font-size: 13px; }
#play-result-banner.show { display: block; }

/* WEAPON INSPECT PANEL (added 2026-09-27, per Aiden - "I want to see enemy
   and friendly attacks available/what is on cooldown... click on the enemy
   to see what attacks it has on cooldown"). Shared look between Watch
   Battle's #watch-weapon-panel and Play Mode's #play-enemy-panel - a plain
   read-only list, same reason/turns_left vocabulary as the existing
   .play-weapon-bar blocked buttons so it reads as the same feature. */
.weapon-panel {
  background: var(--panel2); border: 1px solid var(--border); border-radius: 8px;
  padding: 8px 12px; margin-top: 8px; font-size: 12.5px;
}
.weapon-panel-header { font-weight: 700; margin-bottom: 6px; display: flex; justify-content: space-between; align-items: center; }
.weapon-panel-close { background: none; border: none; color: var(--muted); font-size: 16px; line-height: 1; cursor: pointer; padding: 0 2px; }
.weapon-panel-close:hover { color: var(--text); }
.weapon-panel-row { padding: 3px 0; border-top: 1px solid var(--border); }
.weapon-panel-row:first-of-type { border-top: none; }
.weapon-panel-row.blocked { color: var(--muted); font-style: italic; }
.weapon-panel-row.ready { color: var(--good); }

/* Occupied cells are always click-to-inspect now, not just when
   selectable/targetable for an actual action - subtle hover only (no pulse/
   ring) so it never competes visually with the real selectable/targetable
   affordances above. */
.watch-grid .cell:not(.empty):hover,
.play-grid .cell:not(.empty):not(.selectable):not(.targetable):hover,
.iso-tile:not(.empty):not(.selectable):not(.targetable):hover { cursor: pointer; filter: brightness(1.12); }
.iso-tile:not(.empty) { cursor: pointer; }
#play-result-banner.fail { background: #332222; border-color: #6b3f3f; }

#play-log-panel { height: 140px; overflow-y: auto; font-size: 11px; line-height: 1.5; font-family: Consolas, monospace; background: var(--panel2); border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px; }
#play-log-panel .log-line { padding: 1px 0; border-bottom: 1px solid #ffffff08; }
