#rotate-hint-icon {
  font-size: 64px;
  line-height: 1;
}

/* short camera-shake hit-feedback, triggered from level-scene.js's triggerScreenShake() whenever
   the player actually takes damage (monster touch or wrong answer) - re-triggerable via the same
   remove/reflow/add-class trick used for #wrong-fist's .active class */
@keyframes screen-shake {
  0%, 100% {
    transform: translate(0, 0);
  }
  20% {
    transform: translate(-6px, 4px);
  }
  40% {
    transform: translate(6px, -4px);
  }
  60% {
    transform: translate(-5px, 5px);
  }
  80% {
    transform: translate(5px, -3px);
  }
}

#level-scene.screen-shake {
  animation: screen-shake 0.35s ease;
}

/* three-layer parallax background (far/mid/ground), each two STAGE_WIDTH-wide tiles positioned
   side by side; level-scene.js slides the whole layer via transform each frame (see
   createParallaxLayers) so the second tile seamlessly takes over as the first scrolls off */
#parallax-layers {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

.parallax-layer {
  position: absolute;
  inset: 0;
}

.parallax-tile {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--stage-width);
  height: 100%;
  background-size: cover;
  background-repeat: no-repeat;
}

/* placeholder texture so the parallax motion/seam is visible before real backgrounds land -
   only applied via the .placeholder class (see level-scene.js createParallaxLayers), which is
   added ONLY when that layer's real image failed to load - otherwise these are fully transparent
   so a loaded layer's own transparent regions correctly reveal the layer(s) stacked behind it */
.parallax-far .parallax-tile.placeholder {
  background-color: #16213e;
  background-image: repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.04) 0 2px, transparent 2px 64px);
}

.parallax-mid .parallax-tile.placeholder {
  background-color: #1a2744;
  background-image: repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.06) 0 2px, transparent 2px 48px);
}

.parallax-ground .parallax-tile.placeholder {
  background-color: #20304f;
  background-image: repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.08) 0 2px, transparent 2px 32px);
}

#player {
  position: absolute;
  background-color: var(--color-player-idle);
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center bottom; /* anchor the sprite's feet to the box's bottom edge - "contain" would otherwise letterbox top+bottom and visually float the feet above the actual ground line */
  z-index: 5; /* render above the gate/monster/platforms - the door is passable, so the character shouldn't be hidden behind it */
  pointer-events: none; /* game-world visuals only - never let a rendered entity steal a click meant for a UI button that happens to overlap it on screen */
  transform: scale(2);
  transform-origin: center bottom; /* grow upward from the feet, hitbox/collision size is unchanged - purely a visual enlargement */
}

/* jump.png only has one facing baked in (right) - mirror it in CSS while airborne and moving
   left, toggled by level-scene.js's render() from player.facing. run_left_0..2/run_right_0..2
   already has dedicated left-facing art so this never applies to the run state. */
#player.facing-left {
  transform: scale(2) scaleX(-1);
}

/* background-color is the color-block placeholder; render() sets an inline background-color of
   'transparent' the moment a real sprite loads (see level-scene.js), so the placeholder color
   only shows for as long as the art file is still missing */
#player[data-state='run'] {
  background-color: var(--color-player-run);
}

#player[data-state='jump'] {
  background-color: var(--color-player-jump);
}

/* "knocked flying" tumble after the 鐵拳 wrong-answer knockback (see battery.js's
   wrongAnswerFlyTimer) - the physics launch alone read as too subtle, so this adds a visible
   spin. transform-origin flips to center (from the base rule's center-bottom) so it tumbles like
   a body in the air instead of pivoting at the feet; each keyframe keeps the base scale(2) so the
   character doesn't snap back to its unscaled size mid-animation. */
#player.knocked {
  animation: player-knocked-fly 0.6s ease-out;
  transform-origin: center center;
}

@keyframes player-knocked-fly {
  0% {
    transform: scale(2) rotate(0deg);
  }
  30% {
    transform: scale(2) rotate(-30deg);
  }
  60% {
    transform: scale(2) rotate(20deg);
  }
  100% {
    transform: scale(2) rotate(0deg);
  }
}

/* invincibility-frame flicker on monster contact (規格外新增功能) - triggered via
   level-scene.js's triggerHitFlash(), same re-trigger trick as #wrong-fist/.screen-shake.
   steps(1, end) gives a hard on/off blink rather than a smooth fade. */
#player.hit-flash {
  animation: player-hit-flash 0.5s steps(1, end);
}

@keyframes player-hit-flash {
  0%,
  100% {
    opacity: 1;
  }
  10%,
  30%,
  50%,
  70%,
  90% {
    opacity: 0.25;
  }
  20%,
  40%,
  60%,
  80% {
    opacity: 1;
  }
}

/* shield item active: a pseudo-element ring, sized as its own square so it always renders as a
   true circle regardless of the player's own rectangular hitbox (40x60) - putting border-radius:
   50% on the player box itself came out oval, stretched to match that 40x60 ratio. Toggled by
   level-scene.js's render() from player.shielded - stays on until the shield actually absorbs a
   hit (battery.js clears it); no separate expiry timer per user request. */
