/* Gradual onboarding — progressive UI reveal.
 *
 * The game root carries `data-onboarding-show="space separated list"` listing
 * the element classes (without the `ob-el-` prefix) that should be visible
 * for the current stage. Anything not in the list is hidden. Stage 1 emits
 * an empty list (everything hidden); stage 8 emits all names.
 *
 * To add a new toggleable element:
 *   1. Stick `class="ob-el-<name>"` on the JSX element.
 *   2. Add a rule below using `:not([data-onboarding-show~="<name>"])`.
 *   3. Add the name as a flag in misc/onboardingStages.js per-stage map.
 */

/* Hide each labelled element when its name is NOT in the show list. The
 * tilde-equals attribute selector `[attr~=name]` matches a space-separated
 * token, which is exactly what `data-onboarding-show="a b c"` produces.
 *
 * Use `visibility: hidden` (not `display: none`) so each element retains
 * its layout slot. The cards stay anchored to the same screen position
 * through all eight stages — pieces fade in around them rather than the
 * hand jumping every time something reveals.
 */
#game.onboarding-active .ob-el-playerBars,
#game.onboarding-active .ob-el-scoreBanners,
#game.onboarding-active .ob-el-turnIndicator,
#game.onboarding-active .ob-el-lastPlayedCard,
#game.onboarding-active .ob-el-ownCastle,
#game.onboarding-active .ob-el-opponentCastle,
#game.onboarding-active .ob-el-walls,
#game.onboarding-active .ob-el-ownBricks,
#game.onboarding-active .ob-el-ownBuilders,
#game.onboarding-active .ob-el-ownAttackStat,
#game.onboarding-active .ob-el-ownMagicStat,
#game.onboarding-active .ob-el-oppBricks,
#game.onboarding-active .ob-el-oppBuilders,
#game.onboarding-active .ob-el-oppAttackStat,
#game.onboarding-active .ob-el-oppMagicStat,
#game.onboarding-active .ob-el-opponentResources,
body .navbar {
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity 0.32s ease,
    transform 0.36s cubic-bezier(0.22, 1, 0.36, 1),
    visibility 0s linear;
  will-change: opacity, transform;
}

#game.onboarding-active:not([data-onboarding-show~="playerBars"]) .ob-el-playerBars,
#game.onboarding-active:not([data-onboarding-show~="scoreBanners"]) .ob-el-scoreBanners,
#game.onboarding-active:not([data-onboarding-show~="turnIndicator"]) .ob-el-turnIndicator,
#game.onboarding-active:not([data-onboarding-show~="lastPlayedCard"]) .ob-el-lastPlayedCard,
#game.onboarding-active:not([data-onboarding-show~="ownCastle"]) .ob-el-ownCastle,
#game.onboarding-active:not([data-onboarding-show~="opponentCastle"]) .ob-el-opponentCastle,
#game.onboarding-active:not([data-onboarding-show~="walls"]) .ob-el-walls,
/* Per-resource flags so we reveal bricks → bricks+builders → the rest. */
#game.onboarding-active:not([data-onboarding-show~="ownBricks"]) .ob-el-ownBricks,
#game.onboarding-active:not([data-onboarding-show~="ownBuilders"]) .ob-el-ownBuilders,
#game.onboarding-active:not([data-onboarding-show~="ownAttackStat"]) .ob-el-ownAttackStat,
#game.onboarding-active:not([data-onboarding-show~="ownMagicStat"]) .ob-el-ownMagicStat,
#game.onboarding-active:not([data-onboarding-show~="oppBricks"]) .ob-el-oppBricks,
#game.onboarding-active:not([data-onboarding-show~="oppBuilders"]) .ob-el-oppBuilders,
#game.onboarding-active:not([data-onboarding-show~="oppAttackStat"]) .ob-el-oppAttackStat,
#game.onboarding-active:not([data-onboarding-show~="oppMagicStat"]) .ob-el-oppMagicStat {
  visibility: hidden !important;
  opacity: 0 !important;
  transform: translateY(8px) scale(0.985);
}

/* Hide the opponent's whole resource block with visibility:hidden so it
 * keeps its layout slot — display:none would let the enemy castle shift
 * upward when there's nothing under it. The per-icon ob-el-opp* rules
 * above also use visibility:hidden, so descendants stay invisible too;
 * the empty wrapper carries no background/border, so the player just
 * sees blank space (matching where the resources will appear at stage 6). */
#game.onboarding-active:not([data-onboarding-show~="opponentResources"]) .ob-el-opponentResources {
  visibility: hidden !important;
  opacity: 0 !important;
  transform: translateY(8px) scale(0.985);
}

/* The last-played card is horizontally centered with `translateX(-50%)`
 * (see .last-played-card in style.css / mobile.css). The generic onboarding
 * reveal/hide rules above set `transform: translateY(...)`, which clobbers
 * that pull-back and shoves the card half its own width to the right. Re-apply
 * the centering here, keeping the same Y-offset so the reveal still animates
 * vertically. Specificity matches the generic rules, so source order wins. */
