:root {
  --cream:        #F4EBD9;
  --cream-light:  #FFF8E7;
  --brown:        #3D2914;
  --outline:      #1a0f08;
  --mustard:      #FFCC00;
  --purple:       #512d6d;
  --warm-red:     #C8553D;
  --soft-red:     #9d2c2c;
  --road:         #C9A47A;
  --wood:         #8B5A2B;
  --wood-dark:    #6b4a2e;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: Georgia, 'Times New Roman', serif;
  color: var(--brown);
  background-color: var(--cream);
  background-image: radial-gradient(rgba(0,0,0,0.05) 1px, transparent 1px);
  background-size: 4px 4px;
}

#board { max-width: 1100px; margin: 0 auto; padding: 8px 16px 32px; }

/* Sticky panel: hosts the round indicator + the top number strip. Sticks to the
   viewport top on page scroll. Body/document is the vertical scrolling context,
   which is why we deliberately keep sticky OUT of #board-scroll (whose
   overflow-x: auto is a horizontal scrolling context and would confine sticky). */
#sticky-panel {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--cream);
  padding-top: 6px;
  border-bottom: 1px dashed rgba(61,41,20,0.18);
  /* Extend the cream background across the tiny gap between the panel and the
     dotted page bg pattern behind, so text underneath doesn't peek through. */
  box-shadow: 0 4px 6px -5px rgba(0,0,0,0.18);
}

/* Round indicator: pill + trailing italic sentence, kept out of the horizontal
   scroller so it never drifts sideways. */
.round-info {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 4px 4px 8px;
  min-height: 22px;               /* prevent layout jump before data arrives */
}
.round-info .round-badge {
  font-family: 'Archivo Black', 'Impact', sans-serif;
  font-size: 12px;
  letter-spacing: 1.5px;
  background: var(--mustard);
  color: var(--brown);
  border: 2px solid var(--brown);
  border-radius: 999px;
  padding: 2px 10px 3px;
  text-transform: uppercase;
}
.round-info .round-tail,
.round-info .round-pipe {
  font-family: Georgia, serif;
  font-style: italic;
  font-size: 14px;
  color: var(--brown);
  opacity: 0.85;
}

/* Top strip lives in its own overflow-x container so it can be synced with
   #board-scroll from JS. Scrollbar hidden — the row scroller below owns the
   visible scrollbar. */
.strip-wrap { overflow-x: auto; overflow-y: hidden; scrollbar-width: none; }
.strip-wrap::-webkit-scrollbar { display: none; height: 0; }

/* Single horizontal scroll container for the rows and the bottom strip. */
#board-scroll { overflow-x: auto; scrollbar-width: thin; }

/* Strips and rows share one min-width so their columns line up exactly.
   Bumped 950→980 (was 920→950) because the SVG viewBoxes were widened by
   another 30 units on the left (`-30 0 950 …` → `-60 0 980 …`) so the
   Hird "Coach?" speech bubble at abs x=-46 doesn't clip. */
.number-strip > svg, .row { display: block; width: 100%; height: auto; min-width: 980px; }
.row + .row { border-top: 1px dashed rgba(61,41,20,0.25); }

#site-footer {
  max-width: 1100px; margin: 0 auto; padding: 18px 16px 40px;
  font-size: 13px; color: var(--brown);
  display: flex; flex-wrap: wrap; gap: 10px 18px; align-items: center;
}
#site-footer .refresh-btn {
  font-family: Georgia, serif; font-style: italic; cursor: pointer;
  background: var(--mustard); border: 2px solid var(--brown); border-radius: 8px;
  padding: 4px 10px;
}

/* ============================ Header scene state ============================ */

/* The "You've got the touch" button toggles #header-svg between data-state="studio"
   (default brown-wood scene) and data-state="grouse" (the alternate dark/blue scene
   with redesigned hosts). Elements opt in to a single mode by class. */
#header-svg[data-state="studio"] .grouse-only { display: none; }
#header-svg[data-state="grouse"] .studio-only { display: none; }

