/* ============================================================
   ANIMATIONS
   ============================================================ */

@keyframes screen-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes card-in {
  from { opacity: 0; transform: translateY(14px) scale(0.98); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes title-drop {
  from { opacity: 0; transform: translateY(-28px) scale(0.86); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes light-flicker {
  0%, 100% { opacity: 0.75; }
  46% { opacity: 1; }
  52% { opacity: 0.62; }
}


@keyframes fly-across {
  0%   { left: -50px; top: 30%; transform: rotate(0deg) scale(0.8); opacity: 0; }
  12%  { opacity: 1; }
  50%  { top: 18%; transform: rotate(420deg) scale(1.15); }
  100% { left: 106%; top: 34%; transform: rotate(900deg) scale(0.8); opacity: 0.9; }
}

@keyframes confetti-drift {
  from { transform: translateY(-10vh) rotate(0deg); opacity: 0; }
  10%  { opacity: 0.9; }
  to   { transform: translateY(110vh) rotate(620deg); opacity: 0; }
}

@keyframes confetti-fall {
  from { transform: translateY(0) rotate(0deg); opacity: 1; }
  to   { transform: translateY(120vh) rotate(560deg); opacity: 0; }
}

@keyframes dot-blink {
  0%, 80%, 100% { opacity: 0.2; }
  40% { opacity: 1; }
}

@keyframes loading-fill {
  from { width: 0; }
  to   { width: 100%; }
}

/* Bowler drives in toward the crease as the ball is released. */
/* Far bowler running in, seen from the striker's end.
   He is coming TOWARDS the camera, so the approach is mostly a change of
   scale — a bowler who only slid down the screen read as a cut-out being
   dragged. The gather at 62% is the delivery stride braking before release,
   and the drift after it is the follow-through carrying him through the
   crease. Timed to BAT_RUNUP_MS in js/game.js: the peak lands on the frame
   swap to the delivery stride, so the sprite and the movement agree. */
@keyframes bowler-runup {
  0%   { transform: translateX(-50%) translateY(-10%) scale(0.74); opacity: 0.92; }
  45%  { transform: translateX(-50%) translateY(-3%)  scale(0.90); opacity: 1; }
  62%  { transform: translateX(-50%) translateY(0)    scale(1.02); }
  78%  { transform: translateX(-50%) translateY(2%)   scale(1.10); }
  100% { transform: translateX(-50%) translateY(4%)   scale(1.06); }
}

/* ---------------------------------------------------------------------------
   BALL FLIGHT — composited.

   These animate only `transform` and `opacity`, so they run on the compositor
   instead of forcing layout + paint on the main thread every frame. That
   matters here beyond smoothness: the ball is what the player watches to time
   their tap, so main-thread jank directly cost them timing accuracy.

   Travel distances arrive as CSS variables set per delivery in game.js, because
   transform percentages resolve against the element's own 13px box rather than
   against the pitch panel.
   --------------------------------------------------------------------------- */
/* BATTING delivery — the ball PITCHES on its way to the bat, same shape as the
   bowl-mode keyframes below and at the same 73.3% (BOUNCE_AT in js/game.js, so
   the length marker and the bounce cannot drift apart). Percentages equal the
   down-pitch fraction and the easing stays linear, so `--deliver-y x fraction`
   keeps the recede even; only the height term arcs. It is 0 at the release and
   0 at the bounce — the ball is on the ground there — and climbs from the
   bounce into the bat, which is what a real delivery does off a length. */
@keyframes ball-deliver {
  0%   { transform: translate3d(0, 0, 0) scale(0.7); opacity: 1; }
  40%  { transform: translate3d(calc(var(--swing-x, 0px) * 0.6),
                                calc(var(--deliver-y, 200px) * 0.40 + var(--arc-y, 0px)), 0)
                   scale(0.98); opacity: 1; }
  /* pitching — on the ground, on its line */
  73.3% { transform: translate3d(var(--swing-x, 0px),
                                calc(var(--deliver-y, 200px) * 0.733), 0)
                   scale(1.2); opacity: 1; }
  84%  { transform: translate3d(calc(var(--swing-x, 0px) * 0.66),
                                calc(var(--deliver-y, 200px) * 0.84 + var(--hop-y, 0px) * 0.62), 0)
                   scale(1.3); opacity: 1; }
  100% { transform: translate3d(calc(var(--swing-x, 0px) * 0.15),
                                calc(var(--deliver-y, 200px) + var(--hop-y, 0px)), 0)
                   scale(1.45); opacity: 1; }
}
.ball.deliver { animation: ball-deliver linear forwards; opacity: 1; }

/* SIX — ball launches up and clears the stadium. */
@keyframes ball-launch {
  0%   { transform: translate3d(0, 0, 0) scale(1.3); opacity: 1; }
  30%  { transform: translate3d(calc(var(--fly-x, 0px) * 0.30),
                                calc(var(--fly-y, -300px) * 0.38), 0) scale(1.05); opacity: 1; }
  65%  { transform: translate3d(calc(var(--fly-x, 0px) * 0.68),
                                calc(var(--fly-y, -300px) * 0.72), 0) scale(0.78); opacity: 1; }
  100% { transform: translate3d(var(--fly-x, 0px), var(--fly-y, -300px), 0) scale(0.42); opacity: 0; }
}
.ball.launch { animation: ball-launch 1.05s cubic-bezier(0.25, 0.55, 0.5, 1) forwards; opacity: 1; }

/* FOUR — ball races along the ground to the boundary rope. */
@keyframes ball-boundary {
  0%   { transform: translate3d(0, 0, 0) scale(1.3); opacity: 1; }
  100% { transform: translate3d(var(--fly-x, -120px), var(--fly-y, 80px), 0) scale(1.6); opacity: 0; }
}
.ball.boundary { animation: ball-boundary 0.7s cubic-bezier(0.15, 0.7, 0.4, 1) forwards; opacity: 1; }

/* WICKET — the stumps are knocked back and topple over. Pivots about the base
   so they fall rather than slide. */
/* Wicket on the photo pitch. The stumps are assembled from three sticks and
   two bails (see .pitch-stumps in game.css), so the break is five separate
   animations rather than one topple.

   Bails carry no weight: they leave fast, spin, and are gone before the
   timber has moved far. The sticks then fall outward from the middle —
   that spread is what makes it read as a ball through the middle rather
   than the whole set hinging over as one piece, which is how the old
   single-sprite version looked. */
@keyframes bail-fly-l {
  0%   { transform: translate(0, 0) rotate(0deg); opacity: 1; }
  60%  { transform: translate(-120%, -180%) rotate(-220deg); opacity: 1; }
  100% { transform: translate(-190%, -40%) rotate(-420deg); opacity: 0; }
}
@keyframes bail-fly-r {
  0%   { transform: translate(0, 0) rotate(0deg); opacity: 1; }
  60%  { transform: translate(115%, -200%) rotate(260deg); opacity: 1; }
  100% { transform: translate(185%, -55%) rotate(470deg); opacity: 0; }
}
@keyframes stick-fall-l {
  0%   { transform: rotate(0deg); }
  18%  { transform: rotate(-9deg) translateY(-2%); }
  55%  { transform: rotate(-46deg); }
  100% { transform: rotate(-74deg) translateX(-6%); opacity: 0.9; }
}
@keyframes stick-fall-m {
  0%   { transform: rotate(0deg); }
  16%  { transform: rotate(4deg) translateY(-4%); }
  55%  { transform: rotate(38deg); }
  100% { transform: rotate(68deg) translateX(5%); opacity: 0.9; }
}
@keyframes stick-fall-r {
  0%   { transform: rotate(0deg); }
  18%  { transform: rotate(11deg) translateY(-2%); }
  55%  { transform: rotate(52deg); }
  100% { transform: rotate(80deg) translateX(8%); opacity: 0.9; }
}

/* WICKET — drawn-pitch stumps scatter (photo pitch uses the graphic above). */
@keyframes stump-fly-l {
  to { transform: translate(-22px, 10px) rotate(-72deg); opacity: 0; }
}
@keyframes stump-fly-c {
  to { transform: translate(3px, 16px) rotate(18deg); opacity: 0; }
}
@keyframes stump-fly-r {
  to { transform: translate(22px, 11px) rotate(78deg); opacity: 0; }
}
.stumps.broken span { transform-origin: 50% 100%; }
.stumps.broken span:nth-child(1) { animation: stump-fly-l 0.6s ease-out forwards; }
.stumps.broken span:nth-child(2) { animation: stump-fly-c 0.6s ease-out forwards; }
.stumps.broken span:nth-child(3) { animation: stump-fly-r 0.6s ease-out forwards; }

/* Photo-pitch trajectories — matched to the photograph's perspective,
   where the far wicket sits near 56% and the batting crease near 82%. */
@keyframes ball-deliver-photo {
  0%   { transform: translate3d(0, 0, 0) scale(0.55); opacity: 1; }
  40%  { transform: translate3d(calc(var(--swing-x, 0px) * 0.6),
                                calc(var(--deliver-y, 180px) * 0.40 + var(--arc-y, 0px)), 0)
                   scale(0.86); opacity: 1; }
  /* pitching — on the ground, on its line */
  73.3% { transform: translate3d(var(--swing-x, 0px),
                                calc(var(--deliver-y, 180px) * 0.733), 0)
                   scale(1.12); opacity: 1; }
  84%  { transform: translate3d(calc(var(--swing-x, 0px) * 0.66),
                                calc(var(--deliver-y, 180px) * 0.84 + var(--hop-y, 0px) * 0.62), 0)
                   scale(1.22); opacity: 1; }
  100% { transform: translate3d(calc(var(--swing-x, 0px) * 0.15),
                                calc(var(--deliver-y, 180px) + var(--hop-y, 0px)), 0)
                   scale(1.35); opacity: 1; }
}
@keyframes ball-launch-photo {
  0%   { transform: translate3d(0, 0, 0) scale(1.3); opacity: 1; }
  30%  { transform: translate3d(calc(var(--fly-x, 0px) * 0.30),
                                calc(var(--fly-y, -300px) * 0.38), 0) scale(1); opacity: 1; }
  65%  { transform: translate3d(calc(var(--fly-x, 0px) * 0.68),
                                calc(var(--fly-y, -300px) * 0.72), 0) scale(0.7); opacity: 1; }
  100% { transform: translate3d(var(--fly-x, 0px), var(--fly-y, -300px), 0) scale(0.38); opacity: 0; }
}
@keyframes ball-boundary-photo {
  0%   { transform: translate3d(0, 0, 0) scale(1.3); opacity: 1; }
  100% { transform: translate3d(var(--fly-x, -120px), var(--fly-y, -80px), 0) scale(0.65); opacity: 0; }
}

/* The timing marker's sweep is driven from JS (Web Animations API, see
   startSweep in js/game.js), not a CSS @keyframes. It used to live here as a
   keyframe whose travel came from calc(var(--tm-travel) * -1); that rendered
   only in Chromium and stayed still in Firefox/Safari, so it moved to JS with
   an explicit pixel distance that every engine animates the same way. */

@keyframes flash-pop {
  0%   { opacity: 0; transform: scale(0.55); }
  28%  { opacity: 1; transform: scale(1.14); }
  66%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.02); }
}

@keyframes outcome-in {
  0%   { opacity: 0; transform: scale(0.7); }
  12%  { opacity: 1; transform: scale(1.08); }
  24%  { transform: scale(1); }
  76%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.05); }
}

