/* Ali’s two pixel fonts — keep the real family names (do not rename/alias
   into a stripped “AliPixel” / pixel.ttf duplicate). Most in-game text is
   baked into art; these faces are for any live CSS text and future use.
   Both faces share the same printable set (incl. $ and %). */
@font-face {
  font-family: 'ALI PIXEL FONT CAPS 6x9';
  src: url('../fonts/ALI PIXEL FONT CAPS 6x9.ttf?v=20260727') format('truetype');
  font-display: swap;
}
@font-face {
  font-family: 'ALI PIXEL FONT CAPS 4x6';
  src: url('../fonts/ALI PIXEL FONT CAPS 4x6.ttf?v=20260727') format('truetype');
  font-display: swap;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: #1b1920;
  overflow: hidden;
  font-family: 'ALI PIXEL FONT CAPS 6x9', monospace;
  /* This is a fixed-layout game, never a scrollable/zoomable page - nothing
     here ever wants pinch-zoom or the double-tap-zoom gesture. Without this,
     two taps landing close together in time/position (e.g. a slightly missed
     first tap on a small hotspot like the champion carousel arrows, followed
     by a corrective second tap) get read by the browser as a double-tap and
     zoom the viewport in on that spot - the <meta viewport> maximum-scale/
     user-scalable=no above doesn't reliably stop this on its own (iOS Safari
     has ignored user-scalable=no for zoom gestures since iOS 10). This is the
     actual fix: manipulation keeps single taps/clicks and any legitimate
     panning working, it only drops the double-tap-to-zoom heuristic. */
  touch-action: manipulation;
}

/* Mobile Chrome/Safari draw a default translucent tap-highlight rectangle
   over whatever element receives a touch, sized to that element's own box -
   with no visible art of its own, a <button class="hotspot"> (e.g. the
   champion poke hitbox) shows this as a plain dark box briefly flashing
   over the character on tap. Not related to network/server speed at all -
   this is a per-tap client-side rendering default and would happen on the
   final hosted site too. Disabled everywhere rather than just on
   .hotspot, since every tappable element in the game (carousel arrows,
   difficulty picks, play/info buttons, board squares, etc.) is equally
   exposed to it. */
* {
  -webkit-tap-highlight-color: transparent;
}

#stage-wrapper {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* See index.html's trailing <script> for when/why this comes down. */
#loading-screen {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: #1b1920;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* 9px, not an arbitrary value: the dots sprite's rest/"down" frame has 6px
     of blank space (at this element's real 2x display scale) above the dot
     ink already baked into the art itself, so 9px of flex gap here plus that
     6px lands the true visible gap - from the bottom of the letters to the
     top of the dots - at exactly 15px, which is half the 30px letter height. */
  gap: 9px;
  transition: opacity 0.4s ease;
}
#loading-screen.loading-done {
  opacity: 0;
  pointer-events: none;
}
/* Hidden by default so the title <img> (needs its own network fetch to
   paint) and the dots (pure-CSS animation below, starts immediately once
   this stylesheet parses regardless of whether its sprite has actually
   downloaded yet) can't land at different moments - see the small inline
   script right after this div in index.html, which only reveals both
   together once both of THIS screen's own assets are ready. Separate from
   loading-done above, which is the later "everything in the whole game is
   ready" gate for taking this whole screen down. */
#loading-title, #loading-dots {
  visibility: hidden;
}
#loading-screen.loading-assets-ready #loading-title,
#loading-screen.loading-assets-ready #loading-dots {
  visibility: visible;
}
/* A real <img> (assets/loading_title.png - "MIDDLE CHESS" pre-rendered with
   ALI PIXEL FONT CAPS 6x9, same color/size the live text used), not live
   text - this is the very first thing painted on the page, before the
   6x9 @font-face (font-display: swap, above) has necessarily finished
   downloading, so live text here would flash in a fallback system font
   first and swap fonts a moment later. An image has no such race: it
   either hasn't loaded yet (nothing shows) or it has (the final art
   shows), never a wrong-font intermediate state.
   Letter/word spacing were re-tuned to internally-consistent units rather
   than eyeballed: inter-letter gap is 5px = the width of the font's own
   thinnest stroke (H's left leg, 1 native pixel at this graphic's 5x bake);
   the gap between "MIDDLE" and "CHESS" is 20px = exactly one glyph-cell
   width at this scale. Native size dropped from 332x30 to 285x30 as a
   result, so the width cap below matches the new native size (still capped
   for narrow viewports via 70vw, same as before). */
#loading-title {
  width: min(70vw, 285px);
  height: auto;
  image-rendering: pixelated;
}
/* Formation Chess (?formation=1 - class added by the inline swap script in
   index.html): "FORMATION CHESS" is a longer bake (360x30 native vs 285x30)
   from the same font/recipe, so the cap widens to its own native size to
   keep both titles at the identical 5x glyph scale. */
