* {
  box-sizing: border-box;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}

:root {
  --bg-top: #eef1fb;
  --bg-bottom: #dfe4f7;
  --surface: #ffffff;
  --surface-sunken: #eef0f8;
  --ink: #2f3142;
  --ink-soft: #8c90a6;
  --accent: #6c5ce7;
  --accent-soft: #efeaff;
  --accent-press: #5b4bd1;
  --line: #e6e8f2;
  --tile-face-top: #ffffff;
  --tile-face-bottom: #f2f3fa;
  --tile-edge: #c4c8e2;
  --tile-edge-deep: #a6abcd;
}

html, body {
  height: 100%;
  margin: 0;
  background: linear-gradient(170deg, var(--bg-top), var(--bg-bottom));
  font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
  overflow: hidden;
}

#app {
  max-width: 560px;
  margin: 0 auto;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 8px 6px;
}

.game-title {
  /* Trimmed down from the original 19px/2-12px margins — the title, topbar
     and progress bar together were eating enough height on a phone that
     the board (especially level 2's taller one) ran past the bottom of
     the screen. This row still reads fine smaller; the board is what a
     player actually needs the room for. */
  margin: 0 0 6px;
  font-size: 16px;
  font-weight: 800;
  color: var(--ink);
  letter-spacing: 0.5px;
}

.topbar {
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: var(--ink-soft);
  font-size: 13px;
  font-weight: 500;
  padding: 3px 6px;
  margin-bottom: 6px;
}

.topbar-left {
  display: flex;
  align-items: center;
  gap: 8px;
}

.restart-btn {
  background: var(--surface-sunken);
  color: var(--ink-soft);
  border: none;
  padding: 6px 12px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}

.restart-btn:active {
  background: var(--line);
}

.level-badge {
  background: var(--accent);
  color: #fff;
  padding: 6px 14px;
  border-radius: 999px;
  font-weight: 700;
  font-size: 13px;
}

.mute-btn {
  background: var(--surface-sunken);
  border: none;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  font-size: 13px;
  line-height: 1;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.mute-btn:active {
  background: var(--line);
}

.best {
  opacity: 0.9;
}

/* Covers the whole page until preloadAllAssetsThenStart (game.js) has
   actually fetched every icon/popup image and the BGM track — without
   this, level 2's 210 tiles could start rendering before their icon
   images (assets/icons/*.jpg) had loaded, showing blank tiles that only
   filled in partway through play. Sits above everything (z-index higher
   than spike-overlay's 2000) and is removed from the DOM entirely once
   loading finishes, not just hidden, so it can never block clicks by
   accident. */
.loading-overlay {
  position: fixed;
  inset: 0;
  z-index: 3000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(170deg, var(--bg-top), var(--bg-bottom));
}

.loading-card {
  width: 220px;
  text-align: center;
}

.loading-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--ink-soft);
  margin-bottom: 14px;
}

.loading-bar {
  height: 10px;
  border-radius: 999px;
  background: var(--surface-sunken);
  overflow: hidden;
}

.loading-fill {
  height: 100%;
  width: 0%;
  background: var(--accent);
  border-radius: 999px;
  transition: width 0.15s ease;
}

.loading-percent {
  display: block;
  margin-top: 8px;
  font-size: 12px;
  font-weight: 700;
  color: var(--accent);
}

.progress-wrap {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 6px;
  margin-bottom: 6px;
}

.progress-bar {
  flex: 1;
  height: 8px;
  border-radius: 999px;
  background: var(--surface-sunken);
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  width: 0%;
  background: var(--accent);
  border-radius: 999px;
  transition: width 0.2s ease;
}

.progress-text {
  font-size: 12px;
  font-weight: 700;
  color: var(--accent);
  min-width: 32px;
  text-align: right;
}

.board-wrap {
  flex: 1;
  width: 100%;
  display: flex;
  /* Level 2's board is wider/taller than the viewport at full tile size
     (see UNIT in game.js) — scroll to it instead of centering with
     justify-content, which would clip the unreachable edges under
     overflow: auto in some browsers. #board's margin: auto below still
     centers it whenever it's small enough to fit (level 1). */
  overflow: auto;
  min-height: 0;
}

