* {
  box-sizing: border-box;
}

:root {
  /* Cute cartoon mallet — hand-drawn rubber mallet (rounded pink head with
     a little face, tan handle) rather than the plain 🔨 emoji. Shared by
     the board cursor and the tap-hit swing animation below. */
  --mallet-svg: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><rect x='41' y='42' width='18' height='52' rx='9' fill='%23d9a066' stroke='%238a5a25' stroke-width='3'/><rect x='16' y='6' width='68' height='42' rx='21' fill='%23ff8a80' stroke='%23c0392b' stroke-width='4'/><ellipse cx='33' cy='19' rx='12' ry='7' fill='white' opacity='0.45'/><circle cx='39' cy='24' r='3.5' fill='%232b2b2b'/><circle cx='58' cy='24' r='3.5' fill='%232b2b2b'/><path d='M 36 33 Q 48 41 61 33' stroke='%232b2b2b' stroke-width='3' fill='none' stroke-linecap='round'/></svg>");
  /* Hotspot sits where the head meets the handle so the "hit point" lines
     up with where the student is tapping. */
  --hammer-cursor: var(--mallet-svg) 20 46, pointer;
}

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  overflow-x: hidden;
  /* Disables pinch-zoom and double-tap-zoom (both fall under "zoom" and are
     excluded by omitting pinch-zoom from the allowed gesture set) while
     still allowing normal scrolling — students kept accidentally
     double-tap-zooming mid-game. viewport's user-scalable=no alone doesn't
     stop this since iOS Safari has ignored that flag since iOS 10. */
  touch-action: pan-x pan-y;
}

body {
  font-family: "PingFang TC", "Microsoft JhengHei", "Noto Sans TC", sans-serif;
  background: linear-gradient(180deg, #eaf6ff 0%, #dff0e0 100%);
  color: #2b2b2b;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}

#app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 24px 16px 48px;
}

.screen {
  display: none;
  width: 100%;
  max-width: 900px;
  flex-direction: column;
  align-items: center;
  text-align: center;
  animation: fade-in 0.25s ease;
}

.screen.active {
  display: flex;
}

#screen-game {
  padding: 16px;
}

/* Music-classroom illustration background (piano/xylophone/guitar/drum
   scene), only while the actual game screen is showing — toggled via JS
   (see showScreen()) on <body> rather than on #screen-game itself, since
   #screen-game is nested inside #app's centered/padded/max-width-capped
   layout and painting the image there confined it to a small boxed card
   instead of covering the full viewport (Martin's call: fill the whole
   screen edge-to-edge, not a small boxed area). <body> has no such
   constraints, so background-size:cover here reaches every edge. */
body.staff-game-active {
  background-image: url("../assets/background-classroom.png");
  background-size: cover;
  /* Anchored to the bottom (not top): the illustration's instruments live
     in its top half, and on short/landscape viewports `cover` crops most
     of the image's height away. Anchoring bottom crops from the top first,
     so the instrument row is the first thing to go rather than the empty
     floor the mole board actually sits on — keeps the holes clear of the
     piano/xylophone/guitar art instead of stacking on top of it. */
  background-position: center bottom;
  background-attachment: fixed;
  /* Martin's call, after real-device testing: the staff cue + mole board
     shouldn't move at all — fast repeated tapping was triggering the page's
     normal pan/scroll (html,body allow touch-action:pan-x pan-y globally,
     for other screens that legitimately need to scroll), making the whole
     board visibly slide around mid-round. Locking to none here only while
     the game screen is showing keeps every other screen's normal scrolling
     intact. overflow:hidden is belt-and-suspenders against any residual
     scroll (e.g. a mid-scroll position carried over from a taller previous
     screen — showScreen() also resets scrollTo(0,0) when entering this
     screen, see game.js). */
  touch-action: none;
  overflow: hidden;
}

@keyframes fade-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}

.title {
  font-size: clamp(28px, 6vw, 44px);
  margin: 8px 0 4px;
}

.subtitle {
  font-size: clamp(18px, 3vw, 22px);
  color: #555;
  margin-bottom: 20px;
}

/* Boot / preload loading screen */
.boot-loading {
  position: relative;
  width: min(78vw, 420px);
  height: min(78vw, 420px);
  margin: 24px auto;
}

.boot-ring {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
}

.boot-ring-bg {
  fill: none;
  stroke: #eee;
  stroke-width: 14;
}

