/* GRIDLOCK web view. Same deliberate colored rectangles as the Godot view (S-03):
   early polish caps final quality, and abstract art cannot imply an interaction
   the rules do not implement. */

/* Every board measurement derives from --tile; the Razor never writes a pixel count.
   --board-w/--board-h arrive inline on .game from battle state, so the tile can be
   solved against the viewport: desktop grows it (capped), phones shrink it to fit. */

:root {
    --bg: #14161c;
    --panel-text: #e8eaf0;
    --dim-text: #9aa4b8;
    --player: #529ee6;
    --enemy: #e05c57;
    --active: #ffeb73;
    --reachable: rgba(89, 191, 255, 0.22);
    --pending: rgba(255, 235, 115, 0.30);
    --targetable: rgba(255, 115, 102, 0.35);
    --tile: 48px;
}

* { box-sizing: border-box; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--panel-text);
    font-family: "Segoe UI", system-ui, sans-serif;
    font-size: 15px;
    /* A pull-to-refresh mid-battle throws the run away (Q-8: resolved actions are final). */
    overscroll-behavior-y: contain;
}

.boot { padding: 40px; color: var(--dim-text); }

#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0;
    width: 100%;
    background: #7a2c28;
    padding: 8px 16px;
}

/* Touch: no double-tap zoom on the things players tap rapidly. */
button, .tile { touch-action: manipulation; }

.game {
    display: flex;
    flex-direction: column;
    width: fit-content;
    max-width: 100%;
    margin: 0 auto;
    padding: 8px 10px 28px;
    /* PINNED TO EXACT SPRITE MULTIPLES (Session 022). The tile used to be a clamp() that resolved
       to whatever the viewport gave it — 40-60px — which is fine for coloured rectangles and fatal
       for pixel art: a 32px sprite drawn into a 53px tile is a non-integer resample, i.e. mush.
       64px is exactly 2x, and Rob authorised the larger grid ("if you need to increase grid size
       to make it work that is fine"). A 10-wide board is 640px, which still leaves the panel room
       on a 1366 laptop. */
    --tile: 64px;
}

@media (max-width: 700px) {
    .game {
        /* Phone: exactly 1x. 10 tiles at 32px is 320px, which fits every phone we shoot. */
        --tile: 32px;
        padding: 6px 8px 20px;
    }
}

@media (min-width: 1800px) {
    .game {
        /* S-15i (Session 024, Rob): "upscaled Ultima" — wide desktops get a 128px tile showing
           128-NATIVE art at 1:1 (not the 64 set doubled; the fork was measured on the real
           board, art/samples/scale-128*.png). A 10-wide board is 1280px, which needs this
           breakpoint: it does not fit beside the panel on a 1366 laptop, which keeps 64. */
        --tile: 128px;
    }
}

/* ---- turn-order bar: chip 1 is NOW, then the projection ---- */
.turnbar-caption { font-size: 13px; color: var(--dim-text); margin-bottom: 4px; }
.turnbar { display: flex; gap: 4px; margin-bottom: 10px; max-width: 100%; overflow-x: auto; padding-bottom: 2px; }

.chip {
    flex: none;
    width: 52px;
    height: 40px;
    position: relative;
    border: 2px solid transparent;
    font-size: 12px;
    color: rgba(0, 0, 0, 0.85);
    padding: 2px 4px;
    line-height: 1.4;
}

.chip.player { background: var(--player); }
.chip.enemy { background: var(--enemy); }
.chip.acting { border-color: var(--active); }
.chip .slot { font-size: 10px; font-weight: 600; display: block; }

.battle-row { display: flex; gap: 24px; }

@media (max-width: 700px) {
    /* Stack: board first, then forecast + actions — reading order is acting order. */
    .battle-row { flex-direction: column; gap: 12px; }
}

/* ---- board: a CSS grid of tiles with absolutely positioned units above it ---- */
.board {
    position: relative;
    flex: none;
    width: calc(var(--tile) * var(--board-w, 10));
    height: calc(var(--tile) * var(--board-h, 10) + 8px);
}

