/* =============================================================================
 * PIE CLICKER 2 -- animations.css
 * Keyframes and motion. Everything here is gated by the reduced-animation
 * settings applied to <body> so the whole game can be quietened at once.
 * ========================================================================== */

/* ------------------------------------------------------------ panels ----- */
@keyframes panelIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

/* ------------------------------------------------------------- the pie --- */
@keyframes pieSquash {
  0%   { transform: scale(1, 1); }
  22%  { transform: scale(1.075, .915); }
  55%  { transform: scale(.968, 1.042); }
  78%  { transform: scale(1.018, .988); }
  100% { transform: scale(1, 1); }
}
.pie.clicked { animation: pieSquash .22s var(--ease); }

@keyframes pieBreathe {
  0%, 100% { transform: translateY(0) scale(1); }
  50%      { transform: translateY(-5px) scale(1.008); }
}
body:not(.reduced-animations) .pie { animation: pieBreathe 4.6s ease-in-out infinite; }
/* The click squash must win over the idle breathing. */
body:not(.reduced-animations) .pie.clicked { animation: pieSquash .22s var(--ease); }

/* A slow turn while the player is not clicking. It rides on .pie-body, not on
   .pie, so it never fights the breathing and squash animations for `transform`.
   The animation runs from load and is only ever paused, so the pie keeps its
   angle between idle spells instead of snapping back to zero. */
@keyframes pieSpin { to { transform: rotate(360deg); } }
body:not(.reduced-animations) .pie-body {
  animation: pieSpin 60s linear infinite;
  animation-play-state: paused;
  will-change: transform;
}
body:not(.reduced-animations) .pie.idle .pie-body { animation-play-state: running; }
body.reduced-motion .pie-body { animation: none; }

@keyframes glowPulse {
  0%, 100% { opacity: .55; transform: scale(1); }
  50%      { opacity: .9;  transform: scale(1.05); }
}
body:not(.reduced-animations) .pie-glow { animation: glowPulse 4.6s ease-in-out infinite; }

@keyframes steamRise {
  0%   { transform: translateY(0) scale(.5); opacity: 0; }
  18%  { opacity: .8; }
  100% { transform: translateY(-92px) scale(2.1); opacity: 0; }
}
.steam-puff { animation: steamRise 3.4s ease-out infinite; }
body.reduced-animations .steam-puff, body.reduced-animations .pie-steam { display: none; }

/* --------------------------------------------------------- float / fx ---- */
@keyframes floatUp {
  0%   { transform: translate(-50%, 0) scale(.7); opacity: 0; }
  14%  { transform: translate(-50%, -14px) scale(1.12); opacity: 1; }
  100% { transform: translate(-50%, -104px) scale(.92); opacity: 0; }
}
.float-num { animation: floatUp 1.05s var(--ease) forwards; }

@keyframes critPop {
  0%   { transform: translate(-50%, 0) scale(.4) rotate(-8deg); opacity: 0; }
  16%  { transform: translate(-50%, -20px) scale(1.42) rotate(3deg); opacity: 1; }
  40%  { transform: translate(-50%, -40px) scale(1.06) rotate(0deg); }
  100% { transform: translate(-50%, -128px) scale(.96); opacity: 0; }
}
.float-num.crit { animation: critPop 1.25s var(--ease) forwards; }

/* Crumbs use a per-particle --dx/--dy set by effects.js. */
@keyframes crumbFly {
  0%   { transform: translate(0,0) rotate(0deg) scale(1); opacity: 1; }
  100% { transform: translate(var(--dx), var(--dy)) rotate(var(--rot)) scale(.35); opacity: 0; }
}
.crumb { animation: crumbFly var(--dur, .68s) cubic-bezier(.2,.6,.4,1) forwards; }

@keyframes ringBurst {
  0%   { transform: translate(-50%,-50%) scale(.35); opacity: .85; border-width: 4px; }
  100% { transform: translate(-50%,-50%) scale(1.5); opacity: 0; border-width: 1px; }
}
/* Sized from the pie, not the stage. At 78% of the stage the ring's box grew
   to ~1,340px when scaled, spilling far outside the play area and making the
   centre column scrollable on every critical click. */