@keyframes shake {
  0%, 100% { transform: translate(0, 0); }
  18% { transform: translate(-7px, 3px); }
  36% { transform: translate(7px, -3px); }
  54% { transform: translate(-5px, 2px); }
  72% { transform: translate(5px, -2px); }
  88% { transform: translate(-2px, 1px); }
}
.pitch.shake { animation: shake 0.38s ease; }

/* Outcome-tuned variants — a four shouldn't rattle the screen as hard as a
   six, and a wicket should feel like the biggest jolt of the three. Same
   shape as .shake, scaled. */
@keyframes shake-soft {
  0%, 100% { transform: translate(0, 0); }
  20% { transform: translate(-3px, 1px); }
  40% { transform: translate(3px, -1px); }
  60% { transform: translate(-2px, 1px); }
  80% { transform: translate(2px, -1px); }
}
.pitch.shake-soft { animation: shake-soft 0.28s ease; }

@keyframes shake-hard {
  0%, 100% { transform: translate(0, 0); }
  15% { transform: translate(-11px, 5px); }
  32% { transform: translate(10px, -5px); }
  50% { transform: translate(-8px, 4px); }
  68% { transform: translate(7px, -3px); }
  84% { transform: translate(-3px, 2px); }
}
.pitch.shake-hard { animation: shake-hard 0.46s ease; }