.boot-ring-fill {
  fill: none;
  stroke: #ff9f43;
  stroke-width: 14;
  stroke-linecap: round;
  stroke-dasharray: 565.48;
  stroke-dashoffset: 565.48;
  transition: stroke-dashoffset 0.25s linear;
}

.boot-ring-center {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(28px, 7vw, 44px);
  font-weight: bold;
  color: #333;
}

.boot-note-icons {
  position: absolute;
  inset: 0;
}

.boot-note-icon {
  position: absolute;
  width: 15%;
  height: 15%;
  min-width: 44px;
  min-height: 44px;
  border-radius: 50%;
  background: white;
  object-fit: cover;
  filter: grayscale(100%);
  opacity: 0.35;
  box-shadow: 0 2px 6px rgba(0,0,0,0.08);
  transform: translate(-50%, -50%) scale(0.85);
  transition: filter 0.4s ease, opacity 0.4s ease, transform 0.3s ease;
}

.boot-note-icon.ready {
  filter: grayscale(0%);
  opacity: 1;
  transform: translate(-50%, -50%) scale(1.15);
}

.screen-title {
  font-size: clamp(26px, 5vw, 36px);
  margin: 8px 0 20px;
}

/* Buttons */
.primary-btn {
  font-size: clamp(20px, 3.5vw, 26px);
  font-weight: bold;
  padding: 18px 48px;
  margin-top: 28px;
  border: none;
  border-radius: 20px;
  background: #ff9f43;
  color: white;
  box-shadow: 0 6px 0 #d97e1a;
  cursor: pointer;
  transition: transform 0.05s ease;
}

.primary-btn:active {
  transform: translateY(4px);
  box-shadow: 0 2px 0 #d97e1a;
}

.primary-btn:disabled {
  background: #cfcfcf;
  box-shadow: 0 6px 0 #a8a8a8;
  cursor: not-allowed;
}

/* Secondary action (teacher panel's "進入遊戲" link) — same shape as
   .primary-btn but a calmer color so it doesn't compete with 開始/結束. */
.secondary-btn {
  /* Block (not inline-block) so it always starts on its own line. The
     hidden error <p> between #end-btn and this link collapses to zero
     size (display:none), so with inline-block the two buttons could end
     up side by side in the same inline flow instead of stacked. width:
     max-content + auto margins keeps it sized to its content and centered,
     matching how .primary-btn looks despite the different display type. */
  display: block;
  width: max-content;
  margin: 16px auto 0;
  font-size: clamp(18px, 3vw, 22px);
  font-weight: bold;
  padding: 14px 36px;
  border: none;
  border-radius: 20px;
  background: #4a90d9;
  color: white;
  box-shadow: 0 6px 0 #2f6cb0;
  cursor: pointer;
  text-decoration: none;
  transition: transform 0.05s ease;
}

/* 結束/進入遊戲 on the teacher panel's admin-active screen otherwise end up
   visibly different sizes — .primary-btn/.secondary-btn have different
   font-size and padding globally (used elsewhere with different text
   lengths), so pin both to the same fixed size here without touching those
   shared classes for their other uses. */
#admin-active .primary-btn,
#admin-active .secondary-btn {
  display: block;
  width: 220px;
  margin: 16px auto 0;
  font-size: 22px;
  padding: 16px 0;
  text-align: center;
}

#admin-active .primary-btn {
  margin-top: 28px;
}

.secondary-btn:active {
  transform: translateY(4px);
  box-shadow: 0 2px 0 #2f6cb0;
}

.hidden {
  display: none !important;
}

/* Always-present small copy button (see index.html's comment above
   #diag-copy-fab-btn) — top-right corner, out of the way of the mole
   board and the header stat blocks, but tappable on every screen
   including mid-game if the game ever gets stuck. */
.diag-copy-fab {
  position: fixed;
  top: 8px;
  right: 8px;
  z-index: 30;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: none;
  background: rgba(0, 0, 0, 0.35);
  color: white;
  font-size: 16px;
  line-height: 1;
  padding: 0;
}

.diag-copy-fab:active {
  background: rgba(0, 0, 0, 0.55);
}

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  z-index: 100;
  animation: overlay-fade-in 0.2s ease;
}

.modal-overlay.hidden {
  display: none;
}

.modal-box {
  background: white;
  border-radius: 20px;
  padding: 28px 24px;
  width: 100%;
  max-width: 360px;
  text-align: center;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}