.click-ring {
  position: absolute; left: 50%; top: 50%;
  width: var(--pie-size, 320px); aspect-ratio: 1; border-radius: 50%;
  border: 3px solid rgba(255,214,133,.7);
  pointer-events: none;
  animation: ringBurst .5s var(--ease) forwards;
}

/* ---------------------------------------------------------- counters ----- */
@keyframes counterBump {
  0% { transform: scale(1); }
  40% { transform: scale(1.055); }
  100% { transform: scale(1); }
}
.big-counter.bump { animation: counterBump .3s var(--ease); }
@keyframes counterSurge {
  0%   { transform: scale(1); text-shadow: 0 0 34px rgba(240,178,63,.30); }
  35%  { transform: scale(1.13); text-shadow: 0 0 60px rgba(255,214,133,.9); }
  100% { transform: scale(1); text-shadow: 0 0 34px rgba(240,178,63,.30); }
}
.big-counter.surge { animation: counterSurge .6s var(--ease); }

/* ---------------------------------------------------------- purchases ---- */
@keyframes buyFlash {
  0%   { background-color: rgba(240,178,63,.34); }
  100% { background-color: transparent; }
}
.card.bought { animation: buyFlash .5s var(--ease); }
@keyframes upgPop {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.35) rotate(7deg); filter: brightness(1.8); }
  100% { transform: scale(.2) rotate(-14deg); opacity: 0; }
}
.upg.purchased { animation: upgPop .34s var(--ease) forwards; pointer-events: none; }

@keyframes upgIn {
  from { opacity: 0; transform: scale(.5) translateY(8px); }
  to   { opacity: 1; transform: none; }
}
.upg.fresh { animation: upgIn .3s var(--ease); }

@keyframes transcendFlare {
  0%   { box-shadow: 0 0 0 0 rgba(169,139,255,.75); background-color: rgba(169,139,255,.30); }
  70%  { box-shadow: 0 0 0 22px rgba(169,139,255,0); }
  100% { box-shadow: 0 0 0 0 rgba(169,139,255,0); background-color: transparent; }
}
.card.transcended { animation: transcendFlare .95s var(--ease); }

/* -------------------------------------------------------------- toasts --- */
@keyframes toastIn {
  from { opacity: 0; transform: translateX(46px) scale(.94); }
  to   { opacity: 1; transform: none; }
}
@keyframes toastOut {
  from { opacity: 1; transform: none; max-height: 120px; margin-bottom: 8px; }
  to   { opacity: 0; transform: translateX(46px) scale(.94); max-height: 0; margin-bottom: 0; }
}
.toast { animation: toastIn .34s var(--ease); }
.toast.leaving { animation: toastOut .3s var(--ease) forwards; }

@keyframes achShine {
  0%   { transform: scale(.4) rotate(-18deg); opacity: 0; }
  45%  { transform: scale(1.28) rotate(6deg); opacity: 1; }
  70%  { transform: scale(.96) rotate(-2deg); }
  100% { transform: scale(1) rotate(0); opacity: 1; }
}
body:not(.no-ach-anim) .ach.just-unlocked { animation: achShine .7s var(--ease); }
@keyframes achGlow {
  0%, 100% { box-shadow: inset 0 1px 0 rgba(255,255,255,.08), 0 3px 10px -4px rgba(240,178,63,.4); }
  50%      { box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 0 22px 3px rgba(240,178,63,.75); }
}
body:not(.no-ach-anim) .ach.just-unlocked { animation: achShine .7s var(--ease), achGlow 1.6s ease-in-out 2; }

/* --------------------------------------------------------------- modal --- */
@keyframes modalIn {
  from { opacity: 0; transform: translateY(18px) scale(.96); }
  to   { opacity: 1; transform: none; }
}
@keyframes layerIn { from { opacity: 0; } to { opacity: 1; } }
.modal-layer { animation: layerIn var(--t-med) var(--ease); }
.modal { animation: modalIn var(--t-slow) var(--ease); }

/* ====================== ASCENSION CUTSCENE ============================== */

.cutscene {
  position: fixed; inset: 0; z-index: 300;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
  opacity: 0;
  transition: opacity 1.1s ease;
}
.cutscene.show { opacity: 1; }
.cutscene.fading { opacity: 0; transition: opacity 1.5s ease; }

