/* Colchis frame: every selector is fenced off [data-cf] (gen-css.mjs --fence). */
/* ============================================================================
   motion.css — Rex vs Mummies motion system.

   MUST be linked LAST, after cross-board.css and mummies-layout.css:
     <link rel="stylesheet" href="./motion.css">

   Contents
     §0  the timing scale, mirrored from presentation.mjs
     §1  compositor hygiene — animations that were stuck on the main thread
     §2  the idle scene: awake / dormant / paused / reduced
     §3  gameplay feedback moments
     §4  the tiered win escalation
     §5  merged-block grid placement (replaces positionMerged pixel writes)
     §6  reduced motion, low power
   ============================================================================ */

/* ─────────────────────────────────────────────────────── §0 the timing scale
   presentation.mjs::publishScale() overwrites these on :root at boot and again
   whenever turbo or prefers-reduced-motion changes. The values here are the
   normal-speed defaults, so the sheet is correct even before the module runs. */
:root:where(:not([data-cf],[data-cf] *)){
 --mo-instant:70ms; --mo-tick:140ms; --mo-quick:240ms; --mo-beat:420ms;
 --mo-phrase:700ms; --mo-scene:1200ms; --mo-hold:2000ms;
 --ease-out:cubic-bezier(.22,.61,.36,1);
 --ease-in:cubic-bezier(.55,.06,.68,.19);
 --ease-inOut:cubic-bezier(.65,0,.35,1);
 --ease-land:cubic-bezier(.12,.62,.24,1);
 --ease-back:cubic-bezier(.34,1.42,.64,1);
 --ease-soft:cubic-bezier(.4,0,.6,1);
 --ease-step:steps(1,end);
 /* Per-cell stagger for staggered group reveals. Read as calc(var(--i) * …). */
 --mo-stagger:70ms;
}

/* ──────────────────────────────────────────────── §1 compositor hygiene ─────
   Every rule in this block removes main-thread work from an animation that was
   already running. None of them change what the game looks like at rest. */

/* `.tile[data-symbol=zombie]` animated `box-shadow` forever, on up to 25 tiles.
   box-shadow is a paint property: every frame repainted every matching tile.
   Freeze it at the bright half of its own cycle — same look, zero cost. */
.tile[data-symbol=zombie]:where(:not([data-cf],[data-cf] *)){
 animation:none!important;
 box-shadow:inset 0 0 18px #ffd86c8c,0 0 11px #ffd04f96;
}

/* `mix-blend-mode` forces an element out of the compositor, so its `opacity`
   keyframes were recalculating style on the main thread every frame. The alphas
   below are raised to hold the same apparent brightness without the blend. */
