:root {
  /* Wiseman Skills brand palette (see infra/brand.md) */
  --ws-charcoal: #404040;
  --ws-charcoal-soft: #6b7280;
  --ws-orange: #d05030;
  --ws-orange-soft: #ff855d;
  --ws-teal: #40c0c0;
  --ws-teal-soft: #b8e6e6;
  --ws-bg-soft: #f4f6f8;
  --ws-bg-tint: #edf8fd;
  --ws-border: #e6e6e6;
  --ws-success: #71b351;
  --ws-error: #ef4444;

  /* KIDS exam-level colours (interim, canonical in infra/brand.md) */
  --step1: #81E2C9; /* turquoise */
  --step2: #F17E52; /* orange */
  --step3: #9B60A3; /* violet */
  /* game-type accent = leaf/grass green */
  --type-green: #74a83c;
  /* neutral light-green used for button hover (never orange) */
  --hover-green: #d6ebcf;

  /* semantic aliases used across the game */
  --accent: var(--ws-orange);
  --accent-soft: var(--ws-orange-soft);
  --accent2: var(--ws-teal);
  --ink: var(--ws-charcoal);
  --ink-soft: var(--ws-charcoal-soft);
  --border: var(--ws-border);
  --surface: #ffffff;
  --ok: var(--ws-success);
  --bad: var(--ws-error);
  --warn: #b8860b;
}

* { box-sizing: border-box; }

/* always reserve the scrollbar gutter so the centred panel doesn't jump sideways
   when the picker grows a scrollbar (expanding a topic / selecting words) */
html { scrollbar-gutter: stable; -webkit-tap-highlight-color: transparent; }

body {
  margin: 0;
  font-family: Inter, system-ui, "Segoe UI", sans-serif;
  background: var(--ws-bg-soft);
  color: var(--ink);
  /* these are tap-to-play kids' games — kill accidental text/emoji selection everywhere */
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
/* …but keep real inputs selectable (e.g. the username field) */
input, textarea { -webkit-user-select: text; -moz-user-select: text; user-select: text; }

.stage {
  max-width: 720px;
  margin: 0 auto;
  padding: 24px 16px 64px;
}

h1 { font-size: 1.2rem; }
h2 { font-size: 1rem; margin-bottom: 8px; }

.panel {
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 16px;
  margin-bottom: 16px;
}

.status { font-weight: 600; }
.status.ok { color: var(--ok); }
.status.bad { color: var(--bad); }
.status.warn { color: var(--warn); }

button {
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 8px;
  padding: 10px 16px;
  font-size: 0.95rem;
  cursor: pointer;
  transition: background 0.12s ease, transform 0.06s ease;
}
button:hover { background: var(--hover-green); color: var(--ink); }
button:active { transform: translateY(1px); }
button:disabled { opacity: 0.5; cursor: not-allowed; }

/* start button turns green ("go!") when pressed */
#start-btn { padding: 12px 26px; font-size: 1.05rem; font-weight: 600; border-radius: 999px; }
#start-btn:hover { background: var(--hover-green); color: var(--ink); }
#start-btn:active { background: var(--type-green); }

.hint {
  font-size: 0.8rem;
  color: var(--ink-soft);
  margin-top: 8px;
}

.max-wrap {
  position: relative;
  width: 200px;
  margin: 0 auto 16px;
}

.max-puppet { width: 100%; height: auto; display: block; }

.max-part {
  transform-box: fill-box;
  transform-origin: top center;
  transition: transform 0.25s ease;
}

/* greeting bubble — a cute white cloud with trailing dots, same family as Max's
   thought bubble (the old bordered rectangle read as a dated plate) */
.speech-bubble {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-bottom: 2px;
  width: 232px;
  height: 139px;
  pointer-events: none;
}
.speech-bubble.hidden { display: none; }
.sb-cloud {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.sb-cloud .sb-puffs {
  fill: #fff;
  filter: drop-shadow(0 4px 7px rgba(40, 40, 40, 0.28));
}
.sb-text {
  position: absolute;
  left: 10%;
  right: 10%;
  top: 4%;                /* span the white body (the trailing dots sit below it)… */
  bottom: 12%;
  display: flex;          /* …and centre the phrase within it at any length */
  align-items: center;
  justify-content: center;
  text-align: center;
  font-family: "Chewy", "Inter", system-ui, sans-serif; /* playful rounded display */
  font-size: 1.15rem;
  font-weight: 400;       /* Chewy is a single weight */
  line-height: 1.1;
  color: var(--ink);
}

/* Jump animation: bounce whole puppet, bend legs/arms up */
.max-puppet.is-jumping #max-body,
.max-puppet.is-jumping #max-head,
.max-puppet.is-jumping #max-tail {
  animation: jump-bounce 0.6s ease-in-out;
}
.max-puppet.is-jumping #max-leg-left,
.max-puppet.is-jumping #max-leg-right {
  animation: jump-legs 0.6s ease-in-out;
}
.max-puppet.is-jumping #max-arm-left {
  animation: jump-arm-left 0.6s ease-in-out;
}
.max-puppet.is-jumping #max-arm-right {
  animation: jump-arm-right 0.6s ease-in-out;
}

@keyframes jump-bounce {
  0%   { transform: translateY(0); }
  35%  { transform: translateY(-38px); }
  60%  { transform: translateY(-38px); }
  100% { transform: translateY(0); }
}
@keyframes jump-legs {
  0%   { transform: translateY(0) scaleY(1); }
  35%  { transform: translateY(-38px) scaleY(0.7); }
  60%  { transform: translateY(-38px) scaleY(0.7); }
  100% { transform: translateY(0) scaleY(1); }
}
@keyframes jump-arm-left {
  0%   { transform: translateY(0) rotate(0deg); }
  35%  { transform: translateY(-38px) rotate(-40deg); }
  60%  { transform: translateY(-38px) rotate(-40deg); }
  100% { transform: translateY(0) rotate(0deg); }
}
@keyframes jump-arm-right {
  0%   { transform: translateY(0) rotate(0deg); }
  35%  { transform: translateY(-38px) rotate(40deg); }
  60%  { transform: translateY(-38px) rotate(40deg); }
  100% { transform: translateY(0) rotate(0deg); }
}

#findings { font-size: 0.9rem; line-height: 1.5; }

/* --- Learning mode (Phase 1) ------------------------------------------- */

.learning-stage { max-width: 480px; text-align: center; }

.lm-header { margin-bottom: 16px; }

.lm-controls-row {
  display: flex;
  justify-content: center;
  gap: 16px;
  flex-wrap: wrap;
  margin-top: 12px;
}

.lm-levels, .lm-categories { display: flex; gap: 6px; }

.lm-pill {
  background: #fff;
  color: var(--ink);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 6px 14px;
  font-size: 0.85rem;
}
.lm-pill.is-active {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}

/* level pills carry the KIDS exam-level colours (STEP1 turquoise / STEP2
   orange / STEP3 violet); game-type pills use a leaf-green accent */
#level-selector .lm-pill { color: var(--ink); border-width: 2px; }
#level-selector .lm-pill[data-level="STEP1"] { border-color: var(--step1); }
#level-selector .lm-pill[data-level="STEP2"] { border-color: var(--step2); }
#level-selector .lm-pill[data-level="STEP3"] { border-color: var(--step3); }
#level-selector .lm-pill[data-level="STEP1"].is-active { background: var(--step1); color: var(--ink); }
#level-selector .lm-pill[data-level="STEP2"].is-active { background: var(--step2); color: #fff; }
#level-selector .lm-pill[data-level="STEP3"].is-active { background: var(--step3); color: #fff; }
#category-selector .lm-pill { color: #4a7020; border-color: var(--type-green); border-width: 2px; }
#category-selector .lm-pill.is-active { background: var(--type-green); color: #fff; }

.lm-card {
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 24px 16px;
}

/* verb-object prop (kick a BALL) — sits on the grass in front of Max's feet */
.noun-icon {
  position: absolute;
  left: 52%;
  bottom: -5%;
  width: 48%;
  height: auto;
  transform: translateX(-50%);
  filter: drop-shadow(0 3px 5px rgba(0, 0, 0, 0.22));
  pointer-events: none;
}
.noun-icon.hidden { display: none; }

/* nouns game: Max's thought cloud — a full-field overlay whose SVG coords are
   the 452px field grid, so the puffs/dots sit exactly where authored. It's
   placed before Max in the DOM (no z-index) so the tail dots tuck behind his
   head, reading as a thought rising from his ear. */