/* Floating fan mirrors the same state pattern via its own data-state attribute
   (set from JS in lock-step with #header-svg). Uses display: none so the
   swap coincides cleanly with the header's mid-explosion moment. */
#floating-fan[data-state="studio"] .grouse-only { display: none; }
#floating-fan[data-state="grouse"] .studio-only { display: none; }

/* Fixed to the bottom-right of the viewport with medium-large padding, a
   subtle drop shadow so the character sits above the page pattern.
   pointer-events: none so the illustration doesn't intercept clicks in the
   footer area beneath it. */
/* Ronan container — fixed anchor for the whole floating illustration + its
   speech bubble. Hidden by default via translateX(180px) which parks him
   entirely off-screen right of the drop-shadow. .visible slides him in;
   .fading dips opacity for the exit transition. */
#ronan-container {
  position: fixed;
  right: 32px;
  bottom: 32px;
  z-index: 15;
  pointer-events: none;
  transform: translateX(180px);
  opacity: 1;
  /* Same 0.6s transition in both directions: .visible slides Ronan in,
     removing the class slides him back off to the right at full opacity. */
  transition: transform 0.6s cubic-bezier(.25,.46,.45,1);
}
#ronan-container.visible { transform: translateX(0); }

#floating-fan {
  position: relative;
  /* Was 80px (matching the CSS-pixel width of a host inside the header at
     max viewport). Scaled by 1.2 per the boys' feedback so Ronan reads
     a bit larger on-screen than the in-header hosts. */
  width: 96px;
  height: auto;
  display: block;
  filter: drop-shadow(0 6px 10px rgba(0,0,0,0.22));
}

/* Speech bubble sits above-and-left of Ronan, tail points down-right at his
   mouth. cream fill + dark border matches the site palette; opacity toggles
   the show/hide. white-space:normal + max-width so longer lines wrap. */
.ronan-bubble {
  position: absolute;
  /* Re-anchored after Ronan's 80→96 px scale-up. Container grew ~16 px
     wider and ~19 px taller (both fixed at bottom-right), so Ronan's
     head/mouth position in viewport coords moved ~8 px left + ~10 px up.
     Bumping right 50→60 and bottom 55→68 shifts the bubble to match, so
     the tail continues to point at his mouth. */
  right: 60px;
  bottom: 68px;
  padding: 10px 14px;
  min-width: 90px;
  max-width: 220px;
  background: var(--cream-light);
  border: 2px solid var(--outline);
  border-radius: 12px;
  font-family: Georgia, serif;
  font-size: 12px;
  font-weight: 700;
  color: var(--brown);
  text-align: center;
  line-height: 1.35;
  transition: opacity 0.3s ease-out;
  filter: drop-shadow(0 3px 6px rgba(0,0,0,0.18));
}
.ronan-bubble.hidden { opacity: 0; pointer-events: none; }
.ronan-bubble::after {
  content: '';
  position: absolute;
  right: -8px;
  bottom: 10px;
  width: 12px;
  height: 12px;
  background: var(--cream-light);
  border-right: 2px solid var(--outline);
  border-bottom: 2px solid var(--outline);
  transform: rotate(-45deg);
}

@media (max-width: 720px) {
  #floating-fan { width: 72px; }
  #ronan-container { right: 18px; bottom: 18px; transform: translateX(140px); }
  #ronan-container.visible { transform: translateX(0); }
  .ronan-bubble { right: 48px; bottom: 55px; font-size: 11px; max-width: 160px; padding: 8px 10px; }
}

/* Explosion / sad-reassembly animations rotate + translate each piece. Without
   transform-box: fill-box, transform-origin would resolve against the SVG
   viewBox origin (top-left) and pieces would arc wildly off-screen when spun.
   fill-box + transform-origin: center anchors rotation to each piece's own bbox,
   so spinning looks like tumbling debris. */
#header-svg .studio-only,
#header-svg .grouse-only {
  transform-box: fill-box;
  transform-origin: center;
}

