/* ---------- Reset ---------- */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html, body {
  height: 100%;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  color: #222;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: 24px;

  /* ---------- Retro CRT pastel background ----------
     Same four static, CSS-only layers as the main PUPU app's
     style.css (grain, scanlines, vignette, soft radial glow) so the
     MVP shares the identical background instead of a re-derived one. */
  background-color: #fdf6ec;
  background-image:
    repeating-radial-gradient(
      circle at 0 0,
      rgba(40, 30, 60, 0.035) 0,
      rgba(40, 30, 60, 0.035) 1px,
      transparent 1px,
      transparent 2px
    ),
    repeating-linear-gradient(
      to bottom,
      rgba(30, 20, 50, 0.025) 0,
      rgba(30, 20, 50, 0.025) 1px,
      transparent 1px,
      transparent 3px
    ),
    radial-gradient(
      ellipse 120% 120% at 50% 50%,
      transparent 55%,
      rgba(60, 40, 80, 0.07) 100%
    ),
    radial-gradient(
      ellipse 1000px 780px at 50% 42%,
      #e7fbfa 0%,
      #e6e3fa 32%,
      #fbe1ef 58%,
      #fdecd9 82%,
      #fdf6ec 100%
    );
  background-repeat: repeat, repeat, no-repeat, no-repeat;
  background-position: 0 0, 0 0, center, center;
  background-size: auto, auto, 100% 100%, 100% 100%;
}

/* ---------- Ghost PUPU spooky-mode overlay ----------
   A temporary full-viewport tint, NOT a change to body's own
   background above -- toggled by app.js's maybeShowGhost() via the
   .ghost-overlay-active class. position:fixed + z-index:-1 as a
   direct child of <body> (see index.html) places it behind body's own
   background/border (tier 1 of the stacking order) is impossible for
   a child, but ahead of it -- and behind every other (static, tier 3)
   piece of UI in .container, since z-index:-1 descendants of body
   paint before any non-negative-z-index content regardless of DOM
   order. Fully transparent and inert until the class is added. */
.ghost-overlay {
  position: fixed;
  inset: 0;
  z-index: -1;
  background-color: #2a1145;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.6s ease;
}

.ghost-overlay.ghost-overlay-active {
  opacity: 1;
}

/* ---------- Layout ---------- */
.container {
  width: 100%;
  max-width: 420px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 32px;
  text-align: center;
}

/* ---------- Hero (speech bubble + PUPU) ----------
   Column by default, so the bubble (first in the DOM) sits above
   PUPU on narrow/mobile screens. The desktop media query below is
   the one responsive change on top of the restored pastel visuals:
   it switches this to a row so the bubble sits beside PUPU instead. */
.hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 28px;
  width: 100%;
}

/* ---------- Speech bubble ----------
   Same gradient/border/shadow/tail treatment as .speech-bubble in
   the main app's style.css, adapted to this app's existing
   .bubble/.beat markup. */

/* Thin, non-visual wrapper around .bubble -- exists only so the
   language toggle (see .language-toggle below) has somewhere to live
   that survives renderCard() clearing #bubble's innerHTML on every
   card, while still visually reading as "attached to the bubble".
   Sized identically to .bubble itself (same width/max-width, including
   the desktop override further down) so wrapping it changes nothing
   about the bubble's own layout. */
.bubble-wrap {
  position: relative;
  width: 100%;
  max-width: 320px;
}

