/* Phaser-build overrides.
 *
 * style.css is shared verbatim with the DOM build; this file only adapts the
 * board slot (now a Phaser canvas) and removes the DOM-only leftovers.
 */

/* The playfield container. style.css shrink-wraps #board-wrap around the DOM
   board's inline pixel size; with a canvas there is no such content, so the
   panel needs its own responsive square and Phaser fits the board inside. */
#board {
  display: none !important;
}

#board-wrap {
  position: relative;
  /* App.syncPanelAspect() writes exact px from the scene's metrics (which are
     derived from the viewport, never from the canvas, so there is no feedback
     loop). These are only the pre-boot fallback. */
  width: min(70vmin, 720px);
  aspect-ratio: 500 / 440;
  height: auto;
  padding: 0 !important;
  overflow: hidden;
}

#game {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  overflow: hidden;
}

#game canvas {
  display: block;
  border-radius: inherit;
}

/* style.css switches to a single-column phone layout at 720px. This file is
   linked AFTER it, so an unguarded #board-wrap rule here would silently win
   over that media query and break the phone layout — mirror the breakpoint. */
@media (max-width: 720px) {
  #board-wrap {
    width: 100%;
    max-width: 100%;
    justify-self: stretch;
    padding: 0 !important;
  }
}

/* The agent ghost cursor and replay download belong to the DOM build. */
#ghost-cursor,
#replay-link {
  display: none !important;
}

/* Tray tile art is an <img> in this build (not an emoji glyph). */
.tray-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  border-radius: 8px;
  pointer-events: none;
}

/* Agent race rows: .agent-row / .agent-stat already come from style.css;
   only the collapsed-ladder spacing needs a nudge here. */
.agents-wrap:not(.collapsed) {
  margin-top: 4px;
}

/* Level rail: a THREE-rung window (hud.renderProgressRail), never the whole
   50-rung ladder. Left-aligned like the title under it — centring the row put
   two dots in the middle of an empty band and read as a floating artefact.
   Scrolling is off: with three dots there is nothing to scroll, and the
   reference sheet's overflow-x let the row drift under the header. */
#progress-rail {
  overflow: visible;
  gap: 6px;
  padding: 1px 0;
}
.rail-dot {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.7);
  font-weight: 800;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  background: rgba(0, 0, 0, 0.16);
  color: #fff;
  transition: background 0.25s ease, box-shadow 0.25s ease, transform 0.18s ease;
}
.rail-dot.current {
  background: #ff9a2e;
  box-shadow: 0 0 0 4px rgba(255, 154, 46, 0.2);
}
/* "上一关变绿": the cleared rung is a FILLED green ball with a white ✓ — the
   small reward the owner asked for. The old sheet put the green fill on
   `.cleared` while hud.js emitted `.done`, so this never rendered. */
.rail-dot.cleared {
  background: #58b368;
  border-color: #46a057;
  color: #fff;
  box-shadow: 0 0 0 3px rgba(88, 179, 104, 0.22);
}
.rail-dot.ready:hover:not(:disabled) {
  transform: translateY(-1px);
  background: rgba(0, 0, 0, 0.28);
}
.rail-dot.locked {
  opacity: 0.45;
  cursor: default;
}
/* One-shot pop on the rung that was JUST cleared. Driven by a data attribute
   set from app.refresh() so re-rendering the rail cannot replay it endlessly. */
#progress-rail[data-just-cleared] .rail-dot.cleared {
  animation: railPop 1s ease-out;
}
@keyframes railPop {
  0% { transform: scale(0.6); box-shadow: 0 0 0 0 rgba(88, 179, 104, 0.75); }
  45% { transform: scale(1.22); }
  100% { transform: scale(1); box-shadow: 0 0 0 3px rgba(88, 179, 104, 0.22); }
}

/* Advisory line under the leaderboard. */
#advisor-say {
  font-size: 0.9em;
  line-height: 1.35;
}

/* Toast error variant. */
#toast.error {
  background: #e95a4a;
  color: #fff;
}

/* Loading screen: covers the page until the playable board exists.
   The background is Mochi's key art (qwen-image-3.0), cover-cropped on every
   aspect — the subject sits dead-center so a portrait phone crop keeps her. */