/* Touch button: small affordances so users know it's interactive.
   pointer-events is also toggled inline by JS while an animation is running so
   users can't fire a second click mid-explosion. */
#touch-btn { transition: filter 150ms ease-out; }
#touch-btn:hover { filter: brightness(1.1); }

/* ============================ Card flip ============================ */

/* The flip transforms scaleX around the centre of each piece. fill-box anchors
   the origin to the element's own bounding box. */
.card-inner, .marker-inner { transform-box: fill-box; transform-origin: center; }

/* Flip toggle button (formerly .melb-toggle — now on every card). */
.card-toggle { cursor: pointer; transition: filter 150ms ease-out; }
.card-toggle:hover { filter: brightness(1.08); }
.card-toggle:focus { outline: none; }
.card-toggle:focus-visible circle { stroke: #9d2c2c; stroke-width: 2; }

/* OVER/UNDER original-prediction badge. Purely informational; no hover. */
.card-badge { pointer-events: none; }
.card-badge text { user-select: none; }

/* ============================ animations ============================ */

/* entrance: rows fade/slide in (per-row animation-delay set inline in renderRows) */
@keyframes rowIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.row { animation: rowIn 500ms ease-out both; }

/* idle: mascot gently bobs */
@keyframes bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-1px); } }
.mascot-body { animation: bob 2.4s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }

/* idle: win-tag subtly pulses (re-states its -42 base offset so it stays put) */
@keyframes tagPulse { 0%, 100% { transform: translateY(-42px) scale(1); } 50% { transform: translateY(-42px) scale(1.03); } }
.mascot-tag { animation: tagPulse 3s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }

/* celebrate: a hop when a new win is detected, then resume the idle bob */
@keyframes hop { 0%, 100% { transform: translateY(0); } 30% { transform: translateY(-8px); } 60% { transform: translateY(0); } }
.mascot.celebrate .mascot-body { animation: hop 600ms ease-out 1, bob 2.4s ease-in-out 600ms infinite; }

/* ---- target reached: faster, deeper happy bob + sparkles ---- */
@keyframes bob-happy { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
.mascot.target-reached .mascot-body { animation: bob-happy 0.7s ease-in-out infinite; }

@keyframes twinkle {
  0%, 100% { transform: scale(0.5); opacity: 0.4; }
  50%      { transform: scale(1.2); opacity: 1; }
}
.celebration-deco .spark { transform-box: fill-box; transform-origin: center; animation: twinkle 1.4s ease-in-out infinite; }
.celebration-deco .spark-r { animation-delay: 0.7s; }

/* ---- limit reached: slow slumped sway + dripping tears ---- */
@keyframes bob-sad { 0%, 100% { transform: translateY(2px); } 50% { transform: translateY(3px); } }
.mascot.limit-reached .mascot-body { animation: bob-sad 3s ease-in-out infinite; }

@keyframes tear-drip {
  0%   { transform: translateY(-3px); opacity: 0; }
  20%  { opacity: 1; }
  80%  { opacity: 1; }
  100% { transform: translateY(8px); opacity: 0; }
}
.defeat-deco .tear { transform-box: fill-box; transform-origin: center; animation: tear-drip 1.6s ease-in infinite; }
.defeat-deco .tear-r { animation-delay: 0.8s; }

/* Card decoration — a subtle pulse so it reads as a "lit indicator" rather than
   sitting dead next to the score. Quieter than the mascot deco on purpose. */
@keyframes cardDecoPulse {
  0%, 100% { transform: scale(0.92); opacity: 0.6; }
  50%      { transform: scale(1.18); opacity: 1; }
}
.card-deco { transform-box: fill-box; transform-origin: center; animation: cardDecoPulse 2.5s ease-in-out infinite; }

/* If a new-win hop coincides with a reached state, chain hop -> state bob. */
.mascot.celebrate.target-reached .mascot-body { animation: hop 600ms ease-out 1, bob-happy 0.7s ease-in-out 600ms infinite; }
.mascot.celebrate.limit-reached  .mascot-body { animation: hop 600ms ease-out 1, bob-sad   3s   ease-in-out 600ms infinite; }

/* ============================ Zach Merrett sequence ============ */

/* Wrapper around #rows so #zach-overlay has a positioned container to
   anchor absolute coordinates against. Overflow visible so the ladder
   can span between rows without clipping. */
#rows-wrap { position: relative; }

/* Overlay hidden by default. JS adds .active to run the sequence, and
   writes --ess-top / --haw-top / --row-h from measured row rects so
   the doors and ladder land at the right rows regardless of viewport. */
#zach-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.6s ease-out;
  /* Defaults; overwritten by JS before .active is added */
  --ess-top: 0px;
  --haw-top: 610px;
  --row-h:   122px;
}
#zach-overlay.active { opacity: 1; }

