/* Web fonts moved to a NON-BLOCKING <link> in index.html — an @import here serialized the fetch after
   styles.css AND render-blocked first paint until fonts.googleapis.com answered (seconds-to-timeout on
   filtered/flaky networks). display=swap already covers the fallback-then-swap behavior. */

:root{
  --green:#5fae5f; --green-d:#3f8a47; --green-l:#e7f6e2;
  --cream:#fbf7ed; --ink:#33402f; --muted:#7c8a73;
  --amber:#f2b134; --amber-d:#d6920f;
  /* soft warm off-white cards + gentler shadow = less glare for tired eyes (low-contrast calm) */
  --card:#fffdf7; --line:rgba(60,80,50,.11);
  --sky1:#bfe3f5; --sky2:#dff3e8;
  /* the one high-emphasis element: the center Play button. Mid-greens; the white glyph also carries a
     drop-shadow so it clears the 3:1 non-text floor on every gradient stop (not just the dark end). */
  --fab1:#53a455; --fab2:#3f8a47;
  --r:16px; --r-lg:22px;
  --shadow:0 2px 8px rgba(50,70,40,.10);   /* tighter blur = far cheaper to raster on weak GPUs; calm look unchanged */
  --iris-bg:#0b120c;   /* warm green-black for the menu<->level scene transition (matches --ink tonality) */
  /* F18 — design-token scale. One vocabulary for the polish pass; F19/F13/F15 consume these.
     Every value aliases a size/hex ALREADY in use — nothing new is invented, so adding these
     changes no pixels until a rule references them. */
  --r-sm:10px; --r-md:14px;                                                      /* --r(16)/--r-lg(22) already above */
  --fs-xs:11px; --fs-sm:12px; --fs-md:14px; --fs-lg:16px; --fs-xl:20px; --fs-2xl:24px; --fs-3xl:30px;
  --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:20px; --sp-6:24px;
  /* semantic scene colours — the EXACT hex already scattered through the scene, just named for reuse */
  --c-error:#e8607a; --c-time:#46b6c9; --c-combo:#9a8fe0; --c-critter:#9ec27a; --c-coin:#e6a32e; --c-gem:#3b9ae8;
}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{margin:0;padding:0;height:100%}
body{
  font-family:'Fredoka',system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
  color:var(--ink);
  background:linear-gradient(180deg,#bfe3f5 0%,#dff3e8 38%,#eef7df 100%);
  overscroll-behavior:none;
  touch-action:manipulation;   /* kill the double-tap-zoom tap delay in webviews that ignore user-scalable=no (TMA/iOS) — pan/scroll unaffected; .board's touch-action:none still wins there */
}
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
.hidden{display:none !important}

#app{
  max-width:480px;margin:0 auto;height:100dvh;display:flex;flex-direction:column;
  position:relative;overflow:hidden;
}

/* ---------- HUD ---------- */
/* F16: tightened so all five currency pills + settings fit 360px without clipping the gem/settings.
   lives/coins/gems are the always-visible priority; qp/lvl are compact meta chips. */
#hud{display:flex;align-items:center;justify-content:space-between;padding:8px 6px;gap:5px}
/* Native immersive (tools/android-fullscreen-patch.mjs hides both system bars): the window now extends
   under the camera cutout, so the menu HUD needs the TOP inset. Only #overlay consumed inset-top before
   — every other safe-area consumer in this file is bottom-only, so nothing here double-pads.
   native.js applySafeArea() adds .in-native and publishes --tg-top. */
html.in-native #hud{padding-top:calc(8px + max(env(safe-area-inset-top, 0px), var(--tg-top, 0px)))}
html.in-native body.in-game #screen{padding-top:calc(42px + max(env(safe-area-inset-top, 0px), var(--tg-top, 0px)))}
.hud-left,.hud-right{display:flex;align-items:center;gap:6px;min-width:0}
.hud-pill{
  display:inline-flex;align-items:center;gap:5px;background:rgba(255,255,255,.85);
  border:1px solid var(--line);border-radius:999px;padding:4px 8px;font-weight:600;font-size:14px;
  box-shadow:0 2px 6px rgba(50,70,40,.08);white-space:nowrap;
}
.hud-pill i{font-size:17px}
.hud-pill span{font-variant-numeric:tabular-nums}   /* steady width while coins/gems count up */
/* I1: Tokens (the play-gate) recoloured OFF amber → teal "energy", so it no longer reads as coins.
   Roles are now visually distinct: coins = amber (soft) · gems = blue + premium ring · Tokens = teal. */
.hud-pill.lives i{color:#2ba39b}
.hud-pill.lives{position:relative;overflow:hidden}
.hud-pill.lives::before{content:'';position:absolute;left:0;top:0;bottom:0;width:calc(var(--regen,0)*100%);background:linear-gradient(90deg,rgba(43,163,155,.10),rgba(43,163,155,.30));transition:width .9s linear;pointer-events:none}   /* time-to-next-token fill — a touch richer so the countdown reads at a glance */
/* wallet chips (hud-right): amber coins with a soft accent · blue gems with a faint premium ring */
.hud-pill.coin i{color:#e6a32e}
.hud-pill.gem i{color:#3b9ae8}
.hud-pill.coin{box-shadow:0 2px 6px rgba(50,70,40,.08),inset 0 0 0 1.5px rgba(230,163,46,.16)}
.hud-pill.gem{box-shadow:0 2px 6px rgba(50,70,40,.08),inset 0 0 0 1.5px rgba(59,154,232,.32)}
.hud-pill.lives.full::before{width:0}
.hud-pill.lives>*{position:relative}
/* narrow-phone fit: tighten the 6-item HUD so nothing clips on small screens (coins/gems/level run long) */
@media (max-width:400px){
  #hud{padding:8px 6px;gap:4px}
  .hud-left,.hud-right{gap:4px}
  .hud-pill{padding:3px 7px;font-size:12px;gap:3px}
  .hud-pill i{font-size:15px}
  .hud-timer{display:none}   /* drop the regen/"full" text on narrow — the number + fill already carry the state */
}
/* smallest phones: also fold away the quest-score pill (it's mirrored in the Home quest card) so nothing clips */
@media (max-width:360px){ #qpPill{display:none} }
.hud-pill.lives.beat i{animation:lifeBeat .5s ease}
@keyframes lifeBeat{0%,100%{transform:scale(1)}40%{transform:scale(1.22)}}
.hud-timer{font-size:11px;color:#2ba39b;font-weight:700;letter-spacing:.2px;opacity:.92;display:inline-block;max-width:8ch;overflow:hidden;white-space:nowrap;transition:max-width .3s ease,opacity .3s ease}   /* 8ch: the VIP unlimited window renders '1440:00' */   /* F16 kept: an empty timer (lives full) still frees its ~30px — but eased, so the coin/gem pills glide instead of snapping */
.hud-timer:empty{max-width:0;opacity:0}
.hud-icon{width:38px;height:38px;border-radius:50%;background:rgba(255,255,255,.85);border:1px solid var(--line);display:grid;place-items:center;font-size:19px;box-shadow:0 2px 6px rgba(50,70,40,.08)}
/* ---------- Offline chip (OFFLINE.md §4.1): a MUTED state, never a red error ---------- */
.hud-pill.link{position:relative;cursor:pointer;color:var(--muted);background:rgba(124,138,115,.10);border-color:rgba(124,138,115,.28);padding:4px 9px;max-width:72px;transition:max-width .28s ease,opacity .28s ease,padding .28s ease,margin .28s ease}
.hud-pill.link.chip-off{max-width:0;opacity:0;padding-left:0;padding-right:0;border-width:0;pointer-events:none;overflow:hidden;margin-inline-start:-5px}   /* clip only while collapsed (the Mailbox count badge overflows by design when visible); the negative margin swallows the flex gap a zero-width item would leave */
@media (max-width:400px){ .hud-pill.link.chip-off{margin-inline-start:-4px} }
.hud-pill.link i{font-size:16px;color:var(--muted)}
.hud-pill.link .link-count{position:absolute;top:-5px;inset-inline-end:-5px;min-width:15px;height:15px;padding:0 3px;border-radius:8px;background:var(--amber);color:#fff;font-size:10px;font-weight:800;line-height:15px;text-align:center;box-shadow:0 0 0 2px var(--card,#fffdf7);font-variant-numeric:tabular-nums}
.hud-pill.link.tappable:active,.hud-pill.link:active{transform:scale(.96)}
@media (max-width:400px){ .hud-pill.link{padding:3px 7px} .hud-pill.link i{font-size:14px} }
/* the small ti-cloud-off suffix glyph a gated-but-tappable claim button carries (§4.4) */
.off-suffix{font-size:13px;color:var(--muted);margin-inline-start:5px;vertical-align:-1px}

/* ---------- Screen ---------- */
#screen{flex:1;overflow-y:auto;overflow-x:hidden;padding:6px 16px 18px;-webkit-overflow-scrolling:touch}
.screen-title{font-size:24px;font-weight:700;margin:6px 0 2px}
.screen-sub{color:var(--muted);font-weight:500;margin:0 0 14px}

/* ---------- Nav (6 tabs + a Play FAB docked on the center seam) ---------- */
#nav{position:relative;z-index:5;display:flex;align-items:flex-end;background:rgba(255,253,247,.94);border-top:1px solid var(--line);padding:6px 4px max(6px,env(safe-area-inset-bottom));box-shadow:0 -2px 10px rgba(50,70,40,.05)}
.nav-btn{flex:1;min-width:0;position:relative;display:flex;flex-direction:column;align-items:center;gap:2px;padding:7px 0;border-radius:13px;color:var(--muted);font-weight:600;transition:background .14s ease}
.nav-btn i{font-size:21px;transition:font-size .14s ease}
.nav-btn span{max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:10px;line-height:1.15;padding:0 3px}
.nav-btn.active{color:var(--green-d);background:var(--green-l)}
.nav-btn.active i{font-size:25px}                       /* enlarged active tab — "you are here" (owner req 2026-07-06) */
.nav-btn.active span{font-size:11px;font-weight:700}
.nav-btn.locked{opacity:.5}
.nav-btn.locked .nav-lock{position:absolute;top:3px;inset-inline-start:calc(50% + 4px);width:15px;height:15px;border-radius:50%;background:var(--card,#fffdf7);box-shadow:0 1px 3px rgba(40,50,30,.28);display:grid;place-items:center}
.nav-btn.locked .nav-lock i{font-size:10px;color:#9aa191}
.nav-btn.has-dot::after{content:'';position:absolute;top:4px;inset-inline-start:calc(50% + 7px);width:8px;height:8px;border-radius:50%;background:#e8607a;box-shadow:0 0 0 2px rgba(255,253,247,.94)}
/* Critters-tab arrivals badge — "where my progress goes": a live count of rescued critters not yet viewed */
.nav-btn .nav-count{position:absolute;top:1px;inset-inline-start:calc(50% + 5px);min-width:15px;height:15px;padding:0 3px;border-radius:8px;background:#5fae5f;color:#fff;font-size:10px;font-weight:800;line-height:15px;text-align:center;box-shadow:0 0 0 2px rgba(255,253,247,.94)}
/* the two tabs flanking the seam: nudge content/badges to their OUTER edge so the raised Play never covers an icon */
.nav-btn--seam{padding-top:8px}
.nav-btn[data-screen="events"]{padding-inline-end:16px}
.nav-btn[data-screen="sanctuary"]{padding-inline-start:16px}
.nav-btn[data-screen="events"].has-dot::after{inset-inline-start:calc(50% - 13px)}
.nav-btn[data-screen="sanctuary"] .nav-count{inset-inline-start:calc(50% - 13px)}
@keyframes arrivalsPop{0%{transform:scale(1)}42%{transform:scale(1.06)}100%{transform:scale(1)}}   /* scale 1.06 = the calm ceiling, no higher */
.nav-btn.arrivals-pop{animation:arrivalsPop .5s ease}
@media (prefers-reduced-motion: reduce){ .nav-btn.arrivals-pop{animation:none} .nav-btn i{transition:none} }
body.lowfx .nav-btn.arrivals-pop{animation:none}

/* center Play — raised round action button docked on the seam, absolute-centered (independent of the six tabs'
   flex so all six keep an exact 1/6 width). Stays a CHILD of #nav so body.in-game #nav{display:none} hides it. */
.nav-fab{position:absolute;left:50%;bottom:calc(env(safe-area-inset-bottom) + 30px);transform:translateX(-50%);z-index:6;display:flex;flex-direction:column;align-items:center;gap:1px;background:none;padding:0}
.nav-fab .fab-circle{display:grid;place-items:center;width:54px;height:54px;border-radius:50%;
  background:linear-gradient(155deg,var(--fab1),var(--fab2));border:4px solid var(--card);
  box-shadow:0 8px 18px rgba(63,138,71,.36);animation:fabBreathe 2.8s ease-in-out infinite}
.nav-fab .fab-circle svg{width:25px;height:25px;fill:#fff;margin-left:3px;filter:drop-shadow(0 1px 1.5px rgba(35,60,35,.28))}  /* optical-center the triangle; shadow guarantees glyph separation */
.nav-fab .fab-label{font-size:10px;font-weight:700;color:var(--green-d)}
.nav-fab .fab-sub{font-size:8.5px;font-weight:800;line-height:1;margin-top:1px;padding:1px 6px;border-radius:8px;background:var(--green-d);color:#fff;letter-spacing:.3px;box-shadow:0 1px 3px rgba(63,138,71,.4);font-variant-numeric:tabular-nums}  /* level the Play button resumes into */
.nav-fab .fab-sub:empty{display:none}
.nav-fab.fab-hidden{display:none}  /* hidden on screens that own a primary play action (e.g. Daily) — see FAB_HIDE in ui.js */
.nav-fab:active .fab-circle{animation:none;transform:scale(.94);box-shadow:0 4px 10px rgba(63,138,71,.34)}
#navPlay:focus-visible{outline:3px solid var(--green-d);outline-offset:2px;border-radius:16px}
@keyframes fabBreathe{0%,100%{transform:scale(1)}50%{transform:scale(1.04)}}  /* gentle breathe, within calm ceiling (≤1.06) */
body.calm .nav-fab .fab-circle{animation:none}  /* honor the in-app Calm feel too — matches qReduced() */
@media (prefers-reduced-motion: reduce){.nav-fab .fab-circle{animation:none}}

/* ---------- Buttons ---------- */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;border-radius:14px;font-weight:600;font-size:16px;padding:13px 18px;transition:transform .08s}
.btn:active{transform:scale(.97)}
.btn-primary{background:var(--green);color:#fff;box-shadow:0 4px 0 var(--green-d)}
.btn-primary:active{box-shadow:0 1px 0 var(--green-d);transform:translateY(3px)}
.btn-amber{background:var(--amber);color:#5a3d00;box-shadow:0 4px 0 var(--amber-d)}
.btn-amber:active{box-shadow:0 1px 0 var(--amber-d);transform:translateY(3px)}
.btn-ghost{background:var(--card);border:1px solid var(--line);color:var(--ink);box-shadow:var(--shadow)}
.btn-block{width:100%}
.btn-sm{padding:9px 13px;font-size:14px;border-radius:12px}
.btn[disabled]{opacity:.5;pointer-events:none;filter:grayscale(.3)}
/* "can't afford yet" — a deactivated (not-green) look that STAYS tappable, so a tap
   opens the universal top-up modal instead of silently failing. Used on any currency
   buy button (.btn / .price-btn / .ds-buy / .ld-buy) when the wallet is short. */
.cant{background:#e8e6df!important;color:#9aa49a!important;border:1px solid rgba(60,80,50,.10)!important;box-shadow:none!important;filter:none!important;text-shadow:none!important;cursor:pointer!important}
.cant i{color:#9aa49a!important}
/* F19 — four-tier button grammar for the in-level flow, so a paid action can never masquerade
   as the free one: primary=recommended free action (green) · paid=costs currency (amber + a
   price chip that names the cost) · ghost=free alternative · quiet=low-stakes dismiss. The
   .btn-price chip is tier-agnostic (also rides a quiet ghost, e.g. the de-emphasised $0.99). */
.btn-paid{background:linear-gradient(180deg,#f6bd45,var(--amber));color:#5a3d00;box-shadow:0 4px 0 var(--amber-d);font-weight:700}
.btn-paid:active{box-shadow:0 1px 0 var(--amber-d);transform:translateY(3px)}
.btn-quiet{background:none;color:var(--muted);box-shadow:none;font-weight:600;padding:10px 14px}
.btn-quiet:active{transform:scale(.97)}
.btn .btn-price{display:inline-flex;align-items:center;gap:3px;margin-left:2px;padding:1px 8px;border-radius:999px;background:rgba(60,50,20,.13);font-weight:800;font-variant-numeric:tabular-nums;font-size:.92em}
.btn .btn-price i{font-size:1.05em}
.btn-paid .btn-price i{color:var(--c-gem)}   /* the gem keeps its brand blue on the gold button — reads as "this costs gems" */
/* soft "here's where to top up" highlight after a deep-link into the Shop (calm: no scale, <1Hz) */
@keyframes shopFocusPulse{0%{box-shadow:0 0 0 0 rgba(95,174,95,0)}25%{box-shadow:0 0 0 4px rgba(95,174,95,.32)}100%{box-shadow:0 0 0 0 rgba(95,174,95,0)}}
.shop-focus{animation:shopFocusPulse 1.2s ease-out 2;border-radius:14px}

/* ---------- Cards ---------- */
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);padding:16px;box-shadow:var(--shadow)}
.row{display:flex;align-items:center;gap:12px}
.spacer{flex:1}
.tag{font-size:12px;font-weight:600;padding:3px 9px;border-radius:999px;background:var(--green-l);color:var(--green-d)}

/* ---------- Level map ---------- */
.map-wrap{position:relative}
.map-nodes{position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;gap:14px;padding:8px 0 4px}
.lvl-node{position:relative;width:66px;height:66px;border-radius:50%;display:grid;place-items:center;font-weight:700;font-size:21px;color:#fff;box-shadow:var(--shadow);border:3px solid #fff;transition:transform .12s ease}
.lvl-node.done,.lvl-node.current{cursor:pointer}
.lvl-node.done{background:var(--green)}
.lvl-node.current{background:var(--amber);color:#5a3d00;animation:bob 1.6s ease-in-out infinite}
.lvl-node.current::after{content:'';position:absolute;inset:-3px;border-radius:50%;pointer-events:none;border:3px solid rgba(242,177,52,.55);opacity:0;animation:nodeReady 2.4s ease-out infinite}  /* a soft amber "you're here — play me" expanding ring; transform+opacity so the always-on map anim composites instead of repainting a box-shadow every frame */
@keyframes nodeReady{0%{transform:scale(.9);opacity:.75}70%{transform:scale(1.32);opacity:0}100%{transform:scale(1.32);opacity:0}}
body.calm .lvl-node.current::after{animation:none}
@media (prefers-reduced-motion:reduce){.lvl-node.current::after{animation:none}}
.lvl-node.locked{background:#c8d2c0;color:#fff}
.lvl-node.done:active,.lvl-node.current:active{transform:scale(.93)}
.lvl-node.pop{animation:nodePop .34s ease}
@keyframes nodePop{0%{transform:scale(1)}45%{transform:scale(1.06)}100%{transform:scale(1)}}
/* level-unlock reveal on the map (cosmetic; soft green ring, no scale beyond the node pop). Relocated
   from ftue.js so it also fires for returning players (the FTUE's injected CSS only mounts when it plays). */
.lvl-node.ftue-unlocked{animation:ftueUnlockGlow 1.5s ease-out 1}
@keyframes ftueUnlockGlow{0%{box-shadow:0 0 0 0 rgba(95,174,95,.55)}70%{box-shadow:0 0 0 13px rgba(95,174,95,0)}100%{box-shadow:0 0 0 0 rgba(95,174,95,0)}}
@media (prefers-reduced-motion:reduce){.lvl-node.ftue-unlocked{animation:none;box-shadow:0 0 0 3px rgba(95,174,95,.5)}}
.lvl-stars{position:absolute;bottom:-9px;left:50%;transform:translateX(-50%);display:flex;gap:2px;background:#fff;border-radius:999px;padding:2px 6px;box-shadow:0 1px 3px rgba(0,0,0,.15)}
.star{width:13px;height:13px;display:inline-block;vertical-align:middle}
.star path{fill:none;stroke:#c4c9bd;stroke-width:1.5;stroke-linejoin:round}   /* not won: hollow outline */
.star.on path{fill:var(--amber);stroke:var(--amber-d);stroke-width:1}          /* won: solid gold */
.lvl-stars .star{width:12px;height:12px}
.ev-badge{position:absolute;top:-7px;right:-7px;width:24px;height:24px;border-radius:50%;border:2px solid #fff;overflow:hidden;pointer-events:none;box-shadow:0 1px 4px rgba(50,70,40,.22);z-index:3}
.ev-badge svg{width:100%;height:100%;display:block}
@keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-6px)}}

/* ---------- Living map scene (ambient critters, trail, scenery) ---------- */
.map-bg{position:absolute;inset:0;z-index:1;pointer-events:none;overflow:hidden}
.map-trail{position:absolute;left:0;top:0}
.map-tree{position:absolute;pointer-events:none;filter:drop-shadow(0 3px 3px rgba(40,55,30,.14))}
.map-bush{position:absolute;pointer-events:auto;cursor:pointer;filter:drop-shadow(0 2px 2px rgba(40,55,30,.14))}
.map-bush.rustle{animation:bushRustle .5s ease}
@keyframes bushRustle{0%,100%{transform:rotate(0)}25%{transform:rotate(-3deg)}60%{transform:rotate(2deg)}}
.map-firefly{position:absolute;width:7px;height:7px;border-radius:50%;background:radial-gradient(circle,#fff6c0 0%,rgba(255,224,120,.5) 45%,rgba(255,224,120,0) 70%);animation:ffPulse 3.2s ease-in-out infinite}
@keyframes ffPulse{0%,100%{opacity:.22;transform:translateY(0)}50%{opacity:.9;transform:translateY(-7px)}}
.map-actors{position:absolute;inset:0;z-index:4;pointer-events:none;overflow:hidden}
.map-critter{position:absolute;left:0;top:0;width:46px;height:46px;pointer-events:auto;cursor:pointer;will-change:transform}
.mc-face{width:100%;height:100%;transform-origin:50% 85%;transition:transform .2s ease}
.mc-body{width:100%;height:100%;transform-origin:50% 92%;transition:transform .5s cubic-bezier(.4,0,.2,1),opacity .5s}
.mc-body>svg{width:100%;height:100%;display:block;filter:drop-shadow(0 3px 3px rgba(40,55,30,.18))}
.map-critter.duck .mc-body{transform:scale(.5) translateY(11px);opacity:.4}
.map-critter.startle .mc-body{animation:mcHop .45s ease}
@keyframes mcHop{0%{transform:translateY(0)}35%{transform:translateY(-12px)}70%{transform:translateY(-1px)}100%{transform:translateY(0)}}
.map-emote{position:absolute;left:50%;top:-10px;transform:translateX(-50%);font-size:18px;line-height:1;pointer-events:none;filter:drop-shadow(0 1px 2px rgba(40,55,30,.25));animation:mcEmote 1.6s ease forwards}
@keyframes mcEmote{0%{opacity:0;transform:translate(-50%,6px) scale(.5)}22%{opacity:1;transform:translate(-50%,-6px) scale(1)}80%{opacity:1;transform:translate(-50%,-12px) scale(1)}100%{opacity:0;transform:translate(-50%,-22px) scale(.9)}}
.map-shadow{position:absolute;width:120px;height:54px;border-radius:50%;background:radial-gradient(closest-side,rgba(45,55,35,.20),rgba(45,55,35,0));pointer-events:none;will-change:transform,opacity}

/* ── Living-map LANDSCAPE (CE.Sky-driven): a SKY header above a GRASS meadow.
   The time-of-day tint is motion-free and stays on in calm mode; star twinkle,
   cloud + bird drift are gated by mapAlive(). ── */
/* the grass pattern is a COSMETIC seam (CE.Cosmetic): --cos-grass-img/size swap the texture,
   falling back to the built-in procedural look when no art-set is equipped. --grass (the base
   colour) stays owned by the day/night sky system, so terrain sets never fight the time-of-day. */
.map-ground{position:absolute;inset:0;z-index:0;pointer-events:none;
  background-color:var(--grass,#bfe096);
  background-image:var(--cos-grass-img, repeating-linear-gradient(114deg, rgba(20,60,20,.045) 0 1px, transparent 1px 7px),
    radial-gradient(120% 60% at 50% 0%, rgba(255,255,255,.06), transparent 60%));
  background-size:var(--cos-grass-size, auto);
  transition:background-color 1.2s linear}
.map-sky-header{position:absolute;top:0;left:0;width:100%;z-index:0;pointer-events:none;overflow:hidden;
  background:linear-gradient(180deg,var(--sky-top,#8fc7f0) 0%,var(--sky-mid,#bfe0f5) 58%,var(--sky-horizon,#e6f2e0) 100%);
  transition:background 1.2s linear}
.map-sky-header::after{content:'';position:absolute;left:-6%;bottom:-1px;width:112%;height:40px;
  background:var(--grass,#bfe096);border-radius:50% 50% 0 0/100% 100% 0 0}
.sky-sun,.sky-moon,.sky-cloud,.sky-bird{position:absolute;left:0;top:0;will-change:transform}
.sky-moon.is-day{opacity:.72}
/* ── Moonstruck secret (moonlit.js) — discoverable glow + tappable moon ── */
.sky-moon.moonstruck{pointer-events:auto;cursor:pointer}
.sky-moon.moonstruck svg{filter:drop-shadow(0 0 5px rgba(220,228,255,.6))}                                   /* subtle standing halo (static = calm-safe) */
.sky-moon.moonstruck.is-full svg{filter:drop-shadow(0 0 9px rgba(232,238,255,.92)) drop-shadow(0 0 18px rgba(170,190,255,.55))}  /* real full moon = brighter */
.sky-moon.moonBreath svg{transform-origin:50% 50%;animation:moonBreath 2.6s ease-in-out 2}                   /* one-shot breathing nudge */
.sky-moon.moon-poke svg{animation:moonPoke .4s ease}
@keyframes moonBreath{0%,100%{transform:scale(1);opacity:.92}50%{transform:scale(1.06);opacity:1}}           /* scale capped at the CALM ceiling (1.06) */
@keyframes moonPoke{0%,100%{transform:scale(1)}40%{transform:scale(1.06)}}
.moon-chip{position:fixed;top:66px;right:12px;z-index:40;width:38px;height:38px;border:none;border-radius:50%;
  background:rgba(34,42,78,.5);color:#fff;font-size:18px;line-height:1;cursor:pointer;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);
  box-shadow:0 2px 8px rgba(20,28,60,.3);opacity:.7;transition:opacity .2s,box-shadow .2s}
.moon-chip:hover{opacity:1}
.moon-chip.on{background:rgba(90,108,170,.9);opacity:1;box-shadow:0 0 12px rgba(180,200,255,.6)}
/* the secret cosmetic coat — parent-class keyed off S.d.moonlit; CE.critterSVG is never touched */
.collect-grid.moonlit svg{filter:drop-shadow(0 0 5px #cfd8f0) drop-shadow(0 2px 2px rgba(40,55,30,.18))}
.collect-grid.moonlit.moonlit-full svg{filter:drop-shadow(0 0 7px #e6ecff) drop-shadow(0 0 13px rgba(180,200,255,.55)) drop-shadow(0 2px 2px rgba(40,55,30,.18))}
.pc-av.moonlit svg{filter:drop-shadow(0 0 5px #cfd8f0)}
.pc-av.moonlit.moonlit-full svg{filter:drop-shadow(0 0 8px #e6ecff) drop-shadow(0 0 13px rgba(180,200,255,.5))}
/* unlock celebration card */
.ml-card{text-align:center;padding:4px 4px 2px}
.ml-card-moon{display:flex;justify-content:center;margin:2px 0 6px}
.ml-card-h{margin:2px 0 6px;font-size:20px}
.ml-card-p{margin:0 0 14px;font-size:14px;line-height:1.5;color:#5a6452}
.ml-card-btns{display:flex;flex-direction:column;gap:8px}
@media (prefers-reduced-motion:reduce){.sky-moon.moonBreath svg,.sky-moon.moon-poke svg{animation:none}}
body.calm .sky-moon.moonBreath svg,body.calm .sky-moon.moon-poke svg{animation:none}
.sky-bird{opacity:.72}
.sky-star{position:absolute;border-radius:50%;opacity:var(--so,.7);
  background:radial-gradient(circle,#fff 0%,rgba(255,255,255,.55) 50%,rgba(255,255,255,0) 75%)}
.sky-star.tw{animation:skyTwinkle 4s ease-in-out infinite}
@keyframes skyTwinkle{0%,100%{opacity:calc(var(--so,.7)*.35)}50%{opacity:var(--so,.7)}}
.sky-cloud.drift{animation:skyDrift 46s ease-in-out infinite alternate}
@keyframes skyDrift{from{transform:translateX(-12%)}to{transform:translateX(12%)}}
.sky-bird.fly{animation:birdFly 30s linear infinite}
@keyframes birdFly{0%{transform:translate(-16%,0)}50%{transform:translate(52%,-9px)}100%{transform:translate(116%,0)}}
/* ---- map juice pass: grounded critters, walk-cycle life, ambient visitors, mastery blooms, depth ---- */
.mc-shadow{position:absolute;left:50%;bottom:1px;width:26px;height:7px;margin-left:-13px;border-radius:50%;background:rgba(40,60,30,.16);filter:blur(1px);pointer-events:none}
.map-critter.duck .mc-shadow{opacity:.35}
.map-critter.walking .mc-body{animation:mcWalk .48s ease-in-out infinite alternate}   /* ambient tier: tiny 1.5px bob + 2° sway, silently loops only while moving */
@keyframes mcWalk{from{transform:translateY(0) rotate(-1.6deg)}to{transform:translateY(-1.5px) rotate(1.6deg)}}
.map-butterfly{position:absolute;left:0;top:0;font-size:17px;line-height:1;pointer-events:auto;cursor:pointer;will-change:transform;filter:drop-shadow(0 1px 2px rgba(40,55,30,.2));z-index:5}
.map-drift{position:absolute;font-size:13px;line-height:1;pointer-events:none;will-change:transform;opacity:0}
/* 3★ mastery bloom: perfected levels flower on the map — static, earned, theirs */
.lvl-node.bloom::before{content:'🌼';position:absolute;left:-7px;bottom:-3px;font-size:12px;line-height:1;pointer-events:none;filter:drop-shadow(0 1px 1px rgba(40,55,30,.25))}
.lvl-node.bloom::after{content:'🌷';position:absolute;right:-6px;bottom:-4px;font-size:11px;line-height:1;pointer-events:none;filter:drop-shadow(0 1px 1px rgba(40,55,30,.25))}
/* distant hills: two soft bands tinted from the live sky palette; parallax drifts them at 0.25× scroll */
.map-hills{position:absolute;left:0;right:0;height:60px;z-index:1;pointer-events:none;will-change:transform}
.map-hills svg{position:absolute;inset:0;width:100%;height:100%}
.map-hills .mh-far path{fill:var(--sky-horizon,#dcebc8);opacity:.65}
.map-hills .mh-near path{fill:var(--grass,#bfe096);opacity:.55;filter:brightness(.94)}
/* night lanterns: a static warm glow lifts nodes off the dim meadow (calm-safe) */
.map-wrap.night .lvl-node:not(.locked){box-shadow:var(--shadow),0 0 16px 3px rgba(255,206,120,.42)}
.map-wrap.night .lvl-node{text-shadow:0 1px 2px rgba(20,24,40,.32)}

@media (prefers-reduced-motion:reduce){
  .map-firefly,.map-emote,.map-bush.rustle,.lvl-node.pop,.map-critter.startle .mc-body,
  .map-critter.walking .mc-body,
  .lvl-node.current,.sky-star.tw,.sky-cloud.drift,.sky-bird.fly{animation:none}   /* bob was escaping the guard */
}

/* ---------- Low-FX performance tier (body.lowfx, set by config.js on weak devices) ----------
   Quiets the always-on decorative layer so the calm core scrolls at 60fps on a low-end WebView.
   Pure presentation: never changes game state, the board, or readability. The living-map ambient
   sim (JS) is also skipped in ui.js via CE.lowFx, so these elements mostly aren't even built. */
/* 2026-08-07 — the tier stops switching off things measurement says are FREE. arrivalsPop, bob,
   skyTwinkle, skyDrift, birdFly, ffPulse, mcHop and comboBeat animate transform/opacity ONLY, which
   the compositor owns; a 49-tile transform+opacity board sweep held 60fps / 0.00% janky on the
   reference SD636, and superposing it on a live water scene cost nothing measurable
   (PERF_MEASURED.md §9.2, §9.5).

   CORRECTED 2026-08-08 on the device the tier exists for. "Compositor-safe" means no main-thread
   layout or paint; it does NOT mean free. Every permanently-animating element gets its own
   compositing layer -- GPU memory plus a composite pass every frame -- and a Snapdragon 636 pushing
   1080x2280 has no headroom for a screenful of them. config.js's own probe was calibrated on exactly
   that phone and says its static hints read lowFx FALSE, which is why the MEASURED tier exists at all.
   Ungating these re-created the problem that probe was written to solve. lowfx is a BUDGET control,
   not a correctness control; the ambient layer answers to it again below. */
body.lowfx .nav-fab .fab-circle,
body.lowfx .ltier.cur .lt-ico{animation:none}
/* The always-on map ambient layer. Ungating it above exposed a REAL bug the tier had been hiding:
   these five infinite animations had NO calm / reduced-motion kill at all — the performance tier was
   the only thing that ever stopped them, so a player who asked for reduced motion still got a moving
   map. Cost and consent are different questions; these are cheap (transform/opacity) but must still
   answer to the player. Both kills added — this is what paint-lint P2 exists to catch. */
body.lowfx .fab-attract,
body.lowfx .lvl-node.current,
body.lowfx .nav-btn.arrivals-pop,
body.lowfx .combo-rail.beating .combo-chip,
body.calm .sky-star.tw,
body.calm .sky-cloud.drift,
body.calm .sky-bird.fly,
body.calm .map-firefly,
body.calm .map-critter.walking .mc-body,
body.lowfx .sky-star.tw,
body.lowfx .sky-cloud.drift,
body.lowfx .sky-bird.fly,
body.lowfx .map-firefly,
body.lowfx .map-critter.walking .mc-body{animation:none}
@media (prefers-reduced-motion:reduce){
  .sky-star.tw,.sky-cloud.drift,.sky-bird.fly,.map-firefly,.map-critter.walking .mc-body{animation:none}
}
/* drop-shadow = an offscreen blur pass re-run on every composite/scroll — drop it on weak GPUs */
body.lowfx .mc-body>svg,
body.lowfx .map-tree,
body.lowfx .map-bush,
body.lowfx .map-emote,
body.lowfx .map-butterfly,
body.lowfx .mc-shadow,
body.lowfx .lvl-node.bloom::before,
body.lowfx .lvl-node.bloom::after,
body.lowfx .critter svg,
body.lowfx .hab-fly svg{filter:none}
body.lowfx .map-wrap.night .lvl-node:not(.locked){box-shadow:var(--shadow)}   /* drop the 16px per-node glow */
/* flatten card/list paint: a hairline shadow rasters far cheaper across a scrolling list than a blurred one */
body.lowfx .card,body.lowfx .tcard,body.lowfx .lb-row,body.lowfx .tshort,body.lowfx .hud-pill{box-shadow:0 1px 2px rgba(50,70,40,.12)}
/* in-level: the combo glow (filter:brightness), pipe wave (stroke) + drop-shadow, and tool-target
   pulse (filter:drop-shadow) are PAINT-bound loops that re-rasterize every frame during play — quiet
   them on weak GPUs (gameplay + readability unchanged; only the decorative pulse is dropped) */
body.lowfx .combo-rail.beating .combo-chip,
body.lowfx .combo-rail.beating .combo-fill,
body.lowfx .critter.tool-target{animation:none}
/* .pipe-liquid is no longer tier-gated. Its two kills existed because the drain re-ran a blur filter
   on animating geometry; the glow is now a flat sibling stroke (see .pipe-glow), so the low tier keeps
   the full gauge — a mount wave and a lit drain — instead of a bare line. Refactored, not disabled. */
/* lowfx coverage audit (2026-07-18): infinite animations that ESCAPED the tier gate. The node-ready
   ring alone held the Levels map at ~40fps / ~27% CPU on an SD660 with Reduce effects ON — under
   lowfx every infinite loop must stop. (nodeReady's base opacity is 0, so animation:none hides it.) */
body.lowfx .lvl-node.current::after,
body.lowfx .tool.booster.charged,
body.lowfx .tc-ico.tc-chest,
body.lowfx .win-critters .wc,
body.lowfx .win-critters.st1.go .wc,
body.lowfx .win-critters.st2.go .wc,
body.lowfx .win-critters.st3.go .wc-v0,
body.lowfx .win-critters.st3.go .wc-v1,
body.lowfx .win-critters.st3.go .wc-v2,
body.lowfx .coach-hand,
body.lowfx .coach-glow,
body.lowfx .coach-tapring,
body.lowfx .coach-hand2,
body.lowfx .coach-ripple,
body.lowfx .critter.coach-hero svg,
body.lowfx .type-intro.ready .lt-aff{animation:none}
body.lowfx .coach-glow{box-shadow:0 0 0 3px rgba(242,177,52,.55)}   /* static highlight — mirrors the reduced-motion fallback */
/* in-level low-FX: drop every per-frame blur/shadow pass on the board (critter drop-shadow, mis-tap
   pip glow) and halt board spark particles — JS already caps them to 3, this stops the residual
   animate() repaints on weak GPUs. Gameplay + readability unchanged; only decorative paint is cut. */
body.lowfx.in-game .critter svg,
body.lowfx.in-game .mistakes .mh.used{filter:none}
body.lowfx.in-game .spark{display:none}

/* F7: aligned one-row header. Nav (back+pause) left, title+chip center (title ellipsizes, chip never wraps),
   hearts right — all on one 44px baseline. Truncation is scoped to .puzzle-top so the reused .lvl-label
   leaderboard headers ("You"/"Friends") are untouched. */
.puzzle-top .pz-nav{flex:0 0 auto;display:flex;align-items:center;gap:6px}
/* HUD stability (owner 2026-07-14): the title sits DEAD-CENTRE on every level — a 3-track grid keeps it
   there while the type chip lives in the right-hand well (reserved even when empty, so "Level 16" lands on
   the same pixel with or without a chip). Grid auto-placement follows dir, so RTL mirrors for free. */
.puzzle-top .lvl-mid{flex:1 1 auto;min-width:0;display:grid;grid-template-columns:1fr auto 1fr;grid-template-rows:auto auto;align-items:center;row-gap:1px}
.puzzle-top .lvl-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;grid-column:2;grid-row:1}
.puzzle-top .lm-well{min-width:0;display:flex;align-items:center;overflow:hidden;grid-row:1}
/* the LEFT well holds the stage chip: align it toward the title so the two wells read as a matched pair
   (default flex-start would park it against the pause button). Writing-mode-relative ⇒ correct in RTL. */
.puzzle-top .lm-well:first-child{justify-content:flex-end}
/* LEVEL NAME LINE (owner 2026-07-28). Row 2, spanning all three tracks so the name has the whole
   header width instead of a 112px well — the localized names ("باغِ جانوران", "In the Water") never
   fit beside the title and were being clipped or dropped to an icon. min-height is UNCONDITIONAL: the
   line holds its 17px whether this level has a type or not, so entering a typed level after an
   untyped one cannot move the board, the combo rail or anything below it by a single pixel. */
.puzzle-top .lvl-typeline{grid-column:1 / -1;grid-row:2;min-height:17px;display:flex;align-items:center;justify-content:center;overflow:hidden}
.puzzle-top .lvl-typeline .ltype-chip{flex:0 1 auto;margin:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;padding:1px 8px;line-height:1.35}
/* Stage fraction (multi-stage levels only). A flag glyph + "k/N", never pips — Baby Rescue's cub bar
   below is already a heart-pip row + fraction and a second one would read as another goal counter.
   Deliberately compact: it shares .lvl-mid's leftover space 50/50 with the type chip (both wells are
   1fr), and that leftover is small once the hearts are on the row. LTR-pinned so "2/3" never reverses
   in fa (the numerals are localized by CE.i18n.num when the string is built). */
.puzzle-top .stage-chip{flex:0 0 auto;display:inline-flex;align-items:center;gap:2px;padding:2px 6px;border-radius:999px;
  font-size:12px;font-weight:700;line-height:1.4;white-space:nowrap;direction:ltr;
  background:var(--green-l);color:var(--green-d)}
.puzzle-top .stage-chip .ti{font-size:11px}
/* Phone widths: the 44px row is genuinely tight. On a 390px screen the content box is 350 — nav 82 +
   five hearts (5×24+4×6=144) + gaps 16 leaves .lvl-mid ~108, the label eats ~68, and the two 1fr wells
   split the remaining ~40 → 20px each. The stage fraction still has to survive that, so it tightens
   here. The type chip no longer competes for the row at all — it moved to its own full-width line
   below (.lvl-typeline), which is why the old rule that blanked its label to `font-size:0` on phones
   is gone: that rule WAS the "level name is not visible" bug on every phone. */
@media (max-width:430px){
  .puzzle-top .mistakes{gap:5px}
  .puzzle-top .mistakes .mh{width:21px;height:21px}
}
/* 360px floor with a 5-heart bonus run is the one case that still can't fit both chips; the fraction
   degrades to its numerals there rather than disappearing. */
@media (max-width:380px){
  .puzzle-top .stage-chip .ti{display:none}
  .puzzle-top .mistakes{gap:4px}
  .puzzle-top .mistakes .mh{width:19px;height:19px}
}
.puzzle-top .mistakes{flex:0 0 auto}
/* fixed tool anchors: scout stays far-left, Rewind+Rush far-right — an obstacle tool joining the middle
   never slides the constants (F11 relevance rule unchanged; only geometry is pinned) */
.puzzle-tools .tool-gap{flex:1 1 0;min-width:0;padding:0;border:0;background:none;box-shadow:none;pointer-events:none}
/* F8B pause sheet: sound/music pill toggles + a calm board blur behind the sheet (content zones only, so the
   fixed pipe-frame isn't caught by the filter; off under reduced-motion). */
.pause-togs{display:flex;gap:8px;justify-content:center;margin:2px 0 14px}
.pause-tog{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:999px;border:1.5px solid var(--line);background:#fff;color:var(--muted);font-weight:600;font-size:14px}
.pause-tog.on{border-color:var(--green);color:var(--green-d);background:var(--green-l)}
.pause-tog i{font-size:17px}
body.paused .pz-stage,body.paused .pz-dock{filter:blur(2.5px);transition:filter .18s ease}
@media (prefers-reduced-motion:reduce){body.paused .pz-stage,body.paused .pz-dock{filter:none}}
/* ---------- Puzzle ---------- */
/* F1B/F5B scene tokens (shared so the cell math and the scene padding can never desync — one source each).
   --cell is ONE tile size for EVERY level: computed for the 6-col worst case, clamped, applied to all boards,
   so a 4×4 and a 6×6 render at the same tile size (smaller boards just take a smaller centered footprint). */
body.in-game{
  --scene-pad-x:20px;    /* = #screen in-game horizontal padding */
  --board-pad-x:10px;    /* = .board-wrap.has-pipe horizontal padding */
  --board-gap:7px;       /* = board grid gap */
  --cell:clamp(42px, calc((100vw - 2*var(--scene-pad-x) - 2*var(--board-pad-x) - 5*var(--board-gap)) / 6), 58px);
}
.puzzle-top{display:flex;align-items:center;gap:8px;margin:2px 0 8px;min-height:44px}
/* Roll Call quota strip — calm "bring these home" list above the board (cosmetic; never gates state) */
.quota-bar{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:8px;width:100%;max-width:430px;margin:0 auto 8px}
.quota-bar .quota-lbl{font-size:12px;font-weight:600;color:var(--muted);margin-right:2px}
.quota-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 9px 3px 5px;border-radius:999px;background:rgba(70,90,55,.07);box-shadow:inset 0 0 0 1px rgba(70,90,55,.08);transition:background .25s ease,box-shadow .25s ease,opacity .25s ease}
.quota-chip .qc-art{width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center}
.quota-chip .qc-art svg{width:24px;height:24px;display:block}
.quota-chip .qc-n{font-size:13px;font-weight:700;color:var(--ink,#3c4a35);font-variant-numeric:tabular-nums}
.quota-chip.done{background:rgba(158,194,122,.22);box-shadow:inset 0 0 0 1px rgba(120,170,90,.35)}
.quota-chip.done .qc-n{color:#5a8a3a}

/* ===== Baby Rescue: Survival (BABY_RESCUE.md) — cubs, sleepers, the goal pips + juice (≤ CALM ceiling) ===== */
.cub-bar .cub-pips{display:inline-flex;gap:5px;align-items:center}
.cub-pip{display:inline-flex;color:rgba(150,150,160,.42);font-size:16px}
.cub-pip.saved{color:#e8749a}
.cub-pip.saved i{animation:qpulse .4s ease}
.cub-bar .qc-n{font-size:13px;font-weight:700;color:var(--ink,#3c4a35);font-variant-numeric:tabular-nums;margin-left:3px}
/* the CUB on the board: a soft objective glow (calm opacity pulse — no scale) */
.critter.baby{position:relative}
.critter.baby::after{content:"";position:absolute;inset:-7%;border-radius:26%;pointer-events:none;z-index:-1;
  background:radial-gradient(circle at 50% 46%, rgba(255,182,120,.55), rgba(255,182,120,0) 68%);animation:cubGlow 2.6s ease-in-out infinite}
@keyframes cubGlow{0%,100%{opacity:.32}50%{opacity:.7}}
/* the SLEEPER: gently dimmed + a cool hush until woken */
.critter.sleepy{filter:saturate(.72) brightness(.98)}
.critter.sleepy::after{content:"";position:absolute;inset:-7%;border-radius:26%;pointer-events:none;z-index:-1;
  background:radial-gradient(circle at 50% 46%, rgba(150,170,210,.38), rgba(150,170,210,0) 66%)}
/* R9 SNAIL (R9_SPECIALS_PLAN.md §3.1): a slow-exit critter. A small shell badge marks it; on rescue it
   dashes home at a snail's pace and holds its lane a beat longer. Calm: the badge is static, the slow dash
   is a gentle ease with no strobe, the slime trail is ≤4 soft motes. */
.critter.slow{position:relative}
.critter.slow::after{content:"🐌";position:absolute;right:-7%;bottom:-9%;font-size:42%;line-height:1;
  pointer-events:none;z-index:6;filter:drop-shadow(0 1px 1px rgba(0,0,0,.18));transform:rotate(-4deg)}
.critter.slow-exit{transition:transform .9s cubic-bezier(.33,0,.15,1),opacity .9s cubic-bezier(.4,0,.5,1)!important}
.slime{position:absolute;left:50%;top:54%;width:6px;height:6px;border-radius:50%;pointer-events:none;z-index:4}
/* wake beats — max scale 1.05, within the ceiling */
.critter.yawn{animation:cubYawn .42s ease}
.critter.wakeup{animation:cubWake .46s ease}
@keyframes cubYawn{0%{transform:scaleY(1)}35%{transform:scale(1.05,.94)}70%{transform:scale(.99,1.03)}100%{transform:scale(1)}}
@keyframes cubWake{0%{transform:scale(1)}45%{transform:scale(1.05)}100%{transform:scale(1)}}
/* a heart rises on a cub rescue + the z-puff particle */
.cub-heart{position:absolute;left:50%;top:34%;transform:translate(-50%,0);color:#e8749a;font-size:18px;pointer-events:none;z-index:7;text-shadow:0 1px 2px rgba(0,0,0,.12)}
.zpuff{position:absolute;left:50%;top:42%;width:7px;height:7px;border-radius:50%;background:#cfd6dd;pointer-events:none;z-index:6}
/* all cubs safe: a calm warm wash over the board (opacity only) */
.board.cubs-safe{position:relative}
.board.cubs-safe::after{content:"";position:absolute;inset:-4%;pointer-events:none;z-index:8;border-radius:18px;
  background:radial-gradient(circle at 50% 45%, rgba(255,201,140,.5), rgba(255,182,190,.22) 45%, rgba(255,182,190,0) 72%);animation:cubsSafeWash 1.2s ease-out}
@keyframes cubsSafeWash{0%{opacity:0}28%{opacity:1}100%{opacity:0}}
@media (prefers-reduced-motion:reduce){.critter.baby::after{animation:none}.critter.yawn,.critter.wakeup{animation:none}}
body.calm .critter.baby::after,body.lowfx .critter.baby::after{animation:none}
/* P3: Baby Rescue send-off — the freed cub waves goodbye (art rock only, no scale → 0% impulse), a 👋 emoji rises,
   then the wave-clear "Cubs safe!" banner (scale .96→1 = 4% ≤1.06, fade, no translate). All calm-gated. */
.critter svg.cub-wave{transform-origin:50% 78%;animation:cubWaveBye .42s ease}
@keyframes cubWaveBye{0%{transform:rotate(0)}22%{transform:rotate(-7deg)}52%{transform:rotate(6deg)}78%{transform:rotate(-3deg)}100%{transform:rotate(0)}}
.cub-emoji{position:absolute;left:50%;top:26%;transform:translate(-50%,0);font-size:18px;pointer-events:none;z-index:7}
.cub-wave-banner{position:absolute;left:50%;top:38%;transform:translate(-50%,-50%) scale(.96);display:inline-flex;align-items:center;gap:8px;padding:9px 16px;border-radius:999px;background:linear-gradient(180deg,rgba(255,245,232,.97),rgba(255,232,214,.97));box-shadow:0 6px 20px rgba(180,120,60,.22),inset 0 0 0 1px rgba(220,150,90,.35);color:#7a4a1e;font-weight:800;pointer-events:none;z-index:9;opacity:0;white-space:nowrap}
.cub-wave-banner .cwb-s{font-weight:600;opacity:.72;font-size:.86em}
.cub-wave-banner.in{opacity:1;transform:translate(-50%,-50%) scale(1);transition:opacity .22s ease,transform .28s cubic-bezier(.2,1.2,.3,1)}
.cub-wave-banner.out{opacity:0;transition:opacity .3s ease}
@media (prefers-reduced-motion:reduce){.critter svg.cub-wave{animation:none}.cub-wave-banner,.cub-wave-banner.in{transition:none}}
body.calm .critter svg.cub-wave,body.lowfx .critter svg.cub-wave{animation:none}
/* ENDLESS TAIL (ENDLESS_TAIL.md) ------------------------------------------------ */
/* the GOLDEN CUB (§1.6): a static soft ring from the theme.golden token (--tail-gold/--tail-gold-glow are
   set inline from theme.js — no color constant here); .shimmer adds a slow opacity-only glow (calm-safe:
   no scale, no flash — 2.6s, opacity .10→.18), stripped under reduced-motion/calm like the cub glow */
.critter.golden{position:relative}
.critter.golden::before{content:"";position:absolute;inset:-9%;border-radius:30%;pointer-events:none;z-index:-1;
  box-shadow:0 0 0 2.5px var(--tail-gold);opacity:.55}
.critter.golden.shimmer::after{content:"";position:absolute;inset:-14%;border-radius:34%;pointer-events:none;z-index:-1;
  background:radial-gradient(circle at 50% 46%, var(--tail-gold-glow), transparent 70%);animation:tailGoldShimmer 2.6s ease-in-out infinite}
@keyframes tailGoldShimmer{0%,100%{opacity:.10}50%{opacity:.18}}
@media (prefers-reduced-motion:reduce){.critter.golden.shimmer::after{animation:none;opacity:.12}}
body.calm .critter.golden.shimmer::after,body.lowfx .critter.golden.shimmer::after{animation:none;opacity:.12}
/* ===== In the Water (WATER_LEVELS.md §6) — pond skin · lily pad + wake · Breeze Pinwheel ================
   Everything is scoped to .board.ltype-water / .board-wrap.water-board, so every non-water board is
   byte-identical. Direction lives on the pad prow + wake (rotated as one by --wturn, written once per
   drift turn in ui.js waterTurn) — the critter art stays upright and never re-renders. Calm ceiling:
   pad tween ≤ .26s and ≤1.0 scale, bob ≤2px, pinwheel is decoration; the strength sliver + --wturn are
   tier-0 and keep updating even when reduced-motion stops the spin. */
.board.ltype-water{--pond-a:#a9dced;--pond-b:#6bb8d2;--pond-edge:rgba(18,70,90,.26);--lily-a:#93d07d;--lily-b:#4e9a52;--wake-c:rgba(255,255,255,.9);--breeze:#e0a94a}
.board.ltype-water .slot:not(.wall):not(.void){background:linear-gradient(158deg,var(--pond-a),var(--pond-b));box-shadow:inset 0 2px 8px var(--pond-edge),inset 0 -1px 2px rgba(255,255,255,.34)}
/* the lily pad + wake — one rotating group in the slot, inserted BEFORE the critter so DOM order paints
   the animal on top (no z-index on either → the critter's later paint wins; robust against stacking edits) */
.float-fx{position:absolute;inset:0;pointer-events:none;transform:rotate(var(--wturn,0deg));transition:transform .26s cubic-bezier(.34,1.24,.64,1)}
.float-fx .lily{position:absolute;inset:10%;border-radius:50%;background:radial-gradient(circle at 38% 32%,var(--lily-a),var(--lily-b) 74%);box-shadow:0 2px 5px rgba(18,58,30,.28),inset 0 1px 2px rgba(255,255,255,.34)}
.float-fx .lily::after{content:"";position:absolute;left:50%;top:6%;width:30%;height:40%;transform:translateX(-50%);border-radius:50% 50% 50% 50%/60% 60% 40% 40%;background:linear-gradient(180deg,rgba(255,255,255,.28),rgba(255,255,255,0));pointer-events:none}   /* a lighter "prow" lip → the front of the pad reads even before you find the wake */
.float-fx .wake{position:absolute;left:50%;top:-7%;width:34%;height:26%;transform:translateX(-50%);overflow:visible}
.float-fx .wake path{fill:var(--wake-c);stroke:rgba(22,74,92,.42);stroke-width:1;stroke-linejoin:round}
.float-fx.spent{transition:transform .5s ease,opacity .5s ease;opacity:0;transform:rotate(var(--wturn,0deg)) scale(.82)}
/* the floater: an upright critter that bobs on the water (no signpost, no arrow — the pad carries heading) */
.critter.float svg{animation:floatBob 3.6s ease-in-out infinite;animation-delay:var(--rdelay,0s)}
@keyframes floatBob{0%,100%{transform:translateY(0)}50%{transform:translateY(-2px)}}
/* the grounded anchors: a stone slab so it reads as "this one never turns" */
.critter.stone::before{content:"";position:absolute;inset:6%;border-radius:26%;background:linear-gradient(155deg,#8a99a2,#5d6d72 66%);box-shadow:0 2px 5px rgba(18,40,45,.32),inset 0 1px 2px rgba(255,255,255,.26);z-index:-1}
/* ===== Drift Ring UX pass (WATER_LEVELS_UX.md, flag water-drift-ring) ===================================
   Per-pad countdown: the arc fills in the pad's eddy direction, full circle = one 90° snap; the last
   quarter blends to amber ("about to turn"). PURE CSS per cycle — ui.js ringArm writes --dr-ms/--dr-anim
   once per snap and the browser sweeps alone (no idle rAF, no per-frame JS; restart = keyframe-name flip,
   so re-arming never forces a reflow). Ring completes ≤250ms BEFORE the logical snap (tick granularity) —
   early, never late. Tier-0 legibility like the wake, but cheap: lowfx steps(24) ⇒ ≤24 repaints/cycle,
   calm/reduced steps(4) ⇒ four quarter reads. */
.drift-ring{position:absolute;inset:0;pointer-events:none;transition:opacity .4s ease}
.drift-ring circle{fill:none}
.drift-ring .dr-t{stroke:rgba(255,255,255,.22);stroke-width:2.2}
/* stroke-dashoffset/stroke are SVG PAINT properties — Blink never composites them, so a `linear`
   sweep repaints every ring on every frame. Measured on a Snapdragon 636: seven rings held a water
   board at 33fps / 78% janky; the same board with the ring animation off ran 58.6fps / 1% (A/B
   reproduced twice). The tier steps below already knew this, but they hang off body.lowfx — which
   never engages on a modern phone (config.js CE.lowFx) — so every real device got `linear`.
   steps(30) is the DEFAULT now: same countdown, same warn colour, ~30 repaints per cycle instead of
   one per frame. A countdown reads fine stepped (it is a clock, not a spin). */
.drift-ring .dr-a{stroke:rgba(255,255,255,.9);stroke-width:2.6;stroke-linecap:round;stroke-dasharray:125.7;
  transform:rotate(-90deg);transform-origin:50% 50%;
  animation:var(--dr-anim,none) var(--dr-ms,4000ms) steps(30) forwards}
.drift-ring.ccw{transform:scaleX(-1)}
.drift-ring.spent{opacity:0}
.pond-hold .drift-ring .dr-a{animation-play-state:paused}
@keyframes driftFillA{0%{stroke-dashoffset:125.7;stroke:rgba(255,255,255,.9)}70%{stroke:rgba(255,255,255,.9)}82%{stroke:var(--breeze,#e0a94a)}100%{stroke-dashoffset:0;stroke:var(--breeze,#e0a94a)}}
@keyframes driftFillB{0%{stroke-dashoffset:125.7;stroke:rgba(255,255,255,.9)}70%{stroke:rgba(255,255,255,.9)}82%{stroke:var(--breeze,#e0a94a)}100%{stroke-dashoffset:0;stroke:var(--breeze,#e0a94a)}}
/* whirl glide (D3: still fully tappable): during a gust the pad tween goes linear so chained 90° snaps read
   as one smooth spin; the class drops at settle, so the final retarget lands on the springy .26s. */
.water-board.gust-glide .float-fx{transition:transform .45s linear}
/* islet upgrade: a raised dry stone breaking the surface — "no ring = never turns" */
.wux2 .critter.stone::before{inset:1%;border-radius:46%;background:radial-gradient(circle at 36% 26%,#aeb9bc,#66777d 74%);box-shadow:0 3px 7px rgba(14,42,52,.38),inset 0 2px 3px rgba(255,255,255,.4)}
.wux2 .critter.stone::after{content:"";position:absolute;inset:-7%;border-radius:50%;pointer-events:none;z-index:-1;box-shadow:0 0 0 2px rgba(255,255,255,.34);opacity:.6}
/* tiers: quarter-step ring under calm/reduced, coarse steps on lowfx; glide falls back to the calm snap */
@media (prefers-reduced-motion:reduce){.drift-ring .dr-a{animation-timing-function:steps(4)}
  .water-board.gust-glide .float-fx{transition:transform .12s cubic-bezier(.34,1.24,.64,1)}}
body.calm .drift-ring .dr-a{animation-timing-function:steps(4)}
body.lowfx .drift-ring .dr-a{animation-timing-function:steps(24)}
body.calm .water-board.gust-glide .float-fx,body.lowfx .water-board.gust-glide .float-fx{transition:transform .12s cubic-bezier(.34,1.24,.64,1)}
/* Breeze Pinwheel HUD — top-right of the board, above the tiles */
.board-wrap.water-board{position:relative}
.board-wrap.water-board .lvl-foliage{display:none}   /* meadow leaves off; P4 owns the gust leaves */
.water-hud{position:absolute;top:-34px;right:2px;display:flex;align-items:center;gap:7px;z-index:6;pointer-events:none;
  padding:4px 8px 4px 10px;border-radius:999px;background:rgba(255,255,255,.62);box-shadow:0 2px 8px rgba(30,80,100,.14),inset 0 0 0 1px rgba(255,255,255,.5);backdrop-filter:blur(3px)}
.water-hud .wh-whisper{font-size:12px;font-weight:700;color:var(--breeze,#c98a2e);opacity:0;max-width:0;overflow:hidden;white-space:nowrap;transition:opacity .4s ease,max-width .4s ease}
.water-hud .wh-whisper.show{opacity:1;max-width:150px}
.water-hud .wh-pin{display:block;width:28px;height:28px}
.water-hud .wh-pin svg{width:100%;height:100%;display:block;transform-origin:50% 50%;animation:pinSpin var(--spin-ms,13000ms) linear infinite}
.water-hud .wh-pin .bl-a{fill:#e0a94a}.water-hud .wh-pin .bl-b{fill:#f7ecd6}.water-hud .wh-pin .bl-hub{fill:#3a4b3f}
.water-hud.tele .wh-pin{animation:pinQuiver .16s ease-in-out infinite}
.water-hud .wh-strength{position:relative;width:4px;height:22px;border-radius:3px;background:rgba(30,80,100,.16);overflow:hidden;align-self:center}
.water-hud .wh-strength i{position:absolute;left:0;right:0;bottom:0;height:100%;border-radius:3px;background:var(--breeze);transform:scaleY(var(--wind,0));transform-origin:bottom;transition:transform .12s linear}
@keyframes pinSpin{to{transform:rotate(360deg)}}
@keyframes pinQuiver{0%,100%{transform:rotate(-3deg)}50%{transform:rotate(3deg)}}
/* gust dressing (juice.js CE.juice.water) — leaves skitter across, one sheen sweep at peak, splash droplets.
   All WAAPI-animated + self-removing. The board is NOT clipped (critter exits translate far off-board), so
   leaves just blow a touch past the edge — which reads as wind, not a bug. */
.board.ltype-water .water-leaf{position:absolute;left:0;width:11px;height:7px;pointer-events:none;z-index:5;
  border-radius:72% 28% 64% 36%;background:linear-gradient(120deg,var(--breeze,#e0a94a),var(--lily-a,#93d07d));box-shadow:0 1px 1px rgba(20,50,25,.2)}
.board.ltype-water .water-sheen{position:absolute;top:0;bottom:0;left:0;width:55%;pointer-events:none;z-index:4;
  background:linear-gradient(100deg,transparent,rgba(255,247,220,.5) 45%,rgba(210,240,246,.28) 60%,transparent)}
.water-drop{position:absolute;left:50%;top:42%;width:6px;height:6px;border-radius:50%;pointer-events:none;z-index:6;box-shadow:0 0 3px rgba(150,220,240,.55)}
/* calm / reduced-motion / lowfx: pads snap, bob + blade-spin stop; --wturn + strength sliver stay live (tier-0) */
@media (prefers-reduced-motion:reduce){.float-fx{transition-duration:.12s}.critter.float svg{animation:none}.water-hud .wh-pin svg{animation:none}.water-hud.tele .wh-pin{animation:none}}
body.calm .float-fx,body.lowfx .float-fx{transition-duration:.12s}
body.calm .critter.float svg,body.lowfx .critter.float svg{animation:none}
body.calm .water-hud .wh-pin svg,body.lowfx .water-hud .wh-pin svg,body.calm .water-hud.tele .wh-pin,body.lowfx .water-hud.tele .wh-pin{animation:none}

/* win-card opt-in strip (§1.1): one quiet ghost-tone row ≤64px, before .popup-actions — no pulse, no countdown */
.tail-strip{display:flex;align-items:center;gap:10px;max-height:64px;margin:2px 0 10px;padding:9px 12px;border-radius:14px;
  background:rgba(70,90,55,.06);box-shadow:inset 0 0 0 1px rgba(70,90,55,.09);text-align:left}
.tail-strip .ts-ico{font-size:22px;flex:0 0 auto}
.tail-strip .ts-txt{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
.tail-strip .ts-txt b{font-size:14px;font-weight:800;color:var(--ink,#3c4a35);line-height:1.15}
.tail-strip .ts-sub{font-size:11.5px;font-weight:600;color:#7c8a73;line-height:1.2}
.tail-strip .ts-go{flex:0 0 auto;padding:8px 13px;font-size:13px;white-space:nowrap}
[dir=rtl] .tail-strip{text-align:right}
/* bank interstitial / bank card bits: the banked chip + the personal-best sparkle line */
.tail-bank{font-variant-numeric:tabular-nums}
.tail-pb{color:var(--amber,#e6a32e);font-weight:800;white-space:nowrap}
/* golden-wave pre-level announce: the amber accent on the shipped intro shell */
.si-lvl.golden{color:var(--amber,#e6a32e)}
/* Par Path — calm "moves vs par" chip above the board (cosmetic; never gates state) */
.par-bar{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;max-width:430px;margin:0 auto 8px}
.par-bar .par-lbl{display:inline-flex;align-items:center;gap:4px;font-size:13px;font-weight:700;color:var(--ink,#3c4a35);padding:3px 10px;border-radius:999px;background:rgba(70,90,55,.07);box-shadow:inset 0 0 0 1px rgba(70,90,55,.08)}
.par-bar .par-moves{font-size:13px;font-weight:600;color:var(--muted);font-variant-numeric:tabular-nums;transition:color .25s ease}
.par-bar .par-moves.over{color:#c88a3a}   /* gently warm (not red) once you pass par — no loss-aversion */
/* Stacked critters — a friend hidden on top. A soft peek-card behind hints at the one beneath, so the
   depth is fair (known, not hidden information); revealing on top-clear is a calm scale-in (<=1.06). */
.critter.stack-over{z-index:3}
.critter.stack-over::before{content:'';position:absolute;left:7px;right:-6px;top:9px;bottom:-6px;border-radius:24%;background:rgba(150,170,120,.32);box-shadow:inset 0 0 0 1.5px rgba(70,90,55,.18);z-index:-1}
.critter.reveal-under{animation:revealUnder .34s cubic-bezier(.3,1.3,.5,1)}
@keyframes revealUnder{0%{transform:scale(.62);opacity:0}60%{transform:scale(1.05)}100%{transform:scale(1);opacity:1}}
@media (prefers-reduced-motion:reduce){.critter.reveal-under{animation:none}}
/* in-level MIS-TAP pips: remaining = calm green dot, spent = a red ✕ (distinct from the play-gate hearts) */
.mistakes{display:flex;gap:6px;align-items:center}
.mistakes .mh{width:24px;height:24px;display:block}   /* F15: hearts read a touch smaller than the old dots; header title/chip still fit one row */
.mistakes .mh.on path{fill:var(--c-critter)}                                           /* F15 remaining mis-tap: a calm green heart */
.mistakes .mh.bonus.on path{fill:var(--c-coin)}                                        /* Lucky Acorn's extra mis-tap: a golden heart */
.mistakes .mh.bonus.on{filter:drop-shadow(0 1px 2px rgba(230,163,46,.4))}
.mistakes .mh.used path{fill:#efe7df;stroke:var(--c-error);stroke-width:1.4;opacity:.8} /* spent: a soft hollow heart (calm — not a hard ✕) */
.mistakes .mh.lost-beat{animation:heartLost .55s ease}
@keyframes heartLost{0%{transform:scale(1.06)}55%{transform:scale(.94)}100%{transform:scale(1)}}
.mistakes .mh-wrap{position:relative;display:block}                                     /* Bear's gift pip wears its 🐻 tag (attribution ⑨) */
.mistakes .mh-wrap .mh-tag{position:absolute;bottom:-4px;inset-inline-end:-5px;font-size:9px;font-style:normal;pointer-events:none}
.lvl-label{font-weight:700;font-size:18px}
.board-wrap{display:flex;justify-content:center;margin:6px 0 12px}
.board{display:grid;gap:var(--board-gap,7px);width:100%;max-width:430px;touch-action:none}   /* board never scrolls → drag gestures become deterministic pointer streams for the swipe-coach tracker (COACH.md §v2a) */
/* F1B pre-set play area: width/height are computed from --cell × the deterministic col/row counts, so the
   board's footprint is fixed the instant the stage mounts (no reflow when critters/gate-bars/particles paint)
   AND every level's tile is the SAME size. grid repeat(cols,1fr) then makes each column exactly --cell wide. */
.board[style*="--bcols"]{
  width:calc(var(--cell) * var(--bcols) + var(--board-gap) * (var(--bcols) - 1));
  height:calc(var(--cell) * var(--brows) + var(--board-gap) * (var(--brows) - 1));
  max-width:100%;
}
/* Safety net for boards WIDER than the 6-col --cell design max (the F1B formula above assumes ≤6 cols):
   scale the tile down so the board always fits the scene instead of its slots painting past the viewport
   (aspect-ratio:1/1 slots take the row-track height, so a width-squeezed board still overflowed
   horizontally — the L15 3×9 Rapids bug). For every ≤6-col board --cell-fit ≥ --cell, so min() keeps
   them byte-identical. Shaped boards (.tri-board/.hex-board) self-size in their renderers and stay
   pinned to --cell. */
.board[style*="--bcols"]:not(.tri-board):not(.hex-board){
  --cell-fit:min(var(--cell), calc((100vw - 2*var(--scene-pad-x,20px) - 2*var(--board-pad-x,10px) - (var(--bcols) - 1)*var(--board-gap,7px)) / var(--bcols)));
  width:calc(var(--cell-fit) * var(--bcols) + var(--board-gap,7px) * (var(--bcols) - 1));
  height:calc(var(--cell-fit) * var(--brows) + var(--board-gap,7px) * (var(--brows) - 1));
}
.slot{position:relative;aspect-ratio:1/1;border-radius:13px;background:rgba(255,255,255,.45);box-shadow:inset 0 0 0 1px rgba(70,90,55,.06)}
.critter{position:absolute;inset:0;cursor:pointer;transition:transform .36s cubic-bezier(.5,0,.2,1),opacity .36s}   /* no blanket will-change: it force-promoted 20-28 filtered compositor layers per board; the exit transition promotes on its own for its .36s */
.critter svg{width:100%;height:100%;display:block;filter:drop-shadow(0 2px 2px rgba(40,55,30,.18))}
.critter:active{transform:scale(.93)}
.critter.exit-U{transform:translateY(-900%) scaleY(1.15) scaleX(.82);opacity:0}
.critter.exit-D{transform:translateY(900%) scaleY(1.15) scaleX(.82);opacity:0}
.critter.exit-L{transform:translateX(-900%) scaleX(1.15) scaleY(.82);opacity:0}
.critter.exit-R{transform:translateX(900%) scaleX(1.15) scaleY(.82);opacity:0}
.critter.shake{animation:shk .42s}
.critter.pulse{animation:pulse .6s ease 2}
/* ===== TRUE triangle board (POLYGON_BOARDS.md §5) — absolute triangular tile layer ===== */
/* ONE clean big triangle (POLYGON_BOARDS.md §5). The board IS the triangle's bounding box; a single SVG
   plate fills it edge-to-edge, so there's NO competing rectangular frame or per-tile square. */
.board.tri-board{display:block;gap:0;position:relative;margin:0 auto;background:none!important;box-shadow:none!important}
/* suppress the rectangular play-area frame + texture + watermark + foliage so only the shaped board reads
   (shared by the triangle and hexagon true-geometry boards) */
.board-wrap.has-pipe.shaped-wrap{background:none!important;box-shadow:none!important;padding-top:4px}
.board-wrap.shaped-wrap::before{display:none!important}
.board-wrap.shaped-wrap .lt-watermark,.board-wrap.shaped-wrap .lvl-foliage{display:none!important}
/* the big-triangle plate + subtle tessellation grid + a bold outer border (the "allowed" boundary) */
.tri-plate-wrap{position:absolute;inset:0;z-index:0;pointer-events:none}
.tri-plate-wrap svg{width:100%;height:100%;display:block;overflow:visible}
.tri-plate{fill:var(--lt-l,#e7f0d8);fill-opacity:.5}
.tri-grid polygon{fill:none;stroke:var(--lt-k,#5a6b52);stroke-width:1.1;stroke-opacity:.13;stroke-linejoin:round}
.tri-edge{fill:none;stroke:var(--lt-b,#8FBF52);stroke-width:4.5;stroke-opacity:.55;stroke-linejoin:round}
/* tri-slots: transparent hit-targets that hold critters (clip = the triangle so taps tessellate cleanly) */
.tri-slot{position:absolute;aspect-ratio:auto;border-radius:0!important;background:none!important;box-shadow:none!important;pointer-events:none;z-index:1}
.tri-slot::before{display:none!important}
.tri-slot .critter{pointer-events:auto}
.tri-slot.tri-up .critter{clip-path:polygon(50% 0,100% 100%,0 100%)}
.tri-slot.tri-down .critter{clip-path:polygon(0 0,100% 0,50% 100%)}
/* exit fling — the critter slides STRAIGHT out its edge's outward normal (clip removed so it flies free) */
.critter.exit-D,.critter.exit-U,.critter.exit-UL,.critter.exit-UR,.critter.exit-DL,.critter.exit-DR{clip-path:none!important;opacity:0}
.critter.exit-D{transform:translateY(760%) scale(.82)}                 /* down (up-tile bottom edge) */
.critter.exit-U{transform:translateY(-760%) scale(.82)}                /* up (down-tile top edge) */
.critter.exit-UL{transform:translate(-640%,-370%) scale(.82)}          /* up-left */
.critter.exit-UR{transform:translate(640%,-370%) scale(.82)}           /* up-right */
.critter.exit-DL{transform:translate(-640%,370%) scale(.82)}           /* down-left */
.critter.exit-DR{transform:translate(640%,370%) scale(.82)}            /* down-right */

/* ===== TRUE hexagon board (POLYGON_BOARDS.md §6) — pointy-top honeycomb tile layer ===== */
/* The board IS the honeycomb's bounding box; one SVG draws a faint per-cell fill + a BOLD outer silhouette
   (boundary edges only), so there's NO competing rectangular frame. */
.board.hex-board{display:block;gap:0;position:relative;margin:0 auto;background:none!important;box-shadow:none!important}
.hex-plate-wrap{position:absolute;inset:0;z-index:0;pointer-events:none}
.hex-plate-wrap svg{width:100%;height:100%;display:block;overflow:visible}
.hex-cells polygon{fill:var(--lt-l,#e7f0d8);fill-opacity:.42;stroke:var(--lt-k,#5a6b52);stroke-width:1.1;stroke-opacity:.16;stroke-linejoin:round}
.hex-edge line{fill:none;stroke:var(--lt-b,#8FBF52);stroke-width:5;stroke-opacity:.6;stroke-linecap:round}
/* hex-slots: transparent hit-targets that hold critters (clip = the hexagon so taps tessellate cleanly) */
.hex-slot{position:absolute;aspect-ratio:auto;border-radius:0!important;background:none!important;box-shadow:none!important;pointer-events:none;z-index:1}
.hex-slot::before{display:none!important}
.hex-slot .critter{pointer-events:auto;clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%)}
/* exit fling — the critter slides STRAIGHT out its edge's outward normal (clip removed so it flies free) */
.critter.exit-E,.critter.exit-W,.critter.exit-NE,.critter.exit-NW,.critter.exit-SE,.critter.exit-SW{clip-path:none!important;opacity:0}
.critter.exit-E{transform:translateX(720%) scale(.82)}                 /* east (right edge) */
.critter.exit-W{transform:translateX(-720%) scale(.82)}                /* west (left edge) */
.critter.exit-NE{transform:translate(600%,-500%) scale(.82)}           /* up-right */
.critter.exit-NW{transform:translate(-600%,-500%) scale(.82)}          /* up-left */
.critter.exit-SE{transform:translate(600%,500%) scale(.82)}            /* down-right */
.critter.exit-SW{transform:translate(-600%,500%) scale(.82)}           /* down-left */
@keyframes shk{0%,100%{transform:scale(1)}30%{transform:scale(1.05,.93)}60%{transform:scale(.98,1.02)}}
@keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.12)}}
/* F11: tray fits the in-game content width even in the 6-tool worst case (scout+spin+pick+key+rewind+rush).
   Budget = the #screen in-game content width: 360px floor − 40px (#screen 20px×2 pad, styles.css:435) = 320px.
   6×48 min-width + 5×6 gap = 318 ≤ 320 → never clips or scrolls (tools/quality-gate.mjs asserts this). */
.puzzle-tools{display:flex;gap:6px;justify-content:center;margin-top:4px;min-height:64px;align-items:stretch;flex-wrap:nowrap}
/* fixed tool slots: >=44px tap target + a reserved-width count badge so the row's geometry is set
   ONCE — a count going 1->99 (or the booster 0%->100%) never re-flows the rail under the board. */
.tool{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;background:#fff;border:1px solid var(--line);border-radius:14px;padding:6px 4px;box-shadow:var(--shadow);min-width:48px;min-height:44px}
.tool i{font-size:22px;color:var(--green-d)}
.tool .cnt{font-size:12px;font-weight:600;color:var(--muted);text-align:center;font-variant-numeric:tabular-nums}
.tool .lbl{font-size:11px;line-height:1;font-weight:600;color:var(--muted);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}   /* F11: ellipsize, never wrap — survives long i18n labels */
.tool.armed{box-shadow:0 0 0 2px var(--green),var(--shadow)}
.tool.armed i{color:var(--green)}
.critter.tool-target{animation:toolTargetPulse 1.2s ease-in-out infinite}
@keyframes toolTargetPulse{0%,100%{filter:none}50%{filter:drop-shadow(0 0 6px var(--green))}}
@media (prefers-reduced-motion:reduce){.critter.tool-target{animation:none;outline:2px dashed var(--green);outline-offset:2px}}
body.calm .critter.tool-target{animation:none;outline:2px dashed var(--green);outline-offset:2px}
.tool:active{transform:scale(.96)}
.float-coin{position:absolute;font-weight:600;color:var(--amber-d);pointer-events:none;animation:fup .8s ease forwards;z-index:5;font-size:13px;opacity:.9}   /* P2/D2: de-emphasized (was 17px/700) so the growing combo is the hero; +5 stays honest but quiet */
@keyframes fup{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-40px)}}

/* ---------- Sanctuary ---------- */
.sanc-hero{border-radius:var(--r-lg);padding:18px;background:linear-gradient(160deg,#cbeccf,#aee0c0);box-shadow:var(--shadow);margin-bottom:14px;position:relative;overflow:hidden}
.sanc-hero h3{margin:0 0 2px;font-size:20px}
.sanc-critters{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
.sanc-critters .mini{width:42px;height:42px;background:rgba(255,255,255,.6);border-radius:11px;padding:3px}
.task{display:flex;align-items:center;gap:12px;padding:12px 4px;border-bottom:1px solid var(--line)}
.task:last-child{border-bottom:none}
.task .ti-circle-check{color:var(--green)}
.collect-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(60px,1fr));gap:10px;margin-top:8px}
.collect-cell{background:#fff;border:1px solid var(--line);border-radius:14px;padding:6px;text-align:center;box-shadow:var(--shadow)}
.collect-cell svg{width:100%;height:auto}
.collect-cell.unseen{filter:grayscale(1);opacity:.32}
.collect-cell .cn{font-size:11px;color:var(--muted);font-weight:600;margin-top:2px}

/* ---------- Shop / Daily ---------- */
.shop-card{display:flex;align-items:center;gap:12px;padding:14px;margin-bottom:11px}
.shop-icon{width:50px;height:50px;border-radius:14px;display:grid;place-items:center;font-size:27px;color:#fff;flex-shrink:0}
.shop-card h4{margin:0 0 2px;font-size:16px}
.shop-card p{margin:0;font-size:13px;color:var(--muted);font-weight:500}
/* ONE commerce button (owner feedback 2026-07-22). Every "spend currency" control — shop packs, the
   free gift, piggy/aviary/burrow, Limited-Edition drops, Garden décor — shares this geometry; only the
   HUE says which tender it is. Each module used to invent its own (35/34/28px tall, weights 700/800,
   radii 12/11/10, pressable in the catalogue but flat in drops and décor). Modal CTAs keep the larger
   .btn (45px) — two intentional sizes, not five. Radius stays the literal 12px: --r-md is 14px, so
   the token would silently reshape the canonical button. */
.price-btn,.ld-buy,.ds-buy{background:var(--green);color:#fff;border:none;border-radius:12px;padding:9px 14px;font-weight:700;font-size:var(--fs-md);white-space:nowrap;flex-shrink:0;cursor:pointer;text-align:center;box-shadow:0 3px 0 var(--green-d)}   /* nowrap+no-shrink: fa prices ("۲۹٬۹۰۰ تومان") run longer than "$2.99" and were wrapping to 2–3 lines, giving every buy button a different height */
.price-btn:active,.ld-buy:not(.sold):not(.owned):active,.ds-buy:active{transform:translateY(2px);box-shadow:0 1px 0 var(--green-d)}
/* tender = hue only: gem carries its semantic token, cash (usd) keeps the green. Grey .cant still wins
   over both — every one of its properties is !important. */
.ld-buy.gem,.ds-buy.gem{background:var(--c-gem);color:#fff;box-shadow:0 3px 0 #2a7fc7}
.ld-buy.gem:active,.ds-buy.gem:active{box-shadow:0 1px 0 #2a7fc7}
.ld-buy.usd,.ds-buy.usd{background:var(--green);color:#fff;box-shadow:0 3px 0 var(--green-d)}
/* sold-out / already-owned are NOT buy buttons — they stay flat and unpressable (and are not .cant,
   so the grey shortfall override never reaches them). */
.ld-buy.sold,.ld-buy.owned{box-shadow:none;cursor:default}
.ld-buy.sold:active,.ld-buy.owned:active{transform:none;box-shadow:none}
.badge-best{position:absolute;top:10px;right:10px;background:var(--amber);color:#5a3d00;font-size:11px;font-weight:700;padding:3px 9px;border-radius:999px}
.bundle-head{font-weight:700;font-size:15px;margin:6px 2px 9px}
.bundle-card.picked{outline:2px solid var(--green);outline-offset:1px}
.proto-note{font-size:12px;color:var(--muted);text-align:center;margin:8px 0 2px;font-weight:500;min-height:30px}

/* ---------- Overlay popups ---------- */
/* F14: backdrop snaps to full dim in <=120ms so the board never ghosts through a mid-fade card/text */
#overlay{position:absolute;inset:0;background:rgba(35,45,30,.5);display:grid;place-items:center;z-index:50;padding:max(22px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(22px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));animation:fade .12s ease-out}
@keyframes fade{from{opacity:0}to{opacity:1}}
/* .popup is its own scroll container: max-height fits the safe-area-padded overlay cell, overscroll-behavior
   contains the gesture (no background scroll-chaining, item #8), internal scroll keeps header+close reachable
   on short/notched screens (item #10a). body.popup-open freezes the layer behind the dim. */
/* F14: card settles in 200ms with a NON-overshoot curve (peak scale = 1.0, within the 1.06 calm ceiling — the old .2,1.3,.4,1 could overshoot) */
.popup{position:relative;background:#fff;border-radius:24px;padding:22px 20px;width:100%;max-width:330px;max-height:100%;overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;text-align:center;box-shadow:0 20px 50px rgba(0,0,0,.3);animation:pop .2s cubic-bezier(.2,.9,.3,1)}
body.popup-open,body.popup-open #screen{overflow:hidden}
@media (max-height:560px){.popup{padding:14px 16px}}
@keyframes pop{from{transform:scale(.9);opacity:0}to{transform:scale(1);opacity:1}}
/* Outside-tap on a GUARDED popup (continue/fail/win flows): one gentle "make a choice" pulse — a single
   rise+decay, scale 1.03 (calm ceiling 1.06), never a shake, no sound. */
.popup.need-choice{animation:needChoice .32s ease-out}
@keyframes needChoice{0%{transform:scale(1)}45%{transform:scale(1.03)}100%{transform:scale(1)}}
/* F14: reduced-motion → no entrance animation, card appears instantly opaque (no ghost, no scale) */
@media (prefers-reduced-motion:reduce){#overlay{animation:none}.popup{animation:none}.popup.need-choice{animation:none}}
/* R6 (fail sheet shows the board): a fail/continue popup becomes a bottom SHEET. #overlay.sheet-mode thins the
   scrim to a bottom-weighted gradient (the board reads through the top) and pins the card to the bottom; the card
   goes full-width with a rounded top and slides up. Buttons/flows are unchanged — only the frame moves. */
#overlay.sheet-mode{align-items:end;padding:0;background:linear-gradient(to bottom,rgba(35,45,30,.10) 0%,rgba(35,45,30,.30) 55%,rgba(35,45,30,.6) 100%)}
.popup.popup-sheet{align-self:end;width:100%;max-width:560px;max-height:80%;border-radius:22px 22px 0 0;
  padding:22px 20px calc(20px + env(safe-area-inset-bottom));box-shadow:0 -12px 40px rgba(0,0,0,.28);animation:sheetUp .3s cubic-bezier(.2,.9,.3,1)}
@keyframes sheetUp{from{transform:translateY(100%);opacity:.7}to{transform:translateY(0);opacity:1}}
@media (prefers-reduced-motion:reduce){.popup.popup-sheet{animation:none}}
body.calm .popup.popup-sheet{animation:none}
.popup h2{margin:6px 0 4px;font-size:23px}
.popup p{margin:0 0 14px;color:var(--muted);font-weight:500}
.popup .big-ico{font-size:48px;line-height:1}
.popup .stuck-ico{animation:stuckIn .45s ease-out}   /* "A Critter's Stuck!" — one rise+decay entrance, calm ceiling ≤1.05, no loop */
@keyframes stuckIn{0%{transform:scale(.82);opacity:0}60%{transform:scale(1.05)}100%{transform:scale(1);opacity:1}}
@media (prefers-reduced-motion:reduce){.popup .stuck-ico{animation:none}}
.lpromo{display:flex;align-items:center;gap:10px;background:linear-gradient(135deg,#fef6e4,#fdeccb);border:1.5px solid #f2d79a;border-radius:14px;padding:10px 12px;margin:0 0 12px}   /* League promotion chest banner */
.lpromo-ico{font-size:26px;line-height:1}
.lpromo-body{flex:1;text-align:left;min-width:0}
.lpromo-t{font-weight:800;color:#8a5a12}
.lpromo-r{font-size:13px;color:#a07a2e;display:flex;gap:4px;flex-wrap:wrap;margin-top:2px}
.popup .stars{display:flex;justify-content:center;gap:10px;margin:10px 0}
.popup .stars .star{width:42px;height:42px}
/* §8.2 plump star: a 10-vertex polygon inflated by a same-colour round stroke (paint-order:stroke). ONE style —
   no flawless gradient variant. Unfilled = a soft ghost (no dark outline); filled = gold with a darker gold rim. */
.popup .stars .star polygon{fill:rgba(122,160,106,.18);stroke:none}
.popup .stars .star.on polygon{fill:var(--amber);stroke:var(--amber-d);stroke-width:2.6;stroke-linejoin:round;paint-order:stroke}
.popup .stars .star.on{animation:starPop .42s cubic-bezier(.2,1.45,.4,1) backwards}   /* R1: JS staggers the fill one-by-one */
.popup .stars .star.on polygon{filter:drop-shadow(0 2px 4px rgba(214,146,15,.4))}
@keyframes starPop{0%{transform:scale(.3) rotate(-22deg);opacity:0}60%{transform:scale(1.06) rotate(4deg)}100%{transform:scale(1) rotate(0);opacity:1}}
.win-critters{display:flex;justify-content:center;gap:6px;margin:2px 0 4px}
/* §9.3 phases: entrance pop → gentle idle bob → star-tier verdict move once .go lands (ui.js, last-star beat).
   All ≤ CALM ceiling: travel ≤5px on a 46px sprite, rotate ≤6°, scale ≤1.06. Inline animation-delay staggers
   both the entrance and the loops (the st3 dance troupe moves out of phase on purpose). */
.win-critters .wc{width:46px;height:46px;display:block;transform-origin:50% 80%;animation:wcIn .45s cubic-bezier(.2,1.4,.4,1) both, wcBob 2.6s ease-in-out .45s infinite}
@keyframes wcIn{from{opacity:0;transform:translateY(7px) scale(.86)}to{opacity:1;transform:translateY(0) scale(1)}}
@keyframes wcBob{0%,100%{transform:translateY(0) rotate(-1.5deg)}50%{transform:translateY(-2px) rotate(1.5deg)}}
.win-critters.go .wc{opacity:1}
.win-critters.st1.go .wc{animation:wcSway 2.3s ease-in-out infinite}
@keyframes wcSway{0%,100%{transform:rotate(-4deg)}50%{transform:rotate(4deg) translateY(-2px)}}
.win-critters.st2.go .wc{animation:wcCheer 1.05s cubic-bezier(.3,1.2,.4,1) infinite}
@keyframes wcCheer{0%,100%{transform:translateY(0) scaleY(1)}18%{transform:translateY(0) scaleY(.94)}46%{transform:translateY(-5px) scaleY(1.04)}74%{transform:translateY(0) scaleY(1)}}
.win-critters.st3.go .wc-v0{animation:wcDanceA 1.15s ease-in-out infinite}
.win-critters.st3.go .wc-v1{animation:wcDanceB 1.15s ease-in-out infinite}
.win-critters.st3.go .wc-v2{animation:wcDanceC 1.15s ease-in-out infinite}
@keyframes wcDanceA{0%,100%{transform:translateX(-3px) rotate(-6deg)}50%{transform:translateX(3px) rotate(6deg) translateY(-3px)}}
@keyframes wcDanceB{0%,100%{transform:translateY(0) rotate(4deg)}25%{transform:translateY(-4px) rotate(-2deg)}50%{transform:translateY(0) rotate(-5deg)}75%{transform:translateY(-4px) rotate(2deg)}}
@keyframes wcDanceC{0%,100%{transform:translateY(-3px) scale(1.03)}50%{transform:translateY(1px) scale(.97) rotate(-4deg)}}
body.calm .win-critters .wc{animation:none !important}
/* §9.2 verdict line — a static plain-English sentence (information is never motion-gated) */
.win-sub{font-size:13.5px;font-weight:600;color:#6b7a5c;line-height:1.45;margin:0 14px 10px;text-align:center}
.reward-row{display:flex;justify-content:center;gap:16px;margin:10px 0 16px;font-weight:600}
.reward-row .it{display:flex;align-items:center;gap:5px}
/* daily result: the cause of the score, then what actually landed in the wallet (§F8-A) */
.dmech-why{font-size:12.5px;line-height:1.45;margin:-8px 0 10px;text-align:center;max-width:34ch;
  margin-inline:auto}
.reward-row.dmech-earn{margin:0 0 2px;gap:14px}
.dmech-earn-l{font-size:11px;text-align:center;margin-bottom:8px;letter-spacing:.02em}
/* F1: itemized "earned → where it went" ledger — a calm receipt (icon · amount · muted destination pill) */
.reward-ledger{flex-direction:column;gap:7px;align-items:stretch;max-width:280px;margin-left:auto;margin-right:auto}
.reward-ledger .rl-item{display:flex;align-items:center;gap:8px;font-weight:800;font-size:15px;color:#54634a}
.reward-ledger .rl-item .ti{font-size:19px;flex:none}
.reward-ledger .rl-dest{margin-left:auto;font-weight:700;font-size:11px;color:#8aa07d;background:rgba(122,160,106,.13);padding:2px 9px;border-radius:99px;white-space:nowrap}
.place-strip{display:flex;align-items:center;justify-content:center;gap:6px;margin:-4px 0 12px;font-size:12.5px;font-weight:700;color:#6b7a5c}
.place-strip i{font-size:15px}
/* Endgame completion: live-ops progress fold · gentle "remaining to next" line · placement-strip reveal juice. All calm-ceiling safe (≤7px rise, no scale/flash/particle/haptic). */
.res-progress{display:flex;flex-direction:column;gap:6px;max-width:290px;margin:0 auto 12px}
.res-progress .rp-item{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:700;color:#5f6f52;background:rgba(122,160,106,.10);border-radius:12px;padding:6px 11px;animation:rpIn .42s cubic-bezier(.2,1.1,.4,1) both}
.res-progress .rp-item i{font-size:16px;flex:none;color:#7bbe6a}
.res-progress .rp-next{background:none;color:#8aa07d;font-weight:700;justify-content:center;padding:1px 0 0;font-size:12.5px}
.res-progress .rp-next i{color:#8bbf6f;font-size:15px}
@keyframes rpIn{from{transform:translateY(7px);opacity:0}to{transform:translateY(0);opacity:1}}
.place-strip{animation:rpIn .5s cubic-bezier(.2,1.1,.4,1) both}
/* calm + reduced-motion → render the endgame progress cluster + placement strip STATIC (information is never motion-gated) */
@media (prefers-reduced-motion:reduce){ .res-progress .rp-item,.place-strip{animation:none !important} }
body.calm .res-progress .rp-item,body.calm .place-strip{animation:none !important}
/* ===== Iconic end-game: stat tiles · progress card · rank chip — low-text, count-up + meter (§9.4: heart-pips removed) ===== */
.win-stats{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin:8px auto 14px;max-width:320px}
.stat-tile{position:relative;flex:1 1 0;min-width:56px;max-width:100px;background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:9px 6px 8px;display:flex;flex-direction:column;align-items:center;gap:3px;box-shadow:var(--shadow);animation:rpIn .4s cubic-bezier(.2,1.1,.4,1) both}
.stat-tile>.ti:first-child{font-size:20px}
.stat-tile .stat-n{font-size:20px;font-weight:800;color:var(--ink);line-height:1;font-variant-numeric:tabular-nums}
.stat-tile .stat-max{font-size:10px;color:var(--muted);opacity:.7;margin-top:-2px}
.stat-tile .stat-dest{position:absolute;right:5px;bottom:4px;font-size:12px;color:var(--muted);opacity:.5}
/* A4 Golden Rush: gold-tinted coin tile + the ×2 chip; gold-hit pops on the doubling beat (scale ≤ CALM 1.06) */
.stat-tile.golden{background:linear-gradient(160deg,#fff7e0,#ffedbd);border-color:#ecd28e}
.stat-tile .gold-x2{font-size:10px;font-weight:800;color:#a06e00;background:#ffe9b3;border-radius:999px;padding:1px 6px;line-height:1.4;white-space:nowrap}
.stat-tile.gold-hit{animation:goldHit .42s cubic-bezier(.2,1.2,.4,1)}
@keyframes goldHit{50%{transform:scale(1.06)}}
/* A1: win-card mini wallet — the VISIBLE landing target for the reward flight (the HUD is hidden in-game) */
.win-wallet{position:absolute;top:10px;right:12px;display:flex;gap:6px;z-index:2}
.win-wallet .ww{display:inline-flex;align-items:center;gap:4px;background:var(--card);border:1px solid var(--line);border-radius:999px;padding:4px 9px;box-shadow:var(--shadow);font-weight:800;font-size:13px;color:var(--ink);font-variant-numeric:tabular-nums}
.win-wallet .ww .ti{font-size:14px;color:#e6a32e}
.win-wallet .ww.gem .ti{color:#3b9ae8}
[dir=rtl] .win-wallet{right:auto;left:12px}
/* ===== §8.1 grouped Progress Card — one bordered card folds pass/rxp/event/streak (where earnings ACCRUE);
   the stat tiles above are what you EARNED this run. Rings → linear meters (RTL-safe via inset-inline). ===== */
.win-prog{width:min(320px,100%);margin:8px auto 12px;background:var(--card);border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--shadow);padding:2px 12px}
.wp-row{display:grid;grid-template-columns:24px 1fr auto;column-gap:10px;align-items:center;min-height:38px}
.wp-row+.wp-row{border-top:1px solid var(--line)}
.wp-ico{width:24px;height:24px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex:none}
.wp-ico .ti{font-size:15px}
.wp-ico.t-pass{background:rgba(95,174,95,.14)} .wp-ico.t-pass .ti{color:var(--green-d,#3f8a47)}
.wp-ico.t-rxp{background:rgba(242,177,52,.16)} .wp-ico.t-rxp .ti{color:var(--amber-d,#d6920f)}
.wp-ico.t-event{background:rgba(63,138,71,.14)} .wp-ico.t-event .ti{color:var(--green-d,#3f8a47)}
.wp-ico.t-streak{background:rgba(242,177,52,.16)} .wp-ico.t-streak .ti{color:var(--amber,#f2b134)}
.wp-meter{height:8px;border-radius:999px;background:rgba(122,160,106,.15);position:relative;overflow:hidden}
.wp-fill{position:absolute;inset-block:0;inset-inline-start:0;border-radius:999px;width:0}
.wp-pips{display:flex;gap:4px}
.wp-pips .pip{flex:1;height:8px;border-radius:3px;background:rgba(242,177,52,.22)}
.wp-pips .pip.lit{background:var(--amber,#f2b134)}
.wp-pips .pip.next{box-shadow:inset 0 0 0 2px rgba(242,177,52,.5)}
.wp-val{min-width:48px;display:inline-flex;align-items:center;justify-content:flex-end;font-size:14px;font-weight:800;color:var(--ink);font-variant-numeric:tabular-nums}
.wp-val .num{color:var(--green-d,#3f8a47)}
.wp-val .den{font-size:12px;font-weight:800;color:var(--muted)}
.wp-val .rxp-gain{margin-inline-start:5px}
/* ===== §8.3 ONE pill grammar + the chip shelf (rank chip · Power Hour · refHook · mural · mission · streak gift) ===== */
.win-chips{display:flex;flex-wrap:wrap;justify-content:center;gap:6px;max-width:320px;margin:6px auto 12px}
.win-chip{border:0;border-radius:999px;padding:5px 11px;font-size:12px;font-weight:800;font-variant-numeric:tabular-nums;display:inline-flex;align-items:center;gap:5px}
.win-chip>.ti{font-size:14px}
.win-chip.sm{padding:1px 7px;font-size:11px}   /* the small delta pill (+41, gift chips) */
.win-chip.wc-green{background:var(--green-l,#e7f6e2);color:var(--green-d,#3f8a47)}
.win-chip.wc-amber{background:#fff2cf;color:#8a5a00}
.win-chip.wc-purple{background:rgba(154,143,224,.14);color:#6a4bb0}
.win-chip.done-stamp{cursor:pointer}
.win-chip.done-stamp .mck{color:var(--green,#5fae5f);animation:discBloom .42s ease-out both}
.win-chip .mbc-paw{font-size:14px}
/* Win-card micro-derby (B1/B4): the rank chip becomes a 3-5 row mini-ladder — your row FLIP-slides up
   past the rival you just passed (transforms set inline by runWinDerby; ≤4 rows, ~120ms/row).
   Calm/reduced-motion renders the FINAL order statically with the ± chip as the tell. */
.win-derby{margin:2px auto 12px;max-width:300px;animation:rpIn .42s cubic-bezier(.2,1.1,.4,1) both}
.win-derby .dby-chip{display:inline-flex;align-items:center;gap:4px;justify-content:center;margin:0 auto 6px;font-weight:800;font-size:15px;color:var(--ink);font-variant-numeric:tabular-nums}
.win-derby .dby-chip>.ti{color:var(--amber,#f2b134);font-size:16px}
.win-derby .dby-pop{opacity:0;font-weight:800;font-size:11px;color:#3d8b3d;background:rgba(95,174,95,.18);padding:2px 7px;border-radius:999px}
.win-derby .dby-pop.on{opacity:1;animation:dbyPop .38s cubic-bezier(.2,1.05,.4,1) both}
@keyframes dbyPop{0%{transform:scale(.7);opacity:0}100%{transform:scale(1);opacity:1}}
.win-derby .dby-rows{display:flex;flex-direction:column;gap:2px;overflow:hidden;border-radius:11px}
.win-derby .dby-row{display:grid;grid-template-columns:38px 30px 1fr auto;align-items:center;gap:8px;height:34px;padding:0 8px;border-radius:9px;background:var(--card-2,#f3f1e8);will-change:transform}
.win-derby .dby-row.you{background:rgba(242,177,52,.14);box-shadow:inset 0 0 0 2px var(--amber,#f2b134);position:relative;z-index:1}
.win-derby .dby-rk{font-weight:800;font-size:12.5px;color:#9a9a9a;font-variant-numeric:tabular-nums;text-align:left}
.win-derby .dby-row.you .dby-rk{color:var(--amber-d,#d6920f)}
.win-derby .dby-av{width:26px;height:26px;border-radius:50%;overflow:hidden;background:var(--card,#fff);border:1.5px solid var(--line)}
.win-derby .dby-av svg{width:23px;height:23px;display:block}
.win-derby .dby-nm{min-width:0;text-align:left;font-weight:700;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.win-derby .dby-row.you .dby-nm{color:var(--amber-d,#d6920f)}
.win-derby .dby-sc{font-weight:800;font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
.win-derby .dby-sub{margin-top:5px;font-size:11.5px;color:var(--muted)}
/* rankUpBeat owns the FLIP + chime here; its row ±chip is redundant with the header pop → hide it */
.win-derby .dby-row .rank-delta,.win-derby .dby-row .lb-delta{display:none}
[dir=rtl] .win-derby .dby-rk,[dir=rtl] .win-derby .dby-nm{text-align:right}
/* B5: the dust the water-jar just banked lands here (tap → the daily Dust board); motes fly in, then the chip
   fades up. §8.3: it keeps its pot anchoring + flight but adopts the one pill grammar (padding/gap/size). */
.jar-dust-chip{display:inline-flex;align-items:center;gap:5px;margin:6px auto 0;padding:5px 11px;border:0;border-radius:999px;cursor:pointer;
  font-weight:800;font-size:12px;color:#8a5a00;background:linear-gradient(180deg,#fff2cf,#ffe6a6);box-shadow:inset 0 0 0 1px rgba(230,163,46,.28);
  opacity:0;transform:translateY(4px);transition:opacity .34s ease,transform .34s cubic-bezier(.2,1.05,.4,1)}
.jar-dust-chip.in{opacity:1;transform:translateY(0)}
@media (prefers-reduced-motion:reduce){.jar-dust-chip{transition:none}}
body.calm .jar-dust-chip{transition:none}
[dir=rtl] .stat-tile .stat-dest{right:auto;left:5px}
/* calm + reduced-motion: everything final + static (no count-up roll, no meter fill, no reveal) — info never motion-gated */
@media (prefers-reduced-motion:reduce){ .stat-tile,.win-derby,.win-derby .dby-pop.on,.win-chip .mck{animation:none !important} .wp-fill{transition:none !important} .win-derby .dby-pop{opacity:1} }
body.calm .stat-tile,body.calm .win-derby,body.calm .win-derby .dby-pop.on,body.calm .win-chip .mck{animation:none !important}
body.calm .win-derby .dby-pop{opacity:1}
body.calm .wp-fill{transition:none !important}
/* starPop calm compliance: add the missing body.calm guard (was reduced-motion-only) so calm users get static stars */
body.calm .popup .stars .star.on{animation:none}
/* P1.5 v2 (Fixed Stage, owner Option B 2026-07-14): the "pow" backdrop covers the WHOLE card — a soft green
   ray fan slowly turning (70s, 0 flash) behind a full-card glow that breathes (6s opacity, ≤.18 alpha).
   Present from mount (t0), aria-hidden, canonical green only. body.calm / reduced-motion pin both layers
   static — information never rides the motion, the pattern itself stays. Rays are a FIXED 680px square
   (covers the 330×584 card's diagonal) so weak compositors rasterize one bounded tile, never a %-of-page. */
.popup.win-card{position:relative}
/* clip-path (not just overflow:hidden): ::before/::after are ANIMATED, so the WebView promotes them to
   their own compositing layers, which escape a parent's rounded overflow clip and paint square corners
   over the scrim. clip-path binds composited descendants; keep both — radius for the non-composited path. */
.win-pow{position:absolute;inset:0;min-height:100%;border-radius:24px;clip-path:inset(0 round 24px);pointer-events:none;z-index:0;overflow:hidden;opacity:0;transition:opacity .5s ease-out}
.win-pow::before{content:'';position:absolute;left:50%;top:50%;width:680px;height:680px;margin:-340px 0 0 -340px;background:repeating-conic-gradient(from 0deg at 50% 50%,rgba(95,174,95,.09) 0deg 6deg,transparent 6deg 15deg);animation:powSpin 70s linear infinite}
.win-pow::after{content:'';position:absolute;inset:-14%;background:radial-gradient(circle at 50% 32%,rgba(95,174,95,.18),rgba(95,174,95,.05) 58%,rgba(95,174,95,0) 80%);animation:powBreathe 6s ease-in-out infinite}
@keyframes powSpin{to{transform:rotate(360deg)}}
@keyframes powBreathe{0%,100%{opacity:.6}50%{opacity:1}}
.win-pow.show{opacity:1}
.popup.win-card > *:not(.win-pow){position:relative;z-index:1}
@media (prefers-reduced-motion:reduce){.win-pow{transition:none}.win-pow::before,.win-pow::after{animation:none}}
body.calm .win-pow{transition:none}
body.calm .win-pow::before,body.calm .win-pow::after{animation:none}
body.lowfx .win-pow::before,body.lowfx .win-pow::after{animation:none}
/* Heading juice: "Safe with you!" pops in, then crossfades to a paw (the Critters-tab symbol) whose SHAPE fills up bottom→top = the destination, iconic */
.win-head{position:relative;height:52px;margin:2px 0 6px;display:flex;align-items:center;justify-content:center}
.win-head .wh-text{position:absolute;font-family:Fredoka,system-ui,-apple-system,sans-serif;font-size:25px;font-weight:800;color:var(--green-d,#3f8a47);white-space:nowrap;animation:whPop .5s cubic-bezier(.2,1.4,.4,1) backwards;transition:opacity .38s ease,transform .38s ease}
.win-head .wh-icon{position:absolute;width:46px;height:46px;opacity:0;transform:scale(.85);transition:opacity .38s ease,transform .45s cubic-bezier(.2,1.4,.4,1)}
.win-head.to-icon .wh-text{opacity:0;transform:scale(.9)}
.win-head.to-icon .wh-icon{opacity:1;transform:scale(1)}
.fill-icon{width:46px;height:46px;display:block}
.fill-icon .paw-ghost{fill:rgba(95,174,95,.2)}
.fill-icon .fill-rect{fill:var(--green,#5fae5f);transform:scaleY(0);transform-box:fill-box;transform-origin:center bottom;transition:transform .7s cubic-bezier(.3,1,.4,1)}
.win-head.filled .fill-rect{transform:scaleY(1)}
@keyframes whPop{0%{transform:scale(.85);opacity:0}60%{transform:scale(1.05)}100%{transform:scale(1);opacity:1}}
/* calm + reduced-motion: no pop, no crossfade, no fill — the text simply stays (legible, static) */
@media (prefers-reduced-motion:reduce){ .win-head .wh-text{animation:none} .win-head .wh-icon,.fill-icon .fill-rect{transition:none} }
body.calm .win-head .wh-text{animation:none}
body.calm .win-head .wh-icon,body.calm .fill-icon .fill-rect{transition:none}
.place-strip.refhook{cursor:pointer;color:#3F6A1F;background:#EDF6E0;border:1px solid #cfe6b3;border-radius:11px;padding:7px 10px;margin:2px 0 12px}
.place-strip.refhook:active{transform:scale(.98)}
@media (prefers-reduced-motion:reduce){.place-strip.refhook:active{transform:none}}
/* ===== §3 win-info bottom sheet (tap-for-meaning) + F4 progression bits (§9.4: info-dots removed) ===== */
/* every carrier is a keyboard-focusable button; keep the calm look, add a clear focus ring */
[data-info]{cursor:pointer}
[data-info]:focus-visible{outline:2px solid var(--amber,#f2b134);outline-offset:2px;border-radius:12px}
/* F4/§8.3: the +N XP delta pill on the rxp row — shape/tint come from .win-chip.sm.wc-green; .rxp-gain only
   carries the fade-in (visible from mount at calm). The next-unlock badge moved OFF the row (the sheet names it). */
.rxp-gain{opacity:0;transition:opacity .34s ease}
.rxp-gain.in{opacity:1}
@media (prefers-reduced-motion:reduce){.rxp-gain{transition:none;opacity:1}}
body.calm .rxp-gain{transition:none;opacity:1}
/* the transparent scrim (closes + swallows the tap) + the viewport-anchored bottom sheet (never reflows the card) */
.win-info-scrim{position:fixed;inset:0;z-index:70;background:transparent}
.win-info{position:fixed;left:50%;bottom:max(12px,env(safe-area-inset-bottom));transform:translateX(-50%);z-index:71;
  width:min(330px,calc(100% - 16px));background:var(--card,#fff);border:1px solid var(--line);border-radius:20px;
  padding:16px 16px 14px;box-shadow:0 16px 44px rgba(0,0,0,.28);text-align:center;animation:wiIn .22s cubic-bezier(.2,.9,.3,1)}
@keyframes wiIn{from{transform:translateX(-50%) translateY(14px);opacity:0}to{transform:translateX(-50%) translateY(0);opacity:1}}
.win-info.rebump{animation:wiBump .28s cubic-bezier(.2,1.05,.4,1)}
@keyframes wiBump{0%{transform:translateX(-50%) scale(.96)}60%{transform:translateX(-50%) scale(1.04)}100%{transform:translateX(-50%) scale(1)}}
.win-info .wi-x{position:absolute;top:8px;right:10px;border:0;background:none;font-size:16px;line-height:1;color:var(--muted);cursor:pointer;padding:4px}
[dir=rtl] .win-info .wi-x{right:auto;left:10px}
.win-info .wi-ico{font-size:30px;line-height:1;color:var(--green-d,#3f8a47)}
.win-info .wi-title{font-weight:800;font-size:17px;margin:4px 0 2px;color:var(--ink)}
.win-info .wi-line{margin:2px 0;font-size:13.5px;font-weight:600;color:var(--muted);line-height:1.4}
.win-info .wi-meter{margin:8px auto 2px;font-weight:800;font-size:14px;color:var(--ink);font-variant-numeric:tabular-nums}
.win-info .wi-nu{margin:6px auto 0;font-size:12px;font-weight:700;color:var(--muted);display:flex;gap:5px;justify-content:center;flex-wrap:wrap;align-items:center}
.win-info .wi-nu .ti{font-size:13px}
.win-info .wi-act{margin:12px auto 0;display:inline-flex;align-items:center;justify-content:center;gap:6px;border:0;border-radius:12px;
  padding:9px 16px;font-weight:800;font-size:14px;color:#fff;background:var(--green,#5fae5f);cursor:pointer}
@media (prefers-reduced-motion:reduce){.win-info,.win-info.rebump{animation:none}}
body.calm .win-info,body.calm .win-info.rebump{animation:none}
.popup-actions{display:flex;flex-direction:column;gap:9px}
/* ===== Fixed Stage win card (owner Option B, 2026-07-14) — the FRAME owns its geometry. Six slots in a
   fixed order (header · hero · earned · progress · moment · actions), each with a reserved height; content
   changes INSIDE a slot (cross-fade / ghost placeholders), so the card border and every slot edge sit in
   the same place on every single level.
   Height is a MIN, not a fixed cap: 584px is the resting size (keeps the frame consistent on the common
   level), but the card GROWS to fit when the hero (tall "New friends!" discover banner) and a rich moment
   slot both run large on the same clear — a fixed height forced the flex-grow hero to shrink and CLIP the
   critter portraits + verdict (the "half-loaded card" bug). Capped at the overlay cell; overflow-y:auto is
   the last-resort safety on a genuinely tiny viewport so the Next CTA can never end up clipped off-screen. */
/* FIXED FRAME, SCROLLING MIDDLE BAND. The card used to be the scroll container itself, so on any
   viewport under ~730px (375x667 = 63px of overflow, 360x640 = 90px) the head and then the hero
   portrait slid up under the card's own 24px rounded, clipping edge — the "avatar cut off" report.
   Now head / hero / CTA are fixed and only .wc-scroll moves, so the hero and the primary action can
   never leave the viewport regardless of how tall the celebration content gets. */
.popup.win-card.wc2{min-height:min(584px,100%);max-height:100%;display:flex;flex-direction:column;overflow:hidden;overscroll-behavior:contain;padding:14px 16px 16px}
.wc-scroll{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch}
.wc-head{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:28px}
.wc-lvl{display:inline-flex;align-items:center;gap:5px;min-width:0;background:#f4f2e6;color:var(--muted);border-radius:999px;padding:3px 10px;font-size:12px;font-weight:800;white-space:nowrap;overflow:hidden}
.wc-lvl .ti{font-size:13px;flex:0 0 auto}
.wc-lvl .wc-sep{opacity:.55}
.wc2 .win-wallet{position:static;flex:0 0 auto}   /* the wallet gets a real slot — never absolute over content, never mirrored by a stale dir */
/* flex:1 0 auto — the hero GROWS to fill the resting frame (celebration) but never SHRINKS below its own
   content. Shrinking is what collapsed the abs-positioned paw heading onto the verdict on content-rich
   clears; now the card grows (min-height above) instead of the hero clipping. */
/* overflow:clip + a clip-margin instead of overflow:hidden — the discover-banner portrait's 4px ring
   sits exactly on this box's edge (.wc-hero .discover-banner zeroes its margin) and discBloom's
   overshooting easing pushes it to ~1.05, so a hard hidden shaved the ring on EVERY device, with or
   without scrolling. This is the always-present half of the "avatar is cut off" report. */
.wc-hero{flex:1 1 auto;min-height:128px;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:clip;overflow-clip-margin:14px}
.wc-hero .win-critters{margin:0 0 2px}
.wc-hero .stars{margin:4px 0}
/* flex:0 0 auto reserves the heading's own box (its base height:52px, or 40px on short screens) so the
   absolutely-positioned paw can't be squeezed to ~8px and overflow down onto the verdict line. */
.wc-hero .win-head{margin:0;flex:0 0 auto}
.wc-hero .win-sub{flex:none;margin:2px 8px 0;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
.wc-hero .discover-banner{margin:0;padding-top:6px}
.wc-earn{flex:0 0 auto;min-height:72px}
.wc2 .wc-earn .win-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:7px;margin:0 auto;max-width:302px;flex-wrap:initial}
.wc2 .stat-tile{animation:none;max-width:none}   /* the frame enters as ONE piece — no per-tile "row by row" reveal; the count-ups carry the beat */
.stat-tile.ghost{opacity:.35;box-shadow:none;border-style:dashed;pointer-events:none}
.stat-tile.ghost .stat-n{color:var(--muted)}
.wc-prog{flex:0 0 auto;min-height:66px;margin-top:8px}
/* first-steps levels (idx≤3) render an EMPTY progress slot (progHtml is gated off) — collapse it so its
   reserved band doesn't sit as a hollow rays-only void AND so the freed height flows to the flex-grow hero,
   which otherwise clips the taller "New friends!" discover banner (portraits + verdict cut off). */
.wc-prog:empty{display:none}
.wc-prog .win-prog{margin:0 auto;width:100%}
.wc-prog .reward-row{margin:4px 0 6px}
/* the MOMENT slot: every "sometimes" surface (level-up · rank ladder · Baby-Rescue offer · unlock · drops)
   takes turns here — one at a time, cross-fade, never stacking. Dots = manual nav (info never motion-gated). */
.wc-moment{flex:0 0 auto;position:relative;min-height:108px;margin-top:8px;border-radius:14px;background:rgba(122,160,106,.07);display:grid;align-items:stretch}
.wc-moment .mo{grid-area:1/1;min-width:0;opacity:0;visibility:hidden;transition:opacity .38s ease;display:flex;flex-direction:column;justify-content:center;gap:4px;padding:8px 10px}
.wc-moment .mo.on{opacity:1;visibility:visible}
.wc-moment .win-derby{margin:0;animation:none}
.wc-moment .win-derby .dby-rows{gap:2px}
.wc-moment .win-derby .dby-row{height:28px}
.wc-moment .lvlup-banner{margin-bottom:2px}
.wc-moment .tail-strip{margin:0;background:none;box-shadow:none;padding:4px 2px}
.wc-moment .win-chips{margin:0 auto}
.wc-moment .unlock-banner{margin:0 0 4px}
.wc-moment .mo-rw{margin:0;justify-content:center}
.mo-passed{font-size:11.5px;font-weight:700;color:#3d8b3d;opacity:0;transition:opacity .3s ease;display:flex;gap:4px;justify-content:center}
.mo-ph{font-size:11px;font-weight:700;color:#8a5a00;opacity:0;transition:opacity .3s ease;display:flex;gap:4px;justify-content:center;align-items:center}
.mo-ph .ti{font-size:12px}
.mo-passed.in,.mo-ph.in{opacity:1}
.mo-rank-big{display:flex;align-items:center;justify-content:center;gap:8px;font-weight:800;font-size:18px;color:var(--ink)}
.mo-rank-big .ti{font-size:20px;color:var(--amber)}
.mo-dots{position:absolute;top:5px;right:7px;display:flex;gap:2px;z-index:2}
.mo-dots button{width:16px;height:16px;border-radius:50%;padding:0;display:grid;place-items:center;cursor:pointer}
.mo-dots button::after{content:'';width:6px;height:6px;border-radius:50%;background:rgba(63,138,71,.3)}
.mo-dots button.on::after{background:var(--green-d,#3f8a47)}
.mo-dots button:focus-visible{outline:2px solid var(--amber);outline-offset:-2px}
[dir=rtl] .mo-dots{right:auto;left:7px}
.wc-cta{flex:0 0 auto;margin-top:10px}
.wc-cta .popup-actions{gap:8px}
.wc-cta .cta-row{display:flex;gap:8px}
.wc-cta .cta-row .btn{flex:1 1 0;margin:0}
/* short screens: shrink the hero + moment; the card scrolls only if even the shrunk layout can't fit */
@media (max-height:600px){
  .popup.win-card.wc2{padding:10px 14px 12px}
  .wc-hero{min-height:106px}
  .wc-hero .win-critters .wc{width:38px;height:38px}
  .popup.win-card.wc2 .stars .star{width:32px;height:32px}
  .wc-hero .win-head{height:40px}
  .wc-hero .win-head .wh-icon,.wc-hero .win-head .fill-icon{width:36px;height:36px}   /* fit the paw inside the 40px head so it can't touch the verdict */
  .wc-hero .win-head .wh-text{font-size:21px}
  .wc-hero .win-sub{font-size:12px}
  .wc-moment{min-height:92px}
  .wc-earn{min-height:64px}
  .wc2 .stat-tile{padding:6px 4px 5px}
}
/* calm/reduced-motion: moment cross-fades become instant swaps (manual dots still navigate) */
@media (prefers-reduced-motion:reduce){.wc-moment .mo{transition:none}.mo-passed,.mo-ph{transition:none}}
body.calm .wc-moment .mo{transition:none}
body.calm .mo-passed,body.calm .mo-ph{transition:none}

/* ---------- Toast ---------- */
#toast{position:absolute;left:50%;bottom:78px;transform:translateX(-50%);z-index:60;display:flex;flex-direction:column;gap:6px;align-items:center;pointer-events:none}
.toast-item{background:rgba(40,50,35,.92);color:#fff;font-weight:600;font-size:14px;padding:9px 16px;border-radius:999px;box-shadow:0 4px 12px rgba(0,0,0,.25);animation:tin .3s}
@keyframes tin{from{transform:translateY(10px);opacity:0}to{transform:translateY(0);opacity:1}}

/* ---------- sliding tab transitions ----------
   The new screen glides in by nav direction (forward = from the right, back = from the left).
   Calm: one-shot .28s, ~26px impulse (<12% of viewport), gentle fade; ui.js skips it under the
   calm feel / reduced-motion (mul<=0) and the media query hard-disables it as a backstop. */
#screen.slide-fwd{animation:scrSlideFwd .28s cubic-bezier(.22,1,.36,1)}
#screen.slide-back{animation:scrSlideBack .28s cubic-bezier(.22,1,.36,1)}
@keyframes scrSlideFwd{from{transform:translateX(26px);opacity:.35}to{transform:translateX(0);opacity:1}}
@keyframes scrSlideBack{from{transform:translateX(-26px);opacity:.35}to{transform:translateX(0);opacity:1}}
@media (prefers-reduced-motion:reduce){#screen.slide-fwd,#screen.slide-back{animation:none}}

/* ---------- iris scene transition (menu <-> in-level) ----------
   A black overlay (z 108, in the free 60–140 gap above toast/coach, below FTUE) whose
   transparent hole grows from the centre. JS (CE.juice.iris) drives --ir 150%→0 (cover) then
   0→150% (reveal) with rAF — no @keyframes, no scale, no flash, so it is inherently calm and
   never mounts under calm/reduced-motion (gated in juice.js). The masked area still blocks
   input, locking the board during the swap. */
/* No will-change here: `mask` is not a Blink direct-compositing reason, so declaring it
   promotes nothing and only adds a hint the compositor cannot act on. (Removing it is
   hygiene, not a VRAM saving — the mask animation's real cost is the per-frame re-raster.) */
.iris{position:absolute;inset:0;z-index:108;background:var(--iris-bg);
  -webkit-mask-image:radial-gradient(circle at 50% 50%, transparent var(--ir,0%), #000 calc(var(--ir,0%) + .8%));
  mask-image:radial-gradient(circle at 50% 50%, transparent var(--ir,0%), #000 calc(var(--ir,0%) + .8%));}

/* ---------- in-level chrome: the menu retracts so the board reads as its own world ----------
   Hiding the top HUD + bottom nav reclaims their flex space, letting #screen(flex:1) fill the
   shell — the level feels like a separate part of the game, not a tab. The swap happens behind
   the iris (or instantly under calm), so the layout change is never seen mid-flight. */
body.in-game #hud,
body.in-game #nav{display:none}
/* inset the play content so it sits INSIDE the screen-framing pipe (top run clears the header,
   bottom run clears the tools — the timer number lives on that bottom run) */
/* F5B: in-game screen is a flex column — header (auto) · pz-stage (flex:1, centers the board) · pz-dock (auto).
   Bottom pad clears the fixed TIME BONUS pod (~66px + safe-area). Horizontal pad is the shared cell-math token. */
body.in-game #screen{display:flex;flex-direction:column;padding:42px var(--scene-pad-x) calc(88px + env(safe-area-inset-bottom))}
.pz-stage{flex:1 1 auto;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:0;width:100%}
.pz-dock{flex:0 0 auto;display:flex;flex-direction:column;align-items:center;width:100%}

/* ---------- Play-button attract ----------
   The center Play FAB earns the eye instead of a floating pill: a soft green "ping" ring
   breathes out of its safe area and two small chevrons nod down toward it — a calm, always-on
   "tap here". Pure chrome (never touches state); only renders on the menu (hidden in-game) and
   stands down entirely in Calm / reduced-motion. The ring sits BEHIND the circle (z below). */
.fab-circle{position:relative;z-index:2}
/* attract ping — concentric with the 54px docked circle (top:0 = over the circle, which is the fab's first child) */
.fab-attract{position:absolute;left:50%;top:0;width:54px;height:54px;transform:translateX(-50%);
  pointer-events:none;z-index:1}
.fab-ping{position:absolute;inset:0;border-radius:50%;border:2px solid var(--green);
  opacity:0;animation:fabPing 2.2s ease-out infinite}
@keyframes fabPing{0%{transform:scale(.82);opacity:.45}70%{opacity:0}100%{transform:scale(1.5);opacity:0}}
body.calm .fab-attract,body.in-game .fab-attract{display:none}
@media (prefers-reduced-motion:reduce){.fab-attract{display:none}}

/* ---------- walls + juice ---------- */
.slot.wall{background:#c4b59a;box-shadow:inset 0 -4px 0 rgba(90,70,40,.18),inset 0 0 0 1px rgba(90,70,40,.12)}
.slot.wall svg{position:absolute;inset:16%;opacity:.9}
.critter.spawn{animation:spawnIn .42s cubic-bezier(.3,1.4,.5,1) backwards}
@keyframes spawnIn{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}

/* "eager run" — each critter hops toward the way it's pointed (its exit dir).
   Runs on the inner <svg> so it never fights the critter's spawn/exit/pulse transforms. */
/* Critters stay STILL on their signpost by default — the continuous "run" hop read as too busy.
   The only motion now is the occasional ~2s idle beat below. (run-* classes are kept because they
   encode the facing direction; their hop animation is intentionally disabled.) */
.critter[class*="run-"] svg{animation:none}

/* ── idle "living scene": every 5–10s ONE critter shows a mood FACE for ~2s ──
   The critter stays perfectly still in its tile (no body travel / no go-and-return). The
   expression is carried entirely by the face swap (heart-eyes, wink, star-eyes, …) in
   theme.js + its species voice; ui.js restores the neutral face when the beat ends. */
.slot.blocker-flash{animation:blkFlash .55s}
.slot.tap-ring::after{content:'';position:absolute;inset:0;border-radius:inherit;pointer-events:none;animation:tapRing .5s ease-out}  /* a clean green pulse from the tapped cell as a critter runs home — layers under the spark burst for a juicier rescue (box-shadow only, calm ceiling) */
@keyframes tapRing{0%{box-shadow:0 0 0 0 rgba(120,190,106,.5)}100%{box-shadow:0 0 0 12px rgba(120,190,106,0)}}
body.calm .slot.tap-ring::after{animation:none}
@media (prefers-reduced-motion:reduce){.slot.tap-ring::after{animation:none}}
.critter.blocker-flash svg{animation:blkWobble .55s}
@keyframes blkFlash{0%{box-shadow:0 0 0 0 rgba(242,177,52,0)}45%{box-shadow:0 0 0 4px rgba(242,177,52,.5)}100%{box-shadow:0 0 0 0 rgba(242,177,52,0)}}
@keyframes blkWobble{0%,100%{transform:rotate(0)}40%{transform:rotate(-4deg)}70%{transform:rotate(3deg)}}
.spark{position:absolute;left:50%;top:45%;width:8px;height:8px;border-radius:50%;background:#ffd76b;pointer-events:none;z-index:6}
/* ── juice: reward motes + HUD count-up + species discovery ── */
.reward-mote{position:fixed;width:11px;height:11px;border-radius:50%;pointer-events:none;z-index:200;box-shadow:0 0 6px rgba(242,177,52,.55)}
.hud-pulse{animation:hudPulse .42s ease}
@keyframes hudPulse{0%,100%{transform:scale(1)}45%{transform:scale(1.18)}}
.discover-banner{display:flex;flex-direction:column;align-items:center;gap:6px;margin:2px 0 12px;animation:discBloom .5s cubic-bezier(.2,1.5,.4,1)}
.discover-banner .disc-portrait{width:64px;height:64px;border-radius:50%;background:radial-gradient(circle,#fff7e0,#ffe6a8);padding:6px;box-shadow:0 0 0 4px rgba(242,177,52,.25),0 4px 12px rgba(0,0,0,.12)}
.discover-banner .disc-portrait svg{width:100%;height:100%}
.discover-banner .disc-label{font-weight:700;color:var(--green-d,#3f8a47);font-size:15px;text-align:center;line-height:1.3}
@keyframes discBloom{from{transform:scale(.4);opacity:0}to{transform:scale(1);opacity:1}}
/* win-popup unlock beat — fires regardless of which CTA the player picks (calm bloom, reuses discBloom) */
.unlock-banner{display:flex;align-items:center;justify-content:center;gap:7px;margin:2px 0 12px;padding:7px 15px;border-radius:999px;background:linear-gradient(180deg,#eaf7e3,#d6eec8);color:var(--green-d,#3f8a47);font-weight:800;font-size:15px;animation:discBloom .5s cubic-bezier(.2,1.5,.4,1)}
.unlock-banner i{font-size:18px}
/* ── juice: full reduced-motion coverage (fade/shrink in place, no fling) ── */
@media (prefers-reduced-motion:reduce){
  .critter{transition:opacity .2s !important}
  .critter.exit-U,.critter.exit-D,.critter.exit-L,.critter.exit-R{transform:scale(.6) !important;opacity:0}
  .critter.shake,.critter.spawn,.board.win-zoom,.discover-banner,.unlock-banner,.hud-pulse,.win-critters .wc,.board.wv-wave .slot,.board.wv-wave .slot::after{animation:none !important}
  .float-coin{animation:fupRM .5s ease forwards}
}
@keyframes fupRM{from{opacity:1}to{opacity:0;transform:translateY(-12px)}}
@keyframes sparkFly{from{transform:translate(-50%,-50%) scale(1);opacity:1}to{opacity:0}}
/* P2: escalating combo pop — ⚡×N, anchored ABOVE the rail (never over the board). NUMBER is the true count
   (font-size set inline, 17→28px); INTENSITY is the purple→amber --heat wash (0 at ×2 → amber at ×8). Entrance
   peaks at scale 1.06 (calm max) then settles. Always pointer-events:none. direction:ltr keeps the badge stable in RTL. */
.combo-pop{position:absolute;left:50%;top:-4px;transform:translateX(-50%);pointer-events:none;direction:ltr;display:inline-flex;align-items:center;gap:3px;background:var(--c-combo);background:linear-gradient(rgba(242,177,52,var(--heat,0)),rgba(242,177,52,var(--heat,0))),var(--c-combo);color:#fff;font-weight:800;font-size:16px;line-height:1;padding:3px 12px;border-radius:999px;box-shadow:var(--shadow);z-index:9;white-space:nowrap;animation:comboPop .42s cubic-bezier(.2,1.5,.4,1)}
.combo-pop .ti{font-size:.82em;opacity:.95}
@keyframes comboPop{0%{transform:translateX(-50%) scale(.5);opacity:0}70%{transform:translateX(-50%) scale(1.06);opacity:1}100%{transform:translateX(-50%) scale(1);opacity:1}}
@media (prefers-reduced-motion:reduce){.combo-pop{animation:none}}
body.calm .combo-pop{animation:none}

/* ===================================================================================
   P9 game-scene juice pack (RELEASE_REVIEW_PLAN §9). Every beat obeys the CALM ceiling:
   scale ≤1.06 · translate impulse ≤12% · one rise+decay (no oscillation) · particles ≤8 ·
   no >3Hz flash · no pure-white/saturated-red full-cell flash. All are decoration only.
   =================================================================================== */

/* R1 (piece pop / contrast discipline): every board skin is fundamentally LIGHT (playmat + ≤.17-opacity
   type tints), so a colored critter body reads at only ~1.4–3.5:1 against it — the dark P.k OUTLINE (8–13:1)
   + this drop-shadow carry the pop. In-game we deepen the contact shadow + add a faint 1px dark rim so pale
   critters (lemon / golden cub) lift off any skin. lowfx (2-class specificity) still wins → filter:none. */
body.in-game .critter svg{filter:drop-shadow(0 2px 2px rgba(40,55,30,.34))}   /* ONE shadow pass (slightly deepened to keep pale critters lifted) — the second 1px-rim drop-shadow doubled the per-critter blur passes on every repaint for a near-invisible gain */

/* R3 blocked lean — handled by CE.juice._leanArt (WAAPI on the SVG); no keyframe needed here. */

/* R4 escalating praise ladder — one warm word on the combo rail at ×3/×5/×8. Legibility tier: the WORD
   shows even at calm (only the entrance motion is dropped). Sits above the ⚡×N combo pop. */
.praise-pop{position:absolute;left:50%;top:-22px;transform:translateX(-50%);pointer-events:none;direction:ltr;white-space:nowrap;z-index:10;
  font-weight:800;font-size:15px;color:var(--amber-d,#d6920f);text-shadow:0 1px 3px rgba(255,255,255,.75),0 1px 2px rgba(0,0,0,.12);animation:praisePop .5s cubic-bezier(.2,1.4,.4,1)}
@keyframes praisePop{0%{opacity:0;transform:translateX(-50%) translateY(6px) scale(.82)}60%{opacity:1;transform:translateX(-50%) translateY(0) scale(1.04)}100%{opacity:1;transform:translateX(-50%) translateY(0) scale(1)}}
@media (prefers-reduced-motion:reduce){.praise-pop{animation:none}}
body.calm .praise-pop{animation:none}

/* R5 last-heart tension — the remaining pip rises+decays once (scale ≤1.06). No red, no shake. */
.mistakes .tension-pulse{animation:heartTension .6s ease}
@keyframes heartTension{0%,100%{transform:scale(1)}45%{transform:scale(1.06)}}
@media (prefers-reduced-motion:reduce){.mistakes .tension-pulse{animation:none}}

/* R9 distinct tool signatures (~300ms each) — prefs()-gated in JS (calm → the class is never added). */
.critter.sig-spin::after{content:'';position:absolute;inset:-2px;border-radius:50%;pointer-events:none;z-index:4;
  border:2px solid transparent;border-top-color:var(--green,#5fae5f);border-right-color:var(--green,#5fae5f);animation:sigSpin .34s ease-out}
@keyframes sigSpin{0%{transform:rotate(0);opacity:.9}100%{transform:rotate(300deg);opacity:0}}
.critter.sig-pick svg{animation:sigPick .34s ease}
@keyframes sigPick{0%{transform:scale(1)}42%{transform:scale(1.06)}100%{transform:scale(1)}}
.critter.sig-key svg{animation:sigKeyTurn .34s ease}
@keyframes sigKeyTurn{0%{transform:rotate(0)}45%{transform:rotate(11deg)}100%{transform:rotate(0)}}
.critter.sig-key::after{content:'';position:absolute;inset:0;border-radius:12px;pointer-events:none;z-index:4;overflow:hidden;
  background:linear-gradient(115deg,transparent 38%,rgba(255,255,255,.7) 50%,transparent 62%);animation:sigGlint .34s ease-out}
@keyframes sigGlint{0%{transform:translateX(-110%);opacity:0}30%{opacity:1}100%{transform:translateX(110%);opacity:0}}
.critter.sig-scout::after{content:'';position:absolute;inset:0;border-radius:50%;pointer-events:none;z-index:4;animation:sigScout .36s ease-out}
@keyframes sigScout{0%{box-shadow:0 0 0 0 rgba(95,174,95,.6)}100%{box-shadow:0 0 0 12px rgba(95,174,95,0)}}
@media (prefers-reduced-motion:reduce){.critter.sig-spin::after,.critter.sig-pick svg,.critter.sig-key svg,.critter.sig-key::after,.critter.sig-scout::after{animation:none}}

/* I1 warm run-trail (combo ≥4) — a 200ms warm opacity wake behind the exiting critter, fading toward its
   travel axis. Opacity only (no particles); sits behind the art (z-index:-1 inside the critter's own context). */
.critter.warm-trail::after{content:'';position:absolute;inset:0;border-radius:12px;pointer-events:none;z-index:-1;
  background:radial-gradient(circle,rgba(242,177,52,.42),transparent 70%);animation:warmTrail .2s ease-out}
.critter.warm-trail.exit-R::after{background:linear-gradient(to left,rgba(242,177,52,.5),transparent 72%)}
.critter.warm-trail.exit-L::after{background:linear-gradient(to right,rgba(242,177,52,.5),transparent 72%)}
.critter.warm-trail.exit-U::after{background:linear-gradient(to bottom,rgba(242,177,52,.5),transparent 72%)}
.critter.warm-trail.exit-D::after{background:linear-gradient(to top,rgba(242,177,52,.5),transparent 72%)}
@keyframes warmTrail{0%{opacity:.5}100%{opacity:0}}
@media (prefers-reduced-motion:reduce){.critter.warm-trail::after{animation:none;opacity:0}}

/* I2 board settle-in — tiles fade-drop (≤8px) staggered from centre, once per stage (JS-gated; --sd per tile). */
.slot.tile-settle{animation:tileSettle .16s ease both;animation-delay:var(--sd,0ms)}
@keyframes tileSettle{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
@media (prefers-reduced-motion:reduce){.slot.tile-settle{animation:none}}

/* I3 goodbye wave — the last critter's ART waves (rotate) inside EXIT_MS while the cell dashes home. */
.critter svg.goodbye-wave{transform-origin:50% 80%;animation:goodbyeWave .25s ease}
@keyframes goodbyeWave{0%{transform:rotate(0)}30%{transform:rotate(-9deg)}62%{transform:rotate(7deg)}100%{transform:rotate(0)}}
@media (prefers-reduced-motion:reduce){.critter svg.goodbye-wave{animation:none}}

/* I4 friends watching — two rescued-species friends idle-hop at the finale board edge (JS gates calm/lowfx). */
.board-watcher{position:absolute;bottom:2px;width:30px;height:30px;z-index:1;pointer-events:none;opacity:.92;animation:watcherHop 6s ease-in-out infinite}
.board-watcher svg{width:100%;height:100%;display:block}
.board-watcher.watch-left{inset-inline-start:-7px}
.board-watcher.watch-right{inset-inline-end:-7px}
@keyframes watcherHop{0%,84%,100%{transform:translateY(0)}90%{transform:translateY(-6px)}95%{transform:translateY(0)}}
@media (prefers-reduced-motion:reduce){.board-watcher{animation:none}}
body.calm .board-watcher,body.lowfx .board-watcher{display:none}

/* I5 pot anticipation — the TIME-BONUS gauge tips 3° in the last fifth of the window (static; composes with the centering translate). */
.pipe-time.par-near{transform:translateX(-50%) rotate(3deg);transition:transform .4s ease}

/* I8 lane-clear ripple — a soft light sweep across the now-clear lane when a cage opens (one rise+decay). */
.lane-ripple{position:absolute;inset:0;border-radius:inherit;pointer-events:none;z-index:3;opacity:0;
  background:radial-gradient(circle,rgba(255,240,190,.55),rgba(255,240,190,.16) 68%,transparent);animation:laneRipple .35s ease-out both}
@keyframes laneRipple{0%{opacity:0}42%{opacity:.6}100%{opacity:0}}
@media (prefers-reduced-motion:reduce){.lane-ripple{display:none}}

/* I9 flawless sheen — a static rainbow film on the courier jar after a zero-mistake stage (screen-blended, subtle). */
.tpot.sheen .tp-jar::after{content:'';position:absolute;left:6%;right:6%;top:14%;bottom:4%;pointer-events:none;z-index:3;
  border-radius:44% 44% 50% 50%/38% 38% 62% 62%;mix-blend-mode:screen;
  background:linear-gradient(125deg,rgba(255,120,170,.16),rgba(255,215,90,.16) 38%,rgba(120,225,160,.16) 66%,rgba(130,175,245,.18))}

/* combo "fast-next-action" charge bar — reverse countdown + tightening heartbeat.
   Obeys the JUICE.md calm ceiling: pulse scale <=1.06, soft brightness pulse (not a luminance strobe),
   green->amber warmth capped at Min(8), break is a silent fade. */
/* height + margin are CONSTANT so the rail never reflows the board; only opacity/slide animate on appear */
.combo-rail{position:relative;display:flex;align-items:center;gap:8px;width:100%;max-width:430px;height:18px;margin:0 auto 8px;opacity:0;transform:translateY(-3px);transition:opacity .25s ease,transform .25s ease;pointer-events:none}
.combo-rail.on{opacity:1;transform:translateY(0)}
.combo-bar{position:relative;flex:1;height:9px;border-radius:999px;background:rgba(70,90,55,.14);box-shadow:inset 0 0 0 1px rgba(70,90,55,.06);overflow:hidden;transition:box-shadow .3s}
.combo-rail.on .combo-bar{box-shadow:inset 0 0 0 1px rgba(70,90,55,.06),0 0 calc(2px + var(--warm,0)*9px) rgba(242,177,52,calc(.12 + var(--warm,0)*.42))}
/* Drained via transform:scaleX from comboTick(), never width — width is a LAYOUT property, so a
   width transition costs layout+paint every frame for the whole 0.8-2.0s window and re-arms on each
   rescue. transform-origin pins the drain to the left edge; .combo-bar's overflow:hidden + pill
   radius clip the shape, so nothing about the look changes. */
.combo-fill{position:absolute;inset:0 auto 0 0;width:100%;border-radius:999px;background:linear-gradient(90deg,var(--green),#7fc97f);transform-origin:left center;will-change:transform}
.combo-fill::after{content:'';position:absolute;inset:0;border-radius:999px;background:linear-gradient(90deg,var(--amber),#ffcf5a);opacity:var(--warm,0);transition:opacity .3s}
/* P2/D3: combo identity = purple --c-combo + ti-bolt (matches the win-card combo tile). Dark-plum text keeps ≥4.5:1 on the lilac. */
.combo-chip{flex-shrink:0;display:inline-flex;align-items:center;gap:3px;direction:ltr;background:var(--c-combo);color:#2b2352;font-weight:800;font-size:12px;line-height:1;padding:3px 9px;border-radius:999px;box-shadow:var(--shadow)}
.combo-chip .ti{font-size:13px}
/* P2: live combo-SCORE readout (the sess.score points climbing on the rail — the win-card combo number, live). */
.combo-score{flex-shrink:0;direction:ltr;font-weight:800;font-size:11px;line-height:1;color:var(--c-combo);font-variant-numeric:tabular-nums;letter-spacing:.02em;min-width:0}
.combo-score:empty{display:none}
.combo-rail.beating .combo-chip{animation:comboBeat var(--beat,.8s) ease-in-out infinite}
.combo-rail.beating .combo-fill{animation:comboGlow var(--beat,.8s) ease-in-out infinite}
@keyframes comboBeat{0%{transform:scale(1)}9%{transform:scale(1.06)}20%{transform:scale(1)}30%{transform:scale(1.035)}42%{transform:scale(1)}100%{transform:scale(1)}}
@keyframes comboGlow{0%,100%{filter:brightness(1)}12%{filter:brightness(1.13)}34%{filter:brightness(1.06)}}
@media (prefers-reduced-motion:reduce){.combo-rail.beating .combo-chip,.combo-rail.beating .combo-fill{animation:none}}
/* Magic-Dust timer — a no-fail liquid "pipe" hugging the board (left·bottom·right, open top).
   Drains as the level runs; finishing with time left earns a leaderboard bonus — empty is never a
   loss (JUICE.md loss-aversion ban). Calm / reduced-motion: stepwise 1Hz drain, no wave shimmer. */
/* SIMPLE board: a soft calm card (the busy photo playmat squeezed the tiles). Small padding → bigger tiles.
   The meadow photo stays available as an opt-in cosmetic set (--cos-playmat). */
/* F1B: the card hugs the board (fit-content) so a smaller board gets a smaller centered card, not a wide
   card with a small board floating in it. Horizontal padding uses the shared --board-pad-x (cell-math source). */
.board-wrap.has-pipe{position:relative;display:block;width:fit-content;max-width:100%;margin:6px auto 12px;padding:12px var(--board-pad-x,10px) 14px;
  background:var(--cos-playmat, linear-gradient(170deg,rgba(255,255,255,.5),rgba(238,245,228,.62)));border-radius:22px;
  box-shadow:inset 0 0 0 1px rgba(120,140,100,.10)}
.board-wrap.has-pipe .board{position:relative;z-index:2;margin:0 auto}
.pipe-frame{position:fixed;inset:0;pointer-events:none;z-index:30;overflow:visible;contain:paint}   /* contain:paint scopes the drain's repaint to this layer instead of letting a viewport-sized fixed element dirty the page under it */
.pipe-timer{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}
.pipe-timer path{fill:none;stroke-linecap:round;stroke-linejoin:round}   /* viewBox is now in PX (buildPipe) → uniform, connected drain */
.pipe-casing{stroke:rgba(70,90,55,.14);stroke-width:17;stroke-linecap:round}
/* The glow is its own flat stroke, NOT a filter on the liquid. A drop-shadow on .pipe-liquid meant a
   blur pass re-run every time stroke-dashoffset moved — geometry the compositor cannot touch, on a
   fixed viewport-sized layer, continuously from the first tap until the score floors. Widening a
   translucent stroke behind the liquid reads the same and rasters flat, so it now runs at full
   fidelity on every device instead of being switched off on the low tier. */
.pipe-glow{stroke:rgba(70,182,201,.34);stroke-width:19;stroke-linecap:round;stroke-dasharray:1000 1000;stroke-dashoffset:0}
.pipe-timer.rs-empty .pipe-glow{opacity:.22}
.pipe-liquid{stroke:#46b6c9;stroke-width:13;stroke-linecap:round;stroke-dasharray:1000 1000;stroke-dashoffset:0;animation:pipeWave 2.6s ease-in-out 1}   /* JUICE.md §1: one rise+decay on mount, NOT an oscillating/repeating glow */
.pipe-timer.rs-empty .pipe-liquid{opacity:.22;animation:none}
/* the countdown lives in a FAT bulged section of the bottom pipe run (a water-filled gauge pod) */
.pipe-time{position:absolute;left:50%;bottom:calc(10px + env(safe-area-inset-bottom));transform:translateX(-50%);pointer-events:none;z-index:2;
  display:flex;flex-direction:column;align-items:center;gap:0;padding:4px 26px 6px;min-width:92px;
  background:linear-gradient(#ecfbff,#c9ecf4);border:5px solid #3fb2c6;border-radius:34px;
  box-shadow:0 3px 10px rgba(30,80,95,.22),inset 0 2px 5px rgba(255,255,255,.78),inset 0 -4px 7px rgba(63,178,198,.30)}
.pipe-time-lbl{font-weight:800;font-size:9px;line-height:1.4;letter-spacing:1.6px;color:#2b8298;text-transform:uppercase;opacity:.92}
.pipe-time-val{font-weight:900;font-size:38px;line-height:1;color:#0e4d60;       /* high-contrast fill + white inner stroke + blue outer stroke */
  -webkit-text-stroke:3px #fff;paint-order:stroke fill;
  text-shadow:-1.5px -1.5px 0 #1f8fb3,1.5px -1.5px 0 #1f8fb3,-1.5px 1.5px 0 #1f8fb3,1.5px 1.5px 0 #1f8fb3,0 3px 4px rgba(20,60,75,.32);
  font-variant-numeric:tabular-nums;letter-spacing:1px}
/* D1 professional timer: a quiet readout that just drops in place (no per-digit motion; the gentle
   countdown motion is the liquid drain). The game font's digits are PROPORTIONAL and it has no tabular
   figures, so we lock each digit into a fixed-width cell — every digit keeps a fixed column, no jitter. */
.pipe-time-val{display:inline-block;width:2.1em;text-align:right;letter-spacing:0;white-space:nowrap}
.pipe-time-val .ptd{display:inline-block;width:.7em;text-align:center}
.pipe-stamp{animation:pipeStamp .42s cubic-bezier(.2,1.2,.4,1)}   /* keyframes keep translateX(-50%) so the gauge stays centred */
@keyframes pipeStamp{0%{transform:translateX(-50%) scale(1.06);opacity:.55}60%{opacity:1}100%{transform:translateX(-50%) scale(1)}}
@media (prefers-reduced-motion:reduce){.pipe-stamp{animation:none}}
.pipe-time.rs-empty{border-color:#9cc2cb;background:linear-gradient(#f1f6f7,#dde9ec)}
.pipe-time.rs-empty .pipe-time-val{color:#5f8b97;text-shadow:0 1px 2px rgba(0,0,0,.15)}
/* napping critter: a slow opacity "breathing" (opacity leaves transform free for the pose tilt) */
/* the daily-rank status BUTTON on the time row (tap → opens the daily time board) */
.rank-btn{display:inline-flex;align-items:center;gap:6px;margin-top:5px;padding:5px 8px 5px 10px;border:none;border-radius:11px;cursor:pointer;background:linear-gradient(#fff6df,#ffedc2);box-shadow:0 1px 4px rgba(150,110,20,.18),inset 0 0 0 1.5px #f0d79a;font:800 12.5px/1 "Fredoka",sans-serif;color:#8a6a1e}
.rank-btn:active{transform:scale(.97)}
.rank-btn .rank-lbl{display:inline-flex;align-items:center;gap:4px}
.rank-btn .rank-lbl i{color:#e6b422}
.rank-btn .rank-n{font-size:15px;font-weight:900;color:#5a4a1e;font-variant-numeric:tabular-nums}
.rank-btn .rank-delta{display:inline-flex;align-items:center;gap:2px;font-size:11px;font-weight:800;color:#8a9088}
.rank-btn.up .rank-delta{color:#2f9e52}.rank-btn.down .rank-delta{color:#c67a3a}
.rank-btn .rank-delta i{font-size:12px}
.rank-btn .rank-go{margin-left:2px;color:#c2a86a;font-size:14px}
/* TIME-JAR COURIER — a glass jar hops in on a stage clear (squash landing), its copper lid pops open,
   the stage's remaining time sloshes in (gulps + badge pops + sparkle burst), the lid slams with a
   wiggle and it zips off carrying the bank; it returns docked on the win card. Pure decoration —
   the time is banked before it runs. Calm/reduced-motion-gated off. */
.tpot{position:fixed;left:50%;bottom:24%;width:136px;height:150px;margin-left:-68px;z-index:59;transform:translateY(calc(24vh + 110%));transform-origin:50% 100%}   /* parked FULLY below the viewport on any screen (24vh gap + own height) — must match tpHopIn's 0% */
/* D2: stage-loss stakes row — the banked time-jar + the critters it holds, shown as what a Continue keeps */
.loss-stakes{display:flex;align-items:center;justify-content:center;gap:14px;margin:6px auto 12px;max-width:320px}
.loss-stakes .ls-side{text-align:left;min-width:0}
.loss-stakes .ls-cap{font-size:12.5px;font-weight:700;color:var(--muted);margin-bottom:6px}
.loss-stakes .ls-critters{display:flex;flex-wrap:wrap;gap:4px}
.loss-stakes .ls-cr{width:30px;height:30px;border-radius:50%;overflow:hidden;background:var(--card-2,#f3f1e8);border:2px solid var(--line);display:grid;place-items:center}
.loss-stakes .ls-cr svg{width:26px;height:26px;display:block}
[dir=rtl] .loss-stakes .ls-side{text-align:right}
.tpot-dim{position:fixed;inset:0;z-index:58;background:rgba(46,74,52,.28);-webkit-backdrop-filter:blur(1.5px);backdrop-filter:blur(1.5px);opacity:0;transition:opacity .3s}
.tpot-dim.on{opacity:1}
.tpot .tp-glow{position:absolute;left:50%;top:55%;width:190px;height:190px;margin:-95px 0 0 -95px;border-radius:50%;background:radial-gradient(circle,rgba(140,230,245,.65),rgba(140,230,245,0) 62%);opacity:0;transition:opacity .45s;pointer-events:none}
.tpot.lit .tp-glow{opacity:1}
.tpot .tp-jar{position:relative;width:100%;height:100%;transform-origin:50% 96%;filter:drop-shadow(0 10px 12px rgba(20,70,85,.3))}
.tpot .tp-svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
.tpot .tp-sand{transition:transform .42s cubic-bezier(.3,1.2,.4,1)}   /* tracks the per-drop pour steps tightly */
.tpot .tp-surf{transform-box:fill-box;transform-origin:center}
.tpot .tp-surf.slosh{animation:tpSlosh .55s ease-out}
.tpot .tp-surf.splash{animation:tpSplash .34s cubic-bezier(.3,1.3,.5,1)}   /* crown bulge as a drop lands */
.tpot .tp-rip{transform-box:fill-box;transform-origin:center;opacity:0}
.tpot .tp-rip.rip{animation:tpRip .5s ease-out}                            /* ring spreads across the surface on impact */
/* a falling time-droplet (WAAPI-arced from the gauge into the mouth) */
.tp-drop{position:fixed;width:13px;height:16px;margin:0;z-index:60;pointer-events:none;border-radius:50%;
  background:radial-gradient(120% 95% at 36% 26%,#e6fbff,#5fcadd 52%,#2f9db4);box-shadow:0 0 7px rgba(95,202,221,.7),inset 0 -2px 3px rgba(20,90,110,.35)}
.tpot .tp-bub{animation:tpBub 2.6s ease-in-out infinite}
.tpot .tp-bub.b2{animation-delay:.6s}.tpot .tp-bub.b3{animation-delay:1.3s}
.tpot .tp-lidg{transform-origin:60px 27px}
.tpot.open .tp-lidg{animation:tpLidO .4s cubic-bezier(.3,1.4,.5,1) forwards,tpLidFloat 1.5s ease-in-out .42s infinite alternate}
.tpot.shut .tp-lidg{animation:tpLidC .3s cubic-bezier(.4,0,.6,1.4) forwards}
.tpot .tp-num-wrap{position:absolute;left:50%;bottom:26px;transform:translateX(-50%);display:inline-flex;gap:5px;align-items:center;background:rgba(10,74,90,.85);color:#fff;border-radius:999px;padding:4px 13px;font:900 21px/1 "Fredoka",sans-serif;text-shadow:0 2px 3px rgba(5,40,50,.4);white-space:nowrap;z-index:2}
.tpot .tp-num-wrap i{font-size:16px;color:#9fe6f2}
.tpot .tp-num-wrap.pop{animation:tpNumPop .3s cubic-bezier(.3,1.5,.5,1)}
.tpot .tp-num-wrap.punch{animation:tpNumPunch .42s cubic-bezier(.3,1.7,.5,1)}   /* PHASE 4 · the full-jar number PUNCH (bigger than the per-drop pop) */
.tpot .tp-spark{position:absolute;left:50%;top:24px;width:7px;height:7px;margin-left:-3px;border-radius:50%;background:#c8f2fa;box-shadow:0 0 8px #7ddeee;opacity:0;pointer-events:none}
.tpot.burst .tp-spark.s1{animation:tpSpark1 .65s ease-out .02s}
.tpot.burst .tp-spark.s2{animation:tpSpark2 .7s ease-out .12s}
.tpot.burst .tp-spark.s3{animation:tpSpark3 .6s ease-out .22s}
/* PHASE 1 · ARRIVE — twin dust puffs kick out sideways as the jar slams down */
.tpot .tp-dust{position:absolute;left:50%;bottom:8%;width:30px;height:11px;border-radius:50%;background:radial-gradient(circle,rgba(206,228,236,.6),rgba(206,228,236,0) 70%);opacity:0;pointer-events:none}
.tpot.landed .tp-dust.d1{animation:tpDustL .5s ease-out}
.tpot.landed .tp-dust.d2{animation:tpDustR .5s ease-out}
/* PHASE 2/5 · UNCORK pop-ring at the mouth (reused, pushed DOWNWARD, for the SEAL slam) */
.tpot .tp-cork{position:absolute;left:50%;top:22px;width:30px;height:12px;margin-left:-15px;border-radius:50%;border:2.5px solid rgba(206,246,255,.9);opacity:0;pointer-events:none}
.tpot.uncork .tp-cork{animation:tpCork .5s ease-out}
.tpot.seal .tp-cork{animation:tpSeal .4s ease-out}
/* PHASE 4 · SETTLE shockwave — a ring blasts outward from the full jar */
.tpot .tp-shock{position:absolute;left:50%;top:52%;width:88px;height:88px;margin:-44px 0 0 -44px;border-radius:50%;border:3px solid rgba(150,235,250,.7);opacity:0;pointer-events:none}
.tpot.full .tp-shock,.tpot.dock.burst .tp-shock{animation:tpShock .6s cubic-bezier(.2,.7,.3,1)}
.tpot.full .tp-jar{animation:tpFull .5s cubic-bezier(.3,1.4,.5,1)}   /* satisfied bounce (scale ≤1.05, calm-ceiling-safe) */
/* PHASE 6 · DEPART speed-lines — motion streaks trail the jar as it rockets off */
.tpot .tp-speed{position:absolute;left:-34px;top:38%;width:44px;height:34px;opacity:0;pointer-events:none;background:repeating-linear-gradient(0deg,transparent 0 5px,rgba(185,236,250,.55) 5px 7px);-webkit-mask-image:linear-gradient(90deg,transparent,#000);mask-image:linear-gradient(90deg,transparent,#000)}
.tpot.off .tp-speed{animation:tpSpeed .42s ease-out}
.tpot.in{animation:tpHopIn .52s cubic-bezier(.34,1.2,.5,1) forwards}
.tpot.off{animation:tpZipOff .46s cubic-bezier(.5,.05,.85,.4) forwards}
.tpot .tp-jar.gulp{animation:tpGulp .3s ease-out}
.tpot .tp-jar.wig{animation:tpWig .45s ease-in-out}
@keyframes tpHopIn{0%{transform:translateY(calc(24vh + 110%))}58%{transform:translateY(-5%)}74%{transform:translateY(2.5%) scale(1.07,.88)}88%{transform:translateY(-1.5%) scale(.98,1.03)}100%{transform:translateY(0) scale(1,1)}}
@keyframes tpZipOff{0%{transform:translateY(0)}28%{transform:translate(-10px,5px) scale(1.04,.9) rotate(-4deg)}100%{transform:translate(74vw,-26px) scale(1.12,.92) rotate(9deg)}}
@keyframes tpGulp{0%{transform:scale(1,1)}45%{transform:scale(1.05,.93)}100%{transform:scale(1,1)}}
@keyframes tpWig{0%{transform:rotate(0)}30%{transform:rotate(2.4deg)}62%{transform:rotate(-1.8deg)}100%{transform:rotate(0)}}
@keyframes tpLidO{0%{transform:translateY(0) rotate(0)}60%{transform:translateY(-31px) rotate(-11deg)}80%{transform:translateY(-21px) rotate(-7deg)}100%{transform:translateY(-25px) rotate(-9deg)}}
@keyframes tpLidFloat{0%{transform:translateY(-25px) rotate(-9deg)}100%{transform:translateY(-29px) rotate(-7deg)}}
@keyframes tpLidC{0%{transform:translateY(-25px) rotate(-9deg)}55%{transform:translateY(2.5px) rotate(0)}78%{transform:translateY(-2px)}100%{transform:translateY(0) rotate(0)}}
@keyframes tpNumPop{0%{transform:translateX(-50%) scale(1)}45%{transform:translateX(-50%) scale(1.22)}100%{transform:translateX(-50%) scale(1)}}
@keyframes tpNumPunch{0%{transform:translateX(-50%) scale(1)}40%{transform:translateX(-50%) scale(1.35)}70%{transform:translateX(-50%) scale(.96)}100%{transform:translateX(-50%) scale(1)}}
@keyframes tpDustL{0%{opacity:.55;transform:translateX(-50%) scale(.3)}100%{opacity:0;transform:translate(-150%,3px) scale(1.35)}}
@keyframes tpDustR{0%{opacity:.55;transform:translateX(-50%) scale(.3)}100%{opacity:0;transform:translate(50%,3px) scale(1.35)}}
@keyframes tpCork{0%{opacity:.9;transform:translateY(0) scale(.5)}100%{opacity:0;transform:translateY(-15px) scale(1.6)}}
@keyframes tpSeal{0%{opacity:.85;transform:translateY(-8px) scale(1.35)}100%{opacity:0;transform:translateY(3px) scale(.65)}}
@keyframes tpShock{0%{opacity:.75;transform:scale(.38)}100%{opacity:0;transform:scale(1.5)}}
@keyframes tpFull{0%{transform:scale(1,1)}30%{transform:scale(1.05,.95)}62%{transform:scale(.98,1.03)}100%{transform:scale(1,1)}}
@keyframes tpSpeed{0%{opacity:0;transform:translateX(24px) scaleX(.5)}35%{opacity:.6}100%{opacity:0;transform:translateX(-26px) scaleX(1.4)}}
@keyframes tpSlosh{0%{transform:translateY(0) scaleX(1)}30%{transform:translateY(-4px) scaleX(1.05)}60%{transform:translateY(2px) scaleX(.97)}100%{transform:translateY(0) scaleX(1)}}
@keyframes tpSplash{0%{transform:scaleY(1)}35%{transform:translateY(-2px) scaleY(1.85)}100%{transform:scaleY(1)}}
@keyframes tpRip{0%{transform:scale(.32);opacity:.85}100%{transform:scale(1.16);opacity:0}}
@keyframes tpBub{0%,100%{transform:translateY(0);opacity:.4}50%{transform:translateY(-7px);opacity:.7}}
@keyframes tpBob{0%,100%{transform:translateY(0)}50%{transform:translateY(-2.5px)}}
@keyframes tpSpark1{0%{opacity:0;transform:translate(0,0) scale(1)}18%{opacity:1}100%{opacity:0;transform:translate(-17px,-30px) scale(.4)}}
@keyframes tpSpark2{0%{opacity:0;transform:translate(0,0) scale(1)}18%{opacity:1}100%{opacity:0;transform:translate(2px,-38px) scale(.45)}}
@keyframes tpSpark3{0%{opacity:0;transform:translate(0,0) scale(1)}18%{opacity:1}100%{opacity:0;transform:translate(18px,-28px) scale(.4)}}
/* docked on the win card (inline, centered, drops in with a bounce) */
.tpot.dock{position:relative;left:auto;bottom:auto;width:104px;height:114px;margin:0 auto 4px;transform:none;opacity:0;z-index:auto}
.tpot.dock.in{animation:tpDrop .6s cubic-bezier(.34,1.25,.5,1) forwards}
.tpot.dock .tp-spark{top:20px}
.tpot.dock.settled .tp-jar{animation:tpBob 3.4s ease-in-out infinite}   /* alive-on-the-card idle bob (±2.5px) */
@keyframes tpDrop{0%{opacity:0;transform:translateY(-48px) scale(.9)}55%{opacity:1;transform:translateY(4px)}74%{transform:translateY(0) scale(1.06,.92)}100%{opacity:1;transform:translateY(0) scale(1,1)}}
@media (prefers-reduced-motion:reduce){
  .tpot,.tpot *,.tpot-dim,.tp-drop{animation:none!important;transition:none!important}
  .tpot.in{transform:translateY(0)}.tpot.dock{opacity:1}
  .tpot.open .tp-lidg{transform:translateY(-25px) rotate(-9deg)}
  .tpot.dock.settled .tp-jar{animation:none!important}
}
body.calm .tpot *,body.lowfx .tpot *{animation-duration:.01s!important;transition-duration:.01s!important}
body.calm .tpot .tp-bub,body.lowfx .tpot .tp-bub{animation:none!important}
body.calm .tpot.open .tp-lidg,body.lowfx .tpot.open .tp-lidg{animation:none!important;transform:translateY(-25px) rotate(-9deg)}   /* no .01s infinite float — a >3Hz flicker */
body.calm .tpot.dock.settled .tp-jar,body.lowfx .tpot.dock.settled .tp-jar{animation:none!important}   /* no .01s infinite bob */
/* stage intro — "Stage k/3 · 3·2·1·GO" overlay (calm: backdrop alpha .34<.6, pop scale 1.06) */
.stage-intro{position:fixed;inset:0;z-index:60;display:grid;place-items:center;background:rgba(58,90,55,.34);-webkit-backdrop-filter:blur(1.5px);backdrop-filter:blur(1.5px);animation:siIn .2s ease}
@keyframes siIn{from{opacity:0}to{opacity:1}}
.si-card{text-align:center;color:#fff;padding:0 24px}
.si-lvl{font-size:15px;font-weight:800;letter-spacing:1.4px;text-transform:uppercase;opacity:.9}
.si-stage{font-size:27px;font-weight:900;margin:2px 0 16px;text-shadow:0 2px 6px rgba(0,0,0,.3)}
.si-bar{width:210px;height:13px;border-radius:99px;background:rgba(255,255,255,.25);margin:0 auto 16px;overflow:hidden}
.si-fill{display:block;height:100%;width:0;background:linear-gradient(90deg,#46b6c9,#7fd0dc);border-radius:99px;animation:siFill 2.45s ease forwards}
@keyframes siFill{to{width:100%}}
.si-count{font-size:66px;font-weight:900;color:#fff;text-shadow:0 3px 12px rgba(0,0,0,.35);min-height:74px;line-height:74px}
.si-count.pop{animation:siPop .42s ease}
@keyframes siPop{0%{transform:scale(.62);opacity:.35}55%{transform:scale(1.06)}100%{transform:scale(1);opacity:1}}
@media (prefers-reduced-motion:reduce){.stage-intro{animation:none}.si-fill{animation:none;width:100%}.si-count.pop{animation:none}}
/* one type-identity banner — reused on the pack screen, the loading beat, the result card. Tinted per
   type via --ltb/--ltbl/--ltbk (the type PAL). The loading banner flies to the .ltype-chip on Go. */
.ltype-banner{display:flex;align-items:center;gap:10px;width:max-content;max-width:100%;margin:0 auto;padding:8px 16px 8px 10px;border-radius:999px;background:var(--ltbl,#e7f3d6);color:var(--ltbk,#2c4a20);box-shadow:0 2px 8px rgba(50,70,40,.14)}
.ltype-banner .ltb-ic{flex:0 0 auto;width:30px;height:30px;border-radius:50%;display:grid;place-items:center;background:var(--ltb,#8FBF52);color:#fff;box-shadow:0 1px 4px rgba(0,0,0,.16)}
.ltype-banner .ltb-ic i{font-size:18px}
.ltype-banner .ltb-tx{display:flex;flex-direction:column;text-align:left;line-height:1.2;min-width:0}
.ltype-banner .ltb-tx b{font-size:15px;font-weight:800}
.ltype-banner .ltb-cap{font-size:12px;opacity:.72;font-weight:600}
.ltb-pack{margin:0 auto 12px}.ltb-result{margin:2px auto 8px}
.si-card .ltype-banner{margin:2px auto 14px;background:rgba(255,255,255,.95);animation:ltbIn .34s cubic-bezier(.2,1.3,.4,1)}
@keyframes ltbIn{from{transform:scale(.82);opacity:0}to{transform:scale(1);opacity:1}}
.ltype-chip.chip-land{animation:chipLand .5s cubic-bezier(.2,1.5,.4,1)}
@keyframes chipLand{0%{transform:scale(.62)}55%{transform:scale(1.06)}100%{transform:scale(1)}}
@media (prefers-reduced-motion:reduce){.si-card .ltype-banner{animation:none}.ltype-chip.chip-land{animation:none}}
/* napping critter: a slow opacity "breathing" (opacity leaves transform free for the pose tilt) */
.critter.dozing{filter:saturate(.82) brightness(1.02);animation:critterDoze 3.4s ease-in-out infinite}
@keyframes critterDoze{0%,100%{opacity:1}50%{opacity:.9}}
.critter.dozing.doze-curl{transform:rotate(-3.5deg)}    /* nap-pose variety */
.critter.dozing.doze-lean{transform:rotate(3deg)}
.critter.drowsy{animation:critterDrowsy .44s ease}       /* fall-asleep settle */
.critter.waking{animation:critterWake .36s cubic-bezier(.2,1.4,.4,1)}  /* perk-up on wake (scale<=1.05, calm) */
@keyframes critterDrowsy{0%{transform:scale(1)}60%{transform:scale(.97) translateY(2px)}100%{transform:scale(1)}}
@keyframes critterWake{0%{transform:scale(.96)}55%{transform:scale(1.05)}100%{transform:scale(1)}}
body.tab-hidden .critter.dozing,body.tab-hidden .pipe-liquid{animation-play-state:paused}   /* battery: freeze ambient motion when backgrounded */
@media (prefers-reduced-motion:reduce){.critter.dozing,.critter.drowsy,.critter.waking{animation:none}}
body.calm .critter.dozing,body.lowfx .critter.dozing{animation:none}
@keyframes pipeWave{0%,100%{stroke:#46b6c9}50%{stroke:#58c2d3}}
/* procedural ambient leaves behind the board — calm theme juice (≤6, slow drift, low opacity) */
.lvl-foliage{position:absolute;inset:-10px;z-index:0;pointer-events:none;overflow:hidden;border-radius:22px}
.lvl-foliage .leaf{position:absolute;width:26px;height:26px;opacity:.22;color:#7fae5f;transform-origin:center;
  animation:leafDrift 11s ease-in-out infinite}
.lvl-foliage .leaf svg{width:100%;height:100%;display:block;fill:currentColor}
@keyframes leafDrift{0%,100%{translate:0 0}50%{translate:0 7px}}
body.lowfx .lvl-foliage,body.calm .lvl-foliage{display:none}
@media (prefers-reduced-motion:reduce){.lvl-foliage{display:none}}
@media (prefers-reduced-motion:reduce){.pipe-liquid{animation:none;filter:none}}

/* combo auto-booster — "Rainbow Rush" tool + chain banner */
/* classic boosters read as a distinct (warmer) system vs the green obstacle-tools */
.tool.boost i{color:#e0902f}
.tool.off{opacity:.42;filter:grayscale(.45)}
/* pre-level "Pack your bag" boost-pick cards */
.pack-wrap{text-align:center}
/* ── .swap — a label that CHANGES on tap must never change its own width ──────────────────────────
   Owner feedback (repeated): tapping a control resized the row around it. Cause is always the same —
   a nowrap, flex-shrink:0 state pill whose text grows ("Equip"→"Equipped", "Off"→"On"), stealing width
   from the flex:1 text beside it, which then reflows. Fixing it with a px min-width is a trap: the
   longest label is a different string in each of the 16 packs.
   Instead render BOTH states in ONE grid cell — the box always measures max(off,on) in whatever
   language is live, so toggling changes only which one is visible. Put `.on` on the state element
   (or any ancestor). Both labels stay real text nodes, so i18n translates each independently. */
.swap{display:inline-grid;align-items:center}
.swap>*{grid-area:1/1;white-space:nowrap}
.swap>.sw-on{visibility:hidden}
.swap>.sw-off{visibility:visible}
.on .swap>.sw-on,.on.swap>.sw-on{visibility:visible}
.on .swap>.sw-off,.on.swap>.sw-off{visibility:hidden}
.pack-cards{display:flex;flex-direction:column;gap:9px;margin:2px 0 4px;text-align:left}
.pack-card{display:flex;align-items:center;gap:11px;width:100%;background:#fff;border:1px solid var(--line);border-radius:16px;padding:10px 12px;box-shadow:var(--shadow);cursor:pointer}
.pack-card.equipped{box-shadow:0 0 0 2px var(--amber),var(--shadow);background:linear-gradient(120deg,#fff,#fef6e3)}
.pack-card:active{transform:scale(.99)}
.pc-ic{width:42px;height:42px;flex-shrink:0;border-radius:13px;background:#fdf3e0;display:grid;place-items:center}
.pc-ic i{font-size:23px;color:#e0902f}
.pc-tx{display:flex;flex-direction:column;line-height:1.25;min-width:0;flex:1}.pc-tx b{font-size:14px}.pc-tx .muted{font-size:11.5px}
.pc-state{flex-shrink:0;font-size:12px;font-weight:800;color:var(--muted);display:inline-flex;align-items:center;gap:4px;white-space:nowrap}
.pc-state>i{width:1em;text-align:center;flex-shrink:0}   /* ti-plus ↔ ti-check swap must not nudge the row either */
.pc-state.on{color:var(--green-d)}
.pc-state.buy{color:#2c5a8c;background:#eaf2fb;border:1px solid #c9def5;border-radius:999px;padding:4px 10px}
.pc-own{opacity:.7;font-weight:700}
.tool.booster{position:relative;overflow:hidden}
.tool.booster i{color:#9a8fe0}
.tool.booster::after{content:'';position:absolute;left:0;bottom:0;height:3px;width:calc(var(--chg,0)*100%);background:linear-gradient(90deg,#9a8fe0,#7fc97f);transition:width .25s}
.tool.booster.charged{box-shadow:0 0 0 2px #9a8fe0,var(--shadow);animation:boostReady 1.1s ease-in-out infinite}
.tool.booster.charged i,.tool.booster.charged .cnt{color:#7c5fae}
@keyframes boostReady{0%,100%{transform:scale(1)}50%{transform:scale(1.05)}}
.rainbow-banner{position:absolute;left:50%;top:8px;transform:translateX(-50%);z-index:8;font-weight:800;font-size:16px;padding:5px 16px;border-radius:999px;color:#fff;background:linear-gradient(90deg,#ED93B1,#F4C842,#8FBF52,#85B7EB,#AFA9EC);box-shadow:var(--shadow);animation:comboPop .5s cubic-bezier(.2,1.5,.4,1)}
@media (prefers-reduced-motion:reduce){.tool.booster.charged{animation:none}}
body.calm .tool.booster.charged{animation:none}
.board.win-zoom{animation:winZoom .5s ease}
@keyframes winZoom{0%,100%{transform:scale(1)}40%{transform:scale(1.04)}}
/* ── Sunrise Sweep: the level-end tile wave (TILE_WAVE_PLAN.md, driven by CE.juice.boardWave) ──
   A diagonal crest bottom-left → top-right over the still-mounted grid, one warm gleam per tile, then a
   single gloss band trailing the settled wave. Per-tile phase is the inline --wd the primitive sets and
   strips afterwards (the settleBoardIn technique). Calm ceiling: lift 11% < 12% impulse, 1.045 against the
   winZoom tail ≤ 1.06, no oscillation. Transform/opacity only — the tile shadow is PRE-RENDERED on ::after
   and only its opacity animates. ::before is taken by the type-presentation slot tint, hence ::after. */
.board.wv-wave .slot{animation:wvLift 560ms cubic-bezier(.4,.1,.3,1) var(--wd,0ms) both}
.board.wv-wave .slot::after{content:'';position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0;
  background:linear-gradient(160deg,rgba(255,250,225,.95),rgba(255,236,170,.4) 48%,transparent 78%);
  box-shadow:0 7px 10px rgba(40,60,30,.28);animation:wvGleam 560ms ease var(--wd,0ms) both}
.board.wv-wave .slot.void,.board.wv-wave .slot.void::after{animation:none}   /* off-platform cells keep their dead cell — they still consume a band index, they just never lift */
@keyframes wvLift{0%,100%{transform:none}38%{transform:translateY(-11%) scale(1.045)}72%{transform:translateY(1.5%) scale(.996)}}
@keyframes wvGleam{0%,100%{opacity:0}38%{opacity:1}74%{opacity:0}}
/* The gloss is a CLIPPING frame pinned to the board (.board is overflow:visible — an unclipped band
   sweeps out over the foliage and HUD), holding an over-tall band that does the travelling. Keeping the
   travel on an element the board's own width is what makes translateX(±100%) mean "one board wide": the
   earlier oversized -60% inset made 190% of travel ≈ 4 board widths, so the stripe crossed the tiles in
   ~140ms of its 650 and spent the rest — including its opacity peak — off the board entirely. */
.win-gloss{position:absolute;inset:0;pointer-events:none;z-index:5;overflow:hidden}
.win-gloss i{position:absolute;top:-30%;bottom:-30%;left:0;right:0;display:block;
  background:linear-gradient(105deg,transparent 40%,rgba(255,248,215,.78) 50%,rgba(255,244,200,.28) 56%,transparent 64%)}
body.lowfx .board.wv-wave .slot,body.lowfx .board.wv-wave .slot::after{animation:none}
.diff-row{display:flex;gap:8px;justify-content:center;margin:6px 0 2px}
.diff-chip{padding:7px 14px;border-radius:999px;border:1px solid var(--line);background:#fff;font-weight:600;font-size:14px}
.diff-chip.on{background:var(--green);color:#fff;border-color:var(--green)}

/* utility */
.center{text-align:center}
.mt8{margin-top:8px}.mt12{margin-top:12px}.mt16{margin-top:16px}
.muted{color:var(--muted);font-weight:500}
.big{font-size:18px;font-weight:700}

/* ---------- Today hub (audience-segmented retention) ---------- */
.today-head{margin-bottom:4px}
.seg-chip{display:inline-flex;align-items:center;gap:6px;background:var(--green-l);color:var(--green-d);font-weight:700;font-size:13px;padding:4px 11px;border-radius:999px}
.seg-chip i{font-size:16px}
.seg-prev{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 12px}
.segx{font-size:12px;font-weight:600;padding:5px 11px;border-radius:999px;border:1px solid var(--line);background:#fff;color:var(--muted)}
.segx.on{background:var(--ink);color:#fff;border-color:var(--ink)}
.tcard{margin-bottom:12px}
.tcard.spot{border:1.5px solid var(--green);box-shadow:0 8px 20px rgba(95,174,95,.20)}
.tc-ico{width:42px;height:42px;border-radius:13px;display:grid;place-items:center;font-size:22px;color:#fff;flex-shrink:0}
.tc-ico.green{background:var(--green)}.tc-ico.amber{background:var(--amber)}.tc-ico.flame{background:#ef7a3a}
.tc-ico.emj{background:var(--green-l)}   /* Events-hub module cards: emoji icon on a soft neutral pad */
.tc-ico.sky{background:#5aa9e8}.tc-ico.gold{background:#e6b234}.tc-ico.lilac{background:#9a8fe0}
.tc-ev{width:44px;height:44px;border-radius:13px;background:var(--green-l);display:grid;place-items:center;flex-shrink:0;padding:4px}
.tc-ev .av{width:100%;height:100%}
.qrow{display:flex;align-items:center;gap:9px;padding:7px 2px;font-weight:600;font-size:14px}
.qrow .ti{font-size:20px}
.av{display:inline-block;width:28px;height:28px;vertical-align:middle}
.av svg{width:100%;height:100%;display:block}

/* progress bars */
.prog{height:8px;border-radius:999px;background:rgba(70,90,55,.12);overflow:hidden;margin-top:7px}
.prog-fill{height:100%;border-radius:999px;background:linear-gradient(90deg,var(--green),#7fc97f);transition:width .4s}

/* login calendar */
.logrow{display:flex;gap:5px}
.logcell{flex:1;min-width:0;border-radius:11px;border:1px solid var(--line);background:#fff;padding:7px 3px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:2px;position:relative}
.logcell .d{font-size:11px;font-weight:700;color:var(--muted)}
.logcell .lr{font-size:9px;line-height:1.05;font-weight:600;color:var(--ink)}
.logcell .ti{position:absolute;top:3px;right:4px;font-size:12px;color:var(--green)}
.logcell.got{background:var(--green-l);border-color:transparent;opacity:.85}
.logcell.now{border-color:var(--amber);box-shadow:0 0 0 2px rgba(242,177,52,.35);background:#fffaf0}
.logcell.soon{opacity:.6}

/* missions */
.mrow{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--line)}
.mrow:last-child{border-bottom:none}
.mtxt{flex:1;min-width:0;font-weight:600;font-size:14px}
.mrw{display:flex;gap:6px;align-items:center}
.rw{display:inline-flex;align-items:center;gap:3px;font-size:13px;font-weight:700;color:var(--ink)}
.rw i{font-size:15px}

/* league (rows now render through the canonical .lbrow renderLBRow — the old .lstand/.lrow markup is retired) */
/* B7: daily board keeps time as a muted sub inside the value cell */
.lbrow .lb-n .lb-time{opacity:.6;font-weight:600;font-size:11px;margin-left:4px}
/* event-fill invite CTA + "enroll us both" toggle on the cohort card */
.ce-fill-cta{margin-top:8px}
.ce-arr{display:flex;align-items:center;gap:7px;margin-top:8px;font-size:12.5px;color:var(--muted);cursor:pointer}
.ce-arr input{width:15px;height:15px;accent-color:#5FA84F;flex:none}
/* winners' rewards strip — mirrors the canonical CE.Cohort.REWARD per tier */
.winstrip{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin:8px 0 2px}
.wschip{display:inline-flex;align-items:center;gap:3px;font-size:11px;font-weight:700;color:#6a6450;background:var(--card-2,#f3f1e8);border-radius:999px;padding:3px 9px}
.wschip .wsm{font-size:12px}

/* ===== renderLBRow — ONE canonical leaderboard row (cohort · league · referral-ready) =====
   --lb-nm is the NAME FLOOR (rule S4). The name track was a bare `1fr`, and `.lb-nm{min-width:0}`
   let that fr collapse to nothing, so the name was the only track with no guaranteed size — the
   one thing on the row that must survive was the first thing squeezed. minmax() makes the floor a
   promise the layout has to keep: the value track (`auto`) gives its pixels up first. */
.lbrow{--lb-rk:34px;--lb-av:40px;--lb-dl:30px;--lb-rw:40px;--lb-nm:104px;display:grid;grid-template-columns:var(--lb-rk) var(--lb-av) minmax(var(--lb-nm),1fr) auto var(--lb-dl) var(--lb-rw);align-items:center;gap:8px;height:50px;padding:0 6px;border-radius:11px;position:relative}
.lbrow + .lbrow{margin-top:2px}
.lbrow-wt{grid-template-columns:var(--lb-rk) var(--lb-av) minmax(var(--lb-nm),1fr) auto var(--lb-dl) minmax(64px,auto)}
.lbrow .lb-rk{width:32px;height:32px;border-radius:50%;display:grid;place-items:center;font-weight:800;font-size:14px;color:#9a9a9a;background:var(--card-2,#f3f1e8);font-variant-numeric:tabular-nums}
.lbrow.pod .lb-rk{color:#5a3d05;font-size:15px;box-shadow:inset 0 1px 1px rgba(255,255,255,.55),0 1px 3px rgba(0,0,0,.12)}
.lbrow.r1 .lb-rk{background:radial-gradient(circle at 35% 30%,#ffe08a,#f2b134)}
.lbrow.r2 .lb-rk{background:radial-gradient(circle at 35% 30%,#eef2f6,#c2ccd6)}
.lbrow.r3 .lb-rk{background:radial-gradient(circle at 35% 30%,#e7c39c,#cf9b6a)}
.lbrow .lb-av{width:38px;height:38px;border-radius:50%;display:grid;place-items:center;background:var(--card-2,#f3f1e8);border:2px solid var(--ring,var(--line));overflow:hidden}
.lbrow .lb-av .av,.lbrow .lb-av svg{width:34px;height:34px;display:block}
.lbrow .lb-nm{min-width:0;font-weight:700;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center;gap:4px}
.lbrow .lb-fr{color:#5FA84F;font-size:13px;flex:none}
.lbrow .lb-val{display:flex;flex-direction:column;align-items:flex-end;gap:3px;min-width:52px}
.lbrow .lb-n{font-weight:800;font-size:13px;font-variant-numeric:tabular-nums;line-height:1}
.lbrow .lb-bar{width:60px;height:6px;border-radius:4px;background:var(--card-2,#eee);overflow:hidden}
.lbrow .lb-bar i{display:block;height:100%;border-radius:4px;background:linear-gradient(90deg,var(--green,#5fae5f),var(--green-d,#3f8a47))}
.lbrow .lb-dl{font-weight:800;font-size:11.5px;display:flex;align-items:center;justify-content:center}
.lbrow .lb-dl.up{color:#57b26a}.lbrow .lb-dl.down{color:#9aa7b0}
.lbrow .lb-rw{display:grid;place-items:center}
.lbrow .lb-tail{display:flex;justify-content:flex-end;align-items:center}
.lbrow .lb-up{color:#57b26a;font-size:18px}.lbrow .lb-dn{color:#c1a24a;font-size:18px}
.lbrow.you{background:rgba(242,177,52,.14);box-shadow:inset 0 0 0 2px var(--amber,#f2b134)}
.lbrow.you .lb-nm{color:var(--amber-d,#d6920f)}
.lbrow.promo{background:rgba(95,174,95,.10)}
.lbrow.releg{background:rgba(232,96,122,.09)}
.lbrow.lb-friend{background:rgba(95,174,95,.08)}
.lbrow .lb-chest{vertical-align:-3px}
.lbrow .lb-delta{position:absolute;right:44px;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;gap:1px;font-size:11px;font-weight:800;padding:1px 6px;border-radius:999px;line-height:1.5;pointer-events:none}
.lbrow .lb-delta.up{color:#3d8b3d;background:rgba(95,174,95,.18)}
.lbrow .lb-delta.down{color:#b06a86;background:rgba(176,106,134,.14)}
.lbrow.moved{animation:lbMoved 1.1s ease-out 1}
@keyframes lbMoved{0%{background:rgba(242,177,52,.22)}100%{background:transparent}}
.lb-gap{text-align:center;color:#bcbcbc;font-weight:800;letter-spacing:3px;font-size:13px;padding:1px 0}
.lb-pin{display:flex;align-items:center;gap:7px;margin:5px 2px;padding:5px 9px;border-radius:9px;border:1px dashed var(--amber,#f2b134);background:repeating-linear-gradient(45deg,rgba(242,177,52,.12),rgba(242,177,52,.12) 8px,transparent 8px,transparent 16px);font-weight:800;font-size:11.5px;color:var(--amber-d,#d6920f)}
.lb-pin .lb-pin-f{display:grid;place-items:center;flex:none}
@media (prefers-reduced-motion:reduce){.lbrow.moved{animation:none}.lbrow .lb-delta{position:static;transform:none}}

/* ===== chest-open ceremony (settled-cohort claim) ===== */
.chest-stage{display:grid;place-items:center;height:130px;margin:2px 0 4px}
.chest-big{position:relative;width:120px;height:112px;cursor:pointer;filter:drop-shadow(0 8px 12px rgba(40,55,30,.22))}
.chest-big .chest-big-svg{position:relative;width:120px;height:112px}
.chest-big .cb-lid{position:absolute;left:0;top:6px;transform-origin:50% 100%;transition:transform .28s cubic-bezier(.3,.7,.3,1.2);z-index:2}
.chest-big .cb-body{position:absolute;left:0;bottom:0;z-index:1}
.chest-big.chest-pop{animation:chestPop .3s ease}
@keyframes chestPop{30%{transform:scale(.97)}62%{transform:scale(1.05)}100%{transform:scale(1)}}
/* PRE-OPEN anticipation (ceremony v2): two gentle wobbles, ~190ms each. Transform-only (rotate ≤±3°,
   scale ≤1.04 — CALM ceiling ≤1.06), single run (not infinite), zero luminance flash. Unscoped so the
   non-chest lanes (mailbox gift icon / tail chip) wobble too and degrade safely. */
.chest-wobble{animation:chestWobble .38s ease-in-out 1}
@keyframes chestWobble{0%{transform:rotate(0) scale(1)}22%{transform:rotate(3deg) scale(1.04)}50%{transform:rotate(0) scale(1)}72%{transform:rotate(-3deg) scale(1.04)}100%{transform:rotate(0) scale(1)}}
/* TIER LIGHT: opacity-only radial glow behind the chest (JS animates 0→.7→0 over ~600ms — one
   rise+decay, no flash). z-index:-1 sits it behind the art in the .chest-big filter stacking context. */
.chest-bloom{position:absolute;left:50%;top:50%;width:150%;height:150%;transform:translate(-50%,-50%);border-radius:50%;pointer-events:none;z-index:-1;opacity:0;background:radial-gradient(circle,var(--bc,#ffd76b) 0%,rgba(255,255,255,0) 66%)}
/* raster basket/nest sprite (theme.chestArt seam) — swaps closed→open at the reveal beat; sits above the bloom, replaces the SVG when the art file loads */
.chest-big .chest-raster{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
.chest-big.open .cb-lid{transform:rotate(-104deg)}
.chest-pay{display:flex;justify-content:center;gap:9px;margin:6px 0 2px;min-height:56px;flex-wrap:wrap}
/* SEQUENTIAL DOLE-OUT: chips render one at a time (JS timers, 140ms apart) — .in pops the just-doled
   chip (no --i stagger delay); .shown reveals instantly (skip / calm path). Base state is hidden. */
.chest-pay-chip{background:var(--card,#fffdf7);border:1px solid var(--line);border-radius:12px;padding:8px 11px;display:flex;flex-direction:column;align-items:center;gap:3px;min-width:56px;box-shadow:var(--shadow);opacity:0;transform:scale(.85)}
.chest-pay-chip.in{animation:cpIn .34s cubic-bezier(.2,.85,.25,1) forwards}
.chest-pay-chip.shown{opacity:1;transform:none}
.chest-pay-chip .cpv{font-size:22px}.chest-pay-chip .cpn{font-weight:800;font-size:13px}
@keyframes cpIn{100%{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){.chest-big.chest-pop,.chest-wobble{animation:none}.chest-big .cb-lid{transition:none}.chest-pay-chip{animation:none;opacity:1;transform:none}}
/* RESULTS card — a settled cohort's chest, breathing gently until you open it */
.tc-ico.tc-chest{background:transparent;display:grid;place-items:center;animation:tcChestBreathe 2.6s ease-in-out infinite}
.tc-ico.tc-chest .lb-chest{width:34px;height:34px;filter:drop-shadow(0 2px 5px rgba(40,55,30,.25))}
@keyframes tcChestBreathe{50%{transform:scale(1.04)}}
@media (prefers-reduced-motion:reduce){.tc-ico.tc-chest{animation:none}}

/* ===== P4: Rescuer-rank status badge (HUD) + status sheet ===== */
.hud-pill.status{padding-left:5px;padding-right:5px}
.hud-pill.status .status-ring{position:relative;width:24px;height:24px;border-radius:50%;display:grid;place-items:center;border:2px solid var(--tier,#6bbf59);color:var(--tier,#6bbf59);transition:border-color .5s ease,color .5s ease}
.hud-pill.status .status-ring i{font-size:14px;line-height:1}
.hud-pill.status .status-medals{position:absolute;bottom:-4px;right:-6px;min-width:14px;height:14px;padding:0 3px;border-radius:8px;background:var(--amber,#f2b134);color:#fff;font-weight:800;font-size:9px;line-height:1;display:grid;place-items:center;border:2px solid var(--card,#fffdf7);font-variant-numeric:tabular-nums}
.hud-pill.status.promote{animation:stPromote .7s cubic-bezier(.3,.85,.25,1)}
@keyframes stPromote{35%{transform:scale(1.06)}70%{transform:scale(.99)}100%{transform:scale(1)}}
.hud-pill.status .status-ring.cross{animation:stCross .6s ease}
@keyframes stCross{0%{filter:brightness(1.45)}100%{filter:brightness(1)}}
.hud-pill.status .status-medals.bump{animation:stBump .5s ease}
@keyframes stBump{40%{transform:scale(1.12)}100%{transform:scale(1)}}
.status-hero{text-align:center}
.status-hero-ring{width:64px;height:64px;border-radius:50%;display:inline-grid;place-items:center;border:3px solid var(--tier,#6bbf59);color:var(--tier,#6bbf59);margin:4px 0 0}
.status-hero-ring i{font-size:32px}
.status-sub{text-align:center;font-size:12.5px;margin:8px 0 12px}
.status-medals-row{display:flex;justify-content:center;gap:8px;flex-wrap:wrap}
.status-medals-row .sm-chip{background:var(--card-2,#f3f1e8);border:1px solid var(--line);border-radius:999px;padding:5px 12px;font-weight:800;font-size:13px;font-variant-numeric:tabular-nums}
@media (prefers-reduced-motion:reduce){.hud-pill.status.promote,.hud-pill.status .status-ring.cross,.hud-pill.status #statusMedals.bump{animation:none}}
/* narrow phones (≤400px): tighten HUD gaps/padding so the 7th (status) pill fits without clipping settings */
@media (max-width:400px){ #hud{gap:3px;padding-left:4px;padding-right:4px} #hud .hud-left,#hud .hud-right{gap:3px} #hud .hud-pill{padding-left:4px;padding-right:4px} }
/* live "Settles in …" countdown */
.bcd-line{font-size:12px;color:var(--muted);margin:6px 0 2px}
.bcd-line i{font-size:13px;vertical-align:-2px}
.bcd-line b{color:var(--green-d);font-variant-numeric:tabular-nums}
/* Magic Dust board — daily/weekly/monthly cadence tabs */
.md-tabs{display:flex;gap:6px;justify-content:center;margin:6px 0 4px}
.md-tab{border:1px solid rgba(120,120,120,.22);background:transparent;border-radius:999px;padding:4px 14px;font-weight:700;font-size:13px;color:#7a7a7a;cursor:pointer}
.md-tab.on{background:linear-gradient(90deg,#f6d36b,#ffe9a8);border-color:transparent;color:#6a4f12}
.md-sub{font-size:13px;margin:4px 0}
.md-climb{color:#3d8b3d;font-weight:700;font-size:13px;margin:4px 0}
/* ===== Event lifecycle card (.evcard) + shared popup hero (.ev-hero) — Gardenscapes-register list card.
   Calm: static background-image crop (no motion/particles/flash), 180deg vertical scrim = RTL-safe.
   Legibility (adversarial-panel R3): main scrim + a body backing plate + shadow-all-text keep white text
   >=4.5:1 at the TOP body line over the brightest banner crop (not only at the 100% stop). ===== */
.evcard{position:relative;overflow:hidden;min-height:118px;padding:0;border-radius:18px;
  display:flex;flex-direction:column;justify-content:flex-end;isolation:isolate}
/* E1 (tap card = open detail): pointer ONLY when the body tap actually opens something — a whole-card
   data-coh (cohort routing) or data-open (todayAction routing). A Join-state card reads static. */
.evcard[data-coh],.evcard[data-open]{cursor:pointer}
.ev-art{position:absolute;inset:0;z-index:0;
  /* layered: banner over the deep-night fallback gradient — a 404/gitignored asset degrades to the gradient */
  background-image:var(--ev-img,none),linear-gradient(180deg,#0a0e28 0%,#1b2350 60%,#2b326a 100%);
  background-size:cover,cover;background-position:var(--ev-pos,50% 60%),center;background-repeat:no-repeat}
.ev-scrim{position:absolute;inset:0;z-index:1;background:linear-gradient(180deg,
  rgba(8,10,24,0) 0%,rgba(8,10,24,.12) 34%,rgba(8,10,24,.60) 66%,rgba(8,10,24,.90) 100%)}
/* animated comet overlay on the 'before' starter — a transparent Codex FX layer that slowly drifts along
   the comet's travel axis + fades (the "simple animation, image quality" ask). CALM: transform/opacity only,
   ≤7% impulse, ~9s period (no flash); static under calm/reduced-motion. */
.ev-fx{position:absolute;inset:0;z-index:1;pointer-events:none;background-image:var(--ev-fx);
  background-size:cover;background-repeat:no-repeat;opacity:0;will-change:transform,opacity;
  animation:evComet 8.5s ease-in-out 1.2s infinite}
.evcard .ev-fx{background-position:var(--ev-pos,50% 60%)}
.ev-hero .ev-fx{background-position:var(--ev-hero-pos,50% 32%);animation-duration:9.5s}
@keyframes evComet{0%{opacity:0;transform:translate(3.5%,-2%)}15%{opacity:1}70%{opacity:1}100%{opacity:0;transform:translate(-3.5%,2%)}}
body.calm .ev-fx,body.lowfx .ev-fx{animation:none;opacity:.92;transform:none}
@media (prefers-reduced-motion:reduce){.ev-fx{animation:none;opacity:.92;transform:none}}
/* fx-sky (§6) — the alpha sky plate drifting VERY slowly across the popup hero, under the comet fx +
   scrim. CALM: transform/opacity only, ≤2.5% travel over 14s (no flash); static under calm/reduced. */
.ev-fx-sky{position:absolute;inset:-3% -4%;z-index:1;pointer-events:none;background-image:var(--ev-fx-sky);
  background-size:cover;background-repeat:no-repeat;background-position:var(--ev-hero-pos,50% 32%);
  opacity:.9;will-change:transform;animation:evSkyDrift 14s ease-in-out infinite alternate}
@keyframes evSkyDrift{0%{transform:translateX(-1.2%)}100%{transform:translateX(1.2%)}}
body.calm .ev-fx-sky,body.lowfx .ev-fx-sky{animation:none;transform:none}
@media (prefers-reduced-motion:reduce){.ev-fx-sky{animation:none;transform:none}}
.ev-body{position:relative;z-index:2;padding:16px 13px 12px;text-align:left;
  background:linear-gradient(180deg,rgba(8,10,24,0),rgba(8,10,24,.48))}
.ev-title{color:#fff;font-weight:800;font-size:16px;line-height:1.15;text-shadow:0 1px 2px rgba(0,0,0,.72),0 1px 10px rgba(0,0,0,.4)}
.ev-chips{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-top:7px}
.ev-chips .tag{font-size:11px;padding:3px 9px}
.ev-chips .ev-medal{font-size:14px;filter:drop-shadow(0 1px 2px rgba(0,0,0,.5))}
.ev-meta{color:#fff;font-size:12px;margin-top:7px;text-shadow:0 1px 2px rgba(0,0,0,.6)}
.ev-meta .hub-cd{font-weight:700;font-variant-numeric:tabular-nums}
.ev-claim{position:absolute;top:12px;right:12px;z-index:3;box-shadow:0 2px 8px rgba(0,0,0,.35)}
[dir="rtl"] .ev-claim{right:auto;left:12px}
.ev-chip-btn{border:0;cursor:pointer}
/* popup detail hero header — same plate, sky/comet crop; full-bleed inside .popup's 20px/22px padding */
.ev-hero{position:relative;overflow:hidden;margin:-22px -20px 12px;min-height:150px;border-radius:0 0 20px 20px;
  display:flex;flex-direction:column;justify-content:flex-end;isolation:isolate}
.ev-hero.ev-hero-sm{min-height:92px}
.ev-hero .ev-art{background-position:var(--ev-hero-pos,50% 32%),center}
.ev-hero .ev-body{padding:18px 18px 14px;background:linear-gradient(180deg,rgba(8,10,24,0),rgba(8,10,24,.60))}
.ev-hero .ev-title{font-size:20px}
/* compact stat row for the popup DURING state */
.ev-stats{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin:8px 0 2px}
.ev-stat{background:var(--green-l);color:var(--green-d);border-radius:999px;padding:4px 11px;font-size:12px;font-weight:700}
.ev-howto{text-align:left;font-size:13px;color:var(--muted,#6a6a6a);margin:8px 0;padding-left:2px}
/* controls hosted INSIDE an evcard (cohort/daily/event cards rolled onto the pilot pattern) —
   ghost buttons, the enroll-both checkbox and the progress track must stay legible over art */
.evcard .btn-ghost{color:#fff;border-color:rgba(255,255,255,.45);background:rgba(8,10,24,.30)}
.evcard .ce-arr{color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.6)}
.evcard .rw{color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.6)}   /* E5a reward chips stay legible over art */
/* E3: league podium rows hosted on the grove evcard — white ink over art (the .you name keeps its
   own amber rule, which is more specific and already reads on the dark plate) */
.evcard .lbrow{color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.6)}
.evcard .prog{background:rgba(255,255,255,.30)}
.evcard .ev-chips .tc-ev{box-shadow:0 1px 6px rgba(0,0,0,.35)}
.ev-howto li{margin:3px 0}
/* keep the hero full-bleed in lockstep with .popup's reduced padding on short viewports (styles.css:562) */
@media (max-height:560px){.ev-hero{margin:-14px -16px 12px}}
/* Town Gallery header — the same hero worn as a SCREEN banner (gallery skin): no popup negative
   margins, all-corner radius, back button overlaid top-start (inset-inline = RTL-safe) */
.gal-hero{position:relative}
.gal-hero .ev-hero{margin:0 0 10px;border-radius:20px}
.gal-hero .gal-back{position:absolute;top:10px;inset-inline-start:10px;z-index:3}
/* Critter Dash — race-track header + Dash Pot milestone track (calm: .5s eases, no flash/haptic) */
.race-track{position:relative;height:34px;margin:8px 0 2px;border-radius:999px;background:rgba(120,150,90,.12);box-shadow:inset 0 0 0 1px rgba(120,150,90,.14);overflow:visible}
.race-track .rt-fill{position:absolute;inset:0 auto 0 0;border-radius:999px;background:linear-gradient(90deg,#bfe39a,#9fcf72);transition:width .5s ease}
.race-track .rt-runner{position:absolute;top:50%;transform:translate(-50%,-50%);transition:left .5s cubic-bezier(.2,.8,.2,1)}
.race-track .rt-runner .av,.race-track .rt-runner svg{width:28px;height:28px;display:block}
.race-track .rt-flag{position:absolute;right:5px;top:50%;transform:translateY(-50%);font-size:16px}
/* B2: rival faces on the strip (Magic Dust daily) — score-proportional spots; a tiny startled hop
   (≤12% of the 22px face — calm impulse ceiling) as your runner passes. Runner rides above rivals. */
.race-track .rt-runner{z-index:2}
.race-track .rt-rival{position:absolute;top:50%;transform:translate(-50%,-50%);z-index:1;opacity:.92}
.race-track .rt-rival .av,.race-track .rt-rival svg{width:22px;height:22px;display:block}
.race-track .rt-rival.hop{animation:rtHop .42s cubic-bezier(.3,1.05,.4,1)}
@keyframes rtHop{0%{transform:translate(-50%,-50%)}40%{transform:translate(-50%,calc(-50% - 2.5px)) rotate(-4deg)}100%{transform:translate(-50%,-50%)}}
@media (prefers-reduced-motion:reduce){.race-track .rt-rival.hop{animation:none}}
body.calm .race-track .rt-rival.hop{animation:none}
.dash-pot{margin:8px 0;text-align:left}
.dp-bar{position:relative;height:10px;border-radius:999px;background:rgba(230,163,46,.12);box-shadow:inset 0 0 0 1px rgba(230,163,46,.12);margin-top:5px}
.dp-fill{position:absolute;inset:0 auto 0 0;border-radius:999px;background:linear-gradient(90deg,#f6d36b,#ffe9a8);transition:width .5s ease}
.dp-node{position:absolute;top:50%;width:8px;height:8px;border-radius:50%;background:#fff;box-shadow:0 0 0 2px rgba(180,150,60,.5);transform:translate(-50%,-50%)}
.dp-node.ready{box-shadow:0 0 0 2px #e6a32e,0 0 6px rgba(230,163,46,.6)}
.dp-node.done{background:#7cc06a;box-shadow:0 0 0 2px #5fae5f}
@media (prefers-reduced-motion:reduce){.race-track .rt-fill,.race-track .rt-runner,.dp-fill{transition:none}}

/* ---- Event race track (race.js CE.Race) — the shared competitive-event hero: sky header · up to 5
   lanes (name/avatar chip + runway + score-anchored mover) · checkered finish strip with reward-band
   chest pins. Structure + calm-gated motion only; palette/prop/sky/pin art all come from CE.theme.race.
   Pinned LTR like .board/.map-wrap; ALL motion is transform/opacity-only, gated by .race-calm + RM. ---- */
.race-shell{position:relative;border-radius:14px;overflow:hidden;margin:2px 0 10px;border:1px solid var(--line);background:var(--card-2,#f3f1e8)}
.race-sky{position:relative;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 12px;min-height:34px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.30)}
.race-goal{font-weight:800;font-size:12.5px;line-height:1.25;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.race-cd{flex:none;font-size:12px;font-weight:700;display:inline-flex;align-items:center;gap:4px;background:rgba(0,0,0,.20);border-radius:999px;padding:2px 9px}
.race-cd i{font-size:12px}
.race-cd b{font-variant-numeric:tabular-nums}
.race-grid{display:grid;grid-template-columns:1fr 30px;background:var(--card,#fffdf7)}
.race-lanes{grid-column:1;min-width:0;padding:3px 0 3px 6px}
.race-lane{position:relative;display:grid;grid-template-columns:96px 1fr;align-items:center;gap:8px;height:44px;border-bottom:1px dashed var(--line)}
.race-lane:last-child{border-bottom:0}
.race-lane-you{background:rgba(242,177,52,.14)}
.race-chip{display:flex;align-items:center;gap:6px;min-width:0}
.race-av{width:30px;height:30px;flex:none;border-radius:50%;display:grid;place-items:center;background:var(--card-2,#f3f1e8);border:2px solid var(--line);overflow:hidden}
.race-av svg{width:26px;height:26px;display:block}
.race-lane-you .race-av{border-color:var(--amber,#f2b134)}
.race-who{min-width:0;display:flex;flex-direction:column;line-height:1.12}
.race-nm{font-weight:700;font-size:12.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center;gap:3px}
.race-lane-you .race-nm{color:var(--amber-d,#d6920f)}
.race-fr{color:#5FA84F;font-size:12px;flex:none}
.race-sc{font-size:10.5px;color:var(--muted);font-variant-numeric:tabular-nums}
.race-runway{position:relative;height:100%;min-width:0}
.race-mover{position:absolute;top:50%;left:5%;transform:translate(-50%,-50%);z-index:2;will-change:transform}
.race-bob{display:flex;flex-direction:column;align-items:center}
.race-prop{display:block;width:15px;margin-bottom:-4px;filter:drop-shadow(0 0 5px rgba(250,199,117,.65))}
.race-prop svg{display:block}
.race-crit{width:30px;height:30px;display:block}
.race-crit svg{width:30px;height:30px;display:block}
.race-flag-mk{position:absolute;top:50%;transform:translateY(-50%);z-index:1;color:var(--muted);font-size:13px;opacity:.5}
.race-finish{grid-column:2;position:relative;overflow:hidden}
.race-checks{position:absolute;inset:0;background:repeating-conic-gradient(#e9e6db 0 25%,#cfcabb 0 50%) 0 0/10px 10px;opacity:.6}
.race-pin{position:absolute;right:2px;transform:translateY(50%);z-index:2;filter:drop-shadow(0 1px 2px rgba(40,50,30,.35))}
.race-pin svg{display:block}
/* gentle idle bob (≤2px, 2.4s ease alternate) — a calm ambient, not a flash; killed under calm/RM */
@keyframes raceBob{from{transform:translateY(0)}to{transform:translateY(-2px)}}
.race-shell:not(.race-calm) .race-bob{animation:raceBob 2.4s ease-in-out infinite alternate}
/* glide-in on mount — transform/opacity only, ~1.4s both fill (centering preserved in the keyframes) */
@keyframes raceGlide{from{transform:translate(-50%,-50%) translateX(-16px);opacity:0}to{transform:translate(-50%,-50%);opacity:1}}
.race-shell:not(.race-calm) .race-mover{animation:raceGlide 1.4s cubic-bezier(.2,.8,.25,1) both}
/* finishBeat crossing nudge — the runner glides the last stretch to the line (transform/left only) */
.race-mover.race-crossed{transition:left .5s cubic-bezier(.2,.8,.2,1);left:96%}
[dir="rtl"] .race-shell{direction:ltr}
.race-calm .race-bob,.race-calm .race-mover{animation:none}
@media (prefers-reduced-motion:reduce){.race-bob,.race-mover{animation:none}.race-mover.race-crossed{transition:none}}

/* ---- CE.Race.mountIntro — the first-open-per-window story beat (race.js P2). A fixed full-scene
   overlay over the board/popup: dusk back-plate (pure-CSS gradient today; .ri-plate is the P11 image
   seam) + the skin's floating prop + story caption + reward preview + a Join/Continue affordance.
   Opacity/transform only, within the calm ceiling; calm / reduced-motion never mounts it (race.js
   returns instant). Pinned LTR like the track. ---- */
.race-intro{position:fixed;inset:0;z-index:110;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:hidden;padding:26px;opacity:0;transition:opacity .3s ease}
.race-intro.ri-go{opacity:1}
.race-intro.ri-out{opacity:0}
[dir="rtl"] .race-intro{direction:ltr}
.ri-sky{position:absolute;inset:0;z-index:0}
.ri-plate{position:absolute;inset:0;background-size:cover;background-position:center}   /* P11: pre-baked scene image; absent → the .ri-sky gradient shows */
.ri-scrim{position:absolute;inset:0;z-index:1;background:radial-gradient(120% 82% at 50% 32%,transparent 42%,rgba(18,20,38,.42) 100%)}
/* over a real photo plate the text needs more backing: keep the top sky clear, darken from the caption zone down */
.race-intro.has-plate .ri-scrim{background:linear-gradient(180deg,transparent 0%,transparent 24%,rgba(8,10,24,.34) 48%,rgba(8,10,24,.52) 100%)}
.ri-stage{position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;gap:11px;max-width:340px;text-align:center;color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.42)}
.race-intro.ri-go .ri-stage{animation:riRise .42s cubic-bezier(.2,.9,.3,1) both}
@keyframes riRise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
.ri-prop{width:64px;filter:drop-shadow(0 0 15px rgba(250,199,117,.7))}
.ri-prop svg{width:64px;height:auto;display:block;margin:0 auto}
.race-intro:not(.ri-out) .ri-prop{animation:riProp 2.6s ease-in-out infinite alternate}
@keyframes riProp{from{transform:translateY(0)}to{transform:translateY(calc(-2px * var(--ri-mul,1)))}}   /* ≤2px, calm ceiling */
.ri-goal{font-weight:800;font-size:16px;line-height:1.3}
.ri-cap{font-size:14px;font-weight:600;line-height:1.42;opacity:.96}
.ri-reward{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;font-weight:700;background:rgba(0,0,0,.30);border-radius:999px;padding:5px 13px}
.ri-reward i{color:var(--amber,#f2b134);font-size:14px}
.ri-cta{margin-top:5px;display:inline-flex;align-items:center;gap:6px;border:none;border-radius:999px;padding:11px 22px;font-weight:800;font-size:15px;color:#3a2a10;background:linear-gradient(180deg,#ffd874,#f2b134);box-shadow:0 4px 14px rgba(0,0,0,.28);cursor:pointer;text-shadow:none}
.ri-cta i{font-size:16px}
body.calm .race-intro .ri-prop,body.lowfx .race-intro .ri-prop{animation:none}
@media (prefers-reduced-motion:reduce){.race-intro,.race-intro.ri-go .ri-stage{transition:none;animation:none}.race-intro .ri-prop{animation:none}}

/* ---- Daily Challenge hub (daily.js / renderDaily) ---- */
.daily-zone{margin-bottom:12px}
.dz-emoji{font-size:30px;line-height:1;width:44px;height:44px;display:flex;align-items:center;justify-content:center;background:var(--card-2,#f3f1e8);border-radius:12px}
.dz-h{font-weight:700;font-size:15px;margin-bottom:2px}
.dz-stats{display:flex;gap:8px;margin-top:8px}
.dz-stat{flex:1;background:var(--card-2,#f3f1e8);border-radius:10px;padding:8px 6px;text-align:center}
.dz-stat span{display:block;font-size:11px;color:var(--muted);margin-bottom:2px}
.dz-stat b{font-size:16px}
.dz-toggle{display:flex;align-items:center;gap:7px;font-size:13px;color:var(--muted);cursor:pointer}
.dz-toggle input{width:16px;height:16px}
/* the contest rule, stated on the card before you play (Today's Contest, 2026-08-02) */
.dz-rule{margin-top:8px;padding:8px 10px;background:var(--card-2,#f3f1e8);border-radius:10px;
  font-size:12.5px;line-height:1.45;color:var(--ink,#3a3a34)}

/* ---- Today hub: shortcut row + collapsible category buckets ---- */
.today-shortcuts{display:flex;gap:8px;margin:0 0 12px}
.tshort{flex:1;position:relative;display:flex;flex-direction:column;align-items:center;gap:3px;padding:9px 4px;border:none;border-radius:14px;background:var(--card,#fff);box-shadow:0 1px 3px rgba(60,70,50,.08);font-weight:700;font-size:13px;color:var(--ink,#3a4030);cursor:pointer}
.tshort i{font-size:20px}
.tshort:active{transform:scale(.97)}
.tshort.locked{opacity:.45}
.tshort-dot{position:absolute;top:7px;right:14px;width:8px;height:8px;border-radius:50%;background:#e8607a;box-shadow:0 0 0 2px var(--card,#fff)}
.tbucket{margin-bottom:10px}
.tbucket>summary{list-style:none;cursor:pointer;font-weight:700;font-size:13px;color:var(--muted);padding:4px 2px 8px;display:flex;align-items:center;gap:6px;user-select:none}
.tbucket>summary::-webkit-details-marker{display:none}
.tbucket>summary::before{content:'›';display:inline-block;transition:transform .15s;font-size:18px;line-height:1;color:var(--muted)}
.tbucket[open]>summary::before{transform:rotate(90deg)}
/* layout-stability: animated fold (see ui.js animateBuckets). The row eases 1fr↔0fr; the chevron keys
   off .folded so it rotates with the slide, not at the deferred native close. */
.tbucket .tb-body{display:grid;grid-template-rows:1fr;transition:grid-template-rows .26s ease}
.tbucket.folded .tb-body{grid-template-rows:0fr}
.tbucket .tb-inner{min-height:0;overflow:hidden}
.tbucket._anim[open]>summary::before, .tbucket>summary::before{transition:transform .15s}
.tbucket.folded[open]>summary::before{transform:rotate(0deg)}

/* ---- Critter Quiz (daily quiz mechanic) ---- */
/* Stage label + elapsed clock + dots, as one right-aligned group. Elapsed time is 15% of the
   Daily Score, so it is shown rather than hidden (§F9-A) — but muted and tabular so it reads as
   information, not a countdown. Logical properties: this group mirrors correctly under dir=rtl. */
.quiz-meta{display:flex;align-items:center;gap:8px;margin-inline-start:auto;min-width:0}
.quiz-meta .quiz-dots{margin:0}
.quiz-clock{display:inline-flex;align-items:center;gap:3px;font-size:12px;font-weight:600;
  color:var(--muted);font-variant-numeric:tabular-nums;white-space:nowrap}
.quiz-clock i{font-size:13px}
.quiz-stage{font-size:11px;font-weight:600;color:var(--muted);white-space:nowrap}
/* 375px is the commonest phone width, so the stage label must survive it — "which stage am I on"
   was a reported confusion. Drop the word, keep the position, only on genuinely tiny screens. */
@media (max-width:360px){ .quiz-stage{font-size:10px} .quiz-clock{font-size:11px} .quiz-meta{gap:6px} }
/* the armed state of the score-cancelling reveal — a warning tint, never an alarm flash */
.quiz-hintbar .btn.hint-arm{border-color:var(--amber-d);background:#fdf3df;color:#7a5300}
/* .quiz-opts sets column; this modifier must out-specify it, not just follow it — as a bare class
   it lost the cascade to the later .quiz-opts rule and stacked the six number buttons vertically. */
.quiz-opts.quiz-opts-row{flex-direction:row;flex-wrap:wrap;justify-content:center}
.quiz-dots{display:flex;gap:5px;margin-left:auto}
.quiz-dots .qd{width:8px;height:8px;border-radius:50%;background:#d7ddcf}
.quiz-dots .qd.done{background:var(--green,#7cc36b)}
.quiz-dots .qd.cur{background:var(--green-d,#4e9a4e);transform:scale(1.06)}
.quiz-card{background:var(--card,#fff);border-radius:18px;padding:20px 16px;box-shadow:0 2px 8px rgba(60,70,50,.08);text-align:center}
.quiz-emoji{font-size:60px;line-height:1;margin:4px 0 10px;display:inline-block}
.quiz-q{font-weight:700;font-size:18px;margin-bottom:16px}
.quiz-opts{display:flex;flex-direction:column;gap:9px}
.quiz-opt{padding:13px 14px;border:2px solid #e6e9df;border-radius:13px;background:#fbfcf9;font-weight:600;font-size:15px;color:var(--ink,#3a4030);cursor:pointer;transition:transform .12s,border-color .15s,opacity .15s,background .15s}
.quiz-opt:active{transform:scale(.98)}
.quiz-opt.dim{opacity:.4;pointer-events:none}
.quiz-opt.correct{border-color:var(--green,#7cc36b);background:#eef7e9}
.quiz-opt.reveal{border-color:var(--green,#7cc36b);animation:qpulse 1s ease}
.quiz-opt.wrong{border-color:#e8607a;background:#fbeef0;animation:qshake .3s ease}
@keyframes qpulse{0%,100%{transform:scale(1)}50%{transform:scale(1.04)}}
@keyframes qshake{0%,100%{transform:translateX(0)}25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}
.quiz-hintbar{display:flex;align-items:center;gap:10px;margin-top:14px;text-align:left}
.quiz-hintbar .muted{font-size:13px;flex:1}
@media (prefers-reduced-motion:reduce){ .quiz-opt.reveal,.quiz-opt.wrong{animation:none} }
.count-num{min-width:54px;flex:0 0 auto}
.count-grid{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin:6px 0 16px;font-size:30px;line-height:1}
.count-grid .cc{transition:transform .15s,filter .15s}
.count-grid .cc-on{filter:drop-shadow(0 0 6px var(--green,#7cc36b));transform:scale(1.06)}
@media (prefers-reduced-motion:reduce){ .count-grid .cc{transition:none} }
.odd-grid .cc-btn{border:none;background:transparent;cursor:pointer;border-radius:12px;padding:6px;transition:transform .12s,opacity .15s,background .15s}
.odd-grid .cc-btn:active{transform:scale(.92)}
.odd-grid .cc-btn.dim{opacity:.3;pointer-events:none}
.odd-grid .cc-btn.correct{background:#eef7e9}
.odd-grid .cc-btn.reveal{background:#eef7e9;animation:qpulse 1s ease}
.odd-grid .cc-btn.wrong{animation:qshake .3s ease}
@media (prefers-reduced-motion:reduce){ .odd-grid .cc-btn.reveal,.odd-grid .cc-btn.wrong{animation:none} }
.lboard{max-height:46vh;overflow-y:auto;margin-top:6px}

/* star chase */
.starrow{display:flex;gap:8px;flex-wrap:wrap}
.starmile{display:flex;flex-direction:column;align-items:center;gap:2px;border:1px solid var(--line);border-radius:13px;padding:8px 10px;background:#fff;font-weight:700;font-size:14px;min-width:62px}
.starmile .ti{font-size:18px;color:#cdd5c6}
.starmile .sm-rw{font-size:11px;font-weight:600}
.starmile.ready{border-color:var(--amber);box-shadow:0 0 0 2px rgba(242,177,52,.3)}
.starmile.ready .ti{color:var(--amber)}
.starmile.got{background:var(--green-l);opacity:.8}
.starmile.got .ti{color:var(--green)}
.starmile.soon{opacity:.55}

/* pass popup */
.phead{display:grid;grid-template-columns:42px 1fr 1fr;gap:6px;font-size:12px;font-weight:700;color:var(--muted);padding:0 2px 4px;text-align:center}
.pboard{max-height:46vh;overflow-y:auto}
.prow{display:grid;grid-template-columns:42px 1fr 1fr;gap:6px;align-items:center;padding:6px 0;border-bottom:1px solid var(--line)}
.prow .ptier{width:30px;height:30px;border-radius:50%;background:#eef2ea;display:grid;place-items:center;font-weight:700;margin:0 auto;font-size:14px}
.prow.reached .ptier{background:var(--green);color:#fff}
.pcell{display:flex;align-items:center;justify-content:center;gap:6px;min-height:34px;border-radius:10px;background:#faf8f1;flex-wrap:wrap}
.pcell.prem{background:#f3f0fb}
.pclaim{background:var(--amber);color:#5a3d00;font-weight:700;font-size:12px;padding:5px 10px;border-radius:9px}
.pcell .ok{color:var(--green);font-size:18px}
.tag.gold{background:var(--amber);color:#5a3d00}

/* ---------- Habitats (routing construction) ---------- */
.hab-res{display:flex;gap:8px;margin-bottom:12px}
.hab-res .res{flex:1;display:flex;align-items:center;gap:6px;background:#fff;border:1px solid var(--line);border-radius:14px;padding:9px 10px;box-shadow:var(--shadow);font-weight:700;font-size:15px}
.hab-res .res i{font-size:19px}
.hab-pen{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;min-height:10px}
.pen-chip{position:relative;flex:0 0 50px;width:50px;height:50px;border-radius:13px;background:var(--green-l);padding:4px;box-shadow:var(--shadow);cursor:pointer;transition:transform .1s}
.pen-chip:active{transform:scale(.9)}
.pen-chip svg{width:100%;height:100%;display:block}
.pen-chip .home-dot{position:absolute;bottom:-5px;right:-5px;width:20px;height:20px;border-radius:50%;display:grid;place-items:center;font-size:11px;color:#fff;border:2px solid #fff;box-shadow:0 1px 3px rgba(0,0,0,.18)}
.hab-card{position:relative;margin-bottom:12px}
.hab-head{display:flex;align-items:center;gap:11px}
.hab-ico{width:44px;height:44px;border-radius:13px;display:grid;place-items:center;font-size:23px;color:#fff;flex-shrink:0}
.hab-name{font-weight:700;font-size:17px}
.hab-stage{font-size:13px;color:var(--muted);font-weight:600}
/* stage-ladder pips (SANCTUARY spec P3) — replaces the single stage label + "Task N/9" counter */
.hab-pips{display:flex;align-items:center;gap:4px;flex-wrap:wrap;margin-top:2px}
.hab-pip{display:inline-flex;align-items:center;justify-content:center;gap:3px;border:0;padding:2px 7px;border-radius:999px;
  font-size:11px;font-weight:700;line-height:1.3;font-family:inherit;background:rgba(120,120,110,.12);color:var(--muted);cursor:pointer}
.hab-pip .ti{font-size:11px}
.hab-pip.done{background:#dff0d2;color:#4c8a3f;padding:2px 5px}
.hab-pip.cur{background:#fdeec9;color:#8a6a15}
.hab-pip.todo{padding:2px 4px;opacity:.55}
.hab-pip.todo .ti{font-size:9px}
.hab-card.complete .hab-pip.cur{background:#f5e3b0;color:#7a5a10}
.hab-reward{white-space:nowrap}
/* beds full/max trade line (SANCTUARY spec P5) */
.rpke-note{font-size:12px;color:var(--muted);margin:5px 2px 2px;line-height:1.45}
.hab-spec{display:flex;gap:4px;margin-left:auto}
.hab-spec .mini{flex:0 0 30px;width:30px;height:30px;background:rgba(255,255,255,.6);border-radius:8px;padding:2px}
.hab-spec .mini svg{width:100%;height:100%;display:block}
.hab-barwrap{margin-top:11px}
.hab-barlbl{display:flex;justify-content:space-between;font-size:12px;font-weight:600;color:var(--muted);margin-top:6px}
.hab-actions{display:flex;gap:8px;margin-top:11px}
.hab-actions .btn{flex:1}
.hab-btn2{display:flex;flex-direction:column;align-items:flex-start;gap:1px;text-align:start}   /* cost → outcome, two lines (redesign §2) */
.hab-btn2 .hb-l1{display:flex;align-items:center;gap:5px;font-weight:800}
.hab-btn2 .hb-out{font-size:10.5px;font-weight:600;opacity:.8;line-height:1.25}
.hab-card.complete{background:linear-gradient(160deg,#ffffff,#f1f8e6)}
.hab-card.hab-bloom{animation:habBloom .5s ease}
@keyframes habBloom{0%,100%{transform:scale(1)}45%{transform:scale(1.045)}}
.hab-pop{position:absolute;font-weight:800;color:var(--green-d);pointer-events:none;animation:fup .9s ease forwards;z-index:9;font-size:18px}
.hab-fly{position:fixed;z-index:80;pointer-events:none;will-change:transform,opacity}
.hab-fly svg{width:100%;height:100%;display:block;filter:drop-shadow(0 3px 4px rgba(40,55,30,.25))}
.hab-spark{position:fixed;width:9px;height:9px;border-radius:50%;background:#ffd76b;pointer-events:none;z-index:79}
@media (prefers-reduced-motion:reduce){.hab-card.hab-bloom{animation:none}.prog-fill{transition:none}}

/* ===================== Quest layer ("Ranger's Path") =====================
   All motion obeys the JUICE.md calm ceiling: scale <=1.06, single rise+decay,
   no oscillation, warm green/amber only (no white, no red), reduced-motion safe.
   NOTE: never reuse @keyframes pulse (scale 1.12 x2) — it breaches the ceiling. */
:root{ --q-accent:#5fae5f; --q-gold:#f2b134; --q-glow:rgba(95,174,95,.30); }

/* the host (Pip) chip + card head */
.q-host{display:flex;align-items:center;gap:9px;margin-bottom:6px}
.q-host .av{width:34px;height:34px}
.q-host .qh-name{font-weight:700;font-size:13px;color:var(--green-d)}
.q-host .qh-sub{font-size:12px;color:var(--muted);font-weight:500}
.q-track{display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:700;padding:2px 8px;border-radius:999px}
.q-track.skill{background:var(--green-l);color:var(--green-d)}
.q-track.open{background:#fdeccb;color:#9a6a05}

/* a quest row */
.qrow{display:flex;align-items:center;gap:10px;padding:9px 0;border-top:1px solid var(--line)}
.qrow:first-of-type{border-top:none}
.qrow .qr-ico{width:30px;height:30px;border-radius:9px;display:grid;place-items:center;flex-shrink:0;background:var(--green-l);color:var(--green-d);font-size:17px}
.qrow.worth .qr-ico{background:#fdeccb;color:#c4860a}
.qrow .qr-body{flex:1;min-width:0}
.qrow .qr-title{font-weight:600;font-size:14px;line-height:1.2}
.qrow .qr-pip{font-size:12px;color:var(--muted);font-weight:500;margin-top:1px}
.qrow .qr-right{display:flex;align-items:center;gap:6px;flex-shrink:0}
.q-modebadge{font-size:10px;font-weight:800;padding:2px 6px;border-radius:6px;background:var(--amber);color:#5a3d00}
.q-modebadge.worth{background:#c4860a;color:#fff}
.qrow.claimed .qr-title{opacity:.5;text-decoration:line-through}

/* the one calm row-level spotlight — single rise+decay, runs ONCE */
@keyframes qspot{0%{transform:scale(1)}45%{transform:scale(1.05)}100%{transform:scale(1)}}
.qrow.qspot{animation:qspot .9s ease 1;border-radius:12px;box-shadow:0 0 0 1.5px var(--q-accent),0 6px 16px var(--q-glow);padding-left:8px;padding-right:8px}

/* QP score pill in the HUD */
.hud-pill.qp i{color:var(--green-d)}
@keyframes qArrive{0%,100%{transform:none}45%{transform:scale(1.05)}}
.hud-pill.q-arrive,#qpVal.q-arrive{animation:qArrive .42s cubic-bezier(.2,1.3,.4,1)}

/* QP milestone chips (reuse the star-chase look) */
.qmiles{display:flex;gap:7px;overflow-x:auto;padding:2px 0 2px;margin-top:8px}
.qmile{flex:0 0 auto;display:flex;flex-direction:column;align-items:center;gap:2px;min-width:62px;padding:7px 8px;border-radius:12px;border:1px solid var(--line);background:#fff;font-weight:700;font-size:12px}
.qmile.got{background:var(--green-l);border-color:transparent;opacity:.85}
.qmile.ready{border-color:var(--amber);box-shadow:0 0 0 2px rgba(242,177,52,.35);background:#fffaf0;cursor:pointer}
.qmile .qm-rw{font-size:10px;color:var(--muted);font-weight:600}

/* reward motes flying to the HUD */
.q-mote{position:fixed;z-index:80;pointer-events:none;will-change:transform,opacity;font-size:16px;font-weight:800}

/* one-time worth "treasure" reveal */
@keyframes qTreasure{0%{transform:scale(.3) translateY(10px);opacity:0}55%{transform:scale(1.06);opacity:1}100%{transform:scale(1)}}
.q-treasure{animation:qTreasure .6s cubic-bezier(.2,1.5,.4,1)}

@media (prefers-reduced-motion:reduce){
  .qrow.qspot{animation:none}
  .hud-pill.q-arrive,#qpVal.q-arrive{animation:none}
  .q-mote{display:none}
  .q-treasure{animation:none}
}

/* ---- Guided coachmark ("Show me" — leads the player to the exact place to act) ----
   A soft spotlight (un-dims the target), Pip's pointing hand, and a caption bubble.
   Whole layer is pointer-events:none so the target stays tappable; only the bubble
   captures taps. Calm: the only loop is a gentle hand bob; off under reduced-motion. */
#coach{position:fixed;inset:0;z-index:58;pointer-events:none}
.coach-halo{position:fixed;border-radius:16px;border:3px solid var(--amber);
  box-shadow:0 0 0 4px rgba(242,177,52,.35), 0 0 0 9999px rgba(22,30,16,.46);
  pointer-events:none;transition:left .25s ease,top .25s ease,width .25s ease,height .25s ease;animation:coachIn .35s ease}
@keyframes coachIn{from{opacity:0;transform:scale(.94)}to{opacity:1;transform:scale(1)}}
.coach-hand{position:fixed;font-size:30px;color:var(--amber-d);pointer-events:none;z-index:59;
  filter:drop-shadow(0 2px 3px rgba(40,55,30,.3));animation:coachBob 1.15s ease-in-out infinite}
@keyframes coachBob{0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)}}
/* guided coachmark bubble — DISTINCT class from the in-board .coach-bubble (below) so the
   board-coach rule can't clobber it (it was overriding pointer-events:auto -> none = dead button,
   and #fff -> --ink = dark/cramped). Keep these two coaches on separate class names. */
.guide-bubble{position:fixed;max-width:268px;background:#fff;border-radius:16px;padding:12px 14px;z-index:60;
  box-shadow:0 14px 34px rgba(40,55,30,.30);pointer-events:auto;animation:coachIn .3s ease;display:flex;gap:10px;align-items:flex-start}
.guide-bubble .av{width:36px;height:36px;flex-shrink:0}
.guide-bubble .cb-name{font-weight:700;color:var(--green-d);font-size:12px;margin-bottom:2px}
.guide-bubble .cb-text{font-size:13.5px;font-weight:500;line-height:1.35;color:var(--ink)}
.guide-bubble .cb-ok{margin-top:10px;background:var(--green);color:#fff;border-radius:10px;font-weight:700;font-size:13px;padding:7px 15px;box-shadow:0 3px 0 var(--green-d)}
.guide-bubble .cb-ok:active{transform:translateY(2px);box-shadow:0 1px 0 var(--green-d)}
@media (prefers-reduced-motion:reduce){ .coach-halo,.coach-hand,.guide-bubble{animation:none} }

/* =====================================================================
   Interactive meta-layer — "tap any item to learn what it means / do its action"
   Critterpedia fact card + generic info card + league how-to.
   All motion obeys the JUICE.md calm ceiling: scale <=1.06, gentle shine,
   single rise+decay, no red, no flash, reduced-motion safe.
   ===================================================================== */

/* affordance: anything inspectable gets a soft press + pointer */
.tappable{cursor:pointer;transition:transform .12s ease}
.tappable:active{transform:scale(.95)}
.hud-pill.tappable:active{transform:scale(.96)}
/* hold-to-inspect targets: suppress the native long-press callout / text selection */
.tappable,.critter,.tool{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}

/* one-time "hold to learn" coach-mark on the first special piece (calm: soft ring pulse, no flash) */
.coach-glow{border-radius:14px;animation:coachGlow 1.6s ease-in-out infinite}
@keyframes coachGlow{0%,100%{box-shadow:0 0 0 2px rgba(242,177,52,.38)}50%{box-shadow:0 0 0 5px rgba(242,177,52,.72)}}
.coach-bubble{position:absolute;z-index:30;display:inline-flex;align-items:center;gap:7px;max-width:188px;
  background:var(--ink);color:#fff;font-weight:600;font-size:12.5px;line-height:1.25;padding:8px 12px;border-radius:13px;
  box-shadow:0 6px 18px rgba(0,0,0,.28);pointer-events:none;animation:coachFade .3s ease}
.coach-bubble i{font-size:17px;flex:0 0 auto;color:var(--amber)}
.coach-bubble::after{content:'';position:absolute;left:50%;transform:translateX(-50%);border:6px solid transparent}
.coach-bubble.up{transform:translate(-50%,-100%)}
.coach-bubble.up::after{top:100%;border-top-color:var(--ink)}
.coach-bubble.down{transform:translate(-50%,0)}
.coach-bubble.down::after{bottom:100%;border-bottom-color:var(--ink)}
@keyframes coachFade{from{opacity:0}to{opacity:1}}
@media (prefers-reduced-motion:reduce){
  .coach-glow{animation:none;box-shadow:0 0 0 3px rgba(242,177,52,.55)}
  .coach-bubble{animation:none}
}

/* ---- First-tap coach KIT (v3, COACH.md §v3): spotlight + white-glove hand + ghost demo ----
   Guidance, not juice — it shows even at the calm feel; calm/reduced-motion collapses to the
   static variant (no hand loop / ripple / ghost — spotlight + ring + bubble only). The whole
   kit is pointer-events:none; the input gate lives in tap(). z 31: above board art + HUD/dock
   (both dimmed by the spotlight's 9999px shadow), below every real overlay (tpot-dim 58+). */
.coach-kit{position:absolute;left:0;top:0;width:100%;height:100%;z-index:31;pointer-events:none}
.coach-kit>*{position:absolute}
.coach-spot{border-radius:16px;box-shadow:0 0 0 9999px rgba(22,30,16,.45)}
.coach-tapring{border-radius:16px;border:3px solid var(--amber);animation:coachRingPulse 1.6s ease-in-out infinite}
@keyframes coachRingPulse{0%,100%{transform:scale(1);opacity:.8}50%{transform:scale(1.05);opacity:1}}   /* ≤1.06 calm ceiling */
.coach-lane{border-radius:12px;background:rgba(255,240,170,.28);box-shadow:inset 0 0 0 2px rgba(242,177,52,.4)}
/* the TARGET carries the attention: a calm hop on the tile art itself (the moving thing = the thing to
   tap). On the inner svg so it never fights the .critter div's spawn/run transforms. ≤1.06 scale, ~6%
   lift — calm ceiling; cadence matches the tap-ring so they breathe together. */
.critter.coach-hero svg{transform-origin:50% 88%;animation:coachHeroHop 1.6s ease-in-out infinite}
@keyframes coachHeroHop{0%,100%{transform:none}50%{transform:translateY(-6%) scale(1.05)}}
.coach-ghost{opacity:0;animation:coachGhostRun 3.2s ease-in 3}   /* 3 demo runs, then rests (motion budget) */
.coach-ghost svg{width:100%;height:100%;display:block}
@keyframes coachGhostRun{0%,16%{opacity:0;transform:none}22%{opacity:.5}30%{opacity:.44}55%,100%{opacity:0;transform:var(--run)}}
.coach-hand2{transform-origin:51% 5%;filter:drop-shadow(0 3px 4px rgba(40,55,30,.35));animation:coachHandTap 3.2s ease-in-out infinite}
.coach-hand2 svg{width:100%;height:100%;display:block}
.coach-hand2.still{animation:none;transform:translateY(2px)}
@keyframes coachHandTap{0%,14%,100%{transform:translateY(7px)}5%,9%{transform:translateY(0) scale(.97)}}   /* rest = lifted; 5-9% = fingertip contact */
.coach-ripple{width:38px;height:38px;border-radius:50%;border:3px solid #fff;box-shadow:0 0 0 1px rgba(242,177,52,.6);opacity:0;animation:coachRipple 3.2s ease-out infinite}
@keyframes coachRipple{0%,4%{opacity:0;transform:scale(.4)}9%{opacity:.85;transform:scale(.75)}22%{opacity:0;transform:scale(1.5)}100%{opacity:0}}
.coach-bubble2{transform:translate(-50%,-100%);background:var(--ink);color:#fff;border-radius:15px;padding:10px 16px 9px;text-align:center;
  box-shadow:0 8px 22px rgba(0,0,0,.3);animation:coachFade .3s ease;max-width:240px}
.coach-bubble2 b{display:block;font-size:18px;font-weight:800;letter-spacing:.2px}
.coach-bubble2 span{display:block;font-size:12.5px;font-weight:600;opacity:.88;margin-top:2px}
.coach-bubble2::after{content:'';position:absolute;left:var(--tx,50%);top:100%;transform:translateX(-50%);border:8px solid transparent;border-top-color:var(--ink)}
/* drag-coach wordless strip: [drag ✗] → [tap ✓] inside the standard .coach-bubble */
.cb-col{display:flex;flex-direction:column;align-items:center;gap:6px}
.coach-strip{display:flex;align-items:center;gap:12px;background:rgba(255,255,255,.12);border-radius:9px;padding:4px 9px}
.coach-strip svg{width:40px;height:24px;display:block}
@media (prefers-reduced-motion:reduce){
  .coach-tapring,.coach-hand2,.coach-bubble2,.critter.coach-hero svg{animation:none}
  .coach-ripple,.coach-ghost{display:none}
}
body.calm .coach-tapring,body.calm .coach-hand2,body.calm .coach-bubble2,body.calm .critter.coach-hero svg{animation:none}
body.calm .coach-ripple,body.calm .coach-ghost{display:none}

/* ---- Critterpedia fact card (the animal popup) ---- */
.cpedia .cp-halo{width:118px;height:118px;margin:2px auto 6px;border-radius:50%;
  background:radial-gradient(circle,#fff7e0,#ffe6a8);padding:14px;
  box-shadow:0 0 0 5px rgba(242,177,52,.22),0 6px 16px rgba(0,0,0,.12);animation:discBloom .5s cubic-bezier(.2,1.5,.4,1)}
.cpedia .cp-halo svg{width:100%;height:100%;display:block}
.cpedia .cp-name{font-size:23px;font-weight:700;margin:0;line-height:1.1}
.cpedia .cp-tag{display:inline-flex;align-items:center;gap:5px;margin:7px 0 14px;
  background:var(--green-l);color:var(--green-d);font-weight:700;font-size:13px;padding:4px 12px;border-radius:999px}
.cp-facts{display:flex;flex-direction:column;gap:9px;text-align:left;margin:0 2px 6px}
.cp-fact{display:flex;align-items:center;gap:10px;background:#faf8f1;border:1px solid var(--line);
  border-radius:14px;padding:11px 13px;font-weight:600;font-size:15px;color:var(--ink);
  opacity:0;transform:translateY(6px);animation:cpFactIn .42s ease forwards}
.cp-fact:nth-child(1){animation-delay:.06s}
.cp-fact:nth-child(2){animation-delay:.15s}
.cp-fact:nth-child(3){animation-delay:.24s}
.cp-fact .cp-bul{flex:0 0 26px;width:26px;height:26px;border-radius:9px;display:grid;place-items:center;
  background:var(--green-l);color:var(--green-d);font-size:16px}
@keyframes cpFactIn{to{opacity:1;transform:translateY(0)}}
.cp-home{display:flex;align-items:center;justify-content:center;gap:6px;margin:12px 0 4px;
  font-size:13px;font-weight:600;color:var(--muted)}
.cp-home i{font-size:16px;color:var(--green-d)}
.cp-meta{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;color:var(--muted);
  background:#fff;border:1px solid var(--line);border-radius:999px;padding:3px 11px;margin:2px 0 2px}
.cp-meta.seen{color:var(--green-d);background:var(--green-l);border-color:transparent}

/* ---- generic info card (currencies / boosters / resources) ---- */
.infocard .ic-disc{width:58px;height:58px;margin:0 auto 8px;border-radius:17px;display:grid;place-items:center;
  font-size:30px;color:#fff;box-shadow:0 4px 12px rgba(50,70,40,.18);animation:discBloom .42s cubic-bezier(.2,1.4,.4,1)}
.infocard .ic-mean{font-size:15px;font-weight:600;color:var(--ink);margin:0 0 12px}
.ic-facts{display:flex;flex-direction:column;gap:7px;text-align:left;margin:0 0 4px}
.ic-facts .icf{display:flex;align-items:flex-start;gap:9px;font-size:13.5px;font-weight:500;color:var(--ink)}
.ic-facts .icf i{flex:0 0 auto;font-size:16px;color:var(--green-d);margin-top:1px}

/* ---- league: how-to-play ladder + per-tier hint ---- */
.league-howto{display:flex;flex-direction:column;gap:7px;text-align:left;margin:2px 0 12px}
.league-howto .lh{display:flex;align-items:flex-start;gap:9px;font-size:13.5px;font-weight:500;color:var(--ink)}
.league-howto .lh i{flex:0 0 auto;font-size:17px;color:var(--green-d);margin-top:1px}
.ladder{display:flex;flex-direction:column;gap:7px;margin:2px 0 6px}

/* ===================== Social: Critter Town ===================== */
.town-head{margin-bottom:6px}
.town-h3{margin:16px 0 8px;font-size:15px;font-weight:700}
.town-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
.pc{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);padding:12px 10px;box-shadow:var(--shadow);display:flex;flex-direction:column;align-items:center;gap:6px;text-align:center;position:relative}
.pc.house{cursor:pointer;transition:transform .1s}
.pc.house:active{transform:scale(.97)}
.pc-av{position:relative;width:62px;height:62px;border-radius:50%;background:var(--pcbg,#e7f6e2);display:grid;place-items:center;padding:6px;overflow:hidden;flex-shrink:0}
.pc-av svg{width:100%;height:100%;position:relative;z-index:2}
.pc-av.big{width:88px;height:88px;padding:9px}
.pc-banner{position:absolute;inset:0;display:grid;place-items:center;opacity:.16;z-index:1}
.pc-banner svg{width:135%;height:135%}
.pc-av.frame-leaf{box-shadow:0 0 0 3px #8fbf52}
.pc-av.frame-flame{box-shadow:0 0 0 3px #ef7a3a}
.pc-av.frame-gold{box-shadow:0 0 0 3px var(--amber)}
.pc-av.frame-crown{box-shadow:0 0 0 3px #9a8fe0}
.pc-name{font-weight:700;font-size:13px;line-height:1.15;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pc-name.big{font-size:17px;white-space:normal}
.pc-lvl{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:700;color:var(--green-d);background:var(--green-l);border-radius:999px;padding:3px 10px}
.pc-lvl:active{transform:scale(.95)}
.pc-like{display:inline-flex;align-items:center;gap:4px;font-size:13px;font-weight:700;color:#b54368;background:#fdeef2;border:1px solid #f6d3dd;border-radius:999px;padding:4px 11px}
.pc-like .ti{font-size:15px}
.pc-like.on{background:#f6c9d6;color:#9c2f4f;border-color:#eeb4c5}
.pc-like.static{cursor:default}
@keyframes likedPulse{0%,100%{transform:scale(1)}45%{transform:scale(1.06)}}
.pc-like.liked-pulse{animation:likedPulse .4s ease}
.town-me{align-items:center;margin-bottom:6px}
.town-me-stats{display:flex;gap:8px;align-items:center;margin:2px 0}
.town-me-actions{display:flex;gap:8px;margin-top:6px}

/* profile popup */
.prof{display:flex;flex-direction:column;align-items:center}
.prof .pc-av{margin:0 auto}
.pc-flair{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:700;color:var(--amber-d);background:#fdeccb;border-radius:999px;padding:3px 11px;margin-bottom:4px}
.prof-stats{display:flex;gap:6px;margin:12px 0 14px;width:100%;justify-content:center}
.pst{flex:1;background:#faf8f1;border-radius:13px;padding:9px 4px;display:flex;flex-direction:column;align-items:center;gap:1px}
.pst .ti{font-size:18px;color:var(--green-d)}
.pst-v{font-weight:800;font-size:16px}
.pst-l{font-size:11px;color:var(--muted);font-weight:600}

/* visit / sub-screen toolbar */
.visit-top{display:flex;align-items:center;gap:8px;margin:2px 0 4px}
.visit-title{flex:1;text-align:center;font-weight:700;font-size:16px;display:flex;align-items:center;justify-content:center;gap:6px;min-width:0}
.visit-title .av{width:26px;height:26px;flex-shrink:0}
.visit-jumps{display:flex;gap:6px}
.vmap{display:flex;flex-direction:column;align-items:center;gap:14px;padding:8px 0 6px}
.lvl-node.ro{cursor:default}

/* daily gallery */
.gal-podium{display:flex;gap:8px;align-items:flex-end;justify-content:center;margin:6px 0 12px}
.gal-pod{flex:1;background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);box-shadow:var(--shadow);padding:12px 6px 10px;display:flex;flex-direction:column;align-items:center;gap:4px;position:relative;cursor:pointer}
.gal-pod.p1{transform:scale(1.06);border-color:var(--amber);box-shadow:0 8px 20px rgba(242,177,52,.25)}
.gal-pod .pc-av{width:54px;height:54px}
.gal-pod.p1 .pc-av{width:64px;height:64px}
.gal-rank{position:absolute;top:-9px;left:50%;transform:translateX(-50%);width:24px;height:24px;border-radius:50%;background:var(--green);color:#fff;font-weight:800;font-size:13px;display:grid;place-items:center;border:2px solid #fff;box-shadow:0 1px 3px rgba(0,0,0,.2)}
.gal-pod.p1 .gal-rank{background:var(--amber);color:#5a3d00}
.gal-likes{font-weight:800;font-size:13px;color:#b54368;display:inline-flex;align-items:center;gap:3px}
.gal-list{display:flex;flex-direction:column;gap:2px}
.gal-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:12px;font-weight:600;font-size:14px;cursor:pointer}
.gal-row:active{background:rgba(95,174,95,.08)}
.gal-row.you{outline:2px solid var(--green);outline-offset:-2px;background:var(--green-l)}
.gal-pos{width:22px;text-align:center;color:var(--muted);font-weight:700}
.gal-row .av{width:34px;height:34px;flex-shrink:0}
.gal-nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gal-lk{font-weight:800;color:#b54368;display:inline-flex;align-items:center;gap:3px}

/* customize editor */
.cust-sec{margin:12px 0}
.cust-lbl{font-size:13px;font-weight:700;color:var(--muted);margin-bottom:6px}
.cust-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.cust-preview{margin-bottom:4px}
.sw{width:30px;height:30px;border-radius:50%;border:2px solid #fff;box-shadow:0 0 0 1px var(--line);display:grid;place-items:center;color:#fff}
.sw.on{box-shadow:0 0 0 2px var(--ink)}
.cust-sp{width:48px;height:48px;border-radius:14px;border:1px solid var(--line);background:#fff;padding:4px;display:grid;place-items:center;box-shadow:var(--shadow);color:var(--muted)}
.cust-sp svg{width:100%;height:100%}
.cust-sp.on{border-color:var(--green);box-shadow:0 0 0 2px rgba(95,174,95,.4)}
.cust-name{flex:1;min-width:120px;border:1px solid var(--line);border-radius:12px;padding:10px 12px;font-family:inherit;font-size:15px;font-weight:600;color:var(--ink);background:#fff}
.cust-frame{width:36px;height:36px;border-radius:50%;background:#fff;border:1px solid var(--line);display:grid;place-items:center;color:var(--muted)}
.cust-frame.frame-leaf{box-shadow:inset 0 0 0 3px #8fbf52}
.cust-frame.frame-flame{box-shadow:inset 0 0 0 3px #ef7a3a}
.cust-frame.frame-gold{box-shadow:inset 0 0 0 3px var(--amber)}
.cust-frame.frame-crown{box-shadow:inset 0 0 0 3px #9a8fe0}
.cust-frame.on{outline:2px solid var(--ink);outline-offset:1px}
.cust-frame.locked{opacity:.45}
.cust-flairs{flex-direction:column;align-items:stretch}
.cust-flair{display:flex;align-items:center;gap:8px;padding:9px 12px;border-radius:12px;border:1px solid var(--line);background:#fff;font-weight:600;font-size:14px;text-align:left}
.cust-flair .ti{font-size:18px;color:var(--green-d)}
.cust-flair.on{border-color:var(--green);background:var(--green-l);color:var(--green-d)}
@media (prefers-reduced-motion:reduce){.pc-like.liked-pulse{animation:none}.gal-pod.p1{transform:none}}
.ltier{display:flex;align-items:center;gap:11px;padding:9px 11px;border-radius:14px;border:1px solid var(--line);
  background:#fff;text-align:left}
.ltier .lt-ico{flex:0 0 38px;width:38px;height:38px;border-radius:11px;display:grid;place-items:center;
  font-size:21px;color:#fff;background:var(--green)}
.ltier .lt-body{flex:1;min-width:0}
.ltier .lt-name{font-weight:700;font-size:15px;line-height:1.1}
.ltier .lt-hint{font-size:12px;color:var(--muted);font-weight:500;margin-top:1px}
.ltier.cur{border-color:var(--amber);box-shadow:0 0 0 2px rgba(242,177,52,.32);background:#fffaf0}
.ltier.cur .lt-ico{background:var(--amber);color:#5a3d00;animation:tierShine 2.4s ease-in-out infinite}
.ltier.locked{opacity:.6}
.ltier.locked .lt-ico{background:#c8d2c0}
@keyframes tierShine{0%,100%{transform:scale(1);filter:brightness(1)}50%{transform:scale(1.05);filter:brightness(1.08)}}

/* league card: the tier badge becomes a tappable "how to play" hint chip */
.league-tierbadge{display:inline-flex;align-items:center;gap:6px;cursor:pointer}
.league-hintchip{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:600;
  color:var(--green-d);background:var(--green-l);border-radius:999px;padding:3px 10px;margin-top:8px}
.league-hintchip i{font-size:14px}

@media (prefers-reduced-motion:reduce){
  .cpedia .cp-halo,.cp-fact,.infocard .ic-disc{animation:none;opacity:1;transform:none}
  .ltier.cur .lt-ico{animation:none}
  .tappable:active,.hud-pill.tappable:active{transform:none}
}

/* ---------- Rescuer Level: HUD pill · locked meta cards · level-up · Profile ---------- */
.hud-pill.lvl i{color:#e6a32e}
.tcard.locked{opacity:.72;filter:saturate(.55)}
.tcard.locked .big{color:var(--muted)}
.tc-ico.locked{background:#c8d2c0}
.lock-chip{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:700;color:var(--muted);background:#eef2ea;border-radius:999px;padding:4px 10px}
/* ---------- Universal offline card state (OFFLINE.md §4.3): mirrors .locked — MUTED, never red ---------- */
.card.offline{border-style:dashed;border-color:rgba(124,138,115,.32);background:rgba(124,138,115,.05)}
.card.offline .big{color:var(--muted)}
.tc-ico.offline{background:rgba(124,138,115,.16);color:var(--muted)}
.tc-ico.offline i{font-size:20px}
.card.offline .btn[data-offline-connect]{color:var(--muted)}
/* ---------- Connection sheet (OFFLINE.md §4.2) — a calm bottom-sheet, symmetric actions ---------- */
.conn-sec-h{font-size:12px;font-weight:800;color:var(--muted);text-align:start;margin:10px 2px 4px}
.conn-item{display:flex;align-items:center;gap:9px;padding:7px 0;border-top:1px solid var(--line);text-align:start}
.conn-item:first-of-type{border-top:none}
.conn-ico{width:30px;height:30px;flex:0 0 auto;border-radius:9px;background:var(--green-l);color:var(--green-d);display:grid;place-items:center;font-size:16px}
.conn-note{font-size:13px;font-weight:600;color:var(--ink)}
.conn-rw{font-size:13px;font-weight:700;white-space:nowrap}
.conn-beacon,.conn-streak{display:flex;align-items:center;gap:7px;font-size:13px;color:var(--muted);margin:10px 2px 2px;text-align:start}
.conn-beacon i{color:#ef7a3a}.conn-streak i{color:#ef7a3a}
.conn-beacon b{color:var(--ink)}
.popup-actions .conn-act{flex:1 1 0;min-width:120px}   /* symmetric Retry / Keep-playing (no dark-pattern emphasis) */
.lvlup-banner{display:flex;align-items:center;gap:7px;justify-content:center;background:linear-gradient(90deg,var(--amber),#ffcf5a);color:#5a3d00;font-weight:800;font-size:14px;padding:9px 12px;border-radius:14px;margin-bottom:10px;box-shadow:var(--shadow)}
.lvlup-banner i{font-size:18px}
.lvlup-banner .lvlup-unl{font-weight:600;font-size:12px}
.lvl-unlocks{display:flex;flex-direction:column;gap:6px;margin:2px 0 12px}
.lvl-unl{display:flex;align-items:center;gap:7px;justify-content:center;font-size:14px;font-weight:600;color:var(--green-d)}
.ff-claim{display:flex;flex-direction:column;align-items:center;gap:2px;margin:6px 0 14px}
.ff-claim .ff-h{font-size:30px;font-weight:800;color:#7c5fae;line-height:1}
.ff-claim .ff-xp{font-size:13px;font-weight:600;color:var(--muted)}
.rxp-bar{display:flex;align-items:center;justify-content:space-between;margin:2px 0 10px}
.rxp-hero{margin-bottom:12px;background:linear-gradient(160deg,#ffffff,#f1f8e6)}
/* top-level Town <-> Friends switch (both are one tap from the always-available Town nav tab) */
.social-switch{display:flex;gap:6px;max-width:300px;margin:0 auto 14px}
.social-switch .ss-tab{flex:1;background:#fff;border:1px solid var(--line);border-radius:999px;padding:8px 0;font-weight:700;font-size:14px;color:var(--muted);box-shadow:var(--shadow)}
.social-switch .ss-tab.on{background:var(--green-l);color:var(--green-d);border-color:var(--green)}
.ref-tabs{display:flex;gap:8px;margin:16px 0 10px}
.ref-tab{flex:1;background:#fff;border:1px solid var(--line);border-radius:12px;padding:9px 0;font-weight:700;font-size:14px;color:var(--muted);box-shadow:var(--shadow)}
.ref-tab.on{background:var(--green-l);color:var(--green-d);border-color:var(--green)}
.lb-list{display:flex;flex-direction:column;gap:7px}
.lb-row{display:flex;align-items:center;gap:10px;background:#fff;border:1px solid var(--line);border-radius:14px;padding:9px 12px;box-shadow:var(--shadow)}
.lb-row.you{outline:2px solid var(--green);outline-offset:1px}
.lb-rank{width:22px;text-align:center;font-weight:800;color:var(--muted)}
.lb-row .av{width:34px;height:34px;flex-shrink:0}
.lb-name{font-weight:700;flex:1}
.lb-meta{font-size:13px;color:var(--muted);font-weight:600}
.lb-row .lb-act{flex-shrink:0;margin-left:4px;padding:5px 10px;font-size:12px}
.lb-row.asks{outline:2px solid #f6b8c6;outline-offset:1px;background:#fff6f8}
.lb-row .tag{flex-shrink:0;font-size:11px;font-weight:700;color:var(--green-d,#3f8a47);background:var(--green-l);border-radius:999px;padding:3px 9px}
.hearts-panel{margin:14px 0 4px;background:#fff;border:1px solid var(--line);border-radius:14px;padding:11px 13px;box-shadow:var(--shadow)}
.hearts-panel .hp-row{display:flex;align-items:center;gap:10px;justify-content:space-between;flex-wrap:wrap}
.hearts-panel .hp-txt{font-size:14px}
.hearts-panel .hp-note{font-size:12px;margin-top:7px}
.rxp-top{display:flex;align-items:center;gap:14px}
.rxp-av{position:relative;width:64px;height:64px;border-radius:50%;background:radial-gradient(circle,#fff7e0,#ffe6a8);padding:8px;box-shadow:0 0 0 4px rgba(242,177,52,.22),var(--shadow);flex-shrink:0}
.rxp-av svg{width:100%;height:100%;display:block}
.rxp-badge{position:absolute;right:-4px;bottom:-4px;min-width:24px;height:24px;border-radius:999px;background:var(--green);color:#fff;font-size:13px;font-weight:800;display:grid;place-items:center;border:2px solid #fff;padding:0 5px}
.rxp-name{font-size:20px;font-weight:700}
.rxp-xprow{display:flex;justify-content:space-between;align-items:center}
.rxp-h3{margin:18px 0 8px;font-size:16px;font-weight:700}
.rxp-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
.rxp-stat{background:#fff;border:1px solid var(--line);border-radius:14px;padding:10px 4px;text-align:center;box-shadow:var(--shadow)}
.rxp-stat i{font-size:18px;color:var(--green-d)}
.rxp-stat .rxp-v{font-size:16px;font-weight:800;margin-top:2px}
.rxp-stat .rxp-l{font-size:10px;color:var(--muted);font-weight:600;line-height:1.1;margin-top:1px}
.rxp-ladder{display:flex;flex-direction:column;gap:8px}
.rxp-row{display:flex;align-items:center;gap:11px;padding:10px 12px;border-radius:14px;border:1px solid var(--line);background:#fff;box-shadow:var(--shadow)}
.rxp-row .rxp-rico{flex:0 0 38px;width:38px;height:38px;border-radius:11px;display:grid;place-items:center;font-size:20px;color:#fff;background:var(--green)}
.rxp-row.off{opacity:.72}
.rxp-row.off .rxp-rico{background:#c8d2c0}
.rxp-row .rxp-rbody{flex:1;min-width:0}
.rxp-row .rxp-rname{font-weight:700;font-size:15px}
.rxp-row .rxp-rsub{font-size:12px;color:var(--muted);font-weight:600;margin-top:1px}
.rxp-row .rxp-rlvl{font-weight:800;font-size:14px;color:var(--green-d)}
.rxp-row.off .rxp-rlvl{color:var(--muted)}
.rxp-row .rxp-rlvl .ti{font-size:20px;color:var(--green)}
@media (max-width:360px){ .rxp-stats{grid-template-columns:repeat(3,1fr)} }

/* =======================================================================
   Localization fonts + RTL  (CE.i18n sets <html lang="xx" dir="..">).
   Persian/Arabic use the bundled Vazirmatn (Arabic-script) face; CJK / Thai /
   Cyrillic use platform-native faces (instant, no extra web-font download).
   Latin languages keep the brand Fredoka. The layout mirrors under RTL EXCEPT
   the spatial gameplay surfaces, which stay LTR.
   ======================================================================= */
html[lang="fa"] body,
html[lang="ar"] body{
  font-family:'Vazirmatn','Fredoka',system-ui,-apple-system,'Segoe UI',Tahoma,sans-serif;
  line-height:1.65;
}
html[lang="ru"] body{ font-family:'Fredoka',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif; }
html[lang="th"] body{ font-family:'Thonburi','Leelawadee UI','Noto Sans Thai',system-ui,sans-serif; line-height:1.6; }
html[lang="ko"] body{ font-family:'Apple SD Gothic Neo','Malgun Gothic','Noto Sans KR',system-ui,sans-serif; }
html[lang="ja"] body{ font-family:'Hiragino Sans','Hiragino Kaku Gothic ProN','Yu Gothic','Meiryo','Noto Sans JP',system-ui,sans-serif; }
html[lang="zh-Hans"] body{ font-family:'PingFang SC','Hiragino Sans GB','Microsoft YaHei','Noto Sans SC',system-ui,sans-serif; }
html[lang="zh-Hant"] body{ font-family:'PingFang TC','Microsoft JhengHei','Noto Sans TC',system-ui,sans-serif; }
/* the language picker holds 17 options — wrap + scroll so it never dominates the Settings popup */
.lang-row{ flex-wrap:wrap; gap:6px; max-height:138px; overflow-y:auto; padding:2px 2px 4px; }
/* GAMEPLAY STAYS LTR: the board grid + level map are spatial — critters face
   arrow directions tied to geometry, so mirroring them would contradict the
   puzzle logic. Pin them to LTR even inside the RTL document. */
[dir="rtl"] .board-wrap,
[dir="rtl"] .board,
[dir="rtl"] .pipe-time,
[dir="rtl"] .map-wrap{ direction:ltr; }   /* .pipe-time is a fixed overlay OUTSIDE the board flow, so the board's LTR pin never reached it — under RTL its per-digit cells laid the time bonus out right-to-left ("100"→"001"). Pin the gauge LTR. */

/* right-align the text blocks that were hardcoded text-align:left */
[dir="rtl"] .quiz-hintbar,
[dir="rtl"] .cp-facts,
[dir="rtl"] .ic-facts,
[dir="rtl"] .league-howto,
[dir="rtl"] .cust-flair{ text-align:right; }
[dir="rtl"] .ev-body,[dir="rtl"] .ev-howto{ text-align:right; }
[dir="rtl"] .ev-howto{ padding-left:0; padding-right:2px; }
[dir="rtl"] .ltype-banner .ltb-tx,[dir="rtl"] .lpromo-body,[dir="rtl"] .loss-stakes .ls-side{ text-align:right; }

/* the guided coach is appended INTO .board-wrap (LTR-pinned for the grid), so its Persian text
   inherited LTR and flipped word/punctuation order — re-assert RTL on the text bubbles only, never the grid */
[dir="rtl"] .coach-bubble,[dir="rtl"] .coach-bubble2{ direction:rtl; }
/* same board-LTR-pin trap for the transient in-board banners: cub-wave ("Cubs safe! · Next den…") and
   rainbow-rush are appended into #board and inherited LTR; praise-pop HARDCODES direction:ltr. All three
   carry translated Persian sentence/word text, so re-assert RTL (the [dir=rtl] scope out-specifies the
   hardcode). Never touches the grid itself. */
[dir="rtl"] .cub-wave-banner,[dir="rtl"] .rainbow-banner,[dir="rtl"] .praise-pop{ direction:rtl; }

/* margin-left:auto right-pins were LTR tricks — flip to margin-right:auto */
[dir="rtl"] .quiz-dots,
[dir="rtl"] .hab-spec{ margin-left:0; margin-right:auto; }

/* corner indicators mirror to the opposite horizontal edge */
[dir="rtl"] .badge-best{ right:auto; left:10px; }
[dir="rtl"] .logcell .ti{ right:auto; left:4px; }
[dir="rtl"] .tshort-dot{ right:auto; left:14px; }
[dir="rtl"] .pen-chip .home-dot{ right:auto; left:-5px; }
[dir="rtl"] .rxp-badge{ right:auto; left:-4px; }

/* back-chevrons point the RTL way */
[dir="rtl"] .hud-icon .ti-chevron-left{ display:inline-block; transform:scaleX(-1); }

/* screen slide-in mirrors so "forward" still enters from the leading edge */
[dir="rtl"] #screen.slide-fwd{ animation-name:scrSlideBack; }
[dir="rtl"] #screen.slide-back{ animation-name:scrSlideFwd; }

/* ---- identity / juicy profile: name + flag, edit, avatar frames, medals ---- */
.rxp-top{position:relative}
.rxp-edit{position:absolute;top:0;right:0;color:#7c8a73}
[dir="rtl"] .rxp-edit{right:auto;left:0}
.rxp-name{unicode-bidi:plaintext}
.rxp-av.frame-leaf{box-shadow:0 0 0 4px rgba(95,174,95,.5),var(--shadow)}
.rxp-av.frame-flame{box-shadow:0 0 0 4px rgba(242,120,52,.55),var(--shadow)}
.rxp-av.frame-gold{box-shadow:0 0 0 4px rgba(241,196,15,.6),var(--shadow)}
.rxp-av.frame-crown{box-shadow:0 0 0 4px rgba(155,120,230,.55),var(--shadow)}
.rxp-h3c{font-size:13px;font-weight:600;color:#9aa890;margin-inline-start:6px}
.rxp-medals{display:grid;grid-template-columns:repeat(5,1fr);gap:8px}
@media (max-width:420px){ .rxp-medals{grid-template-columns:repeat(4,1fr)} }
.rxp-medal{display:flex;flex-direction:column;align-items:center;gap:5px;padding:10px 4px;border-radius:14px;background:#f4f7f1;text-align:center;opacity:.55;filter:grayscale(.6);cursor:pointer}   /* P4: tappable → heroic detail popup */
.rxp-medal:active{transform:scale(.97)}
@media (prefers-reduced-motion:reduce){.rxp-medal:active{transform:none}}
.rxp-medal.on{opacity:1;filter:none;background:linear-gradient(160deg,#fff,#f1f8e6);box-shadow:var(--shadow)}
.rxp-mico{width:38px;height:38px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:#e7ece2;color:#9aa890;font-size:20px}
.rxp-medal.on .rxp-mico{background:radial-gradient(circle,#fff7e0,#ffe6a8);color:#e0962b}
.rxp-ml{font-size:11px;font-weight:600;line-height:1.2;color:#5a6b50}
.rxp-medal.on .rxp-ml{color:#3b4a33}

/* ---- settings: Simple/Detailed segmented control + profile card + section headers + language flags ---- */
.set-seg{display:flex;gap:8px;justify-content:center;margin:12px 0 2px}
.set-profile{display:flex;align-items:center;gap:12px;padding:10px 12px;margin:6px 0 10px;border-radius:16px;background:linear-gradient(160deg,#fff,#f1f8e6);box-shadow:var(--shadow)}
.set-pav{width:46px;height:46px;border-radius:50%;background:radial-gradient(circle,#fff7e0,#ffe6a8);padding:5px;flex-shrink:0}
.set-pav svg{width:100%;height:100%}
.set-pinfo{flex:1;min-width:0}
.set-pname{font-size:16px;font-weight:800;unicode-bidi:plaintext;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.set-h{font-size:11.5px;font-weight:800;text-transform:uppercase;letter-spacing:1.1px;color:#9aa890;margin:15px 4px 6px}
.lang-chip .flag-emoji{font-size:1.05em}
/* grouped settings cards + rows (Settings / Developer tools redesign) */
.set-card{background:#fff;border:1px solid var(--line);border-radius:16px;overflow:hidden;box-shadow:var(--shadow)}
.set-row{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;padding:12px 14px;background:#fff;border:0;border-top:1px solid var(--line);font-size:15px;font-weight:600;text-align:left;color:inherit;font-family:inherit;cursor:pointer}
.set-card .set-row:first-child{border-top:0}
.set-row:active{background:#f6faef}
.set-row .lbl{display:flex;align-items:center;gap:10px;min-width:0;flex:1}
.set-row .lbl i{font-size:18px;color:#8a9a80;flex-shrink:0}
.set-row .val{display:flex;align-items:center;gap:6px;font-weight:700;color:#9aa890;font-size:13.5px;flex-shrink:0;white-space:nowrap}
.set-row .val.on{color:var(--green)}
.set-row .val .ti-chevron-right{font-size:17px;color:#c7d0bd;margin-right:-3px}
.set-row.inner{flex-direction:column;align-items:stretch;gap:9px}
.set-row.inner .lbl{flex:none}
.set-row.inner .diff-row{justify-content:flex-start;flex-wrap:wrap;margin:0}
/* A4 audio mixer — per-layer volume sliders. Calm palette (green accent), comfortable touch target. */
.set-row.srow{cursor:default;gap:14px}
.set-row.srow .lbl{flex:0 0 auto}
.volr{flex:1;min-width:110px;max-width:210px;height:26px;margin:-3px 0 -3px auto;accent-color:var(--green);cursor:pointer;background:transparent}
.volr:focus{outline:none}
.dev-badge{background:#eef3e6;color:#7a8a6f;border-radius:6px;padding:1px 7px;font-size:11px;font-weight:800;text-transform:lowercase;letter-spacing:.3px}
.lang-list{display:flex;flex-direction:column;max-height:52vh;overflow:auto;border:1px solid var(--line);border-radius:14px;margin:6px 0}
.lang-opt{display:flex;align-items:center;gap:11px;padding:11px 13px;border:0;border-top:1px solid var(--line);background:#fff;width:100%;text-align:left;font-weight:700;font-size:15px;font-family:inherit;cursor:pointer}
.lang-list .lang-opt:first-child{border-top:0}
.lang-opt:active{background:#f6faef}
.lang-opt.on{background:linear-gradient(160deg,#fff,#eef7e0)}
.lang-opt .lo-flag{font-size:21px;flex-shrink:0;line-height:1}
.lang-opt .lo-name{flex-shrink:0}
.lang-opt .lo-en{flex:1;color:#9aa890;font-weight:600;font-size:12px}
.lang-opt .ti-check{color:var(--green);font-size:19px;flex-shrink:0}

/* reduced-motion / calm: keep the star+heart reward feedback (fill stays) but drop the pulses */
@media (prefers-reduced-motion:reduce){
  .hud-pill.lives.beat i,.mistakes .mh.lost-beat,.mistakes .mh.lost-beat path,.popup .stars .star.on{animation:none}
}

/* ===== Level types (leveltypes.js / LEVEL_TYPES.md) — SKIN + framing only, static (calm ceiling) ===== */
.board.ltype-reversed{box-shadow:inset 0 0 0 3px rgba(143,111,208,.30),0 0 22px rgba(143,111,208,.16)}
.board.ltype-boss{box-shadow:inset 0 0 0 3px rgba(224,168,58,.34),0 0 22px rgba(224,168,58,.15)}
.board.ltype-zen{box-shadow:inset 0 0 0 3px rgba(110,195,175,.32),inset 0 0 42px rgba(150,220,200,.20)}
.board.ltype-overgrown{box-shadow:inset 0 0 0 4px rgba(60,120,60,.38),inset 0 0 42px rgba(48,108,52,.20)}
/* the type chip beside the level label */
.ltype-chip{display:inline-flex;align-items:center;gap:4px;margin-left:8px;padding:2px 9px;border-radius:999px;font-size:12px;font-weight:600;vertical-align:middle;line-height:1.4}
[dir=rtl] .ltype-chip{margin-left:0;margin-right:8px}   /* the 8px gap belongs between the chip and the level label — under RTL that is the chip's RIGHT */
.ltype-chip i{font-size:13px}
.ltype-chip.ltype-reversed{background:rgba(143,111,208,.16);color:#6b52a8}
.ltype-chip.ltype-boss{background:rgba(224,168,58,.20);color:#946a12}
.ltype-chip.ltype-zen{background:rgba(120,180,150,.18);color:#3f7d63}
.ltype-chip.ltype-overgrown{background:rgba(74,120,70,.16);color:#3c6a38}
/* map node badge (top-left corner; mirrors .ev-badge on the right) */
.ltype-badge{position:absolute;top:-7px;left:-7px;width:24px;height:24px;border-radius:50%;border:2px solid #fff;display:flex;align-items:center;justify-content:center;pointer-events:none;box-shadow:0 1px 4px rgba(50,70,40,.22);z-index:3;font-size:13px;color:#fff}
.ltype-badge.ltype-boss{background:#e0a83a}
.ltype-badge.ltype-reversed{background:#8f6fd0}
.lvl-node.locked .ltype-badge{opacity:.72}
/* curse / boss intro card accents */
.curse-card h2{color:#6b52a8}
.boss-card h2{color:#946a12}
/* v2 curses (triangle/hexagon/fog) + standard variants (menagerie/rescue) */
.slot.void{visibility:hidden}   /* off-platform: no tile at all, so the remaining tiles read as the platform shape */
.board.ltype-triangle,.board.ltype-hexagon,.board.ltype-fog{box-shadow:inset 0 0 0 3px rgba(143,111,208,.28),0 0 22px rgba(143,111,208,.14)}
.board.ltype-menagerie{box-shadow:inset 0 0 0 3px rgba(240,140,175,.32),inset 0 0 42px rgba(250,190,120,.18)}
.board.ltype-rescue{box-shadow:inset 0 0 0 3px rgba(240,160,90,.34),inset 0 0 42px rgba(250,180,110,.20)}
.ltype-chip.ltype-triangle,.ltype-chip.ltype-hexagon,.ltype-chip.ltype-fog{background:rgba(143,111,208,.16);color:#6b52a8}
.ltype-chip.ltype-menagerie{background:rgba(90,160,210,.18);color:#2f6a94}
.ltype-chip.ltype-rescue{background:rgba(230,140,160,.18);color:#a84f66}
/* The remaining three LT_META types had NO chip rule, so .ltype-chip fell back to a transparent pill in
   inherited body colour — the header read as bare text jammed against "Level N" rather than a chip.
   water is the one that shows most (newest type, 10 live slots). Every key in ui.js LT_META must have a
   rule here; the map badge below has the same contract. */
.ltype-chip.ltype-water{background:rgba(90,175,215,.20);color:#25708f}
.ltype-chip.ltype-linear{background:rgba(90,175,215,.16);color:#2b7c9c}
.ltype-chip.ltype-babyRescue{background:rgba(240,160,190,.20);color:#a8506f}
.ltype-badge.ltype-triangle,.ltype-badge.ltype-hexagon,.ltype-badge.ltype-fog{background:#8f6fd0}
/* map-node badges: same gap — these seven types drew a WHITE glyph on a transparent circle (invisible). */
.ltype-badge.ltype-water,.ltype-badge.ltype-linear{background:#4fa8cf}
.ltype-badge.ltype-babyRescue{background:#e88bb0}
.ltype-badge.ltype-zen{background:#5fae90}
.ltype-badge.ltype-overgrown{background:#4a8a46}
.ltype-badge.ltype-menagerie{background:#4f96c8}
.ltype-badge.ltype-rescue{background:#dc7f96}

/* ============ STAGE-0 DEEP TYPE IDENTITY (LEVELTYPE_OVERHAUL.md) ============
   Each level type is its own PLACE, not a colored ring on the meadow. ALL color derives from the type
   PAL via --lt-* vars set on .board-wrap (ui.js applyBoardAssets); scoped under body.tp-on so flag-OFF is
   byte-identical. Calm ceiling: textures are STATIC (no animation/flash), element opacity ≤ .17. The
   texture ::before sits behind the board (board z-index:2; translucent cells let it read through); slot
   treatments layer OVER the habitat _skinCSS via .slot::before (stays BELOW the critter). Reskin-safe:
   a Reef/Space swap changes only typePresentation — these selectors follow the tokens. */
/* token-driven ring — supersedes the legacy hardcoded per-type rings when the flag is on (same
   specificity, later in file → wins), so fog reads sky-grey not purple, boss/rescue read amber, etc. */
.board.lt-skinned{box-shadow:inset 0 0 0 3px var(--lt-ring,rgba(143,111,208,.3)),0 0 22px var(--lt-glow,rgba(143,111,208,.14))}
/* type-tinted play-area FRAME — the whole card reads as the type's color family. Respects a purchased
   playmat cosmetic (--cos-playmat wins; the type tint is only the fallback). */
.tp-on .board-wrap[data-ltbg]{background:var(--cos-playmat,linear-gradient(170deg,var(--lt-soft,rgba(255,255,255,.5)),rgba(255,255,255,.55)))}
/* per-type texture layer — one static ::before, bounded to the rounded frame (no overflow:hidden, so the
   run-off exit fling is never clipped) */
.tp-on .board-wrap[data-ltbg]::before{content:'';position:absolute;inset:0;z-index:1;pointer-events:none;border-radius:22px}
.tp-on .board-wrap[data-ltbg='den']::before{opacity:.16;background:radial-gradient(120% 74% at 50% 116%,var(--lt-b),transparent 66%),linear-gradient(180deg,var(--lt-k) 0%,transparent 34%)}                                    /* boss — grand golden hall */
.tp-on .board-wrap[data-ltbg='mirror']::before{opacity:.15;background:linear-gradient(125deg,var(--lt-l) 0%,transparent 44%,transparent 56%,var(--lt-b) 100%)}                                                                 /* reversed — mirror world sheen */
.tp-on .board-wrap[data-ltbg='shard']::before{opacity:.13;background:repeating-linear-gradient(60deg,var(--lt-b) 0 3px,transparent 3px 26px),repeating-linear-gradient(-60deg,var(--lt-l) 0 3px,transparent 3px 26px)}          /* triangle — angular facets */
.tp-on .board-wrap[data-ltbg='comb']::before{opacity:.14;background:radial-gradient(circle at 50% 50%,var(--lt-b) 0 12%,transparent 13%);background-size:26px 30px}                                                             /* hexagon — crystal-comb lattice */
.tp-on .board-wrap[data-ltbg='haze']::before{opacity:.17;background:linear-gradient(180deg,transparent,var(--lt-l) 30%,transparent 46%,var(--lt-l) 66%,transparent 84%)}                                                        /* fog — grey haze bands */
.tp-on .board-wrap[data-ltbg='ripple']::before{opacity:.14;background:repeating-radial-gradient(circle at 50% 46%,var(--lt-b) 0 2px,transparent 2px 22px)}                                                                      /* zen — dawn ripple rings */
.tp-on .board-wrap[data-ltbg='thicket']::before{opacity:.16;background:radial-gradient(70% 60% at 0% 0%,var(--lt-k),transparent 60%),radial-gradient(70% 60% at 100% 100%,var(--lt-k),transparent 60%),radial-gradient(60% 50% at 100% 0%,var(--lt-b),transparent 62%)}   /* overgrown — deep thicket corners */
.tp-on .board-wrap[data-ltbg='confetti']::before{opacity:.16;background:radial-gradient(circle at 18% 22%,var(--lt-b) 0 3px,transparent 4px),radial-gradient(circle at 72% 34%,var(--lt-l) 0 3px,transparent 4px),radial-gradient(circle at 42% 68%,var(--lt-b) 0 2.5px,transparent 4px),radial-gradient(circle at 84% 78%,var(--lt-l) 0 3px,transparent 4px),radial-gradient(circle at 30% 88%,var(--lt-b) 0 2px,transparent 4px)}   /* menagerie — festive specks */
.tp-on .board-wrap[data-ltbg='warmwash']::before{opacity:.15;background:linear-gradient(135deg,var(--lt-l) 0%,transparent 40%,transparent 60%,var(--lt-b) 100%)}                                                                /* rescue — warm sweep */
.tp-on .board-wrap[data-ltbg='rapids']::before{opacity:.16;background:repeating-linear-gradient(0deg,var(--lt-b) 0 2px,transparent 2px 15px),linear-gradient(90deg,var(--lt-l),transparent 60%)}                                  /* linear — horizontal speed lines */
/* per-type SLOT treatment — additive over the habitat skin; ::before keeps it below the critter */
.tp-on .board[data-ltslot] .slot::before{content:'';position:absolute;inset:0;border-radius:inherit;pointer-events:none;background:var(--lt-b);opacity:.07}
.tp-on .board[data-ltslot='warm'] .slot::before{opacity:.10}
.tp-on .board[data-ltslot='frost'] .slot::before{background:var(--lt-l);opacity:.14}
.tp-on .board[data-ltslot='leafy'] .slot::before{opacity:.09}
.tp-on .board[data-ltslot='runic'] .slot{box-shadow:inset 0 0 0 1px var(--lt-ring),inset 0 0 0 1px rgba(70,90,55,.06)}
.tp-on .board[data-ltslot='stone'] .slot{border-radius:5px}
.tp-on .board[data-ltslot='crystal'] .slot{border-radius:5px 13px 5px 13px}
.tp-on .board[data-ltslot='lily'] .slot{border-radius:17px}
/* motif watermark — big faint type silhouette behind the grid (static, opacity ≤ .07 → calm) */
.lt-watermark{position:absolute;inset:0;z-index:1;display:grid;place-items:center;pointer-events:none}
.tp-on .lt-watermark i{font-size:clamp(120px,42vw,210px);line-height:1;color:var(--lt-b,#8FBF52);opacity:.11}

/* ============ TYPE PRESENTATION (TYPE_PRESENTATION.md) — token-driven via --lt-b/l/k ============ */
/* one-time in-view wiggle on special map nodes (§4.6) + filled trophy badge */
.ltype-badge.trophy{border-color:#fff;box-shadow:0 1px 5px rgba(50,70,40,.3)}
.lvl-node.ltype-hero{transform:scale(1.12)}
.lvl-node.ltype-hero .ltype-badge{width:28px;height:28px;font-size:15px;top:-9px;left:-9px}
@keyframes ltWiggle{0%{transform:scale(1) rotate(0)}30%{transform:scale(1.06) rotate(-5deg)}60%{transform:scale(1.03) rotate(4deg)}100%{transform:scale(1) rotate(0)}}
.ltype-badge.wiggle{animation:ltWiggle .55s ease-out}
body.calm .ltype-badge.wiggle,body.lowfx .ltype-badge.wiggle{animation:none}

/* iris hole-edge tint (§3.1) — a faint colored ring following the growing hole */
.iris.tinted::after{content:'';position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(circle at 50% 50%, transparent var(--ir,0%), var(--iris-tint,transparent) calc(var(--ir,0%) + 1.4%), transparent calc(var(--ir,0%) + 4%));opacity:.55}

/* the in-world intro overlay */
.type-intro{position:fixed;inset:0;z-index:107;pointer-events:none;overflow:hidden;display:flex;align-items:center;justify-content:center;flex-direction:column}
.lt-wash{position:absolute;inset:-12%;opacity:0;background:linear-gradient(135deg,transparent 0%,var(--lt-l) 46%,transparent 72%)}
.type-intro.go .lt-wash{animation:ltWash .25s ease-out forwards}
@keyframes ltWash{0%{opacity:0;transform:translate(-30%,-30%)}45%{opacity:.28}100%{opacity:0;transform:translate(18%,18%)}}
.lt-motif{position:absolute;width:40%;max-width:170px;opacity:0}
.type-intro.go .lt-motif{animation:ltMotif .55s cubic-bezier(.3,.7,.3,1) .1s forwards}
@keyframes ltMotif{0%{opacity:0;transform:translate(-22%,-16%) scale(.9)}42%{opacity:.92;transform:translate(0,0) scale(1.04)}100%{opacity:0;transform:translate(18%,14%) scale(1.06)}}
.lt-banner{position:relative;display:inline-flex;align-items:center;gap:8px;padding:8px 18px;border-radius:999px;background:var(--lt-l);color:var(--lt-k);font-weight:700;font-size:20px;box-shadow:0 3px 12px rgba(40,40,60,.18);opacity:0}
.lt-banner i{font-size:22px;color:var(--lt-b)}
.type-intro.go .lt-banner{animation:ltPill .4s ease-out .25s forwards}
.type-intro.ti-static .lt-banner{opacity:1}
@keyframes ltPill{0%{opacity:0;transform:translateY(12px)}100%{opacity:1;transform:translateY(0)}}
.lt-cap{margin-top:10px;color:var(--lt-k);background:rgba(255,255,255,.85);padding:4px 13px;border-radius:12px;font-size:15px;font-weight:600;opacity:0}
.type-intro.teach.go .lt-cap,.type-intro.ti-static .lt-cap{animation:ltFadeIn .4s ease-out .9s forwards}
.type-intro.ti-static .lt-cap{animation-delay:.15s}
@keyframes ltFadeIn{to{opacity:1}}
.lt-aff{position:absolute;bottom:11%;display:flex;align-items:center;gap:1px;color:var(--lt-k);font-size:22px;opacity:0}
.type-intro.ready .lt-aff{animation:ltAff 1.4s ease-in-out infinite}
@keyframes ltAff{0%,100%{transform:translateX(0);opacity:.55}50%{transform:translateX(5px);opacity:1}}
.type-intro.ti-static{animation:ltStatic 1.2s ease-in-out forwards}
@keyframes ltStatic{0%{opacity:0}16%{opacity:1}80%{opacity:1}100%{opacity:0}}
.type-intro.out{animation:ltFadeOut .25s ease-in forwards}
@keyframes ltFadeOut{to{opacity:0}}

/* demo primitives (§3.2) */
.lt-ghost{position:fixed;opacity:.5;z-index:107;pointer-events:none;transition:transform .36s ease-in,opacity .36s ease-in}
/* D1 teach caption — pinned under the board during a first-of-type demo (transform/opacity only) */
.lt-democap{position:fixed;transform:translate(-50%,4px);z-index:108;pointer-events:none;max-width:78vw;text-align:center;
  padding:6px 14px;border-radius:14px;background:rgba(40,40,60,.86);color:#fff;font-weight:700;font-size:14px;
  box-shadow:0 3px 12px rgba(20,20,35,.28);opacity:0;transition:opacity .32s ease,transform .32s cubic-bezier(.2,1.05,.4,1)}
.lt-democap.in{opacity:1;transform:translate(-50%,0)}
@media (prefers-reduced-motion:reduce){.lt-democap{transition:none}}
body.calm .lt-democap{transition:none}
.lt-peek{position:fixed;transform:translate(-50%,-50%);z-index:107;pointer-events:none;font-size:26px;opacity:0;animation:ltPeek 1s ease-in-out forwards}
@keyframes ltPeek{0%{opacity:0;transform:translate(-50%,-50%) scale(.6)}30%{opacity:1;transform:translate(-50%,-50%) scale(1)}70%{opacity:1;transform:translate(-50%,-50%) scale(1)}100%{opacity:0;transform:translate(-50%,-50%) scale(.7)}}
.critter.lt-stomp{animation:ltStomp .6s ease-out}
@keyframes ltStomp{0%{transform:scale(1)}30%{transform:scale(1.03) translateY(-2px)}62%{transform:scale(.99)}100%{transform:scale(1)}}
.lt-trace{position:fixed;border:3px solid;border-radius:16px;pointer-events:none;z-index:107;opacity:0;box-sizing:border-box}
.lt-trace.go{animation:ltTrace .8s ease-out forwards}
@keyframes ltTrace{0%{opacity:0;transform:scale(1.04)}30%{opacity:.85}100%{opacity:0;transform:scale(1)}}
/* P5-A: reversed CURSE cast — a wand sweeps the board diagonal; each critter card-flips (scaleX shrink → 0%
   positional impulse) as its arrow reverses in the wand's wake. Calm/reduced-motion skip both (wandCurse flips
   instantly). Wand peak scale 1.04 (≤1.06); one diagonal pass, no strobe. */
.critter.lt-flip{animation:ltFlip 250ms cubic-bezier(.5,0,.5,1)}
@keyframes ltFlip{0%{transform:scaleX(1)}48%{transform:scaleX(.06)}52%{transform:scaleX(.06)}100%{transform:scaleX(1)}}
.lt-wand{position:fixed;pointer-events:none;z-index:108;opacity:0}
.lt-wand.go{animation:ltWand 900ms ease forwards}
@keyframes ltWand{0%{opacity:0;transform:translate(-16%,-16%) rotate(-6deg) scale(.9)}30%{opacity:1}100%{opacity:0;transform:translate(16%,16%) rotate(-6deg) scale(1.04)}}
@media (prefers-reduced-motion:reduce){.critter.lt-flip{animation:none}.lt-wand.go{animation:none}}
body.calm .critter.lt-flip,body.calm .lt-wand.go{animation:none}

/* type-win beats (§4.5) — curse-break wash drain (cursed); boss handled by sparks + hop in JS */
.type-win.drain{position:fixed;inset:0;z-index:106;pointer-events:none;background:linear-gradient(0deg,transparent 0%,var(--lt-l) 60%,var(--lt-b) 100%);opacity:0}
.type-win.drain.go{animation:ltDrain .55s ease-in forwards}
@keyframes ltDrain{0%{opacity:.32;transform:translateY(0)}100%{opacity:0;transform:translateY(-24%)}}

/* living-board ambient layer (§4) — all transform/opacity, ≤2 concurrent enforced in JS */
.amb{position:absolute;pointer-events:none;z-index:2}
.amb-leaf{top:-8%;width:13px;height:13px;border-radius:0 60% 0 60%;background:var(--lt-l);opacity:.5;animation:ambFall 11s linear forwards}
.amb-petal{top:-8%;width:12px;height:16px;border-radius:60% 0 60% 0;background:var(--lt-l);opacity:.42;animation:ambFall 13s linear forwards}
@keyframes ambFall{0%{transform:translateY(-14px) rotate(0);opacity:0}12%{opacity:.5}90%{opacity:.42}100%{transform:translateY(360px) rotate(210deg);opacity:0}}
.amb-mote2{bottom:-6%;width:8px;height:8px;border-radius:50%;background:var(--lt-b);opacity:.5;box-shadow:0 0 6px var(--lt-l);animation:ambRise 9s ease-in forwards}
@keyframes ambRise{0%{transform:translateY(0);opacity:0}16%{opacity:.6}100%{transform:translateY(-330px);opacity:0}}
.amb-sparkle{top:20%;width:7px;height:7px;background:var(--lt-b);opacity:.5;transform:rotate(45deg);animation:ambDrift 9s ease-in-out forwards}
@keyframes ambDrift{0%{opacity:0;transform:translate(0,0) rotate(45deg)}22%{opacity:.6}100%{opacity:0;transform:translate(26px,42px) rotate(45deg)}}
.amb-mist{top:32%;left:-42%!important;width:60%;height:22px;border-radius:14px;background:var(--lt-l);opacity:0;filter:blur(3px);animation:ambMist 9s linear forwards}
@keyframes ambMist{0%{transform:translateX(0);opacity:0}25%{opacity:.28}100%{transform:translateX(190%);opacity:0}}
/* persistent-class ambients: shape edge-glow breathe + overgrown corner vine (opacity-only) */
.amb-edgeglow{animation:ambEdge 6s ease-in-out infinite}
@keyframes ambEdge{0%,100%{box-shadow:inset 0 0 0 3px var(--lt-glow-lo,rgba(143,111,208,.22)),0 0 8px var(--lt-glow-lo,rgba(143,111,208,.22))}50%{box-shadow:inset 0 0 0 3px var(--lt-glow-hi,rgba(143,111,208,.4)),0 0 18px var(--lt-glow-hi,rgba(143,111,208,.32))}}
.amb-vine::before{content:'';position:absolute;left:-2px;bottom:-2px;width:40px;height:40px;border-radius:0 60% 0 60%;background:var(--lt-l,rgba(120,180,90,.5));opacity:.4;pointer-events:none;transform-origin:bottom left;animation:ambVine 10s ease-in-out infinite;z-index:1}
@keyframes ambVine{0%,100%{transform:rotate(0)}50%{transform:rotate(4deg)}}

/* boss "breathing" idle — big piece only, released the instant it exits so the fling still works */
.board.ltype-boss .critter.big:not(.exit-R):not(.exit-L):not(.exit-U):not(.exit-D){animation:bossBreathe 4s ease-in-out infinite}
@keyframes bossBreathe{0%,100%{transform:scale(1)}50%{transform:scale(1.03)}}
body.calm .board.ltype-boss .critter.big,body.lowfx .board.ltype-boss .critter.big{animation:none}

/* reversed rescue afterglow — a brief purple halo on the exiting critter (opacity-only) */
.critter.lt-afterglow{box-shadow:0 0 12px 3px var(--lt-b,rgba(143,111,208,.6))}

@media (prefers-reduced-motion:reduce){
  .type-intro.go .lt-wash,.type-intro.go .lt-motif,.type-intro.ready .lt-aff,.lt-peek,.lt-trace.go,
  .amb,.amb-edgeglow,.amb-vine::before,.board.ltype-boss .critter.big,.critter.lt-stomp,.type-win.drain.go{animation:none}
  .ltype-badge.wiggle{animation:none}
}
/* Foggy curse: a hidden critter shows a calm "?" until its path is clear (static — calm ceiling) */
.critter.fogged::after{content:'?';position:absolute;inset:2px;background:rgba(206,212,224,.92);border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:700;color:#7b8598;z-index:6}
/* Friends on the map: faces of your friends who cleared a level (added/invited only) — tap to visit their map */
.lvl-friends{position:absolute;bottom:-13px;left:50%;transform:translateX(-50%);display:flex;align-items:center;z-index:4}
.lf-face{width:22px;height:22px;border-radius:50%;border:2px solid #fff;background:#eef3ea;overflow:hidden;padding:0;display:block;box-shadow:0 1px 3px rgba(50,70,40,.28);margin-left:-6px;cursor:pointer;transition:transform .12s ease}
.lf-face:first-child{margin-left:0}
.lf-face:hover{transform:translateY(-2px)}
.lf-face .lf-av,.lf-face .lf-av svg{display:block;width:100%;height:100%}
.lf-more{font-size:10px;font-weight:700;color:#6b7a63;background:#fff;border-radius:9px;padding:1px 5px;margin-left:3px;box-shadow:0 1px 3px rgba(50,70,40,.2)}

/* ============================================================================
   P8 — MENU-SCENE JUICE (RELEASE_REVIEW_PLAN §8). Within the CALM ceiling: scale
   ≤1.06 · small impulse · no flash >3Hz · one rise+decay (except I8's slow ambient
   "breathe", which JUICE.md §4 explicitly permits). Every beat is prefs()-gated in
   JS or guarded here by body.calm / prefers-reduced-motion. "Static" items = colour only.
   ============================================================================ */

/* Home hero (R3 mood · R5 CTA · I4 sign · I6 hold-to-pet) */
.home-hero{display:flex;align-items:center;gap:12px}
.hh-pet{flex:0 0 auto;width:58px;height:58px;position:relative;cursor:pointer;-webkit-user-select:none;user-select:none;touch-action:manipulation}
.hh-pet svg{display:block;width:100%;height:100%}
.hh-copy{min-width:0;flex:1}
.hh-pet.wave svg{transform-origin:50% 82%;animation:cubWaveBye .9s ease-in-out}   /* R3 wave — reuses the calm cub-wave keyframe */
.hh-pet.pet-bounce{animation:hhPet .34s cubic-bezier(.3,1.4,.4,1)}                /* I6 tap bounce (scale ≤1.05) */
@keyframes hhPet{0%,100%{transform:scale(1)}40%{transform:scale(1.05)}}
.pet-heart{position:absolute;left:50%;top:4px;transform:translateX(-50%);font-size:16px;color:#e36a86;pointer-events:none;z-index:3}
.hh-cta{display:inline-flex;align-items:center;gap:6px;padding:6px 13px;border-radius:999px;border:none;font-weight:800;font-size:13px;cursor:pointer;color:#3a2a12;background:var(--amber,#f2b134);box-shadow:0 2px 7px rgba(180,120,40,.25)}
.hh-cta i{font-size:15px}
.hh-cta-play{background:#5fae5f;color:#fff;box-shadow:0 2px 7px rgba(60,130,60,.28)}
.hh-cta-claim{background:#e6a32e;color:#4a3300}
.hh-cta:active{transform:scale(.96)}
@media (prefers-reduced-motion:reduce){.hh-pet.wave svg,.hh-pet.pet-bounce{animation:none}}
body.calm .hh-pet.wave svg{animation:none}
/* I4 welcome-back sign — static; motion lives only in the fade-in */
.hh-welcome{position:absolute;left:50%;bottom:100%;transform:translate(-50%,4px);white-space:nowrap;background:linear-gradient(180deg,rgba(255,248,236,.98),rgba(255,236,214,.98));color:#7a4a1e;font-weight:800;font-size:12px;padding:4px 10px;border-radius:999px;box-shadow:0 4px 12px rgba(180,120,60,.24),inset 0 0 0 1px rgba(220,150,90,.35);opacity:0;pointer-events:none;z-index:4;transition:opacity .3s ease,transform .3s cubic-bezier(.2,1.2,.3,1)}
.hh-welcome.in{opacity:1;transform:translate(-50%,-4px)}
.hh-welcome.out{opacity:0}
.hh-welcome i{font-size:13px;color:#e6a32e}
@media (prefers-reduced-motion:reduce){.hh-welcome{transition:none}}

/* I5 time-of-day header tint — a static var swap on .today-head */
.today-head[data-tod]{border-radius:14px;padding:6px 8px 2px;transition:background .6s ease}
.today-head[data-tod="dawn"]{background:linear-gradient(180deg,rgba(241,200,154,.20),transparent)}
.today-head[data-tod="day"]{background:linear-gradient(180deg,rgba(143,199,240,.13),transparent)}
.today-head[data-tod="dusk"]{background:linear-gradient(180deg,rgba(176,106,146,.18),transparent)}
.today-head[data-tod="night"]{background:linear-gradient(180deg,rgba(70,80,126,.20),transparent)}

/* I1 critter peek-in — motion only, below popups (#overlay z=50) */
.critter-peek{position:fixed;bottom:66px;width:70px;height:70px;z-index:30;pointer-events:none;opacity:.96;transition:transform .5s cubic-bezier(.2,.9,.3,1)}
.critter-peek svg{display:block;width:100%;height:100%;filter:drop-shadow(0 3px 6px rgba(50,70,40,.25))}
.critter-peek.left{left:0;transform:translateX(-105%)}
.critter-peek.right{right:0;transform:translateX(105%)}
.critter-peek.left.in{transform:translateX(-30%)}
.critter-peek.right.in{transform:translateX(30%)}
.critter-peek.left.out{transform:translateX(-105%)}
.critter-peek.right.out{transform:translateX(105%)}
@media (prefers-reduced-motion:reduce){.critter-peek{display:none}}

/* I2 paw-trail nav — opacity only, below popups */
.paw-nav{position:fixed;transform:translate(-50%,-50%);z-index:30;pointer-events:none;color:var(--amber,#f2b134);opacity:0;font-size:20px}
.paw-nav i{display:block}
.paw-nav.go{animation:pawFade .4s ease}
@keyframes pawFade{0%{opacity:0}40%{opacity:.5}100%{opacity:0}}
@media (prefers-reduced-motion:reduce){.paw-nav{display:none}}

/* I3 shooting star — one element, opacity + translate; tap = one sparkle (JS) */
.shooting-star{position:absolute;width:3px;height:3px;border-radius:50%;background:#fff;box-shadow:0 0 6px 1px rgba(255,255,255,.9);opacity:0;z-index:3;cursor:pointer}
.shooting-star::after{content:'';position:absolute;top:50%;width:32px;height:1.5px;transform:translateY(-50%)}
.shooting-star.from-right{right:8%}
.shooting-star.from-right::after{right:3px;background:linear-gradient(90deg,rgba(255,255,255,0),rgba(255,255,255,.8))}
.shooting-star.from-left{left:8%}
.shooting-star.from-left::after{left:3px;background:linear-gradient(90deg,rgba(255,255,255,.8),rgba(255,255,255,0))}
.shooting-star.from-right.go{animation:shootL 1.3s ease-in forwards}
.shooting-star.from-left.go{animation:shootR 1.3s ease-in forwards}
@keyframes shootL{0%{opacity:0;transform:translate(0,0)}14%{opacity:.95}100%{opacity:0;transform:translate(-240px,110px)}}
@keyframes shootR{0%{opacity:0;transform:translate(0,0)}14%{opacity:.95}100%{opacity:0;transform:translate(240px,110px)}}
@media (prefers-reduced-motion:reduce){.shooting-star{display:none}}

/* I9 seasonal map dressing — static */
.season-decor{position:absolute;transform:translate(-50%,-50%);font-size:26px;z-index:1;pointer-events:none;filter:drop-shadow(0 2px 3px rgba(50,70,40,.28))}

/* R8 level-node lock-dissolve (opacity + scale-down, one decay) */
.node-lock-dissolve{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#8a94a6;font-size:20px;pointer-events:none;z-index:3;animation:lockDissolve .6s ease-out forwards}
@keyframes lockDissolve{0%{opacity:.9;transform:scale(1)}100%{opacity:0;transform:scale(.4)}}
@media (prefers-reduced-motion:reduce){.node-lock-dissolve{display:none}}

/* R2 shop-purchase card confirm pulse (soft glow, no scale/flash) */
.shop-card.buy-pulse{animation:buyPulse .5s ease}
@keyframes buyPulse{0%,100%{box-shadow:0 2px 6px rgba(50,70,40,.10)}45%{box-shadow:0 0 0 3px rgba(95,174,95,.5),0 4px 14px rgba(95,174,95,.28)}}
@media (prefers-reduced-motion:reduce){.shop-card.buy-pulse{animation:none}}

/* R10 park Care-level-up dance — reuses the calm win-card cheer keyframe */
.rpk-dance{position:absolute;left:10px;bottom:8px;width:42px;height:42px;z-index:2;transform-origin:50% 82%;pointer-events:auto;cursor:pointer;animation:rpkDanceIn .3s cubic-bezier(.2,1.4,.4,1) both,wcCheer 1.05s cubic-bezier(.3,1.2,.4,1) .3s 2}
.rpk-dance svg{display:block;width:100%;height:100%;filter:drop-shadow(0 2px 4px rgba(180,90,110,.28))}
@keyframes rpkDanceIn{from{opacity:0;transform:translateY(8px) scale(.8)}to{opacity:1;transform:translateY(0) scale(1)}}
@media (prefers-reduced-motion:reduce){.rpk-dance{animation:none}}
body.calm .rpk-dance{animation:none}

/* I7 streak flame — colour warms by tier (static) */
.tc-ico.flame.st0{background:#a9b0a2}
.tc-ico.flame.st1{background:#f0a83a}
.tc-ico.flame.st2{background:#ef7a3a}
.tc-ico.flame.st3{background:#e8563a}

/* I8 breathing lives pill when FULL — ambient "board breathe" (JUICE.md §4); ~0.25Hz ≪ 3Hz, soft glow,
   gated off calm/lowfx/reduced-motion. (Box-shadow glow rather than an opacity pseudo because
   .hud-pill.lives has overflow:hidden, which would clip a pseudo-element glow.) */
.hud-pill.lives.full{animation:livesBreathe 4s ease-in-out infinite}
@keyframes livesBreathe{0%,100%{box-shadow:0 2px 6px rgba(50,70,40,.08),inset 0 0 0 1.5px rgba(43,163,155,.16)}50%{box-shadow:0 2px 11px rgba(43,163,155,.34),inset 0 0 0 1.5px rgba(43,163,155,.34)}}
body.calm .hud-pill.lives.full,body.lowfx .hud-pill.lives.full{animation:none}
@media (prefers-reduced-motion:reduce){.hud-pill.lives.full{animation:none}}

/* I10 badge-dot settle — appears via a soft drop+fade instead of popping in */
.nav-btn.has-dot::after,.nav-btn .nav-count,.hud-pill.link .link-count{animation:dotSettle .24s ease}
@keyframes dotSettle{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
@media (prefers-reduced-motion:reduce){.nav-btn.has-dot::after,.nav-btn .nav-count,.hud-pill.link .link-count{animation:none}}

/* R6 docked event strip — one quiet row: ≤3 live-event icons + countdown chips */
.event-strip{display:flex;align-items:center;gap:8px;padding:8px 12px;margin:0 0 12px;border-radius:14px;background:var(--card,#fffdf7);box-shadow:0 2px 6px rgba(50,70,40,.08),inset 0 0 0 1.5px rgba(230,163,46,.14)}
.event-strip .es-label{flex:0 0 auto;font-weight:800;font-size:13px;color:#8a6a1e;display:inline-flex;align-items:center;gap:5px;white-space:nowrap}
.event-strip .es-label>i{font-size:16px;color:#e6a32e}
.es-chips{flex:1;min-width:0;display:flex;align-items:center;gap:6px;overflow:hidden}
.es-chip{flex:0 0 auto;display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:999px;border:none;background:rgba(124,138,115,.10);cursor:pointer;font-size:11px;font-weight:700;color:#5a6a52}
.es-chip.ready{background:rgba(230,163,46,.18);color:#8a6a1e;box-shadow:inset 0 0 0 1.5px rgba(230,163,46,.4)}
.es-chip .es-emj{font-size:15px;line-height:1}
.es-chip .es-cd{display:inline-flex;align-items:center;gap:2px;font-variant-numeric:tabular-nums}
.es-chip .es-cd i{font-size:11px}
.es-chip:active{transform:scale(.95)}
.es-empty{font-size:12px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.es-more{flex:0 0 auto;font-size:18px}

/* R4 quest replay — the just-completed quest checks off once, then collapses */
.quest-replay{display:flex;align-items:center;gap:8px;margin:8px 0 2px;padding:8px 10px;border-radius:12px;background:rgba(95,174,95,.12);max-height:0;opacity:0;overflow:hidden;transition:max-height .3s ease,opacity .3s ease,padding .3s ease,margin .3s ease}
.quest-replay.go{max-height:48px;opacity:1}
.quest-replay.out{max-height:0;opacity:0;padding-top:0;padding-bottom:0;margin:0}
.qr-check{flex:0 0 auto;width:22px;height:22px;border-radius:50%;background:#5fae5f;color:#fff;display:flex;align-items:center;justify-content:center}
.qr-check i{font-size:14px;animation:qrCheck .5s cubic-bezier(.2,1.4,.4,1)}
.qr-txt{font-weight:700;font-size:13px;color:#3f6b3f}
@keyframes qrCheck{0%{transform:scale(0);opacity:0}60%{transform:scale(1.15)}100%{transform:scale(1);opacity:1}}
@media (prefers-reduced-motion:reduce){.quest-replay{transition:none}.qr-check i{animation:none}}

/* ===== v3 core-loop (CORE_LOOP_PLAN.md): telegraph badges · Rest Stop · Willow's Basket · Rescue Road ===== */
.ltype-badge.ltype-hard{background:#e8a33a}
.lvl-node.wall-node .ltype-badge.ltype-hard{background:#d96f3f}
.lvl-node.hard-node{box-shadow:0 0 0 3px rgba(232,163,58,.40),0 2px 8px rgba(50,70,40,.18)}
.up-ahead .rr-mini{display:flex;align-items:center;gap:6px;color:#b07c2e;font-weight:700}
.rr-pips{display:flex;gap:3px}
.rr-pip{width:8px;height:8px;border-radius:50%;background:#d8dbd0;display:inline-block}
.rr-pip.on{background:#e8a33a}
.hard-chip{display:flex;align-items:center;justify-content:center;gap:6px;margin:0 auto 8px;padding:4px 12px;border-radius:999px;background:#f6e3c0;color:#8a5a17;font-weight:800;font-size:13px;width:max-content}
.hard-chip.wall{background:#f3d3c2;color:#8a3d17}
.pack-basket{display:flex;align-items:center;gap:10px;background:#eef4e4;border:1px solid #d5e3c2;border-radius:12px;padding:8px 12px;margin:0 0 10px;text-align:left}
.pack-basket .pb-ic{font-size:20px}
.pack-basket .pb-tx{flex:1;display:flex;flex-direction:column;line-height:1.25}
.pack-basket .pb-sub{font-size:12px;color:#6b7760}
.pack-basket .pb-gifts{display:flex;gap:6px}
.pack-basket .pb-gift{display:flex;align-items:center;gap:2px;background:#fff;border-radius:8px;padding:3px 7px;font-size:12px;font-weight:800;color:#3f7d4e}
/* ws 0: the row still shows, muted — a player on the base pool needs the way UP to be visible */
.pack-basket.pb-empty{background:#f4f5f0;border-color:#e2e5da}
.pack-basket.pb-empty .pb-ic{opacity:.5}
.pack-basket .pb-sub{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.pack-basket .pb-up{background:#fff;border-radius:6px;padding:1px 6px;font-weight:800;color:#3f7d4e}
/* the dealt mis-tap pool — the one number base-1 makes worth telegraphing before the board opens */
.pack-hearts{display:flex;align-items:center;justify-content:center;gap:6px;margin:0 0 10px;font-size:13px;font-weight:700;color:#8a5a63}
.pack-hearts i{font-size:16px;color:#e8607a}
.ls-streak{display:flex;align-items:center;gap:6px;justify-content:center;font-size:13px;color:#a3721f;margin:6px 0 0}
/* The free refill, made visible: the misread this fixes is "I'm buying back the hearts I lost" — they
   always come back. The inner .mistakes span carries the pip styling (rules are scoped to that class). */
.rest-hearts{display:flex;flex-direction:column;align-items:center;gap:2px;margin:10px 0 2px}
.rest-hearts .rh-cap{font-size:12px;color:#8a948a;font-weight:600}
.rest-preview{display:flex;align-items:center;justify-content:center;gap:10px;margin:10px 0 2px;font-weight:700;color:#556052}
.rest-preview .rp-lbl{font-size:12px;color:#8a948a;font-weight:600;text-transform:uppercase;letter-spacing:.04em}
.rest-preview .rp-ct{display:flex;align-items:center;gap:3px;background:#eef1e6;border-radius:8px;padding:3px 8px;font-size:13px}
.next-tease{display:inline-flex;align-items:center;gap:4px;margin-left:8px;padding:2px 8px;border-radius:999px;background:rgba(255,255,255,.25);font-size:12px;font-weight:800}
.next-tease.hard{background:rgba(232,163,58,.30)}

/* ── Watchful Owl (class A gaze texture — engine._applyOwls / ui.js owlWatches) ──
   Calm contract: the cone is an opacity-only breathe at ≈0.4Hz (far under the 3Hz flash ceiling),
   the hoot peaks at scale 1.06 (the ceiling), and everything is pointer-events:none decoration. */
.slot.wall.owl-slot{cursor:pointer}
.slot.wall.owl-slot svg{inset:8%;opacity:1}
.owl-cone{position:absolute;z-index:1;pointer-events:none;border-radius:10px;
  background:linear-gradient(180deg,rgba(240,196,90,.30),rgba(240,196,90,.10));
  animation:owlBreathe 2.4s ease-in-out infinite;
  transition:left .45s ease,top .45s ease,width .45s ease,height .45s ease}
.owl-cone.dir-U{background:linear-gradient(0deg,rgba(240,196,90,.30),rgba(240,196,90,.10))}
.owl-cone.dir-L{background:linear-gradient(270deg,rgba(240,196,90,.30),rgba(240,196,90,.10))}
.owl-cone.dir-R{background:linear-gradient(90deg,rgba(240,196,90,.30),rgba(240,196,90,.10))}
@keyframes owlBreathe{0%,100%{opacity:.72}50%{opacity:1}}
.owl-cone.owl-pulse{animation:owlPulse .5s ease}
@keyframes owlPulse{35%{opacity:1;filter:brightness(1.12)}}
body.lowfx .owl-cone{animation:none}
@media (prefers-reduced-motion:reduce){.owl-cone{animation:none;transition:none}}
.slot.wall.owl-slot.tool-target{outline:3px solid #ffd76a;outline-offset:-3px;border-radius:12px;
  box-shadow:0 0 12px rgba(255,205,90,.5)}
.owl-hoot{animation:owlHoot .36s ease}
@keyframes owlHoot{35%{transform:scale(1.06) rotate(-3deg)}70%{transform:scale(1.02) rotate(2deg)}}

/* ═══════════════════════════════════════════════════════════════════════════════
   LOW-FX GAME-FLOW PASS (2026-07-19) — measured on a Nokia 7.1 / Adreno 509.
   Ground truth from `dumpsys gfxinfo` on the shipped WebView build (not a desktop
   proxy): hub scrolling is fine (1% janky, p90 11ms), but in-level play ran 91%
   janky with a p50 of 48ms, and level entry hit a p99 of 550ms. Cost is paint and
   composite, not JS — screen rebuilds measured 0-7ms.

   Everything here is DECORATION ONLY under body.lowfx (the device tier: native
   WebView or <=4GB RAM, i.e. every Capacitor/store player). No gameplay signal, no
   affordance and no readability is removed — where a cue was carried by motion, a
   static equivalent replaces it rather than leaving nothing behind.

   These live at the END of the sheet deliberately: several gates tie on specificity
   with their source rule, so source order is what makes them win. Do not relocate
   them into the earlier lowfx block without re-checking each one.
   ═══════════════════════════════════════════════════════════════════════════════ */

/* --- in-play, per-frame or per-tap (the frames the player actually feels) --- */

/* charged booster: an infinite scale loop in the dock for most of every stage (with
   the Fox companion, from frame 0). The ring + recoloured count still read "ready". */
body.calm .tool.booster.charged,body.lowfx .tool.booster.charged{animation:none}

/* tap-ring: box-shadow SPREAD keyframes on every single rescue — spread is paint,
   never composited, on the hottest path in the game. With the animation off the box-shadow
   rests at its 0% state (0 0 0 0) and paints nothing visible, so removing the pseudo-element
   outright is visually identical AND one fewer paint box per rescue. Measured on the Nokia 7.1
   as part of the tap-overlay set below. */
body.lowfx .slot.tap-ring::after{animation:none;display:none}

/* Tap-time decorative overlays, gated on the DEVICE tier only.
   On-device A/B (Adreno 509, 3 rescues per condition, identical fresh 24-critter board):
       overlays on : p90 85ms · p95 150ms · p99 300ms
       overlays off: p90 73ms · p95  93ms · p99 150ms
   The p99 halves. These are the discrete tail spikes — the resting board is free (4 frames/12s)
   and the mounted scene composites at 5ms/frame, so everything felt in play is spawned by a tap.
   warm-trail is a gradient ::after painted behind a moving element; zpuff is a particle. Both are
   pure decoration, so the low tier loses nothing legible.
   NOT gated here, deliberately: .float-coin (that is REWARD FEEDBACK — the reward has to visibly
   land, per the reward-delivery rule) and the critter run animation itself (that is the mechanic). */
body.lowfx .critter.warm-trail::after{display:none}
body.lowfx.in-game .zpuff{display:none}
/* NB: .slot.blocker-flash is deliberately NOT gated. It is the only feedback for a
   mis-tap — a failure signal, not decoration — and it is a one-shot on an error path. */

/* overgrown boards: an infinite rotate mounted for the whole level. */
body.lowfx .amb-vine::before,body.calm .amb-vine::before{animation:none}

/* --- bracketing play (thermal load carried INTO the board) --- */

/* stage intro: a full-viewport backdrop-filter over a viewport-sized animating SVG,
   ~2.6s before every staged entry. The raised alpha keeps text contrast identical —
   nobody perceives a 1.5px blur at that opacity. */
body.lowfx .stage-intro{-webkit-backdrop-filter:none;backdrop-filter:none;background:rgba(58,90,55,.62)}
body.lowfx .si-fill{animation:none;width:100%}
body.lowfx .si-count.pop{animation:none}
body.lowfx .si-card .ltype-banner{animation:none}   /* source is (0,2,0): a bare body.lowfx .ltype-banner would win only on a tiebreak */

/* time-pot courier: a SECOND ungated full-viewport backdrop-filter, twice per level. */
body.lowfx .tpot-dim{-webkit-backdrop-filter:none!important;backdrop-filter:none!important;background:rgba(46,74,52,.42)}
/* The two backdrop-filters the earlier sweep missed, both found by on-device measurement 2026-08-08:
   dumpsys gfxinfo on a Snapdragon 636 put 15.4ms of a 27ms frame in the GPU phase while layout was
   0.1ms and paint 0.3ms -- GPU-bound, not paint-bound, so the fix is fewer/cheaper composited layers.

   .water-hud is the expensive one: unlike the two dims above it is not a one-shot overlay, it is
   PERMANENTLY on screen for the whole of every water level AND it sits directly over the animating
   board. A backdrop-filter there makes the compositor read back the framebuffer and blur it on every
   frame the tiles move, for the entire level. .moon-chip is the same shape on the map.
   Both keep their look via a more opaque background -- the blur is what costs, not the tint. */
body.lowfx .water-hud{-webkit-backdrop-filter:none!important;backdrop-filter:none!important;background:rgba(255,255,255,.86)}
body.lowfx .moon-chip{-webkit-backdrop-filter:none!important;backdrop-filter:none!important;background:rgba(34,42,78,.72)}
body.lowfx .tpot .tp-jar{filter:none}
body.lowfx .tp-drop{box-shadow:none}   /* .tp-drop mounts on document.body, so the existing `body.lowfx .tpot *` descendant rule never reaches it */
body.lowfx .tpot{animation-duration:.18s!important}   /* the existing `body.lowfx .tpot *` uses a DESCENDANT combinator and so misses .tpot's own hop-in/zip-off */
/* Do NOT write animation:none on .tpot — it is parked off-screen at translateY(24vh + 110%)
   and tpHopIn/tpZipOff are `forwards`; killing the animation strands the jar off-screen. */

/* pause: an animated filter:blur across the whole board + dock on every pause open. */
body.lowfx.paused .pz-stage,body.lowfx.paused .pz-dock,
body.calm.paused .pz-stage,body.calm.paused .pz-dock{filter:none!important;transition:none!important;opacity:.55}

/* --- post-play and the resume screen (battery + thermal, not felt in-frame) --- */

/* win card: up to FIVE infinite dances on the most-shown card in the game.
   The !important is LOAD-BEARING — source .win-critters.st3.go .wc-v0 is (0,4,0) and
   this gate is (0,3,1), so it loses on class count without it. Do not "clean it up". */
body.lowfx .win-critters .wc{animation:none!important}

/* "you are here" map ring: the existing gate at the top of this sheet kills the
   animation, but the base rule sets opacity:0 and the keyframe is what makes the ring
   visible — so on the device tier the affordance vanished entirely while every desktop
   tester still saw it. Restore it statically. */
body.lowfx .lvl-node.current::after{opacity:.55}

/* first-special coach: infinite box-shadow spread, ~6.5s during play. The static ring
   matters — a dozing critter carrying .coach-glow otherwise gets no ring at all. */
body.lowfx .coach-glow{animation:none;box-shadow:0 0 0 3px rgba(154,143,224,.85)}

/* --- phantom compositor layers promoted for animations lowfx already killed --- */
body.lowfx .sky-sun,body.lowfx .sky-moon,body.lowfx .sky-cloud,
body.lowfx .sky-bird,body.lowfx .map-hills{will-change:auto}
body.calm .ev-fx,body.lowfx .ev-fx{animation:none;opacity:.92;transform:none;will-change:auto}
body.calm .ev-fx-sky,body.lowfx .ev-fx-sky{animation:none;transform:none;will-change:auto}

/* Combo rail: the warmth glow is a box-shadow whose BLUR RADIUS is driven by --warm, so every
   combo step repaints it over a .3s transition. Keep the colour shift (it carries the warmth
   read) but drop the blur ramp on the device tier — a blurred shadow is paint, never composited. */
body.lowfx .combo-rail.on .combo-bar{box-shadow:inset 0 0 0 1px rgba(70,90,55,.06),0 0 3px rgba(242,177,52,calc(.12 + var(--warm,0)*.42));transition:none}

/* ===== ranger identity on boards (LEADERBOARD_UX_PLAN P1) =====
   The own row now prints the player's REAL name; the small YOU tag keeps the row
   scannable, and the ✎ chip is the standing (never nagging) way into the name sheet
   for anyone who chose "Maybe later". Both are quiet by design — no colour shout. */
/* The name is the point of the row, so it takes the leftover space and everything beside it is
   flex:none. Without this the <bdi> shrank to 0px on the own row — the ✎ chip alone ate all 82px of
   .lb-nm and the player's name rendered as nothing at all (owner 2026-07-28). */
.lbrow .lb-nm bdi{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.lb-you{flex:none;font-size:9.5px;font-weight:900;letter-spacing:.6px;text-transform:uppercase;padding:2px 5px;border-radius:5px;background:var(--amber,#f2b134);color:#5a3d05;line-height:1.1}
.lb-setname{flex:none;display:inline-flex;align-items:center;gap:3px;font:inherit;font-size:10.5px;font-weight:800;padding:2px 7px;border-radius:999px;border:1px dashed var(--amber,#f2b134);background:transparent;color:var(--amber-d,#d6920f);cursor:pointer}
.lb-setname i{font-size:12px}
/* the own row's tap-to-name affordance — a dashed underline on the name itself, so it costs the
   name column nothing (the chip it replaces was 81px of an 82px cell) */
.lbrow .lb-nm-set{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer;text-decoration:underline dashed var(--amber,#f2b134);text-underline-offset:3px}
.lb-row .lb-setname{margin-left:5px}
.dz-wait{text-align:center;padding:14px 0;font-size:13px}

/* the name sheet — one claim surface for every door into it (§2.2/§2.3) */
.nsheet{text-align:center}
.nsheet .ns-av{width:64px;height:64px;margin:0 auto 6px;border-radius:50%;display:grid;place-items:center;background:var(--card-2,#f3f1e8);border:2px solid var(--line)}
.nsheet .ns-av svg{width:52px;height:52px}
.nsheet .ns-h{margin:2px 0 4px;font-size:19px}
.nsheet .ns-sub{font-size:13px;margin:0 0 10px}
.ns-in{width:100%;padding:11px 12px;border-radius:12px;border:1.5px solid rgba(0,0,0,.12);font-size:15px;font-family:inherit;text-align:center}
.ns-err{min-height:17px;font-size:12.5px;color:#c0563f;margin-top:6px}
.nsheet .proto-note{margin-top:8px}
.cust-claim{display:flex;align-items:center;gap:5px;font-size:11.5px;font-weight:700;color:#9aa7b0;margin-top:4px}
.cust-claim.on{color:var(--green-d,#3f8a47)}
.lbrow-plain{grid-template-columns:var(--lb-rk) var(--lb-av) minmax(var(--lb-nm),1fr) auto}
.lbrow-plain .lb-dl,.lbrow-plain .lb-rw{display:none}

/* ===== P2: flag column · pacer disclosure · practice-cup tag (LEADERBOARD_UX_PLAN D3/D8/D9) =====
   The flag cell is FIXED-WIDTH and renders empty when a player never picked one, so names stay
   column-aligned whether or not their neighbours flagged themselves. */
.lb-flag{flex:none;width:20px;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1}
.lb-flag:empty{width:20px}
.lb-flag .flag-code{font-size:.55em;min-width:1.6em}
.lb-pacer{flex:none;font-size:10px;opacity:.62;line-height:1}
.lb-src{margin:6px 2px 0;font-size:11.5px;color:var(--muted,#7d8a74);text-align:center}
.tag.cup-practice{background:var(--card-2,#f3f1e8);color:var(--muted,#7d8a74);font-size:11px;vertical-align:middle}

/* ===== P3: the professional board kit (LEADERBOARD_UX_PLAN §6) =====
   Sticky own-row. Pure CSS: inside any .lb-stick list YOUR row pins to the bottom of the scroll
   port instead of scrolling away, and it cannot escape its own board (the list is the containing
   block). It keeps the amber ring it always had — only a soft lift marks it as floating.
   BOTTOM edge only, deliberately: when both top and bottom are set CSS gives `top` precedence and
   the row never pins at all. Bottom is also the edge that matters — the leaders are above you, so
   the player scrolls up to read who is ahead, and their own standing (with its "+N to pass" line)
   stays on screen the whole time. */
.lb-stick .lb-mine{position:sticky;bottom:2px;z-index:3;border-radius:12px;
  /* OPAQUE while floating: the resting .you tint is 14% amber, which let the rows underneath read
     straight through the pinned copy. Same colour, composited over the card surface instead. */
  background:linear-gradient(rgba(242,177,52,.14),rgba(242,177,52,.14)), var(--card,#fff);
  box-shadow:0 -3px 10px rgba(50,70,40,.10)}
.lb-stick .lb-mine .lbrow.you{position:static;background:none;box-shadow:inset 0 0 0 2px var(--amber,#f2b134)}
.lb-mine .lb-pass{margin:0 10px 6px}
/* a board that renders the own row unwrapped (no pass line to carry) still pins the bare row */
.lb-stick > .lbrow.you{position:sticky;bottom:2px;z-index:3;
  background:linear-gradient(rgba(242,177,52,.14),rgba(242,177,52,.14)), var(--card,#fff);
  box-shadow:inset 0 0 0 2px var(--amber,#f2b134), 0 -3px 10px rgba(50,70,40,.10)}
/* the two board notes: what to do next, and how big the field really is */
.lb-pass{margin:2px 2px 6px;font-size:12px;font-weight:700;color:var(--green-d,#3f8a47);display:flex;align-items:center;gap:4px}
.lb-pass .ti{font-size:13px}
.lb-thin{margin:6px 2px 0;font-size:11.5px;color:var(--muted,#7d8a74);text-align:center}
.lb-empty{padding:18px 8px;text-align:center;font-size:13px}
/* settle chip — the board's own clock, quiet next to its heading */
.lb-headline{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:2px 0 6px}
.lb-headline-t{font-size:12px;font-weight:700;color:var(--muted,#7d8a74)}
.lb-settle{margin-inline-start:auto;display:inline-flex;align-items:center;gap:4px;font-size:11.5px;color:var(--muted,#7d8a74);background:var(--card-2,#f3f1e8);border-radius:999px;padding:3px 9px;white-space:nowrap}
.lb-settle b{font-variant-numeric:tabular-nums;color:var(--ink,#3c4a37)}
/* Town Gallery rows are canonical .lbrow now — the wrapper only carries the tap target */
.gal-open{cursor:pointer;border-radius:12px}
.gal-open:active{background:rgba(95,174,95,.08)}
/* the invite ladder lives directly on the page (no .card behind it), so its canonical rows carry
   the card surface themselves — the own-row keeps its amber ring rather than the plain shadow */
.lb-list .lbrow{background:#fff;border:1px solid var(--line);border-radius:14px;padding:0 10px;box-shadow:var(--shadow)}
.lb-list .lbrow.you{background:rgba(242,177,52,.14);box-shadow:inset 0 0 0 2px var(--amber,#f2b134)}
.lb-list .lbrow + .lbrow{margin-top:0}
.lb-list .lb-pass{margin:0 4px}
/* row-track variants: only the columns that carry something are laid out, so the NAME track keeps
   the rest. Compound rules come after the singles — equal specificity, later wins. */
.lbrow-nodl{grid-template-columns:var(--lb-rk) var(--lb-av) minmax(var(--lb-nm),1fr) auto var(--lb-rw)}
.lbrow-norw{grid-template-columns:var(--lb-rk) var(--lb-av) minmax(var(--lb-nm),1fr) auto var(--lb-dl)}
.lbrow-nodl.lbrow-norw{grid-template-columns:var(--lb-rk) var(--lb-av) minmax(var(--lb-nm),1fr) auto}
.lbrow-nodl.lbrow-wt{grid-template-columns:var(--lb-rk) var(--lb-av) minmax(var(--lb-nm),1fr) auto minmax(64px,auto)}
/* A board inside a popup is ALWAYS narrow: .popup is hard-capped at max-width:330px on every
   screen size. Keying the compact furniture off the VIEWPORT meant a 430px phone gave the row the
   full-size rank chip, avatar and 8px gaps inside the very same 330px box — the wider the phone,
   the less name you could read. The constraint is the container, so the rule follows the container.
   (Boards rendered straight onto the page do get the screen's width, and keep the query below.) */
.popup .lbrow{--lb-rk:30px;--lb-av:36px;--lb-dl:26px;--lb-rw:30px;--lb-nm:88px;gap:6px}
.popup .lbrow .lb-rk{width:28px;height:28px;font-size:13px}
.popup .lbrow .lb-av{width:34px;height:34px}
.popup .lbrow .lb-av .av,.popup .lbrow .lb-av svg{width:30px;height:30px}
.popup .lbrow .lb-bar{width:38px}
.popup .lbrow .lb-val{min-width:38px}
.popup .lb-you{font-size:8.5px;letter-spacing:.4px;padding:2px 4px}
/* narrow phones: every piece of fixed furniture gives a few px back to the NAME — at 375px a
   6-column row had 34px of name left, which is no name at all */
@media (max-width:400px){
  .lbrow{--lb-rk:30px;--lb-av:36px;--lb-dl:26px;--lb-rw:30px;--lb-nm:88px;gap:6px}
  .lbrow .lb-rk{width:28px;height:28px;font-size:13px}
  .lbrow .lb-av{width:34px;height:34px}
  .lbrow .lb-av .av,.lbrow .lb-av svg{width:30px;height:30px}
  .lbrow .lb-bar{width:38px}
  .lbrow .lb-val{min-width:38px}
  .lb-you{font-size:8.5px;letter-spacing:.4px;padding:2px 4px}
}
/* 320px (SE-class): the empty flag cell is reserved so names stay column-aligned across rows, but
   at this width it is 20px of a 64px name. Legibility outranks alignment here — flagged rows still
   reserve their own 20px, so only boards where NOBODY set a flag lose the column, and those are
   aligned anyway. Takes the name 64px → 84px, which is the difference between a name and a stub. */
@media (max-width:340px){ .lbrow .lb-flag:empty{width:0} }

/* Ranger's Toolbox (ECONOMY_V3 §2.2): the day's free ephemeral tool reads as a gift, not as stock.
   Calm-safe — a static tint + ring, no pulse, no flash. */
.tool.free-tool{background:rgba(124,195,107,.14);box-shadow:inset 0 0 0 2px rgba(124,195,107,.55)}
.tool.free-tool .cnt{color:#4e8f42;font-weight:700}

/* ============================================================================
   Critters tab — one button grammar + one hero number
   ----------------------------------------------------------------------------
   Rule S2 (verb · cost → outcome) and S1 (one hero per unit) from the tab audit.
   These live HERE, not in park.js's injected <style>, per rule S10: no feature
   file owns a palette. They read the :root token scale, so a theme change moves
   them. park.js's existing injected block is pre-existing debt, tracked separately.
   ========================================================================== */

/* -- the gain-first action button ------------------------------------------
   The old shape led with the PRICE and buried the outcome in a 10.5px sub-line,
   so a real upgrade ("+3 ❤/s") read as "pay 147 for nothing". Gain leads at
   display size; price sits opposite it as a chip; the before→after is detail. */
.rpk-btn2.rpk-btn-gain{display:flex;flex-direction:column;align-items:stretch;gap:2px;
  text-align:start;padding-block:10px}
.rpkb-gainrow{display:flex;align-items:baseline;justify-content:space-between;gap:10px;width:100%}
.rpkb-gain{display:inline-flex;align-items:baseline;gap:4px;font-size:var(--fs-lg);font-weight:800;
  line-height:1.15;font-variant-numeric:tabular-nums}
.rpkb-gain-u{font-size:var(--fs-sm);font-weight:700;opacity:.8}
.rpkb-gain-k{font-size:var(--fs-md)}
.rpkb-price{display:inline-flex;align-items:center;gap:3px;font-size:var(--fs-sm);font-weight:800;
  font-variant-numeric:tabular-nums;background:rgba(60,50,20,.13);border-radius:999px;
  padding:2px 9px;white-space:nowrap}
.rpk-btn2.rpk-btn-gain .rpkb-l1{font-size:var(--fs-sm);font-weight:700;opacity:.92;display:block}
.rpk-btn2.rpk-btn-gain .rpkb-out{font-size:var(--fs-xs);font-weight:600;opacity:.75;line-height:1.3}

/* -- Hall of Paw: a SHARE of the one Love Rate, never a rival total ---------
   A count (509) and a scoped rate (1,034 ❤/s) used to sit as siblings while a
   third, larger global rate lived on another card. The count leads; the rate
   states what it is a share of and draws the proportion. */
.rpk-hh-tier{font-size:var(--fs-sm);font-weight:700;color:var(--muted);margin-top:2px;
  display:flex;flex-wrap:wrap;gap:5px;align-items:baseline}
.rpk-hh-tier b{color:#c65b74}
.rpk-hh-next{font-weight:600;font-variant-numeric:tabular-nums}
.rpk-hh-share{display:block;width:100%;margin-top:10px;padding:0;background:none;border:none;
  text-align:start;cursor:pointer}
.rpk-hh-sharebar{display:block;height:7px;border-radius:4px;background:rgba(198,91,116,.16);
  overflow:hidden;margin-bottom:5px}
.rpk-hh-sharebar i{display:block;height:100%;border-radius:4px;
  background:linear-gradient(90deg,#ffc2cd,#c65b74);transition:width .4s ease}
.rpk-hh-sharetxt{font-size:var(--fs-sm);font-weight:600;color:var(--ink);opacity:.85;line-height:1.35}
.rpk-hh-share:active{transform:scale(.995)}
.rpk-hh-share:focus-visible{outline:2px solid #c65b74;outline-offset:3px;border-radius:6px}

/* the tier medal carries its progress to the next rung as a conic ring (S5) */
.rpk-medal{position:relative}
.rpk-medal::after{content:'';position:absolute;inset:-3px;border-radius:50%;pointer-events:none;
  background:conic-gradient(#c65b74 var(--rung,0%),rgba(198,91,116,.18) 0);
  -webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 3px),#000 calc(100% - 3px));
  mask:radial-gradient(farthest-side,transparent calc(100% - 3px),#000 calc(100% - 3px))}

/* the next, unearned rung — a goal, not a receipt */
.rpk-mile-next{opacity:.5;font-weight:600}

/* -- Critter Town: one action row, equal basis (rule S3 + S7) ---------------
   All three buttons were already .btn-sm; the ragged widths came from a plain
   flex row letting the longest LABEL decide, so "Invite friends to visit"
   wrapped to two lines — and it breaks differently in each of 16 locales.
   Equal basis + a single-line ellipsis makes the row locale-proof. */
.town-me-actions{display:flex;gap:8px;margin-top:8px}
.town-me-actions .btn{flex:1 1 0;min-width:0;padding-inline:8px;gap:5px}
.town-me-actions .btn span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.town-me-actions .btn i{flex:none}

/* -- Gallery house rules (rule S7) -----------------------------------------
   What is ranked · how to enter · what you win · where you stand. The board
   used to state only its title and reset time, so "what is here?" had no
   answer on the screen that owed one. */
.gal-rules{display:grid;gap:6px;background:var(--card);border:1px solid var(--line);
  border-radius:var(--r);padding:11px 13px;margin:0 0 12px}
.gal-rule{display:grid;grid-template-columns:auto minmax(0,1fr);gap:9px;align-items:start;
  font-size:var(--fs-sm);color:var(--ink);line-height:1.4}
.gal-rule i{font-size:15px;color:var(--muted);margin-top:1px}
.gal-rule-me{border-top:1px dashed var(--line);padding-top:7px;margin-top:1px;font-weight:700}
.gal-rule-me i{color:var(--green-d)}

/* ============================================================================
   The Living Boost — diorama.js scene (CRITTERS_TAB_REDESIGN Wave 3)
   ----------------------------------------------------------------------------
   Five layers, one 3.2s four-beat loop, retimed per animal by two modifier
   classes (entry edge + verb). Calm ceiling, as hard numbers:
     scale ≤ 1.05 (ceiling 1.06) · no positional impulse on the actor
     ≤ 6 motes (ceiling 8) · nothing flashes · no haptic
   The background wash IS the timer: --drain is the remaining fraction, so the
   colour bleeds out of the scene as the visit expires.
   ========================================================================== */
@property --drain{syntax:'<percentage>';inherits:true;initial-value:100%}

.dio{position:relative;height:86px;margin:6px 0 2px;border-radius:var(--r-md);overflow:hidden;
  background:linear-gradient(170deg,rgba(191,227,245,.30),rgba(238,247,223,.45));
  contain:paint}
.dio>*{position:absolute}

/* z0 — the wash, masked by remaining time. Both mask properties: WebKit-only webkit-mask would
   silently show a full wash on Firefox, i.e. a timer that never moves. */
.dio-bg{inset:0;background:
  radial-gradient(circle at 24% 78%,var(--tint,#f2b6c6) 0,transparent 52%),
  radial-gradient(circle at 76% 26%,var(--tint,#f2b6c6) 0,transparent 48%);
  opacity:.55;
  /* feathered edge — a hard line reads as a progress bar, and the scene is meant to look like the
     colour is leaving, not like a second meter got drawn on top of the first */
  -webkit-mask-image:linear-gradient(to right,#000 0 calc(var(--drain,100%) - 7%),transparent var(--drain,100%));
  mask-image:linear-gradient(to right,#000 0 calc(var(--drain,100%) - 7%),transparent var(--drain,100%));
  transition:-webkit-mask-image .9s linear,mask-image .9s linear}
[dir="rtl"] .dio-bg{
  -webkit-mask-image:linear-gradient(to left,#000 0 calc(var(--drain,100%) - 7%),transparent var(--drain,100%));
  mask-image:linear-gradient(to left,#000 0 calc(var(--drain,100%) - 7%),transparent var(--drain,100%))}

/* z1 — prop */
.dio-prop{inset-inline-start:50%;bottom:8px;transform:translateX(-50%);font-size:30px;opacity:.30;
  filter:saturate(.55)}
/* z2 — actor. Breathes only; never moves position (impulse budget spent on the tool instead). */
.dio-actor{inset-inline-start:50%;bottom:10px;width:52px;height:52px;margin-inline-start:-26px;
  transform-origin:50% 100%;animation:dioBreathe 3.2s ease-in-out infinite}
.dio-actor svg{width:100%;height:100%;display:block}
@keyframes dioBreathe{
  0%,100%{transform:scale(1)}
  19%{transform:scale(1.02)}
  44%{transform:scale(1.05,.96)}    /* the reaction squash — at, not over, the calm ceiling */
  62%{transform:scale(1.01)}
}
/* z3 — tool: enters, does the verb, leaves. Entry edge and verb are separate modifiers so eight
   animals reuse three entries × three verbs instead of eight bespoke timelines. */
.dio-tool{font-size:26px;animation-duration:3.2s;animation-iteration-count:infinite;
  animation-timing-function:cubic-bezier(.22,.9,.28,1)}
.dio-side .dio-tool{bottom:34px;inset-inline-start:-18%;animation-name:dioEnterSide}
.dio-top  .dio-tool{top:-18%;inset-inline-start:44%;animation-name:dioEnterTop}
.dio-below .dio-tool{bottom:-18%;inset-inline-start:44%;animation-name:dioEnterBelow}
@keyframes dioEnterSide{
  0%{inset-inline-start:-18%;opacity:0;transform:rotate(-16deg)}
  18%{inset-inline-start:30%;opacity:1;transform:rotate(-8deg)}
  84%{inset-inline-start:30%;opacity:1}
  96%,100%{inset-inline-start:-18%;opacity:0;transform:rotate(-16deg)}
}
@keyframes dioEnterTop{
  0%{top:-18%;opacity:0}
  18%{top:16%;opacity:1}
  84%{top:16%;opacity:1}
  96%,100%{top:-18%;opacity:0}
}
@keyframes dioEnterBelow{
  0%{bottom:-18%;opacity:0}
  18%{bottom:22%;opacity:1}
  84%{bottom:22%;opacity:1}
  96%,100%{bottom:-18%;opacity:0}
}
/* the verb rides on top of the entry, as a second animation on the same element */
.dio-stroke .dio-tool{animation-name:dioEnterSide,dioStroke}
.dio-top.dio-dip .dio-tool{animation-name:dioEnterTop,dioDip}
.dio-below.dio-dip .dio-tool{animation-name:dioEnterBelow,dioDip}
.dio-side.dio-drift .dio-tool{animation-name:dioEnterSide,dioDrift}
.dio-top.dio-drift .dio-tool{animation-name:dioEnterTop,dioDrift}
.dio-below.dio-drift .dio-tool{animation-name:dioEnterBelow,dioDrift}
@keyframes dioStroke{
  0%,18%,84%,100%{transform:rotate(-8deg)}
  30%{transform:rotate(7deg)}42%{transform:rotate(-8deg)}
  54%{transform:rotate(7deg)}66%{transform:rotate(-6deg)}
}
@keyframes dioDip{
  0%,18%,84%,100%{transform:translateY(0)}
  34%{transform:translateY(7px)}48%{transform:translateY(0)}
  60%{transform:translateY(5px)}72%{transform:translateY(0)}
}
@keyframes dioDrift{
  0%,100%{transform:translateX(0) rotate(-4deg)}
  40%{transform:translateX(6px) rotate(4deg)}
  70%{transform:translateX(-4px) rotate(-2deg)}
}
/* z4 — motes */
.dio-fx i{position:absolute;bottom:38px;font-size:13px;opacity:0;font-style:normal;
  animation:dioMote 3.2s ease-out infinite}
@keyframes dioMote{
  0%,48%{opacity:0;transform:translateY(0) scale(.6)}
  60%{opacity:.9;transform:translateY(-12px) scale(1)}
  100%{opacity:0;transform:translateY(-34px) scale(.85)}
}

/* Off-screen, hidden tab, or over the two-scene cap: hold a static frame. The wash still drains,
   so a paused card is never a card that lies about its timer. */
.dio.dio-off *,.dio.dio-capped *{animation-play-state:paused !important}
/* Reduced motion / calm: one resting frame, zero motion, drain intact — the state change stays
   VISIBLE, which is the whole point of the layer. */
.dio-still .dio-actor,.dio-still .dio-tool,.dio-still .dio-fx i{animation:none !important}
.dio-still.dio-side .dio-tool{inset-inline-start:30%;opacity:1;transform:rotate(-6deg)}
.dio-still.dio-top .dio-tool{top:16%;opacity:1}
.dio-still.dio-below .dio-tool{bottom:22%;opacity:1}
@media (prefers-reduced-motion:reduce){
  .dio .dio-actor,.dio .dio-tool,.dio .dio-fx i{animation:none !important}
  .dio.dio-side .dio-tool{inset-inline-start:30%;opacity:1}
  .dio.dio-top .dio-tool{top:16%;opacity:1}
  .dio.dio-below .dio-tool{bottom:22%;opacity:1}
}

/* ============================================================================
   RANK LISTS — rule S4: a board is a LIST, and a list gets its container's width

   The name column was being starved by CHROME, not by its own row. Measured on a
   375px phone, the love board handed the name 39px — enough for four characters —
   while 146px of the screen went to nested padding before the row even began:

     375 viewport → .popup caps at 330 → −40 popup padding → −32 card padding
                  → −12 row padding → 230px of row, and five fixed tracks in it.

   Every previous pass at this fought inside those 230px (shrink the rank chip,
   shrink the avatar, drop empty tracks) and every one of them ran out of room,
   because the pixels were never in the row. So: a board stops paying for padding
   that exists to keep PROSE off the edges. It bleeds out to its container's edge
   and carries its own rhythm. The name goes 39px → 97px, and the row keeps its
   card surface — only the gutters go.

   --pad-i is declared BESIDE the padding it mirrors, so the two cannot drift: a
   future change to .popup's padding moves the bleed with it automatically.
   ============================================================================ */
.popup{--pad-i:20px}
.card{--pad-i:16px}
.lb-bleed,
.popup > .lb-stick,.popup > .lb-list,.popup > .race-shell,
.card > .lb-stick,.card > .lb-list{margin-inline:calc(-1 * var(--pad-i,0px))}
/* a board that IS the card (class="card lb-stick") keeps its surface but drops the prose gutters —
   the rows have their own 6px, which is the rhythm a list wants */
.popup > .card.lb-stick,.popup > .card.lb-list{padding-inline:4px}
/* the sticky own-row and the board's notes live inside that new full-bleed box, so give them back
   the gutter the rows don't want */
.popup > .card.lb-stick > .lb-src,.popup > .card.lb-stick > .lb-thin,
.popup > .card.lb-stick > .lb-headline{padding-inline:8px}

/* Race lanes: the name lived in a hard 96px chip — 60px after the avatar and gap, which cut every
   real name ("MeadowRanger" needs 88px, "کوهنورد کوچک" needs 75px). The chip is now a share of the
   lane, so the name grows with the screen while the runway keeps the majority. */
.race-lane{grid-template-columns:clamp(112px,46%,176px) 1fr}
.race-nm bdi{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* the runway still keeps the majority of the lane (135px of 263px at 375) — the chip only takes
   what a name actually needs, and the avatar gives up 2px on narrow phones to pay for it */
@media (max-width:400px){
  .race-av{width:28px;height:28px}
  .race-av svg{width:24px;height:24px}
}

/* Town Gallery podium: three flex:1 pods with min-width:auto could not shrink below their own
   content, so a long name pushed the row wider than the card instead of ellipsising inside it. */
.gal-pod{min-width:0}

/* Meadow helpers (trade.js) and the mural co-painters (mural.js): neither name span had a shrink
   guard, so a long name walked the tally off the right edge. Same contract as every other rank row. */
.hp-ldr .lrow .lrow-nm,
.mural-row .mural-nm{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* Win derby: same floor as .lbrow, so the ceremony row can never be the one board that starves. */
.win-derby .dby-row{grid-template-columns:38px 30px minmax(var(--lb-nm,88px),1fr) auto}