#game.onboarding-active .ob-el-lastPlayedCard {
  transform: translateX(-50%) translateY(0);
}
#game.onboarding-active:not([data-onboarding-show~="lastPlayedCard"]) .ob-el-lastPlayedCard {
  transform: translateX(-50%) translateY(8px) scale(0.985);
}

/* Navbar — controlled via a body attribute set by main.js after each HTMX
 * swap (the layout is rendered once but state changes per-stage, so this
 * one needs JS plumbing). Same visibility:hidden so its slot at the top
 * of the page stays put, only the contents disappear. */
body[data-onboarding-hide-nav="true"] .navbar {
  visibility: hidden !important;
  opacity: 0 !important;
  transform: translateY(-8px);
}

#game.onboarding-active .onboarding-reveal-enter,
body .navbar.onboarding-reveal-enter {
  animation: onboardingRevealFade 0.9s cubic-bezier(0.22, 1, 0.36, 1);
}

@keyframes onboardingRevealFade {
  0% {
    opacity: 0;
    transform: translateY(10px) scale(0.985);
  }
  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* Gentle pulse on the gold spotlight ring (the SVG cut-out border) to draw the
   eye to the highlighted target. */
.onboarding-cutout-ring {
  animation: onboardingRingPulse 1.5s ease-in-out infinite;
}

@keyframes onboardingRingPulse {
  0%,
  100% {
    stroke-width: 3;
    stroke-opacity: 0.95;
  }
  50% {
    stroke-width: 6;
    stroke-opacity: 0.5;
  }
}

@media (prefers-reduced-motion: reduce) {
  .onboarding-cutout-ring {
    animation: none;
  }
}

body .navbar.navbar-onboarding-reveal {
  visibility: visible !important;
  opacity: 1 !important;
  transform: none !important;
}

body .navbar.navbar-onboarding-reveal::before,
body .navbar.navbar-onboarding-reveal .navbar-bracket {
  animation: onboardingNavbarFrameReveal 0.52s cubic-bezier(0.22, 1, 0.36, 1) both;
}

body .navbar.navbar-onboarding-reveal .navbar-brand,
body .navbar.navbar-onboarding-reveal .navbar-online,
body .navbar.navbar-onboarding-reveal .navbar-center,
body .navbar.navbar-onboarding-reveal .navbar-right,
body .navbar.navbar-onboarding-reveal .burger {
  opacity: 0;
  transform: translateY(10px);
}

body .navbar.navbar-onboarding-reveal .navbar-brand {
  animation: onboardingNavbarItemReveal 0.72s cubic-bezier(0.22, 1, 0.36, 1) 0.24s both;
}

body .navbar.navbar-onboarding-reveal .navbar-online {
  animation: onboardingNavbarItemReveal 0.72s cubic-bezier(0.22, 1, 0.36, 1) 0.42s both;
}

body .navbar.navbar-onboarding-reveal .navbar-center {
  animation: onboardingNavbarItemReveal 0.78s cubic-bezier(0.22, 1, 0.36, 1) 0.58s both;
}

body .navbar.navbar-onboarding-reveal .navbar-right {
  animation: onboardingNavbarItemReveal 0.78s cubic-bezier(0.22, 1, 0.36, 1) 0.76s both;
}

body .navbar.navbar-onboarding-reveal .burger {
  animation: onboardingNavbarItemReveal 0.68s cubic-bezier(0.22, 1, 0.36, 1) 0.9s both;
}

@keyframes onboardingNavbarFrameReveal {
  0% {
    opacity: 0;
    transform: translateY(-10px) scaleY(0.94);
    filter: brightness(0.8);
  }
  100% {
    opacity: 1;
    transform: translateY(0) scaleY(1);
    filter: brightness(1);
  }
}

@keyframes onboardingNavbarItemReveal {
  0% {
    opacity: 0;
    transform: translateY(10px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}

/* ─────────────────────────────────────────────────────────────────────── */
/* Brick-flight animation on card play.
 *
 * When the player plays a brick-costing card during stages 1-3, JS
 * (public/main.js, see onboarding-brick-flight IIFE) adds these
 * transient classes and spawns flying brick particles between the
 * counter and the played card's cost icon — so the player visually ties
 * "10 bricks gone from my pool" to "10 bricks landed on this card". */

/* Brick counter pulses larger for the duration of the flight. */
.onboarding-brick-emphasize {
  animation: onboardingBrickEmphasize 1.1s cubic-bezier(0.4, 0, 0.2, 1);
  transform-origin: center;
}
@keyframes onboardingBrickEmphasize {
  0%   { transform: scale(1); }
  15%  { transform: scale(1.7); }
  75%  { transform: scale(1.5); }
  100% { transform: scale(1); }
}

/* The played card's brick cost icon bumps each time a flying brick
 * lands on it — short and snappy. */
.onboarding-cost-bump {
  animation: onboardingCostBump 0.32s ease-out;
  display: inline-block;
  transform-origin: center;
}
@keyframes onboardingCostBump {
  0%   { transform: scale(1);   filter: brightness(1); }
  45%  { transform: scale(1.6); filter: brightness(1.6); }
  100% { transform: scale(1);   filter: brightness(1); }
}

/* The flying brick particle itself — JS spawns a styled img and toggles
 * `.flying` on the next frame to animate it. */
.onboarding-flying-brick {
  position: fixed;
  width: 24px;
  height: 24px;
  z-index: 10020;          /* above the spotlight overlay */
  pointer-events: none;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.55));
  transition:
    left 0.45s cubic-bezier(0.45, 0.05, 0.55, 0.95),
    top 0.45s cubic-bezier(0.45, 0.05, 0.55, 0.95);
}
.onboarding-flying-brick.landed {
  transition: opacity 0.18s ease;
  opacity: 0;
}

/* ─────────────────────────────────────────────────────────────────────── */
/* OnboardingHand — dedicated layout for the lesson's small hand.
 *
 * The regular .hand uses `flex-grow:1` on every card except :last-child
 * (which is fixed at 100px), so a 2-card hand puts card-0 stretched across
 * ~525px on the left and card-1 fixed at 100px on the right. Worse, the
 * Card component sets `style="flex-grow:1; width:undefined"` inline, which
 * beats any class-based CSS — that's why the previous `.hand:has()` fix
 * didn't work. We override with !important here. */

.onboarding-hand {
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 10px;
  box-sizing: border-box;
  margin-bottom: 10px;
}

.onboarding-hand-row {
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  gap: 18px;
}

/* Beat the inline `style="flex-grow:1; width:undefined"` Card.jsx applies
 * to cards in slots 0-5 (and the `width:100px` it applies to slot 6).
 * Inline style → can only be overridden with !important. */
.onboarding-hand .card-container {
  flex-grow: 0 !important;
  width: 100px !important;
  position: relative;
  isolation: isolate;
}

/* Smooth scale on hover — no layout shift since width is fixed. */
.onboarding-hand .card-container {
  transition: transform 0.2s ease, z-index 0s;
}
.onboarding-hand .card-container:hover,
.onboarding-hand .card-container:focus {
  transform: translateY(-12px) scale(1.06);
  z-index: 10;
}

/* ─────────────────────────────────────────────────────────────────────── */
/* Scroll fix — the in-game card help panel (#cardhelptextdiv) is flow content
 * rendered BELOW the hand and only populated/shown AFTER the board has laid
 * out. During onboarding the hidden pieces keep their layout slots
 * (visibility:hidden), so the board is already near full height; the help panel
 * then tips the page into a vertical scroll on most screens.
 *
 * Pin the panel to the bottom of the viewport so it leaves normal flow and can
 * never extend the page (a fixed element doesn't add to scroll height), and on
 * the passive intro steps lift the hand up into the empty space above it so the
 * panel keeps a clear offset from the cards. Lift with margin, not transform,
 * so the highlighted cards aren't trapped under the spotlight overlay.
 *
 * PORTRAIT ONLY — landscape phones have their own onboarding layout in
 * landscape-mobile.css (hand absolutely positioned, castles in the bottom
 * corners). These lifts must NOT leak into landscape: the -100px hand lift
 * shifts the absolutely-positioned landscape hand up, and the help/lift logic
 * doesn't match that layout. */
/* The desktop help-slot reserve (min-height in style.css) must NOT apply during
 * onboarding: the lesson uses captions, not the card-help panel, and its layout
 * has no spare vertical budget for the reserved height — it scrolls at e.g.
 * 1920x945 (landscape, where the portrait pin below doesn't kick in). Zero the
 * reserve for every onboarding orientation; portrait additionally pins the panel
 * out of flow. */
#game.onboarding-active .card-help-container {
  min-height: 0;
}