.bubble {
  position: relative;
  width: 100%;
  max-width: 320px;
  background: linear-gradient(160deg, #e8fff2 0%, #f2fbf7 45%, #fcf6ff 100%);
  border: 1.5px solid #b8e8d6;
  border-radius: 20px;
  padding: 18px 24px;
  box-shadow: 0 4px 16px rgba(120, 100, 160, 0.1);
  display: flex;
  flex-direction: column;
  justify-content: center;
  cursor: pointer;
  transition: transform 0.08s ease;
}

/* Gentle hover/press feedback -- the bubble itself is now the tap
   target (see handleBubbleAdvance() in app.js), so it needs the same
   kind of affordance a button would have, without looking like one. */
.bubble:hover {
  transform: scale(1.01);
}

.bubble:active {
  transform: scale(0.98);
}

.bubble:focus-visible {
  outline: 2px solid #b8e8d6;
  outline-offset: 3px;
}

/* Applied while a section has finished typing and is waiting to
   auto-advance -- a slow, subtle pulse hints the bubble can be tapped
   right now instead of waiting out the full 10s. Removed the instant
   a new section starts typing (see startTypingSection() in app.js). */
@keyframes bubble-waiting-pulse {
  0%, 100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.015);
  }
}

.bubble.bubble-waiting {
  animation: bubble-waiting-pulse 2.2s ease-in-out infinite;
}

.bubble .beat {
  margin: 0;
  font-size: 0.98rem;
  font-weight: 600;
  line-height: 1.4;
  color: #333333;
  text-align: center;
}

/* ---------- Speech bubble sections ----------
   Splits a card into two scannable beats -- the fact, then the
   mission -- instead of one long run of text. Each section gets its
   own playful emoji label and an accent colour; a dashed rule plus
   vertical rhythm (not a bigger bubble) is what keeps them feeling
   separate during a live lesson. */
/* Sections are inserted into the DOM one at a time, staggered by
   renderCard() in app.js, rather than all at once -- so this plays
   automatically on each fresh .bubble-section as it appears. Slow and
   gentle on purpose, so it reads as PUPU thinking rather than a UI
   transition. The 1.8s duration below must match app.js's
   SECTION_FADE_DURATION_MS constant (kept in sync manually). */