@keyframes score-pop {
  0%   { transform: scale(1); }
  46%  { transform: scale(1.4); color: var(--gold-lt); }
  100% { transform: scale(1); }
}
.hud-value.pop { animation: score-pop 0.42s ease; }

@keyframes hit-pulse {
  0%, 100% {
    box-shadow: 0 12px 32px rgba(217,35,46,0.5), 0 0 0 4px rgba(217,35,46,0.18),
                inset 0 2px 0 rgba(255,255,255,0.35), inset 0 -3px 10px rgba(0,0,0,0.35);
  }
  50% {
    box-shadow: 0 12px 38px rgba(217,35,46,0.7), 0 0 0 9px rgba(217,35,46,0.1),
                inset 0 2px 0 rgba(255,255,255,0.35), inset 0 -3px 10px rgba(0,0,0,0.35);
  }
}

@keyframes pill-pulse {
  0%, 100% { box-shadow: 0 0 14px rgba(217,35,46,0.6); }
  50% { box-shadow: 0 0 24px rgba(217,35,46,0.95); }
}

/* ---------------------------- BOWLING DELIVERY ----------------------------
   The batting keyframes grow the ball as it comes at you (0.55 -> 1.35).
   Bowling sends it away, so the scale runs the other way and --deliver-y
   is negative. Same duration and easing, so the timing meter still lines
   up exactly with what the eye sees. */