.cutscene-bg {
  position: absolute; inset: 0;
  background:
    radial-gradient(900px 620px at 50% 42%, rgba(139,92,246,.30), transparent 62%),
    radial-gradient(700px 520px at 22% 78%, rgba(192,57,43,.20), transparent 60%),
    radial-gradient(760px 560px at 82% 22%, rgba(240,178,63,.14), transparent 62%),
    #05040a;
  animation: cosmicDrift 22s ease-in-out infinite alternate;
}
@keyframes cosmicDrift {
  0%   { transform: scale(1) rotate(0deg); filter: hue-rotate(0deg); }
  100% { transform: scale(1.14) rotate(3deg); filter: hue-rotate(28deg); }
}

.cutscene-stars { position: absolute; inset: 0; pointer-events: none; }
.star {
  position: absolute; border-radius: 50%;
  background: #fff;
  animation: starTwinkle var(--sd, 3s) ease-in-out infinite alternate;
}
@keyframes starTwinkle {
  from { opacity: .18; transform: scale(.7); }
  to   { opacity: 1;   transform: scale(1.3); }
}
@keyframes starRush {
  from { transform: translate(0,0) scale(1); opacity: .9; }
  to   { transform: translate(var(--sx), var(--sy)) scale(2.4); opacity: 0; }
}
/* makeStars() writes a random `animation-delay` inline to stagger the twinkle,
   and that inline delay applies to whatever animation the element is running
   next -- so the rush inherited it and stars left the screen up to three
   seconds after the flash that was supposed to blow them off it, still
   drifting outward under the line that follows. The rush is one event and
   every star has to leave on the same frame, so the delay is overridden here.
   `!important` is required: an inline style beats a stylesheet without it. */
.cutscene.rushing .star { animation: starRush 1.9s ease-in forwards; animation-delay: 0s !important; }

.cutscene-content {
  position: relative; z-index: 2;
  text-align: center; padding: 24px;
  max-width: min(760px, 92vw);
}
.cutscene-line {
  font-size: clamp(28px, 5.4vw, 60px);
  font-weight: 300; letter-spacing: .12em;
  color: #fff;
  text-shadow: 0 0 48px rgba(169,139,255,.8), 0 0 14px rgba(255,255,255,.35);
  min-height: 1.3em;
  line-height: 1.3;
}
.cutscene-line .caret {
  display: inline-block; width: .06em; height: 1em;
  background: #fff; margin-left: .06em;
  vertical-align: -.12em;
  animation: caretBlink .78s steps(1) infinite;
}
@keyframes caretBlink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

.cutscene-sub {
  margin-top: 22px;
  font-size: clamp(12px, 1.5vw, 16px);
  letter-spacing: .3em; text-transform: uppercase;
  color: rgba(214,199,255,.9);
  opacity: 0;
  transition: opacity 1.1s ease;
}
.cutscene-sub.show { opacity: 1; }

.cutscene-skip {
  position: absolute; right: 22px; bottom: 20px; z-index: 3;
  padding: 8px 18px; border-radius: 999px;
  font-size: 12px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: rgba(255,255,255,.65);
  border: 1px solid rgba(255,255,255,.22);
  transition: all .2s var(--ease);
}
.cutscene-skip:hover { color: #fff; border-color: rgba(255,255,255,.6); background: rgba(255,255,255,.08); }

@keyframes flashWhite {
  0%   { opacity: 0; }
  12%  { opacity: 1; }
  100% { opacity: 0; }
}
.cutscene-flash {
  position: absolute; inset: 0; background: #fff; z-index: 4;
  pointer-events: none; opacity: 0;
  animation: flashWhite 1.2s ease-out forwards;
}

/* ----------------------------------------------- settings-driven damping -- */
body.no-particles .crumb, body.no-particles .click-ring { display: none !important; }
body.no-floats .float-num { display: none !important; }
body.reduced-animations .card.bought,
body.reduced-animations .card.transcended,
body.reduced-animations .big-counter.bump,
body.reduced-animations .big-counter.surge,
body.reduced-animations .upg.fresh { animation: none !important; }

/* ---------------------------------------------------------- bonus pies --- */
@keyframes bonusIn {
  0%   { opacity: 0; transform: scale(.2) rotate(-25deg); }
  62%  { opacity: 1; transform: scale(1.18) rotate(6deg); }
  100% { opacity: 1; transform: scale(1) rotate(0deg); }
}
@keyframes bonusFloat {
  0%, 100% { transform: translateY(0) rotate(-4deg); }
  50%      { transform: translateY(-9px) rotate(4deg); }
}
@keyframes bonusPulse {
  0%, 100% { opacity: .55; transform: scale(1); }
  50%      { opacity: 1;   transform: scale(1.16); }
}
@keyframes bonusTaken {
  0%   { transform: scale(1); opacity: 1; }
  40%  { transform: scale(1.6); opacity: 1; filter: brightness(2.2); }
  100% { transform: scale(.25); opacity: 0; }
}
@keyframes bonusGone {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0; transform: scale(.5) translateY(14px); }
}
/* The wrapper handles entry/exit; the glyph does the idle bob, so the two
   never fight over `transform` on the same element. */