@media (orientation: portrait) {
#game.onboarding-active .card-help-container {
  position: fixed;
  left: 50%;
  bottom: 38px;
  transform: translateX(-50%);
  width: 100%;
  max-width: 500px;
  margin: 0;
  z-index: 40;
  pointer-events: none; /* text-only panel — never intercept taps on the cards */
}

/* Intro steps (passive, single card): the castle, last-played card and turn
 * indicator are all hidden but keep their (tall) layout slots, leaving a big
 * empty gap that — together with the help panel — forces a scroll. Collapse
 * that gap by pulling everything below the castle block UP into it. A negative
 * margin-bottom on .castle-container drops the turn indicator + hand (+ the
 * bottom-pinned help) up into the empty (hidden) castle area, shrinking total
 * page height. The resources sit at the TOP of the block, so they don't move.
 * (Lift with margin, not transform, so highlighted cards aren't trapped under
 * the spotlight overlay.) Tune the value if it still scrolls / starts to crowd
 * the resources. */
#game.onboarding-active[data-onboarding-stage="0.1"] .castle-container,
#game.onboarding-active[data-onboarding-stage="0.2"] .castle-container {
  margin-bottom: -170px;
}

/* Stages 2–7: the castle is revealed, but the turn indicator + score/player
 * bars are still hidden-yet-slotted, so the board runs tall and scrolls. Lift
 * the hand up into that empty space (cards render above the castle via z-index)
 * so the hand + bottom-pinned help fit without a scrollbar. Excluded: the intro
 * stages (collapsed via the castle-container lift above), stage 2 (the "this is
 * your castle" lesson — lifting the hand would pull cards over the castle it's
 * pointing at), and stage 8 (the full game). Tune the value if it still scrolls
 * / crowds the castle. */