@keyframes bubble-section-reveal {
  0% {
    opacity: 0;
    transform: translateY(6px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

.bubble-section {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 10px 0;
  animation: bubble-section-reveal 1.8s ease-out;
}

.bubble-section:first-child {
  padding-top: 2px;
}

.bubble-section:last-child {
  padding-bottom: 2px;
}

.bubble-section + .bubble-section {
  border-top: 1.5px dashed rgba(120, 100, 160, 0.25);
  padding-top: 14px;
}

.bubble-label {
  margin: 0 0 2px;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-align: center;
  color: #5fae8f;
}

.bubble-section-mission .bubble-label {
  color: #8b7fd9;
}

/* ---------- Language toggle ----------
   Small pill attached to the bubble's top-right corner, sitting
   slightly on top of it like a tab -- deliberately tiny/quiet so it
   reads as part of the card rather than a separate tool. Built once at
   startup from SUPPORTED_LANGUAGES in app.js (see buildLanguageToggle())
   -- adding a language later is a data change there, not a style change
   here, since every button shares these same rules regardless of which
   language it represents. */
.language-toggle {
  position: absolute;
  top: -10px;
  right: 10px;
  display: flex;
  align-items: center;
  gap: 5px;
  background: #ffffff;
  border: 1.5px solid #b8e8d6;
  border-radius: 999px;
  padding: 3px 6px;
  box-shadow: 0 2px 6px rgba(120, 100, 160, 0.12);
  z-index: 1;
}

.lang-btn {
  font: inherit;
  font-size: 0.66rem;
  font-weight: 700;
  line-height: 1;
  color: #9a9a9a;
  background: none;
  border: none;
  border-radius: 999px;
  padding: 3px 6px;
  cursor: pointer;
  white-space: nowrap;
}

.lang-btn:hover {
  color: #5fae8f;
}

/* The active language is the one visibly "on" -- filled pill, darker
   text -- so it's obvious at a glance which version is showing. */
.lang-btn.lang-active {
  background: #e8fff2;
  color: #3f8a6d;
}

.lang-sep {
  color: #cccccc;
  font-size: 0.6rem;
}

/* Inert (not hidden) while a section is actively typing, so switching
   language can never race the typewriter effect -- see setLanguage()
   in app.js. */
.language-toggle.language-toggle-disabled {
  opacity: 0.45;
  pointer-events: none;
}

/* Restored from the main app's .speech-bubble.pupu-inflate reaction --
   plays when app.js shows a new card, giving the bubble a small pop. */
.bubble.pupu-inflate {
  animation: pupu-bubble-inflate 0.5s ease-out;
}

@keyframes pupu-bubble-inflate {
  0% {
    transform: scale(1);
  }
  40% {
    transform: scale(1.12);
  }
  100% {
    transform: scale(1);
  }
}

/* Tail points down at PUPU by default (mobile: bubble above PUPU). */
.bubble::after {
  content: "";
  position: absolute;
  bottom: -10px;
  left: 50%;
  transform: translateX(-50%);
  border-width: 10px 10px 0 10px;
  border-style: solid;
  border-color: #e8fff2 transparent transparent transparent;
}

/* ---------- PUPU stage ----------
   Restored from the main app's .pupu-stage: holds the static shadow
   (bottom layer) and the animated PUPU circle (top layer). Fixed at
   232px (matching the earlier size-increase request) rather than the
   main app's viewport-relative sizing. */
.pupu-stage {
  position: relative;
  width: 232px;
  height: 232px;
}

/* Shadow sits underneath and never animates -- it shares the same
   canvas alignment as body.png, so it just stays put while PUPU
   floats/breathes above it. */
.pupu-shadow {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
  user-select: none;
  transition: opacity 0.45s ease;
}

/* .pupu-float-wrap (new -- not in the main app, see the earlier size/
   float request) and .pupu-circle (restored breathing wrapper) keep
   the floating (position) and breathing (scale) animations on
   separate elements, since two animations can't both drive
   `transform` on the same element at once. */
.pupu-float-wrap {
  position: absolute;
  inset: 0;
  animation: pupu-float 4.4s ease-in-out infinite;
}

.pupu-circle {
  position: absolute;
  inset: 0;
  animation: pupu-breathe 3.2s ease-in-out infinite;
  transform-origin: center;
}

.pupu-body {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
  user-select: none;
  transition: opacity 0.45s ease;
}

/* Ghost PUPU shares .pupu-body's positioning/sizing above (same
   canvas, see index.html) but stays fully transparent until
   app.js's maybeShowGhost() adds .pupu-body-ghost-visible, and gets
   its own (slightly different) fade timing plus a subtle bob while
   visible -- both independent of .pupu-body's own opacity transition
   above since this is a separate element. */
.pupu-body-ghost {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 0.5s ease;
}

.pupu-body-ghost.pupu-body-ghost-visible {
  opacity: 1;
  animation: pupu-ghost-bob 2.4s ease-in-out infinite;
}

@keyframes pupu-ghost-bob {
  0%, 100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-6px);
  }
}

/* Shared "fade out for the ghost event" toggle -- applied to
   .pupu-shadow and the normal-PUPU group (body/arms/eyes/mouth/
   button) by app.js's maybeShowGhost(). Each of those elements
   already has its own transition/opacity handling above (or, for
   .pupu-button, further below), so this class only needs to set the
   target opacity; the transition timing comes from each element's own
   rule. */
.pupu-ghost-hidden {
  opacity: 0;
}

/* ---------- PUPU arms ----------
   left_arm.png/right_arm.png share body.png's canvas, so they line up
   automatically. transform-origin is pinned to each arm's shoulder
   point (measured from the source artwork) so rotation reads as
   swinging from the shoulder rather than spinning the whole limb. */
.pupu-arm {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
  user-select: none;
  transition: opacity 0.45s ease;
}

.pupu-arm-left {
  transform-origin: 82.3% 57.4%;
}

.pupu-arm-right {
  transform-origin: 13.2% 58.0%;
}

/* ---------- PUPU eyes ----------
   Open/closed blink state is a straight image swap done in app.js. */
.pupu-eye {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
  user-select: none;
  transform-origin: center;
  transition: opacity 0.45s ease;
}

/* ---------- PUPU mouth ----------
   Expression (normal/smile/blow/oh/wide) is a straight image swap
   done in app.js -- no animation of its own. */
.pupu-mouth {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
  user-select: none;
  transition: opacity 0.45s ease;
}

/* ---------- PUPU visual-effects layers (hat / item / effect) ----------
   Reusable overlay layers -- same full-bleed/shared-canvas convention
   as every layer above, controlled generically via app.js's
   setLayer()/clearLayer()/showLayerTemporarily() rather than one-off
   code per asset. All three start with no `src` and are hidden by
   default (via .pupu-layer's display:none) until JS assigns one and
   reveals it, so an empty img never flashes a broken-image icon.
   Painted in DOM order above eyes/mouth so hats/items/effects sit
   visually on top of the face, but below .pupu-button so the belly
   stays clickable and visible at all times. Hat/item are independent
   elements/state from each other (and from effect), so any
   combination of the three can be visible at once. */
.pupu-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: none;
  user-select: none;
  display: none;
  transition: opacity 1.8s ease-out;
}