#loading-screen.formation #loading-title {
  width: min(70vw, 360px);
}
/* The actual authored "..." dots pixel art, lifted from the AI thinking-
   bubble sprites (assets/anim/thinking_bubble/c78-83.png) rather than a CSS
   approximation: that source art is a single flattened bubble+dots image,
   so assets/anim/loading_dots/sheet.png is a new asset built by isolating
   just the 3 dot shapes (dropping the speech-bubble body/outline) and
   recoloring them from the bubble's navy to this screen's own light text
   color, keeping the exact authored pixel shapes and motion.
   The real authored motion is a 5-step traveling wave, held extra frames
   at rest to breathe before it repeats - confirmed by comparing each dot's
   vertical position across all 6 raw source cels:
     step 1 (c78, held 4 frames): left down,  mid down, right down
     step 2 (c80):                left  up,   mid down, right down
     step 3 (c81):                left  up,   mid  up,  right down
     step 4 (c82):                left down,  mid  up,  right  up
     step 5 (c83):                left down,  mid down, right  up
     (then wraps back to step 1/c78 - NOT a raw c80->c83 wraparound, which
     would skip the rest state and double-flip left+right in the same tick)
   so the sheet's 8 frames are laid out left-to-right as
   c78,c78,c78,c78,c80,c81,c82,c83 (rest state held 4 frames rather than the
   raw art's implied 1, per playtesting feedback across two rounds that
   shorter holds read as too quick a breath).
   Pure CSS steps() sprite animation (no JS) so it's already animating from
   the very first paint, same as this div's previous CSS-keyframe circles -
   it doesn't need to wait for main.js to load and run before it can start.
   100ms/frame matches FRAME_MS, the same speed the live thinking bubble
   itself animates at.
   Displayed at a clean 2x the sprite's native 34x11-per-frame size (68x22).
   Earlier tried the pixel-perfect 3x (102x33) originally used, then a
   fractional 2.2x (75x24) split-the-difference size - 3x read as oversized
   next to the title graphic, and while 2.2x fixed that, it's not
   an integer multiple of the source pixels (unlike the title graphic, which
   is never scaled) so image-rendering:pixelated couldn't keep every source
   pixel a perfectly uniform size. 2x is both smaller again and a clean
   integer scale, so it doesn't have that softening.
*/
#loading-dots {
  width: 68px;
  height: 22px;
  background-image: url('../assets/anim/loading_dots/sheet.png');
  background-repeat: no-repeat;
  background-size: 544px 22px;
  image-rendering: pixelated;
  animation: loading-dots-step 0.8s steps(8) infinite;
}
@keyframes loading-dots-step {
  from { background-position: 0 0; }
  to { background-position: -544px 0; }
}

#stage {
  position: relative;
  width: 725px;
  height: 1024px;
  background: #1b1920;
  overflow: hidden;
  image-rendering: pixelated;
  transform-origin: center center;
  transform: scale(var(--scale, 1));
  flex-shrink: 0;
}

.screen {
  position: absolute;
  inset: 0;
  width: 725px;
  height: 1024px;
}

.hidden { display: none !important; }

/* Play screen is display:block but invisible while priming decode under splash. */
.prime-hidden {
  visibility: hidden !important;
  pointer-events: none;
}

.layer {
  position: absolute;
  left: 0;
  top: 0;
  width: 725px;
  height: 1024px;
  pointer-events: none;
  image-rendering: pixelated;
  user-select: none;
}

/* Each champion reaction sprite is a full 725x1024 canvas that bakes in
   BOTH a real window+character in its native slot AND an opaque (non-
   transparent) empty-window placeholder in the OTHER slot, since every
   champion's art was authored assuming a fixed native slot (see
   CHAMP_WINDOW_SHIFT_Y in data.js). #play-champ-top/#play-champ-bottom are
   translateY-shifted by JS to move whichever content belongs in that slot
   into view - but without clipping, the *unshifted* placeholder half of
   whichever image happens to stack on top (bottom always paints over top,
   being later in DOM order at the same z-index) would sit right on top of
   the other slot's real content whenever a champion is viewed in its own
   native slot (shift = 0), fully hiding it. These wrapper clips are the
   fix: each pins a fixed on-screen viewport around just its own slot's
   window art (bounds verified empirically against every reaction pose's
   real alpha-channel extent, with margin) so only whichever content the
   translateY shift has moved into that window can ever be visible, no
   matter which champion/slot pairing or shift is active. */
