* {
  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 would confine it to a small boxed
   card instead of covering the full viewport. */
body.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. */
  background-position: center bottom;
  background-attachment: fixed;
}

@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;
}

/* 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. Mirrors whack-a-mole-staff's teacher panel. */
.step-label {
  font-size: clamp(13px, 2.5vw, 15px);
  font-weight: bold;
  color: #4d96ff;
  letter-spacing: 0.02em;
  margin: 0 0 4px;
}

/* 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;
}

.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;
}

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

/* Clef mode selector (teacher panel) */
.clef-mode-group {
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 320px;
  margin: 16px auto;
  text-align: left;
}

.clef-mode-option {
  display: flex;
  align-items: 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 */
.quiz-header {
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: clamp(16px, 3vw, 22px);
  font-weight: bold;
  margin-bottom: 8px;
}

.timer {
  background: #ff6b6b;
  color: white;
  padding: 8px 18px;
  border-radius: 12px;
  font-variant-numeric: tabular-nums;
}

.question-counter {
  font-size: clamp(16px, 3vw, 20px);
  color: #666;
  margin: 4px 0 16px;
}

.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. */
.hint-left {
  text-align: left;
}

/* Same visual weight as Peter and the Wolf's .play-btn (font-size, padding,
   border-radius, box-shadow depth, disabled colors) — the two buttons play
   the same conceptual role (聽題目音檔), so they should look the same size
   across lessons even though this one also has extra 播放中/再聽一次 state
   handling that .play-btn doesn't need. */
.replay-btn {
  font-size: clamp(20px, 4vw, 28px);
  font-weight: bold;
  padding: 24px 40px;
  margin-bottom: 16px;
  border: none;
  border-radius: 50px;
  background: #4d96ff;
  color: white;
  box-shadow: 0 6px 0 #2f6fd1;
  cursor: pointer;
}

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

/* Without this, a disabled button (播放中, or before the first playback)
   looked pixel-identical to the enabled state — same color, same
   cursor:pointer set above overriding the browser's own disabled cursor —
   so it still looked tappable even though clicks were ignored. iOS Safari
   also flashes its own gray tap-highlight overlay on tap regardless of the
   disabled attribute, which read as "the button did something" even though
   the click was actually a no-op — -webkit-tap-highlight-color suppresses
   that specifically for the disabled state. */
.replay-btn:disabled {
  background: #b8c9de;
  box-shadow: 0 6px 0 #8fa0b5;
  -webkit-tap-highlight-color: transparent;
  cursor: not-allowed;
}

.replay-btn:disabled:active {
  transform: none;
  box-shadow: 0 6px 0 #8fa0b5;
}

/* 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; }
}

.mole-board {
  position: relative;
  width: 100%;
  /* 3x2 layout (6 holes, down from the original 3x3/9) to match the
     five-線譜 version's board and its classroom-scene background art
     (Martin's call: reuse the same layout/visual design). */
  aspect-ratio: 3 / 2;
  /* No card background here on purpose — the board sits directly on the
     classroom scene (body.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. */
  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) would land almost exactly on the
     top row of holes (y:45%), so shift 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, and this mole art's corners are actually fully
     transparent (confirmed via PIL on the sibling whack-a-mole-staff
     lesson's identical art style), so box-shadow was casting a visible
     rectangular "box" outline around every mole — a real, confirmed visual
     regression, not the "reads the same in practice" this was assumed to be
     when box-shadow was first introduced.
     This lesson still reveals up to 4 holes at once per round (see
     revealHoles() in game.js) — unlike the sibling whack-a-mole-staff
     lesson, that batch-reveal itself wasn't restructured away, only given a
     40ms-per-hole animation-delay stagger (see revealHoles()) — so this
     revert carries a bit more residual risk of reintroducing the original
     intermittent partial-render bug than the staff lesson's revert did. The
     stagger is kept as the remaining mitigation; the box artifact is a
     confirmed, always-visible problem vs. the original bug being rare, so
     this is judged the better trade-off. Revisit if the partial-render bug
     reappears here specifically. */
  filter: drop-shadow(0 6px 8px rgba(0,0,0,0.3));
  transition: transform 0.15s ease;
  will-change: transform;
  cursor: var(--hammer-cursor);
  /* Runs once automatically since revealHoles() creates a fresh <img> each
     round — rises up out of the hole with a little overshoot bounce
     instead of just instantly appearing. Deliberately does NOT touch
     opacity (unlike an earlier attempt at this) — if the animation ever
     fails to progress for any reason, the image stays fully visible just
     mis-transformed, rather than risking it getting stuck invisible. */
  animation: mole-emerge 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

@keyframes mole-emerge {
  0% { transform: translateY(70%) scale(0.7); }
  100% { transform: translateY(0) scale(1); }
}

.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); }
}

/* Plays after the correct/wrong feedback (mole-pop/mole-shake) has already
   shown, right before the round advances — sinks the mole back down into
   the hole. Mirrors mole-emerge exactly (same duration/easing/transform
   magnitude, reversed) so the appear and disappear motions match. Selector
   is deliberately MORE specific (three classes) than .mole-hole.correct/
   .wrong img (two classes) so it reliably overrides whichever feedback
   animation was playing, regardless of stylesheet order. Opacity only
   reaches 0 at the very end via forwards-fill, not at the start — if this
   animation ever fails to progress, the mole just stays visible a moment
   longer instead of getting stuck invisible. */
.mole-hole.active.sinking img {
  animation: mole-sink 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}

@keyframes mole-sink {
  0% { transform: translateY(0) scale(1); opacity: 1; }
  100% { transform: translateY(70%) scale(0.7); opacity: 0; }
}

.mole-hole.tried 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 {
  font-weight: bold;
  font-size: clamp(28px, 5vw, 36px);
}

.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;
  }

  .step-label {
    font-size: 13px;
    margin-bottom: 2px;
  }

  .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;
  }
}