.pupu-layer.pupu-layer-visible {
  display: block;
}

/* Slow fade-out for effects that should gradually disperse (currently
   just effect_fart -- see fadeOutLayer() in app.js) instead of vanishing
   instantly like clearLayer()'s default. display:block stays on
   (.pupu-layer-visible isn't removed yet) so the opacity transition
   above is actually visible; clearLayer() removes both classes once the
   fade finishes. */
.pupu-layer.pupu-layer-fading {
  opacity: 0;
}

/* Hat-only grow/pop transform, driven by app.js's
   showHatWithAnimation() -- .pupu-hat is still just a .pupu-layer
   underneath (same position/sizing/hidden-by-default rules above),
   these two classes only add a transform+z-index animation on top,
   scoped to .pupu-hat specifically so items/effects stay plain
   instant show/hide. Only the hat <img> itself scales/moves -- PUPU's
   own body/circle transforms are completely untouched.

   Neither .pupu-body nor any other layer has an explicit z-index (all
   are position:absolute with z-index:auto, so they simply paint in
   DOM order -- body, arms, eyes, mouth, hat, item, effect, button).
   That's what lets a plain z-index:-1 here drop the hat behind every
   sibling including the body, and z-index:0 return it to its normal
   DOM-order slot (already above body/eyes/mouth, below item/effect/
   button) without needing to touch any other element's stacking. */
.pupu-hat.pupu-hat-growing {
  animation: pupu-hat-grow 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}

.pupu-hat.pupu-hat-popping {
  animation: pupu-hat-pop 0.3s ease-in forwards;
}

/* Rises up from behind/below PUPU (small, translated down, z-index
   behind the body) then crosses in front partway through as it
   finishes growing and settles with a small bounce. */
@keyframes pupu-hat-grow {
  0% {
    transform: scale(0) translateY(20%);
    z-index: -1;
  }
  50% {
    z-index: -1;
  }
  51% {
    z-index: 0;
  }
  70% {
    transform: scale(1.12) translateY(-4%);
  }
  100% {
    transform: scale(1) translateY(0);
    z-index: 0;
  }
}

/* Reverse of the above: a small bounce up while still in front, then
   drops behind the body as it shrinks and sinks back down out of
   sight, rather than shrinking away in front of PUPU. */
@keyframes pupu-hat-pop {
  0% {
    transform: scale(1) translateY(0);
    z-index: 0;
  }
  30% {
    transform: scale(1.12) translateY(-4%);
    z-index: 0;
  }
  45% {
    z-index: 0;
  }
  46% {
    z-index: -1;
  }
  100% {
    transform: scale(0) translateY(20%);
    z-index: -1;
  }
}

/* ---------- PUPU chest button ----------
   PUPU's belly is the only primary interaction (see index.html's
   role="button" tabindex="0" and app.js's click/keydown handlers).
   The resting transform + its transition live here (not on a
   "normal" class) so the broken-belly-button "jammed" effect can rely
   on plain CSS cascade/specificity for an instant press-in and a slow
   release. */