.bonus-pie { animation: bonusIn .42s var(--ease); }
body:not(.reduced-animations) .bonus-pie .bonus-glyph {
  animation: bonusFloat 2.4s ease-in-out infinite;
}
body:not(.reduced-animations) .bonus-halo {
  animation: bonusPulse 1.5s ease-in-out infinite;
}
/* Running out of time: faster bob, and the halo flickers. */
.bonus-pie.expiring .bonus-glyph { animation-duration: .9s; }
.bonus-pie.expiring .bonus-halo  { animation-duration: .5s; }
/* A combo pie arrives with a harder entry and keeps a faster halo, so a chain
   reads as escalating rather than as just another bonus pie. The glyph rule
   stays untouched -- only the wrapper and the halo are involved, so nothing
   fights over `transform` (see the note above). */
@keyframes comboIn {
  0%   { opacity: 0; transform: scale(.3) rotate(-25deg); }
  55%  { opacity: 1; transform: scale(1.28) rotate(6deg); }
  100% { opacity: 1; transform: scale(1) rotate(0); }
}
.bonus-pie.combo { animation: comboIn .5s var(--ease); }
body:not(.reduced-animations) .bonus-pie.combo .bonus-halo {
  animation-duration: .85s;
}

.bonus-pie.taken { animation: bonusTaken .38s var(--ease) forwards; pointer-events: none; }
.bonus-pie.gone  { animation: bonusGone .34s var(--ease) forwards; pointer-events: none; }

@keyframes buffIn {
  from { opacity: 0; transform: translateY(-6px) scale(.9); }
  to   { opacity: 1; transform: none; }
}
.buff-chip { animation: buffIn .26s var(--ease); }
body.reduced-animations .buff-chip,
body.reduced-animations .bonus-pie { animation: none !important; }
body.reduced-animations .pie.clicked { animation: none !important; }
body.reduced-animations .pie-body { animation: none !important; }
body.no-ach-anim .ach.just-unlocked { animation: none !important; }

/* The OS preference kills the idle spin as well as the in-game setting; a slow
   continuous rotation is exactly the kind of movement it is asking us to drop. */
@media (prefers-reduced-motion: reduce) {
  .pie-body { animation: none !important; }
}

/* A ripe plot breathes so the eye finds it in a grid of sixteen. Damped with
   every other decorative motion by the reduced-animation body classes. */
@keyframes plotReady {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.14); }
}
body.reduced-animations .plot.is-ready .plot-icon,
body.reduced-motion .plot.is-ready .plot-icon { animation: none; }

/* ========================= THE NOVA CUTSCENE ============================
   Re-skins the shared #cutscene overlay rather than adding a second one, so
   there is still exactly one full-screen layer and one z-index in play. Every
   rule here is scoped to .cutscene.nova and nothing outside it changes. */

.cutscene.nova .cutscene-bg {
  background:
    radial-gradient(1100px 760px at 50% 50%, rgba(255,214,120,.26), transparent 58%),
    radial-gradient(820px 620px at 18% 82%, rgba(255,90,60,.20), transparent 60%),
    radial-gradient(880px 660px at 84% 18%, rgba(120,180,255,.16), transparent 62%),
    #030208;
  animation: novaDrift 12s ease-in-out infinite alternate;
}
@keyframes novaDrift {
  0%   { transform: scale(1)    rotate(0deg);  filter: hue-rotate(0deg)  saturate(1); }
  100% { transform: scale(1.22) rotate(-4deg); filter: hue-rotate(-22deg) saturate(1.5); }
}