#loader {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: grid;
  place-items: center;
  background:
    linear-gradient(180deg, rgba(255, 233, 184, 0.10) 0%, rgba(255, 214, 137, 0.42) 100%),
    url("assets/ui/loader_bg.webp") center 38% / cover no-repeat,
    linear-gradient(165deg, #fff7e3, #ffe0b0);
  transition: opacity 0.3s ease;
}
#loader.loader-done {
  opacity: 0;
  pointer-events: none;
}
.loader-card {
  width: min(78vw, 340px);
  text-align: center;
  padding: 22px 24px 18px;
  border-radius: 18px;
  border: 2px solid var(--border-strong);
  background: rgba(255, 253, 244, 0.82);
  box-shadow: 0 10px 24px rgba(200, 130, 30, 0.22);
  backdrop-filter: blur(3px);
  /* drop below Mochi's face so the sleeping cat reads above the card */
  transform: translateY(9vh);
}
/* Portrait (phone) cover-crop puts Mochi's face near 60% height — drop the
   card further so her eyes stay visible above it. */
@media (max-aspect-ratio: 1/1) {
  .loader-card {
    transform: translateY(20vh);
  }
}
.loader-card h1 {
  margin: 0 0 6px;
  font-size: 30px;
  color: #c8641b;
  letter-spacing: 0.5px;
}
.loader-card p {
  margin: 0 0 14px;
  font-size: 14px;
  color: #8a6a35;
}
.loader-track {
  height: 12px;
  border-radius: 999px;
  background: rgba(200, 130, 30, 0.2);
  overflow: hidden;
}
#loader-bar {
  height: 100%;
  width: 0%;
  border-radius: 999px;
  background: linear-gradient(90deg, #ffc46b, #ff9a2e);
  transition: width 0.18s ease;
}
#loader-pct {
  display: block;
  margin-top: 10px;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: #8a6a35;
}
#loader.loader-error #loader-status {
  color: #c0392b;
  font-weight: 600;
}

/* ---- Opening montage (main.js playIntroGate) ------------------------------ *
 * Three stills cross-faded with a slow Ken Burns push (owner spec: "开场还是
 * 要播放3张加载动画…留下skip选项点击后直接开玩"). Auto-plays — no tap to start:
 * the slides are muted images, so autoplay policy allows them everywhere. Any
 * tap or key skips, and that same gesture unlocks WebAudio in boot(), so a
 * skipper's first deal is audible. The stills already ship as cat-room video
 * posters, so the montage costs zero added bytes.
 */
#boot-intro {
  position: fixed;
  inset: 0;
  z-index: 9100;
  overflow: hidden;
  background: linear-gradient(165deg, #fff7e3, #ffe0b0);
  cursor: pointer;
}
#boot-intro .boot-slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  background: center 40% / cover no-repeat;
  transition: opacity 640ms ease;
}
#boot-intro .boot-slide.on {
  opacity: 1;
  animation: boot-kenburns 2.4s ease-out both;
}
@keyframes boot-kenburns {
  from { transform: scale(1.1); }
  to { transform: scale(1); }
}
/* A soft warm veil keeps the title readable over any slide. */
#boot-intro::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(120, 62, 8, 0.10), rgba(90, 45, 0, 0.38) 88%);
  pointer-events: none;
}
#boot-intro .boot-intro-title {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 12%;
  margin: 0;
  text-align: center;
  font-size: clamp(34px, 7vw, 58px);
  font-weight: 900;
  letter-spacing: 0.5px;
  color: #fff;
  text-shadow: 0 3px 0 #c8641b, 0 10px 26px rgba(90, 45, 0, 0.55);
  animation: zone-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
  z-index: 2;
}
#boot-intro .boot-dots {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 6.5%;
  display: flex;
  justify-content: center;
  gap: 9px;
  z-index: 2;
}
#boot-intro .boot-dots i {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.55);
  box-shadow: 0 1px 2px rgba(90, 45, 0, 0.35);
  transition: background 240ms ease, transform 240ms ease;
}
#boot-intro .boot-dots i.on {
  background: #ffd257;
  transform: scale(1.25);
}
#boot-intro .boot-skip {
  position: absolute;
  top: 14px;
  right: 14px;
  z-index: 3;
  padding: 8px 15px;
  border: 2px solid rgba(255, 246, 220, 0.9);
  border-radius: 999px;
  background: rgba(74, 42, 8, 0.42);
  color: #fff6df;
  font-weight: 800;
  font-size: 14px;
  letter-spacing: 0.3px;
  cursor: pointer;
  backdrop-filter: blur(2px);
  transition: background 160ms ease, transform 160ms ease;
}
#boot-intro .boot-skip:hover { background: rgba(74, 42, 8, 0.62); transform: translateY(-1px); }
#boot-intro.bye { opacity: 0; transition: opacity 280ms ease; }
@media (prefers-reduced-motion: reduce) {
  #boot-intro .boot-slide { transition: none; }
  #boot-intro .boot-slide.on { animation: none; }
  #boot-intro .boot-intro-title { animation: none; }
}