.tiles { display: grid; }

.tile {
    width: var(--tile);
    height: var(--tile);
    border: 1px solid rgba(0, 0, 0, 0.35);
    position: relative;
    cursor: default;
}

/* Height reads through EDGES, not through fill (Rob's call, Session 021 — he is right and it is
   better than the trade I posed). Fill used to carry elevation, which put terrain in direct
   competition with unit armour for the same lightness range: a unit could vanish into a bright
   tile. Fill now sits in a narrow band and says nothing about height.

   Height is carried by a lit top edge that brightens and thickens with elevation, plus a cast
   shadow below it — the way a real tileset draws a cliff. Edges are a DIFFERENT channel from fill,
   so they cost the sprite nothing. Strictly more information for free.

   Palette: DawnBringer 32, ratified by Rob (§10). The corner pips stay as the literal count,
   because the critical signal goes through more than one channel (Interface §9). */
.tile.h0 { background: #323c39; }
.tile.h1 { background: #3a453c; }
.tile.h2 { background: #434e3f; }
.tile.h3 { background: #4b5742; }

/* The edge layer sits ABOVE the tint overlays deliberately: the shot map paints a tile with an
   inset box-shadow, and height must stay readable through it. */
.tile::before {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 1;
}

.tile.h1::before { border-top: 2px solid rgba(203, 219, 252, 0.30); border-bottom: 2px solid rgba(0, 0, 0, 0.35); }
.tile.h2::before { border-top: 3px solid rgba(203, 219, 252, 0.46); border-bottom: 3px solid rgba(0, 0, 0, 0.48); }
.tile.h3::before { border-top: 4px solid rgba(203, 219, 252, 0.66); border-bottom: 4px solid rgba(0, 0, 0, 0.60); }

.tile .pip {
    position: absolute;
    top: 4px;
    width: 5px;
    height: 5px;
    background: rgba(255, 255, 255, 0.5);
}

.tile.reachable { box-shadow: inset 0 0 0 100px var(--reachable); cursor: pointer; }

/* ---- the Shot Map (Session 020): odds from each tile you could stand on ----
   Tint is the primary channel and the number is confirmation, because per-tile text is small at
   1x on a phone. Out-of-range tiles carry nothing at all — the silence is what teaches range. */
.tile.shot-good { box-shadow: inset 0 0 0 100px rgba(96, 214, 130, 0.30); }
.tile.shot-fair { box-shadow: inset 0 0 0 100px rgba(226, 197, 92, 0.26); }
.tile.shot-poor { box-shadow: inset 0 0 0 100px rgba(206, 104, 92, 0.24); }

.tile .odds {
    position: absolute;
    right: 2px;
    bottom: 1px;
    font-size: clamp(9px, calc(var(--tile) * 0.26), 13px);
    font-weight: 700;
    color: rgba(255, 255, 255, 0.92);
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
    pointer-events: none;
}

.tile.shot-good .odds { color: #b6f2c4; }
.tile.shot-poor .odds { color: #f0b3ab; }

/* The unit the map is currently priced against. */
.tile.aimed { outline: 2px dashed rgba(255, 255, 255, 0.75); outline-offset: -3px; }
.tile.pending { box-shadow: inset 0 0 0 100px var(--pending); }
.tile.targetable { box-shadow: inset 0 0 0 100px var(--targetable); cursor: pointer; }
.tile.ability-reach { outline: 3px solid rgba(255, 158, 51, 0.9); outline-offset: -4px; cursor: pointer; }
.tile.intent { box-shadow: inset 0 0 0 100px rgba(255, 217, 77, 0.30); outline: 3px solid #ffd94d; outline-offset: -4px; }
.tile.splash { outline: 3px solid rgba(255, 140, 51, 0.95); outline-offset: -3px; }

/* Units slide via CSS transitions — the eye tracks motion instead of diffing frames.
   Duration rides --glide, set by the view from its playback speed: a fixed glide outlives
   the beats between turns at high speed and units retarget mid-flight. */
.unit {
    position: absolute;
    width: calc(var(--tile) * 0.667);
    height: calc(var(--tile) * 0.667);
    margin: calc(var(--tile) * 0.167);
    transition: left var(--glide, 280ms) ease-in-out, top var(--glide, 280ms) ease-in-out;
    font-size: clamp(9px, calc(var(--tile) * 0.28), 13px);
    font-weight: 600;
    color: rgba(0, 0, 0, 0.85);
    padding: 1px 3px;
    pointer-events: none;
    z-index: 2;
}

.unit.player { background: var(--player); }
.unit.enemy { background: var(--enemy); }
.unit.acting { outline: 3px solid var(--active); outline-offset: 3px; }
.unit.targeted { outline: 2px solid rgba(255, 255, 255, 0.9); outline-offset: 5px; }

.unit .hp {
    position: absolute;
    left: 0;
    bottom: -6px;
    height: 4px;
    width: 100%;
    background: rgba(0, 0, 0, 0.6);
}

.unit .hp > div { height: 100%; background: #66d966; }
.unit .hp > div.low { background: #f2b23f; }

/* Floating results rise and fade over the tile where they happened. */
.impact {
    position: absolute;
    z-index: 3;
    font-size: clamp(12px, calc(var(--tile) * 0.33), 16px);
    font-weight: 700;
    pointer-events: none;
    animation: rise 1.1s ease-out forwards;
    text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
}

.impact.dmg { color: #ff7366; }
.impact.miss { color: #c0c6d4; }
.impact.wait { color: #ccccd9; }
.impact.shoved { color: #ffd94d; }
.impact.tempo { color: #7fd4ff; }

@keyframes rise {
    from { transform: translateY(0); opacity: 1; }
    to { transform: translateY(-18px); opacity: 0; }
}

/* Attacker→target strike line, drawn on an SVG overlay in 48px-per-tile viewBox space —
   the viewBox scales with the board, so the coordinates never need to know --tile. */
.strike-layer { position: absolute; top: 0; left: 0; pointer-events: none; z-index: 4; }

/* ---- side panel ---- */
.panel { width: 320px; max-width: 100%; font-size: 14.5px; line-height: 1.5; }
.panel h3 { margin: 4px 0 6px; font-size: 14px; letter-spacing: 0.06em; color: var(--dim-text); }
.panel pre { font-family: inherit; white-space: pre-wrap; margin: 0 0 10px; }
.panel hr { border: none; border-top: 1px solid #333a48; margin: 10px 0; }

@media (max-width: 700px) {
    .panel { width: 100%; }
}

.panel button, .select button, .feedback button {
    display: block;
    width: 100%;
    background: #2a2f3b;
    color: var(--panel-text);
    border: 1px solid #3c4454;
    padding: 7px 10px;
    margin-bottom: 5px;
    font-size: 14px;
    cursor: pointer;
    text-align: center;
}

.panel button:hover:not(:disabled) { background: #384051; }
.panel button:disabled { opacity: 0.45; cursor: default; }

/* Fingers need bigger targets than cursors (Interface: input assistance is invisible). */
@media (pointer: coarse) {
    .panel button, .select button, .feedback button, .stars button { min-height: 44px; }
}

.event-log { color: var(--dim-text); font-size: 13.5px; }
.order-now { color: var(--active); }

/* Prominence rebalance (Session 020). The forecast is what the player is deciding with, so it is
   the loudest text in the panel; the turn-order list keeps every number but stops competing for
   the same eye — the bar across the top already carries the ordering. */
.panel pre.forecast { font-size: 15.5px; }
.panel pre.forecast .odds-line { font-weight: 700; }
.panel h3.quiet { color: #6c7689; font-size: 12px; margin-top: 2px; }
.panel pre.turn-order-list { color: #7c8699; font-size: 12.5px; line-height: 1.35; }

/* ---- squad select ---- */
.select { padding: 12px 16px 32px; max-width: 860px; margin: 0 auto; }
.select h1 { font-size: 24px; margin: 6px 0; }
.select .premise { color: var(--panel-text); margin: 2px 0 10px; }
.select .facing { color: #f28c85; }
.select .hint { color: var(--dim-text); font-size: 13.5px; }
.select .points { color: #cfe0ff; }
.select .points.over { color: #ff7366; }

/* The one-click path in, visibly the default (steer by arrangement, not instruction). */
.select button.primary {
    background: #2f4a6e;
    border-color: var(--player);
    font-size: 16px;
    padding: 12px 14px;
    margin: 10px 0 0;
}
.select button.primary:hover { background: #3a5c88; }
.select .or { color: var(--dim-text); font-size: 13.5px; margin: 14px 0 2px; }

.select .tablewrap { overflow-x: auto; max-width: 100%; }
.select table { border-collapse: collapse; margin: 10px 0; }
.select td, .select th { padding: 5px 14px 5px 0; text-align: left; font-size: 15px; white-space: nowrap; }
.select th { color: var(--dim-text); font-weight: 500; font-size: 13px; }
.select .add { width: 70px; }
.select .chosen { color: #7fd4ff; }
.select .kit { color: #b6c2dc; }
.select .controls { display: flex; gap: 10px; flex-wrap: wrap; }
.select .controls button { width: auto; min-width: 130px; }

@media (max-width: 700px) {
    .select { padding: 10px 10px 24px; }
    .select td, .select th { padding: 5px 9px 5px 0; font-size: 14px; }
}

/* ---- post-battle feedback (S-13): 0-5 stars + comment, every play ---- */
.overlay {
    position: fixed;
    inset: 0;
    background: rgba(10, 12, 16, 0.82);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 14px;
    z-index: 10;
}

.feedback {
    background: #1d2028;
    border: 1px solid #3c4454;
    padding: 22px 26px;
    width: min(430px, 100%);
    max-height: 92vh;
    overflow-y: auto;
}

@media (max-width: 420px) {
    .feedback { padding: 16px 14px; }
}

.feedback h2 { margin: 0 0 4px; }
.feedback .subtle { color: var(--dim-text); font-size: 13.5px; margin-bottom: 12px; }

.stars { display: flex; gap: 6px; margin: 10px 0; }

.stars button {
    flex: 1;
    max-width: 46px;
    min-width: 0;
    height: 40px;
    font-size: 20px;
    margin: 0;
}

.stars button.lit { background: #5a512a; border-color: var(--active); }

.feedback textarea {
    width: 100%;
    height: 84px;
    background: #14161c;
    color: var(--panel-text);
    border: 1px solid #3c4454;
    padding: 8px;
    font-family: inherit;
    font-size: 14px;
    resize: vertical;
    margin-bottom: 10px;
}

.feedback .row { display: flex; gap: 8px; }
.feedback .note { color: var(--dim-text); font-size: 13px; margin-top: 8px; }

/* ---- attract reel: a recorded battle replaying itself under the squad screen (Session 021) ----
   Reuses .tiles/.tile/.unit wholesale by overriding --tile locally, so the demo cannot drift into
   a second, prettier renderer that shows something the real board does not. Its own --glide is
   slower than the game's: nothing here is waiting on a player, so motion can be read rather than
   tracked. */
.reel {
    margin-top: 34px;
    padding-top: 22px;
    border-top: 1px solid #333a48;
    --tile: 32px;
    --glide: 420ms;
}

.reel h2 {
    font-size: 15px;
    letter-spacing: 0.06em;
    margin: 0 0 4px;
}

.reel h2 .quiet { letter-spacing: 0; font-weight: 400; }
.reel .quiet { color: var(--dim-text); }

.reel-premise {
    color: var(--dim-text);
    font-size: 13.5px;
    margin: 0 0 14px;
    max-width: 60ch;
}

.reel-row { display: flex; gap: 22px; align-items: flex-start; }

.reel-board {
    position: relative;
    flex: none;
    width: calc(var(--tile) * var(--board-w, 10));
    height: calc(var(--tile) * var(--board-h, 10) + 8px);
}

/* The tile the actor is striking from, and the tile it landed on — the reel's whole visual
   grammar, since it has no cursor to follow. */
.reel-board .tile.reel-from { box-shadow: inset 0 0 0 100px rgba(255, 235, 115, 0.20); }
.reel-board .tile.reel-hit { box-shadow: inset 0 0 0 100px rgba(255, 115, 102, 0.34); }

.reel-call {
    font-size: 14px;
    line-height: 1.6;
    min-height: 150px;
    min-width: 34ch;
}

.reel-call pre { font-family: inherit; white-space: pre-wrap; margin: 0; }

/* Below this width the squad table and the enemy preview already own the screen; a second
   board underneath them is noise. gridlockWideScreen() also declines to start the loop at
   all, so this is the second of two locks, not the only one. */
@media (max-width: 1099px) {
    .reel { display: none; }
}

.select .recording { color: var(--dim-text); font-size: 12.5px; margin: 10px 0 14px; }

/* ---- unit sprites (Session 022) --------------------------------------------------------------
   The sprite fills its tile. It does not overflow upward and it is not anchored at the feet — that
   was the Final Fantasy Tactics convention for an oblique board, and Rob's read was right that it
   does not translate to a flat top-down grid. A top-down figure sits IN its square.

   Scale is always an integer multiple of 32: 2x at the 64px desktop tile, 1x at the 32px phone
   tile. Nothing is ever resampled at a fractional ratio. */
.unit.sprite {
    width: var(--tile);
    height: var(--tile);
    margin: 0;
    background-color: transparent;
    background-repeat: no-repeat;
    background-position: center;
    background-size: var(--tile) var(--tile);
    image-rendering: pixelated;
    overflow: visible;
}

/* The sprites are authored at 64x64 and the desktop tile is 64px, so the common case is 1:1 with
   no resampling at all. Session 022 measured why that matters: the same source processed to 32x32,
   48x48 and 64x64 side by side showed 32 destroying the face and the shield entirely — three
   quarters of the resolution was being thrown away, which is why the shipped set looked cruder
   than the probe Rob picked.

   The phone tile is 32px, and rather than let the browser halve a 64px asset (nearest-neighbour at
   0.5x drops every other pixel and aliases badly), a separate 32x32 set is authored from the same
   generation by the same post-process. Two assets, zero resampling, at either size. */
@media (max-width: 700px) {
    .unit.sprite { background-image: var(--sprite32) !important; }
}

/* Wide desktop: the 128-native set at 1:1 (S-15i). Same pattern as the phone swap — each
   breakpoint gets an asset processed to its exact display size, nothing is ever resampled. */
@media (min-width: 1800px) {
    .unit.sprite { background-image: var(--sprite128) !important; }
}

/* Team identity is a ring under the figure rather than the figure's own colour, so each job needs
   one sprite instead of two, and a dark silhouette never sinks into a dark tile. */
.unit.sprite::before {
    content: "";
    position: absolute;
    left: 50%;
    bottom: calc(var(--tile) * 0.06);
    transform: translateX(-50%);
    width: calc(var(--tile) * 0.62);
    height: calc(var(--tile) * 0.20);
    border-radius: 50%;
    background: var(--plate);
    border: 1px solid rgba(0, 0, 0, 0.75);
    z-index: -1;
}

.unit.sprite.player { --plate: #306082; }
.unit.sprite.enemy { --plate: #ac3232; }

/* `outline` traced the old opaque box; on a transparent sprite it frames empty air. drop-shadow
   follows the alpha channel, so the highlight hugs the figure. */
.unit.sprite.acting { outline: none; filter: drop-shadow(0 0 2px #fbf236) drop-shadow(0 0 6px #fbf236); }
.unit.sprite.targeted { outline: none; filter: drop-shadow(0 0 2px #ffffff) drop-shadow(0 0 6px #d95763); }

.unit.sprite .hp { bottom: 0; left: 50%; transform: translateX(-50%); width: calc(var(--tile) * 0.7); }
