/* ============================================================================
 * ui/avatarfx.css — the reaction layer for the Discord avatar bubbles.
 *
 * Partner file to ui/avatarFx.js (Work Item E). D owns `.gg-ava` and its
 * `.gg-ava-img` / `.gg-ava-tile` child; NOTHING here lays them out, sizes them,
 * colours them or positions them. This file only makes them move.
 *
 * THE ONE-SLOT BUDGET (the law the .gg-vwin split was written to obey — see the
 * banner in exec/fx.css). `animation` is a shorthand, so two rules on one
 * element cancel each other:
 *
 *   .gg-ava.gg-avafx-idle        THE LOOP. The idle micro-bob, +/-2px, and the
 *                                ONLY animation this element ever declares.
 *                                Rides --gg-deco-play: it is pure decoration
 *                                and parks with everything else at
 *                                html[data-gg-fx="hot"].
 *   .gg-avafx--*  .gg-ava-img
 *   .gg-avafx--*  .gg-ava-tile   THE ONE-SHOTS. Exactly one at a time, on the
 *                                CHILD, so the loop above survives them. These
 *                                deliberately do NOT ride --gg-deco-play — a
 *                                parked one-shot is a frozen corpse (the
 *                                .gg-vwin--out precedent) and every one of them
 *                                is under 1.1s of information.
 *   .gg-avafx-deco > *           DECORATION. Rings, stars and confetti, each on
 *                                its own free element, built and destroyed with
 *                                the reaction by the JS. Click-through, inside
 *                                `.gg-ava`, no z-index anywhere.
 *
 * .gg-avafx-busy switches the loop off for the length of a reaction (the
 * `.gg-vwin.is-grabbed .gg-vwin-drift` pattern) — a land really does interrupt
 * idle.
 *
 * NO NEW STACKING LAYERS, no z-index, no pointer-events change to anything this
 * file did not create. MERCY (z60) is not named anywhere below and never will
 * be: it is never dimmed, tinted, filtered or animated by another rule.
 *
 * Transform / opacity / filter only. No width, height, margin, top or left is
 * ever animated — a bubble that reflows the HUD every time it gets hit would
 * cost more than every effect on the page put together.
 * ==========================================================================*/

/* --------------------------------------------------------------- the bubble */

/* The ONE layout fact this file asserts, and only on bubbles it has adopted:
   the decoration children are absolutely positioned inside the bubble. No
   z-index, so no stacking context beyond the one the transform already makes.

   DELIBERATELY ONE CLASS of specificity, in its own rule. ui/avatarfx.css is
   linked AFTER ui/screens.css, so a tie would go to E — and D owns where a
   bubble sits (lobby row, VS splash, HUD anchor, recap plate). Any rule D
   writes with two selectors, or on the .gg-ava-slot wrapper, outranks this. */
.gg-avafx-on { position: relative; }

.gg-ava.gg-avafx-on {
  --gga-amp: 1;          /* how hard a hit reads. Overridden for the far side. */
  --gga-dur: 520ms;      /* the JS writes the real one per reaction. */
}

/* A hit on YOUR bubble is your problem; a hit on theirs is a spectator sport.
   Same keyframes, less of them. */
.gg-ava.gg-avafx-on[data-side="opp"] { --gga-amp: 0.62; }

/* ---------------------------------------------------------------- THE LOOP */

/* Alternating micro-bob, so a pair of bubbles breathes out of phase instead of
   pulsing like one object. The negative delay starts the far side mid-cycle. */
.gg-ava.gg-avafx-idle {
  animation: ggAvaBob 3.4s ease-in-out infinite alternate;
  animation-play-state: var(--gg-deco-play, running);
  will-change: transform;
}
.gg-ava.gg-avafx-idle[data-side="opp"] { animation-delay: -1.7s; }
/* A reaction owns the bubble while it plays. */
.gg-ava.gg-avafx-busy { animation: none; }

@keyframes ggAvaBob {
  from { transform: translateY(1px); }
  to   { transform: translateY(-2px); }
}