.pupu-button {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  pointer-events: auto;
  cursor: pointer;
  user-select: none;
  transform: translateY(0) scale(1);
  transition: transform 600ms ease-out, opacity 0.45s ease;
}

.pupu-button:focus-visible {
  outline: 2.5px solid #b8e8d6;
  outline-offset: 6px;
  border-radius: 50%;
}

/* Prevents pressing the belly button again mid-sequence. */
.pupu-button.pupu-button-disabled {
  pointer-events: none;
  cursor: default;
}

/* Broken belly button "jammed" press feel (see app.js's
   playBrokenButtonDud()). Adding this class presses the button in
   instantly -- its higher specificity overrides the base transition
   above with `transition: none`. Removing it lets the base rule ease
   it back out slowly over 600ms. */
.pupu-button.pupu-button-jammed {
  transform: translateY(4px) scale(0.94);
  transition: none;
}

/* ---------- Behaviour / event / gesture animations ----------
   Restored verbatim from the main app's style.css. Each class is
   applied via app.js and removed once its sequence step ends;
   .pupu-circle's continuous breathing above is overridden for as
   long as one of these is present, since only one `animation` value
   can apply at a time on the same element. */

@keyframes pupu-breathe {
  0% {
    transform: scale(1);
  }
  35% {
    transform: scale(1.04);
  }
  42% {
    transform: scale(1.04);
  }
  75% {
    transform: scale(1);
  }
  100% {
    transform: scale(1);
  }
}

@keyframes pupu-float {
  0%, 100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-6px);
  }
}

.pupu-circle.pupu-bounce {
  animation: pupu-bounce 0.6s ease-in-out;
}

.pupu-circle.pupu-bounce .pupu-arm-left {
  animation: pupu-arm-up-left 0.6s ease-in-out;
}

.pupu-circle.pupu-bounce .pupu-arm-right {
  animation: pupu-arm-up-right 0.6s ease-in-out;
}

@keyframes pupu-bounce {
  0% {
    transform: scale(1) translateY(0);
  }
  30% {
    transform: scale(1.08, 0.92) translateY(-14px);
  }
  55% {
    transform: scale(0.95, 1.05) translateY(4px);
  }
  75% {
    transform: scale(1.03, 0.97) translateY(-4px);
  }
  100% {
    transform: scale(1) translateY(0);
  }
}

.pupu-circle.pupu-sneeze {
  animation: pupu-sneeze 0.5s ease-in-out;
}

.pupu-circle.pupu-sneeze .pupu-arm-left {
  animation: pupu-arm-flick-left 0.5s ease-in-out;
}

.pupu-circle.pupu-sneeze .pupu-arm-right {
  animation: pupu-arm-flick-right 0.5s ease-in-out;
}

@keyframes pupu-sneeze {
  0%, 100% {
    transform: scale(1) translateY(0);
  }
  30% {
    transform: scale(0.94, 1.06) translateY(2px);
  }
  50% {
    transform: scale(1.1, 0.9) translateY(-6px);
  }
  70% {
    transform: scale(0.98, 1.02) translateY(2px);
  }
}

.pupu-circle.pupu-distracted {
  animation: pupu-distracted-body 0.7s ease-in-out;
}

@keyframes pupu-distracted-body {
  0%, 100% {
    transform: rotate(0deg);
  }
  30%, 70% {
    transform: rotate(5deg);
  }
}

.pupu-circle.pupu-excited {
  animation: pupu-excited 0.6s ease-in-out;
}

.pupu-circle.pupu-excited .pupu-arm-left {
  animation: pupu-arm-up-left 0.6s ease-in-out;
}

.pupu-circle.pupu-excited .pupu-arm-right {
  animation: pupu-arm-up-right 0.6s ease-in-out;
}

