/* ============================================================
   GAMEPLAY SCREEN — HUD, pitch, timing meter, HIT, overlays
   ============================================================ */

.game-screen {
  justify-content: space-between;
  padding: calc(10px + var(--safe-t)) 10px calc(12px + var(--safe-b));
  gap: 8px;
}

/* ------------------------------ HUD ------------------------------ */

.hud {
  display: flex; gap: 6px;
  width: 100%; max-width: 480px;
  flex-shrink: 0;
}
.hud-tile {
  flex: 1; min-width: 0;
  /* No backdrop-filter here: four blurred tiles are a constant GPU cost for
     the whole innings. A more opaque fill reads the same over the backdrop. */
  background: rgba(6,20,38,0.94);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 6px 4px 7px;
  text-align: center;
}
.hud-label {
  display: block;
  font-family: var(--f-ui); font-weight: 500;
  font-size: 0.52rem; letter-spacing: 1.4px;
  color: var(--tx-3);
}
.hud-value {
  display: block;
  font-family: var(--f-mega); font-size: 1.45rem;
  line-height: 1.05; color: #fff;
}
.hud-tile:nth-child(1) .hud-value,
.hud-tile:nth-child(2) .hud-value { color: var(--gold); }

/* ------------------------------ PITCH ------------------------------ */