#board {
  position: relative;
  margin: auto;
  background: var(--surface);
  border-radius: 22px;
  box-shadow:
    0 1px 2px rgba(47, 49, 66, 0.04),
    0 16px 32px rgba(47, 49, 66, 0.1);
}

/* The fixed slots removeThreeBtn puts tiles into (see RETURN_SLOT_GX /
   renderReturnSlots in game.js) — always drawn, whether or not a tile
   currently sits there, so the destination is visible before you ever
   press the button. z-index: -1 keeps this under every real tile so a
   filled (or stacked) slot just reads as "tiles sitting on their
   marker". */
.return-slot {
  position: absolute;
  z-index: -1;
  border: 2px dashed var(--tile-edge);
  border-radius: 12px;
}

.tile {
  position: absolute;
  border-radius: 12px;
  background: linear-gradient(180deg, var(--tile-face-top), var(--tile-face-bottom));
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Every shadow here is `inset` — drawn inside the tile's own box, never
     past its edges — on purpose: an earlier version used protruding
     shadows (a solid strip below the square plus a soft drop shadow) to
     give the card a "thickness" look, but that painted extent doesn't
     count toward the tile's actual size or the covering check, so two
     tiles placed just far enough apart for their *squares* to clear could
     still have that painted edge visibly touching — looking stacked even
     though neither was actually covering the other. Keeping it inset
     means every tile's painted extent is exactly its square, always, so
     what a tile looks like and what it does can never disagree — and
     every tile (main pile or border) now looks the same way, instead of
     needing a different shadow depending on where it came from.
     The first layer is the covered-state's dark scrim — see .tile.covered
     — declared here too, fully transparent, so both states have the same
     shadow list (count, order, inset-ness) and only its alpha needs to
     animate on the box-shadow transition below. A browser can only
     interpolate box-shadow smoothly when the two lists line up like that;
     otherwise it just snaps straight to the end state, which is what
     made a tile becoming uncovered look like it flashed instead of
     fading in. Never use opacity on the element itself here: with most
     of the level-2 pile covered at once, a see-through tile makes the
     whole board look washed out — the scrim layer dims without that. */
  box-shadow:
    inset 0 0 0 999px rgba(18, 20, 32, 0),
    inset 0 1px 0 rgba(255, 255, 255, 0.9),
    inset 0 -3px 0 var(--tile-edge),
    inset 0 -6px 6px rgba(47, 49, 66, 0.2);
  cursor: pointer;
  transition: transform 0.1s ease, box-shadow 0.15s ease;
}

.tile.covered {
  /* Same 4 shadows, same order, as .tile above — only the scrim's alpha
     (0 → 0.6) and the highlight/edge's alpha/color actually change, so
     the transition is a plain fade instead of a jump. */
  cursor: not-allowed;
  box-shadow:
    inset 0 0 0 999px rgba(18, 20, 32, 0.6),
    inset 0 1px 0 rgba(255, 255, 255, 0.25),
    inset 0 -3px 0 var(--tile-edge),
    inset 0 -6px 6px rgba(47, 49, 66, 0.2);
}

.tile.covered:active {
  transform: none;
}

.tile:not(.covered):active {
  transform: translateY(4px);
  box-shadow:
    inset 0 0 0 999px rgba(18, 20, 32, 0),
    inset 0 1px 0 rgba(255, 255, 255, 0.9),
    inset 0 -1px 0 var(--tile-edge-deep),
    inset 0 -6px 6px rgba(47, 49, 66, 0.2);
}

.tile.shake, .tool-btn.shake {
  animation: shake 0.28s ease;
}

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-4px); }
  75% { transform: translateX(4px); }
}

.tools {
  display: flex;
  gap: 8px;
  margin: 8px 0;
}

.tool-btn {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 9px 12px;
  font-size: 13px;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(47, 49, 66, 0.08);
  white-space: nowrap;
}

.tool-btn:active:not(:disabled) {
  transform: translateY(1px);
  box-shadow: 0 1px 2px rgba(47, 49, 66, 0.08);
}

.tool-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.tool-count {
  background: var(--accent-soft);
  color: var(--accent);
  border-radius: 999px;
  min-width: 18px;
  height: 18px;
  font-size: 11px;
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 4px;
}