.thought-bubble {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.thought-bubble.hidden { display: none; }
.tb-cloud {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.tb-cloud .tb-puffs {
  fill: #fff;
  filter: drop-shadow(0 4px 7px rgba(40, 40, 40, 0.3));
}
/* the imagined object, centred on the cloud's puff mass */
.tb-img {
  position: absolute;
  left: 50%;
  top: 22%;
  width: 30%;
  height: 26%;
  transform: translate(-50%, -50%);
  object-fit: contain;
}
/* "Max imagines it": the cloud pops out from near his ear (lower-right origin) */
.thought-bubble.think {
  animation: tb-think 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
  transform-origin: 59% 46%;
}
@keyframes tb-think {
  0%   { transform: scale(0.15); opacity: 0; }
  55%  { opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
/* correct-answer bob — keeps the centring translate (plain wave-bob would drop
   translate(-50%,-50%) and make the object jump down-right out of the cloud) */
.thought-bubble.is-correct .tb-img {
  animation: tb-bob 0.55s ease-in-out 2;
}
@keyframes tb-bob {
  0%, 100% { transform: translate(-50%, -50%); }
  50%      { transform: translate(-50%, -62%) scale(1.06); }
}

.lm-word {
  font-size: 1.6rem;
  font-weight: 700;
  margin: 8px 0 4px;
}

.lm-progress {
  font-size: 0.85rem;
  color: var(--ink-soft);
  margin: 0 0 16px;
}

.lm-nav {
  display: flex;
  justify-content: center;
  gap: 12px;
  margin-bottom: 12px;
}

.lm-autoplay {
  font-size: 0.85rem;
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

/* Generic placeholder animation groups, driven by data.animGroup.
   Real per-verb art/rigs are a later phase — see README "Дальше". */

.max-puppet.anim-bounce #max-body,
.max-puppet.anim-bounce #max-head,
.max-puppet.anim-bounce #max-tail {
  animation: jump-bounce 0.7s ease-in-out;
}
.max-puppet.anim-bounce #max-leg-left,
.max-puppet.anim-bounce #max-leg-right {
  animation: jump-legs 0.7s ease-in-out;
}
.max-puppet.anim-bounce #max-arm-left {
  animation: jump-arm-left 0.7s ease-in-out;
}
.max-puppet.anim-bounce #max-arm-right {
  animation: jump-arm-right 0.7s ease-in-out;
}

.max-puppet.anim-wave #max-arm-right {
  animation: wave-arm 1.3s ease-in-out 2;
}
.max-puppet.anim-wave #max-body,
.max-puppet.anim-wave #max-head {
  animation: wave-bob 1.3s ease-in-out 2;
}

.max-puppet.anim-sway #max-body,
.max-puppet.anim-sway #max-head {
  animation: sway 1.1s ease-in-out 2;
}
.max-puppet.anim-sway #max-tail {
  animation: sway-tail 1.1s ease-in-out 2;
}

/* Right arm raised beside the head and waggled — negative angles because
   the arm's rest vector points down and CSS +rotate is clockwise. Used both
   for the "wave/sing/clap" verb group and for Max's greeting. */
@keyframes wave-arm {
  0%   { transform: rotate(0deg); }
  15%  { transform: rotate(-150deg); }
  30%  { transform: rotate(-125deg); }
  45%  { transform: rotate(-155deg); }
  60%  { transform: rotate(-125deg); }
  75%  { transform: rotate(-150deg); }
  90%  { transform: rotate(-135deg); }
  100% { transform: rotate(0deg); }
}
@keyframes wave-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-6px); }
}
/* Rocking side-to-side for the "sit/eat/sleep" verb group — clearly not a
   jump: no vertical hop, a wider tilt instead. */
@keyframes sway {
  0%, 100% { transform: rotate(0deg); }
  25%      { transform: rotate(-8deg); }
  75%      { transform: rotate(8deg); }
}
@keyframes sway-tail {
  0%, 100% { transform: rotate(0deg); }
  25%      { transform: rotate(10deg); }
  75%      { transform: rotate(-10deg); }
}

/* --- Layered PNG rig: the real illustrated Max (game.html) --------------
   Parts sit on a 1200x1260 virtual canvas (left/top from the export
   composition); the whole rig is scaled down to fit. Whole-body motions
   (jump/sway) animate .max-stage at screen scale; per-limb motions (wave)
   animate the .mp-* parts inside the scaled rig. Coords match the shipped
   PNGs in assets/max/ — regenerate both together if art is re-exported. */

/* Max stands on the grass to the right of the tree, per the artist's PS
   placement (X=22.66 Y=19.35, 14.14×19.08cm on the 41.66cm scene). Box = his
   hero bbox (7.07×9.54cm → aspect 707/954). */
.max-wrap--rig {
  position: absolute;
  left: 54.39%;
  top: 46.45%;
  width: 33.94%;
  height: auto;
  aspect-ratio: 707 / 954;
  margin: 0;
}

/* soft ground shadow — stays put while Max hops (it's outside .max-stage) */
.max-shadow {
  position: absolute;
  left: 38%;
  bottom: 0.5%;
  width: 60%;
  height: 4%;
  transform: translateX(-50%);
  background: radial-gradient(ellipse at center,
              rgba(60,42,30,0.26) 0%, rgba(60,42,30,0.13) 45%, rgba(60,42,30,0) 72%);
  pointer-events: none;
}

.max-stage {
  position: absolute;
  inset: 0;
  transform-origin: 45% 98%;   /* hop/rock around the feet */
}

/* Max's canvas = his PS hero box (707×954). Parts placed in % of it, computed
   from the artist's per-layer cm coordinates. overflow visible so the raised
   wave arm can reach left of the box. */
.max-rig {
  position: absolute;
  inset: 0;
  overflow: visible;
}

.mp {
  position: absolute;
  user-select: none;
  -webkit-user-drag: none;
}

/* paint order (back→front) set by the DOM: tail, arm-L, legs, body, arm-R,
   head, wave, thumbs. screen-left leg = white (Right_leg). */
.mp-tail       { left: 60.82%; top: 15.30%; width: 39.18%; transform-origin: 50% 88%; }
.mp-arm-L      { left: -1.91%; top: 36.73%; width: 44.77%; }
.mp-leg-left   { left: 1.47%; top: 61.19%; width: 56.49%; }
.mp-leg-right  { left: 37.34%; top: 67.52%; width: 41.42%; }
.mp-body       { left: 13.30%; top: 37.11%; width: 54.03%; }
.mp-arm-R      { left: 38.59%; top: 37.10%; width: 44.71%; }
.mp-head       { left:  1.35%; top: -0.71%; width: 68.59%; transform-origin: 50% 92%; }
.mp-arm-wave   { left: -28.29%; top: 16.32%; width: 56.82%; transform-origin: 82% 84%; display: none; }
.mp-arm-thumbs { left: 33.24%; top: 39.94%; width: 40.59%; display: none; }
/* eat/drink hold: two gripping hands (right = screen-left, left = screen-right) */
.mp-arm-hold-R { left: -1.76%; top: 40.16%; width: 39.70%; display: none; }
.mp-arm-hold-L { left: 34.23%; top: 39.94%; width: 39.60%; display: none; }
.mp-hold-obj   { display: none; }   /* left/top/width set inline per object */

/* arm states: greeting → wave (hide the left hanging arm); correct noun →
   thumbs-up (hide the right bent arm); eat/drink → both hands hold the
   object (hide both resting arms). */
.max-stage.anim-wave   .mp-arm-wave   { display: block; }
.max-stage.anim-wave   .mp-arm-L      { display: none; }
.max-stage.anim-thumbs .mp-arm-thumbs { display: block; }
.max-stage.anim-thumbs .mp-arm-R      { display: none; }
.max-stage.anim-hold .mp-arm-hold-R,
.max-stage.anim-hold .mp-arm-hold-L,
.max-stage.anim-hold .mp-hold-obj { display: block; }
.max-stage.anim-hold .mp-arm-L,
.max-stage.anim-hold .mp-arm-R { display: none; }
/* wear: a hat appears on the head (arms stay resting) */
.mp-wear-obj { display: none; }   /* left/top/width set inline per object */
.max-stage.anim-wear .mp-wear-obj { display: block; }

