:root {
  --c-sky: #8CC9E8;
  --c-cream: #FBF1DA;
  --c-mustard: #F2B04C;
  --c-coral: #F08A5D;
  --c-leaf: #7FBF9E;
  --c-plum: #B79FD1;
  --c-hill-1: #9FCF8E;
  --c-hill-2: #7FBF9E;
  --c-soil: #A9865B;
  --c-snow: #F4F8FA;
  --ink: #4A3418;
  --ink-deep: #153A2B;
  --ink-warm: #4A1B0C;
  --c-rainfall: #7FB4D4;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; user-select: none; }

html, body {
  margin: 0; padding: 0; width: 100%; height: 100%;
  overflow: hidden; background: var(--c-cream);
  font-family: var(--font); color: var(--ink);
  overscroll-behavior: none;
}

.screen { position: fixed; inset: 0; display: none; }
.screen.active { display: block; }
.screen.overlay { display: none; }
.screen.overlay.active { display: flex; align-items: center; justify-content: center; background: rgba(74,52,24,0.55); }

/* CHARACTER SELECT */
/* START / LOADING */
/* One screen doing two jobs. It holds until every pose image has decoded, so
   the first shrug or harvest does not pop in half-drawn — and then it waits for
   a tap, because that tap is the user gesture the audio context and the Web NFC
   permission prompt both require. Never auto-advance it. */
/* Full-bleed: the copy runs the width of the screen rather than sitting in a
   narrow centred card. Hob stays centred above it; everything written is left
   aligned, which is what makes it read as a page rather than a poster.

   Anchored to the TOP, not centred. Centring meant Hob shifted every time the
   thing below him changed height — the bar collapsing, the copy arriving, the
   card slots appearing — so he never sat still between loading and ready. From
   the top he is in the same place for the whole screen's life. */
#screen-start.active {
  display: flex; align-items: stretch; justify-content: flex-start;
  flex-direction: column;
  padding: 9vh 28px calc(24px + env(safe-area-inset-bottom));
  background: var(--c-cream);
}
/* A picture, not a control — the Start button is the control. Centred on its
   own row, which is why it needs the auto margins now the column stretches.
   The height is RESERVED before the art fades in, so nothing below it jumps. */
/* He fades in as soon as HIS OWN file has decoded, which is long before the
   other seven — so he is on screen for the loading, not only after it. That is
   what makes this one screen: the bar fades out from under him and the copy
   arrives in its place, rather than a loading screen being replaced by a
   different screen with a character on it. */
.start-hedgehog {
  width: 44vmin; max-width: 220px; min-width: 140px; height: auto; display: block;
  margin: 0 auto 4vh;
  opacity: 0;
  transition: opacity 420ms ease;
}
.start-hedgehog.loaded { opacity: 1; }
/* Ready: he breathes. Nothing about it says "press me". */
#screen-start.ready .start-hedgehog {
  animation: start-breathe 2.6s ease-in-out infinite 420ms;
}
@keyframes start-breathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.045); }
}
#screen-start.ready .start-loading { opacity: 0; }

/* Determinate, and it fills as the poses decode. There is real work behind it:
   with no drawn fallback left, a pose that has not loaded renders as nothing. */
/* Keeps its height when it fades — collapsing it would shuffle everything
   below at the exact moment the copy arrives. */
.start-loading {
  width: 100%; height: 8px; margin-bottom: 24px; border-radius: 4px;
  background: rgba(74,52,24,0.14); overflow: hidden;
  transition: opacity 300ms ease;
}
.start-loading-bar {
  width: 0%; height: 100%; border-radius: 4px;
  background: var(--c-mustard);
  transition: width 220ms ease;
}

/* Arrives AFTER the bar has faded, in the space the bar was holding — one
   screen changing its mind, not a second screen. */
.start-copy { width: 100%; text-align: left; animation: start-copy-in 420ms ease both; }
@keyframes start-copy-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
.start-state h1 {
  font-size: 22px; line-height: 1.3; margin: 0 0 6px; color: var(--ink);
}
.start-state p { font-size: 16px; line-height: 1.55; margin: 8px 0; color: rgba(74,52,24,0.78); }
.start-note {
  font-size: 16px; line-height: 1.55; margin: 0 0 24px;
  color: rgba(74,52,24,0.78);
}
.start-note strong { color: var(--ink); }

/* The only button in the product. It exists because an AudioContext needs a
   gesture on every load and a card tap is not one — without this press the game
   runs silent. Made to look pressable for exactly that reason. */
.start-button {
  display: block; width: 100%;
  font: inherit; font-size: 17px; font-weight: 600; letter-spacing: 0.01em;
  color: var(--c-cream); background: var(--ink);
  border: none; border-radius: 14px; padding: 16px 24px; cursor: pointer;
  box-shadow: 0 4px 0 rgba(74,52,24,0.28);
  transition: transform 0.08s ease, box-shadow 0.08s ease;
}
.start-button:active {
  transform: translateY(3px);
  box-shadow: 0 1px 0 rgba(74,52,24,0.28);
}
/* `display:block` beats the UA rule for the `hidden` attribute, so without this
   the Continue button shows before the three cards have been checked. Same trap
   as .debug-panel below — it applies to anything given a display and toggled
   with `hidden`. */