#player.shielded::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 76px;
  height: 76px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  border: 2px solid rgba(59, 238, 226, 0.7); /* 變細的線條，原本是 box-shadow 0 0 0 4px 的粗框 */
  box-shadow: 0 0 30px 10px var(--color-radar-glow); /* 加大發光特效 */
  pointer-events: none;
}

.platform {
  position: absolute;
  background: var(--metal-gradient);
  border-top: 3px solid var(--level-accent, var(--color-radar-glow));
  box-shadow: 0 0 10px var(--level-accent, var(--color-radar-glow)), inset 0 2px 3px rgba(255, 255, 255, 0.2);
  pointer-events: none;
}

.monster {
  position: absolute;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center bottom; /* same reasoning as #player - keep the feet flush with the box bottom */
  pointer-events: none;
  transform: scale(2);
  transform-origin: center bottom; /* grow upward from its feet, hitbox/collision size is unchanged - purely a visual enlargement */
}

/* circular sci-fi portal: level-scene.js shows lock.gif at 40% opacity while not yet cleared (or
   cleared but not yet scrolled into view), then switches to unlock.gif at full opacity + the
   .gate-float bob once the player actually sees it (see GATE_LOCK_IMAGE/GATE_UNLOCK_IMAGE and
   updateGate) - both gifs animate on their own once painted, no more JS frame-cycling needed */
.level-gate {
  position: absolute;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  pointer-events: none; /* purely visual/collision obstacle, never an interactive UI element -
    must never intercept clicks meant for buttons like #shop-toggle that happen to overlap it */
}

.level-gate.gate-float {
  animation: gate-float 2s ease-in-out infinite;
}

@keyframes gate-float {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-10px);
  }
}

#battery-hud {
  position: absolute;
  top: 16px;
  left: 16px;
  display: flex;
  gap: 6px;
  z-index: 10;
  pointer-events: none;
}

.battery-segment {
  position: relative;
  width: 36px;
  height: 18px;
  border: 2px solid var(--color-battery-border);
  background: var(--color-battery-empty);
}

.battery-fill {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: 0;
  background: var(--color-battery-fill);
}

.battery-segment.full .battery-fill {
  width: 100%;
}

.battery-segment.half .battery-fill {
  width: 50%;
}

/* pure-CSS hexagon "holographic info ring" (文件 12.1: 六邊形全息資訊環，扁平懸浮 UI 面板感
   ＋外圈發光＋淡淡的六邊形網格紋理). One custom property (--bubble-kind-color) drives the rim,
   the outer glow, and the icon color together so each kind reads as a single coherent hue:
   ::before = thin glowing rim, .bubble itself = dark translucent glass fill (shared by every
   kind, tech-panel base), ::after = faint hex-grid texture on top, .bubble-icon (real child
   element, not just text) = the neon-colored symbol in the middle. */
.bubble {
  position: absolute;
  z-index: 1;
  clip-path: polygon(25% 4%, 75% 4%, 100% 50%, 75% 96%, 25% 96%, 0% 50%);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px;
  background: rgba(26, 26, 46, 0.65); /* 像遊戲本身舞台底色 --color-bg-stage 的暗色系、微透明，四種泡泡共用同一個底色 */
  box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.5), inset 0 2px 4px rgba(255, 255, 255, 0.1);
  pointer-events: none; /* touched via AABB overlap in game logic, not real clicks/taps */
}

.bubble::before {
  /* thin glowing rim - ALWAYS the same cyan tech-frame color on every kind (only the icon in the
     middle differs by kind); filter lives here (not on .bubble) so it can never interact
     with/hide the flex-centered icon child */
  content: '';
  position: absolute;
  inset: -2px;
  z-index: 0;
  clip-path: polygon(25% 4%, 75% 4%, 100% 50%, 75% 96%, 25% 96%, 0% 50%);
  background: var(--color-radar-glow);
  filter: drop-shadow(0 0 7px var(--color-radar-glow));
}

.bubble::after {
  /* faint hex-grid texture overlay on top of the dark fill */
  content: '';
  position: absolute;
  inset: 2px;
  z-index: 1;
  clip-path: polygon(25% 4%, 75% 4%, 100% 50%, 75% 96%, 25% 96%, 0% 50%);
  background-image: repeating-linear-gradient(60deg, rgba(255, 255, 255, 0.14) 0 1px, transparent 1px 8px),
    repeating-linear-gradient(-60deg, rgba(255, 255, 255, 0.14) 0 1px, transparent 1px 8px);
}