/* ------------------------------------------------------------ THE ONE-SHOTS
 * One block per beat. Every selector pair is (img | tile) because D ships one
 * or the other and never both.
 * ------------------------------------------------------------------------- */

.gg-ava.gg-avafx-on .gg-ava-img,
.gg-ava.gg-avafx-on .gg-ava-tile { will-change: transform; }

/* LAND — took a payload. Shake, squash, and a ring you can feel. */
.gg-avafx--land .gg-ava-img,
.gg-avafx--land .gg-ava-tile {
  transform-origin: 50% 60%;
  animation: ggAvaFlinch var(--gga-dur, 620ms) cubic-bezier(.36, .07, .19, .97) 1 both;
}
@keyframes ggAvaFlinch {
  0%   { transform: translateX(0) scale(1, 1); filter: none; }
  8%   { transform: translateX(calc(var(--gga-amp) * -6px)) scale(1.1, 0.86); filter: brightness(1.7) saturate(0.4); }
  20%  { transform: translateX(calc(var(--gga-amp) *  5px)) scale(0.92, 1.08); filter: brightness(1.25); }
  34%  { transform: translateX(calc(var(--gga-amp) * -4px)) scale(1.05, 0.95); filter: none; }
  50%  { transform: translateX(calc(var(--gga-amp) *  3px)) scale(0.98, 1.03); }
  68%  { transform: translateX(calc(var(--gga-amp) * -2px)) scale(1.02, 0.99); }
  84%  { transform: translateX(calc(var(--gga-amp) *  1px)) scale(1, 1); }
  100% { transform: translateX(0) scale(1, 1); }
}

/* FIRE — you let one go. Lean back, then a smug little forward bounce. */
.gg-avafx--fire .gg-ava-img,
.gg-avafx--fire .gg-ava-tile {
  transform-origin: 50% 100%;
  animation: ggAvaWindup var(--gga-dur, 520ms) cubic-bezier(.2, .8, .25, 1.4) 1 both;
}
@keyframes ggAvaWindup {
  0%   { transform: translateX(0) rotate(0deg) scale(1); }
  28%  { transform: translateX(-4px) rotate(-7deg) scale(0.95, 1.05); }
  52%  { transform: translateX(5px) rotate(6deg) scale(1.12, 0.92); }
  72%  { transform: translateX(1px) rotate(-2deg) scale(0.98, 1.02); }
  100% { transform: translateX(0) rotate(0deg) scale(1); }
}

/* ALARM — the other bubble notices, 200ms late. Small. It is not a hit yet. */
.gg-avafx--alarm .gg-ava-img,
.gg-avafx--alarm .gg-ava-tile {
  animation: ggAvaAlarm var(--gga-dur, 240ms) ease-in-out 1 both;
}
@keyframes ggAvaAlarm {
  0%, 100% { transform: rotate(0deg) scale(1); }
  25%  { transform: rotate(-5deg) scale(1.05); }
  60%  { transform: rotate(4deg) scale(1.03); }
  85%  { transform: rotate(-2deg) scale(1); }
}

/* DROP — you armed one. A happy hop with a squashed landing. */
.gg-avafx--drop .gg-ava-img,
.gg-avafx--drop .gg-ava-tile {
  transform-origin: 50% 100%;
  animation: ggAvaHop var(--gga-dur, 520ms) cubic-bezier(.28, .84, .42, 1) 1 both;
}
@keyframes ggAvaHop {
  0%   { transform: translateY(0) scale(1, 1); }
  16%  { transform: translateY(2px) scale(1.1, 0.88); }
  46%  { transform: translateY(-9px) scale(0.94, 1.1); }
  74%  { transform: translateY(1px) scale(1.08, 0.92); }
  90%  { transform: translateY(-2px) scale(0.99, 1.02); }
  100% { transform: translateY(0) scale(1, 1); }
}

/* POP — a popped bubble. This one fires dozens of times a minute, so it is two
   keyframes of scale and nothing else: no filter, no decoration, no shadow. */
.gg-avafx--pop .gg-ava-img,
.gg-avafx--pop .gg-ava-tile {
  animation: ggAvaBop var(--gga-dur, 180ms) ease-out 1 both;
}
@keyframes ggAvaBop {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.09); }
  100% { transform: scale(1); }
}