/* pick up — 2 phases (coords are the artist's, scene-cm → rig-%, see HANDOFF.md §3).
   BEND: Max leans down and reaches for the ball on the ground — body swaps to body-2,
   head tilts -30°, tail +20°, both arms reach, the ball sits low. HELD: he stands and
   holds it, one paw (left_hand_hold) wrapping the ball at his belly. pickup_ball.png is
   the ball cropped tight, so its width maps straight to the artist's 6.5cm. */
.mp-pickup-body { left: 5.73%; top: 36.58%; width: 67%; display: none; }
.mp-pickup-ball { display: none; }   /* per-phase position below */
.mp-pickup-arm  { left: 37.34%; top: 40.57%; width: 39.60%; display: none; }

/* HELD — stand + wrap */
.max-stage.anim-pickup .mp-pickup-ball { display: block; left: 14.36%; top: 53.46%; width: 45.97%; }
.max-stage.anim-pickup .mp-pickup-arm  { display: block; }
.max-stage.anim-pickup .mp-arm-R { display: none; }

/* BEND — reach for the ground ball. Reposition the standing head/tail/arms (same
   assets, per the artist) and swap in the bent body; z-index fixes the paint order. */
.max-stage.anim-pickup-bend .mp-pickup-body { display: block; z-index: 4; }
.max-stage.anim-pickup-bend .mp-pickup-ball { display: block; left: 3.11%; top: 69.60%; width: 45.97%; z-index: 5; }
.max-stage.anim-pickup-bend .mp-body { display: none; }
.max-stage.anim-pickup-bend .mp-head { left: -28.29%; top: 4.66%; transform: rotate(-30deg); z-index: 7; }
.max-stage.anim-pickup-bend .mp-tail { left: 60.61%; top: 19.44%; width: 60.47%; transform: rotate(20deg); z-index: 1; }
.max-stage.anim-pickup-bend .mp-arm-R { left: 28.22%; top: 37.63%; z-index: 6; }
.max-stage.anim-pickup-bend .mp-arm-L { left: -2.76%; top: 48.64%; z-index: 2; }
.max-stage.anim-pickup-bend .mp-leg-left,
.max-stage.anim-pickup-bend .mp-leg-right { z-index: 3; }

/* ride: Max sits on the bike (in front of the legs, behind the body) with one
   paw on the handlebar; both resting arms hide. The bike rolls in from the right. */
.mp-bike      { left: -26.31%; top: 46.23%; width: 141.44%; display: none; }
.mp-bike-hand { left: 13.86%;  top: 35.43%; width: 53.18%;  display: none; }
.max-stage.anim-ride .mp-bike,
.max-stage.anim-ride .mp-bike-hand { display: block; }
.max-stage.anim-ride .mp-arm-L,
.max-stage.anim-ride .mp-arm-R { display: none; }
.max-stage.anim-ride .mp-bike { animation: bike-in 0.6s ease-out; }
@keyframes bike-in {
  0%   { transform: translateX(75%); opacity: 0; }
  55%  { opacity: 1; }
  100% { transform: translateX(0); opacity: 1; }
}

/* kick: the ball sits by Max's (screen-left) foot, then flies up-left with a
   spin while Max winds up and kicks. */
.mp-kick-ball { left: -2.4%; top: 86.58%; width: 31.12%; display: none; }
.max-stage.anim-kick .mp-kick-ball {
  display: block;
  animation: ball-kick 0.75s ease-in both;
}
.max-stage.anim-kick { animation: kick-lean 0.7s ease-in-out; }
@keyframes ball-kick {
  0%, 18% { transform: translate(0, 0) rotate(0deg); opacity: 1; }
  80%     { opacity: 1; }
  100%    { transform: translate(-165%, -95%) rotate(-720deg); opacity: 0; }
}
@keyframes kick-lean {
  0%, 100% { transform: rotate(0deg); }
  28%      { transform: rotate(4deg); }
  52%      { transform: rotate(-5deg); }
}

.max-stage.idle .mp-head { animation: idle-head 6s ease-in-out infinite; }
.max-stage.idle .mp-tail { animation: idle-tail 8s ease-in-out infinite; }
@keyframes idle-head {
  0%, 62%, 100% { transform: rotate(0deg); }
  72%           { transform: rotate(4deg); }
  86%           { transform: rotate(-3deg); }
}
@keyframes idle-tail {
  0%, 52%, 100% { transform: rotate(0deg); }
  66%           { transform: rotate(7deg); }
  82%           { transform: rotate(-4deg); }
}

/* --- Costume change: star-burst transition + baseball-kit overlay ----------
   Laid in but dormant — nothing shows until playOutfitChange() runs. */
#ball-gear { position: absolute; inset: 0; z-index: 6; pointer-events: none; }
#ball-gear.hidden { display: none; }
#ball-gear .cap { position: absolute; left: 4%;  top: -8%; font-size: 2.5rem; transform: rotate(-10deg); }
#ball-gear .bat { position: absolute; left: 60%; top: 30%; font-size: 2.7rem; transform: rotate(38deg); }

.fx { position: absolute; inset: 0; z-index: 12; pointer-events: none; display: none; }
.fx.playing { display: block; }
.fx-stars .fx-flash {
  position: absolute; left: 50%; top: 44%; width: 130%; aspect-ratio: 1;
  transform: translate(-50%, -50%) scale(0); opacity: 0; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,255,255,0.95), rgba(255,245,200,0.5) 45%, rgba(255,255,255,0) 68%);
}
.fx-stars.playing .fx-flash { animation: fx-flash 720ms ease-out forwards; }
@keyframes fx-flash {
  0%   { transform: translate(-50%, -50%) scale(0);    opacity: 0; }
  28%  { transform: translate(-50%, -50%) scale(1);    opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1.25); opacity: 0; }
}
.fx-stars .star {
  position: absolute; left: 50%; top: 44%; font-size: 1.8rem;
  transform: translate(-50%, -50%) scale(0); opacity: 0;
}
.fx-stars.playing .star { animation: fx-star 840ms ease-out forwards; }
@keyframes fx-star {
  0%   { transform: translate(-50%, -50%) scale(0) rotate(0deg); opacity: 1; }
  30%  { transform: translate(calc(-50% + var(--dx) * 0.5), calc(-50% + var(--dy) * 0.5)) scale(1.25) rotate(90deg);  opacity: 1; }
  100% { transform: translate(calc(-50% + var(--dx)),       calc(-50% + var(--dy)))       scale(0.5)  rotate(210deg); opacity: 0; }
}

/* jump */
.max-stage.anim-bounce { animation: rig-bounce 0.7s ease-in-out; }
.max-stage.anim-bounce .mp-leg-left,
.max-stage.anim-bounce .mp-leg-right { animation: rig-legs 0.7s ease-in-out; }
@keyframes rig-bounce {
  0%   { transform: translateY(0); }
  35%  { transform: translateY(-12%); }
  60%  { transform: translateY(-12%); }
  100% { transform: translateY(0); }
}
@keyframes rig-legs {
  0%, 100% { transform: scaleY(1); }
  35%, 60% { transform: scaleY(0.86); }
}

/* wave — greeting + wave/sing/clap group: just waggle the raised arm (no
   whole-body bob — that read as an unnatural little jump). */
.max-stage.anim-wave .mp-arm-wave { animation: rig-wave 0.9s ease-in-out 2; }
@keyframes rig-wave {
  0%, 100% { transform: rotate(0deg); }
  25%      { transform: rotate(15deg); }
  75%      { transform: rotate(-9deg); }
}

/* sway — sit/eat/sleep group: gentle whole-body rock */
.max-stage.anim-sway { animation: rig-sway 1.1s ease-in-out 2; }
@keyframes rig-sway {
  0%, 100% { transform: rotate(0deg); }
  25%      { transform: rotate(-5deg); }
  75%      { transform: rotate(5deg); }
}

/* "can't" — Max shakes his head "no" (verbs he can't do, e.g. fly). Keeps the
   base 5deg head tilt while wobbling side to side. */
.max-stage.anim-cant .mp-head { animation: head-shake 0.72s ease-in-out 2; }
@keyframes head-shake {
  0%, 100% { transform: rotate(0deg) translateX(0); }
  20%      { transform: rotate(0deg) translateX(-5px); }
  40%      { transform: rotate(0deg) translateX(5px); }
  60%      { transform: rotate(0deg) translateX(-4px); }
  80%      { transform: rotate(0deg) translateX(3px); }
}