.bubble-icon {
  position: relative;
  z-index: 2; /* explicitly above ::before (0) and ::after (1) - never guessing at implicit paint order */
  font-size: 16px;
  font-weight: 900;
  color: var(--bubble-kind-color, #fff);
  /* crisp, no glow/blur on the icon itself - only the bubble's outer rim (::before) glows;
     this is just a tight dark outline for legibility against the dark fill */
  text-shadow: 0 0 2px rgba(0, 0, 0, 0.9), 0 1px 1px rgba(0, 0, 0, 0.9);
}

/* real art (trigger + answer bubbles) already has its own baked-in glow ring/frame - strip every
   CSS-drawn layer (hex clip, dark glass fill, rim, grid texture) and just show the image */
.bubble-has-image {
  clip-path: none;
  background: none;
  box-shadow: none;
}

.bubble-has-image::before,
.bubble-has-image::after {
  content: none;
}

.bubble.bubble-has-image {
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}

.bubble-trigger {
  --bubble-kind-color: var(--color-bubble-trigger);
}

.bubble-item {
  --bubble-kind-color: var(--color-item-glow); /* 問號泡泡：亮藍綠色科技感 */
}

.bubble-answer {
  --bubble-kind-color: var(--color-bubble-answer);
  /* fade in when the options appear after the reveal delay (第十一階段第3部分) - opacity ONLY, not
     transform: level-scene.js's render() already writes `transform` on this element every single
     frame for the pulse/float effect, so animating transform here would fight that inline write
     (same class of bug as the countdown urgent-pulse / hint-arrow mirroring issues earlier) */
  animation: bubble-answer-reveal 0.3s ease-out;
}

@keyframes bubble-answer-reveal {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

.bubble-coin {
  --bubble-kind-color: var(--color-bubble-coin);
}

/* coin bubble's icon is a small embossed gold coin token instead of flat text - no outer glow,
   just an inset highlight for the embossed look (the bubble's rim already glows) */
.bubble-coin .bubble-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #fff6c8, #ffd60a 55%, #b8860b 100%);
  color: #7a5c00;
  text-shadow: none;
  font-size: 12px;
  box-shadow: inset 0 0 3px rgba(255, 255, 255, 0.7);
}

/* idle ambient animations for coin/item pickup bubbles, so they read as "alive" while sitting in
   the level (trigger bubbles get their own breathing pulse instead - see entities.js's
   DEFAULT_BUBBLE_PULSE). level-scene.js's render() also writes an inline `transform:
   scale(bubble.scale)` every frame, but coin/item never get a non-zero pulseAmplitude, so that
   inline value is always the no-op scale(1) - a running CSS animation on `transform` takes
   priority over it per the cascade, so these play uninterrupted despite the inline write. */
.bubble-coin {
  animation: coin-flip 1.4s linear infinite;
}

@keyframes coin-flip {
  from {
    transform: rotateY(0deg);
  }
  to {
    transform: rotateY(360deg);
  }
}

.bubble-item {
  animation: item-float 1.6s ease-in-out infinite;
}

@keyframes item-float {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-10px);
  }
}

/* answer-correct effect: swapped in via level-scene.js's playAnswerEffect() right before the
   bubble is removed. Long enough (0.55s, matches ANSWER_EFFECT_DELAY in quiz.js) to read as
   "correct" before the bubble disappears - just a scale+fade+glow, no ✓ icon (removed per user
   request). filter works even on real bubble art (.bubble-has-image), unlike ::before/::after
   which that variant strips. Wrong answers get no equivalent ✗/red treatment either - see
   #wrong-fist for that feedback instead, per user request. */
.bubble-pop {
  animation: bubble-pop 0.55s ease forwards;
}