/* EMOTE — sympathetic wiggle for exactly as long as the emote is on screen.
   The JS hands the dwell in as --gga-dur, clamped to 2600ms. */
.gg-avafx--emote .gg-ava-img,
.gg-avafx--emote .gg-ava-tile {
  transform-origin: 50% 85%;
  animation: ggAvaWiggle var(--gga-dur, 2600ms) ease-in-out 1 both;
}
@keyframes ggAvaWiggle {
  0%, 100% { transform: rotate(0deg) translateY(0); }
  12%  { transform: rotate(-6deg) translateY(-1px); }
  28%  { transform: rotate(5deg) translateY(0); }
  44%  { transform: rotate(-4deg) translateY(-1px); }
  60%  { transform: rotate(4deg) translateY(0); }
  78%  { transform: rotate(-2deg) translateY(0); }
}

/* MERCY — the tap-out. The bubble DEFLATES: squashed, spun slowly out, drained
   of colour. Nothing about it is a punishment beat; it is the safety valve
   being read out loud, and the other bubble bows for it below. */
.gg-avafx--mercy .gg-ava-img,
.gg-avafx--mercy .gg-ava-tile {
  transform-origin: 50% 100%;
  animation: ggAvaDeflate var(--gga-dur, 900ms) cubic-bezier(.35, 0, .2, 1) 1 both;
}
@keyframes ggAvaDeflate {
  0%   { transform: rotate(0deg) scale(1, 1); opacity: 1; }
  22%  { transform: rotate(-3deg) scale(1.06, 0.9); opacity: 1; }
  60%  { transform: rotate(11deg) scale(0.92, 0.82) translateY(3px); opacity: 0.8; }
  100% { transform: rotate(19deg) scale(0.84, 0.78) translateY(5px); opacity: 0.62; }
}
/* The latched state. Grey, small, still there — never hidden, never covered. */
.gg-ava.gg-avafx--tapped { filter: saturate(0.25) brightness(0.86); transition: filter 420ms ease; }

/* BOW — the OTHER bubble when someone taps out. A dip of the head, held, and
   back up. Deliberately slow, deliberately not a taunt: the house rule is that
   mercy is never punished, so there is no bounce and no sparkle on this one. */
.gg-avafx--bow .gg-ava-img,
.gg-avafx--bow .gg-ava-tile {
  transform-origin: 50% 100%;
  animation: ggAvaBow var(--gga-dur, 760ms) ease-in-out 1 both;
}
@keyframes ggAvaBow {
  0%   { transform: rotate(0deg) translateY(0) scale(1); }
  30%  { transform: rotate(9deg) translateY(3px) scale(1, 0.95); }
  62%  { transform: rotate(9deg) translateY(3px) scale(1, 0.95); }
  100% { transform: rotate(0deg) translateY(0) scale(1); }
}

/* WIN — proud double bounce, and the gold arrives with it (ring + confetti in
   the decoration block below). */
.gg-avafx--win .gg-ava-img,
.gg-avafx--win .gg-ava-tile {
  transform-origin: 50% 100%;
  animation: ggAvaProud var(--gga-dur, 900ms) cubic-bezier(.25, .9, .3, 1.3) 1 both;
}
@keyframes ggAvaProud {
  0%   { transform: translateY(0) scale(1, 1); }
  12%  { transform: translateY(3px) scale(1.1, 0.9); }
  34%  { transform: translateY(-11px) scale(0.94, 1.12); }
  54%  { transform: translateY(0) scale(1.08, 0.94); }
  72%  { transform: translateY(-6px) scale(0.97, 1.06); }
  88%  { transform: translateY(0) scale(1.03, 0.98); }
  100% { transform: translateY(0) scale(1, 1); }
}
.gg-ava.gg-avafx--won {
  filter: saturate(1.15) brightness(1.06);
  box-shadow: 0 0 18px rgba(255, 212, 94, 0.55), 0 0 42px rgba(255, 212, 94, 0.22);
  border-radius: 50%;
  transition: filter 320ms ease, box-shadow 320ms ease;
}

