/* ═════════════════════════════════════════════════════════════════════
   GAME ENGINE (CSS) — the tier-3 skill-game layer.
   Extracted 2026-08-20 from the OWNER-APPROVED D&G sling game
   (story-david-goliath.html keeps its page-local originals untouched —
   it is the reference implementation and must never be migrated).

   Load order on a game page:
     story-engine.css  →  game-engine.css  →  page <style>
   Only game pages load this file. Cache-bust as game-engine.css?v=N.
   The JS half is game-engine.js (window.Game; requires story-engine.js).

   THE FROZEN CONTRACT lives in docs/GAME-TOOLKIT.md — build agents code
   against that doc. Changes to this file go through ONE integrator.

   Page skeleton a game section follows:
     <section class="game" id="game">
       <div class="bg" style="background-image:url(…)"></div>
       <div class="veil"></div><div class="veil-b"></div>
       …one .game-actor per transparent character (page sizes it)…
       <canvas class="game-canvas" id="gCanvas"></canvas>
       <div class="game-zone" id="gZone" role="button" aria-label="…"></div>
       <div class="game-ui"><p class="pe">Scene N · Playable</p>
         <h2>…</h2><p class="game-msg" id="gMsg"></p></div>
       <div class="game-tally" id="gTally"><span>Stones</span><i></i>…</div>
       <div class="game-hint" id="gHint"><svg…></svg><span id="gHintTxt">…</span></div>
       <div class="impact-flash" id="gFlash"></div>
       <div class="gmontage" id="gFilm" aria-hidden="true">
         <img src="…" style="--x:8%; --y:9%; --w:min(64vw,470px); --rot:-3deg">
         …   <img src="…" class="wide" style="object-position:40% 50%">
       </div>
       <div class="game-win" id="gWin"><p class="big">…</p><p class="vr">Ref</p>
         <button class="go-on show" id="gWinGo">…</button></div>
     </section>
   ═════════════════════════════════════════════════════════════════════ */

/* ---------- the game stage ---------- */
.game {
  position:relative; height:100vh; height:100svh; overflow:hidden; background:var(--bg);
  /* a long-press must aim, not select text or pop the copy callout */
  user-select:none; -webkit-user-select:none; -webkit-touch-callout:none;
}
/* the arena backdrop sits deep in dusk so the actors and glyphs carry.
   A page may override the grade in its own <style>. */
.game > .bg { filter: brightness(.3) saturate(.8); }

/* the canvas only draws — input lives on the .game-zone, so swipes
   anywhere else scroll the page normally instead of firing the game */
/* width/height 100% is LOAD-BEARING: without it a canvas with inset:0
   lays out at its ATTRIBUTE size (stage × dpr), so on retina screens every
   drawn glyph lands at 2-3× coords — mostly off-stage. That was the D&G
   phones' "no aim feedback at all" bug. Never remove these two lines. */
.game-canvas { position:absolute; inset:0; width:100%; height:100%; z-index:10; pointer-events:none; }

/* ---------- input zone ---------- */
/* desktop: an 84px grab dot the game JS rides on its anchor each frame */
.game-zone {
  position:absolute; width:84px; height:84px; z-index:11;
  border-radius:50%; touch-action:none; cursor:grab;
  -webkit-tap-highlight-color: transparent;
}
.game-zone:active { cursor:grabbing; }
/* touch/narrow: a thumb can't find an 84px dot — the whole valley floor
   becomes the zone (press anywhere low, pull, release). Game JS toggles
   .band-grab via Game.bandMode() and clears the inline left/top. */
.game.band-grab .game-zone {
  left:0; right:0; top:auto; bottom:0; width:auto; height:34%;
  border-radius:0;
}

/* ---------- transparent character layers ---------- */
/* pages set height + aspect-ratio per asset (use the PNG's real pixel
   ratio — overflow-cropping a two-figure sheet is how the bust leaked) */
.game-actor { position:absolute; bottom:0; pointer-events:none; }
.game-actor img { position:absolute; inset:0; width:100%; height:100%; }