@keyframes bubble-pop {
  0% {
    transform: scale(1);
  }
  40% {
    transform: scale(1.5);
    filter: drop-shadow(0 0 16px #2ecc71);
  }
  100% {
    transform: scale(1.8);
    opacity: 0;
    filter: drop-shadow(0 0 16px #2ecc71);
  }
}

/* wrong-hit bubble and the rest of the answer group both just fade out the same way - no bubble
   gets singled out with red/✗ styling anymore, the fist below is the "wrong" feedback now */
.bubble-fade-out {
  animation: bubble-fade-out 0.45s ease forwards;
}

@keyframes bubble-fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

/* pickup feedback burst - level-scene.js's spawnPickupSparkle() creates one .pickup-sparkle
   wrapper (positioned at the pickup's on-screen point) containing a handful of .pickup-sparkle-
   particle dots, each given its own --sparkle-dx/--sparkle-dy flight vector and --sparkle-color
   inline so a single shared keyframe covers every direction/color. Self-removed via setTimeout
   in JS (PICKUP_SPARKLE_DURATION), matching the animation length below. */
.pickup-sparkle {
  position: absolute;
  width: 0;
  height: 0;
  pointer-events: none;
  z-index: 8;
}

.pickup-sparkle-particle {
  position: absolute;
  top: 0;
  left: 0;
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  border-radius: 50%;
  background: var(--sparkle-color, #fff);
  box-shadow: 0 0 6px var(--sparkle-color, #fff);
  animation: pickup-sparkle 0.45s ease-out forwards;
}

@keyframes pickup-sparkle {
  0% {
    transform: translate(0, 0) scale(1);
    opacity: 1;
  }
  100% {
    transform: translate(var(--sparkle-dx), var(--sparkle-dy)) scale(0.2);
    opacity: 0;
  }
}

/* pickup "flies to the HUD" feedback (per user request) - a small copy of the coin/item icon
   travels from the pickup point to whichever HUD element just gained a count, shrinking/fading
   out along the way. level-scene.js's spawnPickupFlyEffect() sets the start position via
   left/top, then changes transform+opacity on the next frame so this transition actually plays;
   self-removed via setTimeout matching PICKUP_FLY_DURATION. ease-in so it reads as "pulled" into
   the HUD rather than drifting there passively. */
.pickup-fly {
  position: absolute;
  width: 32px;
  height: 32px;
  margin: -16px 0 0 -16px; /* centers on the pickup point, same convention as .pickup-sparkle-particle */
  pointer-events: none;
  z-index: 30;
  /* strong ease-in: crawls at first, then rockets toward the target - the "magnet pull"
     acceleration the user asked for (per user report the old plain ease-in read as too gentle/
     linear-feeling and didn't sell "being pulled in") */
  transition: transform 0.6s cubic-bezier(0.65, 0, 0.85, 0.35);
}

/* separate inner element for scale/opacity, decoupled from the outer position transition above -
   stays full-size/opaque for the first half of the flight, then shrinks+fades only in the final
   half (via transition-delay) so it visibly "arrives" at the HUD before disappearing, instead of
   fading out mid-flight while still far from the target (per user report of the old single-
   transition version vanishing before it looked like it had gotten anywhere close) */
.pickup-fly-icon {
  width: 100%;
  height: 100%;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  opacity: 1;
  transition: transform 0.3s ease-in, opacity 0.3s ease-in;
  transition-delay: 0.3s;
}

/* "鐵拳" wrong-answer knockback effect (規格外新增功能, see battery.js's applyWrongAnswerKnockback)
   - punches up from the ground at the character's feet then retreats. Triggered once per wrong
   answer via level-scene.js's playWrongAnswerFist(), which also repositions it, so .active is
   just re-added to restart the animation from scratch each time. */
#wrong-fist {
  position: absolute;
  z-index: 6; /* above #player (5) so it visibly strikes in front of the character */
  background: #c0392b;
  border: 3px solid #7a1f11;
  border-radius: 14px 14px 6px 6px;
  box-shadow: 0 0 14px rgba(192, 57, 43, 0.7);
  pointer-events: none;
  opacity: 0;
  transform: translateY(70px) scale(0.6);
}

/* real fist.png art loaded - drop the color-block placeholder chrome, just show the image */
#wrong-fist.has-image {
  background-color: transparent;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  border: none;
  border-radius: 0;
  box-shadow: none;
}

#wrong-fist.active {
  animation: wrong-fist-punch 1s cubic-bezier(0.2, 0.8, 0.3, 1) forwards;
}

@keyframes wrong-fist-punch {
  0% {
    opacity: 0;
    transform: translateY(80px) scale(0.6);
  }
  20% {
    opacity: 1;
    transform: translateY(-20px) scale(1.1);
  }
  35% {
    opacity: 1;
    transform: translateY(-10px) scale(1);
  }
  75% {
    opacity: 1;
    transform: translateY(-10px) scale(1);
  }
  100% {
    opacity: 0;
    transform: translateY(30px) scale(0.9);
  }
}

/* single merged panel holding both the question and its options (no more separate per-box
   frames) - fixed width every level (not sized to content) so long questions never wrap, just
   slightly narrower than the stage per user request (再向內縮小10px each side, then another 15px
   each side, then another 5px each side). Flat semi-transparent fill in the same hue as the old
   corner-glow accent, plus a matching thin border for edge definition. */
#quiz-banner {
  position: absolute;
  top: 68px; /* +3px, then another +5px per user request */
  left: 50%;
  width: calc(var(--stage-width) - 160px);
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px; /* restored to the original spacing per user request - the "調高一階" ask was about the
    option text's font-size, not moving it closer to the question (see .quiz-banner-option below) */
  padding: 30px;
  background: var(--color-quiz-panel-bg);
  border: 1px solid var(--color-radar-glow);
  z-index: 20;
  opacity: 0;
  transform: translateX(-50%) scaleX(0.02);
  transform-origin: center;
  transition: transform 0.2s ease-in, opacity 0.2s ease-in; /* the "screen closing" collapse when .visible is removed */
  pointer-events: none;
}

/* "開機" power-on effect: starts as a thin vertical line at full brightness, holds briefly, then
   snaps open from narrow to full width - a sharp, obvious snap rather than a gradual fade. */
#quiz-banner.visible {
  opacity: 1;
  pointer-events: auto;
  animation: quiz-screen-open 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

@keyframes quiz-screen-open {
  0%,
  15% {
    transform: translateX(-50%) scaleX(0.02);
  }
  100% {
    transform: translateX(-50%) scaleX(1);
  }
}

/* final countdown stretch (quiz.js's updateCountdownDisplay toggles .active) - a separate overlay
   element layered on top of the banner's own content, deliberately NOT touching #quiz-banner's own
   animation/transform/opacity. A previous version animated those directly on #quiz-banner and it
   broke the display, because #quiz-banner.visible above also sets the `animation` shorthand (for
   the opening snap) and the two animations fought over the same property on the same element. This
   overlay owns its own animation entirely, so that collision can't happen. inset:0 fills the same
   box #quiz-banner already occupies (it's the positioned ancestor), so no extra positioning math
   is needed here. */
.quiz-banner-urgent-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  border: 2px solid var(--color-gate-lock);
  background-color: rgba(230, 57, 70, 0.3);
}

.quiz-banner-urgent-overlay.active {
  animation: quiz-banner-urgent-pulse 0.5s ease-in-out infinite;
}

@keyframes quiz-banner-urgent-pulse {
  0%,
  100% {
    opacity: 0.2;
    box-shadow: 0 0 10px 2px rgba(230, 57, 70, 0.4);
  }
  50% {
    opacity: 0.75;
    box-shadow: 0 0 28px 8px rgba(230, 57, 70, 0.8);
  }
}

