  * { margin:0; padding:0; box-sizing:border-box; }
  html { -webkit-text-size-adjust:100%; }
  body { font-family:'Nunito',sans-serif; overflow:hidden; background:#3DA6D4;
    overscroll-behavior:none; touch-action:manipulation;
    -webkit-user-select:none; user-select:none;
    -webkit-tap-highlight-color:transparent; -webkit-touch-callout:none; }

  /* ===== ONE-CANVAS SCALING (unify units across tablets) — src/canvas-scale.js =====
     The whole visual scene lives inside #app-canvas, a fixed-size logical canvas at the
     iPad landscape ratio. A single transform:scale fits it to any tablet uniformly, so
     every tab scales as one. In CONTAIN mode the leftover strips on differently-shaped
     tablets become the letterbox where Michael's decorative border will mount (Phase 6).
     --canvas-w/--canvas-h are the ONE place the reference size is defined (mirror in
     canvas-scale.js). #app-canvas sits at a LOW z-index so all chrome (menu 55-60,
     panels 84-90, postcard 86) paints above it; the scene's own layering is preserved
     WITHIN this stacking context. */
  :root { --canvas-w:1194; --canvas-h:834; --app-scale:1; }
  #app-canvas {
    position:fixed; left:50%; top:50%;
    width:calc(var(--canvas-w) * 1px);
    height:calc(var(--canvas-h) * 1px);
    transform:translate(-50%,-50%) scale(var(--app-scale,1));
    transform-origin:center center;
    overflow:hidden;   /* crisp letterbox edge; zoomed map clips to the canvas like a viewport */
    z-index:1;
  }

  /* MAP — cartoony */
  #map-wrap { position:fixed; inset:0; }
  /* EXPLORE STAGE (2026-07-18 flavor 1): world-svg + glow-svg + labels-svg move as ONE element
     under the shared camera (src/map-cam.js). pointer-events:none on the stage itself so the
     empty box never eats taps on other tabs (children re-enable their own). */
  #explore-stage { position:absolute; inset:0; transform-origin:0 0;
                   transform:translate3d(0,0,0); pointer-events:none;
                   /* UNIFORM TAB FADE (2026-07-18): the live map dissolves with the SAME curve
                      and timing the mode overlays use — no more display:none snap/flash on tab
                      switch. visibility rides the same transition: it flips to hidden only
                      AFTER the fade lands (and back to visible instantly), so the hidden map
                      costs nothing to paint on other tabs. */
                   transition:opacity 2.363s cubic-bezier(.65,0,.35,1), visibility 2.363s; }
  body:not([data-mode="explore"]) #explore-stage { opacity:0; visibility:hidden; }
  /* ★ FIRST-PAINT PRE-DRAW (2026-07-18, the Town hamburger-prepaint trick): shortly after
     launch the stage renders for REAL at 0.5% opacity — invisible to the eye, but the browser
     pays the one-time cost of drawing all 177 country paths + stars + labels THEN, not during
     the first Animals→Countries crossfade (Michael's "first time still kinda flashes").
     Class added/removed once by src/explore-cam.js; snap in/out (transition:none), like Town's. */
  body.explore-prepaint:not([data-mode="explore"]) #explore-stage {
    opacity:0.005; visibility:visible; transition:none; }
  #explore-stage #world-svg { pointer-events:auto; }
  #world-svg { width:100%; height:100%; display:block; }

  .cont { transition: filter .3s; }

  /* Continent labels */
  .ctlabel {
    font-family:'Fredoka One',cursive;
    fill:white; paint-order:stroke;
    stroke-width:5; stroke-linejoin:round;
    pointer-events:none;
  }

  /* TITLE BANNER */
  #title-banner {
    position:fixed; top:10px; left:50%;
    transform:translateX(-50%);
    z-index:25; pointer-events:none;
  }
  #title-banner svg { width:min(340px,60vw); height:auto; display:block; }

  /* ROPE BORDER FRAME (Michael's design 2026-07-18): the rope lives IN THE MAP WORLD.
     One .map-rope ring sits inside each map stage (#anim-stage/#lean-stage/#explore-stage);
     src/rope-frame.js pins it onto exactly the slice of the map this tablet shows at rest
     (sets left/top/width/height + --rope-w), so at rest it hugs the screen edge on EVERY
     tablet size/aspect, and zooming carries it off-screen with the map. It rides the stage's
     one compositor transform — zero per-frame cost; shows/hides with its stage's own fade.
     Built from 8 background-image pieces, NOT border-image: iPad WebKit refuses to paint
     border-image in this app's layout (simulator bisect 2026-07-18) while backgrounds paint
     fine. Geometry matches a 9-slice: corners are var(--rope-w) squares, edges tile between
     them with background-repeat:round so only whole twists appear (seamless at any width). */
  .map-rope {
    position:absolute; z-index:30;   /* above the map art, below pins (60) / rings (59) */
    pointer-events:none;
  }
  /* Pre-stitch fallback = the ONE master rope stretched corner-to-corner (same file the
     stitcher uses, so it's already decoding). rope-frame.js replaces this with the exact
     aspect-correct stitched bitmap the instant the master loads. The empty .r-* piece divs
     left in the markup are unused now (hidden once .stitched) — the 8 cut pieces were deleted. */
  .map-rope { background:url('../assets/rope/rope-master.webp') 0 0 / 100% 100% no-repeat; }
  .map-rope div { display:none; }

  /* COMPASS ROSE (src/compass.js sizes/positions it from data/compass-layout.js):
     one per map stage, rides the zoom like the rope. Above the map art, below the rope (30). */
  .map-compass { position:absolute; z-index:28; pointer-events:none; }

  /* DIM */
  #map-dim {
    position:fixed; inset:0;
    background:rgba(0,0,0,0);
    transition:background .4s ease;
    pointer-events:none; z-index:40; cursor:pointer;
  }
  #map-dim.active { background:rgba(8,18,48,.6); pointer-events:auto; }

  /* PINS */
  /* The pin containers need a real full-screen box: when zoom.js transforms them, they become
     the pins' positioning ancestor, so the pins' left/top percentages must resolve against the
     full map area (a bare 0-height div would collapse every pin onto one line). pointer-events
     pass through the empty container; the pins themselves re-enable them. */
  /* position:ABSOLUTE, not fixed — #pins now lives inside the transformed #anim-stage (2026-07-16
     animals rebuild), and iOS mis-places fixed descendants of a transformed ancestor (the exact
     per-device pin-skew bug from the Landmarks pins saga). Absolute = anchored to the stage. */
  #pins { position:absolute; inset:0; pointer-events:none; }
  /* the ANIMAL pins too — the base .pin rule says fixed (Animals shared it with Landmarks before
     the split); fixed descendants of the ANIMATED #anim-stage would hit the iOS per-device
     mis-placement bug. Absolute resolves against #pins (= the stage box), same geometry.
     ALSO (zoom-smoothness pass, same day): the base .pin transform + pinFloat keyframes carry
     calc(var(--map-zoom)) — WebKit runs var()/calc() transform animations on the MAIN thread
     (Landmarks landmine #3), which is 12 main-thread animations ticking under every gesture.
     --map-zoom is dead on this tab (the stage zooms, pins ride), so the Animals pins get
     plain-number overrides: same look, compositor-run. */
  /* FIXED-SIZE BOX + PX MARGINS — the full Landmarks pin coordinate system, because it is the
     only centering that survives the --a-hi re-raster flip on iOS. translate(-50%,-50%)
     re-resolves against the pin's rendered size, so when the 3x flip grew the box every pin
     SHIFTED off its coordinate (Michael, TEST 107). A fixed box centered by margins that carry
     the same --a-hi factor cannot move: box 3x, margins 3x, scale(1/3) shows it at the same
     size — the CENTER is invariant by construction. */
  #pins .pin { position:absolute;
               width:calc(110px * var(--a-hi,1));
               height:calc(110px * var(--a-hi,1));
               margin-left:calc(110px * var(--a-hi,1) / -2);
               margin-top:calc(110px * var(--a-hi,1) / -2);
               transform:scale(calc(1 / var(--a-hi,1)));
               justify-content:center;              /* flex column from the base .pin rule */
               animation:apinFloat 3s ease-in-out infinite; }
  @keyframes apinFloat {
    /* translateY BEFORE scale = the bob stays a true 9px (same as the Landmarks lmFloat) */
    0%,100%{ transform:translateY(0) scale(calc(1 / var(--a-hi,1))) }
    50%{ transform:translateY(-9px) scale(calc(1 / var(--a-hi,1))) }
  }
  /* Ring: centered with calc margins (static, allowed), sized in the pin's PRE-SCALE space
     (x --a-hi) so the visible circle stays the same size when deep; ping animates a plain
     scale, exactly like the Landmarks lmPing. */
  #pins .pin-ring {
    left:50%; top:50%;
    /* 39px = 3.3% of the FIXED 1194px canvas. vw units sized the ring to the browser WINDOW
       while everything else follows the canvas, so rings ballooned in wide windows (Michael
       2026-07-17, "owl and dog rings touch"). Fixed px = identical in every window + iPad. */
    width:calc(39px * var(--a-hi,1)); height:calc(39px * var(--a-hi,1));
    margin-left:calc(39px * var(--a-hi,1) / -2);
    margin-top:calc(39px * var(--a-hi,1) / -2);
    border-width:calc(3px * var(--a-hi,1));
    transform:none;
    /* 3s period = LOCKED to the apinFloat bounce cycle (Michael 2026-07-17): every pulse is
       born at the bottom of its pin's bounce, forever in sync. The visible expansion is still
       the approved 2.4s (0→80% of the cycle); the last 0.6s the ring just stays invisible. */
    animation:apinPing 3s ease-out infinite;
  }
  @keyframes apinPing {
    0%{ transform:scale(.5); opacity:.9 }
    80%,100%{ transform:scale(2.4); opacity:0 }
  }
  /* Each pin on its OWN GPU layer so the dive RE-RASTERS the small pin crisp instead of blurring
     a scaled base texture — the same pin-sharpness fix the Landmarks tab uses. */
  body[data-mode="animals"] #pins .pin { will-change:transform; }
  /* NO drop-shadow filter on the Animals pins AT ALL (2026-07-16, Michael: "zooming in is slow
     to respond"). The motion-only version made it WORSE at the endpoints: removing the filter at
     dive START re-rastered 12 pin layers, and re-adding it on LANDING re-drew 12 shadows at 3× —
     the same shadow-raster-at-deep-zoom that froze Mom's iPad 6s on Landmarks. Landmarks cured it
     by dropping the filter on that tab permanently; same medicine here. */
  body[data-mode="animals"] .pin-emoji,
  body[data-mode="animals"] .pin-img { filter:none; }
  /* (body.anim-zooming still gets set by src/anim-map.js while the camera moves, but nothing
     needs to hide anymore: the bob + ring ping above are compositor-run, so they keep playing
     through the dive — Landmarks parity. The earlier pause/display:none hid a main-thread cost
     that no longer exists, and the toggle itself caused a start/land hiccup.) */
  /* ===== ANIMALS STAGE (src/anim-map.js, 2026-07-16) — the Landmarks #lean-stage model exactly:
     one element (baked labeled map + animal pins), one compositor transform, pins ride the zoom. */
  #anim-stage { position:fixed; inset:0; transform-origin:0 0; will-change:transform; }
  #anim-map-canvas { position:absolute; inset:0; width:100%; height:100%;
                     display:block; pointer-events:none; will-change:transform; }
  /* (2026-07-18 uniform fade: the live-map layers no longer display:none-snap per mode —
     the whole #explore-stage fades out on every non-explore tab; rule lives with the stage.) */
  /* Landmark pins anchor to the MAP, not the screen (Michael 2026-07-16). #lm-pins/#lm-rings live
     inside the transformed #lean-stage; position:ABSOLUTE means their left/top % resolve against the
     map stage and ride its zoom cleanly. position:fixed would tie them to the viewport, which iOS
     WebKit mis-places once an ancestor is transformed — that was the per-device pin SKEW (fine on
     Mom's iPad, off on Daniel's). Absolute = locked to the map coordinate on every device. */
  #lm-pins, #lm-rings { position:absolute; inset:0; pointer-events:none; }
  #pins .pin, #lm-pins .pin { pointer-events:auto; }
  .pin {
    position:fixed;
    /* counter-scale: map zooms under the pin, icon keeps its screen size (maps-app standard).
       --pin-ctr overrides the formula per pin — the Landmarks camera (src/lean-map.js) sets it
       so pins PLUMP UP toward finger size while the camera dives; the fallback keeps every
       other pin (and the whole app without lean-in) exactly as before. */
    transform:translate(-50%,-50%) scale(var(--pin-ctr, calc(1/var(--map-zoom,1))));
    cursor:pointer; z-index:60;   /* well above glow (50) AND labels (51) — the glow's drop-shadow filter can otherwise paint over the pin on its own compositor layer */
    isolation:isolate;            /* force a new stacking context so the pin-emoji's own drop-shadow filter can't sneak under the glow's layer */
    display:flex; flex-direction:column; align-items:center; gap:3px;
    animation:pinFloat 3s ease-in-out infinite;
  }
  /* Pin float delays are set per-pin by buildPins() in src/animals.js — each pin gets a negative
     animation-delay based on its `left` position so the bounce rolls left→right across the map.
     Replaces the old 3-pin nth-child stagger (which only ever covered the first 3 pins). */
  @keyframes pinFloat {
    /* the counter-scale must live in these frames too — the animation owns `transform` while it runs */
    0%,100%{transform:translate(-50%,-50%) translateY(0) scale(var(--pin-ctr, calc(1/var(--map-zoom,1))))}
    50%{transform:translate(-50%,-50%) translateY(-9px) scale(var(--pin-ctr, calc(1/var(--map-zoom,1))))}
  }
  /* --a-hi = the Animals deep-zoom re-raster flip (2026-07-16, the Landmarks --lm-hi trick):
     at world view it's 1 (raster == display, nothing to shimmer); a dive sets body.anim-deep
     (src/anim-map.js glideTo, ONCE per glide) and the pin CONTENT re-rasters 3x — its layout
     font-size triples while a static scale(1/3) shows it at the same size, so the texture holds
     3x detail exactly where the zoom displays it ~1:1. The flip lives on .pin-emoji/.pin-img
     (children), NOT in the bob keyframes — apinFloat stays plain numbers = compositor-run. */
  #pins .pin { --a-hi:1; }
  body.anim-deep #pins .pin { --a-hi:3; }
  .pin-emoji {
    font-size:calc(clamp(2rem,3.6vw,3.2rem) * var(--a-hi,1));
    filter:drop-shadow(2px 5px 8px rgba(0,0,0,.55));
    line-height:1; transform:scale(1.28);   /* baked in: every pin now renders at what used to be the HOVER size; hover-grow removed (bee excluded below). The 1/--a-hi scale-back lives on the PIN BOX, not here — content just grows with the box via font-size. */
  }
  .pin-img {
    width:calc(clamp(3rem,5.4vw,4.6rem) * var(--a-hi,1)); height:auto;
    display:block; transform:scale(1.2);   /* baked-in hover size; hover-grow removed; scale-back lives on the pin box */
    filter:drop-shadow(2px 4px 6px rgba(0,0,0,.4));
  }
  /* TEST 34 (Michael): postcard pins were hard to hit — "tiny, I have to touch it a million
     times". An invisible halo widens each landmark pin's TOUCH target by 14 logical px per side
     without changing the icon's visual size (Michael's Pin Layout sizes untouched). Pseudo-
     elements hit-test as part of their pin, so taps on the halo behave exactly like pin taps.
     Dense areas (Europe) may have slivers of halo overlap — the topmost pin wins there, and the
     icons themselves are never ambiguous. */
  /* ===== LANDMARK PIN COORDINATE SYSTEM (Michael 2026-07-16 — keep it DEAD SIMPLE) =====
     One map point per landmark (left/top % of the 1194x834 stage). The pin is a FIXED-SIZE
     square box (--lm-box, plain px — the stage is always 1194 logical px wide, so px are
     map-proportional) whose CENTER is placed on that point via fixed px margins. The icon is
     flex-centered inside the box. NO percent-of-own-size transforms: translate(-50%,-50%)
     depends on the box's rendered size, and iOS resolves it against a 0-sized box when the
     bob animation starts before the icon image loads — baking the pin's TOP-LEFT corner onto
     the coordinate (the "ring at the pin's corner" bug). Fixed numbers can't do that.
     The ring (#lm-rings .lm-ring) is the IDENTICAL box at the identical point. */
  :root { --lm-box:76px; --lm-ring:39px; }   /* 6.4% / 3.3% of the 1194px map = Michael's approved sizes */
  /* CRISP AT DEEP ZOOM (restored on the fixed-box system): the box is built --lm-hi× oversized
     (still a fixed number — 3×76 = 228px) so the icon texture rasterizes at 3× resolution, then
     the whole box is scaled back by 1/--lm-hi. scale() is a PURE NUMBER on a fixed-size box —
     unlike translate(-50%,-50%) it never resolves against a not-yet-loaded image size, so the
     iOS corner-bake bug cannot return. Center stays put (scaling about the box's own center).
     Net on-screen size unchanged; texture is 3× = crisp when the dive scales it up. */
  /* --lm-hi is ZOOM-CONDITIONAL (flash fix v2, 2026-07-16): 1 at world view — texture drawn at
     display size, so the bob has nothing to shimmer (the always-3× version FLASHED while bouncing:
     each bounce redrew a big texture downscaled to a small pin; moving the bob to an inner element
     did NOT help — TEST 98, reverted — because the downscale itself was the shimmer). Diving in
     sets body.lean-deep (src/lean-map.js glideTo) and the pins re-raster ONCE at 3× — crisp at the
     dive scale where they display ~1:1. Box size/margins/scale all follow --lm-hi, so the pin's
     CENTER never moves and pin/ring stay colocated in both states. */
  #lm-pins .pin, #lm-rings .lm-ring { --lm-hi:1; }
  body.lean-deep #lm-pins .pin, body.lean-deep #lm-rings .lm-ring { --lm-hi:3; }
  #lm-pins .pin {
    position:absolute;
    width:calc(var(--lm-box) * var(--pin-size,1) * var(--lm-hi,1));
    height:calc(var(--lm-box) * var(--pin-size,1) * var(--lm-hi,1));
    margin-left:calc(var(--lm-box) * var(--pin-size,1) * var(--lm-hi,1) / -2);
    margin-top:calc(var(--lm-box) * var(--pin-size,1) * var(--lm-hi,1) / -2);
    transform:scale(calc(1 / var(--lm-hi,1)));
    align-items:center; justify-content:center;   /* (flex column from the base .pin rule) */
    animation:lmFloat 3s ease-in-out infinite;    /* bob WITHOUT any percent translate */
  }
  /* ★ TAP CIRCLE UNLINKED FROM VISUAL SIZE (Michael 2026-07-18): the pin element is visuals-only
     (pointer-events none, as are all its art children); the ONLY tappable thing is the .pin-tap
     child — a centred CIRCLE of diameter 76px × --tap-mul (logical px), set per pin by
     buildLandmarkPins from pin-layout.js `tap`. Default --tap-mul = visual size + 0.37 (the old
     14px halo folded in), so untouched pins feel identical — but Michael can now make a tap
     zone smaller than its icon. Taps on the circle BUBBLE to the .pin's own listeners.
     (This replaces the old ::after halo + world-view halo-shrink: the tap circle is one honest
     shape at every zoom — WYSIWYG with the Pin editor's TAP ZONES overlay.) */
  #lm-pins .pin { pointer-events:none; }
  #lm-pins .pin * { pointer-events:none; }
  #lm-pins .pin .pin-tap {
    pointer-events:auto;
    position:absolute; left:50%; top:50%;
    width:calc(var(--lm-box) * var(--tap-mul,1) * var(--lm-hi,1));
    height:calc(var(--lm-box) * var(--tap-mul,1) * var(--lm-hi,1));
    margin-left:calc(var(--lm-box) * var(--tap-mul,1) * var(--lm-hi,1) / -2);
    margin-top:calc(var(--lm-box) * var(--tap-mul,1) * var(--lm-hi,1) / -2);
    border-radius:50%;
  }
  #lm-pins .pin-icon { width:100%; height:100%; }
  #lm-pins .pin-thumb { max-width:100%; max-height:100%; }   /* icon fills the fixed box, aspect kept */
  @keyframes lmFloat {
    /* the animation owns `transform` while it runs, so the 1/--lm-hi scale must live in the
       frames too. translateY comes AFTER scale in the list = the bob stays a true 9px. */
    0%,100%{ transform:translateY(0) scale(calc(1 / var(--lm-hi,1))) }
    50%{ transform:translateY(-9px) scale(calc(1 / var(--lm-hi,1))) }
  }
  /* Europe dive-first (Michael 2026-07-17): at world view the Europe pile's pins are NOT
     tappable at all — the tap falls through to the map and lean-map dives into the continent.
     Leaned in (lean-world clears at scale 1.5) they become normal tappable pins. .pin-eu is
     tagged by buildLandmarkPins (src/landmarks.js) from the LEAN_HOTSPOTS eu box. */
  body.lean-world #lm-pins .pin.pin-eu { pointer-events:none; }
  /* the tap CIRCLE (2026-07-18 unlink) sets its own pointer-events:auto, which IGNORES the
     parent's none — without this line the Europe dive-first rule silently died the moment the
     tap zones were unlinked. The circle itself must go quiet at world view too. */
  body.lean-world #lm-pins .pin.pin-eu .pin-tap { pointer-events:none; }
  .pin-thumb {
    /* landmark thumbnail icons (assets/thumbnails/, 256px lossless WebP sources).
       Michael picked ~96px on-screen (2026-07-06 "middle" size) — clamp tops out there and the
       256px source keeps it crisp on 2x Retina. max-width guards the wide landscape icons so
       neither axis can exceed the picked size; the pin's translate(-50%,-50%) centers the icon
       on the landmark's map coordinate exactly like the emoji pins did. */
    /* --pin-size = per-landmark resting-size multiplier (Michael's Pin Layout editor →
       data/pin-layout.js, applied inline by buildLandmarkPins). The base is a PURE
       proportion of the map width (the map always fills the window), so the pin-to-map
       ratio is identical on every screen AND immune to browser zoom (Michael 2026-07-07 —
       the old rem clamp guardrails stretched with Cmd+plus while the map didn't).
       6.4vw ≈ the ~96px "middle" size Michael picked on his Mac. */
    /* --pin-hi (default 1) renders the icon this many times bigger; the Landmarks pin then scales
       itself DOWN by 1/--pin-hi (see the people-mode rule), so it LOOKS the same size but its texture
       is rasterized at --pin-hi× resolution — staying crisp when the deep-zoom scales it up. */
    max-width:calc(6.4vw * var(--pin-size,1) * var(--pin-hi,1));
    max-height:calc(6.4vw * var(--pin-size,1) * var(--pin-hi,1));
    width:auto; height:auto; display:block;
    filter:drop-shadow(2px 4px 6px rgba(0,0,0,.4));
    -webkit-user-drag:none; user-select:none;
  }
  .pin-label {
    background:white; color:#333;
    font-size:.7rem; font-weight:900;
    padding:3px 9px; border-radius:11px;
    box-shadow:0 2px 6px rgba(0,0,0,.3);
    letter-spacing:.5px;
  }
  .pin-icon{ position:relative; display:flex; align-items:center; justify-content:center; }   /* tight wrapper around the static icon so the ring locks to the icon's centerpoint and bounces with it */
  /* Sonar rings live in a SEPARATE layer beneath the pin icons (Michael 2026-07-16: rings must sit
     BEHIND all pins, never over a neighbour's icon). #lm-rings is a sibling of #lm-pins inside
     #lean-stage at z 59 (< the pins' z 60), so every ring paints under every icon. Each .lm-ring is
     the SAME fixed-size box as its pin, at the same left/top, built in lockstep by
     buildLandmarkPins(); it rides the same lmFloat bob so it stays glued to the icon. */
  /* position:ABSOLUTE — identical anchoring to #lm-pins (the map, not the screen). A leftover
     position:fixed here silently overrode the map-anchoring rule above and put the RINGS in
     screen coordinates while the PINS were in map coordinates = the not-colocated bug on device. */
  #lm-rings{ position:absolute; inset:0; pointer-events:none; z-index:59; }
  /* The ring holder is the IDENTICAL fixed-size box as the pin, at the identical map point —
     same width/height, same px margins, same bob. The circle is flex-centered inside it.
     NO percent-of-own-size translates anywhere in this system (the iOS corner-bake bug). */
  #lm-rings .lm-ring{
    position:absolute;
    width:calc(var(--lm-box) * var(--pin-size,1) * var(--lm-hi,1));
    height:calc(var(--lm-box) * var(--pin-size,1) * var(--lm-hi,1));
    margin-left:calc(var(--lm-box) * var(--pin-size,1) * var(--lm-hi,1) / -2);
    margin-top:calc(var(--lm-box) * var(--pin-size,1) * var(--lm-hi,1) / -2);
    display:flex; align-items:center; justify-content:center;
    transform:scale(calc(1 / var(--lm-hi,1)));   /* identical oversize+scale-back as the pin */
    animation:lmFloat 3s ease-in-out infinite;
  }
  /* .pin-ring is SHARED: animal pins keep it INSIDE .pin-icon (original absolute-centered rule +
     translate ping, unchanged); the LANDMARK ring layer overrides it below with the fixed-box,
     no-percent-translate version. Editing the shared rule alone broke the ANIMAL rings (2026-07-16
     "pulses moved left") — keep the two scoped. */
  .pin-ring {
    position:absolute;
    width:calc(3.3vw * var(--pin-size,1)); height:calc(3.3vw * var(--pin-size,1));
    border-radius:50%;
    border:3px solid rgba(255,255,255,.8);
    top:50%; left:50%;
    transform:translate(-50%,-50%);
    animation:pingRing 2.4s ease-out infinite;
    pointer-events:none;
  }
  @keyframes pingRing {
    0%{transform:translate(-50%,-50%) scale(.5); opacity:.9}
    100%{transform:translate(-50%,-50%) scale(2.4); opacity:0}
  }
  /* LANDMARK ring circle: flex-centered in its fixed-size holder, ping only SCALES —
     no percent-of-own-size translate anywhere in the landmark system (iOS corner-bake bug). */
  #lm-rings .pin-ring {
    position:static; top:auto; left:auto;
    /* sized in the holder's PRE-SCALE space (×--lm-hi) so the visible circle stays 39px */
    width:calc(var(--lm-ring) * var(--pin-size,1) * var(--lm-hi,1));
    height:calc(var(--lm-ring) * var(--pin-size,1) * var(--lm-hi,1));
    border-width:calc(3px * var(--lm-hi,1));   /* pre-scale space: stays a visible 3px */
    transform:none;
    /* 3s period = LOCKED to the lmFloat bounce (Michael 2026-07-17, same as apinPing): every
       pulse is born at the bottom of its pin's dip. Visible expansion unchanged at 2.4s
       (0→80% of the cycle); the last 0.6s the ring stays invisible. SIZE untouched. */
    animation:lmPing 3s ease-out infinite;
  }
  @keyframes lmPing {
    0%{transform:scale(.5); opacity:.9}
    80%,100%{transform:scale(2.4); opacity:0}
  }

  /* FLYER */
  #flyer {
    position:fixed; display:none; z-index:82;
    line-height:1; transform:translate(-50%,-50%);
    cursor:pointer;
    filter:none;   /* GLOW/SHADOW PERF TEST 2026-07-19 (Michael): the flyer's drop-shadow was recomputed every frame as the animal moves/scales/swaps frames — a weak-iPad GPU cost. Removed to test first-play smoothness. Restore tag: anim-glow-shadow-pre-removal. Original: drop-shadow(4px 12px 18px rgba(0,0,0,.55)) */
    will-change:transform;   /* animal-perf Phase 1: keep the flyer on its own compositor layer (old-iPad jank) */
  }
  /* GLOW/SHADOW PERF TEST 2026-07-19 (Michael): the gold language-glow overlay is an
     infinitely-animating drop-shadow-filtered SVG that runs the whole time an animal is open —
     a per-frame GPU cost on the weak iPad. Hidden for the test (feature intact in the DOM; the
     language buttons still work). Restore tag: anim-glow-shadow-pre-removal. */
  #glow-svg .glow-area { display:none !important; }
  /* On water, the foam ripples extend beyond the duck silhouette; drop-shadow would render shadow for
     the ripples too, painting a faint rectangular halo against the ocean. Suppress the shadow once the
     duck is on water (set by the engine entering swim phase). */
  #flyer.on-water { filter:none; }
  #flyer.wiggle { animation:wiggle .45s ease; }
  @keyframes wiggle {
    0%{transform:translate(-50%,-50%) rotate(0deg) scale(1)}
    30%{transform:translate(-50%,-50%) rotate(-4deg) scale(1.05)}
    60%{transform:translate(-50%,-50%) rotate(4deg) scale(1.03)}
    100%{transform:translate(-50%,-50%) rotate(0deg) scale(1)}
  }
  #flyer-hint {
    position:fixed; display:none; z-index:83;
    color:rgba(255,255,255,.78);
    font-size:.78rem; font-weight:700;
    text-align:center; pointer-events:none;
    text-shadow:1px 1px 3px rgba(0,0,0,.5);
    letter-spacing:.3px;
  }

  /* DOG RIG (reference style, alive) */
  .dogrig{display:block;overflow:visible}
  .dogrig .dpart{transform-box:fill-box}
  .dogrig .d-dog{transform-origin:50% 92%; animation:dogBreath 3s ease-in-out infinite}
  .dogrig .d-earL{transform-origin:70% 16%}
  .dogrig .d-earR{transform-origin:30% 16%}
  .dogrig .d-tail{transform-origin:8% 94%; animation:dogTail 1.9s ease-in-out infinite}
  .dogrig .d-eyeL,.dogrig .d-eyeR{transform-origin:50% 50%; animation:dogBlink 4.6s ease-in-out infinite}
  @keyframes dogBreath{0%,100%{transform:scale(1,1)}50%{transform:scale(1.02,0.985)}}
  @keyframes dogTail{0%,100%{transform:rotate(-5deg)}50%{transform:rotate(9deg)}}
  @keyframes dogBlink{0%,90%,100%{transform:scaleY(1)}94%{transform:scaleY(0.12)}}
  #pin-dog .pin-emoji{ font-size:calc(2.9rem * var(--a-hi,1)); }
  #pin-rooster .pin-emoji{ font-size:calc(3.2rem * var(--a-hi,1)); }   /* rooster is tall+narrow; sized up so its pin's visual area matches the dog's */
  #pin-cat .pin-emoji{ font-size:calc(2.7rem * var(--a-hi,1)); }   /* cat sized to ~match the other animals' map pins (its big em made the default rig read oversized) */
  #pin-lion .pin-emoji{ font-size:calc(3.0rem * var(--a-hi,1)); }   /* lion sized up a touch so its map static reads larger (perch size is unaffected — that uses targetFs) */
  #pin-duck .pin-emoji{ font-size:calc(5.75rem * var(--a-hi,1)); }   /* duck's em is small (small Atlantic perch + foam-margined frame), so its pin is enlarged here to read like the other animals' pins */
  #pin-snake .pin-emoji{ font-size:calc(2.8rem * var(--a-hi,1)); }   /* snake's sprite canvas is wide (extended slither) so its coiled body only fills ~43% of it; pin enlarged so the visible body reads similar to the other pins */
  #pin-bee .pin-emoji{ font-size:calc(4.06rem * var(--a-hi,1)); }   /* bee map pin (static + hover) = bee's first-frame size; no hover scaling */
  #pin-bee .pin-emoji{ transform:none; }   /* bee already sits at its hover/first-frame size (4.06rem) -- exclude it from the baked-in 1.28x scale */   /* bee already sits at its hover/first-frame size (4.06rem) -- exclude it from the baked-in 1.28x scale */

  /* CTRL PANEL */
  #ctrl-panel {
    position:fixed; left:0; right:0; bottom:0;
    padding:0 18px 26px;
    z-index:90;
    display:flex; flex-direction:column; align-items:center; gap:11px;
    opacity:0; transform:translateY(24px);
    transition:opacity .4s ease, transform .45s cubic-bezier(.34,1.4,.5,1);
    pointer-events:none;
  }
  #ctrl-panel.open { opacity:1; transform:translateY(0); }
  /* only the interactive bits catch taps; empty space passes through to close */
  #ctrl-panel.open .panel-top, #ctrl-panel.open #subtitle-box,
  #ctrl-panel.open .speed-row, #ctrl-panel.open .action-row, #ctrl-panel.open .lang-slider { pointer-events:auto; }
  /* friendly pop-in when controls appear */
  #ctrl-panel.open .act-btn, #ctrl-panel.open .lang-btn { animation:popIn .42s cubic-bezier(.34,1.7,.5,1) backwards; }
  #ctrl-panel.open .act-btn:nth-child(1){animation-delay:.10s}
  #ctrl-panel.open .act-btn:nth-child(2){animation-delay:.17s}
  #ctrl-panel.open .act-btn:nth-child(3){animation-delay:.24s}
  #ctrl-panel.open .lang-btn:nth-child(1){animation-delay:.30s}
  #ctrl-panel.open .lang-btn:nth-child(2){animation-delay:.36s}
  #ctrl-panel.open .lang-btn:nth-child(3){animation-delay:.42s}
  #ctrl-panel.open .lang-btn:nth-child(4){animation-delay:.48s}
  @keyframes popIn { 0%{opacity:0;transform:scale(.3) translateY(14px)} 100%{opacity:1;transform:scale(1) translateY(0)} }
  .panel-top { display:none; }
  #close-btn {
    background:#F3DCBB; border:none; color:#8A5A28;
    font-family:'Fredoka One',cursive; font-size:.9rem; font-weight:400;
    padding:8px 16px; border-radius:22px; cursor:pointer;
    transition:transform .15s, background .2s; flex-shrink:0;
  }
  #close-btn:hover { background:#EBC999; transform:scale(1.05); }
  #close-btn:active { transform:scale(.95); }
  #panel-name {
    flex:1; text-align:center;
    font-family:'Fredoka One',cursive;
    font-size:clamp(1.4rem,3.4vw,1.9rem);
    color:#fff; letter-spacing:1px;
    text-shadow:0 2px 8px rgba(0,0,0,.55);
  }

  /* SUBTITLE */
  #subtitle-box {
    background:#FFFFFF;
    border-radius:18px;
    padding:11px 18px;
    min-height:54px; text-align:center;
    box-shadow:0 8px 22px rgba(0,0,0,.28);
    max-width:min(560px,90vw);
  }
  #subtitle-main {
    font-size:clamp(.95rem,2.4vw,1.2rem);
    font-weight:800; color:#4A4036;
    line-height:1.45; min-height:1.45em;
    letter-spacing:.3px;
  }
  .syl {
    display:inline-block;
    transition:color .1s ease, transform .1s ease;
    white-space:nowrap;
  }
  .syl.lit {
    color:#FF3060; font-weight:900;
    transform:scale(1.2);
    text-shadow:0 0 10px rgba(255,48,96,.4);
  }
  .ws { display:inline-block; width:.32em; }
  #subtitle-hint { font-size:.76rem; color:#B09877; margin-top:3px; }

  /* SPEED */
  .speed-row { display:flex; align-items:center; gap:8px; justify-content:center; }
  .speed-label { color:#fff; font-size:.82rem; font-weight:800; text-shadow:0 1px 4px rgba(0,0,0,.55); }
  .spd-btn {
    background:#FBEAD2;
    border:2.5px solid #F0D3A8;
    color:#8A5A2A;
    padding:5px 15px; border-radius:20px; cursor:pointer;
    font-family:'Nunito',sans-serif; font-size:.82rem; font-weight:800;
    transition:transform .15s, background .2s, border-color .2s;
  }
  .spd-btn:hover { transform:scale(1.05); }
  .spd-btn:active { transform:scale(.95); }
  .spd-btn.active { background:#FFB84D; border-color:#F59500; color:#fff; }

  /* ACTIONS */
  .action-row { display:flex; gap:12px; flex-wrap:wrap; justify-content:center; }
  .act-btn {
    display:flex; flex-direction:column; align-items:center; gap:3px;
    border:none; border-radius:24px;
    padding:12px 16px; min-width:90px;
    font-family:'Fredoka One',cursive;
    font-size:.84rem; font-weight:400;
    color:#fff; cursor:pointer;
    transition:transform .15s cubic-bezier(.34,1.6,.5,1), filter .15s;
  }
  .act-btn .ico { font-size:1.95rem; line-height:1; filter:drop-shadow(0 2px 2px rgba(0,0,0,.18)); }
  .act-btn .ico img { width:32px; height:32px; display:block; }   /* icon image (replaces emoji) */
  .spd-ico { width:1.25em; height:1.25em; vertical-align:-0.28em; margin-right:3px; }
  .act-btn:hover { transform:translateY(-4px) scale(1.05) rotate(-1.5deg); filter:brightness(1.06); }
  .act-btn:active { transform:translateY(2px) scale(.95); }
  .act-btn.flash { animation:btnFlash .5s ease; }
  @keyframes btnFlash {
    0%,100%{filter:brightness(1)} 40%{filter:brightness(1.7)}
  }
  .btn-name     { background:linear-gradient(160deg,#FF9EC4,#FF5C9A); box-shadow:0 7px 16px rgba(255,92,154,.42); }
  .btn-sound    { background:linear-gradient(160deg,#7BE8B0,#2FC98D); box-shadow:0 7px 16px rgba(47,201,141,.42); color:#06402B; }
  .btn-sentence { background:linear-gradient(160deg,#AAB7FF,#6E7CF0); box-shadow:0 7px 16px rgba(110,124,240,.42); }

  /* LANG */
  .lang-slider { position:relative; width:100%; max-width:680px; height:148px; margin:0 auto; overflow:hidden; touch-action:none; cursor:grab; }
  .lang-slider.grabbing { cursor:grabbing; }
  .lang-item { position:absolute; left:50%; top:50%; display:flex; flex-direction:column; align-items:center; gap:3px; width:60px; will-change:transform,opacity; }
  .lang-item .lf { font-size:7rem; line-height:1; filter:drop-shadow(0 2px 3px rgba(0,0,0,.3)); }
  .lang-item .lf .flag-img { width:96px; height:72px; border-radius:8px; }   /* real flag image (replaces 7rem emoji) */
  .lang-item .ln { font-size:.76rem; font-weight:800; color:#fff; text-shadow:0 1px 4px rgba(0,0,0,.6); white-space:nowrap; }
  .lang-item.center .ln { color:#FFE08A; }

  /* ===== MODE LAYERS (Animals / People / Explore) — smooth cross-fade ===== */
  /* Layers stay in the DOM and fade via opacity so tabs dissolve, not snap. */
  .mode-overlay {
    opacity:0; pointer-events:none;
    transition:opacity 2.363s cubic-bezier(.65,0,.35,1);
  }
  /* ★ Hamburger pre-paint (TEST 124): while the menu is open, the town renders for REAL at 0.5%
     opacity — invisible to the eye but the browser does its one-time paint work, so tapping Town
     opens with zero first-paint stall. Class managed by ui.js (endTownPrepaint). */
  body.town-prepaint:not([data-mode="town"]) #mode-town { opacity:0.005; pointer-events:none; transition:none; }  /* snap, don't fade — 0.005 must be reached instantly for the pre-paint to work */

  body[data-mode="animals"] #mode-animals,
  body[data-mode="people"]  #mode-people,
  body[data-mode="explore"] #mode-explore,
  body[data-mode="home"]    #mode-home,
  body[data-mode="town"]    #mode-town {
    opacity:1; pointer-events:auto;
  }

  .mode-svg {
    opacity:0; pointer-events:none;
    transition:opacity 2.363s cubic-bezier(.65,0,.35,1);
  }
  body[data-mode="people"]  #svg-people,
  body[data-mode="explore"] #svg-explore {
    opacity:1; pointer-events:auto;
  }

  /* ★ MUTUALLY-EXCLUSIVE TAB ASSETS (Michael 2026-07-09). The inactive `.mode-overlay` is
     pointer-events:none, but `#pins .pin` / `#lm-pins .pin` re-enable pointer-events on the pins
     THEMSELVES — and a child re-enabling pointer-events overrides the inactive parent's `none`.
     Result: invisible animal pins stayed CLICKABLE under Landmarks (and vice-versa), and a stray
     tap on a hidden pin fired the wrong tab AND landed a ghost click that closed open postcards
     ("card reaches center then instantly vanishes"). Gate every tab's interactive assets to its
     own mode so only the ACTIVE tab is ever clickable. Higher specificity than the `.pin` rule.
     ★ 2026-07-18 TAP-CIRCLE COMPANION: the tap-zone unlink (af58a2cb) moved the sole tappable
     element to an invisible `.pin-tap` circle CHILD that sets its own `pointer-events:auto` — which
     overrides the ancestor `none` above (exactly the trap the pin-eu rule at line ~304 already
     patches). Without a `.pin-tap`-scoped companion line, the built-once landmark tap circles kept
     stealing taps on the Animals tab after Landmarks was visited (invisible pins opened postcards).
     So gate `.pin-tap` per-mode too. (Animal pins have no `.pin-tap` child today; the line is a
     harmless no-op there and future-proofs them if they ever grow one.) */
  body:not([data-mode="animals"]) #pins .pin,
  body:not([data-mode="animals"]) #pins .pin .pin-tap    { pointer-events:none; }
  body:not([data-mode="people"])  #lm-pins .pin,
  body:not([data-mode="people"])  #lm-pins .pin .pin-tap { pointer-events:none; }
  /* The flyer (a traveling animal) must never appear over another tab. `!important` beats the inline
     `display:block` that closeAnimal sets during travel — closeAnimal only hides it when currentAnimal
     is set, and the travel-only duck can leave it flying over Landmarks (tap-through closed cards). */
  body:not([data-mode="animals"]) #flyer        { display:none !important; }

  /* "coming soon" card for People / Explore */
  .mode-stub {
    position:fixed; top:50%; left:50%;
    transform:translate(-50%,-50%);
    z-index:30; text-align:center;
    background:rgba(255,255,255,.92);
    border:3px solid #B8941E; border-radius:24px;
    padding:26px 34px; max-width:80vw;
    box-shadow:0 10px 30px rgba(0,0,0,.35);
  }
  .stub-emoji { font-size:3.4rem; line-height:1; }
  .stub-title { font-family:'Fredoka One',cursive; font-size:1.7rem; color:#7A4A0A; margin-top:6px; }
  .stub-sub   { font-weight:700; color:#5a4a2a; margin-top:8px; line-height:1.4; }

  /* ===== HAMBURGER MENU ===== */
  #menu-btn {
    position:fixed; top:12px; left:12px; z-index:60;
    width:68px; height:68px; cursor:pointer; border-radius:20px;
    background:#FFEDB5; border:3px solid #8B5A12;
    box-shadow:0 4px 11px rgba(0,0,0,.32);
    display:flex; flex-direction:column; align-items:center; justify-content:center; gap:7px;
    transition:transform .15s;
  }
  #menu-btn:hover { transform:scale(1.08); }
  #menu-btn span { display:block; width:32px; height:4.5px; background:#7A4A0A; border-radius:4px; }
  #menu-scrim {
    position:fixed; inset:0; z-index:55; background:rgba(8,18,48,.45);
    opacity:0; pointer-events:none; transition:opacity .3s;
  }
  body.menu-open #menu-scrim { opacity:1; pointer-events:auto; }
  #menu-drawer {
    position:fixed; top:0; left:0; bottom:0; z-index:58;
    width:250px; max-width:78vw;
    background:linear-gradient(180deg,#FFF7E0,#FFE7B0);
    border-right:3px solid #8B5A12; box-shadow:4px 0 18px rgba(0,0,0,.35);
    transform:translateX(-105%); transition:transform .3s ease;
    padding:14px 12px; display:flex; flex-direction:column; gap:6px;
  }
  body.menu-open #menu-drawer { transform:translateX(0); }
  .menu-head { font-family:'Fredoka One',cursive; font-size:1.3rem; color:#7A4A0A; text-align:center; padding:8px 0 12px; }
  .menu-item {
    display:flex; align-items:center; gap:10px;
    font-family:'Nunito',sans-serif; font-weight:800; font-size:1.05rem; color:#5a3a0a;
    background:transparent; border:none; cursor:pointer; text-align:left;
    padding:11px 12px; border-radius:12px; transition:background .15s;
  }
  .menu-item:hover:not(:disabled) { background:rgba(184,148,30,.22); }
  .menu-item .mi-emoji { font-size:1.3rem; }
  .menu-item.active { background:#FFC93D; color:#5a3a0a; box-shadow:inset 0 0 0 2px #B8941E; }
  .menu-soon { opacity:.5; cursor:default; }
  .menu-soon em { margin-left:auto; font-style:normal; font-size:.65rem; background:#B8941E; color:white; padding:2px 7px; border-radius:8px; }
  .menu-divider { height:2px; background:rgba(184,148,30,.35); margin:6px 4px; border-radius:2px; }

  /* ===== EXPLORE: CLICKABLE COUNTRIES ===== */
  /* In Explore mode the merged continents + continent labels fade out while the
     individual countries fade in — a smooth dissolve, not a hard switch. */
  .cont, .ctlabel { transition:opacity 2.363s cubic-bezier(.65,0,.35,1), filter .3s ease; }
  body[data-mode="explore"] .cont { opacity:0; pointer-events:none; }
  /* continent NAMES stay visible in Explore (label parity across all map tabs, 2026-07-18) */

  /* each country's colour comes from its fill="" attribute (set in buildCountries) */
  .country {
    stroke:#ffffff; stroke-width:0.6;
    stroke-linejoin:round; cursor:pointer;
    transition:filter .15s;
  }
  /* Hover / selected: glow & shimmer gold, the same look used for language areas */
  .country:hover, .country.sel {
    fill:#FFE07A; stroke:#FFC53D; stroke-width:1.1;
    filter:drop-shadow(0 0 6px #FFD24D);
    animation:ctryGlow 2.4s ease-in-out infinite;
  }
  @keyframes ctryGlow{ 0%,100%{opacity:.62} 50%{opacity:.96} }

  /* Tappable marker for tiny countries (currently just Taiwan) so kids can find them */
  .ctry-marker {
    position:fixed; transform:translate(-50%,-50%);
    width:20px; height:20px; border-radius:50%;
    background:#E8784A;            /* set from the country's own colour in JS */
    border:2.5px solid #fff;
    box-shadow:0 2px 5px rgba(0,0,0,.4);
    cursor:pointer; z-index:33;
    transition:transform .15s;
  }
  .ctry-marker:hover { transform:translate(-50%,-50%) scale(1.22); }

  /* Country detail panel — slides up from the bottom */
  #country-panel {
    position:fixed; left:50%; bottom:0; transform:translate(-50%,115%);
    width:min(440px,94vw); z-index:84;
    background:linear-gradient(180deg,#FFF7E0,#FFE7B0);
    border:3px solid #8B5A12; border-bottom:none;
    border-radius:26px 26px 0 0;
    box-shadow:0 -8px 26px rgba(0,0,0,.35);
    padding:18px 18px 28px;
    transition:transform .35s cubic-bezier(.2,.8,.2,1);
  }
  #country-panel.open { transform:translate(-50%,0); }
  .country-hero { text-align:center; margin:2px 0 10px; }
  .country-flag-big { width:62px; height:46px; border-radius:7px; margin:0 auto;
    box-shadow:0 2px 5px rgba(0,0,0,.28), 0 0 0 1px rgba(0,0,0,.12); }
  /* real flag images (assets/flags/*.svg) — uniform rectangles, soft corners, subtle edge */
  .flag-img { display:block; object-fit:cover; border-radius:5px; box-shadow:0 0 0 1px rgba(0,0,0,.15); }

  .panel-close {
    position:absolute; top:9px; right:11px; width:34px; height:34px; z-index:3;
    border:none; border-radius:50%; cursor:pointer; line-height:1;
    background:rgba(139,90,18,.12); color:#7A4A0A; font-size:1.05rem;
  }
  .panel-close:hover { background:rgba(139,90,18,.22); transform:scale(1.08); }

  /* Two "learn rows": a country row and (premium) a capital row. Each is a self-
     contained tinted card, so the free, country-only card still looks complete.
     Globe badge = country, red star badge = capital (matches the map markers). */
  .learn-row {
    display:flex; align-items:center; gap:12px;
    padding:9px 13px; margin:8px 4px 0; border-radius:18px; border:2.5px solid;
  }
  .learn-row.country-row { background:#EAF4FF; border-color:#A7CBEA; }
  .learn-row.cap-row     { background:#FFF2D2; border-color:#E8C45F; }
  .lr-badge { flex:0 0 auto; width:30px; height:30px; display:block; }
  .lr-badge.star  { filter:drop-shadow(0 1px 1px rgba(0,0,0,.28)); }
  .lr-badge.globe { filter:drop-shadow(0 1px 1px rgba(0,0,0,.2)); }
  .lr-name { font-family:'Fredoka One',cursive; font-size:1.15rem; line-height:1.08; color:#7A4A0A;
    flex:1 1 auto; min-width:0; text-align:left;
    /* wrap long country/capital names to at most 2 lines instead of truncating mid-word */
    display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:hidden; }
  .lr-btns { display:flex; gap:9px; flex:0 0 auto; }

  /* Country-card speaker buttons — smaller/softer than the shared .flag-btn used by
     the Landmarks & Household cards (those are intentionally left untouched). */
  #country-panel .flag-btn { width:66px; height:50px; border-width:2.5px; border-radius:13px;
    box-shadow:0 3px 7px rgba(0,0,0,.2); }
  #country-panel .flag-btn .flag-img { width:46px; height:34px; }

  .flag-btn {
    width:114px; height:80px; cursor:pointer;
    border:3px solid #8B5A12; border-radius:16px; background:#fff;
    box-shadow:0 4px 10px rgba(0,0,0,.25);
    display:flex; align-items:center; justify-content:center;
    transition:transform .15s;
  }
  .flag-btn:hover { transform:translateY(-3px) scale(1.04); }
  .flag-btn .flag-face { font-size:3rem; line-height:1; }
  /* real flag images inside the big Landmark flag buttons */
  #landmark-panel .flag-btn .flag-img { width:66px; height:50px; }
  /* household "say it again" speaker icon (square, no flag crop/edge) */
  #home-panel .btn-ico { width:48px; height:48px; display:block; }
  .flag-btn.wiggle { animation:btnWiggle .5s ease; }
  @keyframes btnWiggle {
    0%   { transform:rotate(0)     scale(1);    filter:brightness(1); }
    30%  { transform:rotate(-7deg) scale(1.10); filter:brightness(1.3); }
    60%  { transform:rotate(6deg)  scale(1.06); filter:brightness(1.15); }
    100% { transform:rotate(0)     scale(1);    filter:brightness(1); }
  }

  /* ===== Passport (rewards collection) — first-pass view, to be art-directed ===== */
  #passport-panel {
    position:fixed; left:50%; top:50%; transform:translate(-50%,-50%) scale(.9);
    width:min(560px,92vw); max-height:82vh; z-index:90;
    background:linear-gradient(180deg,#FFF7E0,#FFE7B0);
    border:3px solid #8B5A12; border-radius:24px;
    box-shadow:0 12px 40px rgba(0,0,0,.4);
    padding:16px 16px 20px; overflow:auto;
    opacity:0; pointer-events:none;
    transition:opacity .25s, transform .25s cubic-bezier(.2,.8,.2,1);
  }
  #passport-panel.open { opacity:1; pointer-events:auto; transform:translate(-50%,-50%) scale(1); }
  .pp-head { display:flex; align-items:center; justify-content:center; gap:12px; margin:2px 36px 14px; }
  .pp-title { font-family:'Fredoka One',cursive; font-size:1.5rem; color:#7A4A0A; }
  .pp-count { font-family:'Nunito',sans-serif; font-weight:800; color:#9A6A00;
    background:#FFD98A; border-radius:12px; padding:2px 10px; }
  .pp-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(92px,1fr)); gap:10px; }
  .pp-cell { position:relative; background:#fff; border:2.5px solid #E6C98A; border-radius:14px;
    padding:9px 4px; text-align:center; opacity:.5; filter:grayscale(.75); transition:transform .15s; }
  .pp-cell.got { opacity:1; filter:none; border-color:#F2A93B; box-shadow:0 3px 8px rgba(242,169,59,.35); }
  .pp-icon { font-size:2rem; line-height:1; }
  .pp-name { font-family:'Nunito',sans-serif; font-weight:800; font-size:.7rem; color:#7A4A0A;
    margin-top:3px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .pp-stamp { position:absolute; top:-7px; right:-7px; color:#E23B2E; font-size:1.15rem;
    text-shadow:-1px 0 #FFD23D,1px 0 #FFD23D,0 -1px #FFD23D,0 1px #FFD23D; }

  /* --- tablet / touch tuning --- */
  .pin, .act-btn, .lang-btn, .spd-btn, #flyer, #map-dim, .pin-img { touch-action:manipulation; -webkit-tap-highlight-color:transparent; }
  img, svg { -webkit-user-drag:none; user-drag:none; }
  @media (hover: none) {
    .act-btn:hover, .lang-btn:hover, .spd-btn:hover { transform:none; filter:none; }
  }
  /* language glow — z 50 INSIDE #explore-stage, ABOVE the world map but BELOW the labels-svg
     (z 51 below). Same viewBox as world-svg keeps coordinates aligned.
     ⚠️ position:absolute, NOT fixed — a fixed child inside the transformed stage silently
     stops painting in Chrome (found 2026-07-18). */
  #glow-svg{ position:absolute; inset:0; width:100%; height:100%; z-index:50; pointer-events:none; }
  /* labels-svg holds the ocean/equator/continent text labels, sitting ABOVE the glow so the text
     stays readable when a language lights up large land areas. */
  #labels-svg{ position:absolute; inset:0; width:100%; height:100%; z-index:51; pointer-events:none; }
  /* ===== LANDMARKS LEAN-IN STAGE (src/lean-map.js, 2026-07-16 native-maps rebuild) =====
     ONE element holds the flat map image + the pins; the lean-in zooms just this element with a
     single compositor transform, so the whole scene scales as one GPU layer (pins ride along). This
     replaced the old multi-layer CSS-scale + per-frame JS that froze Mom's 120Hz iPad. On the
     Landmarks tab the shared vector world map + glow + labels are hidden (the image stands in). */
  #lean-stage { position:fixed; inset:0; transform-origin:0 0; will-change:transform; }
  #lean-map-canvas { position:absolute; inset:0; width:100%; height:100%;
                     display:block; pointer-events:none; will-change:transform; }
  /* MOM'S 6s-FREEZE FIX (2026-07-16, profiler-proven): the 36 pins' soft drop-shadow FILTER
     rasterized catastrophically at deep zoom — a 6.1s main-thread FREEZE on Mom's 120Hz iPad
     (Daniel's did it in 0.18s). Dropping that one filter on the Landmarks tab fixed BOTH iPads
     (Mom's now 0.16s, dive runs on the GPU). Keep the shadow off (bake depth into the art if wanted).
     The sonar RING is just a bordered circle (no blur) — safe, so it stays. And promote each pin to
     its OWN GPU layer so the zoom RE-RASTERS the small pin crisp (cheap without the filter) instead
     of blurring a scaled base texture — that's the pin-sharpness fix. */
  body[data-mode="people"] #lm-pins .pin-thumb { filter:none; }
  /* PIN + RING SHARE ONE ANCHOR (Michael 2026-07-16): a landmark pin and its sonar ring MUST land on
     the exact same map point. Both are position:absolute at the same left/top; the pin must therefore
     use the SAME plain centering as the ring — translate(-50%,-50%) scale(1). The old "sharpen"
     oversized the icon 3× (--pin-hi:3) and scaled the pin back 1/3 (--pin-ctr:.3333); on iOS that big
     scaled box lands OFF its coordinate (the ring, being a tiny box, lands ON it) — so the pin drifted
     away from its ring. Dropped the oversample: the pin now centres exactly like the ring, same
     coordinate, colocated on every device. (Deep-zoom crispness = a later, placement-safe pass.)
     Keep will-change so the dive re-rasterises each pin on its own GPU layer (cheap, no filter). */
  body[data-mode="people"] #lm-pins .pin { will-change:transform; }
  /* (2026-07-18 uniform fade: live-map hide moved to the #explore-stage fade rule.) */
  #glow-svg .glow-area{ fill:#FFE07A; stroke:#FFC53D; stroke-width:1.2; opacity:0; filter:drop-shadow(0 0 5px #FFD24D); animation:glowShim 2.85s ease-in-out infinite; }
  #glow-svg .glow-blob{ stroke:none; }
  @keyframes glowShim{ 0%,100%{opacity:.26} 50%{opacity:.62} }
  /* ===== per-animal animation rigs ===== */
  .pin-rig{ font-size:calc(clamp(1.6rem,3.1vw,2.5rem) * var(--a-hi,1)); }   /* x --a-hi: the deep-zoom re-raster flip — without it the 5 pins with no per-pin size (owl/pig/cow/lamb/frog) stayed SMALL at the dive (Michael, TEST 108) */
  .snk2-body{ transform-box:fill-box; transform-origin:50% 86%; animation:snkSway 2.6s ease-in-out infinite; }
  @keyframes snkSway{ 0%,100%{transform:rotate(-3.2deg)} 50%{transform:rotate(3.2deg)} }
  .snk-tongue{ transform-box:fill-box; transform-origin:center top; animation:snkTongue 2.4s ease-in-out infinite; }
  @keyframes snkTongue{ 0%,70%,100%{transform:scaleY(.14)} 82%,92%{transform:scaleY(1)} }
  /* ===== LIVING POSTCARD overlay (coin-flip reveal) ===== */
  #postcard-overlay{
    position:fixed; inset:0;
    background:rgba(8,18,48,0);
    z-index:86;
    display:flex; align-items:center; justify-content:center;
    pointer-events:none;
    transition:background .4s ease;
  }
  #postcard-overlay.active{ background:rgba(0,0,0,.25); pointer-events:auto; }
  #postcard-overlay:not(.active):not(.warming) #postcard-wrapper{ display:none; }
  /* WARMING (2026-07-09): the tapped card's engine boots on finger-down, LAID OUT at its final
     size but invisible + non-interactive, so it measures correctly and is already rendering by the
     time the flip lands. Keeps layout + rAF alive (display:none would zero the size and throttle
     the boot — the two bugs the old cold-boot reveal machinery fought).
     2026-07-10: opacity:0.001 instead of visibility:hidden — hidden let the browser SKIP
     RASTERIZING the card's artwork during the warm, so the first paint of everything happened AT
     reveal: rendering choked to ~20-30fps for the first beat while the animation clock ran at
     wall speed, and every card visibly "fast-forwarded" for a split second on open (Michael's
     super-speed symptom, confirmed by the probe's post-reveal rAF collapse). At 0.001 the card is
     imperceptible but the compositor must paint it, so reveal costs nothing. */
  #postcard-overlay.warming{ pointer-events:none; }
  #postcard-overlay.warming #postcard-wrapper{ opacity:0.001; }
  #postcard-wrapper{
    position:relative;
    overflow:hidden;
    /* width/height set by JS = the card's final display size; overflow:hidden clips
       the iframe overhang (the player's dark stage around the card) */
  }
  /* NATIVE FILL (2026-07-07): the iframe fills the wrapper 1:1 — the wrapper IS booted at
     the card's true on-screen size, and injectCardFill() makes the player's #card fill the
     iframe. NO transform (transforms on iframes render inconsistently on real devices and
     fed the player wrong measurements → "boats teleport to the sky"). frame.style.opacity
     is toggled by JS (0 until revealed). */
  #postcard-frame{
    display:block;
    width:100%; height:100%;
    border:none; outline:none;
  }

  /* ===== PIN→POSTCARD COIN FLIP (proxy card) =====
     Two plain faces + transforms/opacity ONLY — everything stays on the GPU
     compositor thread so the spin is smooth on tablets. The live iframe is never
     rotated (WebKit backface bugs); it sits underneath and is revealed on landing. */
  #flip-proxy{
    position:absolute;               /* JS sets width/height; flex centering of the overlay
                                        doesn't apply (absolute), so JS centers it too */
    display:none;
    transform-style:preserve-3d;
    pointer-events:none;
    z-index:2;
    will-change:transform;
  }
  #postcard-overlay.flipping #flip-proxy{ display:block; }
  /* NOTE (2026-07-07): we deliberately do NOT set #postcard-wrapper{opacity:0} while
     flipping. Making the wrapper fully transparent made the player mis-MEASURE the card
     (it fell back to its landscape default → boats teleported to the sky, Great Wall flag
     to the middle — flip-only bug). Instead the live card is hidden by the IFRAME's own
     opacity (frame.style.opacity=0 in bootCard) — same as the no-flip path — and the
     opaque #flip-proxy covers the centre during the spin. The wrapper stays laid out at
     opacity 1 so the player always measures the true on-screen size. */
  .flip-face{
    position:absolute; inset:0;
    backface-visibility:hidden;
    -webkit-backface-visibility:hidden;
    border-radius:14px;
    overflow:hidden;
  }
  #flip-back{ transform:rotateY(180deg); }
  /* Front face phase 1 = the pin's cut-out thumbnail on a transparent bg; the
     static card preview underneath is revealed at the first edge-on moment. */
  #flip-thumb{
    position:absolute; inset:0; margin:auto;
    max-width:88%; max-height:88%;
    object-fit:contain;
    z-index:2;
  }
  #flip-preview{
    position:absolute; inset:0;
    width:100%; height:100%;
    object-fit:cover;
    opacity:0;
    z-index:1;
  }
  /* the thumb→card swap is a WAAPI opacity animation synced to the spin (see
     runPinFlip) — same timeline as the rotation, so it can never drift, even if
     the tablet stutters. No CSS class swap, no timers. */
  /* The written side of the postcard — cream card, stamp (the landmark's own
     thumbnail!), a wobbly postmark, divider and address squiggles. Pure CSS,
     no text (pre-reader rule), no extra image assets. backface-visibility keeps
     it hidden during phase 1 (it's facing away). */
  .pc-back{
    position:absolute; inset:0;
    background:#fbf3e2;
    border:6px solid #fff;
    border-radius:14px;
    box-shadow:inset 0 0 0 2px #e8dcc2;
  }
  .pc-stamp{
    position:absolute; top:7%; right:5%;
    width:19%; aspect-ratio:1;
    background:#fff;
    border:3px dashed #d9884a;
    border-radius:6px;
    display:flex; align-items:center; justify-content:center;
    transform:rotate(4deg);
  }
  .pc-stamp img{ width:78%; height:78%; object-fit:contain; }
  .pc-postmark{
    position:absolute; top:9%; right:22%;
    width:13%; aspect-ratio:1;
    border:3px solid rgba(200,90,80,.5);
    border-radius:50%;
    transform:rotate(-12deg);
  }
  .pc-divider{
    position:absolute; top:12%; bottom:12%; left:50%;
    width:3px; border-radius:2px;
    background:#d9c9a3;
  }
  .pc-lines{ position:absolute; left:58%; right:8%; top:42%; }
  .pc-lines i{
    display:block; height:4px; border-radius:2px;
    background:#d9c9a3; margin-bottom:14%;
  }
  .pc-lines i:last-child{ width:62%; }

  /* ===== LANDMARK DETAIL (placeholder art + voice) ===== */
  #landmark-panel{
    position:fixed; top:50%; left:50%; transform:translate(-50%,-50%) scale(.92);
    z-index:84; width:min(92vw,420px);
    background:linear-gradient(180deg,#FFF7E6,#FFE7BE);
    border:4px solid #B8941E; border-radius:28px;
    box-shadow:0 18px 50px rgba(0,0,0,.45);
    padding:20px 22px 24px; text-align:center;
    opacity:0; pointer-events:none;
    transition:opacity .3s ease, transform .35s cubic-bezier(.34,1.6,.5,1);
  }
  #landmark-panel.open{ opacity:1; pointer-events:auto; transform:translate(-50%,-50%) scale(1); }
  #lm-close{
    position:absolute; top:8px; right:10px; width:34px; height:34px; border-radius:50%;
    border:none; background:#E9C97A; color:#7A4A0A; font-size:1.05rem; font-weight:900; cursor:pointer;
    box-shadow:0 2px 6px rgba(0,0,0,.25); transition:transform .15s,background .15s;
  }
  #lm-close:hover{ background:#F0D689; transform:scale(1.08); }
  .lm-hero{ font-size:clamp(4rem,17vw,7rem); line-height:1; filter:drop-shadow(2px 6px 10px rgba(0,0,0,.4)); }
  .lm-title{ font-family:'Fredoka One',cursive; font-size:1.55rem; color:#7A4A0A; margin-top:2px; }
  .lm-say{ min-height:1.5em; font-weight:900; font-size:1.3rem; color:#4A4036; margin:8px 0 2px; line-height:1.4; }
  .lm-flags{ display:flex; gap:22px; justify-content:center; margin:12px 0 6px; }
  .lm-info{ font-weight:700; color:#5a4a2a; line-height:1.4; min-height:1.2em; margin-top:8px; font-size:.98rem; }

  /* ===== Sparkle burst module — used by teleportFlyerHome() (Approved Behavior #7, revised) ===== */
  /* Drop-shadows kept subtle on purpose — peak opacity is also capped in the JS keyframes so the sparkles read as a soft shimmer, not a flashbulb. */
  .al-spark { position: fixed; display: grid; place-items: center; pointer-events: none; z-index: 9999; transform: translate(-50%,-50%) scale(0); opacity: 0; }
  .al-spark .arms { width: 100%; height: 100%; background: linear-gradient(135deg, #ffe9a0, #f3c141); clip-path: polygon(50% 0%, 60% 40%, 100% 50%, 60% 60%, 50% 100%, 40% 60%, 0% 50%, 40% 40%); filter: drop-shadow(0 0 3px rgba(255,233,160,.55)); }
  .al-spark .pip  { position: absolute; width: 30%; height: 30%; background: radial-gradient(circle, #fff 0%, #ffe9a0 60%, transparent 100%); border-radius: 50%; filter: drop-shadow(0 0 2px rgba(255,255,255,.7)); }
  .al-spark.t-cream .arms { background: linear-gradient(135deg, #fff, #faecc4); filter: drop-shadow(0 0 3px rgba(250,236,196,.55)); }
  .al-spark.t-warm  .arms { background: linear-gradient(135deg, #ffe9a0, #e89870); filter: drop-shadow(0 0 3px rgba(232,152,112,.55)); }
  /* Flyer fade-out at the teleport source (paired with the outward sparkle burst).
     Duration tuned by Michael — slower fades read as a more deliberate "poof" rather than a snap. */
  .al-teleport-out { transition: opacity 270ms ease-out !important; opacity: 0 !important; }
  /* Pin fade-in at the home spot (paired with the inward sparkle converge).
     OPACITY ONLY on the pin itself — .pin uses transform:translate(-50%,-50%) for centering
     AND runs the pinFloat keyframes; touching transform here would un-center the pin
     (visible "down-right then snap" jump). The "pop" feel comes from a scale on the
     INNER .pin-icon, which has no centering transform of its own. */
  .al-pin-fadein { animation: alPinOpacityIn 480ms ease-out both; }
  .al-pin-fadein .pin-icon { animation: alPinPopIn 480ms cubic-bezier(.34,1.4,.5,1) both; }
  @keyframes alPinOpacityIn { from { opacity: 0; } to { opacity: 1; } }
  @keyframes alPinPopIn { from { transform: scale(.7); } to { transform: scale(1); } }

  /* ===== Parental gate (src/parental-gate.js) — deliberately plain/adult; the one screen with no kid pizzazz ===== */
  .pg-overlay { position: fixed; inset: 0; z-index: 99999; display: flex; align-items: center; justify-content: center;
    background: rgba(20,22,28,.72); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
    animation: pgFadeIn .18s ease-out both; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); }
  .pg-overlay.pg-closing { animation: pgFadeOut .2s ease-in both; }
  @keyframes pgFadeIn { from { opacity: 0; } to { opacity: 1; } }
  @keyframes pgFadeOut { from { opacity: 1; } to { opacity: 0; } }
  .pg-card { position: relative; width: min(86vw, 320px); background: #f4f4f6; color: #2a2d34;
    border-radius: 18px; padding: 22px 20px 20px; box-shadow: 0 16px 48px rgba(0,0,0,.4);
    font-family: -apple-system, system-ui, sans-serif; text-align: center; }
  .pg-card.pg-shake { animation: pgShake .4s; }
  @keyframes pgShake { 0%,100%{transform:translateX(0)} 20%{transform:translateX(-9px)} 40%{transform:translateX(8px)} 60%{transform:translateX(-6px)} 80%{transform:translateX(4px)} }
  .pg-close { position: absolute; top: 8px; right: 10px; width: 32px; height: 32px; border: none; background: transparent;
    color: #9a9da6; font-size: 18px; line-height: 1; cursor: pointer; border-radius: 8px; }
  .pg-close:active { background: #e4e4e8; }
  .pg-title { font-size: 17px; font-weight: 700; letter-spacing: .2px; }
  .pg-sub { font-size: 12px; color: #767a84; margin: 3px 0 14px; }
  .pg-question { font-size: 26px; font-weight: 700; font-variant-numeric: tabular-nums; }
  .pg-entry { min-height: 34px; margin: 8px auto 14px; font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums;
    color: #1a73c0; letter-spacing: 2px; }
  .pg-pad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 9px; }
  .pg-key { height: 52px; border: none; border-radius: 12px; background: #fff; color: #2a2d34; font-size: 21px; font-weight: 600;
    cursor: pointer; box-shadow: 0 1px 2px rgba(0,0,0,.12); font-variant-numeric: tabular-nums; }
  .pg-key:active { background: #e8e9ee; transform: scale(.96); }
  .pg-key-action { background: #e9edf3; color: #4a5160; }

  /* ===== HOUSEHOLD ITEMS (home mode) — cutaway dollhouse (src/household.js) ===== */
  /* In home mode the globe, its labels, the language glow and the title banner all
     hide, and the dollhouse scene shows alone. Hiding (not covering) the map keeps
     the z-index simple: #map-dim (z40) + #home-panel (z84) still layer on top. */
  body[data-mode="home"] { background:#EAD7B6; }
  /* (map layers now fade via the #explore-stage rule; only the banner stays listed here) */
  body[data-mode="home"] #title-banner { opacity:0; }

  /* Locked base art (2026-06-26): the real Firefly-expanded dollhouse fills the screen.
     object-fit: cover keeps the house centered + crops only the filler sky/grass on any
     tablet (verified iPad 4:3 -> 16:9 safe). Item sprites get layered on top next. */
  #mode-home {
    position:fixed; inset:0; z-index:22;
    background:#cfeff7 url("../assets/images/household/house-base.webp?v=clean5") center center / cover no-repeat;
  }
  /* Tappable item sprites layered over the house background (positioned in JS to
     match object-fit:cover, so they stay glued to the rooms on any tablet shape). */
  /* Layers (items + masked hotspots) over the house. Taps hit the TRUE shape via an
     alpha hit-grid (computed at build time); the tapped shape glows gold. */
  #home-stage { position:absolute; inset:0; z-index:2; }
  .home-layer { position:absolute; pointer-events:none; transform-origin:center center; }
  .home-layer img { width:100%; height:100%; display:block; user-select:none; -webkit-user-drag:none; }
  .home-layer.glow img { filter:drop-shadow(0 0 4px #ffd24a) drop-shadow(0 0 4px #ffd24a) drop-shadow(0 0 9px #ffd24a); }
  @keyframes home-pop { 0%{transform:scale(1);} 35%{transform:scale(1.13);} 100%{transform:scale(1);} }
  .home-layer.pop { animation:home-pop .42s cubic-bezier(.34,1.56,.64,1); }

  /* old CSS placeholder dollhouse retired now that the real art is the background */
  #home-scene { display:none; }
  #home-scene-DISABLED { width:min(900px,100%); height:100%; max-height:780px; display:flex; flex-direction:column; }
  #home-roof {
    flex:0 0 auto; height:36px; background:#C85B33;
    clip-path:polygon(0 100%, 50% 0, 100% 100%);
  }
  #home-house {
    flex:1 1 auto; min-height:0;
    background:#9C6B2F; border:5px solid #7A4A0A;
    border-radius:4px 4px 20px 20px; padding:8px;
  }
  #home-rooms {
    height:100%; display:grid;
    grid-template-columns:repeat(2,1fr); grid-auto-rows:1fr; gap:8px; min-height:0;
  }
  .hi-room {
    border-radius:12px; padding:8px 8px 6px;
    display:flex; flex-direction:column; min-height:0;
    box-shadow:inset 0 0 0 2px rgba(255,255,255,.4);
  }
  .hi-living   { background:#FBEBCF; }
  .hi-kitchen  { background:#E6F0D8; }
  .hi-bathroom { background:#DCEBFA; }
  .hi-bedroom  { background:#EAE6FB; }
  .hi-room-name {
    flex:0 0 auto; font-family:'Fredoka One',cursive;
    font-size:.95rem; color:#7A4A0A; margin:0 0 6px 2px;
  }
  .hi-tiles {
    flex:1 1 auto; min-height:0; overflow:auto;
    display:grid; grid-template-columns:repeat(auto-fit,minmax(72px,1fr));
    gap:7px; align-content:start;
  }
  .hi-item {
    cursor:pointer; border:none; background:rgba(255,255,255,.82);
    border-radius:13px; padding:8px 3px 6px;
    display:flex; flex-direction:column; align-items:center; gap:3px;
    transition:transform .12s ease, background .12s;
  }
  @media (hover:hover){ .hi-item:hover { transform:translateY(-3px) scale(1.06); background:#fff; } }
  .hi-emoji { font-size:1.9rem; line-height:1; filter:drop-shadow(1px 3px 4px rgba(0,0,0,.22)); }
  /* Real isometric Kenney sprites — sit directly on the room floor (lighter tile bg)
     with a soft ground shadow so they read as little 3D objects, not flat icons. */
  .hi-sprite { height:50px; width:auto; object-fit:contain; filter:drop-shadow(2px 5px 3px rgba(0,0,0,.28)); }
  .hi-item.has-sprite { background:rgba(255,255,255,.30); }
  @media (hover:hover){ .hi-item.has-sprite:hover { background:rgba(255,255,255,.6); } }
  .hi-hero-sprite { height:118px; width:auto; object-fit:contain; filter:drop-shadow(3px 8px 8px rgba(0,0,0,.34)); }
  .hi-word  { font-size:.7rem; font-weight:900; color:#5a3a0a; text-align:center; line-height:1.05; }
  .hi-item.hi-pop { animation:hiPop .45s cubic-bezier(.34,1.5,.5,1); }
  @keyframes hiPop { 0%{transform:scale(1)} 40%{transform:scale(1.22)} 100%{transform:scale(1)} }

  /* Item detail card — same look as the landmark card; reuses .lm-hero/.lm-title/.flag-btn. */
  /* Bottom language panel for Household items — mirrors the animals' #ctrl-panel. */
  #home-panel {
    position:fixed; left:0; right:0; bottom:0; z-index:84;
    padding:6px 18px 22px;
    display:flex; flex-direction:column; align-items:center; gap:6px;
    opacity:0; transform:translateY(24px); pointer-events:none;
    transition:opacity .4s ease, transform .45s cubic-bezier(.34,1.4,.5,1);
  }
  #home-panel.open { opacity:1; pointer-events:auto; transform:translateY(0); }
  #home-title { font-family:'Fredoka One',cursive; font-size:1.6rem; color:#fff; text-shadow:0 2px 8px rgba(0,0,0,.6); }
  #home-close {
    position:absolute; top:8px; right:10px; width:34px; height:34px; border-radius:50%;
    border:none; background:#E9C97A; color:#7A4A0A; font-size:1.05rem; font-weight:900; cursor:pointer;
    box-shadow:0 2px 6px rgba(0,0,0,.25); transition:transform .15s,background .15s;
  }
  #home-close:hover { background:#F0D689; transform:scale(1.08); }
  .hi-speak-row { display:flex; justify-content:center; margin:16px 0 2px; }

  /* Phone / portrait QA: rooms stack and the scene scrolls (the no-scroll rule is
     for the landscape tablet target; scrolling is fine for phone preview). */
  @media (max-width:560px){
    #mode-home { overflow:auto; align-items:flex-start; padding-top:84px; }
    #home-scene { height:auto; }
    #home-house { flex:0 0 auto; }
    #home-rooms { grid-template-columns:1fr; grid-auto-rows:auto; }
    .hi-tiles { overflow:visible; }
  }

  /* ===== TOWN (town mode) — top-down cartoon town map (src/town.js) ===== */
  /* Like home mode, the globe + labels + glow + banner hide so the town shows alone. */
  body[data-mode="town"] { background:#3DA6D4; }
  /* (map layers now fade via the #explore-stage rule; only the banner stays listed here) */
  body[data-mode="town"] #title-banner { opacity:0; }

  #mode-town { position:fixed; inset:0; z-index:22; }
  /* Map art is a background-image set lazily in JS (first Town open) to keep boot light.
     object-fit:cover equivalent so the 4:3 map fills the screen, cropping only filler
     scenery on wider tablets (iPad 4:3 = perfect, per TOWN_PLAN safe-zone rule). */
  #town-stage { position:absolute; inset:0; background-position:center center;
    background-size:cover; background-repeat:no-repeat; }

  /* Invisible tap-zone over each baked building. On tap it pops + flashes a gold ring;
     a gentle idle shimmer marks it as tappable (no floating pins — TOWN_PLAN locked). */
  .town-zone { position:absolute; transform:translate(-50%,-50%); cursor:pointer; border-radius:14px; }
  .town-ring { position:absolute; inset:6%; border-radius:12px; animation:town-idle 3s ease-in-out infinite; }
  @keyframes town-idle { 0%,100%{box-shadow:0 0 0 0 rgba(255,221,87,0);} 50%{box-shadow:0 0 16px 3px rgba(255,221,87,.45);} }
  .town-zone.pop { animation:town-pop .5s cubic-bezier(.34,1.6,.5,1); }
  @keyframes town-pop { 0%{transform:translate(-50%,-50%) scale(1);} 30%{transform:translate(-50%,-50%) translateY(-7%) scale(1.1);} 100%{transform:translate(-50%,-50%) scale(1);} }
  .town-zone.pop .town-ring { animation:town-flash .5s ease; }
  @keyframes town-flash { 0%,100%{box-shadow:0 0 0 0 rgba(255,221,87,0);} 40%{box-shadow:0 0 30px 9px rgba(255,221,87,.95);} }

  /* Bottom language panel — same look as #home-panel. */
  #town-panel {
    position:fixed; left:0; right:0; bottom:0; z-index:84;
    padding:6px 18px 22px;
    display:flex; flex-direction:column; align-items:center; gap:6px;
    opacity:0; transform:translateY(24px); pointer-events:none;
    transition:opacity .4s ease, transform .45s cubic-bezier(.34,1.4,.5,1);
  }
  #town-panel.open { opacity:1; pointer-events:auto; transform:translateY(0); }
  #town-title { font-family:'Fredoka One',cursive; font-size:1.6rem; color:#fff; text-shadow:0 2px 8px rgba(0,0,0,.6); }
  #town-close {
    position:absolute; top:8px; right:10px; width:34px; height:34px; border-radius:50%;
    border:none; background:#E9C97A; color:#7A4A0A; font-size:1.05rem; font-weight:900; cursor:pointer;
    box-shadow:0 2px 6px rgba(0,0,0,.25); transition:transform .15s,background .15s;
  }
  #town-close:hover { background:#F0D689; transform:scale(1.08); }