/* `--deliver-x` carries the ball in from the bowler's hand to the centre line,
   because the release point is off to his side while the far wicket is centred.
   It is 0 in the first-person view, where the ball already starts centred. */
/* `--arc-y` is the mid-flight lift and `--hop-y` the climb off the pitch after
   the bounce. The hop used to peak just past the bounce and return to 0 at the
   bat, which made the ball dip back down onto the straight line — the delivery
   read as dying after pitching. A real ball climbs from the moment it lands
   until it reaches the batsman, so --hop-y now GROWS to its full value at the
   end (0.62 -> 0.84 -> 1.0) and the ball arrives above the line, at about
   stump height rather than on the ground. --arc-y exists because releasing from the hand
   makes the delivery almost flat on screen — hand height and the far batsman's
   height are nearly the same — so without an arc the ball reads as sliding
   sideways rather than being bowled away. It is 0 in the first-person view,
   which already has a long vertical travel of its own. */
/* The delivery PITCHES. `BOUNCE_AT` (0.733) is where the straight line from
   the release point to the batsman crosses the target ring, so the ball
   touches down exactly on the mark the player was aiming at — game.js derives
   the ring's depth from the same constant, and the two cannot drift apart.

   Keyframe percentages equal the down-pitch fraction and the timing function
   stays linear, so `--deliver-y * <that fraction>` keeps the recede perfectly
   even; only the ADDED height term arcs. Height is therefore 0 at both ends
   (the endpoints are already the hand and the bat) and 0 at the bounce (the
   ball is on the ground there) — it is negative in between, which is up the
   frame and reads as "above the pitch". */
