/* Jungle Duel motion layer (motion.js): overlay, card ghosts, banners, tokens, VS intro.
   Everything here is presentation only and lives above the game in #motion-layer. */
/* Above the board (its top layer is 95) and below every dialog (100+), so flights and sparks never
   float over a popup that opens mid-move. Confetti has its own canvas above the dialogs. */
#motion-layer { position: fixed; inset: 0; z-index: 96; pointer-events: none; overflow: hidden; }
#motion-layer canvas { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; }
canvas.motion-confetti { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 1200; pointer-events: none; }
#motion-layer.over-intro { z-index: 1350; }
.motion-rays { position: fixed; pointer-events: none; mix-blend-mode: screen; }
.motion-card-glow { position: fixed; pointer-events: none; mix-blend-mode: screen; }
.motion-seal { position: absolute; right: -10px; top: 30px; width: 30px; height: 30px; z-index: 4; pointer-events: none; filter: drop-shadow(0 2px 3px rgba(0, 0, 0, .6)); }

.motion-ghost { position: fixed !important; z-index: 2; pointer-events: none !important; transform-origin: 50% 50%; will-change: transform; animation: none !important; transition: none !important; }
.motion-ghost.motion-lifted { box-shadow: 0 16px 26px rgba(0, 0, 0, .55), 0 0 0 2px rgba(255, 230, 160, .55); }

.motion-flipper { position: fixed; z-index: 2; transform-style: preserve-3d; transform-origin: 50% 50%; pointer-events: none; border-radius: 8px; will-change: transform; }
.motion-flipper > .motion-face { position: absolute !important; inset: 0 !important; width: 100% !important; height: 100% !important; margin: 0 !important; visibility: visible !important;
  backface-visibility: hidden; -webkit-backface-visibility: hidden; animation: none !important; transition: none !important; transform: none !important; }
.motion-flipper > .motion-back { transform: rotateY(180deg) !important; }