/* Doors sit in the BOTTOM-LEFT corner of the card interior, matching the
   compact proportions from the boys' screenshot: 22 CSS px wide × 36 tall,
   left edge just inside the card border (12px), bottom flush with the top
   of the card border stroke (row-y=109). Door top lands at row-y=73 — well
   below the mascot region so nothing important is covered when the door
   fades in or opens. Zach walks out at the card bottom. */
.zach-door {
  position: absolute;
  width: 22px;
  height: 36px;
  /* CSS x=0 corresponds to row-viewBox x=-60 after the widening. Card left
     edge is at row-x=8 → CSS 68 (at min-width 980). Door right edge sits a
     couple of pixels shy of the card left edge so the door reads as a
     visible fixture just outside the card border. */
  left: 44px;
  opacity: 0;
  transition: opacity 0.5s ease-out;
}
.zach-door svg { width: 100%; height: 100%; display: block; }
.zach-door-essendon { top: calc(var(--ess-top) + 73px); }
.zach-door-hawthorn { top: calc(var(--haw-top) + 73px); }
.zach-door.visible  { opacity: 1; }

/* Door panel swings open on a left-hinge pivot. transform-origin: left
   center = the hinge; rotateY(-100deg) tucks it back and to the left
   revealing the dark/light interior behind. */
.zach-door-panel {
  transform-origin: 0% 50%;
  transform: rotateY(0deg);
  transition: transform 0.55s cubic-bezier(.4,0,.2,1);
}
.zach-door.open .zach-door-panel { transform: rotateY(-100deg); }

/* Interior fills the door frame. Dark for the Essendon door (Zach
   emerges from the dark), gentle warm-white for Hawthorn (holy light). */
.zach-door-interior { }
.zach-door-interior-light {
  /* Layered radial gradient reads as a glow escaping the frame */
  fill: #fff5c8;
}

/* Ladder positioned so its LEFT edge aligns with the point on the team
   card where the top-right corner curve flattens into the straight
   vertical right border (row-x=144, the card's right edge). Width 20px
   — just a touch wider than the 16px Zach silhouette so his shoulders
   clear the rails. */
.zach-ladder {
  position: absolute;
  /* Card right edge at row-x=144 → CSS 204 (viewBox origin is x=-60,
     so add 60). Ladder left edge aligns with that so the ladder sits
     just outside the card to the right. */
  left: 204px;
  width: 20px;
  top:    calc(var(--ess-top) + 90px);
  height: calc(var(--haw-top) + var(--row-h) - var(--ess-top) - 90px);
  opacity: 0;
  transition: opacity 0.5s ease-out;
}
.zach-ladder.visible { opacity: 1; }
.zach-ladder svg { width: 100%; height: 100%; display: block; }

/* Zach character — 16 CSS px wide. Default is tucked inside the Essendon
   doorway (door left=12, width=22 → centre x=23 → Zach left=15 for centring).
   Feet sit on the card bottom (top of card border stroke, row-y≈109), so
   Zach's top = 109 - 32 = 77. Position + facing direction driven by
   JS-applied classes below. */