/* `--swing-x` is lateral movement THROUGH THE AIR, set per delivery so no two
   balls are identical. It is layered on top of --deliver-x (which is just the
   fixed carry from the bowler's hand to the centre line) and grows as a
   fraction of the flight rather than linearly: conventional swing builds late,
   as the ball slows, so most of the deviation appears in the second half.
   0 on a straight ball, negative for one leaving the batsman.

   It is a CURVE, not a drift: the deviation peaks in the air (0.85 at 40%),
   is already coming back by the time the ball pitches (0.55) and is 0 at the
   stumps. Ending on the full deviation made every delivery finish ~25px wide
   of the wicket on a diagonal — the ball was aimed past the stumps rather
   than at them. Swinging out and straightening onto the stumps is both what
   a real delivery does and what lets the wicket actually be hit. game.js
   offsets the pitch mark by the same 0.55 so the ring stays where the ball
   truly lands. */
@keyframes ball-deliver-bowl {
  0%   { transform: translate3d(0, 0, 0) scale(1.25); opacity: 1; }
  40%  { transform: translate3d(calc(var(--deliver-x, 0px) * 0.40 + var(--swing-x, 0px) * 0.85),
                                calc(var(--deliver-y, -200px) * 0.40 + var(--arc-y, 0px)), 0)
                   scale(1.02); opacity: 1; }
  /* pitching — on the ground, no lift */
  73.3% { transform: translate3d(calc(var(--deliver-x, 0px) * 0.733 + var(--swing-x, 0px) * 0.55),
                                calc(var(--deliver-y, -200px) * 0.733), 0)
                   scale(0.86); opacity: 1; }
  84%  { transform: translate3d(calc(var(--deliver-x, 0px) * 0.84 + var(--swing-x, 0px) * 0.34),
                                calc(var(--deliver-y, -200px) * 0.84 + var(--hop-y, 0px) * 0.62), 0)
                   scale(0.80); opacity: 1; }
  92%  { transform: translate3d(calc(var(--deliver-x, 0px) * 0.92 + var(--swing-x, 0px) * 0.14),
                                calc(var(--deliver-y, -200px) * 0.92 + var(--hop-y, 0px) * 0.84), 0)
                   scale(0.75); opacity: 1; }
  100% { transform: translate3d(var(--deliver-x, 0px),
                                calc(var(--deliver-y, -200px) + var(--hop-y, 0px)), 0)
                   scale(0.70); opacity: 1; }
}
@keyframes ball-deliver-photo-bowl {
  0%   { transform: translate3d(0, 0, 0) scale(1.15); opacity: 1; }
  40%  { transform: translate3d(calc(var(--deliver-x, 0px) * 0.40 + var(--swing-x, 0px) * 0.85),
                                calc(var(--deliver-y, -180px) * 0.40 + var(--arc-y, 0px)), 0)
                   scale(0.95); opacity: 1; }
  /* pitching — on the ground, no lift */
  73.3% { transform: translate3d(calc(var(--deliver-x, 0px) * 0.733 + var(--swing-x, 0px) * 0.55),
                                calc(var(--deliver-y, -180px) * 0.733), 0)
                   scale(0.76); opacity: 1; }
  84%  { transform: translate3d(calc(var(--deliver-x, 0px) * 0.84 + var(--swing-x, 0px) * 0.34),
                                calc(var(--deliver-y, -180px) * 0.84 + var(--hop-y, 0px) * 0.62), 0)
                   scale(0.70); opacity: 1; }
  92%  { transform: translate3d(calc(var(--deliver-x, 0px) * 0.92 + var(--swing-x, 0px) * 0.14),
                                calc(var(--deliver-y, -180px) * 0.92 + var(--hop-y, 0px) * 0.84), 0)
                   scale(0.63); opacity: 1; }
  100% { transform: translate3d(var(--deliver-x, 0px),
                                calc(var(--deliver-y, -180px) + var(--hop-y, 0px)), 0)
                   scale(0.55); opacity: 1; }
}
.pitch.bowl-mode .ball.deliver { animation-name: ball-deliver-bowl; }
.pitch.bowl-mode.photo .ball.deliver { animation-name: ball-deliver-photo-bowl; }