.candle-halo:where(:not([data-cf],[data-cf] *)){mix-blend-mode:normal!important}
.candle-halo:where(:not([data-cf],[data-cf] *)){background:radial-gradient(ellipse at center,#ffd9ad66 0%,#ff6a3f44 20%,#d8262622 42%,transparent 68%)}
.outpost-candle .candle-halo:where(:not([data-cf],[data-cf] *)){background:radial-gradient(ellipse,#ffdcb070,#ff4a3550 25%,#dd1a2c28 45%,transparent 70%)}

/* `will-change` on an idle element pins a texture for the lifetime of the page.
   An infinite transform/opacity animation is promoted by Chromium anyway, so the
   hint buys nothing and costs GPU memory on exactly the devices that can least
   afford it. Budget: zero persistent will-change while idle. */
.candle-relight:where(:not([data-cf],[data-cf] *)),.outpost-fog .fog-wave:where(:not([data-cf],[data-cf] *)),.spin-strip:where(:not([data-cf],[data-cf] *)),.reel-strip:where(:not([data-cf],[data-cf] *)){
 will-change:auto!important;
}

/* The candle "relight" patch repeats the whole 1536x1024 plate as its own
   background, seven times over, and was animating its opacity on top of that.
   Hold it static: the seven textures rasterise once and are never touched again.
   All the flicker now lives in the gradient halo, which is a cheap layer. */
.candle-relight:where(:not([data-cf],[data-cf] *)){animation:none!important;opacity:.52}

/* Containment stops a change inside the scene layers from invalidating the page. */
.outpost-light-plane:where(:not([data-cf],[data-cf] *)){contain:layout paint;backface-visibility:hidden}
.outpost-fog:where(:not([data-cf],[data-cf] *)){contain:strict}

/* ─────────────────────────────────────────────────── §2 the idle scene ─────
   Three gates, cheapest first:
     .scene-dormant   no input for 45s          → every loop paused,0 running
     .effects-paused  tab hidden or window blur → every loop paused
     .low-power       battery saver / save-data / slow display / <=2 cores
                      → fog dropped to one wisp, halo cadence halved
   All three are additive and all three are reversible in one style recalc. */

body.scene-dormant .outpost-candle i:where(:not([data-cf],[data-cf] *)),body.scene-dormant .outpost-fog .fog-wave:where(:not([data-cf],[data-cf] *)),body.effects-paused .outpost-candle i:where(:not([data-cf],[data-cf] *)),body.effects-paused .outpost-fog .fog-wave:where(:not([data-cf],[data-cf] *)){
 animation-play-state:paused!important;
}
/* Dormant is not "frozen mid-flicker": the halos settle to one calm value. */
body.scene-dormant .candle-halo:where(:not([data-cf],[data-cf] *)){opacity:.58;transform:scale(1)}
body.scene-dormant .fog-wave:where(:not([data-cf],[data-cf] *)){opacity:var(--fog-opacity,.42)}
/* Waking is a fade, not a jump. */
.candle-halo:where(:not([data-cf],[data-cf] *)){transition:opacity var(--mo-scene) var(--ease-soft)}

/* A full-screen celebration owns the screen; the tomb behind it stops working. */
body.celebrating-big .outpost-candle i:where(:not([data-cf],[data-cf] *)),body.celebrating-big .outpost-fog .fog-wave:where(:not([data-cf],[data-cf] *)),body.celebrating-mega .outpost-candle i:where(:not([data-cf],[data-cf] *)),body.celebrating-mega .outpost-fog .fog-wave:where(:not([data-cf],[data-cf] *)){animation-play-state:paused!important}

.low-power .outpost-fog .fog-wave:nth-child(n+2):where(:not([data-cf],[data-cf] *)){display:none}
.low-power .candle-halo:where(:not([data-cf],[data-cf] *)){animation-duration:calc(var(--flame-duration) * 2)!important}

/* ──────────────────────────────────────── §3 gameplay feedback moments ─────
   Each block states its duration token, the sfx it is matched to, and that
   sound's declared length in audio.mjs. Motion and audio share one clock. */

/* ---- a paid arm MATCHED — DUR.beat 420ms · sfx.expandSuccess 540ms --------
   The three new cells rise and brighten in reveal order,70ms apart, so the eye
   reads the block growing outward rather than three cells blinking at once. */
.tile.arm-match:where(:not([data-cf],[data-cf] *)){animation:arm-match var(--mo-beat) var(--ease-out) calc(var(--i,0) * var(--mo-stagger)) both}
.tile.arm-match .art:where(:not([data-cf],[data-cf] *)){animation:arm-match-gold var(--mo-beat) var(--ease-out) calc(var(--i,0) * var(--mo-stagger)) both}
@keyframes arm-match{0%{transform:scale(1)}38%{transform:scale(1.065)}100%{transform:scale(1)}}
@keyframes arm-match-gold{0%{filter:brightness(1)}34%{filter:brightness(1.48) saturate(1.2)}100%{filter:brightness(1)}}

/* ---- a paid arm MISSED — DUR.beat 420ms · sfx.expandFail 420ms ------------
   Deliberately neutral. The cells settle three pixels and come back, luminance
   dips and recovers. No red, no shake, no shrink, no stamp, nothing removed.
   It reads "that attempt is closed", which is information, not a verdict. */
.tile.arm-miss:where(:not([data-cf],[data-cf] *)){animation:arm-miss var(--mo-beat) var(--ease-inOut) calc(var(--i,0) * calc(var(--mo-stagger) * .72)) both}
@keyframes arm-miss{0%{transform:translateY(0);opacity:1}45%{transform:translateY(3px);opacity:.82}100%{transform:translateY(0);opacity:1}}

/* ---- a corner attempt opening — DUR.quick 240ms · sfx.cornerAttempt 260ms */
.tile.corner-arming:where(:not([data-cf],[data-cf] *)){animation:corner-arming var(--mo-quick) var(--ease-step) both}
@keyframes corner-arming{0%{transform:scale(1);opacity:1}25%{transform:scale(1.03);opacity:.7}50%{transform:scale(1);opacity:1}75%{transform:scale(1.03);opacity:.7}100%{transform:scale(1);opacity:1}}

/* ---- a corner MATCHED — DUR.phrase + DUR.quick = 940ms · sfx.cornerMatch 800ms
   One conic ring sweeps a full turn around the 3x3 square, then the ninth cell
   pops. The ring is one grid-placed element rotating: transform only. */
.corner-ring:where(:not([data-cf],[data-cf] *)){
 position:relative;z-index:5;pointer-events:none;align-self:stretch;justify-self:stretch;
 background:conic-gradient(from 0deg,transparent 0deg,transparent 200deg,var(--lime,#a2efc4) 300deg,#fff6d0 345deg,transparent 360deg);
 -webkit-mask-image:radial-gradient(closest-side,transparent 60%,#000 66%,#000 78%,transparent 84%);
 mask-image:radial-gradient(closest-side,transparent 60%,#000 66%,#000 78%,transparent 84%);
 animation:corner-ring var(--mo-sweep,700ms) var(--ease-out) both;
}
@keyframes corner-ring{0%{transform:rotate(0);opacity:0}12%{opacity:1}82%{opacity:1}100%{transform:rotate(360deg);opacity:0}}
.tile.corner-hit:where(:not([data-cf],[data-cf] *)){animation:corner-hit var(--mo-quick) var(--ease-back) var(--mo-phrase) both}
@keyframes corner-hit{0%{transform:scale(1)}55%{transform:scale(1.13)}100%{transform:scale(1)}}

/* ---- a corner MISSED — DUR.beat 420ms · sfx.cornerMiss 380ms --------------
   The eight-cell block keeps every pixel of its win. Only the one cell that was
   bought settles back, and it settles to 62%, not to nothing. */
.tile.corner-miss:where(:not([data-cf],[data-cf] *)){animation:corner-miss var(--mo-beat) var(--ease-inOut) both}
@keyframes corner-miss{0%{opacity:1;transform:scale(1)}40%{opacity:.62;transform:scale(.97)}100%{opacity:.78;transform:scale(1)}}

/* ---- Rex lands a +1 charge — 620ms total · sfx.charge 620ms ---------------
   The pip's journey is a WAAPI transform (presentation.mjs::chargeLand); this
   only dresses it. position:fixed because the start and end points come from
   getBoundingClientRect, i.e. viewport coordinates. */
.charge-pip:where(:not([data-cf],[data-cf] *)){
 position:fixed;z-index:9000;pointer-events:none;
 translate:-50% -50%;
 font:900 clamp(15px,3.4vw,24px)/1 Arial,sans-serif;
 color:#fff3d4;text-shadow:0 2px 4px #000,0 0 14px var(--lime,#a2efc4);
 padding:3px 9px;border-radius:999px;
 background:radial-gradient(ellipse,#3c8f6ef2,#123227f2);
 border:2px solid var(--lime,#a2efc4);
 box-shadow:0 0 18px #6fe0ad66;
}
.charge.charge-pop:where(:not([data-cf],[data-cf] *)){animation:charge-pop var(--mo-tick) var(--ease-back) both}
@keyframes charge-pop{0%{transform:scale(.4)}60%{transform:scale(1.35)}100%{transform:scale(1)}}

/* ---- a bonus stage advances — 300ms · sfx.bonusStep 300ms ----------------- */
.bonus-step-chip:where(:not([data-cf],[data-cf] *)){
 position:absolute;right:8px;top:50%;z-index:6;pointer-events:none;
 translate:0 -50%;
 font:800 11px/1 Arial,sans-serif;letter-spacing:.09em;
 color:#fff2d2;padding:4px 8px;border-radius:3px;
 background:linear-gradient(100deg,#3a2450f0,#15102af0);
 border:1px solid var(--amber,#edd3a1);
 animation:bonus-step var(--mo-step,300ms) var(--ease-back) both;
}
@keyframes bonus-step{0%{transform:translateY(6px) scale(.86);opacity:0}30%{transform:none;opacity:1}78%{opacity:1}100%{transform:translateY(-4px);opacity:0}}

/* ──────────────────────────────────────── §4 the tiered win escalation ─────
   Four signatures that differ in KIND, so the tier is legible from motion alone
   before the number is readable:
     WIN    no dim, no scale — a chip rises into place
     POWER  no dim, one overshoot pop and a rim glow
     BIG    dim, medallion impact, one spark burst, a gold seal sweep
     MEGA   dim, the title marches in glyph by glyph, the number swells across a
            second act, a second burst fires on the seal, Rex bounces           */

/* --- tiers 1-2: nothing dims, nothing scales the screen -------------------- */
.win-celebration.win .win-compact:where(:not([data-cf],[data-cf] *)){animation:chip-rise var(--mo-entrance,140ms) var(--ease-out) both}
@keyframes chip-rise{0%{opacity:0;transform:translateY(9px)}100%{opacity:1;transform:none}}
.win-celebration.power .win-compact:where(:not([data-cf],[data-cf] *)){animation:plate-pop var(--mo-entrance,240ms) var(--ease-back) both}
@keyframes plate-pop{0%{opacity:0;transform:translateY(8px) scale(.9)}100%{opacity:1;transform:none}}

/* --- tiers 3-4: the screen dims, as its own layer, so the plate stays sharp -*/
.win-celebration.big:where(:not([data-cf],[data-cf] *)),.win-celebration.mega:where(:not([data-cf],[data-cf] *)){background:none}
.win-celebration.big:where(:not([data-cf],[data-cf] *))::before,.win-celebration.mega:where(:not([data-cf],[data-cf] *))::before{
 content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
 background:radial-gradient(ellipse at center,#04100b99,#020604cc 70%);
 animation:dim-in var(--mo-entrance,240ms) var(--ease-out) both;
}
.win-celebration.mega:where(:not([data-cf],[data-cf] *))::before{background:radial-gradient(ellipse at center,#160a1fa6,#04020ad9 70%)}
@keyframes dim-in{from{opacity:0}to{opacity:1}}

.win-celebration.big .win-medallion:where(:not([data-cf],[data-cf] *)){animation:medallion-impact var(--mo-entrance,240ms) var(--ease-back) both}
.win-celebration.mega .win-medallion:where(:not([data-cf],[data-cf] *)){animation:medallion-impact-mega calc(var(--mo-entrance,240ms) * 1.6) var(--ease-back) both}
@keyframes medallion-impact{0%{opacity:0;transform:scale(.72)}62%{opacity:1;transform:scale(1.055)}100%{opacity:1;transform:scale(1)}}
@keyframes medallion-impact-mega{0%{opacity:0;transform:scale(.55) rotate(-2.5deg)}48%{opacity:1;transform:scale(1.09) rotate(1deg)}78%{transform:scale(.985) rotate(-.4deg)}100%{opacity:1;transform:none}}

/* MEGA's signature tell: the title arrives one glyph at a time on a steps
   cadence. Discrete, so there is no interpolation blur and it reads as a march. */
.win-title.marching:where(:not([data-cf],[data-cf] *)){display:inline-block}
.win-title.marching b:where(:not([data-cf],[data-cf] *)){display:inline-block;font:inherit;opacity:0;animation:glyph-march var(--mo-tick,140ms) var(--ease-step) calc(var(--i,0) * 52ms) both}
@keyframes glyph-march{0%{opacity:0;transform:translateY(-14px) scale(1.5)}55%{opacity:1;transform:translateY(2px) scale(.94)}100%{opacity:1;transform:none}}

/* The value swells across act two of the count-up. presentation.mjs writes the
   scale per frame; this only guarantees a transform origin and no layout jitter.
   No `will-change`: a per-frame transform write already promotes the element, and
   the budget "zero persistent will-change" is easier to police with no exceptions. */
.win-value:where(:not([data-cf],[data-cf] *)){transform-origin:50% 60%;font-variant-numeric:tabular-nums;white-space:nowrap}

/* The seal: one gold band crosses the plate the instant the count lands. */
.win-sweep:where(:not([data-cf],[data-cf] *)){
 position:absolute;inset:0;z-index:3;pointer-events:none;opacity:0;
 background:linear-gradient(105deg,transparent 30%,#fff6cf55 47%,#fff9e0aa 50%,#fff6cf55 53%,transparent 70%);
 background-size:280% 100%;
}
.win-celebration[data-settled=true] .win-sweep:where(:not([data-cf],[data-cf] *)){animation:win-seal var(--mo-seal,420ms) var(--ease-out) both}
@keyframes win-seal{0%{opacity:0;transform:translateX(-65%)}22%{opacity:1}100%{opacity:0;transform:translateX(65%)}}

/* One canvas replaces 46-64 spark elements. Sized to the plate, not the screen. */
.win-sparkfield:where(:not([data-cf],[data-cf] *)){
 position:absolute;left:50%;top:50%;z-index:1;pointer-events:none;
 translate:-50% -50%;
 contain:strict;
}
/* No blend mode on the canvas: the 2D context already composites additively with
   `lighter`, and a CSS blend would take the whole layer off the compositor. */

/* Rex only bounces for BIG and MEGA, and only twice{animation-iteration-count:2!important}

/* The board frame reacts once per tier and never loops. */
.win-impact:where(:not([data-cf],[data-cf] *)){animation:win-impact var(--mo-quick) var(--ease-out) 1!important}
/* game.css tints this glow lime (#a7f951), a leftover from the pre-Egyptian theme;
   at 8px with no offset it bled green through the whole cabinet. Warm gold instead, which is the accent this art direction already uses for a win. */
.win-soft-glow:where(:not([data-cf],[data-cf] *)){filter:drop-shadow(0 0 10px #f0c97a7a) drop-shadow(0 0 22px #e2a85c3d)!important;transition:filter var(--mo-quick) var(--ease-out)}
.win-impact:where(:not([data-cf],[data-cf] *)){filter:drop-shadow(0 0 14px #ffd98c66)}

/* ─────────────────────────── §5 merged blocks without pixel geometry ───────
   Inert until cross-app.mjs sets `boardEl.dataset.mergeMode = "grid"` and writes
   gridRow/gridColumn on each .merged-zombie instead of left/top/width/height.
   See MOTION_REPORT.md §7 for the exact patch. The board is already a 5-column
   grid and grid items are allowed to overlap, so a merged block simply spans the
   same tracks as the tiles underneath it. No getBoundingClientRect, no
   ResizeObserver, no forced layout per resize, and it reflows for free. */
.cross-game .board[data-merge-mode=grid] .merged-zombie:where(:not([data-cf],[data-cf] *)){
 position:relative!important;
 left:auto!important;top:auto!important;width:auto!important;height:auto!important;
 align-self:stretch;justify-self:stretch;
}
.cross-game .board[data-merge-mode=grid] .merged-zombie:where(:not([data-cf],[data-cf] *)){animation:merge-in var(--mo-quick) var(--ease-out) both}
@keyframes merge-in{0%{opacity:0;transform:scale(.94)}100%{opacity:1;transform:none}}

/* ─────────────────────────────────── §6 reduced motion and low power ──────
   Belt and braces over the JS gates: every loop in the tomb, every celebration
   flourish and every feedback gesture is off, while everything that carries
   information still arrives — instantly, and still readable. */
@media (prefers-reduced-motion:reduce){
 .outpost-candle i:where(:not([data-cf],[data-cf] *)),.outpost-fog .fog-wave:where(:not([data-cf],[data-cf] *)),.win-medallion:where(:not([data-cf],[data-cf] *)),.win-compact:where(:not([data-cf],[data-cf] *)),.win-sweep:where(:not([data-cf],[data-cf] *)),.win-title.marching b:where(:not([data-cf],[data-cf] *)),.tile.arm-match:where(:not([data-cf],[data-cf] *)),.tile.arm-match .art:where(:not([data-cf],[data-cf] *)),.tile.arm-miss:where(:not([data-cf],[data-cf] *)),.tile.corner-arming:where(:not([data-cf],[data-cf] *)),.tile.corner-hit:where(:not([data-cf],[data-cf] *)),.tile.corner-miss:where(:not([data-cf],[data-cf] *)),.corner-ring:where(:not([data-cf],[data-cf] *)),.charge.charge-pop:where(:not([data-cf],[data-cf] *)),.bonus-step-chip:where(:not([data-cf],[data-cf] *)),.win-impact:where(:not([data-cf],[data-cf] *)),.tile[data-symbol=zombie]:where(:not([data-cf],[data-cf] *)),.cross-game .board[data-merge-mode=grid] .merged-zombie:where(:not([data-cf],[data-cf] *)){
  animation:none!important;transition:none!important;transform:none!important;
 }
 .win-title.marching b:where(:not([data-cf],[data-cf] *)){opacity:1!important}
 .win-medallion:where(:not([data-cf],[data-cf] *)),.win-compact:where(:not([data-cf],[data-cf] *)){opacity:1!important}
 .win-celebration.big:where(:not([data-cf],[data-cf] *))::before,.win-celebration.mega:where(:not([data-cf],[data-cf] *))::before{animation:none!important;opacity:1}
 .corner-ring:where(:not([data-cf],[data-cf] *)),.charge-pip:where(:not([data-cf],[data-cf] *)),.win-sparkfield:where(:not([data-cf],[data-cf] *)){display:none!important}
 .tile.arm-miss:where(:not([data-cf],[data-cf] *)){opacity:.88}
 .tile.corner-miss:where(:not([data-cf],[data-cf] *)){opacity:.78}
 .candle-halo:where(:not([data-cf],[data-cf] *)){opacity:.4}
}