.quiz-banner-question {
  width: 100%;
  /* reserve room for the top-right countdown badge (see .quiz-banner-countdown) so a wrapped
     line can never run under it */
  max-width: calc(100% - 100px);
  margin: 0 auto;
  font-size: 24px;
  font-weight: 700;
  color: #fff;
  text-align: center;
  /* wrap instead of truncating - long questions must stay fully readable, never cut off with an
     ellipsis (per user request); word-break covers any run of characters/latin text long enough
     to overflow a single line on its own */
  white-space: normal;
  word-break: break-word;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
}

/* options stay on one row, never wrap */
.quiz-banner-options {
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: center;
  gap: 16px;
}

.quiz-banner-option {
  font-size: 24px; /* match .quiz-banner-question's size per user request */
  color: rgba(255, 255, 255, 0.9);
  text-align: center;
  white-space: nowrap;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
}

/* 是非題用來佔位、不顯示文字的選項 - 保留版面高度但不佔用版面寬度以外的視覺空間
   (see quiz.js's showBanner) */
.quiz-banner-option-hidden {
  visibility: hidden;
}

/* answer timeout countdown (第十階段第2批，位置與樣式依第十階段追加需求調整) - back to an absolute
   overlay in the banner's top-right corner (per user request - a separate flex row pushed the
   whole banner taller again, which they didn't want). Overlap with the question is instead
   prevented on the question's side (max-width + ellipsis, see .quiz-banner-question). Bare number
   only, no "倒數/秒" text. Skipped entirely (see quiz.js's showBanner) when answerTimeoutSeconds is
   disabled. */
.quiz-banner-countdown {
  position: absolute;
  top: 10px;
  right: 16px;
  font-size: 32px;
  font-weight: 800;
  color: var(--color-radar-glow);
  text-shadow: 0 0 8px var(--color-radar-glow), 0 1px 3px rgba(0, 0, 0, 0.85);
  animation: countdown-blink 1s ease-in-out infinite;
}

@keyframes countdown-blink {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.45;
  }
}

/* last few seconds (quiz.js toggles this once remaining <= 3s) - faster blink + a size pulse, and
   switches to the same red used for the locked-gate art, so it reads as an alarm, not just a timer */
.quiz-banner-countdown.urgent {
  color: var(--color-gate-lock);
  text-shadow: 0 0 10px var(--color-gate-lock), 0 1px 3px rgba(0, 0, 0, 0.85);
  animation: countdown-urgent 0.4s ease-in-out infinite;
}

@keyframes countdown-urgent {
  0%,
  100% {
    opacity: 1;
    transform: scale(1);
  }
  50% {
    opacity: 0.5;
    transform: scale(1.2);
  }
}

/* Temporary debug hint (see DEBUG_MODE in main.js) - shows what the last collision resolved as. */
#debug-hint {
  position: absolute;
  top: 60px; /* below #gold-hud, which now also sits at top:16/right:16 */
  right: 16px;
  padding: 8px 14px;
  background: rgba(0, 0, 0, 0.75);
  color: #fff;
  border-radius: 6px;
  font-size: 14px;
  z-index: 30;
  opacity: 0;
  transition: opacity 0.4s ease;
  pointer-events: none;
}

#debug-hint.visible {
  opacity: 1;
}

/* now a flex child inside #level-top-right (see below) instead of its own absolutely-positioned
   corner element. coin.png icon + count replaces the old "金幣：N" text label per user request. */
#gold-hud {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  background: rgba(0, 0, 0, 0.6);
  border-radius: 6px;
  pointer-events: none;
}

#gold-hud-icon {
  width: 30px; /* 1.5x per user request (原為20px) */
  height: 30px;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}

#gold-hud-count {
  color: var(--color-bubble-coin);
  font-size: 16px; /* +1 級 per user request (原為14px) */
  font-weight: 700;
}

/* gold + shop + sfx/music toggles, per user request */
#level-top-right {
  position: absolute;
  top: 16px;
  right: 16px;
  display: flex;
  align-items: center;
  gap: 10px;
  z-index: 10;
}

/* "已答題數/總題數" HUD, per user request - purely informational, updated every frame in render() */
#question-progress-hud {
  position: absolute;
  top: 16px;
  left: 50%;
  transform: translateX(-50%);
  padding: 4px 12px;
  background: rgba(0, 0, 0, 0.6);
  border-radius: 6px;
  color: var(--color-radar-glow);
  font-size: 14px;
  font-weight: 700;
  z-index: 10;
  pointer-events: none;
}

/* points toward the next objective (the portal once unlocked, or a randomly-respawned trigger
   bubble the player might not know the location of - see findNextObjective() in level-scene.js)
   - same pink-purple glow language as the gate/portal art (assets/images/gate/*.png).
   Screen-anchored (not world-space) so it stays visible regardless of where the camera currently
   is. Defaults to pointing right (the portal is always further right); .pointing-left mirrors the
   glyph and moves it to the left edge for a respawned trigger bubble that's actually behind the
   player. */
