/* 跃火 / JumpSpark — motion & interaction layer (loaded after arcade.css)
   Everything here degrades gracefully: without JS or with reduced motion the
   catalog simply appears in its resting state. */

:root {
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring: cubic-bezier(0.3, 1.4, 0.45, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
}

/* ---------------------------------------------------------------- entrance */

/* Before the catalog is painted the header waits in place; once arcade.js has
   rendered the cards the whole page arrives in one soft choreography. */
html:not(.ui-ready) .masthead,
html:not(.ui-ready) .toolbar,
html:not(.ui-ready) .filterbar { opacity: 0; }

.ui-ready .masthead { animation: fx-rise 620ms var(--ease-out) both; }
.ui-ready .toolbar { animation: fx-rise 620ms 70ms var(--ease-out) both; }
.ui-ready .filterbar { animation: fx-rise 620ms 130ms var(--ease-out) both; }
.ui-ready .foot { animation: fx-fade 900ms 300ms linear both; }

@keyframes fx-rise {
  from { opacity: 0; transform: translate3d(0, 12px, 0); filter: blur(6px); }
  to { opacity: 1; transform: none; filter: none; }
}
@keyframes fx-fade { from { opacity: 0; } to { opacity: 1; } }

/* staggered card arrival — set inline as --i by arcade.js */
.ui-ready .game { animation: fx-card 700ms var(--ease-out) both; animation-delay: calc(var(--i, 0) * 32ms); }
@keyframes fx-card {
  from { opacity: 0; transform: translate3d(0, 18px, 0) scale(0.985); filter: blur(4px); }
  to { opacity: 1; transform: none; filter: none; }
}
.animate-out .game { animation: fx-card-out 180ms var(--ease-in-out) both; animation-delay: 0ms !important; }
@keyframes fx-card-out {
  to { opacity: 0; transform: translate3d(0, -6px, 0) scale(0.99); }
}

/* --------------------------------------------------------- card interaction */

.game { --lift: 0px; }

/* pointer spotlight: --mx/--my are fed by arcade.js on pointermove */
.game::before {
  content: "";
  position: absolute;
  inset: -14px;
  border-radius: calc(var(--radius) + 14px);
  background: radial-gradient(280px circle at var(--mx, 50%) var(--my, 0%), var(--accent-wash), transparent 70%);
  opacity: 0;
  transition: opacity 280ms var(--ease-out);
  pointer-events: none;
  z-index: 0;
}
.game > * { position: relative; z-index: 1; }
.game > .cover { z-index: 2; }
.game > h2 { z-index: 3; }

@media (hover: hover) {
  .game:hover::before { opacity: 1; }

  .game:hover .cover {
    transform: translate3d(0, calc(-4px + var(--tilt-y, 0px)), 0) rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y-deg, 0deg));
    box-shadow: var(--shadow-2);
  }
  .game:hover .cover img { transform: scale(1.045); }
  .game:hover .game-title { translate: 0 -1px; }
}

/* a soft sheen sweeps the cover on hover */
.cover .sheen {
  position: absolute;
  inset: -1px;
  z-index: 3;
  pointer-events: none;
  border-radius: inherit;
  background: linear-gradient(105deg, transparent 30%, rgb(255 255 255 / 0.24) 48%, transparent 62%);
  background-size: 260% 100%;
  background-position: 130% 0;
  opacity: 0;
}
@media (hover: hover) {
  .game:hover .sheen { opacity: 1; animation: fx-sheen 900ms var(--ease-out) both; }
}
@keyframes fx-sheen {
  from { background-position: 130% 0; }
  to { background-position: -40% 0; }
}

/* subtle 3D tilt driven by the pointer (JS sets the custom properties) */
.cover { transform-style: preserve-3d; }

/* the play pill grows into place */
.game.is-pressing .cover { transform: scale(0.975); transition: transform 120ms ease-out; }
.game.is-pressing .cover img { transform: scale(1.07); }

/* ------------------------------------------------------------- click burst */

.fx-ripple {
  position: fixed;
  z-index: 40;
  inline-size: 18px; block-size: 18px;
  margin: -9px 0 0 -9px;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(circle, color-mix(in oklab, var(--accent) 55%, transparent), transparent 70%);
  animation: fx-ripple 620ms var(--ease-out) forwards;
}
@keyframes fx-ripple {
  from { transform: scale(0.4); opacity: 0.9; }
  to { transform: scale(5.2); opacity: 0; }
}
.fx-ring {
  position: fixed;
  z-index: 40;
  inline-size: 44px; block-size: 44px;
  margin: -22px 0 0 -22px;
  border-radius: 50%;
  border: 2px solid color-mix(in oklab, var(--accent) 70%, transparent);
  pointer-events: none;
  animation: fx-ring 520ms var(--ease-out) forwards;
}
@keyframes fx-ring {
  from { transform: scale(0.5); opacity: 1; }
  to { transform: scale(1.6); opacity: 0; }
}

/* count "pop" when a click is recorded */
.click-count.is-bumped { animation: fx-bump 420ms var(--ease-spring); }
@keyframes fx-bump {
  0% { transform: none; }
  35% { transform: translateY(-3px) scale(1.14); color: var(--accent); }
  100% { transform: none; }
}

/* --------------------------------------------------------- filter chips */