.champ-window-clip {
  /* Same box as the old bare .layer (0,0,725,1024) - deliberately NOT
     repositioned/resized to the slot itself, because the child img's
     translateY shift is computed in ITS OWN layout box (which still spans
     the full 0-1024 canvas, unshifted, inside this wrapper). clip-path
     (unlike overflow+height/top on the wrapper) clips a window fixed in
     THIS static box's own coordinate space, independent of the child's
     transform, so cropping happens post-shift, wherever the shifted
     content actually lands on screen - not pre-shift against the wrong
     portion of the child's box. */
  position: absolute;
  left: 0;
  top: 0;
  width: 725px;
  height: 1024px;
  overflow: hidden;
}
.champ-window-clip-top { clip-path: inset(0 0 874px 0); }
.champ-window-clip-bottom { clip-path: inset(874px 0 0 0); }

.icon-img {
  position: absolute;
  pointer-events: none;
  image-rendering: pixelated;
  user-select: none;
}

.hotspot {
  position: absolute;
  background: transparent;
  border: none;
  padding: 0;
  margin: 0;
  cursor: pointer;
  z-index: 50;
  outline: none;
  /* Same reason as the html/body rule above - put it on the actual
     tappable elements too, not just the document, so a double-tap on a
     hotspot (the case that actually fires the unwanted zoom) is covered
     even if a browser's touch-action ancestor walk is picky about it. */
  touch-action: manipulation;
}
.hotspot:focus { outline: none; }

/* PLAY is caged/non-interactive while an under-construction roster entry
   (see UNDER_CONSTRUCTION_CHAMPIONS in data.js) is showing - the cage art
   fully covers the button, so this just needs to stop the cursor reacting
   and stop clicks from doing anything, not hide/move anything. */
.hotspot-disabled {
  cursor: default;
  pointer-events: none;
}

/* ------------------------------------------------------------------ splash */
#difficulty-bar { position: absolute; inset: 0; }

/* ------------------------------------------------------------------- play */
/* Bubbles render at native art size (no upscale), tucked in tight against
   the near edge of their champion's window (speech bubbles poke out to
   the window's right with a small tail overlapping it; the thinking
   cloud sits just above-right of the character's head, closer in). */
.bubble-slot {
  position: absolute;
  z-index: 40;
  pointer-events: none;
  display: none;
}
.bubble-slot img { display: block; width: 100%; height: 100%; image-rendering: pixelated; }
/* Ground-truth pixel-measured directly from real recorded frames (source
   frame 176 for speech: exact outline bbox x408-521,y6-82 top / the
   equivalent bottom slot at the same x with the constant +872px vertical
   slot offset measured between the two windows; source frame 79 for the
   thinking cloud: exact bbox x400-468,y8-61). Both bubbles sit tucked in
   tight against their window's near edge, NOT floating far off to the
   side as previously placed. */
#bubble-top.speech      { left: 408px; top: 6px;   width: 114px; height: 77px; }
#bubble-top.thinking    { left: 400px; top: 8px;   width: 69px;  height: 54px; }
#bubble-bottom.speech   { left: 408px; top: 878px; width: 114px; height: 77px; }
#bubble-bottom.thinking { left: 400px; top: 880px; width: 69px;  height: 54px; }

#square-highlights { position: absolute; inset: 0; z-index: 12; pointer-events: none; }
/* Square selection highlight - a real 84x84 sprite (see HIGHLIGHT_SPRITES
   in data.js), swapped between the click-flash and sustain-stroke frame by
   setHighlight() in main.js. No computed color/border - the art itself
   already carries both colorways (white/black) and both states. */
.sq-highlight {
  position: absolute;
  width: 84px; height: 84px;
  pointer-events: none;
}

#piece-layer { position: absolute; inset: 0; z-index: 20; }
.piece {
  position: absolute;
  width: 84px; height: 84px;
  image-rendering: pixelated;
  cursor: pointer;
  z-index: 21;
  /* Suppress the browser's own native image-drag (ghost thumbnail + native
     dragstart) and touch scroll/zoom gestures so pointerdown/move/up in
     main.js's drag-to-move handling gets a clean, uninterrupted gesture
     instead of fighting the browser's built-in drag-an-<img> behavior. */
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-user-drag: none;
  /* iOS/Android long-press on an <img> opens "Save Image" / link preview
     callout - same gesture players use to hold a piece while thinking. */
  -webkit-touch-callout: none;
}
/* A piece actively being dragged is still the exact same plain <img> at its
   native size/art - just raised above every other piece (and the
   king-cross-fx burst, z-index 45) so it visibly rides on top of whatever
   square it's currently over. No scale/shadow/recolor - it looks identical
   to a resting piece, only its position and stacking change. */
.piece.dragging {
  z-index: 55;
  cursor: grabbing;
}

