/* ════════════════════════════════════════════════════════
   Bubbles — a counting toy for a three-year-old.

   The successor to Eggs (retired to V1). Same idea — count one
   at a time, then "how many in all" — but the beauty is light,
   motion and music, NOT drawn art. Translucent glass bubbles
   drift up a soft gradient; tap each to pop it with a ripple and
   a rising pentatonic note; clearing the set resolves the melody
   to a home chord and blooms the total as a numeral made of the
   same glass.

   Pure CSS/Canvas. Everything animates on transform/opacity only
   (compositor, 60fps). Sized in vmin so it lives in either iPad
   orientation. No fail, no timer, no reading, no menus.
   ════════════════════════════════════════════════════════ */

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
[hidden] { display: none !important; }

:root {
  --font: 'Fredoka', system-ui, -apple-system, sans-serif;
  /* Bubble diameter — huge, forgiving tap targets. */
  --d: clamp(116px, 19vmin, 210px);
  /* Default mood ("Dawn"); JS swaps these per round for variety. */
  --g1: #ffe9f2;  /* blush  */
  --g2: #ead9ff;  /* lilac  */
  --g3: #d6ecff;  /* sky    */
  --g4: #cff6f0;  /* seafoam*/
  --blob-warm: rgba(255, 246, 224, 0.14);
  --blob-cool: rgba(204, 230, 255, 0.16);
  --ink: #4a3b66;
}

html, body {
  width: 100%; height: 100%;
  overflow: hidden;
  font-family: var(--font);
  color: var(--ink);
  background: #e9e2f5;
  -webkit-user-select: none; user-select: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: none;
  overscroll-behavior: none;
}

/* ── Backdrop ──────────────────────────────────────────── */
.bg { position: fixed; inset: 0; z-index: 0; overflow: hidden; }

/* Two stacked gradient layers; JS crossfades opacity to change mood
   between rounds. Each is scaled up and slowly drifts its focal point. */
.bg-grad {
  position: absolute; inset: -8%;
  opacity: 0;
  transition: opacity 1600ms ease;
  background:
    radial-gradient(120% 95% at 30% 112%, var(--g1) 0%, var(--g2) 38%, var(--g3) 70%, var(--g4) 100%);
  animation: bgDrift 46s ease-in-out infinite alternate;
  will-change: transform;
}
.bg-grad.is-active { opacity: 1; }
@keyframes bgDrift {
  0%   { transform: scale(1.12) translate(-2%, 1%); }
  100% { transform: scale(1.16) translate(2%, -2%); }
}

/* Huge, soft, blurred light-pools that drift — the only thing that makes
   the empty screen feel alive. Screen-blended so they add light. */
.blob {
  position: absolute;
  width: 70vmin; height: 70vmin;
  border-radius: 50%;
  filter: blur(60px);
  mix-blend-mode: screen;
  will-change: transform;
}
.blob-1 { left: 4%;  top: 10%; background: radial-gradient(circle, var(--blob-warm), transparent 62%); animation: blobA 38s ease-in-out infinite alternate; }
.blob-2 { right: 0%; top: 38%; background: radial-gradient(circle, var(--blob-cool), transparent 62%); animation: blobB 46s ease-in-out infinite alternate; }
.blob-3 { left: 32%; bottom: -6%; width: 86vmin; height: 86vmin; background: radial-gradient(circle, rgba(255,255,255,0.10), transparent 60%); animation: blobC 52s ease-in-out infinite alternate; }
@keyframes blobA { from { transform: translate(0,0); } to { transform: translate(14vw, 10vh); } }
@keyframes blobB { from { transform: translate(0,0); } to { transform: translate(-12vw, 8vh); } }
@keyframes blobC { from { transform: translate(0,0) scale(1); } to { transform: translate(8vw, -6vh) scale(1.08); } }

/* A soft sun bloom off the top, and a barely-there vignette to seat depth. */
.sun {
  position: absolute; inset: 0;
  background: radial-gradient(circle at 78% -12%, rgba(255,255,255,0.55), transparent 46%);
  mix-blend-mode: soft-light;
}
.vignette {
  position: absolute; inset: 0;
  background: radial-gradient(140% 120% at 50% 45%, transparent 56%, rgba(80,60,120,0.12) 100%);
  transition: background 900ms ease;
}
/* During the cardinal "inhale" the periphery deepens so the number is alone. */
.bg.inhale .vignette { background: radial-gradient(120% 105% at 50% 45%, transparent 42%, rgba(70,52,110,0.22) 100%); }
.bg.inhale .blob { opacity: 0.5; }

/* ── Bubble layer (also the empty-tap catcher) ─────────── */
.bubble-layer { position: fixed; inset: 0; z-index: 10; }

/* Pop-particle canvas: above the bubbles, below ribbon/cardinal, tap-through. */
.fx { position: fixed; inset: 0; z-index: 20; pointer-events: none; }

/* The hit target is padded ~30% beyond the visible glass so chubby,
   imprecise fingers always connect. .bubble carries position (translate);
   .glass carries the look + breathe/spawn/pop (scale/opacity). */