/* Variant-B exclusions mirror A's: "b0.x" intro steps point at the castles
 * (no cards yet — like stage 2), and "b5" is the full game (like stage 8). */
#game.onboarding-active:not([data-onboarding-stage="0.1"]):not([data-onboarding-stage="0.2"]):not([data-onboarding-stage="2"]):not([data-onboarding-stage="8"]):not([data-onboarding-stage^="b0."]):not([data-onboarding-stage="b5"]) .hands {
  margin-top: -100px;
}

/* Few-card lesson phase (.onboarding-hand = 2-3 cards): the -100px lift above is
 * tuned for the full-width regular hand; on the small centered lesson hand it
 * pulls the cards up over the castle + the wall highlight. There's ample room in
 * this phase, so lift it much less — drop the hand back down clear of the castle.
 * Scoped by :has(.onboarding-hand), so once the hand grows into the regular Hand
 * component it falls back to the -100px above (which is fine there).
 * Tune this single value if the cards still overlap / sit too low. */
#game.onboarding-active:not([data-onboarding-stage="0.1"]):not([data-onboarding-stage="0.2"]):not([data-onboarding-stage="2"]):not([data-onboarding-stage="8"]):not([data-onboarding-stage^="b0."]):not([data-onboarding-stage="b5"]) .hands:has(.onboarding-hand) {
  margin-top: -20px;
}
} /* end @media (orientation: portrait) — onboarding scroll lifts */

/* Short landscape windows (e.g. 1320x644 laptops / windowed browsers): the
 * onboarding scroll-lifts above are portrait-only, and the phone-landscape rules
 * need a coarse pointer + max-height:500px — so nothing fires here and the hand +
 * card-help panel sit too low (the help text drops below the fold). Mirror the
 * portrait lifts: pull the hand up 100px, which (since #cardhelptextdiv is an
 * in-flow sibling right after .hands) carries the card-help text up by the same
 * 100px. Same stage exclusions as portrait, plus the few-card (-20px) override so
 * the small lesson hand doesn't ride up over the castle. */
@media (orientation: landscape) and (max-height: 700px) and (min-width: 1000px) {
  #game.onboarding-active[data-onboarding-stage="0.1"] .castle-container,
  #game.onboarding-active[data-onboarding-stage="0.2"] .castle-container {
    margin-bottom: -170px;
  }
  #game.onboarding-active:not([data-onboarding-stage="0.1"]):not([data-onboarding-stage="0.2"]):not([data-onboarding-stage="2"]):not([data-onboarding-stage="8"]):not([data-onboarding-stage^="b0."]):not([data-onboarding-stage="b5"]) .hands {
    margin-top: -100px;
  }
  #game.onboarding-active:not([data-onboarding-stage="0.1"]):not([data-onboarding-stage="0.2"]):not([data-onboarding-stage="2"]):not([data-onboarding-stage="8"]):not([data-onboarding-stage^="b0."]):not([data-onboarding-stage="b5"]) .hands:has(.onboarding-hand) {
    margin-top: -20px;
  }
}

/* Landscape phones: cards shrink to 70px to mirror the regular hand sizing
 * pattern in landscape-mobile.css. */
@media (orientation: landscape) and (max-height: 500px) and (pointer: coarse) {
  .onboarding-hand .card-container {
    width: 70px !important;
  }
  .onboarding-hand-row {
    gap: 12px;
  }
}

/* Partial onboarding hands (4-6 visible cards) still render through the
 * regular Hand component so mobile can keep its default/coverflow behavior.
 * On wider layouts, JS adds .onboarding-wide-fit when the measured hand width
 * is large enough to show the same centered fixed-width lesson layout. */
.hand.onboarding-wide-fit {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 18px;
}

.hand.onboarding-wide-fit .card-container {
  flex-grow: 0 !important;
  width: 100px !important;
  position: relative;
  isolation: isolate;
}

.hand.onboarding-wide-fit .card-container:hover,
.hand.onboarding-wide-fit .card-container:focus {
  transform: translateY(-12px) scale(1.06);
  z-index: 10;
}

@media (orientation: landscape) and (max-height: 500px) and (pointer: coarse) {
  .hand.onboarding-wide-fit {
    gap: 12px;
  }
  .hand.onboarding-wide-fit .card-container {
    width: 70px !important;
  }
}

/* ─────────────────────────────────────────────────────────────────────── */
/* Onboarding caption — text on the darkened overlay, positioned right next
 * to the highlighted element. The shared speech.jsx bubble is hidden
 * entirely for onboarding; main.js clones its contents into a fresh
 * `#onboarding-caption` element placed near the spotlight target. */

/* Hide the bubble itself while in onboarding mode. The wrapper still
 * exists in the DOM (server-side render needs it as the OOB swap target)
 * but it's visually suppressed. */
.advisor-wrapper[data-tutorial-step^="onboarding-"] {
  display: none !important;
}