#gate-hint-arrow {
  position: absolute;
  top: 50%;
  right: 24px;
  transform: translateY(-50%);
  font-size: 48px;
  line-height: 1;
  color: var(--color-gate-hint);
  text-shadow: 0 0 12px var(--color-gate-hint), 0 0 24px var(--color-gate-hint);
  opacity: 0;
  pointer-events: none;
  z-index: 10;
  transition: opacity 0.3s ease;
}

#gate-hint-arrow.visible {
  opacity: 1;
  animation: gate-hint-sway 1s ease-in-out infinite;
}

#gate-hint-arrow.pointing-left {
  right: auto;
  left: 24px;
  transform: translateY(-50%) scaleX(-1);
}

/* needs its own keyframes (not just the base .pointing-left transform) - once an animation is
   running, its keyframes fully own the `transform` property for the duration, so the base rule's
   scaleX(-1) would otherwise be dropped the moment gate-hint-sway takes over (same pitfall as the
   countdown urgent-pulse bug from earlier) */
#gate-hint-arrow.pointing-left.visible {
  animation: gate-hint-sway-left 1s ease-in-out infinite;
}

@keyframes gate-hint-sway {
  0%,
  100% {
    transform: translateY(-50%) translateX(0);
  }
  50% {
    transform: translateY(-50%) translateX(14px);
  }
}

@keyframes gate-hint-sway-left {
  0%,
  100% {
    transform: translateY(-50%) scaleX(-1) translateX(0);
  }
  50% {
    transform: translateY(-50%) scaleX(-1) translateX(14px);
  }
}

/* unified button skin for every clickable button across the game (level HUD, shop, map, end
   screen) - same metal-gradient + cyan glow language as the platforms/quiz panel, replacing the
   old flat black boxes. Each caller keeps its own sizing (padding/font-size/min-width) on its own
   id/class; .btn only owns the shared look. */
.btn {
  border: 1px solid var(--color-radar-glow);
  border-radius: 6px;
  background: var(--metal-gradient);
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
  box-shadow: 0 0 8px rgba(59, 238, 226, 0.45), inset 0 1px 2px rgba(255, 255, 255, 0.15), inset 0 -2px 4px rgba(0, 0, 0, 0.35);
  cursor: pointer;
  transition: box-shadow 0.15s ease, transform 0.1s ease;
}

.btn:hover:not(:disabled) {
  box-shadow: 0 0 14px rgba(59, 238, 226, 0.7), inset 0 1px 2px rgba(255, 255, 255, 0.2), inset 0 -2px 4px rgba(0, 0, 0, 0.35);
}

.btn:active:not(:disabled) {
  transform: scale(0.96);
}

.btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  box-shadow: none;
}

/* #shop-toggle and #level-exit both just reuse .map-icon-button's square-icon sizing as flex
   children of #level-top-right (see above) - no positioning of their own needed here anymore.
   #level-exit used to be its own bottom-left corner button; moved into this row per user request
   so the touch-jump button below no longer needs to dodge it (see #touch-jump). */

#item-bar {
  position: absolute;
  bottom: 16px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 28px; /* wide enough that each button's own left/right bracket art doesn't collide with its neighbor's */
  z-index: 10;
}

/* on-screen jump/left/right buttons for touch devices (ui/touch-controls.js) - per user request,
   jump sits bottom-left and the left/right dpad sits bottom-right (swapped from the usual layout). */
.touch-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 72px;
  height: 72px;
  border-radius: 50%;
  background: rgba(19, 28, 46, 0.55); /* --color-bg-panel at low opacity, stays legible over any background art */
  color: #fff;
  font-size: 28px;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none; /* no scroll/zoom gestures hijacking a press */
  z-index: 10;
}

.touch-btn.active {
  background: rgba(78, 205, 196, 0.55); /* --color-player-idle tint - visible press feedback */
}

/* custom button art loaded (see ui/touch-controls.js's applyButtonArt) - drop the circular glass
   chrome and arrow glyph font-size, just show the image; still uses the same active/press
   feedback below via filter instead of a background-color swap, since a background-color would
   only show at the edges of a non-circular image */
.touch-btn.has-image {
  background-color: transparent;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  border-radius: 0;
}

.touch-btn.has-image.active {
  background-color: transparent;
  filter: brightness(1.35) drop-shadow(0 0 8px rgba(78, 205, 196, 0.85));
}

/* same bottom offset as #touch-dpad (below) so both touch clusters sit on one shared baseline -
   this used to be pushed up to bottom:100px to dodge #level-exit, which sat at bottom:16/left:16;
   now that button has moved into the top-right icon row (see level-scene.js), this corner is free */
#touch-jump {
  position: absolute;
  bottom: 16px;
  left: 16px;
}

#touch-dpad {
  position: absolute;
  bottom: 16px;
  right: 16px;
  display: flex;
  gap: 14px;
}

/* shown when useItem() fails (e.g. hint pressed before any answer bubbles exist, or charge
   pressed at full battery), and also reused for the level-start "答對 N 題即可過關" toast (see
   level-scene.js's showItemTip calls) - a real player-facing message, always visible, unlike
   #debug-hint. Moved to dead-center of the screen per user request so it can't be missed/mistaken
   for background noise near the item bar. */