/* Keep the tray on ONE row at every width. style.css sizes slots from
   --tray-slot and lets them wrap, which splits 7 (meadow) into 5+2 inside the
   right-hand column. Equal flex slots read as a single tray instead. */
#tray-slots {
  flex-wrap: nowrap;
  align-items: center;
}
#tray-slots .slot {
  flex: 1 1 0;
  min-width: 0;
  width: auto;
  height: auto;
  /* Square slots: without align-self the flex row stretches them to the
     container's min-height, which made them 42x58 instead of 42x42. */
  align-self: center;
  aspect-ratio: 1 / 1;
}
/* Reference: .slot img is 86%, .slot.filled img is 94%. */
#tray-slots .tray-img {
  width: 86%;
  height: 86%;
  object-fit: contain;
}
#tray-slots .slot.filled .tray-img {
  width: 94%;
  height: 94%;
}

/* Golden tray-clear sweep — the jackpot tier of the clutch system. Fired from
   App.maybeTrayGold when a tray that sat at the danger line fully empties:
   a skewed gold bar wipes across the tray while a warm ring pulses once.
   Pure DOM/CSS overlay inside #tray (which is position:relative in the theme);
   no canvas work, pointer-events none, so gameplay input is untouched. */
#tray-gold {
  position: absolute;
  inset: -5px -7px;
  border-radius: 16px;
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
  z-index: 6;
}
#tray-gold::before {
  content: '';
  position: absolute;
  top: -30%;
  bottom: -30%;
  left: -45%;
  width: 38%;
  transform: skewX(-18deg);
  background: linear-gradient(90deg,
    rgba(255, 209, 84, 0) 0%,
    rgba(255, 209, 84, 0.78) 35%,
    rgba(255, 245, 216, 0.95) 50%,
    rgba(255, 209, 84, 0.78) 65%,
    rgba(255, 209, 84, 0) 100%);
  filter: blur(1px);
}
#tray-gold::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow:
    0 0 0 2px rgba(255, 193, 7, 0.9),
    0 0 26px 7px rgba(255, 179, 20, 0.5);
}
/* 2026-09-19: the owner read the sweep as too abrupt — everything now runs at
   2x+ the original window (flash/ring 900→1800ms, sweep 620→1400ms) so the
   gold arc is a visible ride rather than a blink; hud.js holds .go for 1900ms
   to match. Audio (audio.js playTrayGold) was retimed to the same window. */