/* Just plain text on the dark overlay — no panel, no border, no padding.
 * Matches the look of the academy lightning caption: a warm cream label
 * with a strong text-shadow so it stays readable on top of the dimmed
 * background. The caption is anchored next to the spotlighted target by
 * main.js. */
.onboarding-caption {
  position: fixed;
  z-index: 10013;                     /* above .academy-spotlight-overlay (9000) */
  pointer-events: none;               /* never block the highlighted target */
  max-width: min(440px, calc(100vw - 32px));
  color: #ffe9a8;                     /* warm cream — academy palette */
  font-family: sans-serif;
  /* Bigger + fluid so it reads well from tiny phones to fullscreen desktops. */
  font-size: clamp(1.15rem, 1rem + 1.1vw, 1.6rem);
  font-weight: 700;
  line-height: 1.4;
  text-align: center;
  /* A readable panel — needed because the intro steps no longer dim the screen
     behind the text. */
  background: rgba(18, 14, 8, 0.84);
  border: 1px solid rgba(255, 210, 58, 0.45);
  border-radius: 14px;
  padding: 14px 18px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
  animation: onboardingCaptionIn 0.25s ease;
}

@keyframes onboardingCaptionIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.onboarding-caption .onboarding-caption-text {
  display: block;
}

/* Segmented caption (stage 5 Guards demo): the text is split into labelled
 * spans; the one matching the active highlight brightens, the rest dim, so the
 * wording tracks the cost ↔ wall-gain choreography without the box moving. */
.onboarding-caption-seg {
  transition: opacity 0.25s ease, color 0.25s ease, text-shadow 0.25s ease;
}
.onboarding-caption-seg.onboarding-seg-dim {
  opacity: 0.4;
}
.onboarding-caption-seg.onboarding-seg-active {
  opacity: 1;
  color: #fff6cf;
  text-shadow: 0 0 10px rgba(255, 210, 58, 0.55), 0 1px 3px rgba(0, 0, 0, 0.9);
}

/* Pulsing "Tap/Click anywhere to continue" line under the caption text, for
 * the passive tap-to-advance intro steps (data-tap-hint on #speech). Delayed
 * slightly past the caption's own fade-in so the concept lands first. */
.onboarding-tap-hint {
  display: block;
  margin-top: 8px;
  font-size: 0.72em;
  font-weight: 600;
  color: #ffd23a;
  animation: obTapHintPulse 1.6s ease-in-out 0.9s infinite both;
}

@keyframes obTapHintPulse {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 1; }
}

/* "Step X of N" progress hint above the caption text. */
.onboarding-caption-progress {
  display: block;
  font-size: 0.66em;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #ffd23a;
  opacity: 0.9;
  margin-bottom: 7px;
}

/* Bouncing arrow(s) that point at the highlighted target(s). The container holds
   one item per target; each item positions (centred on the point) and its inner
   icon owns the rotation + bounce so they don't conflict. */
.onboarding-arrow {
  position: fixed;
  inset: 0;
  z-index: 10014;
  pointer-events: none;
}

.onboarding-arrow-item {
  position: fixed;
  transform: translate(-50%, -50%);
  pointer-events: none;
}

.onboarding-arrow-icon {
  display: block;
  color: #ffd23a;
  font-size: clamp(22px, 4.2vw, 36px);
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.85));
  /* Base rotation (used when the bounce animation is disabled). */
  transform: rotate(var(--arrow-rot, 0deg));
  animation: onboardingArrowBounce 0.9s ease-in-out infinite;
}

@keyframes onboardingArrowBounce {
  0%, 100% { transform: rotate(var(--arrow-rot, 0deg)) translateX(0); }
  50%      { transform: rotate(var(--arrow-rot, 0deg)) translateX(-9px); }
}

/* Mobile portrait: float the caption near the vertical middle (board centre is
 * empty during onboarding); the arrow still points at the target. The
 * `orientation: portrait` guard keeps these off small landscape phones (≤600px
 * wide in landscape too) — there the captions use JS positioning + the
 * landscape-mobile.css board layout instead. */