.modal-actions {
  display: flex;
  justify-content: center;
  gap: 14px;
  flex-wrap: wrap;
}

.modal-actions .primary-btn,
.modal-actions .secondary-btn {
  margin-top: 12px;
  min-width: 140px;
  font-size: clamp(18px, 3vw, 22px);
  padding: 14px 24px;
}

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

.text-input {
  display: block;
  width: 100%;
  max-width: 320px;
  font-size: clamp(18px, 3vw, 22px);
  padding: 14px 18px;
  margin: 10px auto;
  border: none;
  border-radius: 14px;
  box-shadow: inset 0 2px 6px rgba(0,0,0,0.1);
  text-align: center;
}

.error-text {
  color: #ff6b6b;
  font-weight: bold;
}

/* Teacher panel step indicator ("步驟 1／3：...") — tells the teacher where
   they are in the start-a-class flow and what this screen is for, so the
   next action isn't a guess. */
.step-label {
  font-size: clamp(13px, 2.5vw, 15px);
  font-weight: bold;
  color: #4d96ff;
  letter-spacing: 0.02em;
  margin: 0 0 4px;
}

.slider-group {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  max-width: 320px;
  margin: 8px auto 16px;
}

.slider-group input[type="range"] {
  flex: 1;
  accent-color: #4d96ff;
}

.slider-value {
  font-weight: bold;
  font-variant-numeric: tabular-nums;
  min-width: 3.5em;
  text-align: right;
}

/* Section heading above each option group (teacher panel) — 音域選擇 above
   the clef-mode radios, 顯示唱名 above the show-labels checkbox. */
.option-group-label {
  max-width: 480px;
  margin: 16px auto 4px;
  text-align: left;
  font-weight: bold;
  font-size: clamp(14px, 2.5vw, 17px);
  color: #555;
}

/* Clef mode selector (teacher panel). A grid of equal-width cells, not a
   content-width flex-wrap — flex-wrap left a ragged last row (e.g. 6 items
   then 1 alone) since each pill's width depended on its own label length.
   Grid keeps every cell the same size regardless of item count, so it
   reads as an organized table even when the last row is partial. Always on
   (not gated behind an `orientation: landscape` media query) — that query
   turned out not to reliably match on Martin's real iPad. */
.clef-mode-group {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
  gap: 10px;
  max-width: 480px;
  margin: 16px auto;
}

.clef-mode-option {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font-size: clamp(16px, 3vw, 20px);
  background: white;
  border-radius: 12px;
  padding: 10px 16px;
  box-shadow: 0 3px 8px rgba(0,0,0,0.08);
}

.clef-mode-option input {
  width: 20px;
  height: 20px;
}

/* Show-labels toggle (teacher panel) — same card look as the clef-mode
   options above, just a single standalone checkbox instead of a group. */
.show-labels-option {
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: 320px;
  margin: 16px auto;
  text-align: left;
  font-size: clamp(16px, 3vw, 20px);
  background: white;
  border-radius: 12px;
  padding: 10px 16px;
  box-shadow: 0 3px 8px rgba(0,0,0,0.08);
}

.show-labels-option input {
  width: 20px;
  height: 20px;
}

/* Login numpad */
.seat-display {
  font-size: clamp(36px, 8vw, 56px);
  letter-spacing: 8px;
  font-weight: bold;
  background: white;
  border-radius: 16px;
  padding: 12px 32px;
  margin-bottom: 24px;
  min-width: 200px;
  box-shadow: inset 0 2px 6px rgba(0,0,0,0.1);
}

/* #screen-seat-joined: extra breathing room above the "座號" label + a
   somewhat bigger number than the login screen's live-typing display,
   since here it's the main focal point of an otherwise empty screen. */
.seat-joined-label {
  margin-top: 48px;
  font-size: clamp(18px, 3.5vw, 24px);
  color: #555;
}

.seat-joined-number {
  font-size: clamp(44px, 10vw, 68px);
}

.numpad {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
  width: 100%;
  max-width: 340px;
}

.num-btn {
  font-size: clamp(24px, 5vw, 32px);
  font-weight: bold;
  padding: 22px 0;
  border: none;
  border-radius: 16px;
  background: white;
  box-shadow: 0 5px 0 #c9c9c9;
  cursor: pointer;
}

.num-btn:active {
  transform: translateY(4px);
  box-shadow: 0 1px 0 #c9c9c9;
}