#tray-gold.go { animation: tray-gold-flash 1800ms ease-out forwards; }
#tray-gold.go::before { animation: tray-gold-sweep 1400ms cubic-bezier(0.2, 0.7, 0.25, 1) forwards; }
#tray-gold.go::after { animation: tray-gold-ring 1800ms ease-out forwards; }
@keyframes tray-gold-flash {
  0% { opacity: 0; }
  8% { opacity: 1; }
  70% { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes tray-gold-sweep {
  to { left: 112%; }
}
@keyframes tray-gold-ring {
  0% { opacity: 0.95; }
  100% { opacity: 0; }
}

/* ---- Result card: the celebrating cat (owner spec 2026-09-19) ------------ *
 * "结算页要有一个动态的猫形象庆祝." The motion is CSS on top of one generated
 * illustration: a jump with squash-and-stretch, a breathing warm glow, and a
 * confetti shower built from ten empty spans (colour + delay come from --i).
 * Won-only (hud.renderResultCat toggles .hidden) so a loss is never mocked.
 */
.result-cat {
  position: relative;
  width: 148px;
  height: 148px;
  margin: -4px auto 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}
.result-cat.hidden {
  display: none;
}
.result-cat .cat-glow {
  position: absolute;
  inset: -14px -12px -20px;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 62%, rgba(255, 205, 110, 0.6), rgba(255, 205, 110, 0) 68%);
  animation: cat-glow 2.2s ease-in-out infinite;
}
.result-cat .cat-cheer {
  position: relative;
  width: 100%;
  height: 100%;
  object-fit: contain;
  transform-origin: 50% 94%;
  animation: cat-cheer 1.35s cubic-bezier(0.32, 0.06, 0.36, 1) infinite;
  filter: drop-shadow(0 8px 14px rgba(120, 80, 30, 0.22));
}
.result-cat .cat-confetti {
  position: absolute;
  inset: -18px -10px 0;
  pointer-events: none;
}
.result-cat .cat-confetti span {
  position: absolute;
  top: -6px;
  left: calc(6% + var(--i) * 9.4%);
  width: 7px;
  height: 10px;
  border-radius: 2px;
  opacity: 0;
  animation: cat-confetti 2.6s linear infinite;
  animation-delay: calc(var(--i) * -0.26s);
}
.result-cat .cat-confetti span:nth-child(4n + 1) { background: #ffb43a; }
.result-cat .cat-confetti span:nth-child(4n + 2) { background: #ff8fb8; }
.result-cat .cat-confetti span:nth-child(4n + 3) { background: #7ce0c3; }
.result-cat .cat-confetti span:nth-child(4n + 4) { background: #b98cff; }

@keyframes cat-cheer {
  0% { transform: translateY(0) scale(1, 1) rotate(0deg); }
  18% { transform: translateY(2px) scale(1.06, 0.94) rotate(-1deg); }
  45% { transform: translateY(-16px) scale(0.96, 1.06) rotate(2deg); }
  70% { transform: translateY(0) scale(1.04, 0.96) rotate(-1.5deg); }
  100% { transform: translateY(0) scale(1, 1) rotate(0deg); }
}
@keyframes cat-glow {
  0%, 100% { opacity: 0.6; transform: scale(0.94); }
  50% { opacity: 1; transform: scale(1.05); }
}
@keyframes cat-confetti {
  0% { opacity: 0; transform: translateY(0) rotate(0deg); }
  12% { opacity: 1; }
  85% { opacity: 1; }
  100% { opacity: 0; transform: translateY(168px) rotate(320deg); }
}
@media (prefers-reduced-motion: reduce) {
  .result-cat .cat-cheer,
  .result-cat .cat-glow,
  .result-cat .cat-confetti span {
    animation: none;
  }
}

/* ---- Tray launch tubes (owner spec 2026-09-19) --------------------------- *
 * "最后三张牌，要先呆在卡槽，不要消，用他们作为礼花筒向牌面发礼花弹" — the
 * winning triple must not read as consumed by its own match. The engine has
 * already removed those tiles (that match IS the win), so hud.mountCelebrationTubes
 * paints them back into their slots after the repaint and these rules give them
 * the charged-tube look, the recoil as each shell leaves, and the spent state.
 *
 * z-index 9440 is deliberate: the celebration raises a night curtain at 9400
 * (.fx-curtain) so the additive sparks have something dark to burn against, and
 * the tubes must stay ABOVE it — the three cards being the launchers is the
 * owner's spec, so they glow through the night. They stay BELOW the spark stage
 * (9500) so shells visibly leave the muzzle. The "after the repaint" in the note
 * above is app.finishRun's Hud.renderTray(g) call: arming only sets a flag, and
 * without that repaint these slots never existed at all (fixed 2026-09-19).
 */
.slot.tube {
  position: relative;
  overflow: visible;
  border-color: #f0a52a;
  box-shadow: 0 3px 0 var(--tile-shadow), 0 0 0 3px rgba(255, 174, 42, 0.28),
    inset 0 2px 0 rgba(255, 255, 255, 0.5);
  animation: tube-charge 460ms cubic-bezier(0.2, 0.8, 0.3, 1.25) both;
}

/* Specificity note: body.skin-3d .slot.filled sets z-index: 0 at (0,3,1), so a
   plain .slot.tube (0,2,0) loses regardless of stylesheet order — that is why
   the tubes were pinned at 0 and vanished under the night curtain. Match the
   weight explicitly. */
.slot.tube,
body.skin-3d .slot.tube {
  z-index: 9440;
}
.slot.tube::before {
  content: '';
  position: absolute;
  inset: -6px;
  border-radius: 16px;
  background: radial-gradient(circle at 50% 62%, rgba(255, 196, 92, 0.55), rgba(255, 196, 92, 0) 70%);
  animation: tube-glow 1.1s ease-in-out infinite;
  pointer-events: none;
}
.slot.tube .tube-tile {
  position: relative;
}
.slot.tube .tube-muzzle {
  position: absolute;
  left: 50%;
  top: -7px;
  width: 62%;
  height: 12px;
  margin-left: -31%;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 236, 170, 0.98), rgba(255, 168, 46, 0) 72%);
  opacity: 0;
  pointer-events: none;
}
.slot.tube.firing .tube-muzzle {
  animation: tube-fire 320ms ease-out;
}
.slot.tube.firing .tube-tile {
  animation: tube-recoil 320ms ease-out;
}
.slot.tube.spent .tube-tile {
  opacity: 0.45;
  filter: saturate(0.55);
}
.slot.tube.spent::before {
  animation: none;
  opacity: 0.22;
}
.slot.tube.fade-out {
  animation: tube-out 360ms ease-in forwards;
}

@keyframes tube-charge {
  0% { transform: scale(0.72); }
  60% { transform: scale(1.1); }
  100% { transform: scale(1); }
}
@keyframes tube-glow {
  0%, 100% { opacity: 0.5; }
  50% { opacity: 1; }
}
@keyframes tube-fire {
  0% { opacity: 1; transform: scale(0.6); }
  100% { opacity: 0; transform: scale(1.6); }
}
@keyframes tube-recoil {
  0% { transform: translateY(0) scale(1); }
  22% { transform: translateY(5px) scale(1.08, 0.9); }
  60% { transform: translateY(-2px) scale(0.97, 1.04); }
  100% { transform: translateY(0) scale(1); }
}
@keyframes tube-out {
  to { opacity: 0; transform: scale(0.86); }
}
@media (prefers-reduced-motion: reduce) {
  .slot.tube,
  .slot.tube::before,
  .slot.tube .tube-muzzle,
  .slot.tube .tube-tile {
    animation: none;
  }
}

/* Flying firework shell across viewport (owner spec: from tray tubes to board) */
.firework-shell {
  position: fixed;
  width: 16px;
  height: 16px;
  margin-left: -8px;
  margin-top: -8px;
  border-radius: 50%;
  pointer-events: none;
  z-index: 9999;
  background: radial-gradient(circle, #ffffff 30%, var(--shell-color, #ffd54a) 70%, rgba(255, 200, 50, 0) 100%);
  box-shadow: 0 0 14px 4px var(--shell-color, #ffd54a), 0 0 24px 8px rgba(255, 255, 255, 0.9);
}

.firework-trail-spark {
  position: fixed;
  width: 6px;
  height: 6px;
  margin-left: -3px;
  margin-top: -3px;
  border-radius: 50%;
  pointer-events: none;
  z-index: 9998;
  background: var(--shell-color, #ffd54a);
  box-shadow: 0 0 8px 2px var(--shell-color, #ffd54a);
  animation: shell-trail-fade 280ms ease-out forwards;
}

@keyframes shell-trail-fade {
  0% { opacity: 0.95; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.2) translateY(8px); }
}