@media (max-width: 600px) and (orientation: portrait) {
  /* Single (non-split) captions: a readable full-width strip near the centre.
   * Split-caption steps are excluded — they're positioned by JS (below). */
  .onboarding-caption:not(.onboarding-caption--top):not(.onboarding-caption--bottom) {
    max-width: calc(100vw - 24px);
    left: 12px !important;
    right: 12px !important;
    top: 50% !important;
    bottom: auto !important;
    transform: translateY(-50%);
  }

  /* Step 0.2 ("card action"): the cards are lifted toward the centre (castle
   * lift above), so the centred strip would sit right on top of them. Pin this
   * step's box to the upper area instead — above the cards, below the resources
   * — so it never covers the cards or their action-icon arrows. (More specific
   * than the strip rule above so it wins among the !important declarations.) */
  .onboarding-caption[data-stage="0.2"]:not(.onboarding-caption--top):not(.onboarding-caption--bottom) {
    top: 14% !important;
    bottom: auto !important;
    transform: none !important;
  }

  /* Step 2 ("This is your castle"): the centred strip sits on the highlighted
   * castle. Pin the box to the very top, above the castle cut-out — the arrow
   * still points down at it. (The board is dimmed except the castle cut-out, so
   * sitting over the dimmed resources area up top is fine.) */
  .onboarding-caption[data-stage="2"]:not(.onboarding-caption--top):not(.onboarding-caption--bottom) {
    top: 6% !important;
    bottom: auto !important;
    transform: none !important;
  }

  /* Step 5 / stage 4 (Builder lesson): the arrow sits on the builders icon up in
   * the resource stats, but the centred strip floats far below it. Pin the box
   * up near the builders and keep it compact so the text sits right by the
   * arrow. */
  .onboarding-caption[data-stage="4"]:not(.onboarding-caption--top):not(.onboarding-caption--bottom) {
    top: 13% !important;
    bottom: auto !important;
    /* Right-aligned: the builders icon + arrow are on the LEFT (resource
     * column), so keep the box off to the right so it covers neither. */
    left: auto !important;
    right: 10px !important;
    transform: none !important;
    max-width: min(64vw, 280px) !important;
    font-size: clamp(0.9rem, 0.85rem + 0.7vw, 1.15rem);
    padding: 8px 12px;
  }

  /* Step 6 / stage 5 (Guards demo): the centred strip covers the castle wall.
   * Lift it into the band between the resource stats (top) and the wall, and
   * squeeze it (narrower + compact) so it clears both. Centred so it doesn't sit
   * on the left-side resource column. */
  .onboarding-caption[data-stage="5"]:not(.onboarding-caption--top):not(.onboarding-caption--bottom) {
    top: 17% !important;
    bottom: auto !important;
    left: 50% !important;
    right: auto !important;
    transform: translateX(-50%) !important;
    max-width: min(72vw, 320px) !important;
    font-size: clamp(0.9rem, 0.85rem + 0.7vw, 1.15rem);
    padding: 8px 12px;
  }

  /* Split-caption steps (e.g. the intro "Your resources" / "The card's cost").
   * Do NOT pin to fixed top/bottom strips — on small screens those landed right
   * on top of the resource bar and the card they point at. Stretch each box
   * full-width (left + right) so positionCaption can't fit it beside the target
   * and falls back to its below/above branch, while JS still sets `top` — so
   * each box drops into the empty middle band (below the resources / above the
   * card) and never covers its target. */
  .onboarding-caption--top,
  .onboarding-caption--bottom {
    max-width: calc(100vw - 24px);
    left: 12px !important;
    right: 12px !important;
  }
}

/* Short viewports (mobile landscape): shrink so the panel + arrow always fit. */
@media (max-height: 500px) {
  .onboarding-caption {
    font-size: clamp(0.95rem, 0.9rem + 0.8vw, 1.2rem);
    padding: 9px 14px;
  }
  .onboarding-arrow-icon {
    font-size: clamp(18px, 3.5vw, 26px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .onboarding-caption { animation: none; }
  .onboarding-arrow-icon { animation: none; }
}

/* Stage 5 (Guards lesson) demo: floating ±N numbers + pulse/glow beats. */
.onboarding-float-num {
  position: fixed;
  z-index: 10015;
  transform: translate(-50%, 0);
  font-family: sans-serif;
  font-weight: 800;
  font-size: clamp(1.4rem, 1rem + 1.8vw, 2.2rem);
  pointer-events: none;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.85);
  animation: onboardingFloatRise 1.3s ease-out forwards;
}

.onboarding-float-cost { color: #ff6b6b; }  /* −7 weapons spent */
.onboarding-float-gain { color: #7ee07e; }  /* +12 wall gained */

@keyframes onboardingFloatRise {
  0% { opacity: 0; transform: translate(-50%, 6px) scale(0.85); }
  18% { opacity: 1; transform: translate(-50%, -6px) scale(1.1); }
  100% { opacity: 0; transform: translate(-50%, -52px) scale(1); }
}

.onboarding-demo-pulse {
  animation: onboardingDemoPulse 0.55s ease-in-out 2;
}

@keyframes onboardingDemoPulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.18); filter: brightness(1.25); }
}

.onboarding-demo-glow {
  animation: onboardingDemoGlow 1.2s ease-in-out;
  border-radius: 8px;
}

@keyframes onboardingDemoGlow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 210, 58, 0); }
  50% { box-shadow: 0 0 14px 4px rgba(255, 210, 58, 0.75); }
}

@media (prefers-reduced-motion: reduce) {
  .onboarding-float-num,
  .onboarding-demo-pulse,
  .onboarding-demo-glow {
    animation: none;
  }
}