.zach-character {
  position: absolute;
  width: 16px;
  height: 32px;
  opacity: 0;
  /* Transitions bumped by ~30% (ZACH_SPEED = 1.3 in app.js) for the
     boys' slower-pace ask. */
  transition:
    left    1.3s cubic-bezier(.4,0,.2,1),
    top     1.3s cubic-bezier(.4,0,.2,1),
    opacity 0.65s ease-out;
  /* Default: tucked inside the Essendon doorway. Door left=44 width=22
     → door centre = CSS 55; Zach 16 wide → left = 47 to centre him. */
  left: 47px;
  top:  calc(var(--ess-top) + 77px);
}
.zach-character svg { position: absolute; width: 100%; height: 100%; display: block; }
.zach-character .zach-back  { opacity: 0; transition: opacity 0.2s ease-out; }
.zach-character.back-view .zach-front { opacity: 0; }
.zach-character.back-view .zach-back  { opacity: 1; }

.zach-character.visible { opacity: 1; }

/* Positional waypoints for the sequence — all left values shifted +30 from
   the pre-widening set (viewBox was `-30 …` → `-60 …`). */
.zach-character.at-essendon-front {                                      /* just emerged, facing viewer */
  left: 72px;
  top:  calc(var(--ess-top) + 77px);
}
.zach-character.at-ladder-top {                                          /* at top of ladder — centred on ladder (CSS 204 + 10 - 8 = 206) */
  left: 206px;
  top:  calc(var(--ess-top) + 88px);
}
.zach-character.at-ladder-bottom {                                       /* at bottom of ladder */
  left: 206px;
  top:  calc(var(--haw-top) + var(--row-h) - 40px);
  /* Descent timing is now driven by a WAAPI stepped animation so the
     transition here is only used as a fallback (e.g. reduced motion). */
  transition:
    left 1.2s cubic-bezier(.4,0,.2,1),
    top  5.5s linear,
    opacity 0.5s ease-out;
}
.zach-character.at-hawthorn-front {                                      /* at Hawthorn door */
  left: 72px;
  top:  calc(var(--haw-top) + 77px);
}
.zach-character.at-hawthorn-inside {                                     /* stepping into the light */
  left: 47px;
  top:  calc(var(--haw-top) + 77px);
  opacity: 0;
  transition: left 1.3s ease-out, opacity 1.56s ease-in;
}

/* ============================ Meatball rain (easter egg) ============ */

/* Fires when the user clicks Hird's "Coach?" bubble. 700 meatballs fall
   from above the viewport, each with its own randomised fall duration,
   horizontal position, size and rotation. Per-element custom properties
   drive the single meatballFall keyframe so the browser animates each
   meatball with just one translate3d+rotate transform — cheap enough
   to composite the whole rain in one paint pass. */
#meatball-rain {
  position: fixed;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 200;
}
.meatball {
  position: absolute;
  top: -40px;
  animation: meatballFall var(--fall-duration, 15s) var(--fall-delay, 0s) linear forwards;
  will-change: transform;
}
.meatball svg { width: 100%; height: 100%; display: block; }
@keyframes meatballFall {
  from { transform: translate3d(0, 0, 0) rotate(0deg); }
  to   { transform: translate3d(0, calc(100vh + 60px), 0) rotate(var(--rot-deg, 720deg)); }
}

/* ============================ Hird peeker (Essendon) ============ */

/* Two parallel 15s animations coordinate the peek:
   * hirdPeek rotates the tilt group around bbox-bottom-centre so the head
     swings out past the card edge.
   * hirdFade holds the whole peeker at opacity 0 by DEFAULT, so between
     peeks he isn't just z-covered — he's fully transparent. This prevents
     the "briefly visible during flip" glitch because there's nothing to
     see under the shrinking card.
   Timeline (15s): 0-3% hidden (~0.45s), 3-7% peek out (0.6s),
   7-23% visible (2.4s ≈ 2.5s), 23-27% peek back (0.6s),
   27-100% hidden (~11s). */