.num-btn.wide {
  grid-column: span 2;
  font-size: clamp(16px, 3vw, 20px);
  background: #ffe0e0;
}

/* Teacher panel: 座號 roster (collecting-seats phase) */
.roster-list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  max-width: 480px;
  margin: 16px auto;
}

.roster-tile {
  min-width: 48px;
  padding: 10px 14px;
  font-size: clamp(16px, 3vw, 20px);
  font-weight: bold;
  border: none;
  border-radius: 12px;
  background: white;
  box-shadow: 0 4px 0 #c9c9c9;
  cursor: pointer;
}

.roster-tile:active {
  transform: translateY(3px);
  box-shadow: 0 1px 0 #c9c9c9;
}

.roster-tile:hover {
  background: #ffe0e0;
}

/* Teacher panel: read-only completion view (playing phase) — plain <span>
   tiles, not clickable, so no pointer cursor/hover tint like the
   collecting-phase buttons above. */
#active-roster-list .roster-tile {
  cursor: default;
}

#active-roster-list .roster-tile:hover {
  background: white;
}

.roster-tile.completed {
  background: #eafff0;
  box-shadow: 0 4px 0 #2ecc71;
  color: #1a7a3e;
}

/* Rules */
.rules-list {
  list-style: none;
  padding: 0;
  font-size: clamp(18px, 3.5vw, 24px);
  text-align: left;
  max-width: 480px;
}

.rules-list li {
  background: white;
  border-radius: 14px;
  padding: 16px 20px;
  margin-bottom: 14px;
  box-shadow: 0 3px 8px rgba(0,0,0,0.08);
}

/* Game screen header — 3 stat blocks (個數/總分/時間), matching the friend's
   design doc's top-left badges instead of the ear-training version's single
   big clock + question-number text. */
/* Martin's call: the 3 stat blocks combined must never be wider than the
   staff cue card below them. .quiz-header's max-width is set inline by
   game.js's syncHeaderWidthToCue() (measures .staff-cue's actual rendered
   width — which depends on the staff image's aspect ratio and the
   clamp(90,18vw,144) height rule, not a fixed number — and mirrors it),
   re-run on load/resize. container-type here lets .stat-value/.stat-label
   scale with THAT constrained width (cqw units) instead of the viewport
   (vw), so the text shrinks to fit whatever width the cue ends up being
   rather than overflowing it. */
.quiz-header {
  width: 100%;
  max-width: 100%;
  display: flex;
  justify-content: center;
  gap: 6px;
  margin: 0 auto 8px;
  container-type: inline-size;
}

.stat-block {
  background: white;
  border-radius: 14px;
  padding: 6px 8px;
  box-shadow: 0 3px 0 #d8d8d8;
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 0;
  flex: 1 1 0;
}

/* Fallback (vw-based) for browsers without container-query support, and the
   default before syncHeaderWidthToCue() has run its first measurement —
   overridden by the cqw rules below wherever container queries work. */
.stat-label {
  font-size: clamp(12px, 4vw, 24px);
  color: #888;
}

.stat-value {
  font-size: clamp(22px, 9vw, 53px);
  font-weight: bold;
  font-variant-numeric: tabular-nums;
}

@container (min-width: 0) {
  .stat-label {
    font-size: clamp(9px, 11cqw, 18px);
  }
  .stat-value {
    font-size: clamp(16px, 20cqw, 40px);
  }
}

.hint {
  color: #777;
  font-size: 15px;
  margin-top: 8px;
  max-width: 320px;
}

/* Multi-line explanatory hints read poorly center-aligned (ragged line
   starts) — left-align just these while leaving the short one-line error
   hints (.error-text) centered under their form as before. Widened to match
   .clef-mode-group's max-width (both are centered flex-items in the same
   .screen, per Martin: the narrower default .hint width made this box's
   edges sit inward of the button row above it instead of lining up). */
.hint-left {
  text-align: left;
  max-width: 480px;
}

/* Top-of-screen staff cue: shows the target note's notation for this round.
   Same visual footprint/margin as the ear-training version's .replay-btn
   (which it replaces) so the overall screen layout stays consistent across
   lessons, but styled as a static display card instead of a tappable button
   — there's nothing to press here, it's just always-visible reference art. */
.staff-cue {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 12px 24px;
  margin-bottom: 16px;
  border-radius: 24px;
  background: white;
  box-shadow: 0 6px 0 #d8d8d8;
}