/* ─────────────────────────────────────────────────────────────────────── */
/* Lift the hand on short viewports during onboarding.
 *
 * Normal play already lifts the board + hand by -70px on short-but-wide
 * screens (see the `min-width:661 / 501-600 height` block in mobile.css), so
 * things don't crowd. But that rule is gated on width and tops out at 600px
 * tall. During onboarding most of the board is hidden (its slots stay empty
 * via visibility:hidden), so on any viewport shorter than ~700px the cards
 * end up sitting too low with a big empty gap above them.
 *
 * Pull the hand up across the whole 501-700px height band, at all widths.
 * `max-height:500` (landscape phones) is handled separately in
 * landscape-mobile.css, so we start at 501 to stay disjoint. The caption /
 * spotlight JS measures the cards' live rects, so it follows the moved hand.
 *
 * IMPORTANT: lift with `margin-top`, NOT `transform`. A transform creates a
 * stacking context on `.hands`, which would trap the whole hand subtree —
 * including the spotlighted/highlighted card — UNDER the dark spotlight
 * overlay (z-index 9000), so the "play me" highlight could never shine
 * through. `transform: none` also neutralises the wider-screen lift in
 * mobile.css (the `min-width:661 / 501-600` block) for the same reason. */
@media (min-height: 501px) and (max-height: 700px) {
  #game.onboarding-active .hands {
    transform: none;
    margin-top: -70px;
  }
}

/* …but in PORTRAIT this generic -70px lift double-counts with the
 * portrait-only stage lifts above (the castle-container collapse on intro
 * stages 0.1/0.2, and the `.hands` lift on stages 3-7). Those portrait lifts
 * have no height bound, so on a near-square portrait screen (e.g. 586×586,
 * which lands in this 501-700 band) BOTH fire: the hand gets yanked far too
 * high and leaves a big empty gap between the cards and the bottom of the
 * screen. On tall portrait phones (>700px) only the portrait lift fires, and
 * that's the tuned-good baseline — so here we simply drop the extra generic
 * lift to make short portrait match tall portrait. Stages 2 and 8 have no
 * portrait lift of their own, so they keep the generic -70px. */
@media (orientation: portrait) and (min-height: 501px) and (max-height: 700px) {
  /* b5 (variant B's full game) keeps the generic -70px like stage 8. */
  #game.onboarding-active:not([data-onboarding-stage="2"]):not([data-onboarding-stage="8"]):not([data-onboarding-stage="b5"]) .hands {
    margin-top: 0;
  }
}

/* ─────────────────────────────────────────────────────────────────────── */
/* Keep the "play me" card lit while the spotlight is on a board element.
 *
 * Several gated stages spotlight something OTHER than the hand — the castle
 * at stage 2 ("This is your castle."), the brick-income icon at stage 4, the
 * weapons stat at stage 5 — while the only card the lesson lets you play sits
 * in the hand. The dark spotlight overlay (z-index 9000) dims that whole
 * hand, darkening the pulsing tutorial-highlight so the player can't tell
 * which card to tap. They end up discarding in circles (every other card is
 * either discarded or silently soft-rejected by the onboarding card gate in
 * engine/room.js), and the stage never advances.
 *
 * Hoist the highlighted card above the overlay so it stays brightly lit and
 * pulsing — a clear "tap this" target — even when the spotlight glow is
 * elsewhere. z-index matches the spotlight target (.tutorial-intro-spotlight,
 * 10011); the caption sits above both at 10013. */
/* Apply the hoist ONLY on the stages whose spotlight is a BOARD element while
 * the playable card sits (dimmed) in the hand — stage 2 (the castle) and stage
 * 4 (the brick-income icon). On those, the card needs lifting above the overlay
 * to stay lit and pulsing.
 *
 * Do NOT hoist on stages whose spotlight ring sits ON the card itself (stage 5's
 * card cost/action, stage 6's Remove Bricks card): there the card is already
 * inside a lit cut-out hole, and hoisting it to 10011 (above the SVG ring at
 * 9000) would render the card OVER its own highlight rectangle, hiding the ring
 * behind the card. */
body.academy-spotlight-active #game.onboarding-active[data-onboarding-stage="2"] .card-container.tutorial-highlight,
body.academy-spotlight-active #game.onboarding-active[data-onboarding-stage="4"] .card-container.tutorial-highlight {
  position: relative;
  z-index: 10011 !important;
}

/* The "Hover over a card to see its description" help strip is noise on the
 * passive intro steps: 0.1 has no cards at all and 0.15's Tower isn't
 * interactive yet. From 0.2 on, cards are playable and the tip earns its
 * place again. */
/* !important because the hover-help JS stamps inline display:block. */
#game.onboarding-active[data-onboarding-stage="0.1"] #cardhelptextdiv,
#game.onboarding-active[data-onboarding-stage="0.15"] #cardhelptextdiv,
#game.onboarding-active[data-onboarding-stage^="b0."] #cardhelptextdiv {
  display: none !important;
}