/* Pitch target — breathing pulse while the ball is live, and a flare when
   the delivery actually lands on the mark. */
@keyframes bt-pulse {
  0%, 100% { opacity: 0.55; transform: translate(-50%, -50%) scale(1); }
  50%      { opacity: 1;    transform: translate(-50%, -50%) scale(1.07); }
}
@keyframes bt-flare {
  0%   { opacity: 1;   transform: translate(-50%, -50%) scale(1); }
  55%  { opacity: 0.9; transform: translate(-50%, -50%) scale(1.45); }
  100% { opacity: 0;   transform: translate(-50%, -50%) scale(1.75); }
}

/* Bat swing impulse — a short weight-shift on the batsman container while
   the sprite frames swap. The frames already carry the bat angle, so this
   only supplies momentum; rotating the artwork on top would double it up.
   translateX(-50%) must be repeated in every step or the sprite jumps out
   of its centred position for the duration of the animation. */
@keyframes bat-swing {
  0%   { transform: translateX(-50%) translateY(0)      scale(1); }
  20%  { transform: translateX(-50%) translateY(-1.8%)  scale(1.035); }
  52%  { transform: translateX(-50%) translateY(0.6%)   scale(0.994); }
  100% { transform: translateX(-50%) translateY(0)      scale(1); }
}
.batsman.swinging { animation: bat-swing 300ms cubic-bezier(0.22, 0.9, 0.3, 1); }

/* The BOWL-mode bowler running in, seen from behind him.
   He starts below the frame — behind the camera, where a bowler's run-up
   actually begins — and arrives at the crease. Moving away from the lens, so
   he shrinks slightly as he goes: he was simply appearing at the crease
   already in position, which gave the run-up no travel at all, only frame
   swaps. Duration comes from the delivery (see --fg-runup-ms in game.js),
   because the run-up stretches to cover the timing sweep on slow balls. */
/* Where he waits: back at his mark, nearest the camera, cut off at the knees
   by the bottom of frame — which is what standing behind the lens looks like.
   The run-in starts from exactly this transform, so pressing does not teleport
   him below the frame first. */
.bowler-fg.at-mark { transform: translateY(36%) scale(1.2); }

/* Idle at the mark — the bowler shifting his weight, waiting to be told to
   run in. Same job as the batsman's sway: in AWAITING_FACE nothing else on
   screen is moving, and a still figure reads as a frozen frame.
   The keyframes carry .bowler-fg.at-mark's own transform, because an
   animation replaces the property rather than adding to it — drop it and he
   would snap to the crease the moment the sway started. */
@keyframes bowler-fg-idle {
  0%   { transform: translateY(36%)   scale(1.2) translateX(-1.8%); }
  50%  { transform: translateY(37.2%) scale(1.2) translateX(1.8%); }
  100% { transform: translateY(36%)   scale(1.2) translateX(-1.8%); }
}
.bowler-fg.at-mark.idle { animation: bowler-fg-idle 2800ms ease-in-out infinite; }

@media (prefers-reduced-motion: reduce) {
  .bowler-fg.at-mark.idle { animation: none; }
}