.filterbar { position: relative; overflow: hidden; }
.filters { position: relative; }
.fx-pill {
  position: absolute;
  inset-block-start: 2px;
  inset-inline-start: 0;
  block-size: 36px;
  border-radius: 999px;
  background: var(--ink);
  z-index: 0;
  pointer-events: none;
  transition: transform 460ms var(--ease-spring), inline-size 460ms var(--ease-spring), opacity 200ms linear;
  will-change: transform, inline-size;
}
.filters button { position: relative; z-index: 1; }
.filters button[aria-pressed="true"],
.filters button[aria-pressed="true"]:hover { background: transparent; border-color: transparent; color: var(--paper); box-shadow: none; }
.filters button:active { transform: scale(0.94); }
.filters button[aria-pressed="true"] { position: relative; z-index: 3; }
.filters button { transition: transform 140ms var(--ease-out), background-color 140ms ease, color 140ms ease, border-color 140ms ease; }

/* ---------------------------------------------------------- language switch */

/* language flip: text re-enters */
html.lang-flip .title,
html.lang-flip .brand-name,
html.lang-flip .count,
html.lang-flip .game-title,
html.lang-flip .description { animation: fx-lang 420ms var(--ease-out) both; }
html.lang-flip .game:nth-child(n) { animation-delay: calc(var(--i, 0) * 14ms); }
@keyframes fx-lang {
  from { opacity: 0.15; transform: translateY(4px); filter: blur(2px); }
  to { opacity: 1; transform: none; filter: none; }
}

/* ---------------------------------------------------------------- search */

.search .search-icon { transition: stroke 200ms ease, transform 300ms var(--ease-out); }
.search:focus-within .search-icon { stroke: var(--accent); transform: scale(1.08); }
.search .clear { animation: fx-pop 260ms var(--ease-spring) both; }
@keyframes fx-pop { from { opacity: 0; transform: translate(0, -50%) scale(0.6); } to { opacity: 1; transform: translate(0, -50%) scale(1); } }
mark { animation: fx-mark 520ms var(--ease-out) both; }
@keyframes fx-mark { from { background: transparent; } to { background: var(--mark); } }

/* skeletons while the first paint is being composed */
.skeleton-card { display: flex; flex-direction: column; gap: 12px; }
.skeleton-card .sk-cover,
.skeleton-card .sk-line { background: linear-gradient(100deg, var(--line) 30%, color-mix(in oklab, var(--line) 45%, var(--surface)) 50%, var(--line) 70%); background-size: 220% 100%; animation: fx-shimmer 1.3s linear infinite; border-radius: 8px; }
.skeleton-card .sk-cover { aspect-ratio: 16 / 9; }
.skeleton-card .sk-line { block-size: 14px; }
.skeleton-card .sk-line.short { inline-size: 55%; }
@keyframes fx-shimmer { from { background-position: 120% 0; } to { background-position: -120% 0; } }

/* -------------------------------------------------------- launch overlay */

.fx-launch {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  gap: 14px;
  background: color-mix(in oklab, var(--paper) 86%, transparent);
  backdrop-filter: blur(10px) saturate(1.1);
  -webkit-backdrop-filter: blur(10px) saturate(1.1);
  pointer-events: none;
  isolation: isolate;
  /* "both" is load-bearing: with fill-mode none a veil that is inserted while
     the next document is open (arcade.js runs when the parser hits </body>)
     keeps playing in a frozen/getComputedStyle timeline, never paints, and
     once its 240ms run out falls back to the base opacity of 0. "both" holds
     the to-frame from creation, so the worst case is a dim veil, never 0. */
  animation: fx-veil-in 240ms ease both;
}
.fx-launch > * { pointer-events: none; }
.fx-launch[hidden] { display: none; }
@keyframes fx-veil-in { from { opacity: 0.55; } to { opacity: 1; } }
.fx-launch .launch-card {
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  padding: 26px 34px;
  border-radius: 18px;
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-2);
  animation: fx-pop-in 460ms var(--ease-spring) both;
}
@keyframes fx-pop-in { from { opacity: 0; transform: translateY(10px) scale(0.94); } to { opacity: 1; transform: none; } }
.fx-launch .launch-art {
  inline-size: 120px; block-size: 68px;
  border-radius: 8px;
  background-size: cover; background-position: center;
  box-shadow: var(--shadow-1);
}
.fx-launch .launch-title { margin: 0; font-size: 15px; font-weight: 700; max-inline-size: 260px; text-align: center; }
.fx-launch .launch-note { margin: 0; color: var(--quiet); font-size: 12.5px; }
.fx-launch .launch-bar { inline-size: 160px; block-size: 3px; border-radius: 3px; background: var(--line); overflow: hidden; }
.fx-launch .launch-bar::after { content: ""; display: block; block-size: 100%; inline-size: 40%; background: var(--accent); border-radius: inherit; animation: fx-load 900ms var(--ease-in-out) infinite; }
@keyframes fx-load { from { transform: translateX(-100%); } to { transform: translateX(250%); } }

/* ---------------------------------------------------------------- misc */

.masthead .mark { transition: transform 500ms var(--ease-spring); }
.brand:hover .mark { transform: rotate(-8deg) scale(1.06); }
.skip:focus-visible { box-shadow: 0 8px 24px -10px rgb(0 0 0 / 0.5); }

/* reduced motion turns all of it off */
@media (prefers-reduced-motion: reduce) {
  html:not(.ui-ready) .masthead,
  html:not(.ui-ready) .toolbar,
  html:not(.ui-ready) .filterbar { opacity: 1; }
  .ui-ready .masthead,
  .ui-ready .toolbar,
  .ui-ready .filterbar,
  .ui-ready .game,
  .ui-ready .foot,
  html.lang-flip .title,
  html.lang-flip .brand-name,
  html.lang-flip .count,
  html.lang-flip .game-title,
  html.lang-flip .description { animation: none !important; }
  .fx-pill { transition: none; }
  .game::before { display: none; }
  .cover .sheen { display: none; }
}