#item-tip {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 8px 16px;
  background: rgba(0, 0, 0, 0.8);
  border: 1px solid var(--color-radar-glow);
  border-radius: 6px;
  color: #fff;
  font-size: 19px; /* 原 13px，再往大調兩階 per user request */
  white-space: nowrap;
  z-index: 15;
  opacity: 0;
  transition: opacity 0.25s ease;
  pointer-events: none;
}

#item-tip.visible {
  opacity: 1;
}

/* confirm-before-leave modal, shown on "離開關卡" click while the attempt isn't cleared yet -
   same overlay/panel language as #tutorial-modal */
#exit-confirm-modal {
  display: none;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.65);
  align-items: center;
  justify-content: center;
  z-index: 50;
}

#exit-confirm-modal.visible {
  display: flex;
}

#exit-confirm-panel {
  max-width: 400px;
}

#exit-confirm-text {
  font-size: 15px;
  line-height: 1.6;
  color: #fff;
  margin-bottom: 16px;
  white-space: pre-line; /* textContent's \n only renders as a real line break with this set */
}

#exit-confirm-buttons {
  display: flex;
  gap: 12px;
  justify-content: center;
}

#exit-confirm-buttons .btn {
  min-width: 100px;
  padding: 8px 16px;
  font-size: 14px;
}

/* each item gets its own decorative bracket pair (item-frame-left/right.png) flanking the button,
   positioned just outside its edges rather than baked into a shared bar background. Icon + count
   replaces the old "名稱 xN" text label per user request. No .btn metal-gradient/border/glow chrome
   here (removed per user request) - just the bare icon+count floating over the game view. */
.item-button {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 6px 14px;
  font-size: 13px;
  background: none;
  border: none;
  box-shadow: none;
}

.item-button:hover:not(:disabled) {
  box-shadow: none;
  filter: brightness(1.3);
}

.item-button::before,
.item-button::after {
  content: '';
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 12px;
  height: 34px;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  pointer-events: none;
}

.item-button::before {
  left: -14px;
  background-image: url('../assets/images/ui/item-frame-left.png');
}

.item-button::after {
  right: -14px;
  background-image: url('../assets/images/ui/item-frame-right.png');
}

.item-button-icon {
  width: 42px; /* 1.5x per user request (原為28px) */
  height: 42px;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}

.item-button-count {
  font-size: 14px; /* +1 級 per user request (原為12px) */
  font-weight: 700;
}

/* custom tooltip (not native title="") so it can match the game's holographic-panel look - a
   real child element toggled purely by CSS :hover, no JS needed. Disabled buttons (owned <= 0)
   still show it, so the player can learn what an item does before ever buying one. Name label
   removed per user request - description only. Background alpha lowered to 30% (also per user
   request) so it reads as a translucent HUD overlay rather than a solid opaque card; the
   opacity/transform pair below is the separate show/hide transition, untouched. */
.item-tooltip {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translate(-50%, 4px);
  width: 180px;
  padding: 8px 10px;
  background: rgba(28, 37, 50, 0.3);
  border: 1px solid var(--color-radar-glow);
  border-radius: 6px;
  box-shadow: 0 0 12px rgba(59, 238, 226, 0.35);
  color: #fff;
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease, transform 0.15s ease;
  z-index: 30;
}

.item-tooltip::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-top-color: var(--color-radar-glow);
}

.item-button:hover .item-tooltip,
.item-button:focus-visible .item-tooltip {
  opacity: 1;
  transform: translate(-50%, 0);
}

.item-tooltip-desc {
  font-size: 15px; /* +2 級 per user request (原為11px) */
  line-height: 1.4;
  color: rgba(255, 255, 255, 0.85);
}

#shop-scene {
  display: none;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.65);
  align-items: center;
  justify-content: center;
  z-index: 40;
}

#shop-scene.visible {
  display: flex;
}

/* shared modal panel skin (shop + tutorial) - same metal-gradient/cyan-glow language as .btn */
.panel {
  background: var(--metal-gradient);
  border: 1px solid var(--color-radar-glow);
  border-radius: 12px;
  box-shadow: 0 0 20px rgba(59, 238, 226, 0.25), inset 0 1px 2px rgba(255, 255, 255, 0.08);
  padding: 24px;
  color: #fff;
  text-align: center;
}

#shop-panel {
  min-width: 480px;
  background: rgba(28, 37, 50, 0.5); /* overrides .panel's opaque metal-gradient - 50% per user request */
}

#shop-title {
  font-size: 24px;
  font-weight: 700;
  margin-bottom: 8px;
}

#shop-gold {
  font-size: 16px;
  color: var(--color-bubble-coin);
  margin-bottom: 16px;
}

#shop-cards {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
  margin-bottom: 16px;
}

/* prev/next paging controls, per user request - only populated by shop-scene.js's render() once
   items.json has more than SHOP_PAGE_SIZE entries, so this stays empty/invisible until then */
#shop-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin-bottom: 16px;
}

/* compound selector (not just .shop-pager-btn) so this reliably beats .map-icon-button's own
   sizing regardless of which rule happens to come later in the file */
.shop-pager-btn.map-icon-button {
  min-width: 36px;
  min-height: 36px;
  padding: 4px;
  font-size: 14px;
}

#shop-pager-indicator {
  font-size: 13px;
  color: rgba(255, 255, 255, 0.75);
  white-space: nowrap;
}