@keyframes bowler-fg-runin {
  0%   { transform: translateY(36%) scale(1.2); }
  100% { transform: translateY(0)   scale(1); }
}
.bowler-fg.running {
  animation: bowler-fg-runin var(--fg-runup-ms, 900ms) cubic-bezier(0.3, 0.55, 0.4, 1) both;
}

@media (prefers-reduced-motion: reduce) {
  .bowler-fg.running { animation: none; }
  .bowler-fg.at-mark { transform: none; }
}

/* Bowling's equivalent of the backlift: the bowler is coiled while the press
   is held and delivers on the release. Same contract as .batsman.backlift. */
.bowler-fg.loading {
  transform: translateY(-1.5%) scale(1.04);
  transition: transform 120ms cubic-bezier(0.3, 0.9, 0.4, 1);
  transform-origin: 50% 100%;
}
@media (prefers-reduced-motion: reduce) {
  .bowler-fg.loading { transition: none; }
}

/* Idle sway — the batsman shifting his weight at the crease while the game
   waits to be started. Nothing is happening on screen in AWAITING_FACE, and a
   completely still figure reads as a frozen frame rather than a player waiting
   for a ball. Small and slow on purpose: it must not look like the start of a
   shot, and it must not draw the eye away from the pulsing prompt.

   translateX(-50%) is repeated in every step, as everywhere else on this
   element, or he jumps out of his centred position for the duration. */
@keyframes bat-idle {
  0%   { transform: translateX(-50%) translateX(-2.4%) rotate(-0.5deg); }
  50%  { transform: translateX(-50%) translateX(2.4%)  rotate(0.5deg); }
  100% { transform: translateX(-50%) translateX(-2.4%) rotate(-0.5deg); }
}
.batsman.idle { animation: bat-idle 2600ms ease-in-out infinite; }

@media (prefers-reduced-motion: reduce) {
  .batsman.idle { animation: none; }
}

/* Settling back to guard after a shot. He is already dropping the bat through
   the backlift frame (see swingBat's recovery); this is the weight coming back
   onto the front foot underneath it, so the reset reads as a movement rather
   than a frame swap. Deliberately smaller than the swing impulse — it is the
   quiet beat between balls, not part of the shot. */
@keyframes bat-settle {
  0%   { transform: translateX(-50%) translateY(-1.6%) scale(1.02); }
  55%  { transform: translateX(-50%) translateY(0.5%)  scale(0.995); }
  100% { transform: translateX(-50%) translateY(0)     scale(1); }
}
.batsman.settling { animation: bat-settle 420ms cubic-bezier(0.25, 0.85, 0.35, 1); }

@media (prefers-reduced-motion: reduce) {
  .batsman.settling { animation: none; }
}

/* Backlift — held while the player holds the press. The shot is not played
   until they let go (see playerCharge() in js/game.js), so this has to read as
   "loaded", not as "swinging": weight back, a small lift, and it STAYS there. */
.batsman.backlift {
  transform: translateX(-50%) translateY(-2%) rotate(-3deg) scale(1.03);
  transition: transform 110ms cubic-bezier(0.3, 0.9, 0.4, 1);
  transform-origin: 50% 90%;
}
.batsman.backlift.swinging { animation: none; }
/* A long hold plays as a lofted drive — cosmetic only, it scores the same. */
.batsman.lofted.swinging { animation: bat-swing-lofted 340ms cubic-bezier(0.2, 0.95, 0.3, 1); }

@keyframes bat-swing-lofted {
  0%   { transform: translateX(-50%) translateY(-2%) rotate(-4deg) scale(1.03); }
  35%  { transform: translateX(-50%) translateY(-4%) rotate(6deg)  scale(1.07); }
  100% { transform: translateX(-50%) translateY(0)   rotate(0deg)  scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .batsman.backlift { transition: none; }
  .batsman.lofted.swinging { animation: none; }
}

@media (prefers-reduced-motion: reduce) {
  .batsman.swinging { animation: none; }
}