/* --- Game mode (Phase 2) ------------------------------------------------ */

.hidden { display: none; }

.game-stage { max-width: 720px; text-align: center; padding-top: 16px; position: relative; }

.mute-btn {
  position: absolute;
  top: 8px;
  right: 6px;
  width: 40px;
  height: 40px;
  padding: 0;
  border-radius: 999px;
  background: #fff;
  color: inherit;
  border: 1px solid var(--border);
  font-size: 1.15rem;
  line-height: 1;
  box-shadow: 0 1px 5px rgba(64, 64, 64, 0.12);
  z-index: 5;
}
/* Muted state must be READABLE, not just dimmer: a faded speaker looks the same as a normal one to a
   child glancing at it, so the button reports its state with a red slash and a red tint as well.
   (The icon itself already switches 🔊/🔇 in scene.js.) */
.mute-btn.is-muted {
  opacity: 1;
  background: #fdecea;
  border-color: #e3a99f;
  box-shadow: inset 0 0 0 1px rgba(200, 80, 60, 0.18);
}
.mute-btn:hover { background: #fff; }   /* utility icon — keep it white, not tinted */
.mute-btn.is-muted:hover { background: #fdecea; }

/* "home" button — back to the Vocab Trainer menu; mirrors the mute button */
.home-btn {
  position: absolute;
  top: 8px;
  left: 6px;
  height: 40px;
  padding: 0 14px;
  border-radius: 999px;
  border: 1px solid rgba(0, 0, 0, 0.12);
  background: #fff;
  color: var(--ink);
  font-size: 0.9rem;
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  text-decoration: none;
  cursor: pointer;
  z-index: 5;
}
.home-btn:active { transform: scale(0.96); }

/* selected course level shown in every game; hover shows how to change it */
.level-badge {
  position: absolute;
  top: 8px;
  right: 52px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  padding: 0 16px;
  border-radius: 9px;                 /* rectangular plaque, not a pill */
  border: none;
  background: var(--ink-soft);        /* fallback; the level colour wins below */
  color: #fff;
  font-size: 0.82rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  box-shadow: 0 2px 6px rgba(64, 64, 64, 0.2);
  cursor: help;
  z-index: 5;
}
/* the plaque wears its level colour (set as data-level in offline.js) */
.level-badge[data-level="STEP1"] { background: var(--step1); color: var(--ink); }
.level-badge[data-level="STEP2"] { background: var(--step2); color: #fff; }
.level-badge[data-level="STEP3"] { background: var(--step3); color: #fff; }

/* --- Redesigned window layout (Phase 5) --------------------------------- */

.app-header { margin-bottom: 12px; }
.app-title {
  font-size: 1.7rem;
  margin: 0;
  color: var(--accent);
  letter-spacing: 0.5px;
}
/* game pages have a top button row (Меню / level / mute); on a phone the centred
   title would slide under them, so drop it below the row and shrink it a touch */
@media (max-width: 560px) {
  .app-header--game { padding-top: 42px; }
  .app-header--game .app-title { font-size: 1.4rem; }
}
/* game rules — under the "Начать игру" button, outside the field (menu only) */
.rules-box {
  font-size: 0.85rem;
  color: var(--ink-soft);
  max-width: 460px;
  margin: 14px auto 0;
  line-height: 1.45;
  text-align: center;
}
.rules-label { color: var(--ink); }

/* the window + controls share one cohesive panel (no separate floating plates) */
.game-panel {
  max-width: 720px;
  margin: 0 auto;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 22px;
  padding: 14px 14px 18px;
  box-shadow: 0 8px 26px rgba(64, 64, 64, 0.09);
}

/* centre the game window; soft shadow lifts it off the panel */
.stage-row { display: flex; justify-content: center; }
.stage-row .game-field { box-shadow: 0 3px 14px rgba(64, 64, 64, 0.16); }

/* On a phone the scene is small enough already, so the playing field gets the WHOLE screen width:
   the row breaks out of the stage's 16px and the panel's 14px padding (30px a side) and the square
   grows to the full viewport. Text and controls around it keep their padding — only the field
   escapes. Everything inside the scene is positioned in %, so it scales up with no other change. */
   Scoped to `.ws-play` — the GAME screens. The MENU keeps its scene inside the white card: there the
   frame is the point (Max is presented, not played), and an unscoped rule made the scene burst out
   of a card that stayed narrow — the exact mismatch this rule exists to avoid. */
@media (max-width: 560px) {
  body.ws-play .stage-row { margin-left: -30px; margin-right: -30px; }
  body.ws-play .stage-row .game-field { border-radius: 0; box-shadow: none; }
}

/* ============================================================================================
   ONE FITTING SYSTEM FOR EVERY GAME, AT EVERY SIZE
   The game used to be laid out twice: a 720 px column with a 452 px scene on a desktop, and on a
   phone the same scene sized as `100dvh - <a hand-measured reserve>`. Both were wrong in their own
   way. On a big screen the picture stayed small with dead space under it — for a child who is meant
   to LOOK at the picture, that is the whole product shrinking for no reason. And the reserves had to
   be re-measured for every game, every template and every screen band; each time one was missed the
   bottom bar slid off screen (it happened four times: crossword, domino, riddles, «Составь слово» on
   STEP2).
   So the reserve is gone. The page is one column exactly one viewport tall; the scene's row is the
   flexible part of it and declares itself a size container; the scene then takes the smaller of the
   width and the height that row actually ended up with. Nothing to measure, nothing to keep in sync,
   and it cannot overflow — if the chrome around it grows, the scene simply gets the remainder.
   ============================================================================================ */
body.ws-play .stage {
  max-width: min(1120px, 96vw);
  height: 100dvh;                       /* dvh already accounts for the phone browser's own bars */
  padding: 14px 16px 18px;
  display: flex; flex-direction: column;
}
/* Страховка от «уехавшего вбок экрана»: игровая страница не прокручивается горизонтально НИКОГДА,
   даже если что-то однажды снова вылезет за край. `clip`, а не `hidden`: hidden сделал бы body
   контейнером прокрутки и сломал бы `position: sticky` у нижней панели. */
body.ws-play { overflow-x: clip; }
body.ws-play .game-panel {
  flex: 1 1 auto; min-height: 0; width: 100%; margin-inline: auto;
  display: flex; flex-direction: column;
  /* Keep the card snug around the scene on a wide, short screen — otherwise a 505 px picture floats
     in a 1088 px card and the game reads as empty. --ws-card is roughly the vertical chrome of the
     game (everything that is not the scene); being a little off only makes the white frame a little
     wider or tighter, it can never break the fit. */
  max-width: min(100%, calc(100dvh - var(--ws-card, 200px)));
}
body.ws-play .stage-row {
  flex: 1 1 auto; min-height: 0;
  container-type: size;                 /* 100cqw / 100cqh below = the space this row really got */
  align-items: flex-start;
}
/* :not(.catch-field) — «Лови слова» is the one scene that is deliberately NOT square on a phone: the
   words have to fall the whole height, so it takes the row outright. It sizes itself in catch.html
   from the same container units; excluded here so this rule cannot win on specificity and flatten it
   to zero height, which is exactly what happened the first time. */
/* «Лови слова» — единственная сцена, которая НАМЕРЕННО не квадратная: слова падают во всю высоту.
   Оба правила ниже нужны на ЛЮБОМ размере, а не только на телефоне. Когда они лежали внутри
   телефонного блока, на десктопе у поля оставался отступ сверху 22 px, поле выезжало за свой ряд и
   накрывало таймер — особенно заметно со свёрнутой боковой панелью. */
body.ws-play .stage-row:has(.catch-field) { max-height: none; }
body.ws-play .catch-field { margin-top: 0; }   /* в catch.html своё правило — нужна большая специфичность */

body.ws-play .stage-row .game-field:not(.catch-field) {
  width: min(100cqw, 100cqh); max-width: none; height: auto;
}

/* The sound toggle now lives in the top right corner OF THE PICTURE (moved there by ui.js): outside
   it, in the page's corner, children did not find it — three complaints in a row. Inside the scene it
   is where the eye already is, and a thumb reaches it without regripping the phone. */
.game-field > .mute-btn,
.dom-stage > .mute-btn {
  position: absolute; top: 10px; right: 10px; left: auto; bottom: auto; z-index: 12;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.22);
}

/* The level is part of the game's NAME — «Vocab Trainer: STEP 1» — not a green chip in the corner
   that looks pressable and is not (owner, twice). Inside the title it is plain text. */
.app-title > .level-badge {
  position: static; height: auto; padding: 0; border-radius: 0;
  background: none; font: inherit; box-shadow: none; opacity: 1;
}
/* the level carries its own exam colour (infra/brand.md), written closed up: «STEP1» */
.app-title > .level-badge[data-level="STEP1"] { background: none; color: var(--ws-teal); }   /* the turquoise #81E2C9 is a FILL colour — as text on white it is unreadable, so the darker brand teal of the same family carries it */
.app-title > .level-badge[data-level="STEP2"] { background: none; color: var(--step2); }
.app-title > .level-badge[data-level="STEP3"] { background: none; color: var(--step3); }

/* «Сообщить о проблеме» — кнопка режима тестирования (ui.js). Намеренно маленькая и в углу: она для
   взрослого, который проверяет игру, а не для ребёнка, который играет. */
/* z-index ниже затемнения правил (80): жучок светился поверх затемнённого экрана и перетягивал на
   себя внимание ровно в тот момент, когда ребёнок должен читать правила. */
.ws-bug {
  position: fixed; left: 10px; bottom: 10px; z-index: 70;
  width: 42px; height: 42px; padding: 0; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--border); background: rgba(255, 255, 255, 0.92); font-size: 1.15rem; line-height: 1;
  box-shadow: 0 2px 10px rgba(80, 60, 40, 0.18);
}
.ws-bug:hover { background: #fff; }
.ws-bug > .lbl { display: none; }                  /* в углу — только жучок, подпись там некуда деть */
/* В боковой панели жучок становится обычной кнопкой с подписью и встаёт над рядом действий: у левого
   края экрана владелец его попросту не находила. */
.ws-bug--rail {
  /* `margin-top: auto` прижимает кнопку к низу колонки — вплотную над рядом действий, куда и
     показала владелец. Без этого она повисала сразу под заданием, посреди пустоты.
     У самого ряда действий тоже стоит `margin-top: auto` (он так прижат к низу рейки), и два
     автоматических отступа ДЕЛЯТ свободное место пополам — кнопка зависала посередине. Поэтому,
     когда жучок в колонке, отступ ряда обнуляем: см. правило ниже. */
  position: static; z-index: auto; width: auto; height: auto; min-height: 44px; margin: auto 0 0;
  display: flex; align-items: center; justify-content: center; gap: 8px; padding: 8px 14px;
  font-family: inherit; font-weight: 700; font-size: 0.88rem; color: var(--ink-soft);
  box-shadow: none; background: var(--surface);
}
.ws-bug--rail > .lbl { display: inline; }
body.ws-railed .ws-rail__body:has(.ws-bug--rail) .ws-bar { margin-top: 0; }
/* На игровом экране 🐞 накрывал левую кнопку нижнего ряда (замерено: кнопка 10–52 px, «Подсказка»
   начиналась с 20-го) — палец попадал в отчёт о баге вместо подсказки. Поднимаем над панелью;
   высоту панели ui.js кладёт в --ws-bar-h (0, пока панель спрятана экраном настройки). */
body.ws-play .ws-bug { bottom: calc(var(--ws-bar-h, 68px) + 12px + env(safe-area-inset-bottom, 0px)); }
/* В МЕНЮ жучок живёт наверху слева. Внизу его не видно на телефоне: у Safari на iPhone адресная
   строка снизу и перекрывает ровно этот угол, а `position: fixed` считается от разметочного окна,
   а не от видимой части. Наверху свободно всегда — справа там только выбор языка. */
body[data-ui="menu"] .ws-bug { top: 14px; left: 16px; bottom: auto; }
@media (max-width: 420px) { body[data-ui="menu"] .ws-bug { top: 10px; left: 10px; } }
.ws-bug__card { max-width: 420px; text-align: left; }
.ws-bug__text {
  width: 100%; margin: 0 0 10px; padding: 10px 12px; border-radius: 12px;
  border: 1px solid var(--border); font: inherit; font-size: 1rem; color: var(--ink); resize: vertical;
}
.ws-bug__info { margin: 0 0 14px; font-size: 0.78rem; color: var(--ink-soft); }
.ws-bug__row { display: flex; gap: 10px; }
.ws-bug__row .ws-howto__btn { flex: 1 1 0; margin: 0; padding: 12px 16px; font-size: 0.98rem; }
.ws-bug__cancel { background: #fff; color: var(--ink); border: 1px solid var(--border); box-shadow: none; }

/* AI-voice disclosure (menu only). Quiet by design — it is a legal notice for the adult, not a
   message to the child — but never hidden: the provider's terms require the listener to be told the
   voice is synthetic. */
.ai-voice-note {
  margin: 14px auto 0; max-width: 640px; text-align: center;
  font-size: 0.78rem; line-height: 1.4; color: var(--ink-soft); opacity: 0.75;
}

/* Directly under the picture: the countdown and anything else the child must SEE while playing
   (`data-under-scene`, moved here by ui.js). It stays in the main column on every screen size —
   in the rail it was out of sight during play, and gone entirely once the rail was folded. */
.ws-under { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin: 10px 0 2px; }
.ws-under:empty { display: none; }

/* THE CONTROL RAIL (≥900 px, built by ui.js).
   Everything that is not the picture — counters, the game's own controls, the action buttons — sits
   in one column on the right and folds away behind an arrow, so during play the child can have the
   whole window for the game. Nothing here needs to know how tall anything is: fold the rail and the
   scene's row simply gets wider, so the picture grows by itself. */
.ws-rail { display: none; }
@media (min-width: 900px) {
  body.ws-railed .stage {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-rows: auto minmax(0, 1fr);
    column-gap: 16px;
  }
  body.ws-railed .stage > .app-header { grid-column: 1 / -1; grid-row: 1; }
  body.ws-railed .stage > .game-panel {
    grid-column: 1; grid-row: 2;
    max-width: none;      /* --ws-card keeps a STACKED card snug around its scene; with the controls
                             in the rail there is nothing under the picture to be snug about, and the
                             cap was holding the crossword scene at 350 px inside an 857 px column */
  }
  body.ws-railed .stage > .ws-rail    { grid-column: 2; grid-row: 2; }
  body.ws-railed .stage > audio { display: none; }        /* a grid item with no box would still take a track */

  .ws-rail {
    display: flex; align-items: stretch; gap: 10px; min-height: 0;
  }
  .ws-rail__body {
    /* wide enough for all four counters in ONE row — squeezing them onto two lines is what made the
       rail look like leftovers rather than a panel. The scene loses nothing: it is limited by the
       window's height at this size, not by the width the rail leaves it. */
    width: clamp(340px, 27vw, 400px); min-height: 0; overflow-y: auto;
    display: flex; flex-direction: column; justify-content: flex-start; gap: 18px;
    background: #fff; border: 1px solid var(--border); border-radius: 22px;
    padding: 20px 18px; box-shadow: 0 8px 26px rgba(64, 64, 64, 0.09);
  }
  .ws-rail .pop-tally { flex-wrap: nowrap; gap: 6px; }
  /* on white the dark counter chips read as heavy furniture; outline them instead */
  .ws-rail .ws-pop-pill {
    background: #fff; color: var(--ink); border: 1px solid var(--border);
    font-size: 1.05rem; padding: 5px 10px;
  }
  /* the actions belong at the FOOT of the panel, not floating in the middle of it */
  body.ws-railed .ws-rail .ws-bar { padding-top: 4px; }
  .ws-rail--closed .ws-rail__body { display: none; }
  .ws-rail__fold {
    align-self: center; flex: 0 0 auto; width: 30px; height: 64px; padding: 0;
    border: 1px solid var(--border); border-right: none; border-radius: 14px 0 0 14px;
    background: #fff; color: var(--ink-soft); cursor: pointer; font-size: 0.9rem; line-height: 1;
    box-shadow: -2px 0 10px rgba(80, 60, 40, 0.08);
  }
  .ws-rail__fold:hover { color: var(--ink); }
  /* inside the rail the buttons are a stack of full-width pills, and the bar is not a bar any more */
  /* `body.ws-railed` on purpose: the wide-screen bar rule below is `body.ws-play .ws-bar`, which
     out-specifies a bare `.ws-rail .ws-bar` and was re-centring the buttons at the top of the rail. */
  body.ws-railed .ws-rail .ws-bar {
    flex-direction: column; align-items: stretch; gap: 10px;
    margin: auto 0 0; padding: 0;                     /* auto top margin = the actions sit at the foot */
    background: none; border: none; box-shadow: none; max-width: none; position: static;
  }
  body.ws-railed .ws-rail .ws-bar__btn { width: 100%; }
  .ws-rail .pop-tally { margin: 0; }
  .ws-rail .game-screen { margin: 0; }
  /* the crossword's clue row is `nowrap` because on a phone four pictures must stay one line; in a
     narrow rail that turned into a horizontal scrollbar with the pictures cut off — wrap instead */
  .ws-rail .xw-clues { flex-wrap: wrap; gap: 10px; }
  .ws-rail .xw-clue img { width: 62px; height: 62px; }
}

@media (min-width: 561px) {
  /* On a big screen the bar is not a bar. A white plate with square corners, wider than the card it
     hangs under and stuck to the bottom of the window, reads as a strip bolted onto the game — it
     pulls the eye away from the picture, which is the opposite of what a game screen wants. The
     buttons alone, centred under the card, say the same thing with none of the furniture. The strip
     stays on phones, where the bar IS the thumb zone and needs a surface to sit on. */
  body.ws-play .ws-bar {
    position: static; margin: 12px auto 0; padding: 0;
    background: none; backdrop-filter: none; -webkit-backdrop-filter: none;
    border-top: none;
    max-width: min(100%, calc(100dvh - var(--ws-card, 200px)));
  }
}

/* the word list — a slim collapsible bar under the window, gently inset */
.vocab-bar {
  margin: 10px 0 0;
  background: var(--ws-bg-soft);
  border-radius: 12px;
  overflow: hidden;
  text-align: left;
}
.vocab-summary {
  cursor: pointer;
  padding: 9px 14px;
  font-weight: 600;
  font-size: 0.82rem;
  list-style: none;
  user-select: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.vocab-summary::-webkit-details-marker { display: none; }
.vocab-summary::after { content: "▼"; font-size: 0.65rem; color: var(--ink-soft); }
.vocab-bar[open] .vocab-summary::after { content: "▲"; }
.vocab-list {
  list-style: none;
  margin: 0;
  padding: 4px 10px 8px;
  max-height: 190px;
  overflow-y: auto;
  border-top: 1px solid #eef0f3;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
  gap: 1px 14px;
}
.vocab-list li {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  padding: 3px 4px;
  font-size: 0.8rem;
}
.vocab-en { font-weight: 600; }
.vocab-ru { color: var(--ink-soft); text-align: right; }

/* the game window: Max's tree-house scene (door-framed crop, cover). Bigger
   and square; Max keeps his size so he reads as proportional to the door. */
.game-field {
  position: relative;
  flex: 0 0 auto;
  /* 100% of the PANEL, never 90vw: the viewport unit ignored the stage's 16px and the panel's 14px
     of padding, so on a 375px phone the field came out 337px inside a 315px box and spilled past the
     white card on the right. Phones widen it further — see the breakout rule below. */
  width: min(452px, 100%);
  aspect-ratio: 1 / 1;
  border-radius: 16px;
  border: none;
  background: linear-gradient(#bfe3f2 0%, #cdeaf0 40%, #dcefc6 66%, #cbe6ac 100%);
  background-image: url("assets/bg/scene.jpg");
  background-size: cover;
  background-position: center;
  overflow: hidden;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}
.game-field .max-wrap--rig { margin: 0; }
/* desktop/tablet: grow the square scene to the tallest that still fits the viewport WITHOUT scroll —
   reserve ~500px for the header + the control stack below (length chooser, choices, buttons, tip).
   On short standard desktops the reserve limits it; on tall screens it grows toward 680. Phones use the rule above. */
@media (min-width: 680px) {
  .game-field { width: min(680px, 90vw, max(452px, calc(100vh - 520px))); }  /* never smaller than the phone size; grows toward 680 on tall screens; ~520px reserved for header + control stack */
}

/* loading gate — covers the field with a spinner until the scene + Max parts
   have loaded, so they don't appear one by one. Fades out when ready. */
.loading {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 16px;
  background: linear-gradient(#bfe3f2 0%, #cdeaf0 40%, #dcefc6 66%, #cbe6ac 100%);
  transition: opacity 0.45s ease;
}
.loading.done { opacity: 0; pointer-events: none; }
.loading-spinner {
  width: 46px;
  height: 46px;
  border: 5px solid rgba(255, 255, 255, 0.65);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.play-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
  margin-top: 10px;
}

.end-title { font-size: 1.2rem; }

/* level vs game-type: two labelled groups, separated by a divider (no plates) */
.setup-selectors {
  display: flex;
  gap: 22px;
  justify-content: center;
  align-items: flex-start;
  flex-wrap: wrap;
  margin: 14px 0 16px;
}
.selector-group {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
.selector-group + .selector-group::before {
  content: "";
  position: absolute;
  left: -11px;
  top: 4px;
  bottom: 4px;
  width: 1px;
  background: var(--border);
}
.selector-label {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.pill-group { display: flex; gap: 6px; }

/* field + word bar are already responsive (min(460px, 92vw)); nothing extra */

.game-hud {
  display: flex;
  justify-content: center;
  gap: 12px;
  margin-bottom: 12px;
}

.hud-pill {
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 6px 14px;
  font-weight: 600;
  font-size: 0.95rem;
}

.noun-icon.is-correct {
  animation: wave-bob 0.6s ease-in-out 2;
}

.prompt-cue {
  font-size: 1.7rem;
  font-weight: 700;
  color: var(--ink);
  margin: 10px 0 2px;
  min-height: 1.2em;
}
.prompt-cue:empty { min-height: 0; margin: 0; }

.prompt-gloss {
  font-size: 0.95rem;
  color: var(--ink-soft);
  margin: 0 0 8px;
}
.prompt-gloss.hidden { display: none; }

/* opt-in hint: a bare lightbulb (NOT a button — no chrome, no label, so it
   doesn't beg to be tapped). It sits calm, then starts pulsing after a beat
   (.pulsing) to offer help. Tapping reveals the phrase and costs the point. */
.hint-btn {
  background: none;
  border: none;
  padding: 4px;
  margin: 0 0 6px;
  line-height: 1;
  cursor: pointer;
}
.hint-btn.hidden { display: none; }
.hint-btn:hover { background: none; }   /* bare lightbulb — no hover chrome */
.hint-btn:active { transform: scale(0.92); }
.hint-btn .hint-ico {
  display: inline-block;
  font-size: 1.8rem;
  line-height: 1;
}
/* stays still until it has waited a beat, then pulses to invite a tap */
.hint-btn.pulsing .hint-ico { animation: hint-pulse 1s ease-in-out infinite; }
@keyframes hint-pulse {
  0%, 100% { transform: scale(1);    filter: drop-shadow(0 0 0 rgba(243, 156, 18, 0)); }
  50%      { transform: scale(1.22); filter: drop-shadow(0 0 8px rgba(243, 156, 18, 0.95)); }
}

.hint-box {
  background: #fff6da;
  border: 1px dashed var(--warn);
  border-radius: 8px;
  padding: 6px 10px;
  display: inline-block;
  font-size: 0.95rem;
  margin-bottom: 8px;
}
.hint-box.hidden { display: none; }

.ask-form {
  display: flex;
  gap: 8px;
  justify-content: center;
  margin: 12px 0 6px;
}
.ask-form input {
  flex: 1;
  max-width: 320px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.95rem;
}
.ask-form input:disabled { background: var(--ws-bg-soft); }

.feedback {
  min-height: 1.4em;
  font-size: 0.95rem;
}

.results-list {
  list-style: none;
  padding: 0;
  font-size: 1.05rem;
  line-height: 1.9;
}

.ghost-btn {
  margin-top: 18px;
  background: transparent;
  color: var(--ink-soft);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 14px;
  font-size: 0.85rem;
}
.ghost-btn:hover { background: var(--ws-bg-tint); color: var(--ink); }

/* --- Voice input (Phase 3) ---------------------------------------------- */

.voice-area { position: relative; margin: 10px 0 4px; text-align: center; }
.voice-area.hidden { display: none; }

.mic-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 999px;
  padding: 12px 22px;
  font-size: 1rem;
  cursor: pointer;
}
.mic-btn .mic-ico { font-size: 1.2rem; line-height: 1; }
.mic-btn.is-listening {
  background: var(--bad);
  animation: mic-pulse 1.3s ease-in-out infinite;
}
@keyframes mic-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(192, 57, 43, 0.45); }
  50%      { box-shadow: 0 0 0 12px rgba(192, 57, 43, 0); }
}

/* "press the mic" nudge: a beat after each task, a pointing hand bobs under the
   button and the button gets an amber pulse ring — until the kid presses. */
.mic-btn.nudge {
  animation: mic-nudge 1.2s ease-in-out infinite;
}
@keyframes mic-nudge {
  0%, 100% { box-shadow: 0 0 0 0 rgba(243, 156, 18, 0.55); }
  50%      { box-shadow: 0 0 0 14px rgba(243, 156, 18, 0); }
}
.mic-hint {
  position: absolute;
  left: 50%;
  bottom: -4px;
  transform: translateX(-50%);
  font-size: 1.9rem;
  line-height: 1;
  pointer-events: none;
  animation: mic-hint-bob 0.85s ease-in-out infinite;
}
.mic-hint.hidden { display: none; }
@keyframes mic-hint-bob {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50%      { transform: translateX(-50%) translateY(-8px); }
}

.heard {
  min-height: 1.3em;
  font-size: 0.95rem;
  color: var(--ink);
  margin: 8px 0 0;
}

/* "I hear you" wave — shown while the mic listens (the voice game never shows the
   transcript, matching the learning mode) */
.listen-wave { display: flex; align-items: flex-end; justify-content: center; gap: 4px; height: 26px; margin: 10px 0 0; }
.listen-wave.hidden { display: none; }
.listen-wave span { width: 4px; height: 8px; background: var(--accent); border-radius: 2px; animation: listen-wave 0.9s ease-in-out infinite; }
.listen-wave span:nth-child(2) { animation-delay: 0.12s; }
.listen-wave span:nth-child(3) { animation-delay: 0.24s; }
.listen-wave span:nth-child(4) { animation-delay: 0.12s; }
@keyframes listen-wave { 0%, 100% { height: 8px; } 50% { height: 24px; } }

.voice-note { font-size: 0.85rem; color: var(--ink-soft); margin: 6px 0; }
.voice-note.hidden { display: none; }

/* protect game art: no long-press callout/filename, no selection */
.game-stage img { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }

/* win celebration — emoji/hearts drift slowly down for ~12s (WScene.celebrate) */
.ws-confetti { position: fixed; inset: 0; pointer-events: none; z-index: 80; overflow: hidden;
  -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; }
/* the layer ignores taps (gaps pass through to the game); each emoji catches taps on
   the glyph itself only — no padded box, and no mobile tap-highlight square */
/* dim backdrop for the "pop the confetti" moment (scene.js celebrate({dim:true})) — used only where
   the game keeps running underneath, so taps land on emoji instead of on the game */
.ws-celebrate-back { position: fixed; inset: 0; z-index: 79; background: rgba(20, 14, 8, .45);
  touch-action: none; -webkit-tap-highlight-color: transparent; animation: ws-back-in .25s ease both; }
@keyframes ws-back-in { from { opacity: 0 } to { opacity: 1 } }

.ws-confetti span { position: fixed; top: -9vh; will-change: transform, opacity;
  /* touch-action:none, NOT manipulation — "manipulation" still lets the browser pan the page from a
     touch that starts on the glyph, which is why tapping an emoji made the screen jump and scroll */
  pointer-events: auto; cursor: pointer; touch-action: none;
  -webkit-tap-highlight-color: transparent; -webkit-user-select: none; user-select: none; outline: none;
  -webkit-touch-callout: none; -webkit-user-drag: none;
  animation-name: ws-confetti-fall; animation-timing-function: linear; animation-fill-mode: both; }
@keyframes ws-confetti-fall {
  0%   { transform: translateY(0) translateX(0) rotate(0deg); opacity: 0; }
  10%  { opacity: 1; }
  92%  { opacity: 1; }
  100% { transform: translateY(118vh) translateX(var(--drift)) rotate(var(--rot)); opacity: 0.9; }
}
/* burst when tapped: puff up and fade out in place */
@keyframes ws-pop {
  0%   { transform: scale(1);   opacity: 1; }
  45%  { transform: scale(1.7); opacity: 1; }
  100% { transform: scale(2.3); opacity: 0; }
}
/* per-type "popped" tally — a centered row of pills between the title and the field,
   visible from the moment the game opens (starts at 0) and counting up as you pop */
.pop-tally {
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
  margin: 2px auto 6px; max-width: 92vw;
}
.ws-pop-pill {
  display: inline-flex; align-items: center; gap: 4px;
  font: 400 1.25rem/1 "Chewy", "Inter", system-ui, sans-serif; letter-spacing: 0.5px;
  color: #fff; background: rgba(20, 22, 30, 0.42);
  padding: 6px 13px 7px; border-radius: 999px;
  box-shadow: 0 3px 12px rgba(0, 0, 0, 0.25);
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
}
.ws-pop-pill b { font-weight: 400; font-variant-numeric: tabular-nums; }
.ws-pop-pill.bump { animation: ws-count-bump 0.26s ease-out; }
@keyframes ws-count-bump {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.28); }
  100% { transform: scale(1); }
}

/* On phones the mic is the main control (push-to-talk) — a big, obvious,
   full-width, thumb-friendly tap target. */
@media (max-width: 560px) {
  .mic-btn {
    width: 100%;
    justify-content: center;
    padding: 16px 22px;
    font-size: 1.15rem;
  }
  .mic-btn .mic-ico { font-size: 1.5rem; }
}

/* «Учить»: Max is tappable to hear the word again — a small squash so the tap feels answered */
.max-stage.tapped { animation: ws-max-tap .32s ease; }
@keyframes ws-max-tap {
  0%   { transform: scale(1); }
  38%  { transform: scale(0.94) translateY(3px); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) { .max-stage.tapped { animation: none; } }

/* ============================================================================
   ACTION BAR — постоянный ряд игровых кнопок внизу экрана.
   Why: on a real phone (360×620 with browser chrome) the old in-flow controls sat 35–110 px BELOW
   the fold, and a child does not scroll to look for a button — for them it simply isn't there.
   A bar pinned to the bottom also lands in the thumb zone, which the top corners never do.
   Shared class so every game gets the identical bar in the identical place. */
.ws-bar {
  position: sticky; bottom: 0; z-index: 30;
  display: flex; gap: 8px; justify-content: center; align-items: stretch;
  padding: 8px 8px calc(8px + env(safe-area-inset-bottom, 0px));
  margin: 8px -14px 0;                       /* break out of the panel padding → edge to edge */
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  border-top: 1px solid var(--border);
}
/* A pill, laid out in a ROW. The old column layout (icon over word) was capped at 118 px, so «К
   словам» broke onto two lines while there was room to spare, and the two buttons came out different
   heights — the owner read the pair as lopsided. Nothing wraps now and every button is one shape.
   Colour: a saturated orange fill for the "main" action pulled the eye away from the game every time
   (owner, twice). The accent is a RING, not a fill — the hierarchy survives, the shouting does not. */
.ws-bar__btn {
  flex: 0 0 auto; min-height: 52px; padding: 8px 20px;   /* 52 ≥ the 44 px adult minimum: a child's aim is worse */
  display: inline-flex; flex-direction: row; align-items: center; justify-content: center; gap: 9px;
  white-space: nowrap;
  border: 1px solid var(--border); border-radius: 999px; cursor: pointer;
  font-family: inherit; font-weight: 700; font-size: 0.95rem; line-height: 1;
  color: var(--ink); background: #fff;
  box-shadow: 0 2px 0 rgba(90, 70, 50, 0.10), 0 4px 12px rgba(80, 60, 40, 0.10);
  transition: transform 0.08s ease, box-shadow 0.12s ease, opacity 0.12s ease;
  -webkit-tap-highlight-color: transparent;
}
.ws-bar__btn .ico { font-size: 1.15rem; line-height: 1; }          /* icon AND word: a lone glyph is not readable at 6 */
/* Три кнопки в ряд («Составь слово»: Подсказка / Пропустить / Готово) на экране 375 px не помещались:
   измерено — ряду нужно 413 px при 360 доступных, крайние кнопки уезжали за оба края и появлялась
   горизонтальная прокрутка 12 px. Ребёнок ловил падающие смайлики, попадал мимо — и экран уезжал
   вбок. Отсюда жалоба «скачет экран».
   Ужимаем ступенями, а не одним медиазапросом: ширина зависит от НАДПИСЕЙ, а они у каждой игры свои,
   так что порог по ширине экрана угадать нельзя. ui.js меряет ряд и вешает класс. */
.ws-bar--tight { gap: 6px; }
.ws-bar--tight .ws-bar__btn { padding: 8px 12px; font-size: 0.9rem; gap: 6px; }
.ws-bar--tight .ws-bar__btn .ico { font-size: 1.05rem; }
.ws-bar--tiny { gap: 4px; }
.ws-bar--tiny .ws-bar__btn { padding: 8px 9px; font-size: 0.82rem; gap: 4px; }
.ws-bar--tiny .ws-bar__btn .ico { font-size: 0.95rem; }
.ws-bar__btn:active { transform: translateY(2px); box-shadow: 0 0 0 rgba(0,0,0,0), 0 2px 6px rgba(80,60,40,.14); }
.ws-bar__btn:disabled { opacity: 0.38; cursor: default; transform: none; }
.ws-bar__btn:focus-visible { outline: 3px solid var(--accent2); outline-offset: 2px; }
/* NO colour on the action buttons at all. Orange fill, then an orange ring — the owner objected to
   both, three times: anything coloured down there competes with the picture, which is the one thing
   a child should be looking at. Every button is the same quiet pill; what they do is written on them. */
.ws-bar__btn--primary { background: #fff; border-color: var(--border); color: var(--ink); }
.ws-bar__btn--quiet {                                              /* ending the session must not be a bright target */
  background: var(--surface); color: var(--ink-soft); border: 1px solid var(--border); box-shadow: none;
}

/* How-to gate — the game's instruction over a dimmed screen, before the first move (built by ui.js
   from `data-howto`). It replaces the permanent line of text that used to live under the scene:
   read once, but paid for in height on every screen, and on a short phone it pushed the bottom bar
   out of reach. Same dim as the riddles start gate, so the two read as one family. */
.ws-howto {
  position: fixed; inset: 0; z-index: 80; padding: 22px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(22, 13, 7, 0.66);
}
.ws-howto__card {
  background: #fff; border-radius: 18px; padding: 20px 22px; max-width: 340px; text-align: center;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.34);
}
.ws-howto__text { margin: 0 0 16px; font-size: 1.05rem; line-height: 1.35; color: var(--ink); }
.ws-howto__btn {
  min-height: 52px; padding: 12px 34px; border: none; border-radius: 999px; cursor: pointer;
  font-family: inherit; font-weight: 700; font-size: 1.05rem; color: #fff; background: var(--accent);
  box-shadow: 0 3px 0 rgba(90, 70, 50, 0.2), 0 6px 16px rgba(80, 60, 40, 0.16);
  -webkit-tap-highlight-color: transparent;
}
.ws-howto__btn:active { transform: translateY(2px); box-shadow: 0 1px 0 rgba(90, 70, 50, 0.2); }
.ws-howto__btn:focus-visible { outline: 3px solid var(--accent2); outline-offset: 3px; }

/* The top row is chrome, not the game: keep it legible but never louder than the bar below.
   The level plaque is a LABEL you cannot press — it used to be the brightest thing on screen. */
.level-badge { opacity: 0.9; }
.mute-btn { min-width: 48px; min-height: 48px; font-size: 1.25rem; }

/* PHONE: the bar stays in normal flow — it is NOT pinned to the window.
   A fixed bar anchors to the layout viewport, which on a phone sits BEHIND the browser's own bottom
   toolbar: the buttons ended up half-hidden in the "basement" and scrolling could not bring them up,
   because fixed elements do not scroll. Instead we make the screen FIT: the scene is capped in
   height so the whole game — scene, controls and bar — lands inside the visible area, and the bar is
   simply the last thing on the page. Nothing to get stuck behind.
   dvh (dynamic viewport height) is the unit that already accounts for the browser's bars. */
@media (max-width: 560px) {
  .ws-bar {
    position: static;
    /* full-bleed regardless of how much padding the ancestors happen to have — guessing at a
       negative margin left a 2 px sliver of card down each side */
    margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw);
    border-radius: 0; padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
  }
  .ws-bar__btn { font-size: 0.72rem; max-width: none; }
  body.ws-play { padding-bottom: 0; }

  /* The white card goes edge to edge too. Once the scene fills the screen width, a narrower card
     behind it reads as a stray band down each side — the frame is only meaningful on a desktop,
     where the game sits in the middle of a wide page. */
  body.ws-play .game-panel {
    margin-left: -16px; margin-right: -16px;      /* cancel .stage's side padding */
    border-radius: 0; border-left: none; border-right: none;
    max-width: none;                              /* --ws-card keeps the card snug on a WIDE screen;
                                                     on a phone the card is the screen */
    width: auto;                                  /* a fixed width would make the negative margins
                                                     SHIFT the card instead of stretching it — the card
                                                     stayed 324 px inside a 362 px screen, the answer
                                                     pictures wrapped to a second row, and the scene
                                                     was squeezed to 51 px */
  }
  /* the scene now only has to escape the panel's own 14 px, not the stage's 16 px as well */
  body.ws-play .stage-row { margin-left: -14px; margin-right: -14px; }

  /* trim what is decoration on a small screen, so the game itself fits */
  body.ws-play .stage {
    padding-top: 6px; padding-bottom: 6px;
    max-width: none;      /* the 96vw breathing room is for a desktop; a phone gives it all away */
  }
  body.ws-play .app-title { display: none; }            /* the big title is for the desktop, not for play */
  /* The header must NOT be removed outright: «Меню», the level plaque and the sound button are
     absolutely positioned over the top of the stage, and the header is what reserves their space.
     Hiding it let the score counters slide up underneath them. Keep the block, drop only the title. */
  body.ws-play .app-header { padding-top: 44px; margin-bottom: 2px; }
  body.ws-play .mode-tag { margin: 0; font-size: 0.82rem; }
  body.ws-play .game-panel { padding-top: 8px; padding-bottom: 8px; }
  /* The scene keeps the full screen width; how TALL it may be is decided by the fitting system
     below (`.stage-row` is the measured container), not by a per-game reserve. */
  body.ws-play .stage-row {
    width: 100vw;
    /* …but the row must not be TALLER than the picture it holds. It is the flexible element, so on a
       tall phone it swallowed every spare pixel, the square scene sat at its top and everything below
       fell to the bottom of the screen — measured: a 133 px hole between the picture and the timer in
       «Составь слово». The scene is square and full-width, so 100vw is exactly as tall as it can need. */
    max-height: 100vw;
  }
  /* …кроме «Лови слова»: там поле НАМЕРЕННО вытянутое (слова должны падать во всю высоту), и общий
     потолок делал его квадратным — проверено, поле стало 393×393 вместо портретного. */
  /* With the row capped, the leftover must not become a hole between the picture and the controls —
     it belongs around the whole block, so the card CENTRES its content.
     ⚠️ The card must keep growing (`flex: 1 1 auto`, set above). Making it shrink-to-fit collapsed
     the scene to 0×0: the row's `container-type: size` then has no definite height, `100cqh` resolves
     to 0, and `min(100cqw, 100cqh)` is zero. Measured — the picture vanished. */
  body.ws-play .game-panel { justify-content: center; }
}
@media (prefers-reduced-motion: reduce) { .ws-bar__btn { transition: none; } }

/* «Сейчас в игре» — тихая строка в меню. Это не показатель для ребёнка, а ориентир для взрослого,
   поэтому оформлена так же неброско, как оговорка про синтезированный голос. */
.ws-online {
  margin: 10px auto 0; max-width: 640px; text-align: center;
  font-size: 0.82rem; color: var(--ink-soft);
}

/* Перевод слова на русский в списке слов — для англоязычного интерфейса он бессмыслен: ребёнок,
   выбравший English, читает не по-русски. Сам список остаётся, исчезает только пометка в скобках. */
html[lang="en"] .lm-check .ru { display: none; }
/* тот же перевод в скобках, но в разделе «Учить»: разметка там своя */
html[lang="en"] .lm-checkgrid .ru { display: none; }