/* LOSE — droop and tilt, one slow recover blink in the middle, and it stays
   greyed. It never vanishes: a bubble you cannot see reads as a disconnect. */
.gg-avafx--lose .gg-ava-img,
.gg-avafx--lose .gg-ava-tile {
  transform-origin: 50% 90%;
  animation: ggAvaDroop var(--gga-dur, 1100ms) ease-in-out 1 both;
}
@keyframes ggAvaDroop {
  0%   { transform: translateY(0) rotate(0deg) scale(1, 1); opacity: 1; }
  26%  { transform: translateY(5px) rotate(-7deg) scale(1.04, 0.92); opacity: 1; }
  52%  { transform: translateY(6px) rotate(-8deg) scale(1.03, 0.93); opacity: 0.35; }  /* the blink */
  70%  { transform: translateY(5px) rotate(-7deg) scale(1.03, 0.94); opacity: 1; }
  100% { transform: translateY(3px) rotate(-4deg) scale(1.01, 0.97); opacity: 0.92; }
}
.gg-ava.gg-avafx--lost { filter: grayscale(0.6) brightness(0.9); transition: filter 520ms ease; }

/* DRAW — both bubbles shrug. Nobody lost, so nobody droops. */
.gg-avafx--draw .gg-ava-img,
.gg-avafx--draw .gg-ava-tile {
  transform-origin: 50% 95%;
  animation: ggAvaShrug var(--gga-dur, 760ms) ease-in-out 1 both;
}
@keyframes ggAvaShrug {
  0%, 100% { transform: rotate(0deg) translateY(0); }
  22%  { transform: rotate(-9deg) translateY(-2px); }
  55%  { transform: rotate(9deg) translateY(-2px); }
  80%  { transform: rotate(-3deg) translateY(0); }
}
.gg-ava.gg-avafx--drew { filter: saturate(0.85); transition: filter 320ms ease; }

/* CUE — the announcer says something is coming. Both bubbles lean in and
   squish, a held breath. Subtle on purpose: it is a warning, not a hit. */
.gg-avafx--cue .gg-ava-img,
.gg-avafx--cue .gg-ava-tile {
  transform-origin: 50% 100%;
  animation: ggAvaLean var(--gga-dur, 420ms) ease-out 1 both;
}
@keyframes ggAvaLean {
  0%   { transform: translateY(0) scale(1, 1); }
  40%  { transform: translateY(1px) scale(1.05, 0.94); }
  70%  { transform: translateY(0) scale(0.99, 1.02); }
  100% { transform: translateY(0) scale(1, 1); }
}

/* ------------------------------------------------------------- DECORATION
 * Built by the JS, destroyed by the JS, and never anywhere but inside a
 * `.gg-ava`. Click-through, no z-index: they paint in DOM order above the
 * avatar art and below absolutely nothing else.
 * ------------------------------------------------------------------------- */
.gg-avafx-deco {
  position: absolute;
  inset: -18%;
  pointer-events: none;
  display: block;
  overflow: visible;
}

.gg-avafx-ring {
  position: absolute;
  left: 50%; top: 50%;
  width: 100%; aspect-ratio: 1 / 1;
  margin: 0;
  transform: translate(-50%, -50%) scale(0.4);
  border-radius: 50%;
  opacity: 0;
}
/* the hit: a white core flashing out through a red edge */
.gg-avafx-ring--impact {
  border: 2px solid rgba(255, 255, 255, 0.95);
  box-shadow: 0 0 14px rgba(255, 60, 90, 0.85), inset 0 0 10px rgba(255, 60, 90, 0.5);
  animation: ggAvaRingHit 420ms cubic-bezier(.15, .7, .3, 1) 1 both;
}
@keyframes ggAvaRingHit {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.35); border-width: 3px; }
  18%  { opacity: 1; transform: translate(-50%, -50%) scale(0.7); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1.35); border-width: 1px; }
}
/* the drop: house pink/violet, softer, slower */
.gg-avafx-ring--sparkle {
  border: 2px solid rgba(var(--gg-violet-rgb, 179, 136, 255), 0.9);
  box-shadow: 0 0 16px rgba(var(--gg-pink-rgb, 255, 105, 180), 0.6);
  animation: ggAvaRingSoft 520ms ease-out 1 both;
}
@keyframes ggAvaRingSoft {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.5); }
  30%  { opacity: 0.95; transform: translate(-50%, -50%) scale(0.9); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1.25); }
}
/* the win: gold, and it BREATHES rather than snapping out */
.gg-avafx-ring--gold {
  border: 2px solid rgba(255, 212, 94, 0.95);
  box-shadow: 0 0 22px rgba(255, 212, 94, 0.7), inset 0 0 14px rgba(255, 212, 94, 0.35);
  animation: ggAvaRingGold 900ms ease-in-out 1 both;
}
@keyframes ggAvaRingGold {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.55); }
  22%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  62%  { opacity: 0.85; transform: translate(-50%, -50%) scale(1.08); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1.22); }
}

