/* ===== Number Wilds ===== */
* { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html, body { height:100%; overflow:hidden; }
body {
  font-family: "SF Pro Rounded", "Nunito", "Arial Rounded MT Bold", ui-rounded, system-ui, sans-serif;
  background:#0d1b2a; color:#fff7e8; user-select:none; -webkit-user-select:none; touch-action:manipulation;
  overscroll-behavior:none;   /* iOS: no page rubber-band behind the game */
}
/* pinch-zoom stays enabled (a11y); manipulation kills the 300ms double-tap-zoom on controls */
button, .node, .token, #camp, #trailBoard, #verTag { touch-action:manipulation; }
/* keyboard operability: world objects are real buttons, so give them a visible focus ring */
:focus-visible { outline:3px solid #ffd166; outline-offset:2px; border-radius:8px; }
.node:focus-visible, .token:focus-visible, #camp:focus-visible, #trailBoard:focus-visible { outline-offset:4px; }
button { font-family:inherit; border:none; cursor:pointer; background:none; color:inherit; }
.hidden { display:none !important; }
#buildUpdate {
  position:fixed; z-index:1200; right:max(12px, env(safe-area-inset-right));
  top:max(12px, env(safe-area-inset-top)); min-height:44px; max-width:260px;
  padding:10px 16px; border-radius:999px; background:#ffd166; color:#2f2414;
  font-size:14px; font-weight:1000; box-shadow:0 4px 0 #b77b27, 0 8px 20px #0006;
}

/* 100vh first so older Safari has a fallback for 100dvh */
#app { height:100vh; height:100dvh; display:grid; grid-template-rows:auto auto 1fr auto auto auto auto; grid-template-columns:minmax(0,1fr); }
#app > * { min-width:0; }

/* ===== HUD ===== */
#hud {
  display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap;
  padding:8px 12px; background:#122438; border-bottom:2px solid #1e3a55; z-index:5;
  padding-top:calc(8px + env(safe-area-inset-top));
  padding-left:calc(12px + env(safe-area-inset-left)); padding-right:calc(12px + env(safe-area-inset-right));
}
.hud-group { display:flex; align-items:center; gap:8px; }
#resBar { gap:12px; padding-inline:4px; }
#rankBadge { font-size:18px; background:#1e3a55; border-radius:999px; padding:4px 10px; }
#rankBadge b { font-size:16px; margin-left:2px; }
#xpBar { width:72px; height:10px; background:#0d1b2a; border-radius:999px; overflow:hidden; border:1px solid #2c5378; }
#xpFill { height:100%; width:0%; background:linear-gradient(90deg,#ffd166,#ff9f1c); border-radius:999px; transition:width .4s; }
.res {
  font-size:16px; background:#1e3a55; border-radius:12px; padding:4px 9px; color:#fff7e8;
  display:flex; align-items:center; gap:4px; transition:transform .15s;
  min-height:44px; min-width:44px; justify-content:center;   /* touch target */
}
.res b { font-size:15px; }
.resIcon { width:25px; height:25px; object-fit:contain; display:block; }
.res.pop { animation:resPop .32s ease-out; }
@keyframes resPop {
  0%, 100% { filter:brightness(1); }
  45% { filter:brightness(1.45); }
}
#resBerry {
  gap:6px; padding-inline:10px; font-weight:900;
  background:linear-gradient(#5278d5,#334f9f); border:2px solid #cbe4ff;
  box-shadow:0 3px 0 #20346d, 0 0 0 2px rgba(203,228,255,.12);
}
#resBerry:active { transform:translateY(2px); box-shadow:0 1px 0 #20346d; }
#resBerry.needsFood { border-color:#ffd166; box-shadow:0 0 0 3px rgba(255,209,102,.45), 0 3px 0 #20346d; }
.resAction {
  border-radius:999px; padding:2px 6px; background:#9bdc4b; color:#173b17;
  font-size:9px; font-weight:1000; letter-spacing:.08em; line-height:1.25;
}
#resBerry.needsFood .resAction { background:#ffd166; color:#4a2400; }
#dayInfo { font-size:16px; background:#1e3a55; border-radius:12px; padding:4px 10px; }
/* persistent DEV marker (docs/spec-developer-unlock.md). It is also the way
   back into the panel: it looks tappable, so it has to BE tappable — and
   re-tapping ten times on the sun to reach QA controls is friction nobody
   wants mid-test. Real button, real touch target. */
#devBadge {
  font-size:10px; font-weight:1000; letter-spacing:.08em; color:#3a1400;
  background:linear-gradient(#ffb37a,#ff8a3d); border-radius:999px; padding:4px 8px;
  box-shadow:0 2px 0 #b85a1f;
  min-width:44px; min-height:44px; display:inline-flex; align-items:center; justify-content:center;
}
#devBadge.hidden { display:none; }
.iconBtn {
  font-size:18px; padding:4px 6px; border-radius:10px;
  min-width:44px; min-height:44px; display:inline-flex; align-items:center; justify-content:center;
}
.iconBtn:active { transform:scale(.9); }

/* ===== hunger ===== */
#hungerRow { display:flex; align-items:center; gap:8px; padding:4px 14px; background:#122438; z-index:5; }
#hungerIcon { font-size:15px; }
#hungerBar { flex:1; max-width:220px; height:12px; background:#0d1b2a; border-radius:999px; overflow:hidden; border:1px solid #2c5378; }
#hungerFill { height:100%; width:80%; border-radius:999px; background:linear-gradient(90deg,#8ac926,#c5e063); transition:width .5s, background .5s; }
#hungerFill.low { background:linear-gradient(90deg,#ff5d5d,#ff9f1c); }
#hungerHint { font-size:11px; opacity:.55; }