.cutscene.nova .cutscene-line {
  font-weight: 600;
  letter-spacing: .18em;
  color: #fff5df;
  text-shadow:
    0 0 60px rgba(255,196,92,.95),
    0 0 22px rgba(255,255,255,.6),
    0 0 4px rgba(255,255,255,.9);
}
.cutscene.nova .cutscene-sub { color: rgba(255,228,178,.95); }

/* ------------------------------------------------------- the core ------- */
/* Sits behind the words and pulls inward, so the screen is visibly winding
   up before the detonation rather than simply cutting to white. */
.cutscene-core {
  position: absolute; left: 50%; top: 50%;
  width: 42vmin; height: 42vmin; margin: -21vmin 0 0 -21vmin;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(circle, #fff 0%, #ffd77a 26%, rgba(255,138,60,.55) 52%, transparent 72%);
  filter: blur(4px);
}
.cutscene-core.collapsing { animation: novaCollapse 3.7s cubic-bezier(.55,0,.9,.35) forwards; }
@keyframes novaCollapse {
  0%   { opacity: 0;   transform: scale(1.5)  rotate(0deg); }
  22%  { opacity: .55; transform: scale(1.05) rotate(40deg); }
  82%  { opacity: 1;   transform: scale(.16)  rotate(220deg); filter: blur(1px) brightness(2.4); }
  100% { opacity: 1;   transform: scale(.05)  rotate(300deg); filter: blur(0)   brightness(4); }
}

/* ------------------------------------------------------ the shockwave --- */
.cutscene-shock {
  position: absolute; left: 50%; top: 50%;
  width: 10vmin; height: 10vmin; margin: -5vmin 0 0 -5vmin;
  border-radius: 50%;
  pointer-events: none; z-index: 5;
  border: 3vmin solid rgba(255,225,160,.85);
  box-shadow: 0 0 12vmin rgba(255,196,92,.8), inset 0 0 8vmin rgba(255,255,255,.7);
  animation: novaShock 2.2s cubic-bezier(.14,.7,.2,1) forwards;
}
@keyframes novaShock {
  0%   { transform: scale(.1);  opacity: 1; border-width: 3vmin; }
  60%  { opacity: .55; }
  100% { transform: scale(9);   opacity: 0; border-width: .3vmin; }
}

/* The Nova's white-out is brighter and slower to fall off than Ascension's,
   and it warms as it fades instead of going straight to nothing. */
.cutscene-flash.nova-flash {
  background: radial-gradient(circle at 50% 50%, #fff 0%, #fff 38%, #ffdca0 70%, #ff9a4a 100%);
  animation: novaFlash 2.4s ease-out forwards;
}
@keyframes novaFlash {
  0%   { opacity: 0; }
  6%   { opacity: 1; }
  40%  { opacity: .92; }
  100% { opacity: 0; }
}

/* The whole layer trembles while the core collapses. Applied to the overlay
   and to <body> together: the overlay covers the screen, but the shake has to
   be visible on the edges of the page behind it for the frame to feel loose. */
.cutscene.shaking { animation: novaShake .28s linear infinite; }
body.nova-shake #app { animation: novaShake .34s linear infinite; }
@keyframes novaShake {
  0%   { transform: translate(0, 0); }
  20%  { transform: translate(-5px, 3px); }
  40%  { transform: translate(4px, -4px); }
  60%  { transform: translate(-3px, -2px); }
  80%  { transform: translate(5px, 2px); }
  100% { transform: translate(0, 0); }
}
/* .blown ends the wind-up: the background stops drifting and holds wide open
   so the shockwave is the only thing moving. */
.cutscene.nova.blown .cutscene-bg { animation: none; transform: scale(1.35); filter: saturate(1.8); }

/* Reduced motion strips the shake and the collapse spin but keeps the beats,
   so the scene still reads as an event rather than as nothing happening. */
body.reduced-motion .cutscene.shaking,
body.reduced-motion.nova-shake #app { animation: none; }
body.reduced-motion .cutscene-core.collapsing { animation: novaCollapseCalm 3.7s ease-in forwards; }
@keyframes novaCollapseCalm {
  0%   { opacity: 0; transform: scale(1.3); }
  100% { opacity: 1; transform: scale(.1); }
}
body.reduced-motion .cutscene-shock { animation-duration: 3.4s; }