.bubble {
  position: absolute; left: 0; top: 0;
  width: calc(var(--d) * 1.34);
  height: calc(var(--d) * 1.34);
  margin-left: calc(var(--d) * -0.67);
  margin-top: calc(var(--d) * -0.67);
  display: grid; place-items: center;
  will-change: transform;
  cursor: pointer;
}
.glass {
  position: relative;
  width: var(--d); height: var(--d);
  border-radius: 50%;
  /* Body: bright off-center keylight fading to near-clear, plus a faint
     per-bubble tint (--hue). Translucent so the gradient shows through. */
  background:
    /* sharp top-left keylight */
    radial-gradient(circle at 30% 26%, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0) 20%),
    /* small bottom-right rim-light reflection */
    radial-gradient(circle at 70% 76%, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0) 14%),
    /* bright Fresnel rim — thickens toward the edge, the #1 "hollow sphere" cue */
    radial-gradient(circle at 50% 50%, transparent 56%, rgba(255,255,255,0.55) 87%, rgba(255,255,255,0.12) 100%),
    /* faint hue body — mostly transparent so the background shows through */
    radial-gradient(circle at 50% 50%, hsla(var(--hue,320),85%,82%,0.05) 0%, hsla(var(--hue,320),80%,70%,0.16) 100%);
  /* Sphere shading: a touch of warm inset light, a cool inset shadow
     bottom-right, a hairline rim, and a soft cool (never black) drop shadow. */
  box-shadow:
    inset 6px 8px 18px rgba(255,255,255,0.28),
    inset -10px -12px 26px rgba(96,120,180,0.22),
    inset 0 0 0 1px rgba(255,255,255,0.35),
    0 16px 30px -12px rgba(70,90,150,0.30);
  /* Real refraction of the moving background — single cheap pass (3 passes
     over an always-animating backdrop × N bubbles was the dominant GPU cost).
     Feature-gated in JS: body.no-bdf disables it. The running spawn/breathe
     animations already promote this layer, so no explicit will-change. */
  -webkit-backdrop-filter: blur(1.5px);
  backdrop-filter: blur(1.5px);
  transform-origin: center center;
  /* --bs = per-bubble base scale (size variety). Spawn inflates, then a
     subtle idle breathe takes over (sequenced via animation-delay). All on
     the GPU; the outer .bubble carries position so the two never conflict. */
  --bs: 1;
  transform: scale(var(--bs));
  animation: spawnIn 0.52s cubic-bezier(0.34,1.56,0.64,1) both,
             breathe 3.6s ease-in-out 0.52s infinite;
}
@keyframes spawnIn {
  0%   { transform: scale(calc(var(--bs) * 0.3)); opacity: 0; }
  60%  { opacity: 1; }
  100% { transform: scale(var(--bs)); opacity: 1; }
}
@keyframes breathe {
  0%, 100% { transform: scale(var(--bs)); }
  50%      { transform: scale(calc(var(--bs) * 1.025)); }
}
.glass.pop {
  animation: popBurst 0.30s cubic-bezier(0.2,0.8,0.2,1) forwards;
}
@keyframes popBurst {
  0%   { transform: scale(calc(var(--bs) * 0.94)); opacity: 1; filter: blur(0); }
  100% { transform: scale(calc(var(--bs) * 1.5)); opacity: 0; filter: blur(5px); }
}
/* Fallback when backdrop-filter is unavailable (older/Low-Power iPad): keep the
   Fresnel rim + sphere shading (neither needs backdrop-filter), just bump the
   body tint a notch so it still reads as glass — never a flat opaque disc. */
body.no-bdf .glass {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  background:
    radial-gradient(circle at 30% 26%, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0) 20%),
    radial-gradient(circle at 70% 76%, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0) 14%),
    radial-gradient(circle at 50% 50%, transparent 56%, rgba(255,255,255,0.6) 87%, rgba(255,255,255,0.14) 100%),
    radial-gradient(circle at 50% 50%, hsla(var(--hue,320),85%,82%,0.10) 0%, hsla(var(--hue,320),80%,70%,0.26) 100%);
}
/* Specular pinpoint — the wet catchlight that sells "sphere not disc".
   Position jittered per bubble (--sx/--sy) so a clutch isn't a stamped repeat. */
.glass::before {
  content: ''; position: absolute; top: var(--sy, 13%); left: var(--sx, 19%);
  width: 30%; height: 22%; border-radius: 50%;
  background: radial-gradient(circle at 50% 50%, rgba(255,255,255,0.95), rgba(255,255,255,0) 70%);
  filter: blur(1px); transform: rotate(-18deg);
}
/* Iridescent soap-film rim — a soft luminous ring (cheap radial, no conic/mask/
   spin), hue-shifted per bubble so no two sheens match. */
.glass::after {
  content: ''; position: absolute; inset: 0; border-radius: 50%;
  background: radial-gradient(circle,
    transparent 64%,
    hsla(calc(var(--hue,320) - 45), 95%, 80%, 0.30) 74%,
    hsla(calc(var(--hue,320) + 55), 95%, 82%, 0.32) 86%,
    hsla(var(--hue,320), 95%, 90%, 0.10) 94%,
    transparent 100%);
  mix-blend-mode: screen; opacity: 0.85;
}