/* ===== scene ===== */
#scene {
  /* Camp starts as ordinary grass. The Sinkhole is a live foreground
     landmark, so it can genuinely appear only after the Deep unlocks. */
  --scene-art:url("assets/backgrounds/camp-hub-grass.webp");
  --scene-tint:linear-gradient(transparent, transparent);
  position:relative; overflow:hidden; background:#6e9b56;
}
#sky {
  position:absolute; inset:0;
  background-color:#6e9b56;
  background-image:var(--scene-tint), var(--scene-art);
  background-position:center; background-size:cover; background-repeat:no-repeat;
  transition:filter 2s;
}
#scene.dusk {
  --scene-tint:linear-gradient(rgba(31,35,74,.18), rgba(120,62,30,.28));
}
#scene.night {
  --scene-tint:linear-gradient(rgba(3,9,25,.62), rgba(5,20,25,.72));
}
#scene.dusk #sky { filter:brightness(.8) saturate(.86); }
#scene.night #sky { filter:brightness(.48) saturate(.62); }
#stars { position:absolute; inset:0; opacity:0; transition:opacity 2s; pointer-events:none;
  background-image:radial-gradient(1.5px 1.5px at 12% 18%, #fff 60%, transparent),
    radial-gradient(1px 1px at 32% 8%, #fff 60%, transparent),
    radial-gradient(1.5px 1.5px at 55% 22%, #fff 60%, transparent),
    radial-gradient(1px 1px at 72% 12%, #fff 60%, transparent),
    radial-gradient(1.5px 1.5px at 88% 28%, #fff 60%, transparent),
    radial-gradient(1px 1px at 45% 32%, #fff 60%, transparent); }
#scene.night #stars { opacity:.9; }
#hills { position:absolute; left:-5%; right:-5%; bottom:14%; height:34%; pointer-events:none;
  background:radial-gradient(60% 100% at 22% 100%, #2e5d43 60%, transparent 61%),
             radial-gradient(55% 90% at 62% 100%, #285239 60%, transparent 61%),
             radial-gradient(65% 80% at 95% 100%, #2e5d43 60%, transparent 61%);
  opacity:0; transition:filter 2s; }
#ground { position:absolute; left:0; right:0; bottom:0; height:16%;
  background:linear-gradient(#4a7c46, #3b6339 40%, #2f4f2e); opacity:0; transition:filter 2s; }
#scene.dusk #hills, #scene.dusk #ground { filter:brightness(.85) saturate(.9); }
#scene.night #hills, #scene.night #ground { filter:brightness(.5) saturate(.6); }

/* camp */
#camp { --camp-w:205px; --camp-h:116px; position:absolute; left:1%; bottom:8%; width:var(--camp-w); height:var(--camp-h); line-height:1; cursor:pointer; z-index:3;
  min-width:44px; min-height:44px; padding:0; }
#campTent { position:absolute; left:0; bottom:0; width:112px; height:96px; object-fit:contain; transition:transform .2s; }
#camp:active #campTent { transform:scale(.92); }
#campLogs { position:absolute; right:4px; bottom:0; width:62px; height:38px; object-fit:contain; }
#campFire { position:absolute; right:0; bottom:0; width:72px; height:72px; object-fit:contain; opacity:0; transition:opacity .8s; }
#campFire.lit { opacity:1; animation:flicker 1s infinite alternate; }
@keyframes flicker { from { transform:scale(1); } to { transform:scale(1.12) rotate(-2deg); } }
.campBubble { position:absolute; top:-18px; left:50%; transform:translateX(-50%); }
/* hidden until a recipe is actually affordable — see renderCampBubble().
   Id-scoped so it outranks .bubble's own display, which is declared further down. */
#campBubble { display:none; }
#campBubble.ready { display:inline-flex; }

/* Camp Hub (docs/spec-camp-hub.md) — the three biome exits and the reserved
   Sinkhole corner. Positions are written inline from hub.js's normalized
   anchors (0..1) as left/top percentages of #scene, so they are exact
   fractions of the scene box and cannot drift across rotation or resize. */
/* ABOVE the camp furniture, not below it. The exits are the only way out of
   camp; the tent, the trail board and the tool rack are scenery. At z-index 2
   the scenery won — when the 2.0 art grew #camp to 205x116px, it and the trail
   board covered the River sign completely, and tapping "River" opened the Trail
   Board instead. A whole biome, and with it the capstone, was unreachable, and
   nothing failed: every hub test asserted anchors and gating, which were both
   still correct. The painted-map layout now spreads services across the scene,
   but navigation still has to outrank every decorative layer.
   Navigation outranks decoration — art can be resized freely from now on. */
#hubLayer { position:absolute; inset:0; pointer-events:none; z-index:4; }
#scene.zonedeep #hubLayer { display:none; }   /* exits only make sense back at camp */
.hubExit {
  position:absolute; transform:translate(-50%, -50%); pointer-events:auto; text-align:center;
  min-width:44px; min-height:44px; padding:6px 12px; border-radius:16px; cursor:pointer;
  display:flex; flex-direction:column; align-items:center; justify-content:center; font-weight:800; font-size:15px;
  box-shadow:0 3px 0 rgba(0,0,0,.25), 0 4px 9px rgba(0,0,0,.25); border:2px solid #fff;
}
.hubExit.blocked { opacity:.75; filter:saturate(.55); }
.hubExit-meadow.palette-meadow { background:radial-gradient(#eaffc7, #b7e66a); color:#3a5316; }
.hubExit-forest.palette-forest { background:radial-gradient(#d4f2d8, #4f9d6a); color:#173a22; }
.hubExit-river.palette-river { background:radial-gradient(#d7f0ff, #5fb6e0); color:#0c3a52; }
/* The Sinkhole is the one exit whose button is bigger than anything it draws:
   a transparent 190x142 box holding a 132x104 pinhole with a label pill below.
   That box does NOT scale with the viewport -- 17% of the scene at 1280px, 48%
   at 375px -- so at phone widths its invisible right margin covered the River
   sign at x 0.57. Tapping "River" opened the Deep instead, and the Equal-Groups
   capstone sits behind River: unreachable on a phone.

   Same shape as the Bramble Bridge, and the same fix, which is the rule the hub
   should hold generally: the thing you can see is the thing you can tap. The
   box still positions and lays out the landmark; it just stops answering for
   it. Narrowing a hit area can strand a control, so hit-routing.spec.js pins
   both halves -- River is reachable AND the Sinkhole still opens. */
.hubExit-sinkhole.palette-sinkhole {
  width:190px; height:142px; min-width:0; min-height:0; padding:0;
  background:transparent; color:#241338; border:0; box-shadow:none;
  pointer-events:none;
}
.hubExit-sinkhole .hubLandmarkArt,
.hubExit-sinkhole .hubExitName { pointer-events:auto; cursor:pointer; }
.hubExit-pond.palette-pond { background:radial-gradient(#d2f2ff, #4fa8d8); color:#0b3550; }
.hubExit-dock.palette-dock { background:radial-gradient(#ffe9c2, #e0973f); color:#432405; }
.hubExitName { white-space:nowrap; }
.hubRouteArt { width:86px; height:54px; object-fit:contain; margin:-29px -18px -5px; pointer-events:none; }
.hubRouteSign { width:62px; height:52px; }
.hubLandmarkArt { width:98px; height:70px; object-fit:contain; margin:-42px -22px -4px; pointer-events:none; }
.pondLandmarkArt { width:116px; }
.supplyBoatLandmarkArt { width:142px; height:94px; margin:-62px -34px -5px; }
.hubExit-pond .hubLandmarkArt {
  display:none; /* painted into the Camp map; do not reserve a second invisible row above the label */
}
.hubExit-sinkhole .hubLandmarkArt {
  position:absolute; left:50%; top:50%; width:132px; height:104px; margin:0;
  transform:translate(-50%, -50%); opacity:1;
  filter:drop-shadow(0 4px 5px rgba(0,0,0,.35));
}
.hubExit-sinkhole .hubExitName {
  position:absolute; left:50%; top:calc(50% + 38px); transform:translateX(-50%);
  width:max-content; min-height:44px; padding:6px 13px; border-radius:999px;
  display:inline-flex; align-items:center; justify-content:center;
  background:radial-gradient(#e6d7ff, #8a63c9); border:2px solid #fff;
  box-shadow:0 3px 0 rgba(0,0,0,.25), 0 4px 9px rgba(0,0,0,.25);
  line-height:1; white-space:nowrap;
}
.supplyBoatModalArt { display:block; width:min(100%, 360px); max-height:230px; object-fit:contain; margin:auto; }

/* Number Pond reveal (docs/spec-number-pond.md): the first fish unfolds the
   whole Pond, later fish just swim into it. JS only adds these classes when
   prefers-reduced-motion does NOT match, so the reduced-motion alternative is
   simply: no animation, same end state, same modal explanation. */
@keyframes pondUnfold {
  0% { transform:translate(-50%, -50%) scale(0) rotate(-10deg); opacity:0; }
  60% { transform:translate(-50%, -50%) scale(1.15) rotate(3deg); opacity:1; }
  100% { transform:translate(-50%, -50%) scale(1) rotate(0); }
}
.hubExit-pond.pondUnfold { animation:pondUnfold 1.1s ease-out; }
@keyframes pondArrive {
  0% { transform:translate(-50%, -50%) scale(.6); opacity:.5; }
  60% { transform:translate(-50%, -50%) scale(1.2); opacity:1; }
  100% { transform:translate(-50%, -50%) scale(1); }
}
.hubExit-pond.pondArrive { animation:pondArrive .65s ease-out; }

/* River dock unfold (docs/spec-equal-groups-expedition.md "Completion
   payoff"): a one-time reveal when the capstone completes, reusing the
   Pond's own unfold timing/easing rather than inventing a second animation
   language for the same "a landmark just appeared" moment. */
.hubExit-dock.dockUnfold { animation:pondUnfold 1.1s ease-out; }

/* fish cards (tap the Pond) — plain-language stamina, never pips/×N/fractions */
.fishCardRow { display:flex; flex-wrap:wrap; gap:10px; justify-content:center; margin:10px 0; }
.fishCard {
  min-width:44px; min-height:44px; padding:8px 14px; border-radius:14px; border:2px solid #fff;
  background:#eaf6ff; color:#0b3550; font-weight:800; cursor:pointer;
  display:flex; flex-direction:column; align-items:center; gap:2px;
}
.fishCard.resting { background:#fff3d6; color:#5a3d0a; }
.fishCard.tired { opacity:.55; }
.fishCardNum { font-size:20px; line-height:1; }
.fishCardState { font-size:10px; text-transform:uppercase; letter-spacing:.04em; }
.fishCardStamina { font-size:12px; }

/* nodes */
/* min-HEIGHT as well as min-width: the number bubble used to be in the flow and
   padded the button out to a comfortable height by accident. It is positioned
   above the sprite now (see .node .bubble), so without this floor a 40px emoji
   scaled to .72 on a phone leaves a 29px tap target. flex-end keeps the sprite
   standing on the ground line while the extra height grows upward. */
.node { position:absolute; bottom:11%; transform:translateX(-50%); text-align:center; cursor:pointer; z-index:2; transition:filter .3s;
  min-width:44px; min-height:44px; display:flex; align-items:flex-end; justify-content:center; }
.nodeSprite { display:inline-block; line-height:1; transition:transform .15s; font-size:var(--sz, 40px); }
.nodeSpriteArt { width:var(--sz, 70px); height:var(--sz, 70px); }
.nodeArt { display:block; width:100%; height:100%; object-fit:contain; object-position:center bottom; filter:drop-shadow(0 3px 2px rgba(0,0,0,.25)); }
.node:active .nodeSprite { transform:scale(.93); }
.node.selected .nodeSprite { animation:bounce .6s infinite alternate; }
.node.shake .nodeSprite { animation:shake .4s; }
.node.falling { pointer-events:none; }
.node.falling .nodeSprite { transition:transform .7s, opacity .7s; transform:rotate(78deg) translateY(10px); opacity:0; }
.gateBridgeNode { top:36%; bottom:auto; z-index:2; }
.brambleBridgeNode .nodeArt { opacity:0; filter:none; } /* use the bridge painted into the map */
/* The sprite is invisible here (the bridge is painted into the map art), so its
   130x130 box was an invisible click zone — a third of the width of a 375px
   scene, catching taps aimed at the open ground and the river beside it. Every
   other landmark's target is the thing you can see. So the node itself stops
   taking clicks and the LABEL is the whole control; the sprite still lays out
   normally, it just no longer intercepts. Do not "fix" this by shrinking the
   sprite: --sz also positions the label, which is anchored to bottom:100% of it. */
.brambleBridgeNode { pointer-events:none; }
.brambleBridgeLabel { background:#f5e3b6; color:#533a1f; font-size:14px; }
/* The pill is under the 44px touch floor that .node's min-height was providing
   before this became label-only. Same ::before trick #trailBoard uses, and the
   inset is invisible so the sign keeps its drawn size.

   -11px, not -8px, and the phone is why: the media query below drops this label's
   font-size, so the pill measures 28px tall at desktop but only 23px at 375px.
   -8px cleared 44 on the desktop measurement and left 39px on a phone — the
   smaller sign needs the BIGGER inset, which is the opposite of what picking the
   number off a desktop screenshot suggests. 23 + 2x11 = 45. */
.brambleBridgeLabel::before { content:""; position:absolute; inset:-11px -6px; }
.returnCampNode { bottom:10.5%; }
.returnCampNode .nodeSpriteArt { width:148px; height:111px; }
.returnCampNode .nodeArt { filter:drop-shadow(0 4px 3px rgba(0,0,0,.28)); }
.riverLaunchNode { bottom:19%; }
@keyframes bounce { from { transform:translateY(0); } to { transform:translateY(-7px); } }
@keyframes shake { 0%,100% { transform:translateX(0);} 25% { transform:translateX(-6px);} 75% { transform:translateX(6px);} }
/* A node's number bubble is a LABEL, not a second tap target. It is far wider
   than the 44px sprite it belongs to — "7 ●●●●●●●" runs about 100px — so it
   overhangs whatever stands beside it and swallows taps meant for its neighbour.
   Caught at camp during the authored first minute, where the starter tree's
   bubble was taking the top-left corner of the River sign; the same overhang
   exists between any two close nodes in a biome. The sprite keeps the whole
   44px target (see the touch-target tests) — only the label stops intercepting.

   pointer-events alone was not enough: the bubble is a CHILD of the .node
   button, so its width was the button's width. Lifting it out of the flow (the
   same absolute-above-the-sprite trick #campBubble already uses) puts .node
   back to its 44px sprite box, which is what the touch-target floor is
   measuring in the first place. renderNodes() emits no <br> for this reason. */
.node .bubble {
  position:absolute; bottom:100%; left:50%; transform:translateX(-50%);
  margin-bottom:4px; pointer-events:none;
}
/* …EXCEPT the two labels that are NAMES OF PLACES rather than numbers: "Bramble
   Bridge" and "Camp". The rule above exists because a NUMBER bubble overhangs
   its own sprite and steals taps from whatever stands beside it. But on the camp
   hub every other landmark — Meadow, River, Forest, Sinkhole, Number Pond, Dock —
   is a hub exit whose label IS its button, so the screen teaches "tap the words".
   These two were the only labels where the words were dead: the whole 166x28
   "Bramble Bridge" pill returned #sky, and the bridge read as going nowhere.

   Safe to make live because .node sits at z-index 2, BELOW #camp/#trailBoard (3)
   and #hubLayer (4): where a label overlaps a biome exit the exit still wins, so
   this can only ADD reachable area, never take a tap from another control. That
   ordering is the actual invariant here — check it before raising .node's
   z-index. Number bubbles stay inert; that fix is still load-bearing. */
.node .bubble.navLabel { pointer-events:auto; cursor:pointer; }
.bubble {
  display:inline-flex; align-items:center; gap:3px; background:#fff7e8; color:#4a2e19;
  border-radius:999px; padding:3px 11px; font-weight:800; font-size:19px;
  box-shadow:0 2px 6px rgba(0,0,0,.35); white-space:nowrap;
}
.bubble .lockIcon { font-size:13px; opacity:.7; }
.bubble.matched { background:#c8f7c5; outline:3px solid #6fd66f; animation:bounce .5s infinite alternate; }
.bubble.newBadge { background:#ffe08a; animation:bounce .5s infinite alternate; }
.runeBoundNode .nodeSprite { filter:drop-shadow(0 0 8px rgba(255,205,80,.9)); }
.bubble.runeBoundBubble { flex-direction:column; gap:1px; border:2px solid #d99a29; background:#fff4c9; color:#56380c; line-height:1.05; }
.runeBoundBubble small { font-size:8px; font-weight:900; letter-spacing:.06em; }
.sparklingNode .nodeSprite { filter:drop-shadow(0 0 9px rgba(119,230,255,.95)); }
.bubble.sparklingBubble { flex-direction:column; gap:1px; border:2px solid #37b7d1; background:#e9fbff; color:#07556a; line-height:1.05; }
.sparklingBubble small { font-size:8px; font-weight:900; letter-spacing:.06em; }
.dots { font-size:8px; letter-spacing:1.5px; display:block; color:#8a5a2b; line-height:1; margin-top:1px; }

/* tokens */
.token {
  position:absolute; transform:translateX(-50%); z-index:2; cursor:pointer;
  width:44px; height:44px; border-radius:14px; background:transparent url("assets/ui/number-chip.png") center/contain no-repeat;
  color:#7a4514; font-weight:900; font-size:22px; display:flex; align-items:center; justify-content:center;
  filter:drop-shadow(0 3px 3px rgba(0,0,0,.32)); border:0;
  animation:tokenBob 2.2s ease-in-out infinite;
}
@keyframes tokenBob { 0%,100% { margin-bottom:0; } 50% { margin-bottom:7px; } }
.token:active { transform:translateX(-50%) scale(.88); }

/* Chip uses normalized transparent frames so every pose shares one stable
   baseline and collision footprint. The image is decorative; #scene and its
   semantic controls continue to own all accessible interaction. */
#avatar {
  --chip-size:80px;
  position:absolute; bottom:10.5%; left:8%; transform:translateX(-50%);
  width:var(--chip-size); height:var(--chip-size);
  display:flex; align-items:flex-end; justify-content:center;
  z-index:3; pointer-events:none; transition:filter .5s;
}
#avatarSprite {
  display:block; width:100%; height:100%; object-fit:contain; object-position:center bottom;
  transform-origin:center bottom; user-select:none; -webkit-user-drag:none;
}
#avatar.walking #avatarSprite { animation:chipStep .28s ease-in-out infinite alternate; }
#avatar.flip #avatarSprite { transform:scaleX(-1); }
#avatar.flip.walking #avatarSprite { animation:chipStepFlip .28s ease-in-out infinite alternate; }
#avatar.weak { filter:saturate(.5) brightness(.85); }
#avatar.chopping #avatarSprite { animation:chopAnim .3s; }
@keyframes chipStep { from { transform:translateY(0);} to { transform:translateY(-3px);} }
@keyframes chipStepFlip { from { transform:scaleX(-1) translateY(0);} to { transform:scaleX(-1) translateY(-3px);} }
@keyframes chopAnim { 0% { transform:rotate(0);} 50% { transform:rotate(-28deg);} 100% { transform:rotate(0);} }

/* fire button */
#btnFire {
  position:absolute; left:50%; top:12px; transform:translateX(-50%);
  background:#ff9f1c; color:#4a2400; font-weight:900; font-size:16px;
  border-radius:999px; padding:9px 18px; box-shadow:0 3px 0 #c47407; z-index:4;
  animation:bounce .6s infinite alternate;
}

/* fx */
#fxLayer { position:absolute; inset:0; pointer-events:none; z-index:6; }
.floatTxt { position:absolute; font-weight:900; font-size:20px; transform:translate(-50%,-50%); animation:floatUp 1.1s forwards; text-shadow:0 2px 4px rgba(0,0,0,.4); }
@keyframes floatUp { from { opacity:1; margin-top:0; } to { opacity:0; margin-top:-52px; } }
.flyer { position:fixed; z-index:60; font-size:24px; transition:transform .65s cubic-bezier(.4,-0.2,.7,1.1), opacity .65s; pointer-events:none; }
.confetti { position:absolute; top:-12px; width:9px; height:14px; border-radius:2px; animation:confFall 1.6s linear forwards; }
@keyframes confFall { to { transform:translateY(105vh) rotate(560deg); opacity:.6; } }
.sparkle { position:absolute; font-size:17px; transform:translate(-50%,-50%); animation:sparkleOut .7s forwards; }
@keyframes sparkleOut {
  from { opacity:1; scale:.5; }
  to { opacity:0; scale:1.6; translate:var(--dx) var(--dy); }
}

/* ===== solve bar ===== */
#solveBar {
  background:#122438; border-top:2px solid #1e3a55; padding:7px 12px; min-height:56px; z-index:5;
  padding-left:calc(12px + env(safe-area-inset-left)); padding-right:calc(12px + env(safe-area-inset-right));
}
#taskLabel { font-size:15px; font-weight:700; opacity:.9; padding:2px 0 4px; }
#taskLabel .cancelHint { opacity:.5; font-weight:400; font-size:12px; }
#exprRow {
  display:flex; align-items:center; gap:8px; background:#0d1b2a; border-radius:14px;
  padding:7px 14px; font-size:25px; font-weight:900; min-height:46px;
  flex:1 1 320px; max-width:620px; min-width:0;
}
#exprText { letter-spacing:2px; }
#exprText:empty::before { content:"tap your numbers…"; font-size:14px; font-weight:400; opacity:.4; letter-spacing:0; }
#exprVal { margin-left:auto; color:#ffd166; }
#exprVal.match { color:#6fd66f; animation:bounce .4s infinite alternate; }
#exprWrap { display:flex; gap:8px; align-items:stretch; justify-content:flex-start; }
.solveBtns { display:flex; gap:8px; }
.solveBtns button { border-radius:12px; font-weight:900; min-width:44px; min-height:44px; }
#btnUndo, #btnCancel { background:#1e3a55; width:44px; font-size:18px; }
#btnGo {
  background:#3c5d3c; color:#a8c6a8; font-size:16px; min-width:80px; width:auto; padding:0 12px;
  display:inline-flex; align-items:center; justify-content:center; flex:0 0 auto;
  transition:all .2s; white-space:nowrap;
}
#btnGo.ready { background:linear-gradient(#7ed957,#4caf50); color:#123312; box-shadow:0 3px 0 #2e7d32; animation:bounce .45s infinite alternate; }
#btnGo:disabled { cursor:default; }

/* ===== belt: pouch + runes ===== */
#beltRow {
  display:flex; align-items:center; gap:10px; padding:8px 12px 12px; background:#122438; z-index:5;
  padding-left:calc(12px + env(safe-area-inset-left)); padding-right:calc(12px + env(safe-area-inset-right));
}
/* The bottom safe-area inset belongs to whichever row is LAST, and that changes:
   #fishRow now sits below the belt, but it is display:none whenever no fish are
   eligible, which is most of the time. CSS cannot look forward from #beltRow to
   a later sibling's state, so the container answers the question instead. Without
   this the belt's bottom row of chips sits under the iPhone home indicator every
   time the fish row is away. */
#app:has(#fishRow.hidden) #beltRow { padding-bottom:calc(12px + env(safe-area-inset-bottom)); }
#mathBelt {
  display:flex; align-items:center; align-content:center; gap:12px; flex:1 1 auto;
  min-width:0; flex-wrap:wrap;
}
#pouch { display:flex; gap:10px; flex:0 1 auto; min-width:0; flex-wrap:wrap; min-height:52px; align-items:center; }
#pouch::before { content:"🎒"; font-size:20px; margin-right:2px; }
.pouchTool {
  width:48px; height:48px; flex:0 0 48px; border-radius:14px; font-size:22px;
  display:flex; align-items:center; justify-content:center; background:#1e3a55;
  border:2px solid #ffffff55; box-shadow:0 3px 0 rgba(0,0,0,.35);
  transition:transform .12s, background .2s, opacity .2s;
}
.pouchTool:active:not(:disabled) { transform:scale(.9); }
.pouchTool:disabled { opacity:.3; cursor:default; }
.pouchTool.active {
  background:linear-gradient(#ff8a8a,#d84f4f); border-color:#fff;
  box-shadow:0 0 0 3px #ff8a8a55, 0 3px 0 #8f3030;
}
.chip {
  width:48px; height:48px; border-radius:14px; background:transparent url("assets/ui/number-chip.png") center/contain no-repeat;
  color:#7a4514; font-weight:900; font-size:24px; display:flex; align-items:center; justify-content:center;
  filter:drop-shadow(0 3px 2px rgba(0,0,0,.35)); border:0; transition:transform .12s, opacity .2s;
}
.chip:active { transform:scale(.9); }
.chip.used { opacity:.25; transform:scale(.85); pointer-events:none; }
.chip.trashable {
  animation:trashWiggle .38s ease-in-out infinite alternate;
  outline:3px solid #ff8a8a; background:radial-gradient(#fff9ec,#ffd6d6);
}
@keyframes trashWiggle { from { rotate:-2deg; } to { rotate:2deg; } }
.chip.hintPulse, .rune.hintPulse, .deepWisp.hintPulse, #btnUndo.hintPulse { animation:hintPulse .7s infinite alternate; }
@keyframes hintPulse { from { outline:3px solid #ffd16600; } to { outline:4px solid #ffd166; transform:translateY(-4px); } }
.chip.popIn { animation:popIn .3s; }
@keyframes popIn { from { transform:scale(0);} to { transform:scale(1);} }
#runes {
  display:flex; gap:10px; flex:0 0 auto; padding-left:12px;
  border-left:2px solid rgba(255,255,255,.12);
}
.rune {
  width:48px; height:48px; border-radius:50%; font-size:24px; font-weight:900; color:#fff;
  display:flex; align-items:center; justify-content:center; box-shadow:0 3px 0 rgba(0,0,0,.35);
  border:2px solid #ffffff88; transition:transform .12s;
}
.rune:active { transform:scale(.9); }
.rune[data-op="add"] { background:linear-gradient(#66bb6a,#388e3c); }
.rune[data-op="sub"] { background:linear-gradient(#64b5f6,#1976d2); }
.rune[data-op="mul"] { background:linear-gradient(#ba68c8,#7b1fa2); }
.rune[data-op="div"] { background:linear-gradient(#26c6da,#00838f); }
.rune.locked { background:#1e3a55; color:#557; border-style:dashed; font-size:18px; }
.rune.justEarned { animation:runeEarn 1s; }
@keyframes runeEarn { 0% { transform:scale(0) rotate(-180deg);} 60% { transform:scale(1.4);} 100% { transform:scale(1);} }

/* ===== Number Pond fish row (docs/spec-number-pond.md) =====
   A DEDICATED row, always visible when eligible — never a drawer, never mixed
   into #mathBelt's pouch/rune controls. Fish 1-6 must stay fully visible with
   NO scrolling at supported phone/iPad sizes; tile width uses clamp() so six
   tiles always fit one line instead of wrapping or overflowing. Stamina is
   plain text only (see .fishStamina) — never a pip, ×3, or fraction. */
#fishRow {
  display:flex; align-items:center; gap:6px; padding:6px 12px; background:#0d2438;
  border-top:2px solid #1e3a55; z-index:5;
  /* Last row when it is present, so it carries the bottom inset — see the
     :has() rule beside #beltRow for what happens when it is not. */
  padding-bottom:calc(6px + env(safe-area-inset-bottom));
  padding-left:calc(12px + env(safe-area-inset-left)); padding-right:calc(12px + env(safe-area-inset-right));
}
#fishRow.hidden { display:none; }
/* Left-aligned, NOT centred. Centring meant the school slid sideways every time
   a fish tired out or a new one unlocked, so the fish a child had been tapping
   was somewhere else on the next repaint. Flush left it shares one start edge
   with the pouch directly above it, and each fish keeps its position for good. */
#fishTiles { display:flex; align-items:center; gap:6px; flex:1 1 auto; justify-content:flex-start; }
.fishTile {
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  width:clamp(44px, 14vw, 72px); min-width:44px; min-height:44px; border-radius:12px;
  background:linear-gradient(#dff3ff,#a8d8f0); color:#0c3a57; border:2px solid #fff;
  box-shadow:0 3px 0 #4a7ea8; font-weight:900; transition:transform .12s, opacity .2s;
}
.fishTile:active:not(:disabled) { transform:scale(.9); }
.fishTile .fishNum { height:38px; line-height:1; }
.fishArtWrap { position:relative; display:inline-flex; width:52px; height:38px; align-items:center; justify-content:center; }
.fishArt { display:block; width:100%; height:100%; object-fit:contain; }
/* The number sits in the pale medallion painted on the fish's flank. Every
   number-fish PNG is 160x120 with that circle centred at 52% x 49.5% and about
   30% x 33% of the art — measured off the shipped files, not eyeballed, and
   identical across all six. At left:25% the number landed out on the tail fin,
   tiny and unreadable against the fin colour, so the fish read as blank and the
   medallion read as a design flourish. The art reserves that space FOR the
   number; put it there. */
.fishMedallion {
  position:absolute; left:52%; top:49.5%; transform:translate(-50%,-50%); color:#3a2414;
  font-size:15px; font-weight:1000; line-height:1; text-shadow:0 1px #fff8;
}
.fishCard .fishArtWrap { width:72px; height:52px; }
.fishCard .fishMedallion { font-size:20px; }
.fishTile .fishStamina { font-size:9px; font-weight:700; opacity:.85; margin-top:2px; white-space:nowrap; }
.fishTile:disabled, .fishTile.tired {
  opacity:.4; background:linear-gradient(#c9d6de,#9fb0bb); box-shadow:0 3px 0 #6a7a84; cursor:default;
}

/* ===== overlays ===== */
.overlay {
  position:fixed; inset:0; background:rgba(5,10,20,.82); z-index:50;
  display:flex; align-items:center; justify-content:center; padding:18px;
  padding-top:calc(18px + env(safe-area-inset-top)); padding-bottom:calc(18px + env(safe-area-inset-bottom));
  padding-left:calc(18px + env(safe-area-inset-left)); padding-right:calc(18px + env(safe-area-inset-right));
}
.panel {
  background:#16283f; border:2px solid #2c5378; border-radius:22px; padding:22px;
  max-width:430px; width:100%; max-height:92vh; max-height:92dvh; overflow-y:auto; text-align:center;
  box-shadow:0 10px 40px rgba(0,0,0,.5);
  overscroll-behavior:contain; -webkit-overflow-scrolling:touch;   /* iOS: no rubber-band of the game underneath */
}
.panel h2 { margin-bottom:12px; font-size:22px; }
.titlePanel {
  max-width:920px; padding:12px 12px 16px;
  background:linear-gradient(180deg,#132b43,#16283f 62%); border-color:#3a6288;
}
.titleSplash {
  position:relative; isolation:isolate; min-height:clamp(250px,35vw,320px); overflow:hidden;
  display:grid; grid-template-columns:minmax(280px,37%) minmax(0,63%); align-items:center;
  border-radius:16px; background:#071b31 url("assets/ui/title-splash-chip.webp") center / cover no-repeat;
  box-shadow:inset 0 0 0 2px rgba(255,255,255,.12),0 7px 22px rgba(0,0,0,.35);
}
.titleSplash::after {
  content:""; position:absolute; z-index:0; inset:0 0 0 39%;
  background:linear-gradient(90deg,transparent,rgba(4,19,36,.28) 18%,rgba(4,19,36,.76) 100%);
}
.titleLockup {
  position:relative; z-index:1; grid-column:2; padding:26px clamp(18px,2.5vw,34px) 24px 4px;
  text-align:left; text-wrap:balance;
}
.titleEyebrow {
  margin-bottom:5px; color:#ffd166; font-size:clamp(10px,1.25vw,13px); font-weight:1000;
  letter-spacing:.18em; text-shadow:0 2px 8px #000;
}
.logo {
  margin:0 0 8px; color:#fff7e8; font-size:clamp(38px,4.4vw,58px); font-weight:1000;
  letter-spacing:.025em; line-height:.95; white-space:nowrap;
  text-shadow:0 3px 0 #76502d,0 6px 20px rgba(0,0,0,.75);
}
.tagline {
  margin:0; color:#eaf3dc; font-size:clamp(13px,1.65vw,18px); font-weight:800;
  line-height:1.28; text-shadow:0 2px 8px #000;
}
.titleSetup { max-width:650px; margin:0 auto; padding:17px 12px 0; }
.gradePrompt { font-size:18px; font-weight:900; margin-bottom:4px; }
.gradeHelp { font-size:13px; line-height:1.35; opacity:.72; max-width:360px; margin:0 auto 12px; }
#gradeCards { display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:9px; margin-bottom:12px; }
.gradeCard {
  display:flex; min-height:72px; flex-direction:column; align-items:center; justify-content:center; gap:1px;
  background:#1e3a55; border-radius:15px; color:#fff7e8; transition:transform .12s; border:2px solid transparent;
  padding:7px 5px;
}
.gradeCard:active { transform:scale(.96); }
.gradeCard:hover { border-color:#ffd166; }
.gradeNum { font-size:26px; line-height:1; font-weight:1000; color:#ffd166; }
.gradeName { font-weight:800; font-size:11px; opacity:.78; }
.bigBtn {
  background:linear-gradient(#ffd166,#ff9f1c); color:#4a2400; font-weight:900; font-size:19px;
  border-radius:999px; padding:12px 26px; box-shadow:0 4px 0 #c47407; margin-top:6px;
}
.bigBtn:active { transform:translateY(2px); box-shadow:0 2px 0 #c47407; }
/* both stay visually small on purpose — padding grows the hit area, not the type */
.tinyBtn { opacity:.5; font-size:12px; margin-top:4px; text-decoration:underline; padding:12px 18px; min-height:44px; }
#verTag {
  opacity:.3; font-size:10px; margin-top:0; cursor:pointer;
  display:inline-flex; align-items:center; justify-content:center; min-height:44px; min-width:44px; padding:0 18px;
}
@media (max-width:620px) {
  .overlay { padding:10px; padding-top:calc(10px + env(safe-area-inset-top)); padding-bottom:calc(10px + env(safe-area-inset-bottom)); }
  .titlePanel { padding:8px 8px 12px; border-radius:18px; }
  .titleSplash {
    min-height:250px; grid-template-columns:1fr; align-items:end;
    background-position:33% center;
  }
  .titleSplash::after {
    inset:34% 0 0;
    background:linear-gradient(transparent,rgba(4,19,36,.48) 24%,rgba(4,19,36,.92));
  }
  .titleLockup {
    grid-column:1; width:100%; padding:54px 14px 16px; text-align:center;
    background:linear-gradient(transparent,rgba(3,14,27,.84));
  }
  .titleEyebrow { font-size:9px; margin-bottom:4px; }
  .logo { font-size:clamp(32px,10vw,46px); white-space:normal; }
  .tagline { font-size:12px; }
  .titleSetup { padding:14px 4px 0; }
  .gradePrompt { font-size:17px; }
  .gradeHelp { font-size:12px; margin-bottom:10px; }
  #gradeCards { gap:7px; }
  .gradeCard { min-height:64px; }
}
.howtoStep { display:flex; align-items:center; gap:14px; text-align:left; margin:13px 0; }
.howtoIcon { font-size:27px; white-space:nowrap; }
.howtoStep p { font-size:15px; line-height:1.35; }
/* the "later stuff arrives later" reassurance — quieter than a step, never a step itself */
.howtoNote { font-size:13px; line-height:1.35; opacity:.7; margin:16px 0 4px; }

/* onboarding calibration — one untimed problem per screen */
.calibrationPanel { max-width:500px; }
.calHeader { display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:12px; }
.calTrail { font-size:14px; font-weight:900; color:#ffd166; text-align:left; }
.calNoTimer { font-size:11px; opacity:.65; white-space:nowrap; }
.calProgress { display:flex; justify-content:center; gap:8px; margin:8px 0 18px; }
.calProgress span {
  flex:0 0 16px; width:16px; height:16px; box-sizing:border-box;
  border-radius:50%; background:#2c5378; border:2px solid #6b8299;
}
.calProgress span.done { background:#7ed957; border-color:#bdf59f; }
.calProgress span.current { background:#ffd166; border-color:#fff1bb; box-shadow:0 0 0 2px rgba(255,209,102,.2); }
.calPrompt { font-size:21px; font-weight:900; line-height:1.35; min-height:58px; display:flex; align-items:center; justify-content:center; }
.calPromptText { display:inline; }
.calArt {
  min-height:56px; margin:6px 0 12px; padding:9px; border-radius:15px; background:#0d1b2a;
  display:flex; align-items:center; justify-content:center; font-size:24px; line-height:1.4;
}
.calArt:empty { display:none; }
.calArray { display:inline-block; line-height:1.45; letter-spacing:5px; }
.calDivisionModel { display:flex; align-items:center; justify-content:center; gap:11px; width:100%; }
.calDivisionCard {
  flex:0 0 auto; min-width:72px; padding:7px 9px; border-radius:12px; background:#1e3a55;
  display:flex; flex-direction:column; align-items:center; justify-content:center; line-height:1.1;
}
.calDivisionCard > span { font-size:24px; }
.calDivisionCard b { margin-top:3px; font-size:13px; white-space:nowrap; color:#ffd166; }
.calDivisionArrow { flex:0 0 auto; display:flex; flex-direction:column; align-items:center; line-height:1; color:#ffd166; }
.calDivisionArrow small { font-size:9px; font-weight:900; letter-spacing:.04em; text-transform:uppercase; white-space:nowrap; }
.calDivisionArrow b { margin-top:2px; font-size:29px; }
.calDivisionGroups { display:flex; align-items:stretch; justify-content:center; gap:5px; min-width:0; }
.calDivisionGroup {
  min-width:51px; padding:5px 4px; border:2px solid #2c5378; border-radius:11px; background:#16283f;
  display:flex; flex-direction:column; align-items:center; justify-content:center; line-height:1.05;
}
.calDivisionGroup > span { font-size:21px; }
.calDivisionGroup b { margin-top:4px; font-size:10px; white-space:nowrap; color:#b9d9f5; }
.calFractionModel { display:inline-flex; align-items:center; justify-content:center; gap:14px; }
.calFractionCircle {
  display:inline-block; flex:0 0 42px; width:42px; height:42px; box-sizing:border-box;
  border:3px solid #fff7e8; border-radius:50%;
}
.calFractionHalf { background:linear-gradient(90deg, #fff7e8 50%, transparent 50%); }
.calFractionQuarter { background:conic-gradient(from -90deg, #fff7e8 0 25%, transparent 25% 100%); }
.calFractionOperator { font-size:34px; font-weight:900; line-height:1; }
.calEquationWithUnit {
  display:inline-flex; align-items:baseline; justify-content:center; flex-wrap:wrap;
  column-gap:.25em;
}
.calChoices { display:flex; flex-wrap:wrap; justify-content:center; gap:12px; margin-top:10px; }
.calChoice {
  min-width:76px; min-height:58px; padding:10px 14px; border-radius:14px;
  background:#fff7e8; color:#4a2e19; font-size:22px; font-weight:1000; box-shadow:0 3px 0 #c98d3f;
}
.calChoice:active:not(:disabled) { transform:scale(.94); }
.calChoice.correct { background:#c8f7c5; color:#173b17; box-shadow:0 3px 0 #4caf50; }
.calChoice.pickedWrong { background:#dfe8f2; color:#526476; box-shadow:0 3px 0 #6b8299; }
.calChoice:disabled { cursor:default; }
.calEncourage { min-height:24px; margin-top:12px; font-size:14px; font-weight:800; color:#bdf59f; }
.trailResult { font-size:62px; margin:3px 0 5px; }
.trailResultName { color:#ffd166; font-size:25px; font-weight:1000; margin-bottom:8px; }
.trailResultText { line-height:1.45; font-size:15px; opacity:.9; max-width:390px; margin:0 auto 12px; }
.placementChoices { display:flex; flex-direction:column; align-items:center; gap:8px; }
.calSecondary {
  min-height:44px; padding:9px 18px; border-radius:999px; background:#1e3a55; color:#fff7e8;
  font-size:14px; font-weight:900; border:2px solid #2c5378;
}
.calBack { min-height:44px; padding:9px 15px; font-size:12px; opacity:.65; text-decoration:underline; }

/* modal content bits */
.modalArt { font-size:52px; margin:6px 0 10px; }
.assetModalArt { height:150px; }
.barrierArt { width:min(100%, 300px); height:150px; object-fit:contain; }
.encounterArt { display:block; width:min(100%, 230px); height:170px; object-fit:contain; margin:auto; }
.gateTroll.small .encounterArt { height:105px; }
.nightSceneArt .encounterArt { height:180px; }
.modalText { font-size:15px; line-height:1.45; margin-bottom:12px; }
.recipe {
  display:flex; align-items:center; gap:10px; background:#1e3a55; border-radius:14px;
  padding:10px 12px; margin:8px 0; text-align:left;
}
.recipe .rIcon { font-size:26px; width:42px; flex:0 0 42px; display:flex; justify-content:center; }
.recipeArt, .toolArt { width:42px; height:42px; object-fit:contain; display:block; }
.inlineToolArt { width:24px; height:24px; object-fit:contain; vertical-align:middle; }
.recipe .rName { font-weight:800; font-size:15px; }
.recipe .rNeeds { font-size:13px; opacity:.85; }
.recipe .need-ok { color:#7ed957; }
.recipe .need-no { color:#ff8a8a; }
.recipe button {
  margin-left:auto; background:linear-gradient(#7ed957,#4caf50); color:#123312;
  font-weight:900; border-radius:10px; padding:8px 13px; box-shadow:0 2px 0 #2e7d32; white-space:nowrap;
  min-height:44px; min-width:44px; display:inline-flex; align-items:center; justify-content:center;
  text-align:center; line-height:1;
}
.practiceTrailRow > button, .rushRow > button { flex:0 0 96px; width:96px; }
.recipe button:disabled { background:#2c4358; color:#6b8299; box-shadow:none; }
.recipe.done { opacity:.55; }
.campToolsLine { font-size:14px; opacity:.85; margin-bottom:8px; }
.modalClose {
  position:sticky; float:right; top:0; z-index:2; font-size:20px; opacity:.9; padding:2px 8px;
  background:#16283f; border-radius:999px;
  min-width:44px; min-height:44px; display:inline-flex; align-items:center; justify-content:center;
}

/* Developer Panel (docs/spec-developer-unlock.md) — every control here holds
   the same 44px floor as the rest of the game's touch targets. */
.devWarn {
  background:#3a2410; border:2px solid #ffb454; border-radius:12px; padding:10px 12px;
  font-size:14px; line-height:1.45; margin:10px 0;
}
.devBtn {
  display:flex; width:100%; align-items:center; justify-content:center; text-align:center;
  min-height:48px; min-width:44px; padding:12px 20px; margin-top:10px;
}
.devBtn.devGhost { background:#1e3a55; color:#fff7e8; box-shadow:0 3px 0 #12283e; }
.devBtn.devDanger { background:linear-gradient(#ff9a8a,#e0524f); color:#3a0a0a; box-shadow:0 4px 0 #a83937; }
.devBtn.devDanger:disabled { background:#2c4358; color:#6b8299; box-shadow:none; opacity:.6; }
.devSummary { font-size:14px; line-height:1.6; margin:4px 0 10px 20px; }
.devSummary li { margin:4px 0; }

/* quiz */
.quizPrompt { font-size:17px; font-weight:800; margin-bottom:10px; line-height:1.4; }
.quizArt { font-size:26px; line-height:1.5; letter-spacing:2px; margin-bottom:14px; word-spacing:6px; }
.quizArt .gone { opacity:.28; position:relative; }
.quizArt .grp { display:inline-block; background:#1e3a55; border-radius:12px; padding:5px 9px; margin:3px; }
.shareGroups { display:flex; gap:8px; justify-content:center; align-items:stretch; flex-wrap:wrap; letter-spacing:0; word-spacing:0; }
.shareGroup {
  display:flex; flex-direction:column; justify-content:flex-start; align-items:center; gap:4px;
  min-width:76px; padding:7px; border:2px solid #5fd4e0; border-radius:14px; background:#0d1b2a;
}
.shareItems { max-width:96px; line-height:1.15; letter-spacing:1px; word-spacing:0; }
.shareEquation { display:flex; justify-content:center; align-items:center; gap:10px; flex-wrap:wrap; font-size:17px; letter-spacing:0; word-spacing:0; }
.quizChoices { display:flex; gap:12px; justify-content:center; flex-wrap:wrap; }
.quizChoices button {
  min-width:64px; min-height:56px; padding:12px; font-size:23px; font-weight:900; border-radius:14px;
  background:#fff7e8; color:#4a2e19; box-shadow:0 3px 0 #c98d3f;
}
.quizChoices button:active { transform:scale(.94); }
.quizChoices button.wrongPick { animation:shake .4s; background:#ffd6d6; }
.quizProgress { margin-top:12px; font-size:14px; opacity:.6; letter-spacing:3px; }

/* River Supply Launch capstone — Share among boats / Fill equal boats
   (docs/spec-equal-groups-expedition.md). Reuses .shareGroups/.shareGroup/
   .shareItems and .quizChoices above; these two are the only new rules. */
.riverStack { font-size:15px; font-weight:800; margin:10px 0; }
.riverEmptyDock { font-size:14px; opacity:.6; margin:10px 0; }

/* authored Chip guides + no-timer Practice Trail */
.practiceTrailRow { border:1px solid #4f7da3; }
.practiceTrailTitle { display:flex; flex-direction:column; align-items:flex-start; gap:2px; line-height:1.05; }
.optionalBadge {
  display:inline-flex; padding:3px 7px; border-radius:999px; background:#365b78; color:#dceeff;
  font-size:9px; line-height:1; font-weight:900; letter-spacing:.08em; text-transform:uppercase;
}
.learningIntro { max-width:430px; margin-left:auto; margin-right:auto; }
.learningCards { display:flex; flex-direction:column; gap:9px; margin:12px 0; }
.learningCard {
  display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:10px;
  padding:11px 12px; border-radius:15px; background:#142b40; border-left:5px solid #64b5f6; text-align:left;
}
.learningCard.op-add { border-left-color:#70d68b; }
.learningCard.op-sub { border-left-color:#64b5f6; }
.learningCard.op-mul { border-left-color:#ba68c8; }
.learningCard.op-div { border-left-color:#5fd4e0; }
.learningIcon { font-size:28px; }
.learningCopy { min-width:0; }
.learningActions { display:flex; gap:7px; }
.learningActions button, .learningBack {
  min-height:44px; min-width:44px; padding:8px 11px; border-radius:11px;
  background:#1e3a55; color:#fff7e8; border:2px solid #2c5378; font-size:13px; font-weight:900;
}
.learningActions button:last-child { background:linear-gradient(#7ed957,#4caf50); color:#123312; border-color:#7ed957; }
.learningBack { display:block; margin:0 0 2px; }
.learningGuideLabel { color:#ffd166; font-size:13px; font-weight:900; margin:2px 0 5px; }
.learningProgress { display:flex; justify-content:center; gap:8px; margin:8px 0 14px; }
.learningProgress span {
  width:14px; height:14px; box-sizing:border-box; border-radius:50%;
  background:#2c5378; border:2px solid #6b8299;
}
.learningProgress span.done { background:#7ed957; border-color:#bdf59f; }
.learningProgress span.current { background:#ffd166; border-color:#fff1bb; }
.learningStepArt {
  min-height:76px; display:flex; align-items:center; justify-content:center; flex-wrap:wrap;
  margin:8px 0 13px; padding:13px; border-radius:17px; background:#0d1b2a;
  font-size:25px; line-height:1.35; letter-spacing:.3px;
}
.learningNudge {
  max-width:420px; margin:10px auto; padding:10px 12px; border-radius:13px;
  background:#203d52; color:#dff5ff; font-size:14px; line-height:1.4; text-align:left;
}
.practicePrompt { font-size:24px; font-weight:900; line-height:1.35; margin:12px 0; }
.practiceArt { margin:-3px 0 13px; font-size:14px; opacity:.78; }
.practiceChoices { display:flex; justify-content:center; flex-wrap:wrap; gap:12px; }
.practiceChoices button {
  min-width:76px; min-height:58px; padding:10px 14px; border-radius:14px;
  background:#fff7e8; color:#4a2e19; font-size:22px; font-weight:1000; box-shadow:0 3px 0 #c98d3f;
}
.practiceChoices button.correct { background:#c8f7c5; color:#173b17; box-shadow:0 3px 0 #4caf50; }
.practiceChoices button.pickedWrong { background:#dfe8f2; color:#526476; box-shadow:0 3px 0 #6b8299; }
.practiceFeedback { min-height:24px; margin-top:14px; font-size:14px; font-weight:800; color:#bdf59f; }
@media (max-width:520px) {
  .learningCard { grid-template-columns:auto minmax(0,1fr); }
  .learningActions { grid-column:1 / -1; }
  .learningActions button { flex:1; }
  .learningStepArt { min-height:62px; font-size:21px; }
}

/* ===== gate monster: the Bramble Troll ===== */
.gateTroll { font-size:60px; }
.gateTroll.small { font-size:38px; margin:2px 0 6px; }
.gateSay { font-style:italic; }
.gateBridge { display:flex; align-items:center; justify-content:center; gap:7px; margin:8px auto 14px; padding:12px 14px 8px; background:#0d1b2a; border-radius:16px; max-width:320px; transition:box-shadow .5s; }
.gateBridge.open { animation:bridgeSurge 1s forwards; }
@keyframes bridgeSurge { 0% { box-shadow:0 0 0 #ffd16600; } 50% { box-shadow:0 0 26px #ffd166aa; } 100% { box-shadow:0 0 9px #ffd16655; } }
.gateStone { position:relative; width:44px; height:44px; border-radius:50%; background:radial-gradient(#5c6b78,#37444f); box-shadow:inset 0 -4px 0 rgba(0,0,0,.3); display:flex; align-items:center; justify-content:center; font-size:17px; transition:background .5s, box-shadow .5s; }
.gateStone.lit { background:radial-gradient(#ffe9a8,#ffd166 60%,#ff9f1c); box-shadow:0 0 16px #ffd16688; }
.gateStone.mossy { background:radial-gradient(#57705a,#3c5241); }
.gateStone.surge { animation:stoneSurge .8s; }
@keyframes stoneSurge { 40% { transform:scale(1.3); box-shadow:0 0 28px #ffd166; } }
.gateStone .vine { position:absolute; top:-11px; left:50%; transform:translateX(-50%); font-size:19px; font-style:normal; opacity:1; transition:opacity .5s, transform .5s; pointer-events:none; }
.gateStone .vine.snapped { opacity:0; transform:translateX(-50%) scale(1.8) rotate(40deg); }
.plank { width:18px; height:8px; border-radius:4px; background:#6b4f2e; opacity:.45; transition:opacity .5s, background .5s, box-shadow .5s; }
.gateBridge.open .plank { opacity:1; background:#c98d3f; box-shadow:0 0 10px #ffd16677; }
.gatePrompt { font-size:26px; font-weight:900; margin:6px 0 12px; letter-spacing:1px; }
.gateBlank { display:inline-flex; min-width:44px; padding:0 8px; justify-content:center; background:#0d1b2a; border-radius:10px; color:#ffd166; border:2px dashed #2c5378; }
.keypad { display:grid; grid-template-columns:repeat(3, minmax(56px, 76px)); gap:8px; justify-content:center; }
.keypad button { height:56px; font-size:24px; font-weight:900; border-radius:14px; background:#fff7e8; color:#4a2e19; box-shadow:0 3px 0 #c98d3f; transition:transform .1s; }
.keypad button:active { transform:scale(.94); }
.keypad button[data-key="bk"] { background:#1e3a55; color:#fff7e8; box-shadow:0 3px 0 #0d1b2a; }
.keypad .keyOk { background:linear-gradient(#7ed957,#4caf50); color:#123312; box-shadow:0 3px 0 #2e7d32; }
.keypad .keyOk:disabled { background:#2c4358; color:#6b8299; box-shadow:none; cursor:default; }
.gateChoices { display:flex; flex-wrap:wrap; gap:10px; justify-content:center; }
.gateChoices button { min-width:64px; min-height:56px; padding:10px 14px; font-size:24px; font-weight:900; border-radius:14px; background:#fff7e8; color:#4a2e19; box-shadow:0 3px 0 #c98d3f; }
.gateChoices button:active { transform:scale(.94); }
.gateChoices button.wrongPick { animation:shake .4s; background:#ffd6d6; }
.gateShake { animation:shake .4s; }
.bubble.gateSleep { background:#dfe8f2; color:#3a4a5c; font-size:14px; }

/* Production biome art lives on #sky so every interactive object remains a
   separate foreground layer. Forest and the Trailblazer zone intentionally
   share the grove, with the latter receiving a darker runtime treatment. */
#scene.zonemeadow { --scene-art:url("assets/backgrounds/meadow.webp"); }
#scene.zoneforest { --scene-art:url("assets/backgrounds/forest.webp"); }
#scene.zoneriver { --scene-art:url("assets/backgrounds/river.webp"); }
#scene.zonedeep { --scene-art:url("assets/backgrounds/forest.webp"); }
#scene.zonedeep #sky { filter:brightness(.72) saturate(.82) hue-rotate(5deg); }
#scene.zonedeep.dusk #sky { filter:brightness(.56) saturate(.72); }
#scene.zonedeep.night #sky { filter:brightness(.34) saturate(.52); }

/* ===== deep forest ZONE (zonedeep) — Chip is actually across the bridge ===== */
#scene.zonedeep #camp, #scene.zonedeep #campGlow, #scene.zonedeep #btnFire { display:none; }

/* Camp furniture stays at camp; the Trail Board remains a live foreground
   control in every biome because reading it never costs daylight. */
#scene.zonemeadow #camp, #scene.zonemeadow #campGlow, #scene.zonemeadow #btnFire,
#scene.zoneforest #camp, #scene.zoneforest #campGlow, #scene.zoneforest #btnFire,
#scene.zoneriver #camp, #scene.zoneriver #campGlow, #scene.zoneriver #btnFire { display:none; }
#scene.zonedeep #toolRack, #scene.zonemeadow #toolRack, #scene.zoneforest #toolRack, #scene.zoneriver #toolRack { display:none; }

/* crossing set-piece: darkness sweeps over the scene, then lifts in the new zone */
#scene::after { content:""; position:absolute; inset:0; background:#050a14; opacity:0; pointer-events:none; z-index:8; transition:opacity .55s ease; }
#scene.zoneFade::after { opacity:1; }

/* arrival banner — a moment, not a toast */
.zoneBanner {
  position:absolute; left:50%; top:30%; transform:translate(-50%,-50%); z-index:9; pointer-events:none;
  font-size:32px; font-weight:900; letter-spacing:2px; text-align:center; white-space:nowrap;
  color:#ffd166; background:rgba(5,10,20,.78); border:2px solid #ffd16688; border-radius:22px;
  padding:16px 34px; box-shadow:0 8px 34px rgba(0,0,0,.55); text-shadow:0 2px 6px rgba(0,0,0,.5);
  animation:bannerIn 2.4s forwards;
}
.zoneBanner small { display:block; font-size:14px; font-weight:700; letter-spacing:.5px; color:#fff7e8; opacity:.85; margin-top:4px; }
@keyframes bannerIn {
  0% { opacity:0; transform:translate(-50%,-50%) scale(.6); }
  12% { opacity:1; transform:translate(-50%,-50%) scale(1.08); }
  20% { transform:translate(-50%,-50%) scale(1); }
  80% { opacity:1; }
  100% { opacity:0; transform:translate(-50%,-50%) scale(1); }
}

/* ===== campfire progression (Fire Level 2 — Steady Flame) & trail board ===== */
#campGlow { position:absolute; left:-6%; bottom:4%; width:24%; height:32%; border-radius:50%; pointer-events:none;
  background:radial-gradient(closest-side, #ffd16650, #ff9f1c22 55%, transparent 72%);
  opacity:0; transition:opacity 1.4s ease, width 1.4s ease, height 1.4s ease; }
#scene.fire2 #campGlow { opacity:1; width:54%; height:62%; }     /* the light expands right, toward the troll */
#scene.dusk.fire2 #campGlow, #scene.night.fire2 #campGlow { opacity:.85; }
#campRing { position:absolute; right:-7px; bottom:-7px; width:88px; height:44px; object-fit:contain; opacity:0; transition:opacity .9s; pointer-events:none; }
#campRing.built { opacity:1; }                                   /* stone fire ring — the camp remembers */
#scene.fire2 #campFire { width:76px; height:80px; }              /* bigger hearth */
#scene.fire2 #campFire.lit { opacity:1; }
/* sibling of #camp (buttons can't nest), so it positions against #scene.
   At Camp it is a small planted service board to the right of the tool rack.
   Biome-specific rules below move the same live control beside the Camp return
   board, where both pairs of posts share one ground line. */
#trailBoard { position:absolute; left:420px; bottom:8%; width:72px; height:54px; cursor:pointer; display:inline-block; transition:transform .15s; z-index:3; }
#trailBoard img { display:block; width:100%; height:100%; object-fit:contain; }
#trailBoard::before { content:""; position:absolute; inset:-11px; }   /* small sign, ≥44px hit area */
#trailBoard:active { transform:scale(.9); }
#trailBoard.fresh { animation:bounce .6s infinite alternate; }
#trailBoard.fresh::after { content:"!"; position:absolute; top:-7px; right:-8px; width:18px; height:18px; border-radius:50%;
  background:#ff5d5d; color:#fff; font-size:12px; font-weight:900; display:flex; align-items:center; justify-content:center; }
#scene.zonedeep #trailBoard, #scene.zonemeadow #trailBoard,
#scene.zoneforest #trailBoard, #scene.zoneriver #trailBoard {
  left:calc(17% + 82px); bottom:10.5%; width:126px; height:95px;
}
.recipe.trailObj.rec { background:#2a4a2e; border:2px solid #ffd166; }
.recipe.trailObj.rec .rName { font-size:17px; }
#toolRack { position:absolute; left:294px; bottom:8%; width:116px; height:80px; z-index:2; pointer-events:none; }
.toolRackBase { position:absolute; inset:0; width:100%; height:100%; object-fit:contain; }
#toolRackItems { position:absolute; left:18%; right:6%; top:23%; bottom:15%; display:flex; align-items:center; justify-content:center; gap:0; }
.rackTool { width:29px; height:48px; object-fit:contain; margin-left:-5px; filter:drop-shadow(0 1px 1px #0005); }

/* toasts */
#toastLayer { position:fixed; left:50%; bottom:26%; transform:translateX(-50%); z-index:70; pointer-events:none; display:flex; flex-direction:column; align-items:center; gap:6px; }
.toast {
  background:#fff7e8; color:#4a2e19; font-weight:800; font-size:15px;
  border-radius:999px; padding:8px 18px; box-shadow:0 4px 14px rgba(0,0,0,.4);
  animation:toastIn .25s, floatUp 1s 1.7s forwards; max-width:92vw;
  white-space:normal; text-align:center; line-height:1.3;
}
@keyframes toastIn { from { transform:scale(.6); opacity:0; } }

@media (max-width:520px) {
  /* The hub is a map, so phone mode preserves every painted landmark instead
     of cropping the river or forest edge to fill a portrait scene. */
  #scene:not(.zonemeadow):not(.zoneforest):not(.zoneriver):not(.zonedeep) #sky { background-size:100% 100%; }
  .chip, .rune, .pouchTool { width:44px; height:44px; flex-basis:44px; font-size:21px; }   /* 44px floor — never shrink below the touch minimum */
  #pouch, #runes { gap:8px; }
  #mathBelt { gap:8px; }
  #runes { padding-left:8px; }
  /* The shorthand would reset BOTH the bottom inset and the left/right insets the
     base rule sets, and this block only applies at phone widths — precisely where
     the home indicator and the notch cutouts are. The horizontal padding stays at
     the belt's 12px rather than tightening to 8px: the rows are stacked on each
     other, and 4px of disagreement reads as a misaligned row. Six 44px fish plus
     five 4px gaps plus 24px of padding is 308px, so they still fit at 375px. */
  #fishRow {
    padding:5px 12px; padding-bottom:calc(5px + env(safe-area-inset-bottom));
    padding-left:calc(12px + env(safe-area-inset-left)); padding-right:calc(12px + env(safe-area-inset-right));
  }
  #fishTiles { gap:4px; }
  .fishTile { width:clamp(44px, 14vw, 60px); }   /* six fish, zero scroll, never below the 44px touch floor */
  #exprRow { font-size:21px; padding:6px 10px; }
  .node .bubble { font-size:16px; padding:2px 9px; }
  /* The Bramble Bridge sign was being PAINTED OVER, not clipped by the viewport:
     it read as "le Bridge" on a phone. The sign is centred on the gate node at
     x .67 and the pill is 139px wide — 37% of a 375px scene against 13% of a
     1280px one — so its left half reaches back to x 182, and the River exit sits
     at 186–241. #hubLayer is z-index 4 and .node is 2, deliberately (see
     `.node .bubble.navLabel`), so the River sign wins and eats the first word.

     Fixed by dropping the sign below River rather than shrinking it. Shrinking
     is the tempting move and it is wrong twice over: at 14px the pill is ~21px
     tall, which takes the ::before target to 21 + 2x11 = 43 and back under the
     44px floor that inset exists to hold; and even at 14px the pill still
     reaches x 190, so it stays under the River sign anyway.

     -26px, measured: the pill's top must clear River's bottom (321 at 375px,
     346 at 430px) and the label bottom sits 4px above the node top. Bare
     clearance is 17px, and 20 was not enough — the hub exits unfold with a
     transform animation, so River's box is momentarily lower than its resting
     one, and a 3px gap closes while it settles. 26 leaves 9px. Below this the
     sign lands on the painted bridge, which is where it belongs. Desktop is
     untouched — there the two signs already miss each other by 12px, and the
     drop would take the sign off the bridge art.

     Selector is `.node .bubble.brambleBridgeLabel`, not `.brambleBridgeLabel`:
     the margin being overridden is set by `.node .bubble`, and a media query
     adds no specificity, so the short form silently loses and nothing moves. */
  .node .bubble.brambleBridgeLabel { margin-bottom:-26px; }
  #hungerHint { display:none; }
  .nodeSprite:not(.nodeSpriteArt) { font-size:calc(var(--sz, 40px) * .72); }
  /* The cap is geometry, not taste. The last harvest slot sits at 88.5% of the
     scene (world-slots.js SLOTS), leaving 11.5% of the width to its right — and
     a node is centred on its slot, so anything wider than twice that margin
     hangs off the screen. The old giant pine fit with about a pixel to spare,
     which meant the row's right edge was quietly tuned to one sprite's size:
     growing it to read as the big prize pushed it off screen. Written as a cap
     so the next size change cannot repeat that.

     The scale below the cap is .70 rather than .78 for a reason worth stating,
     because it looks like a nudge. A cap only bites the LARGEST sprite, so
     scaling everything at .78 shrank the giant pine to the cap while leaving
     the ordinary tree at full size — 1.5x on a desktop collapsed to 1.26x on
     the phone, which is where a small screen makes the difference hardest to
     see and most worth having. Lowering the scale pulls the smaller trees down
     with it and keeps the hierarchy at roughly 1.4x. Nothing lands under the
     44px touch floor: the sapling, the smallest of them, renders at 46px.
     browser-tests/biomes.spec.js measures the rendered ratio, not this rule. */
  .nodeSpriteArt {
    width:min(calc(var(--sz, 70px) * .70), 22vw);
    height:min(calc(var(--sz, 70px) * .70), 22vw);
  }
  .returnCampNode .nodeSpriteArt { width:116px; height:87px; }
  /* longhands, not the shorthand — the shorthand would wipe #hud's safe-area insets */
  #hud { gap:5px; padding-top:calc(6px + env(safe-area-inset-top)); padding-bottom:6px;
         padding-left:calc(8px + env(safe-area-inset-left)); padding-right:calc(8px + env(safe-area-inset-right)); }
  .res, #dayInfo { font-size:13px; padding:3px 7px; }
  #resBar { gap:8px; padding-inline:0; }
  #resBerry { padding-inline:7px; }
  #rankBadge { font-size:14px; padding:3px 8px; }
  #xpBar { width:44px; }
  #avatar { --chip-size:66px; }
  /* The hub signs had NO narrow override at all — they rendered at full desktop
     size on the device most children actually hold, which is how "Sinkhole to
     the Deep" grew to 178px and started colliding with everything near it. The
     44px floors below are untouched: only the label shrinks, never the target.
     Long names may wrap here (they cannot at desktop width) because two short
     lines cost far less horizontal room than one long one, and horizontal room
     is the scarce thing on a phone. */
  .hubExit { font-size:13px; padding:4px 9px; border-radius:13px; max-width:42vw; }
  .hubExit .hubExitName { white-space:normal; line-height:1.15; }
  .hubExit-pond .hubExitName, .hubExit-sinkhole .hubExitName { white-space:nowrap; }
  #camp { --camp-w:156px; --camp-h:88px; }
  #campTent { width:82px; height:74px; }
  #campLogs { width:46px; height:30px; right:3px; }
  #camp #campFire { width:52px; height:54px; right:0; }
  #scene.fire2 #camp #campFire { width:58px; height:62px; }
  #campRing { right:-5px; bottom:-6px; width:64px; height:34px; }
  #trailBoard { left:318px; width:64px; height:48px; }
  #scene.zonedeep #trailBoard, #scene.zonemeadow #trailBoard,
  #scene.zoneforest #trailBoard, #scene.zoneriver #trailBoard {
    left:calc(17% + 66px); bottom:10.5%; width:96px; height:72px;
  }
  #toolRack { left:220px; width:88px; height:64px; }
  .token { font-size:19px; }                 /* 44×44 box holds at every width — only the digit shrinks */
  .res { padding:3px 6px; }                  /* height floor is set on .res itself */
  #taskLabel { font-size:13px; }
  #btnGo { font-size:15px; padding:0 12px; }
  .gatePrompt { font-size:22px; }
  .gateTroll { font-size:48px; }
  .gateStone { width:40px; height:40px; }   /* stones may shrink — keypad keys stay ≥56px */
  .calDivisionModel { gap:6px; }
  .calDivisionCard { min-width:60px; padding-inline:6px; }
  .calDivisionArrow small { font-size:8px; }
  .calDivisionGroup { min-width:44px; padding-inline:2px; }
}
/* ===== Daily Trail Rush ===== */
/* Covers the scene only — the belt and solve bar stay live underneath, because
   the run reuses the real expression builder rather than cloning it. */
#rushOv {
  position:absolute; inset:0; z-index:7; display:flex; align-items:flex-start; justify-content:center;
  padding:10px; background:linear-gradient(#0b1f33ee, #12304aee); backdrop-filter:blur(2px);
}
.rushCard {
  width:100%; max-width:430px; background:#16283f; border:2px solid #2c5378; border-radius:20px;
  padding:12px 14px; box-shadow:0 10px 32px rgba(0,0,0,.45); display:flex; flex-direction:column; gap:9px;
}
.rushTop { display:flex; align-items:center; gap:10px; }
.rushScore { font-size:19px; font-weight:900; color:#ffd166; }
.rushClock {
  margin-left:auto; font-size:26px; font-weight:900; font-variant-numeric:tabular-nums; color:#fff7e8;
}
.rushClock.low { color:#ffb37a; }
.rushLanterns { margin-left:auto; display:flex; gap:4px; font-size:20px; }
.rushLantern { opacity:.28; filter:grayscale(1); transition:opacity .3s, filter .3s, transform .3s; }
.rushLantern.lit { opacity:1; filter:none; transform:translateY(-2px); }
.rushBtn {
  min-width:44px; min-height:44px; border-radius:12px; background:#1e3a55; font-size:19px;
  display:inline-flex; align-items:center; justify-content:center;
}
.rushTop .rushBtn { margin-left:6px; }
.rushMeter { height:9px; border-radius:999px; background:#0d1b2a; border:1px solid #2c5378; overflow:hidden; }
.rushMeterFill { height:100%; width:100%; border-radius:999px; background:linear-gradient(90deg,#ffd166,#ff9f1c); transition:width .25s linear; }
.rushMeter.gentle .rushMeterFill { background:linear-gradient(90deg,#ffd9a0,#ffc16b); }   /* Sprout: warm glow, never urgency */
.rushTargetLine { font-size:20px; font-weight:800; text-align:center; }
.rushTargetLine b { font-size:30px; color:#ffd166; }
.rushFoot { display:flex; align-items:center; gap:10px; font-size:13px; opacity:.9; }
.rushFoot #rushHint { margin-left:auto; }
.rushCard.yay { animation:rushYay .35s; }
.rushCard.nope { animation:shake .38s; }
@keyframes rushYay { 50% { border-color:#7ed957; box-shadow:0 0 20px #7ed95755; } }
/* summary */
.rushDone { align-items:center; text-align:center; gap:7px; }
.rushBigScore { font-size:40px; font-weight:900; color:#ffd166; }
.rushSummaryRows { display:flex; gap:12px; flex-wrap:wrap; justify-content:center; font-size:14px; opacity:.9; }
.rushBestLine { font-size:14px; line-height:1.5; }
.rushSummaryBtns { display:flex; gap:9px; flex-wrap:wrap; justify-content:center; }
.rushLeave { background:linear-gradient(#8fb6d6,#5c8fb8); box-shadow:0 4px 0 #3f6a8d; }
@media (max-width:520px) {
  .rushTargetLine { font-size:17px; }
  .rushTargetLine b { font-size:26px; }
  .rushBigScore { font-size:34px; }
  .rushClock { font-size:22px; }
}

/* ===== The Deep ===== */
/* Darkness and light only. Nothing here may lunge or chase the diver — the ruin
   drifts gently into view, and the light is what reveals its inscription. */
#deepOv { position:absolute; inset:0; z-index:8; overflow:hidden; }
#deepWater {
  position:absolute; inset:0;
  background-color:#5fd4e0;
  background-image:linear-gradient(rgba(4,35,58,.06), rgba(2,18,42,.24)), url("assets/backgrounds/deep.webp");
  background-position:center; background-size:cover; background-repeat:no-repeat;
  background-blend-mode:multiply, soft-light;
  transition:background-color 1.1s ease;
}
/* the light cone: the child carries it, so the reveal is theirs */
#deepLight {
  position:absolute; inset:0; pointer-events:none; --r:100%;
  /* ellipse, not circle: a circle radius must be a length, so `circle 67%` is
     invalid and silently kills the whole declaration (and with it the darkness) */
  background:radial-gradient(ellipse var(--r) var(--r) at 22% 62%, rgba(255,247,232,.30) 0%, rgba(255,247,232,.13) 45%, rgba(0,0,0,.62) 100%);
  transition:background 1.1s ease;
}
#deepDiver {
  position:absolute; left:22%; bottom:38%; transform:translate(-50%,50%);
  width:132px; height:106px; line-height:1; z-index:3; animation:deepBob 3.4s ease-in-out infinite;
}
#deepDiver img {
  display:block; width:100%; height:100%; object-fit:contain; object-position:center bottom;
  user-select:none; -webkit-user-drag:none;
}
@keyframes deepBob { 0%,100% { margin-bottom:0; } 50% { margin-bottom:10px; } }
#deepCreature {
  position:absolute; right:2%; bottom:45%; width:min(72%, 350px); font-size:46px; z-index:3; text-align:center;
  filter:brightness(.06) saturate(0); opacity:.75; transition:filter .5s ease, opacity .5s ease, transform .5s ease;
  animation:deepDrift 7s ease-in-out infinite;
}
#deepCreature.revealed { filter:none; opacity:1; }        /* readable the moment your light finds it */
#deepCreature.flip .deepCrSprite { animation:deepFlip .6s; }
.deepCrSprite { display:block; width:100%; height:150px; }
.deepRuinArt { display:block; width:100%; height:100%; object-fit:contain; }
@keyframes deepFlip { 50% { transform:rotate(-18deg) scale(1.18); } }
@keyframes deepDrift { 0%,100% { transform:translateX(0); } 50% { transform:translateX(-14px); } }
.deepCrBubble {
  display:flex; justify-content:center; margin:6px auto 0; padding:5px 9px;
  max-width:100%; font-size:16px; letter-spacing:0; overflow:hidden;
}
.ruinGap { display:inline-block; width:.2em; }
.ruinSlot {
  display:inline-flex; align-items:center; justify-content:center; min-width:1.35em; height:1.35em;
  border-radius:5px; color:#805416; background:#ffe19a; outline:2px dashed #a56b1d;
  font-weight:1000; line-height:1;
}
.ruinSlot.runeSlot { border-radius:50%; color:#315b73; background:#c9efff; outline-color:#49819e; }
.ruinSlot.next { animation:ruinSlotGlow .75s ease-in-out infinite alternate; }
.ruinFixed { opacity:.72; }
.ruinFill { color:#164d68; text-shadow:0 0 7px #75d7ff; }
@keyframes ruinSlotGlow { to { transform:translateY(-2px); box-shadow:0 0 0 3px #ffd16688; } }
#exprText .ruinSlot { font-size:.9em; vertical-align:middle; }
.deepWisp {
  position:absolute; transform:translateX(-50%); z-index:3;
  width:44px; height:44px; border-radius:14px; background:radial-gradient(#fff9ec,#bfe9ff);
  color:#12496b; font-weight:900; font-size:20px; display:flex; align-items:center; justify-content:center;
  box-shadow:0 0 14px #9fe3ff88, 0 3px 0 #6aa9c8; border:2px solid #ffffffcc;
  animation:deepWispBob 3s ease-in-out infinite;
}
@keyframes deepWispBob { 0%,100% { margin-bottom:0; } 50% { margin-bottom:9px; } }
#deepTop { position:absolute; top:8px; left:0; right:0; display:flex; align-items:flex-start; gap:8px; padding:0 10px; z-index:5; }
#deepMeter { background:#0b1f33cc; border-radius:14px; padding:5px 12px; font-weight:900; font-size:16px; font-variant-numeric:tabular-nums; }
#deepMeter b { font-size:20px; color:#9fe3ff; }
#deepZoneV { font-size:11px; font-weight:600; opacity:.75; letter-spacing:.4px; }
/* oversized and always there: surfacing must never feel like a hidden option */
#deepUp {
  margin-left:auto; background:linear-gradient(#ffd166,#ff9f1c); color:#4a2400; font-weight:900; font-size:16px;
  border-radius:999px; padding:12px 18px; min-height:48px; box-shadow:0 4px 0 #c47407; white-space:nowrap;
}
#deepUp:active { transform:translateY(2px); box-shadow:0 2px 0 #c47407; }
#deepBubbleWrap {
  position:absolute; left:10px; top:78px; bottom:14px; width:26px; border-radius:999px;
  background:#0b1f3399; border:2px solid #2c5378; overflow:hidden; display:flex; align-items:flex-end; z-index:5;
}
#deepBubbleFill { width:100%; height:100%; background:linear-gradient(#bfe9ff,#5fd4e0); transition:height .3s linear; }
#deepBubbleFill.low { background:linear-gradient(#ffd9a0,#ff9f1c); animation:deepLow 1.1s ease-in-out infinite; }
@keyframes deepLow { 50% { opacity:.55; } }
#deepBubbleIcon { position:absolute; top:-2px; left:4px; font-size:15px; }
#deepBestLine {
  position:absolute; left:0; right:0; top:46%; border-top:2px dashed #ffffff55; text-align:right;
  font-size:11px; color:#ffffffcc; padding-right:8px; z-index:2; display:none;
}
#deepOv.descending #deepWater { animation:deepRise .5s; }
@keyframes deepRise { from { transform:translateY(-14px); } to { transform:translateY(0); } }
.deepWhale { position:absolute; left:110%; bottom:52%; width:230px; height:150px; opacity:.62; z-index:2; animation:deepWhaleSwim 9s linear forwards; pointer-events:none; }
.oldHushArt { width:100%; height:100%; object-fit:contain; }
@keyframes deepWhaleSwim { to { left:-30%; } }
@media (max-width:520px) {
  #deepUp { font-size:14px; padding:11px 14px; }
  #deepCreature { font-size:38px; }
  #deepDiver { width:110px; height:88px; }
}

/* ===== skill crews (docs/spec-skill-crews.md) ===== */
/* colored spark flyers — tinted ✦ per rune, matching the rune button colors */
.flyer.sparkFly { font-size:18px; font-weight:900; text-shadow:0 0 8px currentColor; }
.sparkFly.op-add { color:#7ed957; }
.sparkFly.op-sub { color:#64b5f6; }
.sparkFly.op-mul { color:#ce93d8; }
.sparkFly.op-div { color:#4dd0e1; }
.crewTally { font-weight:900; }
.crewTally.op-add { color:#7ed957; }
.crewTally.op-sub { color:#64b5f6; }
.crewTally.op-mul { color:#ce93d8; }
.crewTally.op-div { color:#4dd0e1; }
.crewNeed { opacity:.7; }

/* Trained crews live in a compact status strip below the map. This keeps the
   hub geography readable while preserving one-tap crew visits and 44px touch
   targets. The row collapses completely until the first crew is trained. */
#crewLayer {
  display:flex; align-items:center; justify-content:center; gap:8px; min-width:0;
  padding:5px 12px; background:#10263b; border-top:2px solid #274b6b; z-index:5;
}
#crewLayer:empty { display:none; }
.crewSprite {
  position:relative; flex:1 1 82px; max-width:100px; min-width:56px; min-height:58px;
  padding:3px 8px; cursor:pointer; border-radius:13px;
  display:flex; align-items:center; justify-content:center; gap:2px;
  background:#1b3a56; box-shadow:inset 0 0 0 1px #356184;
}
.crewGlyph { width:52px; height:52px; line-height:1.1; display:inline-block; animation:crewBob 2.8s ease-in-out infinite; }
.crewArt { display:block; width:100%; height:100%; object-fit:contain; }
.crewTag { width:19px; height:19px; flex:0 0 19px; line-height:1; opacity:.95; margin-left:-7px; }
.crewResourceArt { width:100%; height:100%; object-fit:contain; }
.crewSprite.sleepy .crewGlyph { filter:saturate(.5) brightness(.85); animation-duration:5.5s; }
.crewSprite.sleepy .crewTag { animation:crewDoze 2.4s ease-in-out infinite; }
@keyframes crewBob { 0%,100% { transform:translateY(0); } 50% { transform:translateY(-3px); } }
@keyframes crewDoze { 0%,100% { opacity:.5; } 50% { opacity:1; } }

/* training modal bits */
.crewEqGrid { display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:9px; margin:6px 0 10px; }
.crewEq {
  min-height:52px; padding:9px 8px; border-radius:14px; background:#fff7e8; color:#4a2e19;
  font-size:18px; font-weight:900; box-shadow:0 3px 0 #c98d3f; white-space:nowrap;
}
.crewEq:active:not(:disabled) { transform:scale(.95); }
.crewEq.caught {
  background:#c8f7c5; color:#173b17; box-shadow:0 3px 0 #4caf50; cursor:default;
  text-decoration:line-through; text-decoration-thickness:2px;
}
.crewEq.caught::after { content:" 🕵️"; text-decoration:none; }
.crewEq.solidFlash { animation:shake .4s; background:#dfe8f2; }
.crewWrongVal { color:#c0392b; opacity:.75; margin-right:2px; }
.crewMeter {
  height:12px; max-width:220px; margin:2px auto 12px; border-radius:999px;
  background:#0d1b2a; border:1px solid #2c5378; overflow:hidden;
}
.crewMeterFill { height:100%; border-radius:999px; background:linear-gradient(90deg,#7ed957,#c5e063); transition:width .5s; }
.crewHead { margin-top:14px; border-top:2px solid rgba(255,255,255,.12); padding-top:10px; }

@media (max-width:520px) {
  .crewEq { font-size:15px; min-height:48px; }
  #crewLayer { gap:5px; padding:4px 6px; }
  .crewSprite { min-width:52px; min-height:52px; padding:2px 4px; }
  .crewGlyph { width:45px; height:45px; }
  .crewTag { width:16px; height:16px; flex-basis:16px; }
}

@media (prefers-reduced-motion: reduce) {
  .crewGlyph, .crewSprite.sleepy .crewTag, .crewEq.solidFlash { animation:none !important; }
  .crewMeterFill { transition:none; }
}

@media (prefers-reduced-motion: reduce) {
  .token, .bubble.matched, #btnGo.ready, .node.selected .nodeSprite, #btnFire, #trailBoard.fresh, .chip.trashable { animation:none !important; }
  #avatar.walking #avatarSprite, #avatar.chopping #avatarSprite { animation:none !important; }
  .rushCard.yay, .rushCard.nope { animation:none !important; }
  .rushLantern { transition:none; }
  /* the Deep: keep the reveal legible, drop the drifting and the descend lurch */
  #deepDiver, #deepCreature, .deepWisp, .ruinSlot.next, #deepBubbleFill.low, #deepOv.descending #deepWater { animation:none !important; }
  .deepWhale { animation:deepWhaleSwim 9s linear forwards; }   /* it must still cross, just once and calmly */
  #campGlow { transition:opacity .3s; }   /* light expansion becomes a static state change */
  .hubExit-pond.pondUnfold, .hubExit-pond.pondArrive { animation:none !important; }   /* belt-and-suspenders: JS already skips adding these classes */
  .hubExit-dock.dockUnfold { animation:none !important; }   /* belt-and-suspenders: JS already skips adding this class */
  /* gate: vine-snap / surge become dissolves with static before-after states */
  .gateStone.surge, .gateBridge.open, .gateShake, .gateChoices button.wrongPick { animation:none !important; }
  .gateBridge.open { box-shadow:0 0 9px #ffd16655; }
  .gateStone .vine.snapped { transform:translateX(-50%); }
  /* zone crossing: fade becomes an instant swap (JS skips it); banner shows statically */
  #scene::after { transition:none; }
  .zoneBanner { animation:none; }
}

/* Camp Hub opening set piece (docs/spec-camp-hub.md "Opening moments"): the
   barrier unfolds once, on the Camp return after the child earned it. Motion
   only — the modal explanation is identical with or without it, so nothing
   about the MEANING depends on the animation running. */
@keyframes hubUnfold {
  0%   { transform: translate(-50%, -50%) scale(.55) rotate(-8deg); opacity: 0; }
  60%  { transform: translate(-50%, -50%) scale(1.12) rotate(2deg); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1) rotate(0); opacity: 1; }
}
.hubExit.hubUnfold { animation: hubUnfold .9s cubic-bezier(.22,1.2,.36,1) both; z-index: 3; }
@media (prefers-reduced-motion: reduce) { .hubExit.hubUnfold { animation: none; } }

/* Property Workshop — the physical rectangle leads; equations name what the
   turn showed. Fixed small cells keep the array legible at phone widths. */
.rotateTable { display:flex; justify-content:center; margin:10px 0 12px; }
.rotateGrid { display:grid; grid-template-columns:repeat(var(--rotate-cols), 22px); gap:3px; padding:7px; background:#8c5b35; border:3px solid #f6d48b; border-radius:10px; box-shadow:0 3px 0 #54321f; }
.rotateGrid i { width:22px; height:22px; border-radius:4px; background:#f5c85b; border:2px solid #c9852c; box-sizing:border-box; }
.rotateCompare { display:flex; align-items:center; justify-content:center; gap:9px; margin:8px 0 12px; }
.rotateCompare > div { display:grid; justify-items:center; gap:5px; font-size:13px; }
.rotateCompare .rotateGrid { transform:scale(.8); margin:-5px; }
.rotateCompare > span { font-size:25px; color:#f6d48b; }
@media (max-width:360px) { .rotateGrid { grid-template-columns:repeat(var(--rotate-cols), 18px); gap:2px; padding:5px; } .rotateGrid i { width:18px; height:18px; } }

.nestedCrates { display:flex; flex-wrap:wrap; justify-content:center; gap:6px; max-width:260px; margin:10px auto 12px; padding:8px; border:3px dashed #f6d48b; border-radius:12px; background:#513b2d; }
.nestedCrate { display:grid; grid-template-columns:repeat(2, 11px); gap:2px; padding:4px; border:2px solid #c9852c; border-radius:4px; background:#9f6a3e; }
.nestedCrate i { width:11px; height:11px; border-radius:2px; background:#f5c85b; }
.nestedCrate.regrouped { border-color:#9be26d; background:#58784b; }
.splitArray { display:flex; align-items:center; justify-content:center; gap:7px; margin:10px auto 12px; }
.splitArray > b { color:#f6d48b; font-size:22px; }
.splitGrid { display:grid; grid-template-columns:repeat(var(--split-cols), 18px); gap:2px; padding:5px; background:#8c5b35; border:3px solid #f6d48b; border-radius:8px; }
.splitGrid i { width:18px; height:18px; box-sizing:border-box; border-radius:3px; background:#f5c85b; border:2px solid #c9852c; }