.start-button[hidden] { display: none; }
.start-cue {
  margin-top: 20px !important; font-size: 16px !important; font-weight: 600;
  color: var(--ink) !important;
}
/* Only the cue that is actually waiting on a card breathes. */
.start-cue-live { animation: cue-pulse 1.8s ease-in-out infinite; }
@keyframes cue-pulse {
  0%, 100% { opacity: 0.5; }
  50%      { opacity: 1; }
}
.start-copy .admin-btn { display: block; width: 100%; margin-top: 14px; text-align: center; }

/* Three card slots along the bottom of the setup screen. `margin-top: auto`
   is what pins them there — the column is top-anchored, so this eats the
   remaining space instead of Hob having to move. */
.start-cards {
  display: flex; gap: 14px; justify-content: center;
  margin-top: auto; padding-top: 32px;
}
.start-cards[hidden] { display: none; }
.start-card {
  position: relative; width: 62px; height: 44px; border-radius: 8px;
  border: 2.5px dashed rgba(74,52,24,0.3); background: transparent;
  transition: border-color 260ms ease, background 260ms ease, transform 260ms ease;
}
/* The magnetic stripe, so it reads as a card rather than an empty box. */
.start-card::before {
  content: ''; position: absolute; left: 0; right: 0; top: 11px; height: 7px;
  background: rgba(74,52,24,0.18);
  transition: background 260ms ease;
}
.start-card svg {
  position: absolute; inset: 0; margin: auto; width: 26px; height: 26px;
  opacity: 0; transform: scale(0.5);
  transition: opacity 240ms ease, transform 320ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.start-card.done {
  border-style: solid; border-color: var(--ink); background: var(--ink);
  transform: translateY(-3px);
}
.start-card.done::before { background: rgba(251,241,218,0.25); }
.start-card.done svg { opacity: 1; transform: scale(1); }

/* INTRO */
/* Lower third, so the sky — where the cloud, the rain, the sun and the moon all
   happen — is never covered. Pointer-events off, so the five-tap corner gesture
   in the last beat still reaches #hidden-zone underneath. */
.intro {
  position: absolute; inset: 0; z-index: 40; pointer-events: none;
}
.intro-bubble {
  position: absolute; left: 4%; right: 4%; bottom: 4%;
  background: var(--c-cream); border: 2px solid var(--ink);
  border-radius: 18px; padding: 16px 18px;
  box-shadow: 0 6px 0 rgba(74,52,24,0.18);
  pointer-events: auto;
  animation: intro-in 320ms ease both;
}
@keyframes intro-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* The tail points UP at Hob, who stands above the bubble — it used to sit on
   the bottom edge pointing down into the bushes, at nothing at all. `--tail-x`
   is written by the intro from the creature's measured centre, so it tracks him
   as he moves rather than guessing. */
.intro-bubble::after {
  content: ''; position: absolute;
  left: var(--tail-x, 50%); margin-left: -9px; top: -10px;
  width: 18px; height: 18px; background: var(--c-cream);
  border-left: 2px solid var(--ink); border-top: 2px solid var(--ink);
  transform: rotate(45deg); border-radius: 4px 0 0 0;
  transition: left 300ms ease;
}
.intro-text p { font-size: 15px; line-height: 1.5; margin: 0 0 10px; color: var(--ink); }
.intro-text p:last-child { margin-bottom: 0; }
.intro-cue {
  margin: 12px 0 0; font-size: 14px; font-weight: 600; color: var(--ink);
  animation: cue-pulse 1.8s ease-in-out infinite;
}
.intro-help { margin: 8px 0 0; font-size: 12px; line-height: 1.4; color: rgba(74,52,24,0.6); }
.intro-help button {
  border: none; background: none; padding: 0; font: inherit;
  color: var(--ink); text-decoration: underline; cursor: pointer;
}
.intro-next {
  margin-top: 12px; width: 100%; padding: 11px; font: inherit; font-size: 15px;
  font-weight: 600; color: var(--c-cream); background: var(--ink);
  border: none; border-radius: 12px; cursor: pointer;
}
.intro-next:active { transform: scale(0.98); }

/* Marks the top-right corner during the five-tap beat. Purely a pointer — the
   real target is #hidden-zone underneath it. */
/* Sized and placed to sit exactly over #hidden-zone, so it reads as a target
   rather than as a stray line near the edge. */
.intro-corner {
  position: absolute; top: 5px; right: 5px; width: 60px; height: 60px;
  border: 3px dashed var(--ink); border-radius: 16px;
  background: rgba(251,241,218,0.4);
  animation: cue-pulse 1.4s ease-in-out infinite;
}

/* The intro no longer resets the scene on the way out — the plant the parent
   grew is what the child arrives at — so there is nothing left to hide behind
   a crossfade. */

.pin-hint {
  margin: 0 0 4px; font-size: 14px; font-weight: 600;
  color: var(--ink); text-align: center;
}

/* PLAY SCREEN */
/* The scene box has the EXACT aspect of the art canvas (1080x1920) and is sized
   by Scene.fit() to COVER the screen, so a percentage in here is the same thing
   as a canvas coordinate on any phone shape. */
.scene-wrap {
  position: relative; width: 100%; height: 100%; overflow: hidden;
  /* Must be the exact colour the sky gradient STARTS at, or the top edge of the
     scene box shows as a hard seam on a tall phone. */
  background: #EDF4F7;
}

/* Sized and centred by Scene.fit() so it always COVERS the screen while keeping
   the canvas aspect exactly. That is what makes a percentage in here identical
   to a canvas coordinate, on any phone shape. */
.scene-root { position: absolute; }
.scene-root.no-anim, .scene-root.no-anim * { transition: none !important; }
.layer { position: absolute; inset: 0; }
.layer svg, .layer img { position: absolute; inset: 0; width: 100%; height: 100%; display: block; object-fit: cover; }

/* ---------- The world strip ----------
   Three plots stand side by side on one continuous strip of ground, seven
   canvas widths across, starting one width to the LEFT of plot 0 (which is
   where the warehouse lives). The camera translates these two containers;
   everything outside them — sky, sun, moon, cloud, rain, tints — stays put.

   Keep 700%/-100% in step with TILES and TILE_MIN in scene.js. */
.scene-far, .scene-world {
  position: absolute; top: 0; left: -100%; width: 700%; height: 100%;
  will-change: transform;
}

/* ---------- Day and night ----------
   The sun and moon travel an arc, right to left, and both ends of it are below
   the mountain tops — and the mountains are drawn in the layer in front, so a
   setting sun is genuinely occluded rather than just faded. A tint over the
   whole scene carries the change of light.

   **Their `transform` belongs to JS** (`Scene.setSkyArc`), because where they
   are is a function of how far through the day you are, not of which class is
   on the root. Do not add a transform to these two in CSS — an inline style
   wins, so the rule would silently do nothing.

   There is deliberately NO transform transition here either. JS steps them
   along the arc frame by frame, and a transition would interpolate between the
   frames in a straight line and fight it. Opacity only. */
.layer-sun, .layer-moon, .layer-stars, .layer-night {
  transition: opacity 1400ms ease;
}
.sun-rays { animation: sun-spin 90s linear infinite; }
@keyframes sun-spin { to { transform: rotate(360deg); } }

.layer-stars { opacity: 0; }
.layer-moon  { opacity: 0; }
/* Lighter at the TOP. The moon and stars sit under this tint (so they can rise
   out from behind the mountains), so a dark sky band would swallow them. Ground
   goes darkest, which is also what real dusk does. */
.layer-night {
  opacity: 0; pointer-events: none; mix-blend-mode: multiply;
  background: linear-gradient(#8FA3C6, #5D77AA 42%, #2C4680);
}

.scene-root.night .layer-sun   { opacity: 0; }
.scene-root.night .layer-moon  { opacity: 1; }
.scene-root.night .layer-stars { opacity: 1; }
.scene-root.night .layer-night { opacity: 0.62; }

/* The moon breathes very slightly, so night doesn't read as "paused". */
.scene-root.night .moon-glow { animation: moon-breathe 6s ease-in-out infinite; }
@keyframes moon-breathe {
  0%, 100% { opacity: 0.75; }
  50% { opacity: 1; }
}

/* Tree grows upward from the soil line rather than fading in place. The origin
   is set from JS — it has to sit on the CURRENT plot's trunk, which moves.

   THE GROWTH ITSELF IS A SCRIPT ANIMATION (Scene.setTrees), not a class, because
   where it starts is a function of the stage it grew OUT of: the new drawing
   opens at the height of the old one and stretches up to its own. A fixed
   keyframe cannot know that, and the old one (scale 0.86, opacity 0.35 → 1) read
   as one tree vanishing and another arriving rather than as growing.

   The old drawing is left behind as a ghost that fades under it, so the two
   overlap through the stretch instead of one being swapped for the other. */
.layer-tree { transition: transform 800ms ease, filter 800ms ease; }
.tree-ghost { pointer-events: none; }

/* Blossom → fruit. The tree does NOT grow again here — it is the same tree, one
   tap later. Each flower that set is swapped for the fruit in its own place. */
.bloom-out, .fruit-in, .petal, .petal-sway {
  transform-box: fill-box; transform-origin: center;
}
.bloom-out { animation: bloom-out 520ms ease forwards; }
@keyframes bloom-out {
  0%   { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.45); }
}
.fruit-in { animation: fruit-in 620ms cubic-bezier(0.34, 1.5, 0.5, 1) 160ms both; }
@keyframes fruit-in {
  0%   { opacity: 0; transform: scale(0.25); }
  55%  { opacity: 1; }
  100% { opacity: 1; transform: scale(1); }
}
/* And the flowers that did NOT set come off, slowly, the way a petal does. They
   live in their own layer so the tree can be redrawn under them (the crop drops
   while they are still on their way down) without wiping them out of the air. */
.petal { animation: petal-fall 2600ms cubic-bezier(0.35, 0, 0.7, 1) forwards; }
@keyframes petal-fall {
  0%   { opacity: 1; transform: translateY(0); }
  70%  { opacity: 0.85; }
  100% { opacity: 0; transform: translateY(430px); }
}
/* The drift is a SECOND element: one transform doing both x and y is a straight
   line, and a petal that falls in a straight line is a dropped stone. */
.petal-sway { animation: petal-sway 1500ms ease-in-out infinite alternate; }
/* Starts at rest, or a petal still waiting its turn on the branch would jump
   sideways the moment its delay ran out. */
@keyframes petal-sway {
  0%   { transform: translateX(0) rotate(0deg); }
  100% { transform: translateX(26px) rotate(18deg); }
}
/* Flooded. A mood applied over whatever stage is showing — no progress is lost,
   so this must never be mistaken for going backwards.

   It must NOT change the tree's size. `scaleY` here read as the tree shrinking
   back a stage, which is the one thing flooding is not allowed to look like.
   A lean and a drained colour say "sodden" without touching the shape. */
.layer-tree.droop { transform: rotate(-3deg) skewX(2deg); filter: saturate(0.5) brightness(0.9); }

/* Water in the soil — the visible half of the game's only rule. Origin from JS,
   same reason as the tree. */
.layer-puddle.ripple { animation: ripple 800ms cubic-bezier(0.34, 1.36, 0.5, 1); }
@keyframes ripple {
  0% { transform: scale(0.72); opacity: 0.25; }
  60% { transform: scale(1.07); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}

/* Six separate puffs drifting in from different directions to gather over the
   sky and cover the sun. Each one's transform is set inline from JS — see the
   note in scene.js about var() inside @keyframes. */
/* Tall enough that the lowest puff clears the BOTTOM OF THE SUN'S RAYS (canvas
   y=714), not just the sun's disc — otherwise the rays poke out underneath and
   the sun still reads as visible. */
.cloud { position: absolute; top: 0; left: 0; width: 100%; height: 52%; }
.cloud .puff {
  position: absolute;
  transition: transform 1250ms cubic-bezier(0.22, 0.85, 0.3, 1), opacity 650ms ease;
}
.cloud .puff svg, .cloud .puff img { width: 100%; height: auto; display: block; }
.cloud.heavy .puff { filter: brightness(0.82) saturate(0.75); }
.cloud .puff { transition-property: transform, opacity, filter; }
.cloud.nudge .puff { animation: nudge 700ms ease; }
@keyframes nudge {
  0%, 100% { translate: 0 0; }
  30% { translate: 0 -16px; }
  65% { translate: 0 6px; }
}

/* Overcast: the whole scene loses its light while the cloud is over it. Stacks
   with the night tint, so a cloudy night is darker than either alone. */
.layer-overcast {
  opacity: 0; pointer-events: none; mix-blend-mode: multiply;
  background: linear-gradient(#AEBAC6, #93A2B2);
  transition: opacity 1100ms ease;
}
/* Lighter than it was while the clouds were grey — with white clouds a heavy
   tint reads as a storm rather than as overcast. */
.scene-root.cloudy .layer-overcast { opacity: 0.38; }

/* Rain falls from the TOP OF THE SCREEN all the way to the soil line, not out
   of the cloud's own outline. Starting the drops at the puffs left a dead band
   of clear sky above them that read as a gap rather than as weather.
   Travel: -6% to the soil at 67.7% = 73.7% of the canvas, over a drop 3.4%
   tall = 2170% of its own height. */
.layer-rain { opacity: 0; transition: opacity 400ms ease; pointer-events: none; }
.layer-rain.on { opacity: 1; }
.layer-rain i {
  position: absolute; top: -6%; width: 5px; height: 3.4%; border-radius: 3px;
  background: var(--c-rainfall); animation-name: fall; animation-iteration-count: infinite;
  animation-timing-function: linear;
}
@keyframes fall {
  0% { transform: translateY(0); opacity: 0; }
  8% { opacity: 0.95; }
  92% { opacity: 0.95; }
  100% { transform: translateY(2170%); opacity: 0; }
}

/* Rain card with no cloud: a few drops out of a clear sky that give up partway
   down. Visibly NOT rain, and visibly not a failure either — so they start high
   like real rain but never reach the ground. */
.layer-rain i.sunshower {
  top: 6%; animation-name: sunshower; animation-duration: 1400ms;
  animation-iteration-count: 1; animation-timing-function: ease-in;
}
@keyframes sunshower {
  0%   { transform: translateY(0) scale(1); opacity: 0; }
  18%  { opacity: 0.9; }
  70%  { transform: translateY(700%) scale(0.9); opacity: 0.85; }
  100% { transform: translateY(830%) scale(0.15); opacity: 0; }
}

/* Flood. The biggest thing that happens in the game, and it costs nothing. */
.layer-splash { opacity: 0; pointer-events: none; }
.layer-splash.on { opacity: 1; }
/* Flies "up" in its own rotated frame — the SVG rotate on the wrapper group
   turns that into a fan. Deliberately free of var(), see splash() in scene.js. */
.layer-splash .drop {
  transform-box: fill-box; transform-origin: center;
  animation-name: splash-drop; animation-timing-function: cubic-bezier(0.2, 0.7, 0.4, 1);
  animation-fill-mode: forwards;
}
@keyframes splash-drop {
  0%   { transform: translateY(0) scale(0.3); opacity: 0; }
  12%  { opacity: 1; }
  55%  { transform: translateY(-560px) scale(1.15); opacity: 1; }
  100% { transform: translateY(-120px) scale(0.5); opacity: 0; }
}
.layer-splash .ring {
  transform-box: fill-box; transform-origin: center;
  animation: splash-ring 1300ms cubic-bezier(0.2, 0.8, 0.3, 1) forwards;
}
.layer-splash .ring-2 { animation-delay: 220ms; animation-duration: 1500ms; }
@keyframes splash-ring {
  0%   { transform: scale(0.15); opacity: 1; }
  100% { transform: scale(3.4); opacity: 0; }
}
/* A whole sheet of water thrown up, so the flood reads from across the room
   instead of as a handful of dots. */
.layer-splash .wave {
  transform-box: fill-box; transform-origin: center bottom;
  animation: splash-wave 1200ms cubic-bezier(0.15, 0.85, 0.35, 1) forwards;
}
@keyframes splash-wave {
  0%   { transform: scaleY(0.05) scaleX(0.5); opacity: 0.85; }
  35%  { transform: scaleY(1.15) scaleX(1.1); opacity: 0.65; }
  100% { transform: scaleY(0.3) scaleX(1.5); opacity: 0; }
}
/* The whole scene takes the hit. Small enough not to be alarming, big enough
   that the flood feels like an EVENT. */
.scene-root.quake { animation: quake 620ms cubic-bezier(0.36, 0.07, 0.19, 0.97); }
@keyframes quake {
  0%, 100% { translate: 0 0; }
  15% { translate: -7px 5px; }
  32% { translate: 6px -4px; }
  50% { translate: -5px 3px; }
  68% { translate: 4px -2px; }
  85% { translate: -2px 1px; }
}

/* Feet land on the soil line at y=1300. `left` and `width` are BOTH set in JS:
   the creature lives inside the world strip, which is seven canvas widths
   across, so a percentage in here is not a percentage of the canvas. */
.creature {
  position: absolute; bottom: 31.5%;
  transform: translateX(-50%);
  transition: left 620ms cubic-bezier(0.4, 0.1, 0.3, 1);
}
.creature svg, .creature img { width: 100%; height: auto; display: block; }

/* The drawing and anything layered on it — the sleep bubbles, the armful of
   fruit — live together in here, so they mirror as ONE thing when the creature
   turns round and the fruit stays in its paws. Positioned, because the overlays
   are placed against it in percentages. */
.creature-body { position: relative; }
.creature-extra { position: absolute; pointer-events: none; }

/* The cart. Same deal as the creature: wheels on the soil line, `left` and
   `width` both set in JS because it lives in the seven-wide world strip. It is
   there from the first tap — the child should see where the fruit is going
   before there is any fruit.

   BOTTOM-ANCHORED ON PURPOSE. The drawing's viewBox grows upward as the load
   grows, so the element gets taller with the pile; anchoring the bottom keeps
   the wheels on the soil while the heap climbs. This 30.8% is mirrored by
   CART_BOTTOM_PCT in scene.js, which is how a fruit in flight works out where
   in the cart to land — change one and change the other. */
.cart {
  position: absolute; bottom: 30.8%;
  transform: translateX(-50%);
  transition: left 620ms cubic-bezier(0.4, 0.1, 0.3, 1);
}
.cart svg, .cart img {
  width: 100%; height: auto; display: block;
  transition: transform 420ms ease;
}
/* Handle on the side the creature is pushing from. Transitioned, so it passes
   through edge-on and reads as the cart being turned around. */
.cart.flip svg, .cart.flip img { transform: scaleX(-1); }
/* Facing. Flipping the body rather than the element keeps it clear of the
   creature's own transform, which every pose keyframe already owns — and
   flipping the BODY rather than the drawing inside it carries the overlays
   along, so the armful of fruit mirrors into the other paw instead of staying
   put while the animal turns away from it. */
.creature.flip .creature-body { transform: scaleX(-1); }
/* Asleep: a slow breath, and no reaction to anything. */
.creature.asleep { animation: breathe 3.6s ease-in-out infinite; }
@keyframes breathe {
  0%, 100% { transform: translateX(-50%) scale(1); }
  50% { transform: translateX(-50%) scale(1.035); }
}
.sleep-bubbles { animation: sleep-rise 3.4s ease-in-out infinite; }
@keyframes sleep-rise {
  0%   { opacity: 0; transform: translateY(6px); }
  30%  { opacity: 0.9; }
  100% { opacity: 0; transform: translateY(-16px); }
}

/* Fruit dropping out of the tree. The whole crop falls at once and is picked
   straight up into the cart, so everything on the ground is always new.

   Each one is three nested elements — see svgFallen(). The throw into the cart
   moves x on `.fallen-fruit` and y on `.fallen-lift`, both from JS, so that the
   two can carry different easing and the path is an arc rather than a chord.
   `.fallen-shadow` deliberately does NOT lift: it stays on the soil and fades,
   which is what makes the fruit read as leaving the ground. */
.fallen-fruit.drop {
  animation: fruit-drop 700ms cubic-bezier(0.3, 1.5, 0.5, 1) backwards;
}
@keyframes fruit-drop {
  0%   { transform: translateY(-320px); opacity: 0; }
  35%  { opacity: 1; }
  70%  { transform: translateY(0); }
  85%  { transform: translateY(-26px); }
  100% { transform: translateY(0); }
}

/* Fruit counters, top centre — one per fruit, all three there from the start so
   the whole run has a visible shape. The plot being grown is the lit one; the
   others stay faint until they have something in them. */
.apple-counter {
  position: absolute; top: 14px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 4px;
  background: rgba(255,255,255,0.82); border-radius: 999px;
  padding: 4px 10px; z-index: 40;
  box-shadow: 0 2px 0 rgba(74,52,24,0.12);
}
.fruit-chip {
  display: flex; align-items: center; gap: 4px;
  padding: 2px 7px; border-radius: 999px;
  transition: background 300ms ease, opacity 300ms ease;
}
.fruit-chip.empty { opacity: 0.42; }
.fruit-chip.active { background: rgba(224,123,92,0.16); opacity: 1; }
.apple-icon { width: 24px; height: 24px; display: block; }
.apple-counter svg { width: 100%; height: 100%; display: block; }
.fruit-count {
  font-size: 19px; font-weight: 700; color: var(--ink);
  font-variant-numeric: tabular-nums; line-height: 1;
}
/* The three crop pips that used to sit beside the fruit counts are gone — one
   more thing to read on a screen that is deliberately text-free, for progress
   the cart's rising pile already shows. In git if they are ever wanted back. */
/* The chip scales, not the bar — the bar is centred with its own transform, and
   animating that fights the centring. */
.fruit-chip.bump { animation: counter-bump 520ms cubic-bezier(0.34, 1.6, 0.5, 1); }
@keyframes counter-bump {
  0%, 100% { transform: scale(1); }
  40% { transform: scale(1.24); }
}

/* ---------- The finish ----------
   The only screen in the game with a sentence on it. It is aimed at the adult
   and the older child; the row of fruit totals and the falling fruit carry it
   for the one who cannot read. */
.confetti { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 60; }
.confetti[hidden] { display: none; }
.confetti i {
  position: absolute; top: -14%; display: block;
  animation-name: confetti-fall; animation-timing-function: linear;
  animation-iteration-count: infinite;
}
.confetti i svg { width: 100%; height: auto; display: block; }
@keyframes confetti-fall {
  0%   { transform: translateY(0) rotate(0deg); opacity: 0; }
  8%   { opacity: 1; }
  90%  { opacity: 1; }
  100% { transform: translateY(118vh) rotate(420deg); opacity: 0; }
}

.finale {
  position: absolute; inset: 0; z-index: 70;
  display: flex; align-items: center; justify-content: center;
  pointer-events: none;   /* a stray touch must never read as a card tap */
}
.finale[hidden] { display: none; }
.finale-card {
  background: rgba(255,255,255,0.93); border-radius: 26px;
  padding: 20px 26px 22px; text-align: center; min-width: 210px;
  box-shadow: 0 6px 0 rgba(74,52,24,0.14);
  animation: finale-in 620ms cubic-bezier(0.34, 1.5, 0.5, 1);
}
@keyframes finale-in {
  0%   { transform: scale(0.7); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}
.finale-fruits { display: flex; gap: 12px; justify-content: center; margin-bottom: 6px; }
.finale-fruit { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.finale-fruit svg { width: 30px; height: 30px; display: block; }
.finale-fruit span {
  font-size: 14px; font-weight: 700; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.finale-total {
  margin: 4px 0 2px; font-size: 52px; font-weight: 800; color: var(--ink);
  font-variant-numeric: tabular-nums; line-height: 1;
}
.finale-best { margin: 0 0 8px; font-size: 12px; color: rgba(74,52,24,0.6); }
.finale-best[hidden] { display: none; }
.finale-restart { margin: 0; font-size: 14px; color: var(--ink); }

/* Lock indicator — lock used to be completely invisible. */
.lock-indicator {
  position: absolute; top: 14px; right: 56px; width: 30px; height: 30px;
  opacity: 0.9; z-index: 40;
}
.lock-indicator svg { width: 100%; height: 100%; display: block; }
.creature.bounce { animation: bounce 700ms ease; }
.creature.shrug { animation: shrug 700ms ease; }
/* Every creature keyframe MUST carry the translateX(-50%) that centres it on
   its `left` position — transform is one property, so omitting it here snaps
   the creature half a body-width sideways the moment it reacts to anything. */
@keyframes bounce {
  0%, 100% { transform: translateX(-50%) translateY(0) scale(1); }
  35% { transform: translateX(-50%) translateY(-14px) scale(1.04); }
  70% { transform: translateX(-50%) translateY(0) scale(0.98); }
}
/* Looking up at an empty sky. Must not read as the shrug — that one means
   "I don't know this card", and the child has to be able to tell them apart. */
.creature.look-up { animation: look-up 1600ms ease; }
@keyframes look-up {
  0% { transform: translateX(-50%) rotate(0) translateY(0); }
  18%, 78% { transform: translateX(-50%) rotate(-9deg) translateY(-5px); }
  100% { transform: translateX(-50%) rotate(0) translateY(0); }
}
/* Soaked: shaking the water off, like a dog. */
.creature.shake { animation: shake 620ms ease 3; }
@keyframes shake {
  0%, 100% { transform: translateX(-50%) rotate(0); }
  20% { transform: translateX(calc(-50% - 6px)) rotate(-6deg); }
  45% { transform: translateX(calc(-50% + 6px)) rotate(6deg); }
  70% { transform: translateX(calc(-50% - 4px)) rotate(-4deg); }
  88% { transform: translateX(calc(-50% + 4px)) rotate(4deg); }
}

/* Styles for the two parked indicators. The markup is commented out in
   index.html; these stay so uncommenting it is the only step needed. */
.listen-indicator { position: absolute; top: 14px; left: 14px; width: 26px; height: 26px; opacity: 0.85; }
.listen-indicator svg { width: 100%; height: 100%; }
.pulse-ring { fill: none; stroke: var(--c-coral); stroke-width: 2; animation: pulse 2s ease-out infinite; transform-origin: center; }
.pulse-dot { fill: #fff; opacity: 0.9; }
@keyframes pulse {
  0% { r: 5; opacity: 0.9; }
  100% { r: 11; opacity: 0; }
}

.hidden-zone { position: absolute; top: 0; right: 0; width: 70px; height: 70px; background: transparent; border: none; padding: 0; }

@keyframes shrug {
  0%, 100% { transform: translateX(-50%) rotate(0deg); }
  25% { transform: translateX(-50%) rotate(-7deg) translateY(-3px); }
  75% { transform: translateX(-50%) rotate(7deg) translateY(-3px); }
}

.stage-dots { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); display: flex; gap: 10px; }
.stage-dots span { width: 10px; height: 10px; border-radius: 50%; background: rgba(90,70,50,0.22); box-shadow: 0 0 0 2px rgba(255,255,255,0.45); }
.stage-dots span.filled { background: #F6E3CE; }

/* `display:flex` beats the UA rule for the `hidden` attribute, so without this
   the admin "hide test buttons" toggle sets hidden and nothing happens. Same
   trap applies to any flex element that is toggled with `hidden`. */
.debug-panel[hidden], .apple-counter[hidden] { display: none; }
/* Above the finale overlay, or the test buttons become unreachable exactly when
   they are needed to prove the restart works. */
.debug-panel { position: absolute; bottom: 14px; right: 14px; display: flex; gap: 8px; z-index: 80; }
/* Desktop dev path: with no NFC these buttons stand in for card taps, so they
   cannot sit on top of the intro bubble. Clear of the top-right corner too —
   that one belongs to the five-tap gesture. */
#intro:not([hidden]) ~ .debug-panel { bottom: auto; top: 45%; flex-direction: column; }
.debug-panel button {
  width: 40px; height: 40px; border-radius: 50%; border: none;
  background: rgba(255,255,255,0.85); font-size: 18px; cursor: pointer;
}

/* PIN */
.pin-card { background: var(--c-cream); border-radius: 20px; padding: 24px; width: 260px; text-align: center; }
.pin-dots { font-size: 22px; letter-spacing: 8px; margin: 0 0 16px; color: var(--ink); }
.pin-pad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.pin-pad button {
  padding: 14px 0; border-radius: 12px; border: none; background: #fff;
  font-size: 18px; color: var(--ink); cursor: pointer;
}
.pin-pad button:active { background: var(--c-mustard); }
.pin-cancel, .pin-back { font-size: 13px !important; background: transparent !important; color: var(--ink); }

/* ADMIN */
.admin-card {
  background: var(--c-cream); border-radius: 20px; padding: 24px;
  width: 300px; max-height: 80vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: 10px;
}
/* Everything in the admin screens is centred — headings, buttons and field
   labels alike. It is a stack of full-width controls, and a ragged left edge
   down a centred card read as an unfinished form. */
.admin-card h1 { font-size: 18px; margin: 0 0 6px; color: var(--ink); text-align: center; }
.admin-btn {
  padding: 12px 16px; border-radius: 12px; border: none; background: #fff;
  font-size: 15px; text-align: center; color: var(--ink); cursor: pointer;
}
.admin-exit { background: transparent; color: var(--ink); margin-top: 4px; }
/* The one coloured button on a screen is the one the parent came to press.
   That is Save — never Delete, which sits in white with everything else. */
.admin-accent { background: var(--c-coral); color: #fff; font-weight: 600; }

.confirm-message {
  margin: 0 0 6px; font-size: 14px; line-height: 1.5;
  color: rgba(74,52,24,0.75); text-align: center;
}

.card-list { display: flex; flex-direction: column; gap: 8px; max-height: 260px; overflow-y: auto; }
/* Centred like everything else here. The chevron comes out of the flow rather
   than sharing the row, so it cannot pull the label off centre. */
.card-row {
  position: relative;
  border: 1px solid rgba(74,52,24,0.15); border-radius: 10px; padding: 10px 26px;
  display: flex; justify-content: center; align-items: center; cursor: pointer;
  text-align: center;
}
.card-row > span { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); }
.card-row .card-meta p { margin: 0; }
.card-row .card-name { font-size: 14px; font-weight: 600; }
.card-row .card-action { font-size: 12px; color: rgba(74,52,24,0.6); margin-top: 2px; }

.tap-zone {
  border: 2px dashed rgba(74,52,24,0.3); border-radius: 12px; padding: 16px;
  text-align: center; font-size: 13px; color: rgba(74,52,24,0.7);
}
.tap-zone.listening { border-color: var(--c-leaf); color: var(--ink); }

.field-label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--ink); text-align: center; }
.field-label input, .field-label select {
  padding: 10px; border-radius: 10px; border: 1px solid rgba(74,52,24,0.2); font-size: 15px;
  text-align: center;
}

.toast {
  position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
  background: var(--ink); color: #fff; padding: 10px 16px; border-radius: 10px;
  font-size: 13px; z-index: 999; max-width: 80vw; text-align: center;
}

@media (prefers-reduced-motion: reduce) {
  .pulse-ring { animation: none; opacity: 0.5; }
  .creature.shrug, .creature.bounce, .creature.look-up, .creature.shake,
  .creature.asleep, .sleep-bubbles, .layer-puddle.ripple,
  .petal-sway,
  .sun-rays, .scene-root.quake, .fruit-chip.bump,
  .fallen-fruit.drop, .scene-root.night .moon-glow,
  .cloud.nudge .puff, .finale-card { animation: none; }
  /* These two must still REACH their end state — a spent flower left sitting on
     top of its own fruit is not a reduced animation, it is a broken tree. */
  .bloom-out { animation: none; opacity: 0; }
  .fruit-in { animation: none; }
  .petal { animation-duration: 4s; }
  /* The confetti is the celebration — it stays, but slowly and without spin. */
  .confetti i { animation-duration: 6s !important; }
}

/* ---------- NFC diagnostics ---------- */
.diag-card { max-width: 620px; width: 92vw; }
.diag-checks { display: flex; flex-direction: column; gap: 2px; }
.diag-row {
  display: grid; grid-template-columns: 18px 1fr; grid-template-areas: "mark label" ". detail";
  gap: 0 6px; padding: 5px 0; border-bottom: 1px solid rgba(74,52,24,0.08); font-size: 13px;
}
.diag-mark { grid-area: mark; font-weight: 700; }
.diag-label { grid-area: label; font-weight: 600; }
.diag-detail {
  grid-area: detail; font-size: 11px; line-height: 1.35; color: rgba(74,52,24,0.7);
  word-break: break-word;
}
.diag-ok .diag-mark { color: var(--c-leaf); }
.diag-bad .diag-mark { color: var(--c-coral); }
.diag-bad .diag-detail { color: var(--ink-warm); }

.diag-actions { display: flex; gap: 8px; }
.admin-btn.diag-inline { flex: 1; margin: 0; font-size: 13px; padding: 10px 8px; }
.diag-hint { font-size: 11px; color: rgba(74,52,24,0.65); margin: 2px 0 0; line-height: 1.4; }
.diag-log {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; line-height: 1.5;
  white-space: pre-wrap; word-break: break-word; background: rgba(74,52,24,0.06);
  border-radius: 8px; padding: 8px; height: 130px; overflow-y: auto; user-select: text;
}

/* raw UID readout on the play screen, only while test mode is on */
.read-flash {
  position: absolute; top: 8px; left: 50%; transform: translateX(-50%);
  background: var(--ink); color: #fff; padding: 4px 10px; border-radius: 8px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px;
  opacity: 0; pointer-events: none; z-index: 50;
}
.read-flash.flash { animation: readflash 1.4s ease-out; }
@keyframes readflash {
  0% { opacity: 0; transform: translateX(-50%) translateY(-4px); }
  12% { opacity: 1; transform: translateX(-50%) translateY(0); }
  75% { opacity: 1; }
  100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .read-flash.flash { animation: none; opacity: 1; }
}