@keyframes pupu-excited {
  0%, 100% {
    transform: scale(1) rotate(0deg);
  }
  20% {
    transform: scale(1.06) rotate(-6deg);
  }
  40% {
    transform: scale(1.1) rotate(6deg);
  }
  60% {
    transform: scale(1.06) rotate(-4deg);
  }
  80% {
    transform: scale(1.08) rotate(4deg);
  }
}

.pupu-circle.pupu-sleepy {
  animation: pupu-sleepy 1.4s ease-in-out;
}

.pupu-circle.pupu-sleepy .pupu-arm-left {
  animation: pupu-arm-down-left 1.4s ease-in-out;
}

.pupu-circle.pupu-sleepy .pupu-arm-right {
  animation: pupu-arm-down-right 1.4s ease-in-out;
}

@keyframes pupu-sleepy {
  0% {
    transform: translateY(0) rotate(0deg);
  }
  40% {
    transform: translateY(10px) rotate(6deg);
  }
  70% {
    transform: translateY(12px) rotate(6deg);
  }
  100% {
    transform: translateY(0) rotate(0deg);
  }
}

.pupu-circle.pupu-thinking {
  animation: pupu-thinking-body 1s ease-in-out infinite;
}

@keyframes pupu-thinking-body {
  0%, 100% {
    transform: rotate(0deg) translateY(0);
  }
  50% {
    transform: rotate(-4deg) translateY(-2px);
  }
}

.pupu-circle.pupu-finish {
  animation: pupu-finish 0.35s ease-in-out;
}

@keyframes pupu-finish {
  0%, 100% {
    transform: scale(1) translateY(0);
  }
  40% {
    transform: scale(1.03) translateY(-4px);
  }
}

.pupu-circle.pupu-laugh {
  animation: pupu-laugh 0.6s ease-in-out;
}

.pupu-circle.pupu-laugh .pupu-arm-left {
  animation: pupu-arm-up-left 0.6s ease-in-out;
}

.pupu-circle.pupu-laugh .pupu-arm-right {
  animation: pupu-arm-up-right 0.6s ease-in-out;
}

@keyframes pupu-laugh {
  0%, 100% {
    transform: translateY(0) rotate(0deg);
  }
  15% {
    transform: translateY(-6px) rotate(-3deg);
  }
  30% {
    transform: translateY(0) rotate(3deg);
  }
  45% {
    transform: translateY(-6px) rotate(-3deg);
  }
  60% {
    transform: translateY(0) rotate(3deg);
  }
  80% {
    transform: translateY(-3px) rotate(0deg);
  }
}

/* ---------- Broken belly button (hidden behaviour) ---------- */
.pupu-circle.pupu-broken-payoff {
  animation: pupu-broken-payoff 0.9s ease-in-out;
}

@keyframes pupu-broken-payoff {
  0% {
    transform: scale(1);
    animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
  }
  22.2% {
    transform: scale(1.5);
  }
  61.1% {
    transform: scale(1.5);
  }
  100% {
    transform: scale(1);
  }
}

/* Broken-button payoff variants -- alternatives to the inflate above,
   randomly picked by BROKEN_BUTTON_PAYOFF_VARIANTS in app.js. Each one
   starts and ends at transform: scale(1)/rotate(0deg) (PUPU's normal
   appearance) so nothing can accumulate between repeated triggers, same
   as the inflate keyframe above. Durations here must match their
   durationMs counterpart in app.js. */