/* ───────────────────────────────────────────────────────────────────────────
 * Stage 0.1 / 0.15 intro choreography.
 *
 * One concept per step. Stage 0.1 (empty board):
 *   (2) the resources panel frame fades in,
 *   (3) the bricks pop in inside it,
 *   (4) the "Your resources" caption + tap hint.
 * Then the player taps → stage 0.15:
 *   (5) the Tower card flies up from the bottom of the screen,
 *   (6) the "The card's cost" caption, once the card has settled.
 *
 * Every timing below is a delay measured from stage entry. #game already
 * carries data-onboarding-stage="0.1" on the very first render, so the whole
 * sequence is pure CSS — no JS trigger, and the hidden start state is inherent
 * to the selector (no flash of the fully-revealed board). Stage 0.15's delays
 * restart from the tap, since the stage swap re-stamps the attribute.
 *
 * Skipped entirely under prefers-reduced-motion: the normal stage reveal shows
 * everything at once instead.
 * ─────────────────────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: no-preference) {
  /* (2) Resources panel frame (player's own — not the opponent block). */
  #game.onboarding-active[data-onboarding-stage="0.1"] .main-resources-group:not(.ob-el-opponentResources) {
    animation: ob01Fade 0.5s ease 0.15s both;
  }

  /* (3) Bricks inside the panel. The second selector also overrides the generic
     reveal-enter (a more specific rule that would otherwise flash the bricks in
     immediately) with the SAME delayed pop — identical params, so toggling the
     reveal-enter class doesn't restart it. `both` holds the bricks hidden from
     t=0 through the delay, keeping the panel empty until they pop. */
  #game.onboarding-active[data-onboarding-stage="0.1"] .ob-el-ownBricks,
  #game.onboarding-active[data-onboarding-stage="0.1"] .ob-el-ownBricks.onboarding-reveal-enter {
    animation: ob01Pop 0.45s cubic-bezier(0.22, 1, 0.36, 1) 0.75s both;
  }

  /* (5) The Tower card flies up from the bottom of the screen when stage 0.15
     begins (i.e. right after the tap — short 0.15s beat, not the old 1.6s
     wait). Animates the independent `translate` property (NOT `transform`) so
     it composes with the hand's own coverflow/fan transform instead of
     clobbering its position. */
  #game.onboarding-active[data-onboarding-stage="0.15"] #red_0_Tower,
  /* Variant B: the Catapult (its first playable card, slot 0) flies in the
     same way when the intro taps land on stage b1. */
  #game.onboarding-active[data-onboarding-stage="b1"] #red_0_Catapult {
    animation: ob01CardFly 0.7s cubic-bezier(0.22, 1, 0.36, 1) 0.15s both;
  }

  /* (4) + (6) The step captions. They live on <body> with data-stage, so
     they're scoped directly here. `both` keeps each invisible through its delay
     even while the settle loop repositions it — 0.1's caption waits for the
     bricks pop; 0.15's stays hidden while the card flies and only fades in
     once it has settled. */
  .onboarding-caption[data-stage="0.1"] {
    animation: onboardingCaptionIn 0.3s ease 1.05s both;
  }
  .onboarding-caption[data-stage="0.15"] {
    animation: onboardingCaptionIn 0.3s ease 0.95s both;
  }

  /* The pointing arrows share one container (rebuilt on every reposition, but the
     container element itself persists), so fade the container in with each
     step's caption. */
  body.onboarding-stage-01 #onboarding-arrow {
    animation: ob01Fade 0.3s ease 1.05s both;
  }
  body.onboarding-stage-015 #onboarding-arrow {
    animation: ob01Fade 0.3s ease 0.95s both;
  }
}

@keyframes ob01Fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes ob01Pop {
  from { opacity: 0; transform: translateY(10px) scale(0.96); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

@keyframes ob01CardFly {
  0% { opacity: 0; translate: 0 100vh; }
  55% { opacity: 1; }
  100% { opacity: 1; translate: 0 0; }
}

/* ───────────────────────────────────────────────────────────────────────────
 * First onboarding card play choreography (runOnboardingFirstPlayChoreo).
 * While `ob-firstplay-hold` is on #game, keep the castle hidden so its reveal
 * can be its own beat AFTER the played card lifts. The animation:none override
 * is needed because the generic reveal-enter keyframe would otherwise win over
 * the hold (CSS animations beat !important).
 * ─────────────────────────────────────────────────────────────────────────── */
#game.onboarding-active.ob-firstplay-hold .ob-el-ownCastle,
#game.onboarding-active.ob-firstplay-hold .ob-el-opponentCastle {
  visibility: hidden !important;
  opacity: 0 !important;
}
#game.onboarding-active.ob-firstplay-hold .ob-el-ownCastle.onboarding-reveal-enter,
#game.onboarding-active.ob-firstplay-hold .ob-el-opponentCastle.onboarding-reveal-enter {
  animation: none !important;
}

/* Warm glow pulse on the castle while its value counts up (filter, so it never
   clobbers the renderer's own transforms). */
.ob-firstplay-castle-glow {
  filter: drop-shadow(0 0 14px rgba(255, 210, 58, 0.85));
  transition: filter 0.4s ease;
}

/* Pop the played card's cost block as the resources fly into it. Plays once,
   settling back to its resting scale. */
.card-costs.ob-firstplay-cost-pop {
  animation: ob01CostPop 1s cubic-bezier(0.22, 1, 0.36, 1);
  transform-origin: center center;
}
@keyframes ob01CostPop {
  0% { transform: scale(1); }
  30% {
    transform: scale(1.28) translateY(-3px);
    filter: drop-shadow(0 0 9px rgba(255, 210, 58, 0.85));
  }
  100% { transform: scale(1); filter: none; }
}
