      /* Confetti */
      .confetti { position: fixed; inset: 0; pointer-events: none; overflow: hidden; z-index: 50; }
      .confetti i {
        position: absolute; top: -4vh; display: block; border-radius: 4px;
        animation: fall linear forwards;
      }
      @keyframes fall { to { transform: translateY(112vh) rotate(560deg); } }

      @media (max-width: 520px) {
        .hero { flex-direction: column; text-align: center; }
        .hero h1 { font-size: 27px; }
        .pet-face { width: 112px; height: 112px; }
        .bubble .b-text { font-size: 20px; }
        .bubble .b-big { font-size: 50px; }
        .pet-card .p-face { width: 96px; height: 96px; }
        .pet-card .p-name { font-size: 23px; }
        .choice-btn { font-size: 46px; min-height: 96px; }
        .lang-btn { font-size: 18px; }
        .task { font-size: 24px; }
        .cookie-btn { font-size: 50px; }
        .cookie-choices.cols4 .cookie-btn { font-size: 40px; }
      }
      @media (prefers-reduced-motion: reduce) {
        *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
      }
      .age-picker{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:10px 0 16px}
    .age-picker .age-label{font-weight:800;color:var(--ink)}
    .age-btn{border:3px solid #ffb84d;background:#fff8e1;border-radius:999px;padding:10px 16px;font-size:17px;font-weight:800;cursor:pointer;color:var(--ink)}
    .age-btn.active{background:var(--sky)!important;color:#fff!important;border-color:var(--sky-dark)}
    .sync-status{margin-top:10px;font-size:13px;color:var(--ink-soft)}

    /* ===== REDESIGN v10 — game-app layout (presentation only) =====
       Home is one big pet-yard scene; care screen is a full stage with a
       fixed bottom toolbar. All game logic / ids / i18n stay untouched. */
    .wrap { padding: 10px 12px 28px; }

    /* --- Compact HUD: sticker row + language / age segmenteds --- */
    .topbar { margin-bottom: 8px; gap: 8px; }
    .top-actions { gap: 8px; }
    .sticker-box { padding: 4px 12px 4px 6px; gap: 7px; border-width: 2.5px; }
    .sticker-box .s-ico { width: 30px; height: 30px; }
    .sticker-box .s-ico svg { width: 30px; height: 30px; }
    .sticker-box .s-num { font-size: 23px; }
    .sticker-box .s-label { font-size: 11.5px; }
    .icon-btn { min-height: 44px; min-width: 44px; font-size: 19px; padding: 4px 10px; border-width: 2.5px; }
    .home-btn { min-height: 44px; padding: 6px 14px; font-size: 16px; border-width: 2.5px; }
    .hud { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
    .langbar {
      display: flex; gap: 6px; margin-bottom: 0; padding: 5px;
      background: rgba(255, 255, 255, .55); border: 2px solid rgba(255, 255, 255, .85); border-radius: 999px;
    }
    .lang-btn {
      flex: 1 1 0; min-width: 0; min-height: 46px; border-radius: 999px; font-size: 16.5px; padding: 4px 2px;
      border-width: 2.5px; box-shadow: none; background: rgba(255, 255, 255, .78); line-height: 1.05;
      white-space: nowrap; overflow: hidden;
    }
    /* v44 (user QA): pill rows must NEVER stick out of the screen —
       every pill in a row may shrink (flex + min-width: 0) and its
       label is font-fitted by fitTextEl instead of being cut. */
    .topbar { min-width: 0; }
    .top-actions { display: flex; align-items: center; min-width: 0; }
    .home-btn { white-space: nowrap; min-width: 0; overflow: hidden; flex: 0 1 auto; }
    .lang-pop-btn { white-space: nowrap; min-width: 0; }
    .sticker-box { min-width: 0; flex: 0 1 auto; }
    .hud-ages .age-btn, .age-picker.seg .age-btn { min-width: 0; }
    .lang-btn small { font-size: 10.5px; }
    .lang-btn.active { box-shadow: 0 3px 0 rgba(35, 50, 70, .18); }
    .hud-ages, .age-picker.seg {
      display: flex; gap: 6px; margin: 0 0 10px; padding: 5px; flex-wrap: nowrap;
      background: rgba(255, 255, 255, .55); border: 2px solid rgba(255, 255, 255, .85); border-radius: 999px;
    }
    .hud-ages .age-btn, .age-picker.seg .age-btn {
      flex: 1 1 0; min-height: 44px; border-radius: 999px; padding: 4px 2px; font-size: 14.5px;
      border-width: 2.5px; background: rgba(255, 255, 255, .82); white-space: nowrap; overflow: hidden;
    }

    /* --- HOME: one big pet-yard scene --- */
    .farm-scene {
      position: relative; isolation: isolate; height: clamp(330px, 58dvh, 540px); min-height: 0;
      /* v96 (bi 2026-10-29 11:43 — "bỏ hẳn viền xanh lá, nền thông
         suốt toàn bộ màn hình"): the yard is NOT a card any more.
         No border, no radius, no shadow, and the box itself breaks
         out to the full viewport width (the negative margins cancel
         the wrap's side padding on phones and the centred column's
         margins on wide screens), so sky and grass run edge to edge
         and there is no frame edge left to mismatch. The PETS keep
         the old limits: .farm-pets below stays a centred <=620px
         column and the farm director measures THAT box — the herd's
         world is unchanged, only the scenery grew. overflow visible:
         the scenery layer clips itself (.scene-bg keeps its own
         overflow hidden) and the butterfly now genuinely flies in
         from the screen's edge. */
      border: none; border-radius: 0; box-shadow: none; overflow: visible;
      margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw);
      /* v74: gradient sun + static clouds removed — the farm's
         injected SVG scenery (anim.js) owns the one sun and the
         drifting clouds; ground flower dots stay. */
      /* v96: the sky/grass BASE is no longer painted here — it lives
         on body.on-home as ONE viewport-sized gradient, so the base
         cannot end at the scene's box edge on wide screens (a
         scene-sized gradient next to a body-sized one read as a
         pale column border at 768/1280 in the first v96 pass).
         Only the ground flower dots stay, painted over the shared
         base; the drawn hills in .scene-bg give the horizon its
         shape over the base's soft sky->grass fade. */
      background:
        radial-gradient(circle at 7% 90%, #ff8fb2 0 6px, rgba(255, 143, 178, 0) 9px),
        radial-gradient(circle at 30% 95%, #ffd93c 0 5px, rgba(255, 217, 60, 0) 8px),
        radial-gradient(circle at 66% 92%, #ffffff 0 5px, rgba(255, 255, 255, 0) 8px),
        radial-gradient(circle at 94% 94%, #ff6b6b 0 6px, rgba(255, 107, 107, 0) 9px);
    }
    /* v50: title chip + shop button share ONE flex header row at the
       top of the yard — side by side in normal flow, so the button can
       never overlap the speech bubble again (user photo 2026-10-26:
       the absolutely-positioned button sat on the bubble's edge).
       The row paints above the roaming pets (z 300 > pet z <= 210);
       the farm director measures this row's height and keeps pet
       faces below it. */
    /* v72 (Addendum 9, as amended): nothing textual lives inside the
       farm scene any more — the greeting line and the hint sit in a
       left-aligned block BELOW the scene frame, and the shop button
       is a compact round button at the scene's bottom-right. The
       scene keeps its full height for sky, fence and pets. */
    .farm-greet { text-align: left; padding: 8px 2px 0; }
    .farm-greet .foot-note { margin: 6px 2px 0; }
    /* v76 (addendum 2, position set by the user 14:54): version badge
       at the empty right end of the TOPBAR, after the sticker pill —
       outside the yard, so it can never touch a pet, the sun or the
       shop button. It rides the topbar's own visibility (the topbar
       itself is hidden on the pet screen by menu.css). Not a button:
       pointer-events none, never eats a tap. Height stays under the
       sticker pill's, so the topbar never grows. */
    .ver-badge { flex: 0 1 auto; min-width: 0; display: inline-flex; align-items: center; gap: 5px; background: rgba(255, 255, 255, .72); border: 1.5px solid rgba(35, 50, 70, .10); border-radius: 999px; padding: 2px 9px 2px 3px; pointer-events: none; line-height: 1.12; }
    .ver-badge-cat svg { width: 20px; height: 20px; display: block; }
    .ver-badge-txt b { display: block; font-size: 11px; font-weight: 800; color: var(--ink); white-space: nowrap; }
    .ver-badge-txt i { display: block; font-style: normal; font-size: 9.5px; font-weight: 700; color: #5b7a99; white-space: nowrap; }
    /* Narrow phones: the topbar already carries the lang button,
       sound / fullscreen / parent-gate buttons and the sticker pill.
       FIT-FIRST: the badge compacts to logo + version only (the
       "e2z kids" wordmark drops), text shrinks, nothing overflows
       and the existing buttons are never pushed or covered. */
    @media (max-width: 600px) {
      .ver-badge { gap: 4px; padding: 2px 7px 2px 3px; }
      .ver-badge-cat svg { width: 16px; height: 16px; }
      .ver-badge-txt b { display: none; }
      .ver-badge-txt i { font-size: 9px; }
    }
    /* At 320-380 the topbar row is saturated: the sticker pill's
       min-content used to overflow its shrunk flex box and slide
       under the badge. FIT-FIRST micro-fit for that band only: the
       pill's label/number shrink a step and the badge switches to a
       slim column (logo over version). Nothing is hidden, nothing
       wraps, the row's height is untouched. */
    @media (max-width: 380px) {
      .sticker-box { gap: 5px; padding: 4px 8px 4px 5px; }
      .sticker-box .s-num { font-size: 20px; }
      .sticker-box .s-label { font-size: 10px; }
      .ver-badge { flex-direction: column; gap: 0; padding: 2px 5px; border-radius: 12px; }
      .ver-badge-cat svg { width: 14px; height: 14px; }
      .ver-badge-txt i { font-size: 8px; }
    }
    .farm-title-chip {
      display: inline-block; max-width: 100%; min-width: 0;
      background: rgba(255, 255, 255, .9); border: 2.5px solid #ffb84d; color: var(--ink);
      font-weight: 800; font-size: 15.5px; line-height: 1.3; text-align: center;
      padding: 6px 14px; border-radius: 20px;
      box-shadow: 0 3px 0 rgba(35, 50, 70, .12); white-space: normal;
    }
    /* v96: the LOGICAL yard — a centred column of at most 620px
       inside the full-bleed scene. Pets, plates and the director's
       coordinates live in this box and never in the scenery bleed
       around it (bi 2026-10-29 11:45: "vẫn có giới hạn như cũ —
       thú di chuyển giới hạn trong khung chuẩn"). */
    .farm-pets { position: absolute; top: 0; bottom: 0; left: max(0px, calc(50% - 310px)); right: max(0px, calc(50% - 310px)); }
    /* v86 (PLAY1 yard plates): the name-plate overlay layer — one
       layer above every pet card (cards top out at z 210), so a
       plate is never painted under another pet's body or dimmed
       into the fence. The layer itself takes no taps; each plate
       takes its own tap and forwards it to its pet's card. */
    .farm-plates { position: absolute; inset: 0; z-index: 300; pointer-events: none; }
    #petGrid .farm-plates .fp-plate {
      position: absolute; left: 0; top: 0; margin-top: 0; z-index: auto;
      background: #ffffff; border-color: rgba(255, 255, 255, .95);
      pointer-events: auto; cursor: pointer;
    }
    /* v111 (bi QA 2026-10-30 "Tên và Tim rối che tốn chỗ quá"): the
       overlay plates are NOT inside .farm-pet, so the compact
       .farm-pet .p-name/.p-hearts typography never reached them —
       name and the 5 emoji hearts rendered at the default 16px,
       a 152x34 pill per pet (measured on the harness yard), and
       two front-row pets walled the yard with white. Compact type
       sized for the overlay layer itself: */
    #petGrid .farm-plates .fp-plate { gap: 5px; padding: 2px 8px 3px; }
    #petGrid .farm-plates .fp-plate .p-name { font-size: 12.5px; font-weight: 800; line-height: 1.15; }
    #petGrid .farm-plates .fp-plate .p-hearts { font-size: 10.5px; letter-spacing: 0; line-height: 1.2; }
    #petGrid .farm-plates .fp-plate.fp-lock-badge { font-size: 12.5px; font-weight: 800; }
    /* v72: shared hand-drawn scenery layer (injected by anim.js into
       the farm scene, the care stage and the game yard). It sits at
       z-index 0 — every pet, bubble and button paints above it — is
       pointer-events:none, and animates ONLY transform/opacity so it
       stays inside the v50 smoothness budget: clouds drift, the sun
       rays turn slowly, the tree sways. Reduced motion stills it. */
    .scene-bg { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
    .scene-bg svg { width: 100%; height: 100%; display: block; }
    /* v89 addendum 4: the farm's standalone fence layer — the farm
       director sets its inline z-index from the fence foot line
       (same fy scale as the pets), so the fence genuinely occludes
       the pets standing behind it and is occluded by the pets
       inside the yard. Default z stays below every pet until the
       director's first tick places it. */
    .sc-fence-layer { position: absolute; inset: 0; z-index: 1; pointer-events: none; }
    .sc-fence-layer svg { width: 100%; height: 100%; display: block; }
    /* v72 perf rework: moving ornaments are small independent
       layers animated on their own boxes (composited transforms),
       never groups inside the stage-wide ground SVG — animating
       those forced a full-scene repaint per frame (v50-smooth). */
    .scene-bg .sc-anim { position: absolute; pointer-events: none; }
    .scene-bg .sc-sunw { right: 3%; top: 3%; width: 25%; aspect-ratio: 112 / 114; }
    /* v79 addendum D1 (real-device QA): inside the game yard the
       floating scenery must never show through a target — the bath
       bubbles are translucent, and the sun sat exactly in their
       flight band (measured: sun/cloud/tree intersected target
       glyphs in 255 bath samples + 96 chase samples out of 125).
       The yard keeps its ground/fence backdrop; the floating sun,
       clouds and tree are stage/care scenery only. */
    #gameYard .scene-bg .sc-sunw,
    #gameYard .scene-bg .sc-cloudw,
    #gameYard .scene-bg .sc-treew { display: none; }
    /* v72 (Addenda 5b/9): on the pet stage the sun drops below the
       top chrome band (speaker / parent gate live up there) and
       shrinks a step, so the disc is never cut by a button. */
    /* v95 (bi photo QA 2026-10-29 08:20 — "the speaker covers the
       sun"): at 19%/19% the disc's right edge reached 81% of the
       stage and its halo (inset -22%) ~85% — in wide/full-bleed
       stage layouts that slides under the top-right speaker
       cluster. The sun now sits a step further in (right edge at
       76%, halo ~80%) so no layout can park it under a button. */
    /* v98: the stage's scene layer bleeds to 100vw (see the stage
       rules below), so the sun's right offset anchors to the
       LOGICAL column instead of the screen: max() keeps the phone
       rule (24% of a full-width stage) at <=620px and the
       column-anchored offset on wide screens — the disc's right
       edge stays ~149px inside the column's right edge, exactly
       where v95 put it. Width capped like the farm's sun (v96). */
    #screen-pet .pet-stage .sc-sunw { top: 13%; right: max(24%, calc(50% - 161px)); width: min(17.5%, 150px); }
    /* v98: the farm's design-size caps (v96) for the stage's
       clouds and tree too — they ride the bled layer out to the
       screen edges but never grow with it. */
    #screen-pet .pet-stage .sc-cw1 { width: min(31%, 250px); }
    #screen-pet .pet-stage .sc-cw2 { width: min(22%, 180px); }
    #screen-pet .pet-stage .sc-treew { width: min(23%, 185px); }
    /* v98: the game yard's scenery layer bleeds to the screen
       edges as well (care.css bleeds its painted backdrop); the
       yard's box and all game geometry stay in the column. */
    #gameYard .scene-bg { left: calc(50% - 50vw); width: 100vw; right: auto; }
    /* v98: the shop overlay's scene is already full-screen, but
       its ornaments still scaled with the screen (a ~170px sun
       disc at 1280 wide) — the same design-size caps as the farm
       and the stage apply there too. */
    #shopOverlay .sc-sunw { width: min(25%, 150px); }
    #shopOverlay .sc-cw1 { width: min(31%, 250px); }
    #shopOverlay .sc-cw2 { width: min(22%, 180px); }
    #shopOverlay .sc-treew { width: min(23%, 185px); }
    /* v72 (Addendum 8): the sun's halo breathes (opacity + scale)
       and its rays turn very slowly — transform/opacity only. */
    .scene-bg .sc-sunw::before {
      content: ""; position: absolute; inset: -22%;
      background: radial-gradient(circle, rgba(255, 217, 60, .5) 0%, rgba(255, 217, 60, 0) 62%);
      border-radius: 50%; animation: scHalo 7s ease-in-out infinite alternate;
    }
    .scene-bg .sc-sun { position: relative; }
    .scene-bg .sc-rays { transform-box: fill-box; transform-origin: center; animation: scRays 240s linear infinite; }
    @keyframes scHalo { from { opacity: .55; transform: scale(1); } to { opacity: .9; transform: scale(1.07); } }
    @keyframes scRays { to { transform: rotate(360deg); } }
    /* v72 (Addendum 8): clouds TRAVEL the whole sky on a discreet
       loop (they leave one edge fully before re-entering the other),
       two layers at different speeds = gentle parallax. */
    .scene-bg .sc-cloudw { will-change: transform; }
    .scene-bg .sc-cw1 { left: 2%; top: 5%; width: 31%; aspect-ratio: 130 / 45; animation: scTravel1 150s linear infinite; animation-delay: -55s; }
    .scene-bg .sc-cw2 { left: 44%; top: 1.5%; width: 22%; aspect-ratio: 92 / 32; animation: scTravel2 210s linear infinite; animation-delay: -120s; }
    @keyframes scTravel1 { from { transform: translateX(-115%); } to { transform: translateX(435%); } }
    @keyframes scTravel2 { from { transform: translateX(-115%); } to { transform: translateX(565%); } }
    .scene-bg .sc-treew { left: 0.5%; bottom: 5%; width: 23%; aspect-ratio: 92 / 136; }
    /* Perf (v50-smooth budget, measured on the harness): animating
       groups inside one stage-wide SVG cost 19 long tasks/15 s;
       split layers with every ornament animated cost 13; rotating
       or scaling layers ~9. What fits: ornaments on their own
       small layers, and the cloud drift DUTY-CYCLED — it glides
       for a few seconds, then rests (a gust pattern), so most
       frames recomposite nothing behind the pets. */
    /* v72 (Addenda 6/7/10): seated pets (locked friends along the
       fence, and the first-run pick row) invite with a gentle
       side-to-side wave — transform on the face only, staggered by
       nth-child delays so the row never moves in lockstep. */
    .farm-pet.seated-pet .p-face { animation: seatWave 2.8s ease-in-out infinite; transform-origin: 50% 88%; }
    @keyframes seatWave { 0%, 100% { transform: rotate(-2.4deg) translateY(0); } 50% { transform: rotate(2.4deg) translateY(-3px); } }
    #petGrid .farm-pet:nth-child(2) .p-face { animation-delay: -.6s; }
    #petGrid .farm-pet:nth-child(3) .p-face { animation-delay: -1.25s; }
    #petGrid .farm-pet:nth-child(4) .p-face { animation-delay: -1.9s; }
    #petGrid .farm-pet:nth-child(5) .p-face { animation-delay: -2.45s; }
    .fp-lock-badge { font-size: 12.5px; font-weight: 800; letter-spacing: .2px; }
    /* v72 (Addendum 5b): the stage pet hops with delight when the
       child places a division/multiplication item — keyframes carry
       the stage's -50% centring so the hop never shifts it sideways. */
    #petFace.feed-react { animation: feedReact .45s ease-out; }
    @keyframes feedReact { 0% { transform: translateX(-50%) translateY(0); } 40% { transform: translateX(-50%) translateY(-9px); } 100% { transform: translateX(-50%) translateY(0); } }
    /* v72 (Addenda 6/8): ambient life layer (js/pets/ambient.js) —
       sits directly above the scenery (z 1) and below every pet,
       bubble and button; only the creatures themselves take taps
       (a tap just makes one dart away). Transform/opacity only. */
    .ambient-layer { position: absolute; inset: 0; z-index: 1; pointer-events: none; overflow: hidden; }
    .amb-bee, .amb-bird, .amb-leaf { position: absolute; left: 0; top: 0; pointer-events: auto; will-change: transform; }
    .amb-bee { width: 34px; height: 25px; }
    .amb-bird { width: 46px; height: 37px; }
    .amb-leaf { width: 18px; height: 18px; pointer-events: none; }
    .amb-wing { transform-box: fill-box; transform-origin: 50% 92%; animation: ambFlutter .09s ease-in-out infinite alternate; }
    @keyframes ambFlutter { from { transform: scaleY(.35) rotate(-6deg); } to { transform: scaleY(1) rotate(5deg); } }
    .amb-bw { transform-box: fill-box; transform-origin: 85% 92%; animation: ambFlap .26s ease-in-out infinite alternate; }
    @keyframes ambFlap { from { transform: rotate(24deg); } to { transform: rotate(-17deg); } }
    .amb-bird.perched .amb-bw { animation: none; transform: rotate(40deg); }
    @media (prefers-reduced-motion: reduce) { .amb-wing, .amb-bw { animation: none !important; } }
    @media (prefers-reduced-motion: reduce) { .scene-bg * { animation: none !important; } }
    .farm-pet {
      position: absolute; padding: 0; text-align: center; background: none; border: none;
      box-shadow: none; width: 38%;
    }
    .farm-pet .p-face {
      /* No spotlight disc behind farm pets (user feedback): pets stand
         directly on the grass, like on the care stage. The soft ground
         shadow in ::after below stays. */
      position: relative; width: 100%; aspect-ratio: 1; margin: 0 auto; border-radius: 50%;
    }
    .farm-pet .p-face::after {
      content: ""; position: absolute; left: 13%; right: 13%; bottom: 1%; height: 11%;
      border-radius: 50%; background: radial-gradient(ellipse at center, rgba(35, 80, 30, .22), transparent 68%);
    }
    .farm-pet[data-pet="cat"] { left: 1%; bottom: 1.5%; width: 47%; z-index: 4; }
    .farm-pet[data-pet="dog"] { right: 1%; bottom: 3%; width: 41%; z-index: 3; }
    .farm-pet[data-pet="rabbit"] { left: 4%; top: 11.5%; width: 33%; z-index: 2; }
    .farm-pet[data-pet="hen"] { right: 5%; top: 9.5%; width: 30%; z-index: 2; }
    .fp-plate {
      display: inline-flex; align-items: center; gap: 6px; margin-top: 2px; position: relative; z-index: 5;
      /* v50 smoothness: no backdrop-filter here — the plates move
         every frame and a live blur repaint per plate was part of
         the yard's frame cost; the .82 white is nearly opaque. */
      background: rgba(255, 255, 255, .82); border: 2px solid rgba(255, 255, 255, .9);
      border-radius: 999px; padding: 2px 9px 3px; box-shadow: 0 2px 0 rgba(35, 50, 70, .12);
      max-width: 100%; white-space: nowrap;
    }
    .farm-pet .p-name { font-size: 13.5px; font-weight: 800; line-height: 1.15; margin: 0; }
    .farm-pet .p-hearts { font-size: 9.5px; letter-spacing: .5px; line-height: 1.2; margin: 0; }
    /* v46 audit: on narrow screens the plate (name + 5 hearts) is
       nearly as wide as the front-row face, so two neighbours'
       plates overlap as much as their bodies may touch. Shrink the
       plate contents a step so the plate stays clearly narrower
       than the face; text still fits in full (fit-first). */
    @media (max-width: 420px) {
      .fp-plate { gap: 4px; padding: 2px 7px 3px; }
      .farm-pet .p-name { font-size: 12.5px; }
      .farm-pet .p-hearts { font-size: 8.5px; letter-spacing: 0; }
    }
    .farm-pet:active .fp-plate { transform: translateY(2px); }
    /* v65 (Issue #109 item 1): the farm-pet BUTTON is a wide
       rectangle, but the pet FIGURE is a centred standing shape —
       overlapping button rectangles let a tap on one pet's body
       land on a neighbour's higher-z button (the cat granting the
       dog on the first, irreversible pick). Hit area = the figure:
       the button and its face/canvas take no pointer events; only
       the figure ellipse (.p-hit, centred, ~74% of the face wide,
       full height, bottom-anchored like the standing rig) and the
       nameplate receive taps. Clicks still bubble to the button's
       own handler, so the tapped pet is always the pet whose figure
       (or plate) is visually at that point. */
    .farm-pet { pointer-events: none; }
    .farm-pet .p-face { pointer-events: none; }
    .farm-pet .p-face > * { pointer-events: none; }
    .farm-pet .p-hit {
      position: absolute; left: 50%; bottom: -1%; width: 74%; height: 100%;
      transform: translateX(-50%); border-radius: 50%; pointer-events: auto;
    }
    .farm-pet .fp-plate { pointer-events: auto; }
    /* v65: starter-pick confirmation (first free pet is final —
       one friendly check before it sticks). */
    .starter-confirm {
      position: fixed; inset: 0; z-index: 66; display: flex;
      align-items: center; justify-content: center; padding: 18px;
      background: rgba(30, 45, 70, .45);
    }
    .starter-confirm[hidden] { display: none; }
    .sc-card {
      background: #fffdf5; border-radius: 26px; padding: 20px 22px 18px;
      max-width: min(92vw, 430px); width: 100%; min-width: 0; text-align: center;
      box-shadow: 0 14px 40px rgba(20, 35, 60, .35);
      border: 3px solid #ffd166;
    }
    .sc-face { width: min(52vw, 210px); aspect-ratio: 1; margin: 0 auto; position: relative; }
    .sc-face svg, .sc-face canvas { width: 100%; height: 100%; display: block; }
    .sc-q { font-size: clamp(20px, 5.6vw, 27px); font-weight: 800; color: #26436b; margin: 8px 0 14px; line-height: 1.3; overflow-wrap: break-word; }
    .sc-btns { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
    .sc-btns button {
      font: inherit; font-weight: 800; border: none; cursor: pointer;
      border-radius: 999px; padding: 13px 20px; font-size: clamp(16px, 4.4vw, 20px);
    }
    .sc-yes { background: #37b24d; color: #fff; box-shadow: 0 4px 0 #2b8a3e; }
    .sc-no { background: #eef2f7; color: #40536e; box-shadow: 0 4px 0 #cfd9e4; }
    .sc-btns button:active { transform: translateY(2px); box-shadow: none; }
    /* Roaming farm (JS director): pets are placed by transform, sized by
       depth; the corner rules above stay as the no-director fallback. */
    .farm-scene.farm-live .farm-pet { left: 0 !important; right: auto !important; top: 0; bottom: auto !important; will-change: transform; }
    .farm-butterfly {
      position: absolute; z-index: 8; width: 44px; height: 44px; margin: -22px 0 0 -22px;
      pointer-events: none; font-size: 34px; line-height: 44px; text-align: center;
    }
    .farm-butterfly .fb-in { display: inline-block; animation: fbFlap .34s ease-in-out infinite alternate; }
    @keyframes fbFlap { from { transform: scaleX(1) rotate(-7deg); } to { transform: scaleX(.55) rotate(7deg); } }
    .farm-fx {
      position: absolute; z-index: 9; pointer-events: none; font-size: 19px; line-height: 1;
      text-shadow: 0 1px 3px rgba(255, 255, 255, .9);
    }
    
    #screen-home .foot-note { margin-top: 12px; font-size: 12.5px; }

    /* --- v57 (Addenda 12+13): NO bottom nav. The parent gate is a
       round button in the topbar now (see #parentGateBtn below);
       the home screen keeps NO bottom reserve — the 104px padding
       the old fixed bar needed is gone, so the farm scene gets the
       full frame height back. --- */
    /* Parent gate in the topbar: an .icon-btn (round, like the
       sound toggle) carrying the .hold-btn fill mechanics. Shown on
       the home screen only — exactly where the deleted bottom nav
       used to offer it; the 3s hold + fill + aria are unchanged. */
    #parentGateBtn { display: none; width: auto; font-size: 21px; line-height: 1; }
    body.on-home #parentGateBtn { display: inline-flex; }
    #parentGateBtn .gate-ico { line-height: 1; }
    /* v66: fullscreen toggle in the same topbar cluster. It is a
       plain .icon-btn (same size/style as the sound toggle); the
       two inline SVGs swap on the .is-fs state class that
       toggles.js keeps in sync with fullscreenchange. The script
       hides the button (inline display:none) where the Fullscreen
       API does not exist. */
    #fsBtn svg { display: block; }
    #fsBtn .fs-ico-shrink { display: none; }
    #fsBtn.is-fs .fs-ico-expand { display: none; }
    #fsBtn.is-fs .fs-ico-shrink { display: block; }
    /* One-screen rule: home is a fixed flex frame like the care screen —
       the farm scene flexes to whatever height is left, nothing scrolls. */
    /* v97 (bi photo QA 2026-10-29 12:17, wide frame): this rule's
       overflow:hidden CLIPPED the v96 full-bleed scene back into the
       wrap's own box. The wrap is the centred <=620px column, so on
       wide screens the scene's box measured 0..100vw (negative
       margins) but its PAINTING — hills, fence, flowers, the lawn
       tint — stopped dead at the column edges, leaving a visible
       colour step at both sides (measured at 1280x800: lawn
       rgb(91,201,82) inside vs rgb(76,194,74) outside at the same
       row; hiding .scene-bg removed the step, and getBoundingClientRect
       alone never showed it — only a pixel scan does). The wrap no
       longer clips: the column's children are all width-bounded and
       the scene breakout is exactly 100vw, so nothing can spill;
       body.on-home below carries the horizontal guard instead, and
       the fixed 100dvh flex column (min-height:0 chain) keeps the
       one-screen rule vertically. */
    body.on-home .wrap { display: flex; flex-direction: column; height: 100vh; height: 100dvh; height: var(--app-h, 100dvh); overflow: visible; }
    /* v93 fix2 (lead verification, starter picker): the home
       screen's backdrop was the generic page gradient, which
       fades to pale cream at the bottom — so the farm scene sat
       in a rounded frame on a pale page and the greeting block
       under it floated on cream, a different world from the
       yard. On the home screen the backdrop now CONTINUES the
       scene: same sky at the top, and the stage's own grass
       recipe below, so the frame's surroundings and the whole
       strip under it read as the yard itself. Paint only; the
       scene card, its border and every layout rule stand.
       v96 SUPERSEDES the last sentence: the scene card and its
       border are GONE (see .farm-scene) and this backdrop is now
       the ONLY base layer — the continuation became the thing
       itself. */
    body.on-home {
      /* v97: the horizontal spill guard lives here now (the wrap
         must not clip — see the .wrap rule above). The body is
         viewport-sized, so this clips only true spill past the
         screen edge and never the scene itself. */
      overflow-x: hidden;
      /* v96: THE one base layer for the whole home screen — a single
         viewport-sized sky->grass fade (soft, no hard stop: the
         drawn hills in the scene's .scene-bg own the horizon line,
         the v95 lesson). The scene itself paints no base any more,
         so at every width the sky above the topbar, the yard and
         the grass under the greeting are literally one gradient:
         no top edge, no bottom edge, no side column. The fade's
         mid-band (36-58%) sits behind the hills/fence at every
         matrix frame, measured on the v96 harness pass. */
      background:
        radial-gradient(ellipse 190px 54px at 14% 3%, rgba(255, 255, 255, 0.95) 58%, rgba(255, 255, 255, 0) 62%),
        radial-gradient(ellipse 150px 44px at 86% 9%, rgba(255, 255, 255, 0.9) 58%, rgba(255, 255, 255, 0) 62%),
        linear-gradient(180deg, #6ec9ff 0%, #aee9ff 26%, #c9f2c0 36%, #8fe06a 44%, #63d64f 58%, #4cc24a 78%, #4cc24a 100%);
    }
    /* the parent-facing note under the scene keeps its words
       readable on the grass — the same soft label the game
       menu lays on the scene (menu.css .menu-zone-label). */
    body.on-home .farm-greet .foot-note {
      background: rgba(255, 255, 255, .78); border-radius: 14px; padding: 6px 10px;
    }
    body.on-home #screen-home.active { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
    body.on-home .farm-scene { flex: 1 1 auto; height: auto; min-height: 0; }
    body.on-home #screen-home .foot-note { flex: 0 0 auto; margin-top: 8px; }
    /* v47 (real-device QA): short viewports — landscape phones, small
       screens, browser chrome eating height. The home chrome (lang /
       age rows, foot-note, nav reserve) used to keep its full height
       and crush the farm scene below the pets' own height, so the pet
       buttons spilled out of the scene and their taps landed on the
       foot-note / bottom nav instead ("bấm không phản hồi"). Compact
       the chrome in steps so the scene always keeps room for the
       pets; the farm director also caps pet size by scene height. */
    @media (max-height: 640px) {
      .langbar { margin-bottom: 8px; gap: 6px; }
      .lang-btn { min-height: 42px; font-size: 16px; padding: 4px; border-radius: 15px; }
      .hud-ages, .age-picker.seg { margin-bottom: 6px; padding: 3px; }
      .hud-ages .age-btn, .age-picker.seg .age-btn { min-height: 34px; font-size: 13px; }
      body.on-home #screen-home .foot-note { margin-top: 5px; font-size: 11px; line-height: 1.35; }
    }
    @media (max-height: 500px) {
      body.on-home #screen-home .foot-note { display: none; }
      .lang-btn { min-height: 36px; }
      /* The farm face is a fixed-size box (96/118px) that ignores the
         button width the director sets — in a short scene the button
         (face + plate ≈ 131–153px) could not shrink at all and pets
         overflowed the yard no matter what the JS clamp did. Shrink
         the face itself so the whole button fits a short scene. */
      .pet-card.farm-pet .p-face { width: 62px; height: 62px; }
    }
    /* v75: sentence is a mode panel on the pet screen (below). */
    /* Part F (feedback #12): the sentence builder joins the one-screen
       flex budget — header + slots + read-out footer are fixed, the
       three pick rows share whatever height is left and their option
       cards stretch/shrink with it. Nothing scrolls, nothing clips. */
    #mode-sentence .task, #mode-sentence .task-sub { flex: 0 0 auto; }
    #mode-sentence .sentence-slots { flex: 0 1 auto; }
    /* The three chosen-word slots always share ONE row — at 104px
       min-width they wrapped to two rows on 360px phones and crushed
       the pick area into unusable slivers (Part F screenshot find).
       They are compact chips (icon + word side by side; the role word
       repeats the pick-row labels and is dropped) so the pick groups
       below get the height instead. */
    #mode-sentence .slot { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; justify-content: center; gap: 7px; padding: 7px 9px; border-radius: 16px; }
    #mode-sentence .slot .s-pic { font-size: 30px; line-height: 1; }
    #mode-sentence .slot .s-word { font-size: 19px; line-height: 1.25; }
    #mode-sentence .slot .s-word:has(ruby) { line-height: 1.9; }
    #mode-sentence .slot .s-role { display: none; }
    #mode-sentence .sentence-slots { margin: 10px 0 2px; gap: 8px; flex-wrap: nowrap; }
    #mode-sentence .pick-row, #mode-sentence .pick-opt { overflow: hidden; }
    /* v21 (feedback #20): the v20 protections lived only inside
       a short-height band; the rules below apply at all heights
       (v91 fix 2026-10-28: the surgery dropped this comment's
       close, swallowing the rules + the 900px band below). */
    #mode-sentence .pick-opt { flex: 1 1 0; min-width: 0; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 6px 4px; word-break: keep-all; overflow-wrap: normal; }
    #mode-sentence .pick-opt .p-pic { margin-bottom: 0; }
    #mode-sentence .slot .s-word { white-space: nowrap; word-break: keep-all; overflow-wrap: normal; }
    #mode-sentence .sentence-text { word-break: keep-all; overflow-wrap: normal; }
    #mode-sentence .msg { min-height: 26px; margin-top: 6px; }
    #mode-sentence .speaker-btn.huge { min-height: 62px; font-size: 23px; padding: 10px 24px; }
    #mode-sentence .speaker-btn.huge svg { width: 32px; height: 32px; }
    /* v100: flex 0 0 auto — the v84/v88 shrink-to-fit flex (1 1
       auto + min-height 0) let the panel squeeze #sentPicks to
       47px while its rows measured 178px, so the tiles PAINTED
       131px over the .sentence-read strip below (both static, DOM
       paint order). The picks area now sizes to its content at
       every frame; the room comes from the v100 stage cap +
       pick-card compaction in responsive.css. */
    #sentPicks { flex: 0 0 auto; min-height: 0; display: flex; flex-direction: column; }
    #mode-sentence .sentence-read { flex: 0 0 auto; margin-top: 12px; }
    @media (max-height: 900px) {
      #mode-sentence .task-sub { display: none; }
      #mode-sentence .sentence-text { font-size: 24px; margin-bottom: 8px; }
      #mode-sentence .speaker-btn.huge { min-height: 54px; font-size: 21px; }
      #mode-sentence .pick-opt .p-pic { font-size: 30px; }
    }
    
    

    /* --- PET CARE: full-scene stage + bottom sheet + fixed toolbar ---
       The care screen is a fixed app frame: its height comes from JS
       (--app-h = visualViewport height, kept current on resize) with
       vh/dvh fallbacks, so the page itself never scrolls in the kid
       area on any browser — older WebViews without dvh included. */
    body.on-pet .wrap { display: flex; flex-direction: column; height: 100vh; height: 100dvh; height: var(--app-h, 100dvh); padding-bottom: 8px;
      /* v93 (night order — the SHARED frame guarantee): the kid
         area can never scroll the document, whatever a single
         game panel does. A panel whose content outgrows its slot
         used to push the whole page (win banner up = scrollbar +
         clipped pet pill, bi's dots photo) because nothing in the
         frame clipped the overflow. The frame now clips at its
         own edge: a too-tall panel loses its overflow INSIDE the
         screen instead of scrolling the app, and each lead game
         additionally fits its content properly (flex budgets).
         Fixed overlays (menu, shop, confirms) are unaffected —
         overflow:clip creates no containing block for them. */
      /* v98: was overflow:clip — the clip now lives on the
         VIEWPORT box instead (body.on-pet below): the pet screen's
         scenery layers bleed to the screen edges like the home
         scene (v96/v97), and a clip on this 620px column would cut
         their painting at the column edges — the exact v97 farm
         bug. The v93 guarantee is unchanged (the document cannot
         scroll in the kid area); an over-tall panel is clipped at
         the screen's edge rather than the column's. */
      overflow: visible; }
    body.on-pet { overflow: hidden; }
    body.on-pet #screen-pet { flex: 1 1 auto; min-height: 0; }
    #screen-pet .pet-panel {
      background: none; border: none; box-shadow: none; padding: 0; text-align: center;
      display: flex; flex-direction: column; height: 100%; min-height: 0;
    }
    #screen-pet .pet-stage {
      position: relative; flex: 0 0 auto; min-height: 150px; height: calc(var(--stage-w, 366px) * 1.06); margin: 0; padding: 0;
      border: 3px solid #4cb944; border-radius: 26px; box-shadow: var(--shadow);
      /* v98: overflow visible (was hidden) and no backdrop of its
         own — the backdrop moved onto the scenery layer below,
         which bleeds to the screen edges. The stage box itself
         does not move: pet, bubble, HUD and every measured
         geometry stay in the <=620px column exactly as before.
         The scenery layer clips its own painting; the Pixi host
         clips the pet to its own box. */
      overflow: visible; background: none;
    }
    /* v98 (bi photo QA 2026-10-29 13:36-13:38): the borderless
       requirement covers every screen, not just home. The stage's
       scenery layer runs edge to edge (the stage is centred, so
       50% - 50vw lands on the viewport's left edge) and carries
       the stage's backdrop gradient, so sky, hills and fence read
       as one endless scene across any window; the v97 sky-only
       state for yard games retargets this same layer. */
    #screen-pet .pet-stage .scene-bg {
      left: calc(50% - 50vw); width: 100vw; right: auto;
      background: linear-gradient(180deg, #8fd8ff 0%, #c8f2ff 54%, #93e77e 54.5%, #63d64f 100%);
    }
    /* v93 addendum 3 (bi 23:00, phone photo — the home scene was a
       band across the top ~55% of a tall phone, dead pale space
       below): in the care landing the stage GROWS into whatever
       panel height is left over, so sky + grass fill the whole
       frame at every aspect ratio. Extra height extends the
       SCENERY only: the sky gradient stretches, the ground layer
       is bottom-anchored (anim.js xMidYMax slice), the pet stays
       grounded at the stage's bottom edge at its capped size.
       Gated OFF while the game menu is open — the menu slims the
       stage with an inline height and flex-grow would defeat it
       (the menu re-measures the natural stage, as before). Game
       modes are untouched: their panels are not #mode-care. */
    /* Portrait only: the landscape split (stage as a side column)
       has its own proven geometry (v81 land bases) and a rotate
       prompt on touch devices — the grow is for tall phones. */
    @media (max-aspect-ratio: 1/1) {
      html:not(:has(#menuOverlay:not([hidden]))) #screen-pet .pet-panel:has(> #mode-care.active) > .pet-stage {
        flex: 1 1 auto;
        /* Capped: past ~1.3x the natural stage the bottom-anchored
           ground slice (anim.js) scales the fence and tree far
           past their drawn proportions (measured on the first
           uncapped pass at 390x915 — pickets twice their design
           size). The stage takes the first share of the leftover;
           whatever remains goes to the care panel below, which is
           grass at every width (responsive.css) — the framed
           scene + grass field composition the menu and the dots
           board already wear, all artwork at design scale. */
        max-height: calc(var(--stage-w, 366px) * 1.30);
      }
    }
    #screen-pet .pet-stage > .pet-face {
      position: absolute; left: 50%; bottom: 6px; transform: translateX(-50%);
      /* The pet scales with the stage: its height always leaves the
         same ~132px of sky for the speech bubble, so the bubble can
         never cover the pet at any stage size. */
      height: min(192px, calc(100% - 132px)); width: auto; aspect-ratio: 1 / 1;
    }
    #screen-pet .pet-head {
      position: absolute; top: 6px; left: 6px; z-index: 6; margin: 0;
      display: flex; align-items: center; gap: 6px;
      background: rgba(255, 255, 255, .92); border: 2.5px solid #ffb84d; border-radius: 999px;
      padding: 2px 10px 2px 9px; box-shadow: 0 3px 0 rgba(35, 50, 70, .12);
    }
    #screen-pet .pet-head .ph-name { font-size: 17px; }
    #screen-pet .pet-head .ph-hearts { font-size: 12px; letter-spacing: .5px; }
    /* v78: the bubble clears the name/hearts pill in EVERY band —
       the old trace tops (36-40px) slid it under the pill. Tops
       below are pill bottom + a real margin; fitBubbleText still
       guards the pet side. Radius: the soft cloud shape. */
    #screen-pet .bubble {
      position: absolute; top: 58px; left: 10px; right: auto; z-index: 46;
      /* v100 (bi photo QA 2026-10-29 15:51 — the JA chase bubble
         still spanned ~600px as a one-line bar over the sun):
         the v89 caps (620px/78%, 600px/72%) were wider than a
         ~30-char sentence plus its target badge, so nothing ever
         wrapped. The cap is now a true comic-balloon size —
         min(320px, 78%) by default (phones had NO % cap at all
         and ran 0.95 of the stage), min(380px, 62%) on wide
         stages — so a long request wraps to 2-3 lines and the
         badge sits beside the text instead of stranded at the
         far edge of a bar. fitBubbleText still guarantees the
         text is never clipped (it steps the font, and the
         bubble grows taller, never wider). */
      width: fit-content; max-width: min(320px, 78%);
      display: flex; align-items: center; gap: 9px; text-align: left;
      padding: 7px 11px; border-radius: 32px;
    }
    /* v89 addendum 8 (bi design QA 2026-10-28 11:25): the bubble
       HUGS its content like a comic speech balloon — it used to
       stretch into a full-width subtitle bar on big screens (one
       long line across the whole scene). Width now follows the
       text with a hard cap on wide stages, and long lines WRAP
       to 2-3 lines instead of widening (fitBubbleText allows the
       third line on tall wide stages). The tail still tracks
       the pet.
       The % caps are relative to the stage, so they only apply
       when the stage is the full-width band (portrait / tablet):
       in the split landscape layout the stage is a ~252px column
       and 72-78% of it strangles the bubble into ~8 lines at the
       font floor, straight into the pet (v81 land-word corner,
       measured diag89g) — short viewports keep the plain
       fit-content width instead. */
    @media (min-width: 700px) and (min-height: 501px) {
      #screen-pet .bubble { max-width: min(380px, 62%); }
    }
    @media (min-width: 1100px) and (min-height: 501px) {
      #screen-pet .bubble { max-width: min(380px, 62%); }
    }
    /* v88: tray dock — while the pet eats at the food tray and a
       tall question bubble would hang over it, home.js docks the
       bubble to the free side of the stage (max-width set inline
       from live geometry, so the bubble and the pet's box can never
       intersect). The text stays fully readable: the bubble grows
       taller on its side instead of covering the pet. */
    #screen-pet .bubble.dock-l { left: 10px; right: auto; }
    #screen-pet .bubble.dock-r { left: auto; right: 10px; }
    /* v67: comic tail points DOWN at the pet; its horizontal spot
       follows the pet (--tailx, kept current by syncBubbleTail in
       home.js — the pet walks, the tail walks with it). */
    #screen-pet .bubble::before {
      left: var(--tailx, 50%); top: auto; bottom: -15px; margin: 0;
      transform: translateX(-50%);
      border: 10px solid transparent; border-top-color: var(--ink); border-bottom: 0;
      border-right-color: transparent; border-left-color: transparent;
    }
    #screen-pet .bubble::after {
      left: var(--tailx, 50%); top: auto; bottom: -8px; margin: 0;
      transform: translateX(-50%);
      border: 7px solid transparent; border-top-color: #fffdf4; border-bottom: 0;
      border-right-color: transparent; border-left-color: transparent;
    }
    /* v95 (bi photo QA 2026-10-29 08:20 — "the speech bubble's
       arrow is wrong"): on bi's wide-frame device the feed bubble
       rendered with the BASE left-pointing tail (care.css) instead
       of the down tail — in that layout the #screen-pet-scoped
       rules above did not reach the bubble. The same down tail is
       therefore ALSO scoped to the stage/panel containers
       themselves, so any intermediate reparenting inside the pet
       screen keeps the tail pointing down at the pet. Identical
       declarations — no visual change where the rules above
       already applied. */
    #screen-pet .pet-stage .bubble::before, #screen-pet .pet-panel .bubble::before {
      left: var(--tailx, 50%); top: auto; bottom: -15px; margin: 0;
      transform: translateX(-50%);
      border: 10px solid transparent; border-top-color: var(--ink); border-bottom: 0;
      border-right-color: transparent; border-left-color: transparent;
    }
    #screen-pet .pet-stage .bubble::after, #screen-pet .pet-panel .bubble::after {
      left: var(--tailx, 50%); top: auto; bottom: -8px; margin: 0;
      transform: translateX(-50%);
      border: 7px solid transparent; border-top-color: #fffdf4; border-bottom: 0;
      border-right-color: transparent; border-left-color: transparent;
    }
    #screen-pet .bubble .b-text {
      font-size: 16.5px; line-height: 1.22; flex: 1 1 auto;
      
    }
    #screen-pet .bubble .b-big { font-size: 34px; line-height: 1; flex: 0 0 auto; }
    #screen-pet #bubbleSpeak {
      position: absolute; top: 6px; right: 6px; z-index: 6;
      width: 44px; height: 44px; min-height: 44px; padding: 0; border-radius: 50%;
      display: flex; align-items: center; justify-content: center;
    }
    #screen-pet #bubbleSpeak svg { width: 22px; height: 22px; }
    #screen-pet #bubbleSpeak .spk-label {
      position: absolute; width: 1px; height: 1px; overflow: hidden;
      clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
    }
    /* Colour choice: one small round button; the swatches live in a
       popover that only opens on tap (kids pick a colour once, then
       rarely touch it again — it must not own a whole row). */
    #screen-pet .pet-colors {
      position: absolute; left: 8px; bottom: 8px; z-index: 6; margin: 0;
    }
    #screen-pet #petColorBtn {
      width: 44px; height: 44px; border-radius: 50%; padding: 0;
      font-size: 21px; line-height: 1; cursor: pointer;
      background: rgba(255, 255, 255, .92); border: 2.5px solid rgba(255, 255, 255, .95);
      box-shadow: 0 3px 0 rgba(35, 50, 70, .14);
      display: flex; align-items: center; justify-content: center;
    }
    #screen-pet #petColorBtn:active { transform: translateY(2px); box-shadow: none; }
    #screen-pet .pet-color-pop {
      position: absolute; left: 0; bottom: 52px; z-index: 7;
      display: none; align-items: center; margin: 0;
      background: rgba(255, 255, 255, .94); backdrop-filter: blur(4px);
      border: 2.5px solid rgba(255, 255, 255, .95); border-radius: 999px;
      padding: 6px 10px; box-shadow: 0 4px 10px rgba(35, 50, 70, .18);
    }
    #screen-pet .pet-colors.open .pet-color-pop { display: flex; }
    #screen-pet .pc-row { flex-wrap: nowrap; flex: 0 0 auto; gap: 8px; }
    #screen-pet .pc-swatch { width: 44px; height: 44px; border-width: 2.5px; flex: 0 0 auto; }
    #screen-pet .pc-swatch.active { transform: scale(1.1); }
    /* Feeding tray: correct food flies here and piles up; the pet
       walks over and eats at the tray. Pure DOM/CSS, no Pixi. */
    #screen-pet .food-tray {
      position: absolute; right: 10px; bottom: 4px; z-index: 4;
      width: 110px; pointer-events: none; text-align: center;
    }
    #screen-pet .food-tray[hidden] { display: none; }
    /* v79 (frame stability): the tray box is FIXED — its items area
       always reserves the full pile height (12 minis = 3 rows), so
       the box's top/height never move as food lands; the pile grows
       upward from the bowl inside the reserved area
       (align-content: flex-end), and the count badge rides the
       frame's fixed top instead of the pile's moving edge. */
    #screen-pet .tray-items {
      position: relative; z-index: 2; height: 86px; margin: 0 4px -9px;
      display: flex; flex-wrap: wrap; justify-content: center; align-items: flex-end; align-content: flex-end;
      gap: 0 1px;
    }
    #screen-pet .tray-mini {
      width: 25px; height: 25px; display: flex; align-items: center; justify-content: center;
      font-weight: 800; line-height: 1;
      /* v54: fed minis PILE in the bowl — overlapping, slightly
         askew, later arrivals on top — instead of a neat icon row. */
      position: relative; margin-left: -9px;
    }
    #screen-pet .tray-mini:first-child { margin-left: 0; }
    #screen-pet .tray-mini:nth-child(odd) { transform: rotate(-7deg); }
    #screen-pet .tray-mini:nth-child(even) { transform: rotate(6deg); }
    #screen-pet .tray-mini:nth-child(3n) { margin-bottom: 3px; }
    #screen-pet .tray-mini svg { width: 23px; height: 23px; }
    /* v23: a fish landing in the tray flops once (parabolic hop + wiggle). */
    #screen-pet .tray-mini.flop { animation: trayFlop .6s ease-in-out 1; transform-origin: 50% 85%; }
    @keyframes trayFlop {
      0% { transform: translateY(-7px) rotate(0deg); }
      30% { transform: translateY(-11px) rotate(-17deg); }
      55% { transform: translateY(0) rotate(11deg); }
      78% { transform: translateY(-3px) rotate(-6deg); }
      100% { transform: translateY(0) rotate(0deg); }
    }
    /* v56: the mini's own num-badge is GONE — at 10px, half-buried
       in the pile, it was the unreadable "tiny number behind the
       grapes" of the 12:47 QA photo. The number for the stage bowl
       lives on #trayBadge alone (below). */
    #screen-pet .tray-mini .num-badge { display: none; }
    /* v56: the stage bowl's own BIG count badge — same standard as
       the panel badge (bright yellow pill, 28px numeral, white rim,
       high contrast). Anchored to the tray's top-right: v79 made the
       tray box fixed-height, so the badge now sits at a fixed spot
       fully above the tallest possible pile (3 rows) at every fill
       level and no food icon can ever overlap it. It sits on the
       side AWAY from the approaching pet (the pet walks in from the
       stage centre, i.e. the tray's left). */
    #screen-pet .tray-badge {
      position: absolute; right: -2px; top: 0; z-index: 6;
      transform: translateY(-118%);
      min-width: 54px; padding: 3px 13px 5px;
      background: linear-gradient(180deg, #ffe259, #ffb52e);
      border: 3px solid #fff; border-radius: 999px;
      box-shadow: 0 3px 0 rgba(35, 50, 70, .25);
      color: #4a3200; font-size: 28px; font-weight: 900; line-height: 1.05;
      text-align: center; pointer-events: none;
    }
    #screen-pet .tray-badge[hidden] { display: none; }
    /* v72: the tray mini cookie is the same drawn SVG face as the
       choice buttons, glyph centred on top at pile size. */
    #screen-pet .tray-mini.cookie { color: #5b3a12; font-size: 11px; }
    #screen-pet .tray-mini.cookie .cookie-ch {
      position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
      font-weight: 800; color: #5b3a12; line-height: 1;
    }
    #screen-pet .tray-bowl {
      position: relative; height: 27px; margin: 0 auto; width: 104px;
      background: linear-gradient(180deg, #ff8a5c, #f06423);
      border: 3px solid #d14d12; border-top: none;
      border-radius: 6px 6px 52px 52px / 4px 4px 30px 30px;
      box-shadow: 0 3px 0 rgba(35, 50, 70, .15);
    }
    #screen-pet .tray-bowl::before {
      content: ""; position: absolute; left: -6px; right: -6px; top: -6px; height: 12px;
      background: linear-gradient(180deg, #ffb59d, #ff8a5c);
      border: 3px solid #d14d12; border-radius: 50%;
    }
    /* Legacy-SVG walk bob while the pet crosses to the tray */
    #screen-pet .pet-face.tray-walking .pet-art { animation: trayBob .26s ease-in-out infinite alternate; }
    @keyframes trayBob { from { transform: translateY(0); } to { transform: translateY(-4px); } }
    /* Bedtime: calm full-screen lock once the daily limit is reached.
       Since v57 the topbar parent gate sits UNDER this overlay, so
       the overlay carries its own quiet hold-to-open gate (.bt-gate,
       v59); the parent screen replaces home, hiding the overlay. */
    .bedtime-overlay {
      position: fixed; inset: 0; z-index: 60;
      background: linear-gradient(180deg, #b8c9f2 0%, #cfd8f7 42%, #e6e0f5 78%, #f3e9f7 100%);
      display: flex; flex-direction: column; align-items: center; justify-content: center;
      text-align: center; padding: 24px; gap: 6px;
    }
    .bedtime-overlay[hidden] { display: none; }
    .bedtime-overlay .bt-moon { font-size: 84px; line-height: 1.1; }
    .bedtime-overlay .bt-stars { font-size: 22px; letter-spacing: 8px; opacity: .85; }
    .bedtime-overlay .bt-title { font-size: 30px; font-weight: 800; color: #33406e; margin: 10px 0 0; }
    .bedtime-overlay .bt-msg { font-size: 20px; font-weight: 700; color: #55619c; margin: 4px 0 0; max-width: 420px; line-height: 1.45; }
    .bedtime-overlay .bt-zzz { font-size: 40px; margin-top: 12px; animation: btFloat 2.6s ease-in-out infinite alternate; }
    .bedtime-overlay .bt-gate { position: absolute; right: 14px; bottom: 14px; width: auto; min-height: 40px; padding: 8px 14px; font-size: 13px; border-radius: 999px; opacity: .72; background: rgba(255,255,255,.55); }
    @keyframes btFloat { from { transform: translateY(0); } to { transform: translateY(-9px); } }
    /* Parent-zone limit buttons: mark the active choice */
    #limitBtns button.active { background: var(--sky); border-color: var(--sky-dark); color: #fff; }
    /* Yard games: stage compacts to a slim speech bar above the yard */
    #screen-pet #gameYard { flex: 0 0 auto; height: 248px; margin: 10px 0 0; }
    #screen-pet .pet-panel:has(> #gameYard:not([hidden])) .pet-colors { display: none; }
    /* Trace mode: slim stage strip so the canvas fits the sheet.
       The bubble text clamps to one line here and the pet keeps a
       ~124px sky, so the bubble tail still clears the pet's ears. */
    #screen-pet .pet-panel:has(#mode-trace.active) .pet-stage > .pet-face { height: min(148px, calc(100% - 128px)); width: auto; aspect-ratio: 1 / 1; bottom: 8px; }
    #screen-pet .pet-panel:has(#mode-trace.active) .bubble .b-text { }
    /* v87 addendum 4 (bi QA 07:26): the tracing board grows — the
       260px cap made the glyph small for little fingers. 320px /
       88vw / 48dvh keeps the whole panel (bar + actions + picker)
       inside the one-screen frame at 390x844 and 320px widths;
       the guide, checkpoints and the child's brush all live in the
       660-unit trace space, so every stroke renders proportionally
       thicker and scoring is unchanged (v61 semantics). */
    /* The pet bubble already speaks + shows each mode's instruction,
       so the static task line inside the trace/word panels is dropped
       at every height to keep those panels inside the app frame. */
    #mode-trace .task-sub, #mode-word .task-sub, #mode-dots .task-sub { display: none; }
    /* Word mode: slightly slim stage, the panel content is tall */
    /* Dots mode: slim stage like tracing (panel holds the big board) */
    /* v15 (real-device feedback #7b): in dots mode the bubble shrinks
       to one line and the pet keeps a clearly visible size — at short
       heights the generic 132px sky reserve left a ~28px pet hiding
       under a two-line bubble. The one-line bubble incl. tail ends
       ~104px below the stage top, so a 114px reserve still clears
       the pet's ears while roughly doubling the pet at 160px stage. */
    #screen-pet .pet-panel:has(#mode-dots.active) .bubble .b-text { }
    #screen-pet .pet-panel:has(#mode-dots.active) .pet-stage > .pet-face { height: min(132px, calc(100% - 114px)); bottom: 8px; }
    /* Yard-game modes (chase / bath / maze): the stage is a slim
       speech strip and the pet itself plays down in the yard — clamp
       the bubble to one line so the strip reads as a caption bar
       instead of a wall of text where the pet "should" be. The full
       line is still spoken aloud and replayable via the speaker. */
    #screen-pet .pet-panel:has(> #gameYard:not([hidden])) .bubble .b-text { }
    /* Height bands: shorter viewports get a shorter stage, a tighter
       bubble and a smaller yard — the pet-face height rule above keeps
       the bubble clearance constant at every step. */
    @media (max-height: 840px) {
      #mode-trace { padding-top: 10px; }
    }
    
    
    /* Bottom sheet: active mode panel scrolls internally, page never scrolls */
    #screen-pet .mode-panel.active {
      flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
      background: linear-gradient(180deg, rgba(255, 255, 255, .95), rgba(242, 251, 255, .95));
      border: 3px solid var(--line); border-radius: 26px; box-shadow: var(--shadow);
      padding: 10px 12px 14px; margin-top: 10px; text-align: center;
    }
    #screen-pet .modes-hint { flex: 0 0 auto; margin: 8px 4px 0; font-size: 12.5px; }
    /* Two-row toolbar (narrow screens): panels pad past the taller bar,
       and the generic hint line yields its height to the panels. */
    @media (max-width: 600px) {
      #screen-pet .mode-panel.active { padding-bottom: 14px; }
      #screen-pet .modes-hint { display: none; }
      #mode-maze .maze-toprow { flex-wrap: nowrap; }
      #mode-maze .maze-toprow .game-score { font-size: 15px; white-space: nowrap; }
      #mode-maze .maze-toprow .ghost-btn { padding: 5px 10px; font-size: 14px; white-space: nowrap; min-height: 38px; }
    }
    /* Win bars dock at the panel's lower edge instead of adding
       flow height — a finished round never pushes content off-screen.
       v69: the bar is the slim shared bar (base.css); each mode keeps
       its artwork clear of the bar's strip via win-state reserve +
       compaction rules (games.css / dots.css). */
    #screen-pet .mode-panel { position: relative; }
    #screen-pet .win-banner.show {
      position: absolute; left: 12px; right: 12px; bottom: 12px; z-index: 6;
      margin-top: 0; box-shadow: 0 8px 22px rgba(120, 80, 0, .28);
    }
    @media (max-width: 600px) {
      #screen-pet .win-banner.show { bottom: 12px; }
    }
    /* Count-mode compaction extends to tablet-portrait heights: the
       mul/div visualisations are the tallest content in the app. */
    @media (max-height: 1100px) {
      #mode-count .pond { gap: 6px; margin-top: 0; }
      #mode-count .food-btn { min-height: 52px; border-radius: 16px; }
      #mode-count .pond.dense { grid-template-columns: repeat(6, minmax(0, 1fr)); }
      #mode-count .pond.dense .food-btn { min-height: 38px; padding: 2px; }
      #mode-count .pond.dense .food-btn svg { max-width: 30px; }
      #mode-count .pond.dense2 { grid-template-columns: repeat(8, minmax(0, 1fr)); }
      #mode-count .pond.dense2 .food-btn { min-height: 26px; padding: 2px; }
      #mode-count .pond.dense2 .food-btn svg { max-width: 20px; }
      /* v21: ponds can now hold up to 100 items (6-7 totals) — a third,
         tighter density keeps them inside the one-screen panel. */
      #mode-count .pond.dense3 { grid-template-columns: repeat(10, minmax(0, 1fr)); gap: 4px; }
      #mode-count .pond.dense3 .food-btn { min-height: 22px; padding: 1px; }
      #mode-count .pond.dense3 .food-btn svg { max-width: 17px; }
      #mode-count .add-eq { font-size: 22px; min-height: 24px; margin-top: 2px; }
      #mode-count .bowl-count { font-size: 14px; margin-top: 0; }
      #mode-count .progress-dots { margin-top: 4px; gap: 3px; flex-wrap: nowrap; }
      #mode-count .progress-dots span { width: 12px; height: 12px; }
      #mode-count .choices { gap: 8px; margin-top: 10px; }
      #mode-count .choice-btn { min-height: 68px; font-size: 36px; }
      #mode-count .muldiv-stage { margin-top: 0; }
      #mode-count .md-groups, #mode-count .md-bowls { gap: 6px; }
      #mode-count .md-group, #mode-count .md-bowl { flex: 1 1 0; min-width: 0; padding: 4px; }
      #mode-count .md-mini { width: 17px; height: 17px; }
      #mode-count .md-mini svg { width: 15px; }
      #mode-count .md-mini.empty { width: 15px; height: 15px; }
      #mode-count .md-group-items, #mode-count .md-bowl-items { min-height: 20px; max-width: none; gap: 2px; }
      #mode-count .md-group-label, #mode-count .md-bowl-label { font-size: 12px; margin-top: 2px; }
      #mode-count .md-reset { min-height: 36px; margin-top: 4px; padding: 4px 16px; font-size: 15px; }
      #mode-count .msg { min-height: 20px; margin-top: 2px; }
    }
    /* v79 (frame stability): the dots panel's bottom reserve is
       PERMANENT — the win bar's strip is reserved during play too,
       so the win moment changes no geometry at all (the v21 rule
       that returned the reserve while the banner showed is gone;
       the bar docks into space that was always there). */
    #mode-dots .msg { min-height: 22px; margin-top: 2px; }
    /* v21 worst-case rounds: fourth pond density + compact mul/div
       trays so totals to 100 stay inside the one-screen panel. */
    #mode-count .pond.dense4 { grid-template-columns: repeat(13, minmax(0, 1fr)); gap: 3px; }
    #mode-count .pond.dense4 .food-btn { min-height: 19px; padding: 1px; border-radius: 9px; }
    #mode-count .pond.dense4 .food-btn svg { max-width: 14px; }
    #mode-count .pond.dense5 { grid-template-columns: repeat(15, minmax(0, 1fr)); gap: 2px; }
    #mode-count .pond.dense5 .food-btn { min-height: 18px; padding: 0; border-radius: 8px; }
    #mode-count .pond.dense5 .food-btn svg { max-width: 13px; }
    #mulDivStage.md-compact .md-group, #mulDivStage.md-compact .md-bowl { flex: 1 1 18%; }
    /* huge rounds: the per-item progress dots are meaningless at ~100
       items and cost a row the panel needs */
    #mode-count.huge-round .progress-dots { display: none; }
    #mulDivStage.md-compact .md-groups, #mulDivStage.md-compact .md-bowls { gap: 4px; }
    #mulDivStage.md-compact .md-group, #mulDivStage.md-compact .md-bowl { padding: 3px; border-radius: 12px; }
    #mulDivStage.md-compact .md-mini { width: 12px; height: 12px; }
    #mulDivStage.md-compact .md-mini svg { width: 11px; }
    #mulDivStage.md-compact .md-mini.empty { width: 10px; height: 10px; border-width: 1px; }
    #mulDivStage.md-compact .md-group-items, #mulDivStage.md-compact .md-bowl-items { min-height: 13px; gap: 1px; }
    #mulDivStage.md-compact .md-group-label, #mulDivStage.md-compact .md-bowl-label { font-size: 10px; margin-top: 1px; }
    #mulDivStage.md-compact .md-reset { min-height: 30px; margin-top: 2px; padding: 2px 12px; font-size: 13px; }
    /* One-screen rule on phone-height viewports: slim the chrome and
       every mode panel so nothing scrolls inside the kid area. The
       speech bubble carries the instructions, so the duplicated task
       line can go; game pieces shrink a step; count rounds that live
       in the panel (more/left/missing/mul/div) collapse the stage to
       a speech strip via .count-panel-round (set by startCount). */
    @media (max-height: 880px) {
      body.on-pet .lang-btn { min-height: 40px; }
      body.on-pet .icon-btn { min-height: 40px; min-width: 40px; }
      body.on-pet .home-btn { min-height: 40px; }
      #screen-pet .mode-panel .task-sub { display: none; }
      #screen-pet .mode-panel.active { padding-top: 6px; }
      .game-score { margin-top: 4px; min-height: 24px; font-size: 18px; }
      #mode-word .word-pic { font-size: 64px; margin: 0 auto; }
      #mode-word .word-target { font-size: 25px; min-height: 40px; line-height: 1.7; }
      #mode-word .word-tiles { flex-wrap: nowrap; }
      #mode-word .word-tile { flex: 1 1 0; min-width: 0; }
      #mode-word .word-slots { flex-wrap: nowrap; }
      #mode-word .word-slot { flex: 1 1 0; min-width: 0; }
      /* v87 addendum 4: 205px -> 264px in this band too (the band
         that actually binds at 390x844); stage is already slimmed
         to 188px here, the panel absorbs the growth. */
      #mode-trace .trace-actions { margin-top: 4px; }
      #mode-trace .trace-actions .ghost-btn { padding: 6px 14px; min-height: 40px; }
      #mode-trace .trace-check-btn { min-height: 46px; padding: 7px 20px; font-size: 18px; }
      #mode-letter .cookie-btn { max-width: 88px; }
      .msg { min-height: 34px; margin-top: 8px; }
      #mode-word .word-slots { min-height: 50px; margin: 4px 0 0; gap: 6px; }
      #mode-word .word-slot { width: 44px; height: 50px; font-size: 28px; }
      #mode-word .word-tiles { gap: 7px; margin-top: 5px; }
      #mode-word .word-tile { min-width: 48px; height: 52px; font-size: 27px; }
    }
    

    /* v30: Garden mode — slim the stage so the board + choices + msg
       all stay inside the fixed app frame; the pet keeps its sky
       reserve via the generic stage rule, and the garden buddy stands
       in the board itself. */
    
    
    

    /* v31: Listen & guess — same slim-stage pattern as the garden:
       replay button + show strip + cards + score + msg must all stay
       inside the fixed app frame at phone heights. */
    
    
    

    /* v35: Pet race — same slim-stage pattern: track + show strip +
       cards + score + msg must all stay inside the fixed app frame. */
    
    
    

    /* v36: Cooking by menu — same slim-stage pattern: menu row +
       pot + tray + score + msg must all stay inside the fixed app
       frame at phone heights. */
    
    
    

    /* v37: Fishing — same slim-stage pattern: target row + pond +
       bucket row + score + msg must all stay inside the fixed app
       frame at phone heights. */
    
    
    

    /* v38: Build a house — same slim-stage pattern: house scene +
       part chips + part/show row + cards + score + msg must all
       stay inside the fixed app frame at phone heights. */
    
    
    

    /* v65: the unified feed panel carries the count family's
       contents (pond/groups/dots/choices) under #mode-feed ids —
       mirror the count compaction + stage-height rules so the
       unified game fits one screen exactly like count did. */
    @media (max-height: 1100px) {
      #mode-feed .pond { gap: 6px; margin-top: 0; }
      #mode-feed .food-btn { min-height: 52px; border-radius: 16px; }
      #mode-feed .pond.dense { grid-template-columns: repeat(6, minmax(0, 1fr)); }
      #mode-feed .pond.dense .food-btn { min-height: 38px; padding: 2px; }
      #mode-feed .pond.dense .food-btn svg { max-width: 30px; }
      #mode-feed .pond.dense2 { grid-template-columns: repeat(8, minmax(0, 1fr)); }
      #mode-feed .pond.dense2 .food-btn { min-height: 26px; padding: 2px; }
      #mode-feed .pond.dense2 .food-btn svg { max-width: 20px; }
      #mode-feed .pond.dense3 { grid-template-columns: repeat(10, minmax(0, 1fr)); gap: 4px; }
      #mode-feed .pond.dense3 .food-btn { min-height: 22px; padding: 1px; }
      #mode-feed .pond.dense3 .food-btn svg { max-width: 17px; }
      #mode-feed .add-eq { font-size: 22px; min-height: 24px; margin-top: 2px; }
      #mode-feed .bowl-count { font-size: 14px; margin-top: 0; }
      #mode-feed .progress-dots { margin-top: 4px; gap: 3px; flex-wrap: nowrap; }
      #mode-feed .progress-dots span { width: 12px; height: 12px; }
      #mode-feed .choices { gap: 8px; margin-top: 10px; }
      #mode-feed .choice-btn { min-height: 68px; font-size: 36px; }
      #mode-feed .muldiv-stage { margin-top: 0; }
      #mode-feed .md-groups, #mode-feed .md-bowls { gap: 6px; }
      #mode-feed .md-group, #mode-feed .md-bowl { flex: 1 1 0; min-width: 0; padding: 4px; }
      #mode-feed .md-mini { width: 17px; height: 17px; }
      #mode-feed .md-mini svg { width: 15px; }
      #mode-feed .md-mini.empty { width: 15px; height: 15px; }
      #mode-feed .md-group-items, #mode-feed .md-bowl-items { min-height: 20px; max-width: none; gap: 2px; }
      #mode-feed .md-group-label, #mode-feed .md-bowl-label { font-size: 12px; margin-top: 2px; }
      #mode-feed .md-reset { min-height: 36px; margin-top: 4px; padding: 4px 16px; font-size: 15px; }
      #mode-feed .msg { min-height: 20px; margin-top: 2px; }
    }
    #mode-feed .pond.dense4 { grid-template-columns: repeat(13, minmax(0, 1fr)); gap: 3px; }
    #mode-feed .pond.dense4 .food-btn { min-height: 19px; padding: 1px; border-radius: 9px; }
    #mode-feed .pond.dense4 .food-btn svg { max-width: 14px; }
    #mode-feed .pond.dense5 { grid-template-columns: repeat(15, minmax(0, 1fr)); gap: 2px; }
    #mode-feed .pond.dense5 .food-btn { min-height: 18px; padding: 0; border-radius: 8px; }
    #mode-feed .pond.dense5 .food-btn svg { max-width: 13px; }
    #feedGroups.md-compact .md-group, #feedGroups.md-compact .md-bowl { flex: 1 1 18%; }
    #mode-feed.huge-round .progress-dots { display: none; }
    #feedGroups.md-compact .md-groups, #feedGroups.md-compact .md-bowls { gap: 4px; }
    #feedGroups.md-compact .md-group, #feedGroups.md-compact .md-bowl { padding: 3px; border-radius: 12px; }
    #feedGroups.md-compact .md-mini { width: 12px; height: 12px; }
    #feedGroups.md-compact .md-mini svg { width: 11px; }
    #feedGroups.md-compact .md-mini.empty { width: 10px; height: 10px; border-width: 1px; }
    #feedGroups.md-compact .md-group-items, #feedGroups.md-compact .md-bowl-items { min-height: 13px; gap: 1px; }
    #feedGroups.md-compact .md-group-label, #feedGroups.md-compact .md-bowl-label { font-size: 10px; margin-top: 1px; }
    #feedGroups.md-compact .md-reset { min-height: 30px; margin-top: 2px; padding: 2px 12px; font-size: 13px; }
    

    /* ================= v101: feed — ONE session, ONE scene =======
       The panel is a flex column (same pattern as sentence, v75)
       whose sections never flex-shrink, and #feedMsg carries
       margin-top:auto — so the message line (and the win strip
       docked under it) sits at the panel's bottom edge in EVERY
       request family and phase, however tall the play content
       above it runs. That is the spec's "play-region reserve"
       invariant (msg/win never jump vertically between deals)
       achieved without reserving the tallest stack's height in
       every configuration. When the content genuinely fills the
       panel the auto margin collapses to the msg's own 2px and
       the layout is the old block flow. */
    #screen-pet .mode-panel#mode-feed.active { display: flex; flex-direction: column; }
    #mode-feed > * { flex: 0 0 auto; }
    #mode-feed .msg { margin-top: auto; }
    /* v101: the mul/div ask phase keeps the pond on stage —
       dimmed and touch-locked while the choices are up (feed.js
       toggles .ask-inert instead of the old display:none). */
    #mode-feed .pond.ask-inert { opacity: .45; pointer-events: none; }

    /* ================= v75: sentence as a normal mode =================
       The builder panel joins the pet frame's flex budget: the stage
       shrinks to a speech strip (pet + bubble stay fully visible),
       slots/read-out are fixed, the pick rows share the rest.
       Nothing scrolls beyond the shared panel behaviour, nothing
       clips (fitSentence still steps fonts down). */
    #screen-pet .mode-panel#mode-sentence.active { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
    #mode-sentence .pick-row { min-height: 46px; }
    #mode-sentence .sentence-slots { flex: 0 0 auto; }
    #mode-sentence #sentPicks { flex: 0 0 auto; min-height: 0; display: flex; flex-direction: column; }
    #mode-sentence .sentence-read { flex: 0 0 auto; margin-top: 6px; }
    #mode-sentence .sentence-text { font-size: 22px; margin-bottom: 6px; }
    #mode-sentence .speaker-btn.huge { min-height: 50px; font-size: 20px; padding: 8px 20px; }
    #screen-pet #mode-sentence.mode-panel:has(> .win-banner) { padding-bottom: 88px; }
    #mode-sentence .sent-d4-bar { margin-top: 2px; }
    #mode-sentence .sent-d4-btn { min-height: 34px; padding: 3px 12px; }
    #mode-sentence .sentence-slots { margin: 6px 0 0; }
    #mode-sentence .slot { padding: 5px 8px; }
    #mode-sentence .pick-row .p-label { font-size: 16px; margin-bottom: 3px; }
    #mode-sentence .pick-opt { min-height: 44px; }
    #mode-sentence .sentence-read { margin-top: 4px; }
    #mode-sentence .msg { min-height: 20px; margin-top: 2px; }
    
    
    /* ================= v84: sentence pick cards never clip =========
       PLAY1 (and bi's own eyes at v81) found the core builder's three
       pick rows flex-shrunk BELOW their content: .pick-row had
       flex 1 1 0 + min-height 0 and overflow hidden, so every card
       was silently cut to a ~40% sliver — at short heights and in
       landscape the rows even spilled under the read-out. The panel
       budget is deterministic now: core cards carry a FIXED height
       and fitSentence() steps their fonts to fit inside it; rows
       size to their content first and only grow with leftover
       space; chrome compacts (the JA D4 toggle no longer wraps to
       two lines; slots and the speak button slim down). Wide frames
       spend width instead of begging for height: the three groups
       become three columns and the read-out one row. The 88px win
       reserve, the stage heights and the D4 pattern grid are
       untouched — the pattern sub-mode already presented cleanly. */
    #mode-sentence .sent-d4-btn { white-space: nowrap; font-size: 14px; padding: 3px 10px; }
    #mode-sentence .pick-row { flex: 1 1 auto; min-height: max-content; margin-top: 6px; }
    #mode-sentence .pick-row:first-child { margin-top: 2px; }
    #mode-sentence .pick-row .p-label { line-height: 1.2; }
    #mode-sentence #sentPicks:not(.d4-picks) .pick-opts { flex: 0 0 auto; }
    #mode-sentence #sentPicks:not(.d4-picks) .pick-opt { height: 46px; min-height: 46px; padding: 2px 4px; }
    /* v88: the core bank grew to 9 who / 8 verbs / up to 4 objects —
       one rigid row of 9 would crush every card to a sliver, so core
       rows WRAP at 3 cards per row (basis 30%, capped at a third of
       the row so a leftover card never stretches full-width). The
       fixed card heights above still apply per card, at every
       width (the stepper below shows one wrapped group at a time). */
    #mode-sentence #sentPicks:not(.d4-picks) .pick-opts { flex-wrap: wrap; }
    #mode-sentence #sentPicks:not(.d4-picks) .pick-opt { flex: 1 1 30%; max-width: calc((100% - 16px) / 3); }
    /* the ruby 1.9 line-height belongs to the WORD, not the icon:
       inherited into .p-pic it inflated the icon's line box to
       ~34px and fitSentence() answered by crushing card fonts to
       9px — uncut but unreadable. Icons sit on a 1.0 line now. */
    #mode-sentence #sentPicks:not(.d4-picks) .pick-opt .p-pic { line-height: 1; }
    /* v88 stepper (ALL widths): even wrapped, three groups =
       ~8 card lines (~412px) against ~200-350px of picks height,
       and the >=700px column variant stacks 9 cards into ~446px
       against ~270px — the builder opens ONE group at a time,
       named by data-step (sentence.js). The slots above stay
       fully visible; their chips are tappable (slot-tap) and the
       open group's chip carries the slot-open ring. Picking
       advances the step. D4 (.d4-picks) is excluded: it is a
       single row already. */
    #mode-sentence #sentSlots .slot-tap { cursor: pointer; }
    #mode-sentence #sentSlots .slot.slot-open { box-shadow: 0 0 0 3px #ff9f45; }
    #mode-sentence #sentPicks:not(.d4-picks)[data-step="who"] .pick-row:not([data-group="who"]),
    #mode-sentence #sentPicks:not(.d4-picks)[data-step="verb"] .pick-row:not([data-group="verb"]),
    #mode-sentence #sentPicks:not(.d4-picks)[data-step="what"] .pick-row:not([data-group="what"]) { display: none; }
    #mode-sentence .sentence-slots { margin-top: 4px; }
    #mode-sentence .slot { padding: 3px 8px; }
    #mode-sentence .slot .s-pic { font-size: 26px; }
    #mode-sentence .slot .s-word { font-size: 17px; }
    #mode-sentence .sentence-read { margin-top: 2px; }
    #mode-sentence .sentence-text { line-height: 1.7; }
    #mode-sentence .speaker-btn.huge { min-height: 52px; padding: 6px 18px; line-height: 1.15; }
    #mode-sentence .speaker-btn.huge svg { width: 28px; height: 28px; display: block; }
    /* v93: short portrait frames (bi's 414x736 above all) — the
       v84 budget overflows there by ~60px: the open group's row is
       min-height:max-content (by design, cards never clip), so the
       row painted straight over the read-out and its button
       (glyph-measured: text overlap 20px, button overlap 18px).
       The permanent 88px win reserve (v79 frame stability) stays;
       the playing content itself compacts in two height bands —
       the same trick the landscape band already uses — until
       slots + one stepper group + read-out fit the panel whole. */
    @media (orientation: portrait) and (max-height: 820px) {
      #mode-sentence #sentPicks:not(.d4-picks) .pick-opt { height: 44px; min-height: 44px; }
      #mode-sentence .pick-row { margin-top: 4px; }
      #mode-sentence .sentence-slots { margin-top: 2px; }
    }
    @media (orientation: portrait) and (max-height: 760px) {
      #mode-sentence #sentPicks:not(.d4-picks) .pick-opt { height: 36px; min-height: 36px; }
      #mode-sentence #sentPicks:not(.d4-picks) .pick-opts { gap: 5px; }
      #mode-sentence .sent-d4-btn { min-height: 28px; }
      #mode-sentence .slot { padding: 2px 8px; }
      #mode-sentence .slot .s-pic { font-size: 24px; }
      #mode-sentence .speaker-btn.huge { min-height: 44px; }
      #mode-sentence .sentence-text { font-size: 21px; margin-bottom: 2px; }
      #mode-sentence .sentence-read { margin-top: 0; }
      #mode-sentence .pick-row .p-label { margin-bottom: 2px; }
      #mode-sentence .msg { min-height: 16px; }
    }
    
    @media (min-width: 700px) {
      /* v88: the former 3-column pick layout is gone — with 9 who
         cards a column needs ~446px of picks height (1280x800
         offers ~270px). The stepper's wrapped-row layout (above)
         now applies at every width; only the read-out keeps its
         wide side-by-side form. */
      #mode-sentence .sentence-read { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 6px 12px; }
      #mode-sentence .sentence-text { flex: 1 1 auto; margin-bottom: 0; }
      #mode-sentence .sentence-read .msg { flex: 1 1 100%; }
    }
    @media (orientation: landscape) and (max-height: 500px) {
      #mode-sentence .slot { padding: 2px 6px; }
      #mode-sentence .slot .s-pic { font-size: 20px; }
      #mode-sentence .slot .s-word { font-size: 15px; }
      #mode-sentence .sent-d4-btn { min-height: 26px; }
      #mode-sentence .pick-row .p-label { font-size: 14px; margin-bottom: 0; }
      #mode-sentence #sentPicks:not(.d4-picks) .pick-opts { gap: 4px; }
      #mode-sentence .msg { min-height: 14px; }
      /* fixed card height in short landscape too (v88: the wrapped
         rows keep their 30% basis — only the height is pinned), so
         a group's size is its label + card lines + gaps — never a
         max-content surprise */
      #mode-sentence #sentPicks:not(.d4-picks) .pick-opt { height: 34px; min-height: 34px; }
    }
    /* v88: at <=360px wide the icon-left core cards cannot fit a
       5-kana/ruby word beside the icon in a 3-per-row card (measured
       at 320x740: content 87px vs 79px card — the new verbs
       あそびます/うたいます overflowed sideways even at the 13px
       fit floor). Core cards switch to icon-on-top there: the card
       only needs max(icon, word) wide, and the stepper gives the
       one open group the panel's full height, so the taller card
       (52px) still fits: worst group = label + 3 lines = ~190px. */
    @media (max-width: 360px) and (orientation: portrait) {
      #mode-sentence #sentPicks:not(.d4-picks) .pick-opt {
        flex-direction: column; justify-content: center; gap: 1px;
        height: 50px; min-height: 50px; padding: 2px;
      }
      #mode-sentence #sentPicks:not(.d4-picks) .pick-opt .p-pic { font-size: 18px; }
      /* the open group must fit the picks box whole: at 320x740 the
         box is 185px and label + 3 card lines + gaps must stay
         under it (measured overflow was 7px at 52px cards) */
      #mode-sentence #sentPicks:not(.d4-picks) .pick-opts { gap: 6px; }
      #mode-sentence .pick-row .p-label { margin-bottom: 0; }
    }
    /* ================= v75: exit confirmation ================= */
    .exit-confirm {
      position: fixed; inset: 0; z-index: 90; display: flex;
      align-items: center; justify-content: center; padding: 18px;
      background: rgba(30, 45, 70, .45);
    }
    .exit-confirm[hidden] { display: none; }
    .exit-confirm-card {
      background: #fffdf5; border-radius: 26px; padding: 22px 22px 18px;
      max-width: min(92vw, 430px); width: 100%; min-width: 0; text-align: center;
      box-shadow: 0 14px 40px rgba(20, 35, 60, .35);
      border: 3px solid #ffd166;
    }
    .exit-confirm-q { font-size: clamp(20px, 5.6vw, 27px); font-weight: 800; color: #26436b; margin: 4px 0 16px; line-height: 1.35; }
    .exit-confirm-btns { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
    .exit-confirm-btns button {
      font: inherit; font-weight: 800; border: none; cursor: pointer;
      border-radius: 999px; padding: 14px 20px; font-size: clamp(16px, 4.4vw, 20px); min-height: 56px;
    }
    .exit-confirm-back { background: #ff8a3d; color: #fff; box-shadow: 0 4px 0 #d96f1e; }
    .exit-confirm-stay { background: #37b24d; color: #fff; box-shadow: 0 4px 0 #2b8a3e; }
    .exit-confirm-btns button:active { transform: translateY(2px); box-shadow: none; }

    /* ===== v92 — CHUẨN VÙNG ĐÁY (prop-entry-spec §5; bi QA 20:10) =====
       Trace reference implementation. The old block panel scrolled:
       at 414×736 it overflowed by 128px and the ✔ Kiểm tra button
       sat below the fold — a real-device dead end. The stack is now
       sized by a FORMULA, not by flow: its side is the smallest of
       (its v91 natural size) and (the panel height minus the fixed
       rows' budget, 222px: task + progress bar + actions + pick
       row + msg at its longest + panel padding). The panel never
       scrolls (overflow hidden), the canvas shrinks to whatever
       height the frame leaves, the action row is always on screen,
       and — because the size is a pure function of the frame — the
       win moment CANNOT move the board (a flex-column draft of
       this fix drifted 2px when the win message wrapped to two
       lines and the column redistributed; the formula has no such
       coupling). --panel-h is published by syncStage (init.js). */
    :root { --panel-h: 100dvh; }
    #screen-pet .mode-panel#mode-trace.active { overflow: hidden; }
    /* v95: one shared width for the whole trace stack (board,
       progress row, action grid, pick row) via --trace-w, and the
       board budget now deducts the FULL control stack (progress
       26 + actions 114 + pick 60 + msg 42 + margins ~10 = ~252) —
       at the old 222 the msg line spilled below the panel onto
       the bottom bar (backlog 47). */
    /* v99 (bi photo QA 2026-10-29 14:35 — "vẫn phải ưu tiên khung
       vẽ to chứ; nút to để làm gì nếu không vẽ được"): the board
       is the game, the buttons are servants. The control stack is
       compacted to the comfortable tap floor (ghost/pick 44px,
       Check 46px, slimmer progress row and msg — care.css), so
       the deduction drops 258 -> 234 (measured stack ~214 +
       safety; the first v99 pass at 228 left the msg 3px past
       the panel at 1024x1024 where the short-frame band is off),
       the width cap rises 330 -> 380 for tall/tablet frames, and
       wide-short windows also trim the stage (responsive.css) —
       the board roughly doubles at 1024x768/1280x800
       (81/113px -> ~184/216px) and grows at every other frame. */
    #screen-pet #mode-trace { --trace-w: min(calc(var(--stage-w) * 0.88), 380px, calc(var(--panel-h) - 234px)); }
    #screen-pet #mode-trace .trace-wrap {
      width: var(--trace-w);
      margin: 4px auto 0;
    }
    #screen-pet #mode-trace .trace-actions, #screen-pet #mode-trace .trace-pick-row {
      width: var(--trace-w); margin-left: auto; margin-right: auto;
    }
    #screen-pet #mode-trace .trace-stack { width: 100%; aspect-ratio: 1 / 1; }
    #screen-pet #mode-trace .trace-stack canvas { height: 100%; }
    #screen-pet #mode-trace .trace-bar-row { width: 100%; }