/* ---------- header + message ---------- */
.game-ui { position:absolute; top:12vh; left:0; right:0; text-align:center; z-index:12; pointer-events:none; padding:0 20px; }
.game-ui .pe, .game-ui h2 { transition: opacity .8s ease; }
.game-ui.dim .pe, .game-ui.dim h2 { opacity:0; } /* once play starts, the title bows out */
.game-ui .pe { font:600 11px/1 var(--font-body); letter-spacing:.32em; text-transform:uppercase; color:var(--gold); }
.game-ui h2 { font:400 clamp(28px,4.4vw,48px)/1.15 var(--font-display); margin-top:12px; text-shadow:0 4px 30px #000; }
.game-ui h2 em { font-style:italic; color:var(--gold-bright); }
.game-msg { margin-top:12px; min-height:30px; font:italic 400 clamp(16px,2vw,20px)/1.5 var(--font-display); color:var(--gold-bright); text-shadow:0 2px 14px #000; transition:opacity .3s; }

/* while aiming, the chrome gets out of the way (Game.aiming(true/false)) */
body.game-aiming .hud, body.game-aiming .hud-bar { opacity:.12; }

/* Story.shake(host) on a .game section — story-engine.css only wires
   .stage.shaking, so without this rule every montage shake was a silent
   no-op (it was one in the D&G original too; here it works as intended) */
.game.shaking { animation: shakeHit .38s cubic-bezier(.36,.07,.19,.97); }

/* ---------- resource tally (stones / tries / jars…) ---------- */
.game-tally { position:absolute; left:20px; top:14vh; z-index:12; display:flex; flex-direction:column; gap:8px; align-items:center; }
.game-tally span { font:600 9.5px/1 var(--font-body); letter-spacing:.24em; text-transform:uppercase; color:var(--ink-faint); writing-mode:vertical-rl; }
.game-tally i { width:13px; height:11px; border-radius:48% 52% 55% 45%; background:#b9c0cf; box-shadow: inset -2px -2px 3px rgba(0,0,0,.4); transition: opacity .3s; }
.game-tally i.used { opacity:.14; }

/* ---------- how-to-play hint ---------- */
.game-hint {
  position:absolute; bottom:8vh; left:50%; transform:translateX(-50%); z-index:12; pointer-events:none;
  display:flex; align-items:center; gap:10px;
  background:rgba(10,12,18,.6); border:1px solid var(--stroke); backdrop-filter:blur(10px);
  padding:12px 20px; font:500 12px/1.5 var(--font-body); letter-spacing:.06em; color:var(--ink-dim);
  clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px);
  transition: opacity .4s;
}
.game-hint svg { width:17px; height:17px; stroke:var(--gold); stroke-width:1.6; fill:none; stroke-linecap:round; stroke-linejoin:round; }
.game-hint b { color:var(--gold-bright); font-weight:600; }

/* ---------- impact flash ---------- */
.impact-flash { position:absolute; inset:0; z-index:14; background:#fff; opacity:0; pointer-events:none; }
.impact-flash.hit { animation: gmFlash .5s ease-out; }
@keyframes gmFlash { 0%{opacity:.85} 100%{opacity:0} }

/* ---------- the impact montage ---------- */
/* the win reveal: gold-framed, tilted 16:9 cards slam in and STACK over
   the dimmed stage, the whiteout scatters the stack, and the .wide frame
   settles full-bleed under the win overlay. Cards show their WHOLE 16:9
   frame — nothing gets portrait-cropped (the bust rule, by construction).
   Per-card position/size/tilt ride inline --x/--y/--w/--rot vars. */
.gmontage { position:absolute; inset:0; z-index:13; display:none; background:rgba(6,7,11,.5); }
.gmontage img {
  position:absolute; left:var(--x); top:var(--y); width:var(--w);
  aspect-ratio:16/9; object-fit:cover; opacity:0;
  border:1px solid rgba(217,171,90,.4);
  box-shadow: 0 18px 50px rgba(0,0,0,.65);
  clip-path: polygon(14px 0, 100% 0, 100% calc(100% - 14px), calc(100% - 14px) 100%, 0 100%, 0 14px);
  transform: rotate(var(--rot)) scale(1.35);
}
.gmontage img.on { animation: gmCardSlam .42s cubic-bezier(.2,.8,.3,1) both; }
@keyframes gmCardSlam {
  0%   { opacity:0; transform: rotate(calc(var(--rot) + 4deg)) scale(1.45); }
  62%  { opacity:1; transform: rotate(var(--rot)) scale(.985); }
  100% { opacity:1; transform: rotate(var(--rot)) scale(1); }
}
.gmontage img.away { animation: gmCardAway .45s ease both; }
@keyframes gmCardAway {
  from { opacity:1; transform: rotate(var(--rot)) scale(1); }
  to   { opacity:0; transform: rotate(var(--rot)) scale(.93); }
}
/* the full-bleed settle frame — MUST survive a ~26% portrait strip
   (face close-ups crop heads; pick a wide, then set object-position) */
.gmontage img.wide {
  inset:0; left:0; top:0; width:100%; height:100%; aspect-ratio:auto;
  clip-path:none; border:none; box-shadow:none; transform:none;
}
.gmontage img.wide.on { animation: gmWideIn 1.7s ease both; }
@keyframes gmWideIn { from { opacity:0; transform:scale(1.05); } to { opacity:1; transform:none; } }
.gmontage::after { /* pull the bright film frames into the page's dusk grade */
  content:''; position:absolute; inset:0; pointer-events:none;
  background: radial-gradient(115% 90% at 50% 42%, transparent 52%, rgba(6,7,11,.55));
}

/* ---------- win overlay ---------- */
.game-win {
  position:absolute; inset:0; z-index:14; display:none; align-items:center; justify-content:center;
  flex-direction:column; gap:18px; text-align:center; padding:0 24px;
  background: radial-gradient(80% 60% at 50% 55%, rgba(6,7,11,.2), rgba(6,7,11,.78));
  pointer-events:none;
}
.game-win.show { display:flex; animation: gmFadeUp 1s ease both .5s; }
@keyframes gmFadeUp { from { opacity:0; translate:0 14px; } to { opacity:1; translate:0 0; } }
.game-win p.big { font: italic 400 clamp(26px,4vw,44px)/1.3 var(--font-display); color:#fff; text-shadow:0 4px 30px #000; max-width:16em; }
.game-win p.vr { font:500 11px/1 var(--font-body); letter-spacing:.24em; text-transform:uppercase; color:var(--gold); }
.game-win .go-on { pointer-events:auto; }

/* ---------- phones ---------- */
@media (max-width: 720px) {
  /* portrait: landscape compositions collapse — actors get page-local
     portrait sizes; the shared chrome tightens up here */
  .game-tally { top:9vh; left:14px; bottom:auto; flex-direction:row; }
  .game-tally span { writing-mode:horizontal-tb; }
  .game-hint { bottom:4vh; width:min(320px, 86vw); }
  .game-ui { top: calc(64px + 5vh); }
}

@media (prefers-reduced-motion: reduce) {
  .gmontage img.on, .gmontage img.away, .gmontage img.wide.on { animation-duration:.01s; }
  .impact-flash.hit { animation:none; }
  .game.shaking { animation:none; }
  .game-win.show { animation:none; }
}