.motion-ring { position: fixed; border: 3px solid #efd080; border-radius: 50%; box-shadow: 0 0 14px rgba(255, 215, 106, .55); pointer-events: none; }
img.motion-ring { border: 0; box-shadow: none; }

.motion-token { position: fixed; z-index: 4; min-width: 40px; height: 28px; padding: 0 9px; border-radius: 14px; font: 900 15px/28px var(--font-body, "Segoe UI", Arial, sans-serif);
  text-align: center; color: #241a06; background: linear-gradient(#fff0b0, #e3b04c); box-shadow: 0 0 12px #ffd76a, 0 2px 4px rgba(0, 0, 0, .6); pointer-events: none; }
.motion-token.negative { color: #fff0ea; background: linear-gradient(#e0694c, #8e3423); box-shadow: 0 0 12px #e0694c, 0 2px 4px rgba(0, 0, 0, .6); }

.motion-banner { position: fixed; left: -24px; right: -24px; height: 62px; z-index: 5; display: grid; place-items: center; overflow: hidden; pointer-events: none; }
.motion-banner .mb-band { position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(14, 43, 31, .94) 12%, rgba(21, 74, 51, .97) 50%, rgba(14, 43, 31, .94) 88%, transparent);
  border-top: 2px solid #efd080; border-bottom: 2px solid #efd080; background-size: 100% 100%; }
.motion-banner.tone-rival .mb-band { background-color: transparent; background-image: linear-gradient(90deg, transparent, rgba(58, 19, 12, .94) 12%, rgba(106, 36, 22, .97) 50%, rgba(58, 19, 12, .94) 88%, transparent); border-color: #ff9e7d; }
.motion-banner.tone-neutral .mb-band { background-image: linear-gradient(90deg, transparent, rgba(20, 28, 24, .94) 12%, rgba(44, 52, 40, .97) 50%, rgba(20, 28, 24, .94) 88%, transparent); }
.motion-banner .mb-band[style*="background-image"] { border: 0; }
.motion-banner .mb-text { position: relative; font: 400 28px/1 var(--font-display, "Rye", Georgia, serif); color: #ffe7a0; letter-spacing: .04em; white-space: nowrap;
  text-shadow: 0 3px 0 #2c1d08, 0 0 18px rgba(255, 207, 106, .55); }
.motion-banner .mb-text span { display: inline-block; }
.motion-banner .mb-gleam { position: absolute; top: 0; bottom: 0; left: -90px; width: 60px; background: linear-gradient(90deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, .45), rgba(255, 255, 255, 0));
  transform: skewX(-20deg); mix-blend-mode: overlay; }

.motion-modal-exit { position: fixed; inset: 0; z-index: 99; pointer-events: none; }
.motion-letter { display: inline-block; }
.motion-word { display: inline-block; white-space: nowrap; }

/* Level start: VS splash */
/* Text is sized to the stage (container units), not the window: on wide or landscape screens the stage is narrower. */
.motion-intro { position: fixed; inset: 0; z-index: 1300; overflow: hidden; background: #06110b; cursor: pointer; color: #fff1c4; container-type: size; }
.motion-intro .mi-half { position: absolute; inset: 0; background-size: cover; }
.motion-intro .mi-left { background-color: #0d2a1c; background-position: 100% center; background-repeat: no-repeat; clip-path: polygon(0 0, 62% 0, 38% 100%, 0 100%); }
.motion-intro .mi-right { background: radial-gradient(circle at 70% 40%, #f0b46a, #6a2416 70%) center/cover; clip-path: polygon(62% 0, 100% 0, 100% 100%, 38% 100%); }
/* Medallion fallback: sized by screen height too, so it stays above the VS emblem on short phones. */
.motion-intro .mi-rival { position: absolute; right: 6%; top: 18%; width: min(34%, 190px, 21cqh); aspect-ratio: 1; border-radius: 50%; background: #2a1408 center/cover;
  box-shadow: 0 0 0 4px #ffe7a0, 0 10px 30px rgba(0, 0, 0, .65); }
.motion-intro .mi-kicker { position: absolute; left: 0; right: 0; top: calc(env(safe-area-inset-top, 0px) + 9%); text-align: center; font: 800 12px var(--font-body, "Segoe UI", sans-serif);
  letter-spacing: .2em; text-transform: uppercase; color: #ffe7a0; text-shadow: 0 2px 4px #000; }
.motion-intro .mi-title { position: absolute; left: 16px; right: 16px; top: calc(env(safe-area-inset-top, 0px) + 9% + 20px); text-align: center; font: 400 clamp(17px, 6cqw, 34px)/1.1 var(--font-display, "Rye", Georgia, serif);
  text-shadow: 0 3px 0 #2c1d08, 0 0 18px rgba(0, 0, 0, .8); text-wrap: balance; }
.motion-intro .mi-name { position: absolute; font: 400 clamp(14px, 4.6cqw, 24px) var(--font-display, "Rye", Georgia, serif); text-shadow: 0 2px 0 #000, 0 0 12px #000; }
.motion-intro .mi-you { left: 18px; bottom: calc(env(safe-area-inset-bottom, 0px) + 16%); }
.motion-intro .mi-them { right: 18px; bottom: calc(env(safe-area-inset-bottom, 0px) + 16%); text-align: right; max-width: 52%; }
.motion-intro .mi-vs { position: absolute; left: 50%; top: 50%; width: 140px; height: 100px; margin: -50px 0 0 -70px; display: grid; place-items: center;
  font: 400 72px/1 var(--font-display, "Rye", Georgia, serif); color: #ffe08a; text-shadow: 0 4px 0 #3a2208, 0 0 30px #ffb84d; }
.motion-intro .mi-vs img { width: 100%; height: 100%; object-fit: contain; filter: drop-shadow(0 6px 10px rgba(0, 0, 0, .6)); }
.motion-intro .mi-vs.has-art { width: 190px; height: 142px; margin: -71px 0 0 -95px; }
/* With a full rival painting the medallion steps aside and the name sits low, opposite "You". */
/* motion.js places each painting from its rival's face position (VS_FOCUS); this is the fallback. */
.motion-intro.has-rival-painting .mi-right { background-position: right center; background-size: cover; }
.motion-intro.has-rival-painting .mi-rival { display: none; }
.motion-intro .mi-skip { position: absolute; left: 0; right: 0; bottom: calc(env(safe-area-inset-bottom, 0px) + 18px); text-align: center; font: 700 12px var(--font-body, sans-serif); color: rgba(255, 241, 196, .7); }

/* Screens: a slightly longer, eased fade. Opacity only: a transform here would re-anchor the
   fixed-position chrome inside some screens for the length of the fade. */
@media (prefers-reduced-motion: no-preference) {
  body:not(.reduced-motion) .screen.is-fading-in { animation: motionScreenIn 280ms cubic-bezier(.2, .8, .2, 1) backwards; }
}
@keyframes motionScreenIn { from { opacity: 0; } to { opacity: 1; } }

/* Turn ownership: the active score box glows on change instead of switching flat. */
#battle-scoreboard .battle-player { transition: border-color 240ms ease, background-color 240ms ease, box-shadow 320ms ease; }
#battle-scoreboard .battle-player.is-active { box-shadow: inset 0 -3px #efd080, 0 0 14px rgba(239, 208, 128, .35); }

/* Toasts: pop and settle. */
@media (prefers-reduced-motion: no-preference) {
  /* backwards, not both: a held end state would override the .leaving fade-out. */
  body:not(.reduced-motion) #toast-host > * { animation: motionToastIn 260ms cubic-bezier(.2, 1.3, .4, 1) backwards; }
}
@keyframes motionToastIn { from { opacity: 0; transform: translateY(-10px) scale(.92); } }

/* ---------- Between rounds ---------- */
/* Round medallions in the header (MOTION_ASSET_ORDERS.md order 3). The four coins share one rim, so
   a state change reads as the crest being stamped in. The discs fill ~82% of the art, hence 122%. */
#header-bar #round-pips.round-coins { display: flex; justify-content: center; align-items: center; gap: 5px; letter-spacing: 0; }
.round-coin { display: inline-block; width: 20px; height: 20px; border-radius: 50%; position: relative;
  background: url("assets/motion/round/medallion-empty.webp") center / 122% no-repeat; filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .55)); }
.round-coin.upcoming { opacity: .62; }
.round-coin.current { filter: drop-shadow(0 0 3px rgba(243, 210, 122, .95)) drop-shadow(0 1px 1px rgba(0, 0, 0, .55)); }
.round-coin.won { background-image: url("assets/motion/round/medallion-you.webp"); filter: drop-shadow(0 0 3px rgba(120, 220, 120, .7)); }
.round-coin.lost { background-image: url("assets/motion/round/medallion-rival.webp"); filter: drop-shadow(0 0 3px rgba(230, 90, 70, .7)); }
.round-coin.tied { background-image: url("assets/motion/round/medallion-tie.webp"); }
.round-coin.played { opacity: .8; }
@media (max-width: 360px) { .round-coin { width: 17px; height: 17px; } }

/* Wind streak that sweeps the board between rounds. */
.motion-gust { position: fixed; pointer-events: none; will-change: transform, opacity; }

/* ---------- Jungle Trail map tokens (order 3) ---------- */
/* Stone tokens replace the drawn medallions; the game prints the level number on the plain centre. */
#story-map-body.trail-v2 .story-node, #story-map-body.trail-v2 .story-node.locked, #story-map-body.trail-v2 .story-node:not(.current):not(.locked),
#story-map-body.trail-v2 .story-node.current, #story-map-body.trail-v2 .trail-chapter .story-node:not(.current):not(.locked) {
  border: 0; border-radius: 50%; box-shadow: none; background: var(--node-art) center / 118% no-repeat !important;
  filter: drop-shadow(0 4px 5px rgba(0, 0, 0, .55)) !important; }
#story-map-body.trail-v2 .story-node::before, #story-map-body.trail-v2 .story-node::after { display: none; }
/* Each state names its art in --node-art, so chapter-specific rules elsewhere never outrank it. */
#story-map-body .story-node { --node-art: url("assets/motion/map/node-open.webp"); }
#story-map-body .story-node.cleared:not(.current) { --node-art: url("assets/motion/map/node-cleared.webp"); }
#story-map-body .story-node.mastered:not(.current) { --node-art: url("assets/motion/map/node-mastered.webp"); }
#story-map-body .story-node.locked { --node-art: url("assets/motion/map/node-locked.webp"); }
#story-map-body.trail-v2 .story-node.mastered:not(.current) .story-node-number { color: #173d26; text-shadow: 0 1px 0 #fff3c4; }
#story-map-body.trail-v2 .story-node.locked { opacity: 1 !important; }
/* The locked token carries its own carved padlock; the number stays in the button's label. */
#story-map-body.trail-v2 .story-node.locked .story-node-number, #story-map-body.trail-v2 .story-node-lock { visibility: hidden; }
#story-map-body.trail-v2 .story-node.current { filter: drop-shadow(0 0 10px rgba(242, 201, 76, .8)) drop-shadow(0 4px 5px rgba(0, 0, 0, .55)) !important; }
#story-map-body.trail-v2 .story-node.current .story-node-number { color: #fff6d6; text-shadow: 0 1px 3px #000c; }
#story-map-body.trail-v2 .story-node .story-node-number { color: #fff6dc; text-shadow: 0 1px 3px #000c; }

/* The explorer stands on the current level (no ground in the art; the shadow is drawn here). */
#story-map-body.trail-v2 .trail-pin.has-art { width: 46px; height: 69px; background: url("assets/motion/map/explorer-standing.webp") center bottom / contain no-repeat;
  filter: drop-shadow(0 3px 3px rgba(0, 0, 0, .45)); }
#story-map-body.trail-v2 .trail-pin.has-art::after { content: ""; position: absolute; left: 50%; bottom: -3px; width: 30px; height: 8px; margin-left: -15px; border-radius: 50%;
  background: radial-gradient(closest-side, rgba(0, 0, 0, .45), transparent); z-index: -1; }

/* Foreground leaves at the map's edges; they drift against the scroll for depth. */
.trail-foliage { position: absolute; top: 0; bottom: 0; width: min(30%, 150px); z-index: 7; pointer-events: none; background: center / 100% 100% no-repeat;
  transform: translateY(var(--fol-y, 0px)); transition: transform .5s cubic-bezier(.2, .7, .3, 1); filter: blur(.4px) brightness(.85); }
.trail-foliage.left { left: 0; background-image: url("assets/motion/map/foliage-left.webp"); background-position: left center; background-size: auto 100%; }
.trail-foliage.right { right: 0; background-image: url("assets/motion/map/foliage-right.webp"); background-position: right center; background-size: auto 100%; }

/* Round result plaque */
.motion-round-result { position: fixed; inset: 0; z-index: 1120; display: grid; place-items: center; padding: 16px; cursor: pointer; }
.motion-round-result .mrr-scrim { position: absolute; inset: 0; background: radial-gradient(ellipse at 50% 45%, rgba(4, 14, 9, .55), rgba(2, 8, 5, .82)); }
.motion-round-result .mrr-plaque { position: relative; width: min(330px, 100%); padding: 18px 18px 14px; border-radius: 18px; text-align: center; color: #fbeec8;
  background: linear-gradient(#3a2614, #24170c); box-shadow: 0 18px 40px rgba(0, 0, 0, .7), inset 0 0 0 2px #c9a75a, inset 0 0 0 6px #3a2614, inset 0 0 0 7px #8a6a33; }
/* The painted plaque keeps its own 900x520 shape. Its dark centre panel spans 18.5%-76.5% of the height and
   9.3%-90.7% of the width (18.5% of the height = 10.7% of the width; computed from the plaque's own width, since % padding would use the full-screen grid's). The scores
   sit on that panel and the two buttons hang just below the plaque. */
.motion-round-result .mrr-plaque.has-art { --pw: min(390px, calc(100vw - 32px)); width: var(--pw); aspect-ratio: 900 / 520; box-sizing: border-box;
  padding: calc(var(--pw) * .107) calc(var(--pw) * .096) calc(var(--pw) * .136);
  display: flex; flex-direction: column; justify-content: center; margin-bottom: 64px;
  background: center / 100% 100% no-repeat; box-shadow: none; border-radius: 0; filter: drop-shadow(0 16px 26px rgba(0, 0, 0, .6)); }
.mrr-plaque.has-art .mrr-title { font-size: 19px; }
.mrr-plaque.has-art .mrr-scores { margin: 6px 0 4px; gap: 10px; }
.mrr-plaque.has-art .mrr-side strong { font-size: 32px; }
.mrr-plaque.has-art .mrr-side em { font-size: 11px; }
.mrr-plaque.has-art .mrr-line { font-size: 13px; }
.mrr-plaque.has-art .mrr-actions { position: absolute; left: 5%; right: 5%; top: calc(100% + 10px); margin: 0; }
@media (max-width: 360px) {
  .mrr-plaque.has-art .mrr-title { font-size: 16px; }
  .mrr-plaque.has-art .mrr-scores { margin: 3px 0 2px; }
  .mrr-plaque.has-art .mrr-side { min-width: 80px; gap: 1px; }
  .mrr-plaque.has-art .mrr-side small, .mrr-plaque.has-art .mrr-side em { font-size: 10px; }
  .mrr-plaque.has-art .mrr-side strong { font-size: 26px; }
  .mrr-plaque.has-art .mrr-line { font-size: 12px; }
}
.motion-round-result.is-final .mrr-plaque:not(.has-art) { box-shadow: 0 18px 40px rgba(0, 0, 0, .7), 0 0 30px rgba(255, 196, 92, .35), inset 0 0 0 2px #f0cf78, inset 0 0 0 6px #3a2614, inset 0 0 0 7px #b38a3e; }
.mrr-title { font: 400 22px/1 var(--font-display, "Rye", Georgia, serif); color: #ffe7a0; letter-spacing: .04em; text-shadow: 0 2px 0 #1a0f05; }
.mrr-scores { display: flex; justify-content: center; align-items: flex-start; gap: 14px; margin: 12px 0 8px; }
.mrr-side { display: grid; gap: 2px; min-width: 96px; }
.mrr-side small { font: 800 11px var(--font-body, "Segoe UI", sans-serif); letter-spacing: .14em; color: #d9c79a; }
.mrr-side strong { display: inline-block; font: 900 40px/1 var(--font-body, "Segoe UI", sans-serif); font-variant-numeric: tabular-nums; }
.mrr-side.you strong { color: #b9ef9f; } .mrr-side.them strong { color: #ffb39b; }
.mrr-side em { font: 700 12px var(--font-body, "Segoe UI", sans-serif); font-style: normal; color: #e8dcb8; }
.mrr-vs { align-self: center; margin-top: 10px; font: 400 16px var(--font-display, "Rye", Georgia, serif); color: #b69c62; }
.mrr-line { font: 800 15px var(--font-body, "Segoe UI", sans-serif); color: #ffe7a0; text-wrap: balance; }
.mrr-actions { display: flex; gap: 8px; margin-top: 14px; }
.mrr-actions button { flex: 1; min-height: 44px; border-radius: 11px; font: 800 14px var(--font-body, "Segoe UI", sans-serif); cursor: pointer; }
.mrr-details { border: 1px solid #8a6a33; background: rgba(0, 0, 0, .25); color: #f0e2bd; }
.mrr-go { border: 0; background: linear-gradient(#3e8a4c, #24603a); color: #fff; box-shadow: 0 3px 0 #123a20; }
.mrr-lead-change { position: absolute; right: -14px; top: -18px; padding: 6px 10px; border-radius: 8px; font: 400 14px/1 var(--font-display, "Rye", Georgia, serif);
  color: #fff1c4; background: #1d5a33; border: 2px solid #f0cf78; box-shadow: 0 4px 10px rgba(0, 0, 0, .55); pointer-events: none; }
img.mrr-lead-change { width: 140px; height: auto; padding: 0; border: 0; background: none; box-shadow: none; right: -30px; top: -42px; }

/* Banner subtitle and the final-round tone */
.motion-banner.has-sub { height: 88px; }
.motion-banner.has-sub .mb-band { bottom: 26px; }
.motion-banner .mb-sub { position: absolute; left: 0; right: 0; bottom: 2px; width: fit-content; margin: 0 auto; padding: 2px 12px; border-radius: 999px; background: rgba(8, 16, 10, .78); text-align: center; font: 800 14px var(--font-body, "Segoe UI", sans-serif); color: #fff3d0;
  text-shadow: 0 1px 3px #000, 0 0 10px #000; letter-spacing: .03em; }
.motion-banner.has-sub .mb-text { margin-bottom: 26px; }
.motion-banner.tone-final .mb-band { background-image: linear-gradient(90deg, transparent, rgba(60, 26, 6, .95) 12%, rgba(122, 64, 14, .98) 50%, rgba(60, 26, 6, .95) 88%, transparent);
  border-color: #ffd47a; box-shadow: 0 0 24px rgba(255, 190, 80, .45); }
.motion-banner.tone-final .mb-text { color: #fff0b8; text-shadow: 0 3px 0 #2c1406, 0 0 22px rgba(255, 180, 70, .85); }

/* Time of day: daylight, golden hour, then dusk with fireflies for the final round. A light grade over
   the whole game column (cards included) with clear centres, so lanes and totals stay readable. It sits
   above the board, since the board art underneath is already darkened, and never takes a tap. */
#tod-layer { position: absolute; inset: 0; z-index: 25; pointer-events: none; opacity: 0; transition: opacity 1.2s ease, background 1.2s ease; }
/* Time of day is a colour mood, never a dimmer: the centre of the board stays clear and only the outer edge
   takes a light tint (a darker final round read as low screen brightness on phones). */
body.tod-2 #tod-layer { opacity: 1; background: radial-gradient(ellipse 85% 75% at 50% 50%, rgba(255, 170, 70, 0) 55%, rgba(200, 110, 30, .14) 100%), linear-gradient(180deg, rgba(255, 180, 90, .08), rgba(255, 150, 60, 0) 45%); }
body.tod-3 #tod-layer { opacity: 1; background: radial-gradient(ellipse 85% 75% at 50% 50%, rgba(90, 60, 160, 0) 55%, rgba(70, 40, 130, .16) 100%), linear-gradient(180deg, rgba(120, 80, 200, .08), rgba(120, 80, 200, 0) 45%); }
body.tod-3 #tod-layer::before, body.tod-3 #tod-layer::after { content: ""; position: absolute; inset: 0;
  background-image: radial-gradient(2.5px 2.5px at 12% 30%, #ffe9a0, transparent), radial-gradient(2.5px 2.5px at 78% 22%, #ffe9a0, transparent),
    radial-gradient(2px 2px at 40% 62%, #fff3c0, transparent), radial-gradient(2.5px 2.5px at 88% 70%, #ffe9a0, transparent),
    radial-gradient(2px 2px at 22% 84%, #fff3c0, transparent), radial-gradient(2.5px 2.5px at 60% 40%, #ffe9a0, transparent),
    radial-gradient(2px 2px at 6% 58%, #fff3c0, transparent), radial-gradient(2.5px 2.5px at 94% 44%, #ffe9a0, transparent); filter: drop-shadow(0 0 3px #ffd56a); }
body.tod-3 #tod-layer::after { background-position: 30px -40px; opacity: .7; }
@media (prefers-reduced-motion: no-preference) {
  body.tod-3:not(.reduced-motion) #tod-layer::before { animation: todFireflies 7s ease-in-out infinite alternate; }
  body.tod-3:not(.reduced-motion) #tod-layer::after { animation: todFireflies 9s ease-in-out infinite alternate-reverse; }
}
@keyframes todFireflies { 0% { transform: translate(0, 0); opacity: .35; } 50% { opacity: 1; } 100% { transform: translate(14px, -22px); opacity: .5; } }

/* ---------- Jungle Trail map ---------- */
/* Gold line over the finished trail (path-glow art from order 3 can replace the stroke). */
#story-map-body .trail-progress-line { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; overflow: visible; }
#story-map-body .trail-progress-line path { fill: none; stroke-linecap: round; stroke-linejoin: round; }
#story-map-body .trail-progress-line .tpl-glow { stroke: rgba(48, 24, 4, .55); stroke-width: 11; }
#story-map-body .trail-progress-line .tpl-core { stroke: #ffd76a; stroke-width: 5; }
#story-map-body .trail-progress-line .tpl-spark { stroke: #fff6cf; stroke-width: 2.2; stroke-dasharray: 3 18; opacity: .9; }
#story-map-body .trail-progress-line [pathLength] { stroke-dasharray: 1 1; }
@media (prefers-reduced-motion: no-preference) {
  body:not(.reduced-motion) #story-map-body .trail-progress-line .tpl-spark { animation: tplFlow 1.4s linear infinite; }
}
@keyframes tplFlow { to { stroke-dashoffset: -21; } }

/* Mist over the locked stretch of the current chapter; it rolls back as levels unlock. */
#story-map-body .trail-mist { position: absolute; left: -6%; right: -6%; top: 0; height: var(--mist-y, 0%); z-index: 3; pointer-events: none;
  background: linear-gradient(180deg, rgba(214, 228, 212, .62) 0%, rgba(200, 220, 200, .55) 72%, rgba(200, 220, 200, 0) 100%);
  transition: height 1.1s cubic-bezier(.3, .7, .2, 1), opacity 1s ease; -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent); mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent); }
/* The bands stay inside the mist box: its side-fade mask clips anything outside, which would leave a hard edge. */
#story-map-body .trail-mist::before, #story-map-body .trail-mist::after { content: ""; position: absolute; left: -10%; right: -10%; bottom: 0; height: min(220px, 100%);
  background: url("assets/motion/map/mist-1.webp") left bottom / 520px 260px repeat-x; }
#story-map-body .trail-mist::after { background-image: url("assets/motion/map/mist-2.webp"); background-size: 640px 320px; bottom: 26px; height: min(240px, 100%); opacity: .75; }
#story-map-body .trail-mist.lifting { opacity: 0; }
@media (prefers-reduced-motion: no-preference) {
  body:not(.reduced-motion) #story-map-body .trail-mist::before { animation: mistDrift 16s ease-in-out infinite alternate; }
  body:not(.reduced-motion) #story-map-body .trail-mist::after { animation: mistDrift 22s ease-in-out infinite alternate-reverse; }
}
@keyframes mistDrift { from { transform: translateX(-6%); } to { transform: translateX(6%); } }

/* Up next panel tucks down while the map scrolls. */
#story-map-body .story-preview { transition: transform 280ms cubic-bezier(.2, .8, .2, 1); }
#story-map-body .story-preview.is-tucked { transform: translateY(calc(100% - 62px)); }

/* Iris that opens onto the map after a won duel. */
.motion-iris { position: fixed; inset: 0; z-index: 1250; background: #06110b; pointer-events: none; }

/* Wide screens: dark bands beside the portrait stage. */
.motion-intro-letterbox { position: fixed; inset: 0; z-index: 1299; pointer-events: none;
  background: radial-gradient(ellipse at 50% 50%, #0d2116, #040a06 75%); }

/* VS clash: a burning seam along the diagonal split, and a flash on impact. */
.motion-intro .mi-seam { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; opacity: 0;
  filter: drop-shadow(0 0 6px #ffd56a) drop-shadow(0 0 16px rgba(255, 150, 40, .8)); }
.motion-intro .mi-seam line { vector-effect: non-scaling-stroke; stroke-linecap: round; }
.motion-intro .mi-seam-glow { stroke: rgba(255, 196, 80, .85); stroke-width: 9; }
.motion-intro .mi-seam-core { stroke: #fff7d8; stroke-width: 3; }
.motion-intro .mi-flash { position: absolute; inset: 0; pointer-events: none; opacity: 0;
  background: radial-gradient(ellipse 60% 70% at 50% 50%, #fff9e6, rgba(255, 222, 150, .65) 40%, rgba(255, 190, 90, 0) 75%); }

/* The round summary button is gone while the board sweeps into the next round. */
body.round-continuing #round-summary-cta { visibility: hidden; pointer-events: none; }

/* ======================================================================================
   Polish pass (qa_overnight/polish_audit_2026-10-14.md). Loaded last, so these settle
   alignment and spacing details across the shell without touching the base sheets.
   ====================================================================================== */
/* A4: the Settings/Record divider sits after the last row instead of being pinned to the
   scroll box's bottom edge (it rode up through "Match history" when the panel scrolled). */
#screen-panel-settings #settings-body.panel-body::after { position: static; display: block; margin: 26px auto 0; }
/* A5: ".journal-identity span" (display:block, margin-top) also matched the avatar span and undid its
   centring grid, leaving the level number 17 px left and 8 px high. */
#screen-panel-journal .journal-identity .journal-avatar { display: grid; place-items: center; margin: 0; letter-spacing: 0; color: #fff3cf; font: 700 25px/1 var(--font-display, "Rye", Georgia, serif); }
#screen-panel-journal .journal-identity .journal-avatar b { display: block; line-height: 1; }
/* A6: on narrow phones the right-hand "20 TO / CANOPY HUNTER" block squeezed the copy column to ~117 px,
   stacking "EXPLORER LEVEL / UNRANKED EXPLORER" into four lines (two at 390). On phones it moves under the bar, on one line. */
@media (max-width: 430px) {
  .home-player-progress { grid-template-columns: 40px minmax(0, 1fr); row-gap: 4px; }
  .home-player-progress > span:last-child { grid-column: 2; display: flex; align-items: center; gap: 6px; }
  .home-player-progress .home-player-next { text-align: left; }
  .home-player-progress .home-player-next br { display: none; }
  .home-player-copy strong, .home-player-copy .home-player-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}
/* A7: on short screens the sheet scrolls; the Start / Replay button stays pinned to its bottom edge,
   with a fade of the sheet's paper behind it so rows slide under it cleanly. */
.level-sheet .level-sheet-start { position: sticky; bottom: 0; z-index: 3;
  box-shadow: 0 3px 0 #072a17, 0 8px 18px #0004, 0 -18px 14px -6px #f3e8c8, 0 22px 0 6px #f3e8c8; }
/* A8: header text never wraps (the Daily label used to push "JUNGLE DUEL" onto two lines). */
#header-bar .wordmark, #header-bar .round-label { white-space: nowrap; }
/* C18: 44 px tap targets for the game menu button. */
#header-bar #menu-btn { min-width: 44px; min-height: 44px; }
/* B1: every Settings toggle is the same width, with equal halves, so the column lines up on both edges
   (they sized to their labels: left edges landed at 190, 194 and 211). */
#settings-body .settings-seg { width: min(168px, 46vw); flex: 0 0 auto; }
#settings-body .settings-seg button { flex: 1 1 0; min-width: 0; padding-left: 6px; padding-right: 6px; }
/* B2: the home wordmark plaque had fixed 20 px side margins and a content-sized width, so on wider
   columns it sat flush left (desktop: 20 px left, 199 px right). Auto margins centre it at every width. */
#screen-home.v2 .home-wordmark-plaque { margin-left: auto; margin-right: auto; max-width: calc(100% - 40px); }
@media (max-width: 360px) { #screen-home.v2 .home-wordmark-plaque { max-width: calc(100% - 28px); } }
/* B5: painted bridge tiles dropped their 3 px border, so their label (anchored to the padding box) sat 3 px
   lower than CLEARING / SWAMP and their art filled 3 px more. A transparent border keeps every tile's box identical. */
body.painted-board .oasis-card.portal { border: 3px solid transparent; }
/* B3: home list cards had 10 px on the icon side and 14 px on the chevron side. */
.guest-practice { padding-left: 14px !important; }
/* B4: the two small tiles centre their text block; a one-line subtitle ("See your trail.") made that block
   shorter, so "Your record" sat ~8 px below "How to play". Both subtitles reserve two lines. */
.home-utility-card > strong + span { min-height: 2.6em; }
/* Headings that wrap ("Expedition Journal") did the same to the next row: every tile now starts its content at the top. */
#screen-home .home-utility-card { align-content: start; align-items: start; }
/* B6: How-to-play pages centred their content, so the kicker jumped between pages (y 202, 225, 152, 90, 196)
   and short pages opened on ~130 px of empty card. Every page now starts at the same top line. */
#screen-panel-how-to-play .htp-page { justify-content: flex-start; padding-top: 14px; }
/* B7: one panel header for every panel. How to play and the Journal used a flush 64 px bar (and the Journal's
   centred title ran into the back button); they now share the framed plaque header of Settings and Record. */
#screen-panel-how-to-play, #screen-panel-journal { padding: 6px; background: radial-gradient(circle at 50% -10%, #315c38 0, #143b26 48%, #092218 100%); box-shadow: inset 0 0 34px #020d08; }
#screen-panel-how-to-play .panel-header, #screen-panel-journal .panel-header { position: relative; height: 82px; min-height: 82px; margin: 0 0 5px; padding: env(safe-area-inset-top) 12px 0;
  border: 2px solid #b28b45; border-radius: 12px 12px 3px 3px; background: linear-gradient(145deg, #184b2c, #0d301d);
  box-shadow: inset 0 0 0 3px #082416, inset 0 -12px 24px #061b11aa, 0 3px 0 #5b3e1e; color: #fff3ce; }
#screen-panel-how-to-play .panel-header::after, #screen-panel-journal .panel-header::after { content: ""; position: absolute; left: 24%; right: 24%; bottom: 13px; height: 1px;
  background: linear-gradient(90deg, transparent, #d4aa5c 22%, #d4aa5c 78%, transparent); }
#screen-panel-how-to-play .panel-title, #screen-panel-journal .panel-title { color: #fff1c5; font-size: clamp(27px, 8vw, 34px); letter-spacing: .5px;
  text-shadow: 0 2px 0 #092015, 0 0 7px #d1a65455; text-transform: none; }
#screen-panel-journal .panel-title { font-size: clamp(19px, 5.3vw, 30px); white-space: nowrap; }
#screen-panel-how-to-play .panel-back, #screen-panel-journal .panel-back { position: relative; z-index: 2; width: 48px !important; height: 48px !important; min-height: 48px !important;
  border: 2px solid #c7a05b; border-radius: 10px; color: #fff0bf; background: linear-gradient(#255e39, #123d25); box-shadow: inset 0 0 0 2px #0c2d1b, 0 3px 0 #472f18; font-size: 27px; }
/* B7 (cont.): every panel title is centred on the whole header, with the back button's width kept clear on
   both sides (it used to centre in the space right of the button, so long titles sat off-centre or touched it). */
.screen.panel .panel-header { position: relative; }
.screen.panel .panel-header .panel-title { position: absolute; inset: env(safe-area-inset-top) 64px 0; display: grid; place-items: center; margin: 0; text-align: center; white-space: nowrap; pointer-events: none; }
#screen-panel-journal .panel-title { font-size: clamp(16px, 5vw, 30px); }
/* B8: the status column sized to its text ("LEVEL 101" vs "LEVEL 21"), so each card's middle column had a
   different width. A fixed column keeps titles, copy wrapping and labels aligned card to card. */
#screen-panel-journal .prestige-card { grid-template-columns: 66px minmax(0, 1fr) 52px; column-gap: 6px; }
#screen-panel-journal .prestige-card > :first-child { max-width: 66px; }
#screen-panel-journal .prestige-status { text-align: right; }
/* B9: "+50 XP" aligns to the achievement title's baseline (it sat ~4 px high). */
#screen-panel-journal .achievement-head { align-items: baseline; }
/* B11 + C18: SCOUT keeps the first slot in the move bar on every turn (the rival's status line was inserted in
   front of it, pushing it to the right), and is a full 44 px target with its label centred. */
#move-status-overlay #scout-choice { order: -1; min-height: 44px; align-items: center; justify-content: center; padding-top: 0; padding-bottom: 0; line-height: 1; }
/* B12: the level-up halo ended at 20% opacity, 1.35x, leaving a faint ring through "Explorer Level n reached"
   and the reward chest. It is now a one-shot pulse that fades out completely. */
.levelup-medallion::before { animation-name: medallionHaloOnce; }
@keyframes medallionHaloOnce { 0% { opacity: 0; transform: scale(.45); } 35% { opacity: 1; } 100% { opacity: 0; transform: scale(1.35); } }
/* B13: the standing explorer stands beside the current node with his feet at its base, instead of the badge
   pin's offsets (lifted fully above the node and up to 60 px aside, so he floated over the jungle). */
#story-map-body.trail-v2 .trail-pin.has-art, #story-map-body.trail-v2 .trail-pin.has-art.pin-level { --pin-dy: calc(-100% + 22px); --pin-dx: 54px; }
#story-map-body.trail-v2 .trail-pin.has-art.lean-left, #story-map-body.trail-v2 .trail-pin.has-art.pin-level.lean-left { --pin-dx: -54px; }
#story-map-body.trail-v2 .trail-pin.has-art.lean-right, #story-map-body.trail-v2 .trail-pin.has-art.pin-level.lean-right { --pin-dx: 54px; }
/* B14: on very short screens (iPhone SE class) the board's slot rows shrank to ~26 px strips. The hand tray
   gives back 12 px (smaller cards, same 3:4 art) so each of the six rows gains ~2 px. */
@media (max-height: 600px) {
  #screen-game #hand-tray { height: calc(88px + env(safe-area-inset-bottom)); min-height: calc(88px + env(safe-area-inset-bottom)); }
  #screen-game #hand-cards.centered { --hand-w: min(56px, calc((100vw - 48px) / 4)); }
}
/* B15: a long reward label pushed the XP value 1.2 px into the row's padding (1fr would not shrink). */
.xp-source-row { grid-template-columns: 30px minmax(0, 1fr) auto; }
/* C1: one play-button style through the Trail: the map's "Play level n" and the sheet's "Start duel" both centre
   their label with the same chevron after it (one was left-aligned with an SVG chevron, the other a ▸ glyph). */
.story-preview #story-play, .level-sheet .level-sheet-start { display: inline-flex; align-items: center; justify-content: center; gap: 8px; text-align: center; }
.story-preview #story-play .ui-icon, .level-sheet .level-sheet-start .ui-icon { width: 20px; height: 20px; flex: 0 0 auto; }
/* C3: a negative score's sign at display size reads as a full minus, not a hyphen. */
.num-sign { font-family: var(--font-body, "Segoe UI", sans-serif); font-weight: 800; margin-right: .04em; }
/* C5: the Round summary's continue button spans the dialog like every other dialog's primary button. */
#round-summary-modal .rs-next-btn { display: flex; align-items: center; justify-content: center; width: 100%; box-sizing: border-box; }
/* R2: the four record stats keep their labels on one line ("BEST STREAK" wrapped and sat lower than the rest). */
#screen-panel-stats .stats-summary span { white-space: nowrap; letter-spacing: .2px; }
@media (max-width: 360px) { #screen-panel-stats .stats-summary span { font-size: 9px; letter-spacing: 0; } }
/* C10: About's five links break 3 + 2, both rows centred (it was 4 + "Contact" alone), and the Credits button
   matches the links' size (it was 12 px against their 14 px). */
.about-links { max-width: 290px; margin-left: auto; margin-right: auto; row-gap: 0; }
.about-links button { min-height: 44px; padding: 0 8px; font-size: 14px; text-underline-offset: 3px; }
/* C13: list items joined with " · " never split inside an item ("Journal / stamp 1 of 7", "Vine Streak / 1"). */
.nowrap-item { white-space: nowrap; }
/* C12: the Jungle Rush "Tap to continue" hint was too faint to notice. */
.rush-stage > small { color: #f1e6bf; font-size: 13px; opacity: .9; }
/* C14: the last How-to-play page sets its content on the cream card like every other page (it switched to a
   dark green hero card). Kicker, title, body and callout use the same colours as the pages before it. */
#screen-panel-how-to-play .htp-finale { padding: 0; border: 0; border-radius: 0; background: none; box-shadow: none; color: #23301f; overflow: visible; }
#screen-panel-how-to-play .htp-finale h3 { color: #163d26 !important; font-size: clamp(22px, 6vw, 28px) !important; }
#screen-panel-how-to-play .htp-finale p { color: #2c3a26; }
#screen-panel-how-to-play .htp-finale .htp-kicker { color: #7a5b26; }
/* C15: the Pass & play curtain gets the game's dressing (night jungle, crest, framed plaque) instead of a flat
   panel. It stays fully opaque, so the board underneath is never visible. */
#handoff-curtain { background: linear-gradient(#04120bcc, #04120bf2), url("assets/ui/welcome-jungle-night.webp") center / cover no-repeat, #04120b; cursor: pointer; }
#handoff-curtain::before { content: ""; width: 92px; height: 92px; border: 2px solid #8c692e; border-radius: 50%;
  background: #15321c url("assets/ui/crest-lion-serpent.webp") center / cover no-repeat; box-shadow: 0 3px 0 #4a3015, 0 0 0 4px #f7e8bf, 0 0 28px #f2c94c55; }
#handoff-curtain .pass-title { padding: 14px 22px 10px; border: 2px solid #c9a24f; border-radius: 16px; background: linear-gradient(#184b2c, #0d301d);
  box-shadow: inset 0 0 0 3px #082416, 0 4px 0 #5b3e1e, 0 14px 30px #000a; color: #fff1c5; line-height: 1.05; text-shadow: 0 2px 0 #092015; }
#handoff-curtain .pass-sub { color: #f1e6bf; font-weight: 700; letter-spacing: .3px; }
/* C16: a locked level's "Tough" tag floated above the mist. It shows once the level opens. */
#story-map-body .story-node.locked .story-node-tag { visibility: hidden; }
/* C17: the scoreboard name is a grid item with overflow hidden, so its column could shrink below the word when the
   score beside it grew ("RIVAL" lost 2 px). It keeps at least its longest word; long online names still ellipsize. */
#battle-name-0, #battle-name-1 { min-width: min-content; max-width: 9em; }
/* C18: the Level Start sheet's close button is a 44 px target (it was 40). */
.level-sheet .level-sheet-close { width: 44px; height: 44px; right: 10px; top: 8px; }
/* C19: body copy avoids a lone last word (widows) across menus, panels, sheets and tips. */
p, small, .settings-help, .settings-row small, .guest-practice small, .difficulty-option span, .sheet-rules li, .sheet-goals small,
.htp-a-head + p, .achievement-card p, .trail-locked-card span, .welcome-card p, .home-utility-card span, .xp-source-row span { text-wrap: pretty; }
h1, h2, h3, .level-sheet h2, .mrr-line { text-wrap: balance; }
/* C19 (cont.): short two-line helper copy is balanced, which also clears the lone last word. */
.difficulty-copy span, .guest-practice small, .settings-row small, .home-utility-card span, .sheet-rules li, #jit-tip p { text-wrap: balance; }
/* H7: the Sign in card's chevron was dark on dark. */
.home-signin-card .ui-chevron, .home-signin-card > svg:last-child { color: #f1e6bf; opacity: 1; }
/* How-to-play pages that scroll (the animal list) fade out above the footer instead of a hard cut. */
#screen-panel-how-to-play .htp-page.is-active { -webkit-mask-image: linear-gradient(#000 calc(100% - 22px), transparent); mask-image: linear-gradient(#000 calc(100% - 22px), transparent); padding-bottom: 22px; }
/* B1 (cont.): with equal-width toggles the control column must not shrink (the toggle spilled left over long
   descriptions such as "A calm jungle soundscape for each chapter"); the label takes the rest and wraps. */
#settings-body .settings-row .settings-label { flex: 1 1 0; min-width: 0; }
#settings-body .settings-row .settings-control { flex: 0 0 auto; margin-left: 12px; }
/* C19 (cont.): How-to-play callouts balance their lines too ("most points / wins."). */
#screen-panel-how-to-play .htp-rule-callout, #screen-panel-how-to-play .htp-rule-callout p { text-wrap: balance; }
@media (max-width: 380px) { #settings-body .settings-row .settings-control { margin-left: 0; } #settings-body .settings-row .settings-label { flex-basis: 100%; } }
#story-map-body.trail-map #story-play, #story-map-body #story-play { justify-content: center; gap: 8px; }
/* C13 (cont.): on the Jungle Rush overlay each gain sits on its own line (a wrapped " · " list left a dangling dot). */
.rush-stage .rush-lines .nowrap-item { display: block; }
.rush-stage .rush-lines .nowrap-item + .nowrap-item { margin-top: 2px; }
/* On short screens the tip card moves up into the toast's strip, so a "Rival plays…" toast landed on top of it.
   While a tip is open there, toasts wait underneath it (they are transient and the move itself is animated). */
@media (max-height: 700px) { body:has(#jit-tip:not(.leaving)) #toast-host { visibility: hidden; } }
/* Map token numbers sit on the painted face, which the three-quarter view puts ~2-3 % above the token's middle
   (measured against the inner ring at 3x: digits were 1.7-2.8 px low). */
#story-map-body.trail-v2 .story-node .story-node-number { transform: translateY(-6%); }
/* The foliage fades out over its last few pixels so its end edge never reads as a cut line. */
.trail-foliage { -webkit-mask-image: linear-gradient(#000 calc(100% - 18px), transparent); mask-image: linear-gradient(#000 calc(100% - 18px), transparent); }