.pitch {
  position: relative;
  width: 100%; max-width: 480px;
  flex: 1; min-height: 260px;
  border-radius: 16px;
  overflow: hidden;
  border: 1px solid var(--line);
  background: linear-gradient(to bottom, #0a1c30 0%, #123a1c 32%, #1c5a22 100%);
  touch-action: none;
  box-shadow: 0 18px 44px rgba(0,0,0,0.5);
}

/* ---------- PHOTO PITCH ----------
   The supplied stadium art is a batting-view pitch, so in photo mode it
   replaces the drawn turf/strip/crease/stumps/crowd entirely and only the
   moving pieces (ball, bowler, batsman, hitting zone) sit on top.
   `.photo` is applied by app.js once the image loads. */
.pitch.photo { background: #0d2f14; }
/* On the camera layer, not .pitch, so it scales with the big-shot zoom. */
.pitch.photo .pitch-cam {
  background: var(--pitch-img) center 58%/cover no-repeat;
}
.pitch.photo .pitch-crowd,
.pitch.photo .pitch-turf,
.pitch.photo .pitch-strip,
.pitch.photo .crease,
.pitch.photo .stumps-far,
.pitch.photo .stumps-near,
.pitch.photo .fielder { display: none; }

/* Geometry recalibrated to the photo's perspective: the far wicket sits
   around 58% and the near crease around 84% of the frame. */
/* Bowler stands at the far wicket, which the photo puts around 53%.
   Small, to respect the photo's perspective at that distance. */
/* Sprites are sized as a share of the panel height, not fixed pixels, so
   they keep the same scale on a 568px-tall phone as on a tall viewport. */
.pitch.photo .bowler { top: 45%; height: 9%; }
.pitch.photo .ball { top: 54%; }
.pitch.photo .ball.deliver { animation-name: ball-deliver-photo; }
.pitch.photo .ball.launch { animation-name: ball-launch-photo; }
.pitch.photo .ball.boundary { animation-name: ball-boundary-photo; }
.pitch.photo .hitting-zone { bottom: 15%; width: 104px; }
/* Stumps stand ON the near crease line — the white mark stadium.jpg paints
   at pixel row 1599 of its 1672. (The photo shows two crease lines: a wide
   one at row 1458 and this nearer one at 1599; the stumps belong on 1599.)

   That row cannot be a fixed `bottom: %`. The backdrop is `cover` at
   `center 58%`, so the image is SCALED by the panel width but POSITIONED
   against its height; the line therefore drifts as the panel aspect ratio
   changes, and any single percentage is only right at one size. Solving
   `cover` + `58%` for an image row Y gives:

       bottom = 42% of height  +  100 x (0.58 x 1672 - Y) / 941  % of width

   For Y = 1599 that constant is -66.8693, hence the calc below. Container
   units express it directly (cqh/cqw = 1% of the .pitch-cam layer), so the
   stumps stay welded to the line at any panel size. Re-derive with the
   formula above if you swap the photo, move the crease, or change `58%`.

   The `max()` is a floor, not a fudge. This line sits low in frame, so on a
   short, wide panel (a 320x568 phone gets ~298x361) `cover` crops it clean
   off the bottom and the raw value goes negative. There the line simply is
   not in shot, so the stumps rest on the bottom edge instead.

   Width follows from the sprite own 0.287 aspect, which matches real stumps
   (9in across, 28in tall), so this reads at true scale.

   z-index 4 puts the stumps IN FRONT of the batsman (3). That matches the
   depth the crease line implies: the stumps base sits lower in frame than
   his feet, so they are the nearer object and must occlude him, not the
   reverse. Still under the ball (5), which passes in front of everything. */
.pitch.photo .pitch-stumps {
  display: block;
  position: absolute;
  left: 50%;
  bottom: 3%; /* fallback for engines without container units */
  bottom: max(0%, calc(42cqh - 66.8693cqw));
  height: 15%; width: auto;
  transform: translateX(-50%);
  transform-origin: 50% 100%;
  z-index: 4;
  filter: drop-shadow(0 4px 8px rgba(0,0,0,0.55));
}
/* Stumps rattled — only meaningful now the photo has none baked in. */
.pitch.photo .pitch-stumps.broken {
  animation: stumps-topple 0.85s cubic-bezier(0.25, 0.8, 0.35, 1) forwards;
}

/* Batter stands beside the stumps and slightly across.
   z-index 3 puts him BEHIND them (4) — see the stumps block above; they are
   the nearer object, so they occlude him. The wicket therefore always reads
   in full whatever `left` is set to, which is what the topple on a wicket
   needs; moving him now only changes how much of HIM the stumps cover. */
.pitch.photo .batsman { bottom: 5%; left: 40%; height: 35.5%; z-index: 3; }
.pitch.photo .pitch-cam.zoom { transform: scale(1.28); }

/* Camera layer — scales the whole scene for big-shot zoom. */
.pitch-cam {
  position: absolute; inset: 0;
  /* A size container so the stumps can be placed off BOTH panel dimensions.
     Safe here: the layer is inset:0, so its size never depends on contents.
     Size containment implies layout+style, not paint, so nothing new clips. */
  container-type: size;
  transform-origin: 50% 82%;
  transition: transform 0.45s cubic-bezier(0.2, 0.9, 0.3, 1);
  will-change: transform;
}
.pitch-cam.zoom { transform: scale(1.38); }
.pitch-cam.zoom-soft { transform: scale(1.16); }

.pitch-crowd {
  position: absolute; top: 0; left: 0; right: 0; height: 13%;
  background:
    repeating-linear-gradient(90deg,
      rgba(217,35,46,0.32) 0 1.4%, rgba(29,78,216,0.3) 1.4% 2.8%,
      rgba(247,197,21,0.22) 2.8% 4.2%, rgba(255,255,255,0.16) 4.2% 5.8%),
    linear-gradient(to bottom, rgba(3,10,20,0.72), rgba(3,10,20,0.92));
  filter: blur(3px);
}

/* Outfield with mown stripes receding into the distance */
.pitch-turf {
  position: absolute; top: 11%; left: -18%; right: -18%; bottom: -14%;
  border-radius: 50% 50% 0 0 / 22% 22% 0 0;
  background:
    repeating-linear-gradient(to bottom,
      rgba(255,255,255,0.045) 0 4%, transparent 4% 9%),
    radial-gradient(ellipse at 50% 96%, #3a8a40 0%, #23682a 46%, #12401a 100%);
}

/* Batting strip in perspective */
.pitch-strip {
  position: absolute; left: 50%; bottom: 0;
  transform: translateX(-50%);
  width: 46%; height: 78%;
  background: linear-gradient(to top, #d9c489 0%, #c4ad6c 48%, #9d8a4d 100%);
  clip-path: polygon(16% 100%, 84% 100%, 61% 0%, 39% 0%);
  opacity: 0.92;
  box-shadow: 0 0 40px rgba(0,0,0,0.25);
}

.crease {
  position: absolute; left: 50%; bottom: 14%;
  transform: translateX(-50%);
  width: 34%; height: 2px;
  background: rgba(255,255,255,0.7);
  box-shadow: 0 -46px 0 -0.5px rgba(255,255,255,0.35);
}

/* Real stumps sprite. Hidden on the drawn pitch, which has its own .stumps. */
.pitch-stumps { display: none; }

.stumps { position: absolute; display: flex; gap: 3px; z-index: 3; }
.stumps span { width: 2.5px; background: #f2e8cf; border-radius: 1px; box-shadow: 0 1px 3px rgba(0,0,0,0.5); }
.stumps-far { top: 26%; left: 50%; transform: translateX(-50%); }
.stumps-far span { height: 20px; }
.stumps-near { bottom: 11%; left: 50%; transform: translateX(-50%); }
.stumps-near span { height: 32px; }

.fielder {
  position: absolute; top: 22%; right: 22%;
  font-size: 1.1rem; opacity: 0.75;
  filter: drop-shadow(0 3px 5px rgba(0,0,0,0.5));
}

/* Bowler and batsman are real sprites, anchored by their feet so the
   figure stays planted while frames of differing height swap in. */
.bowler {
  position: absolute; top: 24%; left: 50%;
  transform: translateX(-50%);
  z-index: 3; height: 9.5%;
}
.bowler img {
  display: block; height: 100%; width: auto;
  filter: drop-shadow(0 4px 8px rgba(0,0,0,0.55));
}
.bowler.bowling { animation: bowler-runup 0.5s ease-out; }

.batsman {
  position: absolute; bottom: 12%; left: 50%;
  transform: translateX(-50%);
  z-index: 4; height: 16%;
  display: flex; align-items: flex-end;
}
.batsman canvas {
  display: block; height: 100%; width: auto;
  filter: drop-shadow(0 6px 10px rgba(0,0,0,0.6));
}

/* Sized against the stumps, which are the only true-scale reference on the
   pitch. A real ball is ~0.10 of stump height; at 13px it read 0.16 and made
   the wicket look like a toy, and 11px was still visibly oversized at 0.12.
   9.5px is ~0.105 - essentially true scale. It survives hard difficulty only
   because the bowl-mode keyframes now start it near the camera and shrink it
   away, giving the eye a size cue the flat 11px dot never had. */
.ball {
  position: absolute; top: 28%; left: 50%;
  width: 9.5px; height: 9.5px; margin-left: -4.75px;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 28%, #ff7b7b, var(--np-red) 55%, var(--np-red-dk) 100%);
  box-shadow: 0 2px 8px rgba(0,0,0,0.6), 0 0 12px rgba(217,35,46,0.5);
  opacity: 0; z-index: 5;
  will-change: transform;
}
.ball.seam::after {
  content: ''; position: absolute; inset: 1px;
  border-radius: 50%; border: 1px dashed rgba(255,255,255,0.55);
}

/* Bat-ball contact flash, BAT mode only. A frame swap alone (swingBat())
   changes the sprite but the bat never visibly REACHES the ball — this is
   the missing "they touched" beat, a burst at the exact point game.js
   re-anchors the ball to before it flies off. Built from CSS only (radial
   glow + 5 spark rays via ::before/::after and box-shadow, not an image)
   so it stays crisp at any size and needs no asset. Sized/positioned like
   `.ball` (9.5px, centred via negative margin) so it sits exactly on it. */
.bat-impact {
  position: absolute; top: 28%; left: 50%;
  width: 9.5px; height: 9.5px; margin-left: -4.75px; margin-top: -4.75px;
  z-index: 6;                /* above the ball/bat, below the outcome banner */
  opacity: 0;
  pointer-events: none;
}
.bat-impact.burst {
  animation: bat-impact-flash 0.32s ease-out;
}
.bat-impact.burst::before {
  content: '';
  position: absolute; inset: -14px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255,255,255,0.95), rgba(247,197,21,0.55) 45%, transparent 72%);
}
/* 5 spark rays, evenly fanned, drawn once via box-shadow rather than 5
   separate elements — cheaper and there's nothing per-ray to animate
   differently. */
.bat-impact.burst::after {
  content: '';
  position: absolute; top: 50%; left: 50%;
  width: 3px; height: 3px; margin: -1.5px 0 0 -1.5px;
  border-radius: 50%;
  background: #fff8dd;
  box-shadow:
    0 -16px 0 -1px #fff8dd,
    15px -8px 0 -1px #fff8dd,
    15px 8px 0 -1px #fff8dd,
    -15px 8px 0 -1px #fff8dd,
    -15px -8px 0 -1px #fff8dd;
  animation: bat-impact-rays 0.32s ease-out;
}
@keyframes bat-impact-flash {
  0%   { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes bat-impact-rays {
  0%   { transform: scale(0.2); opacity: 1; }
  100% { transform: scale(1.6); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .bat-impact.burst { animation: none; }
  .bat-impact.burst::before, .bat-impact.burst::after { animation: none; opacity: 0; }
}

.hitting-zone {
  position: absolute; bottom: 24%; left: 50%;
  transform: translateX(-50%);
  width: 76px; height: 40px;
  border: 2px dashed rgba(247,197,21,0.4);
  border-radius: 12px;
  opacity: 0; transition: opacity 0.18s;
  z-index: 3;
}
.hitting-zone.active { opacity: 1; box-shadow: 0 0 22px rgba(247,197,21,0.22) inset; }

.difficulty-badge {
  position: absolute; top: 8px; right: 8px; z-index: 6;
  font-family: var(--f-ui); font-weight: 600;
  font-size: 0.55rem; letter-spacing: 1.6px;
  color: var(--gold);
  background: rgba(2,8,18,0.75);
  border: 1px solid var(--line-gold);
  padding: 3px 9px; border-radius: 999px;
}

/* ------------------------ IN-PITCH OVERLAYS ------------------------ */

.timing-flash {
  position: absolute; inset: 0; z-index: 7;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--f-mega); font-size: 1.9rem;
  letter-spacing: 2px; color: #fff;
  text-shadow: 0 0 22px currentColor, 0 3px 0 rgba(0,0,0,0.5);
  opacity: 0; pointer-events: none;
}
.timing-flash.show { animation: flash-pop 0.62s ease forwards; }

/* Cinematic SIX / FOUR / OUT display */
.outcome {
  position: absolute; inset: 0; z-index: 9;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  gap: 2px; opacity: 0; pointer-events: none;
  /* Deliberately no backdrop-filter: this overlay covers the whole pitch and
     is on screen for over a second while up to 54 confetti pieces animate.
     Blurring that area every frame was the single worst gameplay cost; a
     heavier scrim gives the same separation for free. */
  background: radial-gradient(ellipse at 50% 50%, rgba(2,8,18,0.62), rgba(2,8,18,0.90));
}
.outcome.show { animation: outcome-in 1.25s ease forwards; }
.outcome.wicket { background: radial-gradient(ellipse at 50% 50%, rgba(142,18,25,0.55), rgba(2,8,18,0.9)); }

.outcome-kicker {
  font-family: var(--f-mega); font-size: 1.5rem;
  letter-spacing: 2px; color: #6ee87a;
  text-shadow: 0 0 20px rgba(110,232,122,0.6), 0 3px 0 rgba(0,0,0,0.5);
}
.outcome-main {
  font-family: var(--f-mega);
  font-size: clamp(3.4rem, 23cqw, 6.2rem);
  line-height: 0.9; letter-spacing: 2px;
  background-image: linear-gradient(180deg, #fff8d4 0%, var(--gold) 42%, #d19b00 72%, #7d5c00 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  filter: drop-shadow(0 5px 0 rgba(2,8,18,0.6)) drop-shadow(0 10px 20px rgba(0,0,0,0.6));
}
.outcome.wicket .outcome-main {
  background-image: linear-gradient(180deg, #ffb3b3 0%, var(--np-red) 45%, #7a0f16 100%);
}
.outcome.wicket .outcome-kicker { display: none; }
.outcome-sub {
  font-family: var(--f-ui); font-weight: 600;
  font-size: 1rem; letter-spacing: 2px; color: #fff;
  margin-top: 4px;
  text-shadow: 0 2px 8px rgba(0,0,0,0.7);
}

.confetti-layer { position: absolute; inset: 0; z-index: 10; pointer-events: none; overflow: hidden; }
.confetto { position: absolute; top: -14px; width: 7px; height: 12px; opacity: 0.9; animation: confetti-fall linear forwards; }

.pause-overlay {
  position: absolute; inset: 0; z-index: 12;
  display: none; flex-direction: column;
  align-items: center; justify-content: center; gap: 12px;
  background: rgba(2,8,18,0.9);
  backdrop-filter: blur(6px);
}
.pause-overlay.show { display: flex; }
.pause-overlay h3 {
  font-family: var(--f-mega); font-size: 2rem;
  letter-spacing: 3px; margin: 0 0 6px; color: var(--gold);
}
.pause-overlay .btn { min-width: 180px; padding: 12px; }

/* ------------------------ TIMING METER ------------------------ */

.timing-meter { width: 100%; max-width: 480px; flex-shrink: 0; }

.tm-track {
  position: relative;
  display: flex; align-items: stretch;
  height: 26px; border-radius: 7px; overflow: hidden;
  border: 1px solid rgba(255,255,255,0.14);
  box-shadow: 0 4px 14px rgba(0,0,0,0.45);
}
/* Widths come from app.js (inline flex-basis), never from here -- they are
   derived from ScoringConfig so the bar cannot drift out of step with the
   scoring again. Only the colours live in CSS. */
.tm-seg {
  display: flex; align-items: center; justify-content: center;
  flex-grow: 0; flex-shrink: 0;
  overflow: hidden; white-space: nowrap;
  font-family: var(--f-ui); font-weight: 600;
  font-size: 0.58rem; letter-spacing: 1.4px; color: #fff;
  text-shadow: 0 1px 3px rgba(0,0,0,0.6);
}
.tm-seg-early,
.tm-seg-late { background: linear-gradient(180deg, #e0524f, #a3242c); }
.tm-seg-early_good,
.tm-seg-late_good { background: linear-gradient(180deg, #6fa8dc, #3b6ea5); }
.tm-seg-perfect { background: linear-gradient(180deg, #4fd463, #24a13a); }
/* The risk bands are narrow; drop their labels rather than clip them. */
.tm-seg-early, .tm-seg-late { font-size: 0.5rem; letter-spacing: 0.6px; }

.tm-indicator {
  position: absolute; top: -3px; left: 0;
  width: 3px; height: calc(100% + 6px);
  background: #fff; border-radius: 2px;
  box-shadow: 0 0 10px rgba(255,255,255,0.95), 0 0 22px rgba(255,255,255,0.5);
  z-index: 2;
  will-change: transform;
}
.tm-indicator.sweep { animation: indicator-sweep linear forwards; }

/* ------------------------ HIT BUTTON / FOOTER ------------------------ */

.game-footer {
  display: flex; align-items: center; gap: 12px;
  width: 100%; max-width: 480px;
  flex-shrink: 0;
}

.btn-hit {
  flex: 1;
  padding: 19px;
  font-family: var(--f-mega);
  font-size: 1.85rem; letter-spacing: 5px;
  color: #fff;
  border-radius: 999px;
  background:
    radial-gradient(ellipse at 50% 20%, rgba(255,255,255,0.35), transparent 60%),
    linear-gradient(180deg, #ff4d4d 0%, var(--np-red) 45%, var(--np-red-dk) 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);
  text-shadow: 0 2px 6px rgba(0,0,0,0.45);
  animation: hit-pulse 1.8s ease-in-out infinite;
}
.btn-hit:disabled { opacity: 0.4; animation: none; }
.btn-hit:active { transform: scale(0.94); }

@media (min-height: 720px) {
  .pitch { min-height: 320px; }
}
@media (min-width: 768px) {
  .hud, .pitch, .timing-meter, .game-footer { max-width: 560px; }
  .btn-hit { font-size: 2.1rem; padding: 22px; }
}

/* ============================ BOWLING MODE ============================
   Bowling is played FIRST-PERSON: the player is the bowler, so their own
   figure is never drawn. That is deliberate and not a missing sprite --
   it is also why bowling needed no new artwork.

   What changes on the pitch:
     - the near batsman is hidden (you are standing where he stood),
     - the far-end figure becomes the opposing batsman rather than a
       bowler running in,
     - the ball travels AWAY up the pitch (see the -bowl keyframes).
   The near stumps stay: they are the bowler's-end wicket and give the
   delivery something to leave from. */

.pitch.bowl-mode .batsman { display: none; }

/* The far figure is now a real front-on batsman sprite, so the bowler sprite
   no longer has to stand in for one. */
.pitch.bowl-mode .bowler { display: none; }

/* Opposing batsman at the far crease. Hidden outside bowl mode, and hidden
   until a frame is set so a missing asset degrades to an empty crease rather
   than a broken image. */
.far-batsman {
  position: absolute;
  left: 50%; top: 43%;
  height: 11%;
  transform: translateX(-50%);
  display: none;
  align-items: flex-end;
  z-index: 3;
}
.pitch.bowl-mode .far-batsman:has(img[src]) { display: flex; }

.far-batsman img {
  display: block; height: 100%; width: auto;
  filter: drop-shadow(0 4px 7px rgba(0,0,0,0.5));
}

/* Mode badge, mirroring .difficulty-badge on the opposite corner. */
.mode-badge {
  position: absolute; top: 10px; left: 10px;
  z-index: 6;
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(6,20,38,0.86);
  border: 1px solid var(--line);
  font-family: var(--f-ui); font-weight: 700;
  font-size: 0.55rem; letter-spacing: 1.2px;
  color: var(--gold);
  pointer-events: none;
}

/* ==================== BOWLING — OVER-THE-SHOULDER VIEW ====================
   Reworked to match the supplied reference: the camera sits BEHIND the
   bowler, not at their eyes.

   This is not a cosmetic preference. First-person bowling means the player
   never sees their own jersey, which silently defeats the personalised-name
   feature. Putting the camera over the shoulder is what makes that name
   visible in bowl mode, so the two features are designed together.

   Requires artwork we do not yet have: a bowler seen FROM BEHIND, arm
   raised, with a clear flat back panel for the name and number.
   `assets/images/baller.png` is front-on and cannot stand in. Until that
   sprite lands, `.bowler-fg` has no image and the view falls back to the
   first-person framing, which still plays correctly.
   ------------------------------------------------------------------------ */

/* The pitch target — the glowing length marker the delivery aims at.
   Drawn as three flattened ellipses rather than an image so its depth can
   move per delivery: a yorker's ring sits deep near the batsman, a bouncer's
   sits short. `--target-depth` is the only thing game.js needs to set. */
.bowl-target {
  --target-depth: 62%;   /* distance down the pitch, set per delivery */
  --target-size: 30%;    /* width as a share of the panel */
  position: absolute;
  left: 50%;
  top: var(--target-depth);
  width: var(--target-size);
  aspect-ratio: 1;
  transform: translate(-50%, -50%) scaleY(0.34); /* flatten into perspective */
  transform-origin: 50% 50%;
  display: none;
  z-index: 2;            /* on the turf, under ball, stumps and players */
  pointer-events: none;
}
.pitch.bowl-mode .bowl-target { display: block; }

.bowl-target .bt-ring,
.bowl-target .bt-core {
  position: absolute;
  left: 50%; top: 50%;
  border-radius: 50%;
  transform: translate(-50%, -50%);
}
.bowl-target .bt-ring-outer {
  width: 100%; height: 100%;
  border: 2px solid rgba(90, 200, 255, 0.55);
  box-shadow: 0 0 18px rgba(90, 200, 255, 0.45),
              inset 0 0 22px rgba(90, 200, 255, 0.25);
}
.bowl-target .bt-ring-inner {
  width: 58%; height: 58%;
  border: 2px solid rgba(120, 215, 255, 0.75);
  box-shadow: 0 0 14px rgba(120, 215, 255, 0.5);
}
.bowl-target .bt-core {
  width: 20%; height: 20%;
  background: radial-gradient(circle, rgba(190,240,255,0.95), rgba(90,200,255,0.35) 70%, transparent);
  box-shadow: 0 0 16px rgba(150, 225, 255, 0.8);
}

/* Breathes while the ball is live so the eye is drawn to the length. */
.bowl-target.live .bt-ring-outer { animation: bt-pulse 1.15s ease-in-out infinite; }
.bowl-target.live .bt-ring-inner { animation: bt-pulse 1.15s ease-in-out infinite 0.18s; }

/* Struck: the ring flares once when the ball lands on it. */
.bowl-target.hit .bt-ring-outer,
.bowl-target.hit .bt-ring-inner { animation: bt-flare 0.5s ease-out forwards; }

/* ---- Foreground bowler (over-the-shoulder) ----
   Anchored bottom-left like the reference, large and close to camera.
   Hidden automatically while it has no image, so the absence degrades to
   the first-person view rather than to a broken-image box. */
/* Pulled back down the pitch and scaled down to match. Moving "back" in this
   view means smaller AND higher up the frame, since the ground recedes
   upwards — changing only the height would just shrink him in place. He also
   drifts toward the centre line, because the pitch converges with distance. */
/* Standing at his own crease, wide of the stumps — feet at ~79% of the panel,
   just above the near crease line at ~88%, and offset left so the stumps and
   the pitch target stay unobscured.
   He is deliberately NOT mid-pitch: from there he both blocks the far batsman
   and reads as though he has already run past the crease. */
/* The sprite frame AND the personalised name/number are baked together into
   this canvas's pixels by drawBowlerFrame() in js/animations.js — see
   docs/features/stadium-and-art.md for why (replaced a separate img plus
   two live-CSS-transformed text spans). `display` is toggled via `.ready`,
   added once the first frame has actually been drawn — a canvas has no
   `src` attribute to gate visibility on the way `:has(img[src])` did. */
.bowler-fg {
  position: absolute;
  left: 16%; bottom: 4%;
  height: 34%;
  width: auto;
  display: none;
  z-index: 6;            /* nearest object in the scene */
  pointer-events: none;
  filter: drop-shadow(0 10px 18px rgba(0,0,0,0.55));
}
.pitch.bowl-mode .bowler-fg.ready { display: block; }

/* Bowl mode hides the near batsman either way — you are standing where he
   stood. The far figure is the opposing batsman. */
.pitch.bowl-mode .batsman { display: none; }