/* Stars orbit out of the centre: rotate to a bearing, walk outward, and
   counter-rotate so the diamond never tumbles. --gga-a is per-star, inline. */
.gg-avafx-star {
  position: absolute;
  left: 50%; top: 50%;
  width: 5px; height: 5px;
  margin: -2.5px 0 0 -2.5px;
  border-radius: 1px;
  background: #fff;
  box-shadow: 0 0 8px rgba(255, 212, 94, 0.95);
  opacity: 0;
  animation: ggAvaStar 600ms cubic-bezier(.2, .7, .3, 1) 1 both;
}
.gg-avafx-star--spark {
  background: var(--gg-pink, #ff69b4);
  box-shadow: 0 0 10px rgba(var(--gg-violet-rgb, 179, 136, 255), 0.95);
  animation-duration: 520ms;
}
@keyframes ggAvaStar {
  0%   { opacity: 0; transform: rotate(var(--gga-a, 0deg)) translateX(3px) rotate(calc(var(--gga-a, 0deg) * -1)) scale(0.4); }
  25%  { opacity: 1; }
  100% { opacity: 0; transform: rotate(calc(var(--gga-a, 0deg) + 90deg)) translateX(26px) rotate(calc((var(--gga-a, 0deg) + 90deg) * -1)) scale(1); }
}

/* Confetti: six bits thrown out and down, each on its own short delay. */
.gg-avafx-bit {
  position: absolute;
  left: 50%; top: 50%;
  width: 4px; height: 7px;
  margin: -3.5px 0 0 -2px;
  border-radius: 1px;
  background: linear-gradient(160deg, #ffd45e, #ff8fc7);
  opacity: 0;
  animation: ggAvaBit 900ms cubic-bezier(.15, .6, .35, 1) var(--gga-d, 0ms) 1 both;
}
@keyframes ggAvaBit {
  0%   { opacity: 0; transform: rotate(var(--gga-a, 0deg)) translateX(0) rotate(0deg); }
  15%  { opacity: 1; }
  100% { opacity: 0; transform: rotate(var(--gga-a, 0deg)) translateX(34px) translateY(14px) rotate(320deg); }
}

/* ---------------------------------------------------------------------------
 * MOTION — the hard off switch. Every reaction collapses to at most a one-frame
 * state change: ui/avatarFx.js builds no decoration and drops the class on the
 * next tick, and nothing below is left with a keyframe to play. The LATCHED
 * states (won / lost / drew / tapped) survive on purpose — a colour is a
 * read-out, not motion, and a calm player still needs to know who won.
 * ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .gg-ava.gg-avafx-idle,
  .gg-ava.gg-avafx-on .gg-ava-img,
  .gg-ava.gg-avafx-on .gg-ava-tile,
  .gg-avafx-deco,
  .gg-avafx-deco * { animation: none !important; }
  /* Belt and braces: the JS already skips building these. */
  .gg-avafx-deco { display: none !important; }
  /* The latched read-outs stay, but they arrive instantly rather than fading. */
  .gg-ava.gg-avafx--won,
  .gg-ava.gg-avafx--lost,
  .gg-ava.gg-avafx--drew,
  .gg-ava.gg-avafx--tapped { transition: none !important; }
}