.tray-wrap {
  width: 100%;
  display: flex;
  justify-content: center;
}

#tray {
  display: flex;
  gap: 8px;
  background: var(--surface);
  padding: 10px;
  border-radius: 18px;
  box-shadow:
    0 1px 2px rgba(47, 49, 66, 0.04),
    0 10px 22px rgba(47, 49, 66, 0.1);
}

.tray-slot {
  width: 52px;
  height: 52px;
  border-radius: 12px;
  background: var(--surface-sunken);
  background-size: cover;
  background-position: center;
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 26px;
}

.tray-slot.filled {
  background: linear-gradient(180deg, var(--tile-face-top), var(--tile-face-bottom));
  border: 1px solid var(--line);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.9),
    0 3px 0 var(--tile-edge-deep);
}

.overlay {
  position: fixed;
  inset: 0;
  background: rgba(47, 49, 66, 0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.overlay.hidden {
  display: none;
}

.overlay-card {
  background: var(--surface);
  border-radius: 20px;
  padding: 30px 32px;
  text-align: center;
  max-width: 320px;
  box-shadow: 0 20px 44px rgba(47, 49, 66, 0.25);
}

/* Placeholder art slot for a popup — win/lose/finalWin and the three
   tool-confirm popups each get one, set via config.js's `image` (or
   `images`, for finalWin's random pick) field on that TEXTS entry. Swap
   the referenced file in assets/popups/ to replace the placeholder;
   leave the field out and the slot just stays hidden (see showOverlay /
   showConfirm in game.js), which is how the restart-confirm popup skips
   it entirely. */
.overlay-image {
  width: 132px;
  height: 132px;
  margin: 0 auto 14px;
  border-radius: 18px;
  background-size: cover;
  background-position: center;
  background-color: var(--surface-sunken);
}

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

.overlay-card h2 {
  margin: 0 0 8px;
  font-size: 21px;
  color: var(--ink);
}

.overlay-card p {
  margin: 0 0 20px;
  color: var(--ink-soft);
  font-size: 14px;
}

.overlay-card button {
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 14px;
  padding: 11px 26px;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 8px 20px rgba(108, 92, 231, 0.35);
}

.overlay-card button:active {
  transform: translateY(1px);
  background: var(--accent-press);
}

.confirm-actions {
  display: flex;
  gap: 10px;
  justify-content: center;
}

.confirm-actions button {
  flex: 1;
  padding-left: 10px;
  padding-right: 10px;
  font-size: 14px;
  white-space: nowrap;
}

.confirm-cancel {
  background: var(--surface-sunken) !important;
  color: var(--ink) !important;
  box-shadow: none !important;
}

.confirm-cancel:active {
  background: var(--line) !important;
}

.spike-overlay {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  background: radial-gradient(circle, #ff6b5e, #d92e3b);
  pointer-events: none;
}

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

/* Optional art above the text (see TEXTS.difficultySpike.image in
   config.js) — same placeholder-box treatment as .overlay-image, just
   sized for a brief full-screen flash instead of a card. Hidden via the
   same .hidden class/pattern when no image is configured. */
.spike-image {
  width: 160px;
  height: 160px;
  border-radius: 20px;
  background-size: cover;
  background-position: center;
  background-color: rgba(255, 255, 255, 0.15);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
  opacity: 0;
}

.spike-image.hidden {
  display: none;
}

.spike-text {
  font-size: 40px;
  font-weight: 900;
  color: #fff;
  letter-spacing: 1px;
  text-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
  opacity: 0;
}

.spike-text.spike-play,
.spike-image.spike-play {
  animation: spikePulse 1.4s ease-in-out forwards;
}

@keyframes spikePulse {
  0% { transform: scale(0.3) rotate(-8deg); opacity: 0; }
  18% { transform: scale(1.3) rotate(4deg); opacity: 1; }
  32% { transform: scale(0.95) rotate(-2deg); }
  46% { transform: scale(1.08) rotate(1deg); }
  60% { transform: scale(1) rotate(0deg); opacity: 1; }
  85% { transform: scale(1) rotate(0deg); opacity: 1; }
  100% { transform: scale(1.15) rotate(0deg); opacity: 0; }
}