.pupu-circle.pupu-broken-payoff-shrink {
  animation: pupu-broken-payoff-shrink 0.7s cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes pupu-broken-payoff-shrink {
  0% {
    transform: scale(1);
  }
  35% {
    transform: scale(0.28);
  }
  60% {
    transform: scale(0.28);
  }
  100% {
    transform: scale(1);
  }
}

.pupu-circle.pupu-broken-payoff-spin {
  animation: pupu-broken-payoff-spin 0.8s cubic-bezier(0.45, 0, 0.55, 1);
}

@keyframes pupu-broken-payoff-spin {
  0% {
    transform: rotate(0deg);
  }
  85% {
    /* 1080deg = exactly 3 full rotations, renders identically to 0deg */
    transform: rotate(1080deg);
  }
  100% {
    transform: rotate(1080deg);
  }
}

.pupu-circle.pupu-broken-payoff-squash {
  animation: pupu-broken-payoff-squash 0.7s cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes pupu-broken-payoff-squash {
  0% {
    transform: scale(1, 1);
  }
  30% {
    transform: scale(1.5, 0.55);
  }
  60% {
    transform: scale(0.6, 1.45);
  }
  100% {
    transform: scale(1, 1);
  }
}

.pupu-circle.pupu-soft-wobble {
  animation: pupu-soft-wobble 0.5s ease-in-out;
}

@keyframes pupu-soft-wobble {
  0%, 100% {
    transform: rotate(0deg);
  }
  25% {
    transform: rotate(-4deg);
  }
  55% {
    transform: rotate(3deg);
  }
  80% {
    transform: rotate(-1deg);
  }
}

.pupu-circle.pupu-idle-brightness-pulse {
  animation: pupu-idle-brightness-pulse 0.5s ease-in-out;
}

@keyframes pupu-idle-brightness-pulse {
  0%, 100% {
    filter: brightness(1);
  }
  50% {
    filter: brightness(1.08);
  }
}

/* ---------- Between-bubble reactions ----------
   A separate small pool of short flourishes from the belly-press
   BEHAVIOURS above -- played via app.js's playBubbleReaction() when
   the child advances from box 1 to box 2, so PUPU feels like it's
   reacting to the interaction. Same plain-transform pattern as every
   animation above; nothing new architecturally. */

.pupu-circle.pupu-spin {
  animation: pupu-spin 0.7s ease-in-out;
}

@keyframes pupu-spin {
  0% {
    transform: rotate(0deg) scale(1);
  }
  60% {
    transform: rotate(360deg) scale(1.05);
  }
  100% {
    transform: rotate(360deg) scale(1);
  }
}

.pupu-circle.pupu-puff {
  animation: pupu-puff 0.9s ease-in-out;
}

@keyframes pupu-puff {
  0%, 100% {
    transform: scale(1);
  }
  40% {
    transform: scale(1.18);
  }
  70% {
    transform: scale(0.92);
  }
}

.pupu-circle.pupu-look-around {
  animation: pupu-look-around 0.8s ease-in-out;
}

@keyframes pupu-look-around {
  0%, 100% {
    transform: rotate(0deg);
  }
  25% {
    transform: rotate(-8deg);
  }
  60% {
    transform: rotate(8deg);
  }
  85% {
    transform: rotate(-3deg);
  }
}

.pupu-circle.pupu-yawn {
  animation: pupu-yawn 1.3s ease-in-out;
}

@keyframes pupu-yawn {
  0%, 100% {
    transform: scale(1, 1) translateY(0);
  }
  40%, 60% {
    transform: scale(1.05, 1.08) translateY(-4px);
  }
}

.pupu-circle.pupu-surprised {
  animation: pupu-surprised 0.55s ease-in-out;
}

@keyframes pupu-surprised {
  0% {
    transform: scale(1) translateY(0);
  }
  30% {
    transform: scale(1.15) translateY(-6px);
  }
  60% {
    transform: scale(0.97) translateY(1px);
  }
  100% {
    transform: scale(1) translateY(0);
  }
}

.pupu-circle.pupu-silly-dance {
  animation: pupu-silly-dance 1.4s ease-in-out;
}

@keyframes pupu-silly-dance {
  0%, 100% {
    transform: translateX(0) rotate(0deg);
  }
  15% {
    transform: translateX(-6px) rotate(-6deg);
  }
  30% {
    transform: translateX(6px) rotate(6deg);
  }
  45% {
    transform: translateX(-6px) rotate(-6deg);
  }
  60% {
    transform: translateX(6px) rotate(6deg);
  }
  75% {
    transform: translateX(-3px) rotate(-3deg);
  }
  90% {
    transform: translateX(3px) rotate(3deg);
  }
}

.pupu-circle.pupu-wake-up {
  animation: pupu-wake-up 0.7s ease-in-out;
}

@keyframes pupu-wake-up {
  0% {
    transform: translateY(10px) rotate(6deg) scale(1);
  }
  50% {
    transform: translateY(-8px) rotate(-4deg) scale(1.08);
  }
  100% {
    transform: translateY(0) rotate(0deg) scale(1);
  }
}

.pupu-circle.pupu-exaggerated-float {
  animation: pupu-exaggerated-float 1.2s ease-in-out;
}

@keyframes pupu-exaggerated-float {
  0%, 100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-16px);
  }
}