/* ── Count ribbon (pips) ───────────────────────────────── */
.ribbon {
  position: fixed; left: 50%; bottom: 5.5%;
  transform: translateX(-50%);
  z-index: 25;
  display: flex; gap: clamp(8px, 1.6vmin, 16px);
  align-items: center;
  pointer-events: none;
}
.pip {
  width: clamp(14px, 2.4vmin, 24px);
  height: clamp(14px, 2.4vmin, 24px);
  border-radius: 50%;
  background: radial-gradient(circle at 36% 32%, rgba(255,255,255,0.95), hsla(var(--hue,320),85%,72%,0.5) 70%, transparent);
  box-shadow: 0 2px 8px rgba(80,60,130,0.18), inset 0 0 0 1px rgba(255,255,255,0.5);
  opacity: 0.72;
  /* each new pip flares as it's born (the count LANDS in real time) then settles */
  animation: pipIn 460ms cubic-bezier(0.34,1.56,0.64,1) both;
}
@keyframes pipIn {
  0%   { transform: scale(0) translateY(8px); opacity: 0; }
  45%  { transform: scale(1.45); opacity: 1; }
  100% { transform: scale(1) translateY(0); opacity: 0.72; }
}
.pip.flare { animation: pipFlare 520ms cubic-bezier(0.34,1.56,0.64,1); }
@keyframes pipFlare {
  0% { transform: scale(1); opacity: 0.55; }
  40% { transform: scale(1.5); opacity: 1; box-shadow: 0 0 18px 4px rgba(255,255,255,0.7); }
  100% { transform: scale(1); opacity: 0.85; }
}

/* ── Cardinal numeral (the climax) ─────────────────────── */
.cardinal {
  position: fixed; inset: 0; z-index: 30;
  display: grid; place-items: center;
  pointer-events: none;
}
.cardinal span {
  font-weight: 800;
  font-size: 42vmin;
  line-height: 0.8;
  /* Glassy: a TRANSLUCENT fill (the drifting bg reads through it) + a bright
     white rim (text-stroke, the Fresnel edge) + a tight white core glow and a
     wide hue-tinted bloom + a real soft shadow — carved from the same light as
     the bubbles, not flat ink. */
  color: hsla(var(--card-hue, 320), 80%, 94%, 0.30);
  -webkit-text-stroke: 2px rgba(255,255,255,0.55);
  text-shadow:
    0 0 4px rgba(255,255,255,0.85),
    0 0 18px hsla(var(--card-hue, 320), 95%, 85%, 0.55),
    0 0 60px hsla(var(--card-hue, 320), 90%, 78%, 0.32),
    0 16px 36px rgba(70,50,110,0.28);
  animation: cardinalIn 620ms cubic-bezier(0.34,1.56,0.64,1) both;
}
@keyframes cardinalIn {
  0% { transform: scale(0.3); opacity: 0; }
  60% { transform: scale(1.1); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
.cardinal.out span { animation: cardinalOut 600ms ease forwards; }
@keyframes cardinalOut {
  to { transform: translateY(-7vmin) scale(1.04); opacity: 0; filter: blur(4px); }
}

/* Floating per-pop count numeral (rises off the burst point). JS creates these. */
.count-float {
  position: fixed; z-index: 26;
  font-weight: 800; font-size: clamp(40px, 9vmin, 96px);
  color: hsla(var(--cf-hue, 320), 90%, 97%, 0.95);
  text-shadow: 0 0 10px hsla(var(--cf-hue, 320), 95%, 85%, 0.7), 0 4px 14px rgba(80,60,130,0.35);
  pointer-events: none;
  animation: countFloat 900ms cubic-bezier(0.2,0.8,0.2,1) forwards;
}
@keyframes countFloat {
  0% { transform: translate(-50%,-50%) scale(0.6); opacity: 0; }
  30% { transform: translate(-50%,-58%) scale(1.1); opacity: 1; }
  100% { transform: translate(-50%,-110%) scale(1); opacity: 0; }
}

/* ── HUD ───────────────────────────────────────────────── */
.hud {
  position: fixed; top: 0; left: 0; right: 0; z-index: 40;
  height: 60px; display: flex; align-items: center; justify-content: space-between;
  padding: 12px 14px; pointer-events: none;
}
.hud > * { pointer-events: auto; }
.hud-back {
  width: 46px; height: 46px; display: grid; place-items: center;
  border-radius: 50%; background: rgba(255,255,255,0.82); color: var(--ink);
  font-size: 24px; font-weight: 800; text-decoration: none;
  box-shadow: 0 4px 14px rgba(76,52,28,0.16);
  transition: transform 120ms ease, background 120ms ease;
}
.hud-back:active { transform: scale(0.92); background: #fff; }
.profile-chip { }

@media (prefers-reduced-motion: reduce) {
  .bg-grad, .blob, .glass, .glass::after { animation: none !important; }
  .glass { transform: scale(var(--bs)); opacity: 1; }
}