/* Illegal-destination-click feedback: a real 3-frame animated X (see
   ILLEGAL_SQUARE_ANIM_FRAMES in data.js, showIllegalX in main.js) - sits
   above every piece so it's never partially hidden by the piece occupying
   that square. */
.illegal-x {
  width: 84px; height: 84px;
  z-index: 46;
  display: none;
}

/* A won game's king-crossed sparkle burst is real art that intentionally
   overflows its own tile on every side - this is the exact same king
   <img>, just temporarily grown/repositioned and raised above everything
   else while its src cycles through the burst frames (see applyKingAnim in
   main.js). Never a separate element. */
.piece.king-cross-fx {
  width: 110px; height: 120px;
  z-index: 45;
}

/* ------------------------------------------------ Formation Chess overlays */
/* Setup-period art (?formation=1 — see the FORMATION CHESS section in
   main.js). Cover hides the opponent's half above the pieces (21) but below
   the illegal-X (46) so a bad drop onto the covered half still shows its X;
   the DONE plate sits above that; the done hotspot uses the standard
   .hotspot z (50); a dragged piece (55) rides over everything, same as the
   in-game drag. All hidden by default — normal games never show them. */
#formation-cover { z-index: 42; display: none; }
#formation-shine { z-index: 43; display: none; }
#formation-done-img { z-index: 47; display: none; }

/* --------------------------------------------------------------- captured trays */
.tray {
  position: absolute;
  width: 242px; height: 26px;
  z-index: 30;
  image-rendering: pixelated;
}
#tray-top { left: 20px; top: 91px; }
#tray-bottom { left: 20px; top: 119px; }
.tray-slot {
  position: absolute;
  top: 0;
  width: 16px; height: 26px;
  image-rendering: pixelated;
  pointer-events: none;
}

/* Occupies the exact same 16x26 footprint as one captured-piece slot and
   sits flush against the tray's ground line, immediately to the left of
   the first slot. Real authored glyphs (extracted from mockup_14's
   "material + white/black" and "material white/black numbers" layers),
   positioned as absolutely-placed children rather than CSS text - the
   pixel-measured source layout has the plus sign flush with the box's
   bottom-right corner group and the digit immediately to its right with a
   2px gap, both glyphs' bottom edges flush with the box's bottom edge
   (the tray's own ground line). Multi-digit values grow leftward off the
   box's right edge so the ones-digit always lands in the source-authored
   spot. */
.tray-advantage {
  position: absolute;
  width: 16px;
  height: 26px;
  z-index: 30;
}
.tray-advantage img {
  position: absolute;
  bottom: 0;
  image-rendering: pixelated;
}
.tray-advantage .adv-plus { width: 6px; height: 6px; }
.tray-advantage .adv-digit { width: 8px; height: 12px; }
#tray-adv-top { left: 4px; top: 91px; }
#tray-adv-bottom { left: 4px; top: 119px; }

#diff-readout-img {
  image-rendering: pixelated;
  pointer-events: none;
  z-index: 30;
}

/* --------------------------------------------------------------------- clock */
/* clock_top.png / clock_bottom.png are the real housing art (re-extracted
   from mockup_14's clean "timer bg" layer - a blank plate with no baked
   placeholder digits, unlike the old art), full-stage size, used directly
   as `.layer`s in the HTML. The digits/colon are real authored art
   (SegClock/segdisplay.js swaps <img> src per digit/color instead of
   drawing SVG segments), overlaid at the display's exact position within
   that full-canvas art. */
.clock-frame {
  position: absolute;
  left: 569px; width: 134px; height: 51px;
  z-index: 30;
}
.seg-burnin {
  position: absolute;
  left: 0; top: 0;
  width: 134px; height: 51px;
  image-rendering: pixelated;
  display: none;
}
#clock-top { top: 78px; }
#clock-bottom { top: 895px; }
.seg-digit {
  position: absolute;
  top: 0; width: 26px; height: 51px;
  image-rendering: pixelated;
}
.seg-colon {
  position: absolute;
  top: 11px; width: 5px; height: 25px;
  image-rendering: pixelated;
}

/* --------------------------------------------------------------------- modals */
.modal {
  position: absolute;
  inset: 0;
  z-index: 100;
}

/* The info window's background art sits at a razor-thin margin from both
   the difficulty bar above it (~3px clearance from the "easy" pill) and
   the play button below it (~4px clearance) - moving the whole window
   either direction just trades one clipping bug for the other. Since
   nothing behind an open modal should be visible anyway, simply hide the
   specific splash elements that risk peeking past those thin edges while
   the info modal is open. */
#splash-screen.info-open #difficulty-bar,
#splash-screen.info-open #play-btn-art,
#splash-screen.info-open #play-btn-shine,
#splash-screen.info-open #play-btn-cage,
#splash-screen.info-open #info-arrow-img {
  visibility: hidden;
}