/* 總複習模式換題時的視覺提示 — 取代原本的提示音（Martin's call: 不喜歡用聲音
   提示，改用動畫；先試過縮放彈跳，Martin 覺得像抖動、改成翻卡牌的感覺）。JS
   端（game.js's triggerCuePulse）在換圖的同一刻先移除再重新加上這個 class
   （中間強制 reflow 一次）以觸發動畫重播。學習模式不需要這個，關卡之間本來
   就有 3-2-1 倒數畫面當訊號，不會呼叫這裡。
   perspective(...) 寫在 transform 值裡（而不是設在父層的 perspective
   屬性），這樣可以直接讓這個元素自己 3D 翻轉，不用改動 .staff-cue 外層的堆疊
   結構。 */
.staff-cue.cue-pulse {
  animation: cue-pulse 0.5s ease;
}

@keyframes cue-pulse {
  0% { transform: perspective(600px) rotateY(0deg); box-shadow: 0 6px 0 #d8d8d8, 0 0 0 rgba(74, 144, 226, 0); }
  50% { transform: perspective(600px) rotateY(90deg); box-shadow: 0 6px 0 #d8d8d8, 0 0 24px rgba(74, 144, 226, 0.6); }
  100% { transform: perspective(600px) rotateY(0deg); box-shadow: 0 6px 0 #d8d8d8, 0 0 0 rgba(74, 144, 226, 0); }
}

.staff-cue img {
  height: clamp(90px, 18vw, 144px);
  /* Fixed instead of width:auto (Martin's call) — the staff_only art's
     aspect ratio varies by note/register (1.61–2.04 measured across the
     full set), so an auto width meant .staff-cue's own rendered box (and
     therefore .quiz-header's synced max-width, see syncHeaderWidthToCue in
     game.js) visibly resized every time the shown note/register changed —
     most jarring during 總複習模式's pre-round countdown, where the cue
     used to go from hidden to visible in one jump. Width is now fixed to
     fit the WIDEST image in the set (ratio ~2.04) so every image fits
     inside via object-fit without ever changing the box's own size;
     narrower images just get a bit of horizontal breathing room instead of
     stretching to fill it. */
  width: clamp(184px, 37vw, 294px);
  object-fit: contain;
  display: block;
}

.staff-cue-label {
  font-size: clamp(20px, 4vw, 28px);
  font-weight: bold;
  color: #2b2b2b;
}

/* Mole board */
.mole-board-wrap {
  position: relative;
  width: 100%;
  max-width: 640px;
}

/* One-shot mallet swing spawned at the tap point on every hole click —
   independent of correct/wrong outcome, which is shown separately via the
   hole's own glow/shake. Removed from the DOM once the animation ends. */
.mallet-hit {
  position: absolute;
  /* Matches the mallet's native 100x100 size (same as --hammer-cursor) so
     the swing animation isn't visibly smaller than the resting cursor. The
     margins are NOT just "center the box on the tap point" — the mallet
     head (bottom-center of the head rect, local ~(50,48) in the 100x100
     SVG) needs to land exactly on the tap point at the animation's final
     rotate(10deg) frame (pivoting around transform-origin 32% 92%), so the
     box offset is solved backwards from that rotated head position, not
     from the box's own geometric center or the cursor's hotspot.
     See scratchpad calc: rotate (50,48) by 10deg around (32,92) -> land at
     tap point => margin-left/top = -(that rotated point). */
  width: 100px;
  height: 100px;
  margin-left: -57px;
  margin-top: -52px;
  background-image: var(--mallet-svg);
  background-size: contain;
  background-repeat: no-repeat;
  transform-origin: 32% 92%;
  pointer-events: none;
  z-index: 20;
  animation: mallet-swing 0.3s ease-out forwards;
}

@keyframes mallet-swing {
  0% { transform: rotate(-55deg) scale(1); opacity: 1; }
  55% { transform: rotate(10deg) scale(1.08); opacity: 1; }
  100% { transform: rotate(10deg) scale(1); opacity: 0; }
}

/* Floating +1/-1 spawned at the tap point (see spawnScorePopup) — same
   positioning approach as .mallet-hit (absolute, centered on the tap
   coordinates within .mole-board-wrap) but drifts upward and fades out
   instead of a one-shot swing. */
.score-popup {
  position: absolute;
  transform: translate(-50%, -50%);
  font-size: clamp(32px, 7vw, 46px);
  font-weight: bold;
  pointer-events: none;
  z-index: 21;
  /* White outline (via layered text-shadow, not a real stroke) so the
     colored text stays legible over both the light floor and any nearby
     mole/mallet art, instead of only a soft drop shadow. */
  text-shadow:
    -2px -2px 0 white, 2px -2px 0 white, -2px 2px 0 white, 2px 2px 0 white,
    0 4px 6px rgba(0,0,0,0.35);
  animation: score-popup-float 0.9s ease-out forwards;
}

.score-popup.correct {
  /* Same green as the correct-mole glow (.mole-hole.correct img's
     drop-shadow) for visual consistency. */
  color: #2ecc71;
}

.score-popup.wrong {
  color: #e74c3c;
}

@keyframes score-popup-float {
  0% { transform: translate(-50%, -50%) scale(0.8); opacity: 0; }
  20% { transform: translate(-50%, -50%) scale(1.15); opacity: 1; }
  100% { transform: translate(-50%, -150%) scale(1); opacity: 0; }
}

.mole-board {
  position: relative;
  width: 100%;
  /* 3x2 layout (6 holes, down from the ear-training version's 3x3/9) reads
     better a bit wider/shorter than the original 4:3. */
  aspect-ratio: 3 / 2;
  /* No card background here on purpose — the board sits directly on the
     classroom scene (body.staff-game-active's background-image), it isn't
     a boxed panel anymore. Only the individual hole markers below are
     drawn. */
  cursor: var(--hammer-cursor);
}

.countdown-display {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  font-size: clamp(56px, 14vw, 96px);
  font-weight: bold;
  color: white;
  /* Bigger/darker shadow than a typical text-shadow so the number stays
     legible directly against the classroom floor art now that the board
     no longer has its own dark card behind it (Martin's call: no boxed
     frame here either). */
  text-shadow: 0 0 16px rgba(0,0,0,0.65), 0 4px 12px rgba(0,0,0,0.5);
  pointer-events: none;
  /* Dead-center of this box (50% down) lands almost exactly on the top
     row of holes (y:45%), so the number visually collided with a hole.
     Shifting down to the midpoint between the two rows (~70%) instead. */
  transform: translateY(20%);
}

.countdown-display.visible {
  display: flex;
}

/* The hole marker: a completely separate, always-present layer — fixed
   size, fixed position, identical in every state (idle / revealed / tried /
   sinking). Previously the "hole" was just .mole-hole's own background,
   which meant it inherited whatever size/position .mole-hole.active grew to
   (bigger, to make the revealed mole+staff card more legible) — so the
   visible hole itself appeared to jump in size and position between states.
   Decoupling it here means the mole art (in .mole-hole) can be as big as it
   wants without ever moving or resizing the hole it's supposed to be
   emerging from/sinking into. Rendered once per slot by renderMoleBoard(),
   never touched again by any game-state class toggle. */
.mole-hole-marker {
  position: absolute;
  width: 20%;
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  /* Radial shading (dark pit center fading to a lighter dirt rim) plus a
     bright highlight along the top-front rim and a soft cast shadow below
     stand in for real geometry — reads as a hole dug into the floor rather
     than a flat painted circle, without needing new art. */
  background:
    radial-gradient(circle at 50% 42%, #1c1108 0%, #3d2b1a 55%, #6b4a28 82%, #8a6636 100%);
  box-shadow:
    inset 0 8px 14px rgba(0,0,0,0.55),
    inset 0 -3px 4px rgba(255,214,150,0.25),
    0 3px 6px rgba(0,0,0,0.3);
  pointer-events: none;
}

/* Idle hole: the interactive button itself has no visual of its own now
   (the marker above provides it) — sized to match the marker only so its
   hit-target lines up while idle. */
.mole-hole {
  position: absolute;
  width: 20%;
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%);
  border: none;
  outline: none;
  padding: 0;
  border-radius: 50%;
  background: transparent;
}

/* Active hole: this question's 4 live targets — wider, shows the
   mole+staff card art with no circular clipping so the staff renders
   fully instead of being cropped by the hole shape. Sits on top of (and
   deliberately larger than) the fixed-size marker underneath. */
.mole-hole.active {
  width: 27.5%;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  overflow: visible;
  background: transparent;
  box-shadow: none;
  cursor: var(--hammer-cursor);
}

/* Labeled art's aspect ratio is taller than the default (extra room for the
   baked-in Do/Re/Mi text) — scaled down so its rendered height roughly
   matches the default art at 22%, since holeSlots' vertical spacing assumes
   that height and doesn't know which art variant is active. */
.mole-board.labels-on .mole-hole.active {
  width: 23.1%;
}

/* Re-derives the img's bottom-alignment margin (see .mole-hole.active img
   below) for the narrower labels-on box. Must be recalculated whenever this
   box's width changes, since the margin is a % of the box's own (variable)
   width, not of the board — measured empirically the same way as the
   default value (getBoundingClientRect(), not a closed-form formula; see
   that comment for why). */
.mole-board.labels-on .mole-hole.active img {
  margin-bottom: 21.52%;
}

.mole-hole.active img {
  width: 100%;
  height: auto;
  display: block;
  /* Exactly aligns the img's bottom edge with the fixed-size hole marker's
     bottom edge (both centered on the same point via translate(-50%,-50%)).
     POSITIVE margin-bottom shifts the img up here — with align-items:
     flex-end, the margin box's edge aligns to the container's bottom, so a
     positive margin pulls the visible border-box up away from that edge.
     Not a clean closed-form % (which would be (22-16)/(2*22) ≈ 13.64% if
     this box were truly square): .mole-hole.active is a flex container
     whose own height doesn't actually honor aspect-ratio:1/1 the way the
     plain (non-flex) idle .mole-hole/.mole-hole-marker do — its real height
     comes out taller, and margin-bottom on the img itself is IN that height
     calculation too, so bumping the margin also grows the container height,
     which is why this needed 3 rounds of measure-and-adjust with
     getBoundingClientRect() to converge on the value below (img.bottom ==
     marker.bottom to within 1px) instead of a single formula. */
  margin-bottom: 18.72%;
  /* 2026-09-09: reverted from box-shadow back to filter: drop-shadow() —
     box-shadow doesn't follow the image's alpha shape, it shadows the whole
     rectangular <img> box. This mole art's corners are actually fully
     transparent (confirmed via PIL: corner pixels are alpha=0, the art is a
     rounded card shape, not a filled rectangle), so box-shadow was casting
     a visible rectangular "box" outline around every mole — a real visual
     regression, not the "reads the same in practice" this was assumed to be
     when box-shadow was first introduced.
     Safe to revert now that the actual root cause of the intermittent
     partial-render bug this was originally defending against has been fixed
     properly: runReviewRound() (waveEngine.js) no longer force-clears +
     synchronously batch-spawns the whole board on every target change — see
     that file's targetLoop() — so multiple filtered/animated moles no longer
     reliably animate in lockstep the way they did before. drop-shadow is
     more expensive to composite than box-shadow, but that cost is no longer
     compounded by guaranteed simultaneous triggers. */
  filter: drop-shadow(0 6px 8px rgba(0,0,0,0.3));
  transition: transform 0.15s ease;
  /* Promotes each hole's image to its own compositing layer so the
     mole-pop/mole-shake animation (transform + this filter) on the tapped
     hole repaints in isolation instead of visually jittering neighboring,
     untouched hole images — a known WebKit/Safari compositing artifact when
     several filtered/animated siblings share an unpromoted paint region. */
  will-change: transform;
  /* Declared explicitly (not just inherited from .mole-hole.active) because
     the drop-shadow filter above creates its own compositing layer, which
     can make a custom-image cursor silently disappear over the img even
     though it works fine over the rest of the hole's hit area. */
  cursor: var(--hammer-cursor);
}

.mole-hole.correct img {
  filter: drop-shadow(0 0 10px #2ecc71) drop-shadow(0 6px 8px rgba(0,0,0,0.3));
  animation: mole-pop 0.4s ease;
}

@keyframes mole-pop {
  0% { transform: scale(0.7); }
  60% { transform: scale(1.15); }
  100% { transform: scale(1); }
}

.mole-hole.wrong img {
  animation: mole-shake 0.4s ease;
}

@keyframes mole-shake {
  0%, 100% { transform: rotate(0deg); }
  25% { transform: rotate(-6deg); }
  75% { transform: rotate(6deg); }
}


/* Dims the mole once it's been resolved and is waiting to sink — except a
   correct hit, which should stay fully visible/bright through its hold
   (see CORRECT_HOLD_MS in game.js) instead of looking washed out right as
   it's supposed to be celebrating. */
.mole-hole.tried:not(.correct) img {
  opacity: 0.5;
}

.mole-hole:disabled,
.mole-hole.disabled {
  cursor: not-allowed;
}

/* Results */
.score-summary {
  font-size: clamp(18px, 3.5vw, 24px);
  background: white;
  border-radius: 16px;
  padding: 20px 28px;
  box-shadow: 0 4px 10px rgba(0,0,0,0.08);
  margin-bottom: 20px;
}

.result-count {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-weight: bold;
  font-size: clamp(28px, 5vw, 36px);
}

/* ✓/✗ render with different natural glyph heights/baselines (✗ sits
   noticeably higher and smaller than ✓ by default), which threw off
   left-right alignment between the two result lines. Pinning both to the
   same fixed box (flex-centered within, same width) keeps "打對"/"打錯"
   lined up regardless of the glyphs' own metrics. */
.result-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1em;
  font-weight: bold;
}

.result-icon.correct {
  color: #2ecc71;
}

.result-icon.wrong {
  color: #e74c3c;
}

.result-comment {
  font-size: clamp(20px, 4vw, 28px);
  font-weight: bold;
  color: #ff9f43;
  margin: 8px 0 20px;
}

/* Landscape compaction: an iPad held sideways has much less vertical room
   than portrait, so several screens (seat keypad, teacher class-setup form)
   overflow and need a scroll without this. Tightens vertical spacing only —
   scoped to short-height landscape so tall landscape (e.g. iPad Pro 12.9")
   is untouched since it already fits without help. */
@media (orientation: landscape) and (max-height: 900px) {
  .title {
    margin: 4px 0 2px;
  }

  .subtitle {
    margin-bottom: 8px;
  }

  .seat-display {
    margin-bottom: 10px;
    padding: 8px 24px;
  }

  .numpad {
    gap: 8px;
  }

  .num-btn {
    padding: 12px 0;
  }

  .primary-btn {
    margin-top: 12px;
    padding: 12px 36px;
  }

  /* Teacher panel: class-setup form (#admin-class) */
  .text-input {
    margin: 6px auto;
  }

  .option-group-label {
    margin: 8px auto 3px;
  }

  .clef-mode-group {
    gap: 6px;
    margin: 8px auto;
  }

  .clef-mode-option {
    padding: 6px 14px;
  }

  .hint {
    margin-top: 4px;
  }

  /* whack-a-mole-staff's teacher panel has one more option group (唱名選擇)
     than the piano version this media query was originally tuned for, plus
     the hang-time slider — needs more aggressive compaction than the base
     landscape rules above to fit real iPad Safari, whose usable viewport
     is meaningfully shorter than the device's nominal resolution once the
     address/tab bar chrome is accounted for (measured overflow against a
     conservative 650px-tall worst case, not just the nominal ~768-834px
     landscape heights). */
  #teacher-panel {
    max-width: 640px;
  }

  #teacher-panel .title {
    font-size: 24px;
    margin: 2px 0 0;
  }

  #teacher-panel .step-label {
    font-size: 13px;
  }

  #teacher-panel .subtitle {
    font-size: 15px;
    margin-bottom: 4px;
  }

  /* Smaller minmax than the base 90px so all 7 唱名 checkboxes reliably fit
     in a single row instead of wrapping a lone 7th item to its own row. */
  #teacher-panel .clef-mode-group {
    max-width: 640px;
    grid-template-columns: repeat(auto-fit, minmax(66px, 1fr));
    gap: 6px;
    margin: 4px auto;
  }

  #teacher-panel .hint-left {
    max-width: 640px;
  }

  #teacher-panel .clef-mode-option {
    padding: 5px 10px;
    font-size: 15px;
  }

  #teacher-panel .option-group-label {
    max-width: 640px;
    margin: 6px auto 2px;
    font-size: 13px;
  }

  #teacher-panel .text-input {
    max-width: 400px;
    margin: 4px auto;
    padding: 8px 14px;
  }

  #teacher-panel .slider-group {
    max-width: 400px;
    margin: 2px auto 6px;
  }

  #teacher-panel #start-btn,
  #teacher-panel #open-seat-input-btn {
    margin-top: 6px;
    padding: 10px 28px;
  }

  #teacher-panel .step-label {
    margin-bottom: 2px;
  }
}