.hird-peeker {
  animation: hirdFade 15s ease-in-out infinite;
}
.hird-tilt {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: hirdPeek 15s ease-in-out infinite;
}
@keyframes hirdFade {
  0%,  3%     { opacity: 0; }
  5%,  25%    { opacity: 1; }
  27%, 100%   { opacity: 0; }
}
@keyframes hirdPeek {
  0%,  3%     { transform: rotate(0deg); }
  7%,  23%    { transform: rotate(-30deg); }
  27%, 100%   { transform: rotate(0deg); }
}

/* Force Hird invisible for the duration of Essendon's flip. flipTeam() adds
   .flipping to the row on Phase 1 start and removes it on cleanup; the
   opacity:0 wins over the .hird-peeker animation because it's an inline
   override on the ancestor. Prevents the "flash of Hird" as the card
   shrinks. */
.row.flipping .hird-peeker { opacity: 0 !important; animation: none !important; }

/* "Coach?" speech bubble — hidden by default. app.js flips .showing on
   30% of peek cycles (setTimeout aligned with Hird's visible window).
   Clickable via cursor:pointer + delegated handler in wireHirdBubbles. */
.hird-bubble {
  opacity: 0;
  pointer-events: none;
  cursor: pointer;
  transition: opacity 0.35s ease-out;
}
.hird-bubble.showing {
  opacity: 1;
  pointer-events: auto;
}

/* ============================ crashed-jet mascot (Essendon) ============ */

/* Flames flicker from a bottom-centre pivot so they read as growing/
   shrinking rather than shifting sideways. Each flame has a different
   duration + delay so the group looks alive rather than synchronised. */
.crashed-jet .flame {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: flameFlicker 0.45s ease-in-out infinite;
}
.crashed-jet .flame-1 { animation-duration: 0.42s; }
.crashed-jet .flame-2 { animation-duration: 0.58s; animation-delay: 0.15s; }
.crashed-jet .flame-3 { animation-duration: 0.36s; animation-delay: 0.30s; }
@keyframes flameFlicker {
  0%, 100% { transform: scale(1, 1); }
  50%      { transform: scale(1.15, 1.35); }
}

/* Smoke wisps drift up and fade. Staggered delays so they read as a
   continuous plume rather than three synchronised puffs. */
.crashed-jet .smoke {
  transform-box: fill-box;
  transform-origin: center;
  animation: smokeRise 2.4s ease-out infinite;
}
.crashed-jet .smoke-b { animation-delay: 0.8s; }
.crashed-jet .smoke-c { animation-delay: 1.6s; }
@keyframes smokeRise {
  0%   { transform: translate(0, 4px) scale(0.7); opacity: 0.6; }
  60%  { opacity: 0.35; }
  100% { transform: translate(-1px, -10px) scale(1.4); opacity: 0; }
}

/* respect reduced-motion preferences */
@media (prefers-reduced-motion: reduce) {
  .row, .mascot-body, .mascot-tag,
  .mascot.celebrate .mascot-body,
  .mascot.target-reached .mascot-body,
  .mascot.limit-reached .mascot-body,
  .mascot.celebrate.target-reached .mascot-body,
  .mascot.celebrate.limit-reached .mascot-body,
  .celebration-deco .spark, .defeat-deco .tear,
  .card-deco,
  .crashed-jet .flame, .crashed-jet .smoke,
  .hird-tilt, .hird-peeker { animation: none !important; }
}

/* ============================ loading state ============================ */

#rows.loading { position: relative; min-height: 200px; }
@keyframes shimmer { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } }
#rows.loading::after {
  content: "Loading the season…";
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-style: italic; color: var(--brown); animation: shimmer 1.4s ease-in-out infinite;
}

/* ============================ responsive ============================ */

@media (max-width: 1000px) {
  #board { padding: 8px 8px 24px; }
  /* let trails + strips shrink together (still aligned) before scrolling kicks in */
  .number-strip > svg, .row { min-width: 820px; }
}
@media (max-width: 640px) {
  #site-header svg { max-width: 100%; }
  #site-footer { font-size: 12px; }
}