.shop-card {
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(59, 238, 226, 0.25);
  border-radius: 8px;
  padding: 12px;
}

/* invisible but still occupies its grid cell (same internal structure as a real card, see
   shop-scene.js) - keeps the grid, and everything below it, at a consistent height across pages */
.shop-card-filler {
  visibility: hidden;
}

.shop-card-icon {
  width: 56px;
  height: 56px;
  margin: 0 auto 8px;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}

.shop-card-name {
  font-size: 16px;
  font-weight: 700;
  margin-bottom: 6px;
}

.shop-card-desc {
  font-size: 12px;
  line-height: 1.4;
  color: rgba(255, 255, 255, 0.6);
  margin-bottom: 8px;
  min-height: 2.8em; /* reserves 2 lines so every card's buy button still lines up regardless of description length */
}

.shop-card-detail {
  font-size: 13px;
  color: rgba(255, 255, 255, 0.75);
  margin-bottom: 4px;
}

.shop-card-buy {
  margin-top: 8px;
  padding: 6px 14px;
  font-size: 13px;
}

#shop-close {
  padding: 8px 20px;
  font-size: 14px;
}

.scene-hidden {
  display: none !important;
}

/* shared layout + background for the two full-page scenes - #end-scene now uses the same
   background image as the map screen, per user request */
#map-scene,
#end-scene {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  color: #fff;
  background-image: url('../assets/images/frontPage_background.png');
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

#map-title {
  font-size: 32px;
  font-weight: 700;
  color: var(--color-radar-glow);
}

#end-message {
  font-size: 32px;
  font-weight: 700;
}

#radar-screen {
  position: relative;
  width: 480px;
  height: 480px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--color-radar-bg) 0%, #000 100%);
  border: 2px solid var(--color-radar-glow);
  box-shadow: 0 0 24px var(--color-radar-sweep);
  overflow: hidden;
}

.radar-grid {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.radar-grid circle,
.radar-grid line {
  fill: none;
  stroke: var(--color-radar-line);
  stroke-width: 0.4;
}

/* degree tick marks + numbers around the rim, per 遊戲關卡參考圖.png's military-radar look */
.radar-tick {
  stroke: var(--color-radar-glow);
  stroke-width: 0.5;
  opacity: 0.6;
}

.radar-tick-label {
  fill: var(--color-radar-glow);
  font-size: 3.2px;
  font-family: inherit;
  opacity: 0.75;
}

.radar-sweep {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(from 0deg, var(--color-radar-sweep), transparent 60deg, transparent 360deg);
  animation: radar-rotate 5s linear infinite;
  pointer-events: none;
}

@keyframes radar-rotate {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.radar-level {
  position: absolute;
  width: 56px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px; /* extra room below the icon since scale(1.5) overflows its own 56px layout box */
  z-index: 5;
}

.radar-level-icon {
  width: 56px;
  height: 56px;
  border: none;
  background: none;
  cursor: pointer;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  /* pure image, no circle chrome - just a bigger monster portrait, gently bobbing in place until
     hovered/focused (animation-delay staggered per icon in map-scene.js so the row isn't synced) */
  animation: radar-icon-bob 2.2s ease-in-out infinite;
}

@keyframes radar-icon-bob {
  0%,
  100% {
    transform: scale(1.5) translateY(0);
  }
  50% {
    transform: scale(1.5) translateY(-6px);
  }
}

/* hovering/focusing a monster "selects" it: stop the bob and light it up with the radar glow */
.radar-level-icon:hover,
.radar-level-icon:focus-visible {
  animation-play-state: paused;
  filter: drop-shadow(0 0 8px var(--color-radar-glow)) drop-shadow(0 0 16px var(--color-radar-glow)) brightness(1.2);
}

.radar-level-icon.cleared {
  filter: grayscale(1);
}

.radar-level-icon.cleared:hover,
.radar-level-icon.cleared:focus-visible {
  animation-play-state: paused;
  filter: grayscale(1) drop-shadow(0 0 8px var(--color-radar-glow)) drop-shadow(0 0 16px var(--color-radar-glow)) brightness(1.2);
}

.radar-level-label {
  font-size: 12px;
  color: #fff;
  text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
  white-space: nowrap;
}

#map-top-buttons {
  position: absolute;
  top: 16px;
  right: 16px;
  display: flex;
  gap: 12px;
  z-index: 10;
}

.map-icon-button {
  min-width: 44px;
  min-height: 44px;
  padding: 8px 14px;
  font-size: 16px;
}

#tutorial-modal {
  display: none;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.65);
  align-items: center;
  justify-content: center;
  z-index: 50;
}

#tutorial-modal.visible {
  display: flex;
}

#tutorial-panel {
  max-width: 560px;
}

#tutorial-title {
  font-size: 26px;
  font-weight: 700;
  margin-bottom: 19px;
}

#tutorial-text {
  font-size: 20px; /* 原 15px，放大一階到 18px，再放大到 20px per user request */
  line-height: 1.6;
  text-align: left;
  margin-bottom: 16px;
}

#tutorial-close {
  min-width: 44px;
  min-height: 44px;
  padding: 8px 24px;
  font-size: 14px;
}

.end-button {
  min-width: 220px;
  padding: 14px 24px;
  font-size: 18px;
}