/* ---------- PUPU arm keyframes ----------
   Shared by whichever behaviour/event triggers them. Every keyframe
   returns to rotate(0deg) by 100% -- these are brief, one-shot
   reactions, never a continuous idle motion. */
@keyframes pupu-arm-up-left {
  0%, 100% {
    transform: rotate(0deg);
  }
  50% {
    transform: rotate(-8deg);
  }
}

@keyframes pupu-arm-up-right {
  0%, 100% {
    transform: rotate(0deg);
  }
  50% {
    transform: rotate(8deg);
  }
}

@keyframes pupu-arm-down-left {
  0% {
    transform: rotate(0deg);
  }
  40% {
    transform: rotate(6deg);
  }
  70% {
    transform: rotate(6deg);
  }
  100% {
    transform: rotate(0deg);
  }
}

@keyframes pupu-arm-down-right {
  0% {
    transform: rotate(0deg);
  }
  40% {
    transform: rotate(-6deg);
  }
  70% {
    transform: rotate(-6deg);
  }
  100% {
    transform: rotate(0deg);
  }
}

@keyframes pupu-arm-flick-left {
  0%, 100% {
    transform: rotate(0deg);
  }
  30% {
    transform: rotate(10deg);
  }
}

@keyframes pupu-arm-flick-right {
  0%, 100% {
    transform: rotate(0deg);
  }
  30% {
    transform: rotate(-10deg);
  }
}

.status {
  display: none; /* dev/admin-only metadata (fact ID, engine, review status) -- kept updated in app.js for internal use, just not shown to students */
  margin: 0;
  font-size: 0.8rem;
  color: #999999;
  min-height: 1em;
}

.install-button {
  padding: 12px 24px;
  font-size: 0.95rem;
  font-weight: 600;
  border: 1.5px solid #b8e8d6;
  border-radius: 999px;
  background: linear-gradient(160deg, #e8fff2 0%, #f2fbf7 45%, #fcf6ff 100%);
  color: #333333;
  cursor: pointer;
  box-shadow: 0 4px 16px rgba(120, 100, 160, 0.1);
  transition: transform 0.08s ease, background 0.15s ease;
}

.install-button:hover {
  background: linear-gradient(160deg, #d9f7e8 0%, #e6f4ee 45%, #f5e9ff 100%);
}

.install-button:active {
  transform: scale(0.97);
}

/* ---------- Responsive: bubble beside PUPU on desktop ---------- */
@media (min-width: 700px) {
  .container {
    max-width: 640px;
    gap: 40px;
  }

  /* row-reverse keeps the bubble first in the DOM (so mobile's column
     layout still puts it above PUPU) while visually placing PUPU on
     the left and the bubble on the right. */
  .hero {
    flex-direction: row-reverse;
    justify-content: center;
    align-items: center;
    gap: 40px;
  }

  .bubble-wrap,
  .bubble {
    max-width: 280px;
  }

  /* Tail swings to point left at PUPU now that the bubble sits to PUPU's right. */
  .bubble::after {
    bottom: auto;
    right: auto;
    top: 50%;
    left: -10px;
    transform: translateY(-50%);
    border-width: 10px 10px 10px 0;
    border-color: transparent #e8fff2 transparent transparent;
  }
}
