/* Mini game animations for the homepage card thumbnails.
   Spec and behaviour table: HOMEPAGE-PLAN.md §2. Built by mini-anims.js, which refuses to
   replace a card's fallback emoji unless --mini-ready below says this file arrived. */

:root { --mini-ready: 1; }

/* Shared: .play on a thumb switches its keyframes on, .paused freezes them, and removing
   .play resets to frame zero. Per-game blocks follow, each under a "mini: name" banner.
   Deliberately a bare class, not ".game-card-thumb .mini": a per-game block that sets --u
   on this same element must be able to win on source order alone. */
.mini {
  --u: 24px;
  position: relative;
  background: #fff;
  border-radius: .6rem;
  padding: .45rem;
  box-shadow: 0 4px 14px rgba(124, 58, 237, .16);
  font-size: calc(var(--u) * .42);
  font-weight: 800;
  line-height: 1;
  color: #5a4478;
}

.game-card-thumb.paused .mini,
.game-card-thumb.paused .mini *,
.game-card-thumb.paused .mini *::before,
.game-card-thumb.paused .mini *::after { animation-play-state: paused !important; }

@media (prefers-reduced-motion: reduce) {
  .game-card-thumb.play .mini,
  .game-card-thumb.play .mini *,
  .game-card-thumb.play .mini *::before,
  .game-card-thumb.play .mini *::after { animation-duration: 0s !important; animation-delay: 0s !important; }
}

/* mini: nonograms (nn) — colours match nonograms.html's board */
.mini-nono {
  display: grid;
  grid-template-columns: calc(var(--u) * 1.5) repeat(5, var(--u));
  grid-template-rows: calc(var(--u) * .9) repeat(5, var(--u));
}

.nn-ct, .nn-cl { display: flex; }
.nn-ct { align-items: flex-end; justify-content: center; padding-bottom: 3px; border-bottom: 2px solid var(--purple); }
.nn-cl { align-items: center; justify-content: flex-end; padding-right: 4px; border-right: 2px solid var(--purple); white-space: nowrap; }

.nn-ct span, .nn-cl span { position: relative; }
.nn-ct span::after, .nn-cl span::after {
  content: '';
  position: absolute;
  left: -1px; right: -1px; top: 50%;
  height: 2px;
  background: #a5b4fc;
  transform: scaleX(0);
  transform-origin: left;
}

.nn-c { position: relative; border: 1px solid #d8b4fe; }
.nn-c::before { content: ''; position: absolute; inset: 0; background: var(--purple); transform: scale(0); }
.nn-c.x::after {
  content: '✕';
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #c084fc;
  font-size: calc(var(--u) * .55);
  opacity: 0;
}

.game-card-thumb.play .nn-c.f::before { animation: nnFill .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * .12s) both, nnPink .5s var(--end) both; }
.game-card-thumb.play .nn-c.x::after  { animation: nnMark .25s calc(var(--i) * .12s) both; }
.game-card-thumb.play .nn-ct.d,
.game-card-thumb.play .nn-cl.d        { animation: nnDone .3s calc(var(--i) * .12s) both; }
.game-card-thumb.play .nn-ct.d span::after,
.game-card-thumb.play .nn-cl.d span::after { animation: nnStrike .3s calc(var(--i) * .12s) both; }
.game-card-thumb.play .mini-nono      { animation: nnPop .5s var(--end) both; }

@keyframes nnFill   { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes nnMark   { from { opacity: 0; transform: scale(.3); } to { opacity: 1; transform: scale(1); } }
@keyframes nnDone   { to { color: #a5b4fc; } }
@keyframes nnStrike { to { transform: scaleX(1); } }
@keyframes nnPink   { to { box-shadow: inset 0 0 0 var(--u) var(--pink); } }
@keyframes nnPop    { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.08); } }

/* mini: norinori (no) — region colours, borders and domino gradient match norinori.html.
   Cells draw only their right and bottom edges so every line is drawn once. */
.mini-nori {
  display: grid;
  grid-template-columns: repeat(6, var(--u));
  grid-template-rows: repeat(6, var(--u));
  border: 2px solid #a855f7;
}
.no-c { position: relative; border-right: 1px solid #e9d5ff; border-bottom: 1px solid #e9d5ff; }
.no-c.br { border-right: 2px solid #a855f7; }
.no-c.bb { border-bottom: 2px solid #a855f7; }
.no-c:nth-child(6n) { border-right: 0; }
.no-c:nth-last-child(-n+6) { border-bottom: 0; }
.no-c.s::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, #ec4899, #a855f7);
  box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .35);
  transform: scale(0);
}

.game-card-thumb.play .no-c.s::before { animation: noShade .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * .14s) both; }
.game-card-thumb.play .no-board       { animation: noPop .5s var(--end) both; }

@keyframes noShade { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes noPop   { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.08); } }

/* mini: skyscrapers (sk) — cell, bar and clue colours match skyscrapers.html.
   Each cell's ::before is the building (height from --h), ::after its number. */
.mini-sky {
  --u: 27px;                 /* 4 cells + 2 clue tracks = 5.4u; 27px makes it the size of a 6×6 */
  display: grid;
  grid-template-columns: calc(var(--u) * .7) repeat(4, var(--u)) calc(var(--u) * .7);
  grid-template-rows: calc(var(--u) * .7) repeat(4, var(--u)) calc(var(--u) * .7);
  color: #3b82f6;
  font-size: calc(var(--u) * .42);
}
.sk-k { display: flex; align-items: center; justify-content: center; }
.sk-c { position: relative; border: 1px solid #bfdbfe; border-radius: 3px; background: #f8fbff; }
.sk-c::before {
  content: '';
  position: absolute;
  left: 20%; right: 20%; bottom: 1px;
  height: calc(var(--h) * 16%);
  background: linear-gradient(135deg, var(--blue), #6366f1);
  border-radius: 2px 2px 0 0;
  transform: scaleY(0);
  transform-origin: bottom;
}
.sk-c::after {
  content: attr(data-h);
  position: absolute;
  top: 0; left: 0; right: 0;
  text-align: center;
  font-size: calc(var(--u) * .34);
  color: var(--text);
  opacity: 0;
}

.game-card-thumb.play .sk-c::before { animation: skRise .35s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * .16s) both; }
.game-card-thumb.play .sk-c::after  { animation: skNum .25s calc(var(--i) * .16s + .2s) both; }
.game-card-thumb.play .sk-k.d       { animation: skDone .3s calc(var(--i) * .16s) both; }
.game-card-thumb.play .sk-board     { animation: skPop .5s var(--end) both; }

@keyframes skRise { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes skNum  { from { opacity: 0; } to { opacity: 1; } }
@keyframes skDone { to { color: #a5b4fc; } }
@keyframes skPop  { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.08); } }

/* mini: sudoku (su) — cell lines, box borders and digit colours match sudoku.html.
   Full 9×9 at a small unit, because 4×4 and 6×6 are separate cards. */
.mini-sudo {
  --u: 17px;
  display: grid;
  grid-template-columns: repeat(9, var(--u));
  grid-template-rows: repeat(9, var(--u));
  border: 2px solid #16a34a;
  font-size: calc(var(--u) * .58);
  font-weight: 900;
  color: var(--text);
}
.su-c {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  border-right: 1px solid #bbf7d0;
  border-bottom: 1px solid #bbf7d0;
  background: #fff;
}
.su-c.br { border-right: 2px solid #16a34a; }
.su-c.bb { border-bottom: 2px solid #16a34a; }
.su-c:nth-child(9n) { border-right: 0; }
.su-c:nth-last-child(-n+9) { border-bottom: 0; }
.su-c.e::before { content: attr(data-v); color: #059669; transform: scale(0); }

.game-card-thumb.play .su-c.e::before { animation: suEnter .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * .2s) both; }
.game-card-thumb.play .su-c.f        { animation: suFlash .5s var(--end) both; }
.game-card-thumb.play .su-board      { animation: suPop .5s var(--end) both; }

@keyframes suEnter { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes suFlash { 0% { background: #fff; } 35% { background: #bbf7d0; } 100% { background: #f0fdf4; } }
@keyframes suPop   { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.08); } }

/* mini: kakuro (ka) — black cells, split clue cells and digit colours match kakuro.html.
   Grid lines are the 1px gap showing the orange background through. */
.mini-kaku {
  --u: 28px;
  display: grid;
  grid-template-columns: repeat(5, var(--u));
  grid-template-rows: repeat(5, var(--u));
  gap: 1px;
  background: #fed7aa;
  border: 1px solid #8b5cf6;
  font-size: calc(var(--u) * .55);
  color: var(--text);
}
.ka-b { background: linear-gradient(135deg, #a78bfa, #7c3aed); box-shadow: 0 0 0 1px #8b5cf6; }
.ka-k {
  position: relative;
  overflow: hidden;
  background: linear-gradient(to bottom left, #60a5fa calc(50% - 1px), #fff calc(50% - .5px), #fff calc(50% + .5px), #f472b6 calc(50% + 1px));
  box-shadow: 0 0 0 1px #8b5cf6;
  font-size: calc(var(--u) * .34);
}
.ka-k span { position: absolute; line-height: 1; }
.ka-a { top: 2px; right: 3px; color: #1e40af; }
.ka-d { bottom: 2px; left: 3px; color: #9d174d; }
.ka-w { display: flex; align-items: center; justify-content: center; background: #fffbf5; }
.ka-w.g { font-weight: 900; }
.ka-w.e::before { content: attr(data-v); color: #ea580c; transform: scale(0); }

.game-card-thumb.play .ka-w.e::before { animation: kaEnter .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * .28s) both; }
.game-card-thumb.play .ka-k span.d   { animation: kaDone .4s calc(var(--i) * .28s) both; }
.game-card-thumb.play .ka-board      { animation: kaPop .5s var(--end) both; }

@keyframes kaEnter { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes kaDone  { 0% { transform: scale(1); } 50% { transform: scale(1.5); color: #fff; } 100% { transform: scale(1); color: #166534; } }
@keyframes kaPop   { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.08); } }

/* mini: code breaker (cb) — peg colours, empty rings, feedback dots and the hidden
   code's checkerboard match code-breaker.html. Delays are in 0.1s steps. */
.mini-code {
  --p: calc(var(--u) * .9);
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: calc(var(--u) * .36);
  color: #a16207;
}
.cb-row { display: flex; align-items: center; gap: 3px; padding: 2px 3px; border-radius: 5px; }
.cb-secret { border-bottom: 2px solid #fde68a; padding-bottom: 4px; margin-bottom: 2px; }
.cb-n { width: calc(var(--u) * .5); text-align: center; }
.cb-peg { position: relative; width: var(--p); height: var(--p); border-radius: 50%; border: 2px solid #e5e7eb; background: #f9fafb; }
.cb-peg.s { background: repeating-conic-gradient(#e5e7eb 0% 25%, #d1d5db 25% 50%); border-color: #d1d5db; }
.cb-peg::before {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: 50%;
  background: var(--c);
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, .15);
  transform: scale(0);
}
.cb-fb { display: grid; grid-template-columns: 1fr 1fr; gap: 2px; margin-left: 2px; width: 14px; }
.cb-fb i { position: relative; width: 6px; height: 6px; border-radius: 50%; background: #e5e7eb; }
.cb-fb i::before { content: ''; position: absolute; inset: 0; border-radius: 50%; transform: scale(0); }
.cb-fb i.b::before { background: #1f2937; }
.cb-fb i.w::before { background: #fff; box-shadow: inset 0 0 0 1.5px #9ca3af; }

.game-card-thumb.play .cb-peg::before,
.game-card-thumb.play .cb-fb i.b::before,
.game-card-thumb.play .cb-fb i.w::before { animation: cbPeg .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * .1s) both; }
.game-card-thumb.play .cb-row.g { animation: cbRow .8s calc(var(--i) * .1s) both; }
.game-card-thumb.play .cb-board { animation: cbPop .5s var(--end) both; }

@keyframes cbPeg { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes cbRow { 0% { background: transparent; } 15% { background: #fef9c3; } 100% { background: transparent; } }
@keyframes cbPop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.08); } }

/* mini: 2048 (tl) — board, cell and tile colours match 2048.html. Every tile is one
   fixed-length "life" per move (pop in, hold, slide, vanish or stay), so one keyframe
   set drives them all; --x0/--y0 and --x1/--y1 are cell indices. */
.mini-2048 {
  --t: 32px; --g: 4px; --st: calc(var(--t) + var(--g)); --D: .75s;
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, var(--t));
  grid-template-rows: repeat(4, var(--t));
  gap: var(--g);
  padding: var(--g);
  background: #be123c;
  border-radius: 6px;
  font-size: calc(var(--t) * .45);
  font-weight: 900;
}
.mini-2048 i { background: rgba(255, 255, 255, .18); border-radius: 4px; }
.tl {
  position: absolute;
  top: var(--g); left: var(--g);
  width: var(--t); height: var(--t);
  display: flex; align-items: center; justify-content: center;
  border-radius: 4px;
  opacity: 0;
}

.game-card-thumb.play .tl.l    { animation: tlLife var(--D) linear calc(var(--i) * var(--D)) forwards; }
.game-card-thumb.play .tl.b    { animation: tlBirth calc(var(--D) * .3) ease-out calc(var(--i) * var(--D)) forwards; }
.game-card-thumb.play .tl-board { animation: tlPop .5s var(--end) both; }

@keyframes tlLife {
  0%    { transform: translate(calc(var(--x0) * var(--st)), calc(var(--y0) * var(--st))) scale(var(--s0)); opacity: 1; animation-timing-function: cubic-bezier(.34, 1.56, .64, 1); }
  15%   { transform: translate(calc(var(--x0) * var(--st)), calc(var(--y0) * var(--st))) scale(1); }
  50%   { transform: translate(calc(var(--x0) * var(--st)), calc(var(--y0) * var(--st))) scale(1); animation-timing-function: ease-out; }
  70%   { transform: translate(calc(var(--x1) * var(--st)), calc(var(--y1) * var(--st))) scale(1); opacity: 1; }
  71%   { opacity: var(--om); }
  99.9% { transform: translate(calc(var(--x1) * var(--st)), calc(var(--y1) * var(--st))) scale(1); opacity: var(--om); }
  100%  { transform: translate(calc(var(--x1) * var(--st)), calc(var(--y1) * var(--st))) scale(1); opacity: var(--oe); }
}
@keyframes tlBirth {
  0%    { transform: translate(calc(var(--x0) * var(--st)), calc(var(--y0) * var(--st))) scale(0); opacity: 1; }
  60%   { transform: translate(calc(var(--x0) * var(--st)), calc(var(--y0) * var(--st))) scale(1.2); }
  99.9% { transform: translate(calc(var(--x0) * var(--st)), calc(var(--y0) * var(--st))) scale(1); opacity: 1; }
  100%  { transform: translate(calc(var(--x0) * var(--st)), calc(var(--y0) * var(--st))) scale(1); opacity: var(--oe); }
}
@keyframes tlPop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.08); } }

/* mini: battleship (bs) — board, water, ship and count colours match battleship.html.
   Delays are in seconds. */
.mini-bs {
  --u: 21px;
  display: grid;
  grid-template-columns: calc(var(--u) * .7) repeat(6, var(--u));
  grid-template-rows: calc(var(--u) * .7) repeat(6, var(--u));
  gap: 2px;
  padding: 2px;
  background: #164e63;
  border-radius: 6px;
  font-size: calc(var(--u) * .48);
  color: #a5f3fc;
}
.bs-k { display: flex; align-items: center; justify-content: center; background: #155e75; border-radius: 2px; }
.bs-c { position: relative; background: #1e3a5f; border-radius: 3px; }
.bs-c::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 3px;
  display: flex; align-items: center; justify-content: center;
  transform: scale(0);
}
.bs-c.w::before { content: '~'; background: #7dd3fc; color: #0284c7; font-size: calc(var(--u) * .5); font-weight: 900; }
.bs-c.s::before { background: linear-gradient(180deg, #fbbf24, #d97706); box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .45); }
.bs-c.r-l::before { border-radius: 50% 3px 3px 50%; }
.bs-c.r-r::before { border-radius: 3px 50% 50% 3px; }
.bs-c.r-t::before { border-radius: 50% 50% 3px 3px; }
.bs-c.r-b::before { border-radius: 3px 3px 50% 50%; }
.bs-c.r-m::before { border-radius: 2px; }
.bs-c.r-o::before { border-radius: 50%; }

.game-card-thumb.play .bs-c.w::before { animation: bsIn .25s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .bs-c.s::before { animation: bsIn .35s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
.game-card-thumb.play .bs-k.d        { animation: bsMet .3s calc(var(--i) * 1s) both; }
.game-card-thumb.play .bs-board      { animation: bsPop .5s var(--end) both; }

@keyframes bsIn  { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes bsMet { to { color: #86efac; } }
@keyframes bsPop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.08); } }

/* mini: mahjong (mj) — tile faces, 3D edges, upper-layer tint, the purple pick ring and
   the fade-out match copy mahjong.html. Blocked tiles wear a white veil until freed.
   --x/--y are tile positions in tile units, --z the layer; delays are in seconds. */
.mini-mj {
  --tw: 30px; --th: 36px;
  position: relative;
  width: calc(4 * (var(--tw) + 2px) + 4px);
  height: calc(3 * (var(--th) + 2px) + 4px);
  font-size: calc(var(--tw) * .6);
  line-height: 1;
}
.mj-t {
  --edge: #c4b5fd; --bd: #d4d4d8;
  position: absolute;
  left: calc(4px + var(--x) * (var(--tw) + 2px) - var(--z) * 4px);
  top: calc(4px + var(--y) * (var(--th) + 2px) - var(--z) * 4px);
  z-index: var(--z);
  width: var(--tw); height: var(--th);
  display: flex; align-items: center; justify-content: center;
  border: 2px solid var(--bd);
  border-radius: 5px;
  background: #fff;
  box-shadow: 2px 3px 0 var(--edge), 3px 5px 10px rgba(0, 0, 0, .12);
}
.mj-t.l1 { --edge: #fbbf24; --bd: #fde68a; background: #fefce8; }
.mj-t.b::after { content: ''; position: absolute; inset: -2px; border-radius: 5px; background: rgba(255, 255, 255, .55); }
.mj-win { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: calc(var(--tw) * 1.3); transform: scale(0); }

.game-card-thumb.play .mj-t          { animation: mjGo .6s ease-in calc(var(--i) * 1s) both; }
.game-card-thumb.play .mj-t.b::after { animation: mjFree .3s calc(var(--f) * 1s) both; }
.game-card-thumb.play .mj-win        { animation: mjWin .5s cubic-bezier(.34, 1.56, .64, 1) var(--end) both; }

@keyframes mjGo {
  0%   { transform: none; border-color: var(--bd); box-shadow: 2px 3px 0 var(--edge), 3px 5px 10px rgba(0, 0, 0, .12); opacity: 1; }
  35%  { transform: translateY(-3px); border-color: #9333ea; box-shadow: 0 0 0 3px #9333ea, 3px 5px 10px rgba(0, 0, 0, .15); }
  70%  { transform: translateY(-3px); opacity: 1; }
  100% { transform: translateY(-3px) scale(.7); border-color: #9333ea; box-shadow: 0 0 0 3px #9333ea, 3px 5px 10px rgba(0, 0, 0, .15); opacity: 0; }
}
@keyframes mjFree { to { opacity: 0; } }
@keyframes mjWin  { from { transform: scale(0); } to { transform: scale(1); } }

/* mini: slitherlink (sl) — grid, dot, clue and loop colours match slitherlink.html's
   canvas. The whole loop is one path drawn with stroke-dashoffset, so it closes on the
   last frame. First of the Path pattern; Masyu, Hashi and Dots and Boxes reuse this. */
.mini-sl { --u: 26px; line-height: 0; }
.sl-svg { display: block; width: calc(var(--u) * 5); height: calc(var(--u) * 5); }
.sl-g { stroke: #d9f99d; stroke-width: .8; fill: none; }
.sl-d { fill: #4d7c0f; }
.sl-n { font: 800 9px 'Segoe UI', system-ui, sans-serif; text-anchor: middle; fill: #365314; }
.sl-n.z { fill: #a3a3a3; }
.sl-n.t { fill: #065f46; }
.sl-p {
  fill: none;
  stroke: #4d7c0f;
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: var(--len);
  stroke-dashoffset: var(--len);
}

.game-card-thumb.play .sl-p     { animation: slDraw 2.6s ease-in-out both; }
.game-card-thumb.play .sl-board { animation: slPop .5s var(--end) both; }

@keyframes slDraw { to { stroke-dashoffset: 0; } }
@keyframes slPop  { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.08); } }

/* mini: 4 in a row (fr) — board, disc gradients, drop and win pulse match 4-in-a-row.html.
   Discs fall in from above and the board clips them, exactly as the real board does, since
   it too is rounded at the bottom only. --row is the landing row; the drop distance is
   derived from it so it survives the phone resize. */
.mini-fr { --u: 18px; --gp: 3px; }
.fr-board {
  display: grid;
  grid-template-columns: repeat(7, var(--u));
  grid-template-rows: repeat(6, var(--u));
  gap: var(--gp);
  padding: 5px;
  background: linear-gradient(145deg, #2563eb, #1d4ed8);
  border-radius: 0 0 8px 8px;
  overflow: hidden;
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, .15);
}
.fr-c {
  --dy: calc((var(--row) + 1) * (var(--u) + var(--gp)));
  position: relative;
  border-radius: 50%;
  background: #fdf6ff;
  box-shadow: inset 0 2px 4px rgba(0, 0, 0, .15);
}
.fr-c::before { content: ''; position: absolute; inset: 0; border-radius: 50%; opacity: 0; }
.fr-c.r::before { background: radial-gradient(circle at 35% 35%, #fca5a5, #ef4444 60%, #b91c1c); box-shadow: inset 0 -2px 4px rgba(0, 0, 0, .2); }
.fr-c.y::before { background: radial-gradient(circle at 35% 35%, #fef08a, #facc15 60%, #a16207); box-shadow: inset 0 -2px 4px rgba(0, 0, 0, .15); }

.game-card-thumb.play .fr-c.r::before,
.game-card-thumb.play .fr-c.y::before { animation: frDrop .35s cubic-bezier(.25, .1, .25, 1.4) calc(var(--i) * .28s) both; }
/* A winning cell is also .r, so this must stay after that rule to add the pulse. */
.game-card-thumb.play .fr-c.w::before {
  animation: frDrop .35s cubic-bezier(.25, .1, .25, 1.4) calc(var(--i) * .28s) both,
             frWin .4s ease-in-out var(--end) 2;
}
.game-card-thumb.play .fr-board { animation: frPop .5s var(--end) both; }

@keyframes frDrop { from { transform: translateY(calc(var(--dy) * -1)); opacity: .7; } to { transform: translateY(0); opacity: 1; } }
@keyframes frWin  { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.14); } }
@keyframes frPop  { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.08); } }

/* mini: minesweeper (ms) — board, tile, revealed face and number colours match
   minesweeper.html. Each cell is already its revealed face; the green cover sits on top as
   ::before and vanishes on reveal, which is the right way round: a flag goes on a cell whose
   cover is still there. */
.mini-ms { --u: 22px; }
.ms-board {
  display: grid;
  grid-template-columns: repeat(6, var(--u));
  grid-template-rows: repeat(6, var(--u));
  gap: 2px;
  padding: 4px;
  background: #15803d;
  border-radius: 7px;
  font-size: calc(var(--u) * .5);
  font-weight: 800;
}
.ms-c {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  border-radius: 3px;
  background: #f0fdf4;
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, .06);
}
.ms-c::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: 3px;
  background: linear-gradient(145deg, #86efac, #4ade80);
  box-shadow: inset 0 1px 2px rgba(255, 255, 255, .45), inset 0 -1px 2px rgba(0, 0, 0, .08);
}
.ms-c.fg::after {
  content: '🚩';
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex; align-items: center; justify-content: center;
  font-size: calc(var(--u) * .62);
  opacity: 0;
  transform: scale(0);
}
.ms-c.n1 { color: #2563eb; }
.ms-c.n2 { color: #16a34a; }
.ms-c.n3 { color: #dc2626; }
.ms-c.n4 { color: #7c3aed; }
.ms-c.n5 { color: #b91c1c; }
.ms-c.n6 { color: #0891b2; }
.ms-c.n7 { color: #1e293b; }
.ms-c.n8 { color: #6b7280; }

.game-card-thumb.play .ms-c.rv::before { animation: msOpen .2s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .ms-c.fg::after  { animation: msFlag .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
.game-card-thumb.play .ms-board        { animation: msPop .5s var(--end) both; }

@keyframes msOpen { to { opacity: 0; transform: scale(.55); } }
@keyframes msFlag { to { opacity: 1; transform: scale(1); } }
@keyframes msPop  { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.08); } }

/* mini: tents and trees (tt) — cell borders, the tree, tent and grass fills, the clue
   colours and the little CSS tent all match tents-and-trees.html. Trees and clue numbers
   are given, so they are there from frame one and never animate in. */
.mini-tt { --u: 24px; }
.tt-board {
  display: grid;
  grid-template-columns: repeat(5, var(--u)) calc(var(--u) * .62);
  grid-template-rows: repeat(5, var(--u)) calc(var(--u) * .62);
  font-size: calc(var(--u) * .5);
  line-height: 1;
}
.tt-k {
  display: flex; align-items: center; justify-content: center;
  font-weight: 900;
  font-size: calc(var(--u) * .46);
  color: #2d1b4e;
}
.tt-c {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid #a3e635;
  background: #fefce8;
}
.tt-c::before { content: ''; position: absolute; inset: 0; opacity: 0; }
.tt-c.gr::before { background: linear-gradient(135deg, #ecfccb, #d9f99d); }
.tt-c.te::before { background: linear-gradient(135deg, #fef3c7, #fde68a); }
.tt-c.tr { background: linear-gradient(135deg, #dcfce7, #bbf7d0); }
.tt-c.tr::before { content: none; }
.tt-t { position: relative; z-index: 1; display: block; transform: scale(0); }
.tt-t::before {
  content: '';
  display: block;
  width: 0; height: 0;
  border-left: calc(var(--u) * .26) solid transparent;
  border-right: calc(var(--u) * .26) solid transparent;
  border-bottom: calc(var(--u) * .32) solid #92400e;
  margin: 0 auto;
}
.tt-t::after {
  content: '';
  display: block;
  width: calc(var(--u) * .6);
  height: calc(var(--u) * .1);
  background: #78350f;
  border-radius: 0 0 2px 2px;
  margin: 0 auto;
}

.game-card-thumb.play .tt-c.gr::before,
.game-card-thumb.play .tt-c.te::before { animation: ttIn .25s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .tt-t     { animation: ttPop .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
.game-card-thumb.play .tt-k.d   { animation: ttMet .3s calc(var(--i) * 1s) both; }
.game-card-thumb.play .tt-board { animation: ttBoard .5s var(--end) both; }

@keyframes ttIn    { to { opacity: 1; } }
@keyframes ttPop   { to { transform: scale(1); } }
@keyframes ttMet   { to { color: #65a30d; } }
@keyframes ttBoard { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.08); } }

/* mini: samegame (sg) — board, tile colours, the highlight and the shrinking pop match
   samegame.html. Two clicks on one shared timeline: the tile element carries the falling
   (translate) and its ::before carries the face (highlight, then pop), so the two never
   fight over transform. --x1/--y1 are cell offsets after the first click, --x2/--y2 after
   the second, both measured from where the tile started. */
.mini-sg { --u: 21px; --gp: 2px; --st: calc(var(--u) + var(--gp)); --D: 3.4s; }
.sg-board {
  display: grid;
  grid-template-columns: repeat(6, var(--u));
  grid-template-rows: repeat(6, var(--u));
  gap: var(--gp);
  padding: 3px;
  background: #fce7f3;
  border: 2px solid #f9a8d4;
  border-radius: 7px;
}
.sg-t { position: relative; }
.sg-t::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 3px;
  background: var(--c);
  box-shadow: inset 0 -2px 0 var(--d);
}

.game-card-thumb.play .sg-t.mv          { animation: sgFall var(--D) linear both; }
.game-card-thumb.play .sg-t.g1::before  { animation: sgGo1 var(--D) linear both; }
.game-card-thumb.play .sg-t.g2::before  { animation: sgGo2 var(--D) linear both; }
.game-card-thumb.play .sg-board         { animation: sgPop .5s var(--end) both; }

@keyframes sgFall {
  0%        { transform: translate(0, 0); }
  26%       { transform: translate(0, 0); animation-timing-function: ease-in; }
  38%, 68%  { transform: translate(calc(var(--x1) * var(--st)), calc(var(--y1) * var(--st))); }
  69%       { transform: translate(calc(var(--x1) * var(--st)), calc(var(--y1) * var(--st))); animation-timing-function: ease-in; }
  80%, 100% { transform: translate(calc(var(--x2) * var(--st)), calc(var(--y2) * var(--st))); }
}
@keyframes sgGo1 {
  0%, 10%   { filter: none; transform: scale(1); opacity: 1; }
  16%, 23%  { filter: brightness(1.3); transform: scale(1.08); opacity: 1; }
  26%, 100% { filter: brightness(1.3); transform: scale(.3); opacity: 0; }
}
@keyframes sgGo2 {
  0%, 52%   { filter: none; transform: scale(1); opacity: 1; }
  58%, 65%  { filter: brightness(1.3); transform: scale(1.08); opacity: 1; }
  68%, 100% { filter: brightness(1.3); transform: scale(.3); opacity: 0; }
}
@keyframes sgPop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.08); } }

/* mini: the small sudokus (sx) — 4x4 and 6x6 share everything except grid size, box shape
   and step. Pale purple lines, thick purple box dividers and a different colour per digit,
   all from sudoku-4x4.html and sudoku-6x6.html. Those colours are the point: they stop
   either card looking like a shrunken copy of the green 9x9 on the Sudoku card. */
.mini-s4, .mini-s6 {
  display: grid;
  border: 3px solid #6d28d9;
  border-radius: 4px;
  font-weight: 900;
  color: #2d1b4e;
}
/* --st, the per-digit step, is set inline by the builder so it and --end cannot drift. */
.mini-s4 {
  --u: 30px;
  grid-template-columns: repeat(4, var(--u));
  grid-template-rows: repeat(4, var(--u));
  font-size: calc(var(--u) * .58);
}
.mini-s6 {
  --u: 23px;
  grid-template-columns: repeat(6, var(--u));
  grid-template-rows: repeat(6, var(--u));
  font-size: calc(var(--u) * .62);
}
.sx-c {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  border-right: 2px solid #e9d5f5;
  border-bottom: 2px solid #e9d5f5;
  background: #fff;
}
.sx-c.br { border-right: 3px solid #6d28d9; }
.sx-c.bb { border-bottom: 3px solid #6d28d9; }
.mini-s4 .sx-c:nth-child(4n) { border-right: 0; }
.mini-s4 .sx-c:nth-last-child(-n+4) { border-bottom: 0; }
.mini-s6 .sx-c:nth-child(6n) { border-right: 0; }
.mini-s6 .sx-c:nth-last-child(-n+6) { border-bottom: 0; }
.sx-c.n1 { color: #e53e3e; }
.sx-c.n2 { color: #3182ce; }
.sx-c.n3 { color: #38a169; }
.sx-c.n4 { color: #d69e2e; }
.sx-c.n5 { color: #9f1eb0; }
.sx-c.n6 { color: #e05d10; }
.sx-c.e::before { content: attr(data-v); transform: scale(0); }

.game-card-thumb.play .sx-c.e::before { animation: sxIn .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * var(--st)) both; }
.game-card-thumb.play .sx-c.e         { animation: sxTap .45s calc(var(--i) * var(--st)) both; }
.game-card-thumb.play .mini-s4,
.game-card-thumb.play .mini-s6        { animation: sxDone .6s var(--end) both; }

@keyframes sxIn   { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes sxTap  { from { background: #ede9fe; } to { background: #fff; } }
@keyframes sxDone { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.07); } }

/* mini: hitori (hi) — pale orange grid lines, the purple shaded gradient that keeps its
   number faintly visible and shrinks the cell, and the green ring on a confirmed cell, all
   from hitori.html. The number sits in an <i> so it stays above the gradient overlay. */
.mini-hi {
  --u: 26px;
  display: grid;
  grid-template-columns: repeat(5, var(--u));
  grid-template-rows: repeat(5, var(--u));
  border-top: 2px solid #fed7aa;
  border-left: 2px solid #fed7aa;
  font-size: calc(var(--u) * .5);
  font-weight: 800;
  color: #2d1b4e;
}
.hi-c {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  border-right: 2px solid #fed7aa;
  border-bottom: 2px solid #fed7aa;
  background: #fff;
}
.hi-c i { position: relative; z-index: 1; font-style: normal; }
.hi-c::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, #7c3aed, #6d28d9);
  opacity: 0;
}

.game-card-thumb.play .hi-c.s::before { animation: hiIn .3s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .hi-c.s         { animation: hiShade .3s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .hi-c.m         { animation: hiMark .35s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .mini-hi        { animation: hiPop .5s var(--end) both; }

@keyframes hiIn    { to { opacity: 1; } }
@keyframes hiShade { to { color: rgba(255, 255, 255, .65); transform: scale(.94); } }
@keyframes hiMark  { to { background: #f0fdf4; box-shadow: inset 0 0 0 2px #22c55e; } }
@keyframes hiPop   { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.07); } }

/* mini: suko (su) — pastel colour groups, white cell gaps, and the four white circle clues
   ringed in dark purple sitting on the internal intersections, all from suko.html.
   --x/--y are which intersection (1 or 2), so the circles follow any --u change. */
.mini-su {
  --u: 40px; --gp: 5px;
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, var(--u));
  grid-template-rows: repeat(3, var(--u));
  gap: var(--gp);
  font-size: calc(var(--u) * .45);
  font-weight: 800;
}
.su-c {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  border: 2px solid rgba(255, 255, 255, .5);
  border-radius: 3px;
  color: #1e1145;
  font-weight: 900;
}
.su-c.q1 { background: #fbcfe8; }
.su-c.q2 { background: #bfdbfe; }
.su-c.q3 { background: #bbf7d0; }
.su-c.e { color: #7e22ce; font-weight: 800; }
.su-c.e::before { content: attr(data-v); transform: scale(0); }
.su-k {
  position: absolute;
  left: calc(var(--x) * (var(--u) + var(--gp)) - var(--gp) / 2);
  top: calc(var(--y) * (var(--u) + var(--gp)) - var(--gp) / 2);
  z-index: 3;
  width: calc(var(--u) * .6);
  height: calc(var(--u) * .6);
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: #fff;
  border: 3px solid #7e22ce;
  color: #7e22ce;
  font-size: calc(var(--u) * .28);
  font-weight: 900;
  box-shadow: 0 2px 8px rgba(126, 34, 206, .2);
  transform: translate(-50%, -50%);
}

.game-card-thumb.play .su-c.e::before { animation: suIn .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
.game-card-thumb.play .su-k           { animation: suMet .4s calc(var(--i) * 1s) both; }
.game-card-thumb.play .mini-su        { animation: suPop .5s var(--end) both; }

@keyframes suIn  { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes suMet {
  0%   { transform: translate(-50%, -50%) scale(1); border-color: #7e22ce; color: #7e22ce; }
  50%  { transform: translate(-50%, -50%) scale(1.25); }
  100% { transform: translate(-50%, -50%) scale(1); border-color: #16a34a; color: #16a34a; }
}
@keyframes suPop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.07); } }

/* mini: dots and boxes (db) — dot colour, the dashed undrawn-line hints and the pink and
   blue player colours with their 25% box fills and marks, all from dots-and-boxes.html.
   Lines draw themselves with stroke-dashoffset; --len is one line's length in viewBox
   units and is set by the builder so the two cannot drift apart. */
.mini-db { --u: 34px; line-height: 0; }
.db-svg { display: block; width: calc(var(--u) * 3.8); height: calc(var(--u) * 3.8); }
.db-hint { fill: none; stroke: #e2d5f0; stroke-width: 2; stroke-dasharray: 4 4; }
.db-d { fill: #2d1b4e; }
.db-l {
  fill: none;
  stroke-width: 4;
  stroke-linecap: round;
  stroke-dasharray: var(--len);
  stroke-dashoffset: var(--len);
}
.db-l.p1 { stroke: #ec4899; }
.db-l.p2 { stroke: #3b82f6; }
.db-b { opacity: 0; }
.db-b.p1 { fill: rgba(236, 72, 153, .25); }
.db-b.p2 { fill: rgba(59, 130, 246, .25); }
.db-m { font: 700 11px 'Segoe UI', system-ui, sans-serif; text-anchor: middle; opacity: 0; }
.db-m.p1 { fill: #ec4899; }
.db-m.p2 { fill: #3b82f6; }

.game-card-thumb.play .db-l { animation: dbDraw .2s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .db-b,
.game-card-thumb.play .db-m { animation: dbFill .3s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .mini-db { animation: dbPop .5s var(--end) both; }

@keyframes dbDraw { to { stroke-dashoffset: 0; } }
@keyframes dbFill { to { opacity: 1; } }
@keyframes dbPop  { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.06); } }

/* mini: super tic tac toe (st) — the sky-blue super board, pale blue mini boards, the
   yellow ring that marks the board you must play in next, and a won board taken over by its
   owner's colour with a big white mark, all from super-tic-tac-toe.html. The ring is what
   makes this game "super", so it gets its own element per activation and follows the play. */
.mini-st { --u: 13px; }
.st-super {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
  padding: 4px;
  background: linear-gradient(145deg, #0ea5e9, #0284c7);
  border-radius: 8px;
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, .15);
}
.st-m {
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, var(--u));
  grid-template-rows: repeat(3, var(--u));
  gap: 2px;
  padding: 2px;
  background: #c4dff6;
  border-radius: 4px;
}
.st-c {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  background: #fff;
  border-radius: 2px;
  font-weight: 900;
  font-size: calc(var(--u) * .72);
  line-height: 1;
}
.st-c.x { color: #ef4444; }
.st-c.o { color: #a16207; }
.st-c.p::before { content: attr(data-m); transform: scale(0); }
.st-a {
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: 4px;
  box-shadow: 0 0 0 2.5px #facc15, 0 0 10px rgba(250, 204, 21, .6);
  opacity: 0;
}
.st-w {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex; align-items: center; justify-content: center;
  border-radius: 4px;
  color: #fff;
  font-weight: 900;
  font-size: calc(var(--u) * 1.7);
  line-height: 1;
  text-shadow: 0 2px 6px rgba(0, 0, 0, .18);
  opacity: 0;
}
.st-w.x { background: #ef4444; }
.st-w.o { background: #facc15; }

.game-card-thumb.play .st-c.p::before { animation: stIn .25s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
.game-card-thumb.play .st-a           { animation: stAct .28s calc(var(--i) * 1s) both; }
.game-card-thumb.play .st-w           { animation: stWin .35s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
.game-card-thumb.play .st-super       { animation: stPop .5s var(--end) both; }

@keyframes stIn  { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes stAct { 0% { opacity: 0; } 20%, 80% { opacity: 1; } 100% { opacity: 0; } }
@keyframes stWin { from { opacity: 0; transform: scale(0); } to { opacity: 1; transform: scale(1); } }
@keyframes stPop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.06); } }

/* mini: reversi (rv) — green board, translucent cells and the purple and pink radial discs
   from reversi.html. A disc can flip more than once in a real game, so the builder writes a
   whole animation list into --anim: a place, then one flip per capture. The fills are
   `forwards` only, never `both`, so an upcoming flip cannot back-fill its colour over the
   disc's current one. The colour swaps while the disc is squeezed edge-on, which is what
   the game's rotateY flip looks like at this size. */
.mini-rv {
  --u: 15px;
  --d1: radial-gradient(circle at 35% 35%, #c4b5fd, #7c3aed 60%, #5b21b6);
  --d2: radial-gradient(circle at 35% 35%, #fbcfe8, #ec4899 60%, #be185d);
  display: grid;
  grid-template-columns: repeat(8, var(--u));
  grid-template-rows: repeat(8, var(--u));
  gap: 2px;
  padding: 5px;
  background: linear-gradient(145deg, #16a34a, #15803d);
  border-radius: 7px;
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, .12);
}
.rv-c {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  border-radius: 2px;
  background: rgba(255, 255, 255, .08);
}
.rv-d {
  position: absolute;
  width: 85%; height: 85%;
  border-radius: 50%;
  background: var(--bg);
  box-shadow: inset 0 -2px 4px rgba(0, 0, 0, .2), 0 1px 3px rgba(0, 0, 0, .15);
}
.rv-d.n { opacity: 0; }                 /* placed during play, so hidden until its turn */

.game-card-thumb.play .rv-d    { animation: var(--anim); }
.game-card-thumb.play .mini-rv { animation: rvPop .5s var(--end) both; }

@keyframes rvPlace { from { transform: scale(0); opacity: .5; } to { transform: scale(1); opacity: 1; } }
@keyframes rvTo1 {
  0%   { transform: scaleX(1); background: var(--d2); }
  49%  { transform: scaleX(.06); background: var(--d2); }
  51%  { transform: scaleX(.06); background: var(--d1); }
  100% { transform: scaleX(1); background: var(--d1); }
}
@keyframes rvTo2 {
  0%   { transform: scaleX(1); background: var(--d1); }
  49%  { transform: scaleX(.06); background: var(--d1); }
  51%  { transform: scaleX(.06); background: var(--d2); }
  100% { transform: scaleX(1); background: var(--d2); }
}
@keyframes rvPop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.06); } }

/* mini: futoshiki (fu) — cell border and fill, the peachy given, the orange player digit and
   the burnt-orange inequality signs, all from futoshiki.html, which lays the board out as
   cells alternating with narrower constraint tracks. Signs are given clues so they are there
   from frame one; the pulse as each pair fills is the card's own addition. */
.mini-fu {
  --u: 26px; --g: calc(var(--u) * .48);
  display: grid;
  grid-template-columns: repeat(3, var(--u) var(--g)) var(--u);
  grid-template-rows: repeat(3, var(--u) var(--g)) var(--u);
  font-weight: 800;
}
.fu-c {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  border: 2px solid #fdba74;
  border-radius: 4px;
  background: #fffbf5;
  font-size: calc(var(--u) * .54);
  color: #2d1b4e;
}
.fu-c.g { background: linear-gradient(135deg, #fff7ed, #fed7aa); }
.fu-c.e { color: #ea580c; }
.fu-c.e::before { content: attr(data-v); transform: scale(0); }
.fu-s {
  display: flex; align-items: center; justify-content: center;
  color: #c2410c;
  font-weight: 900;
  font-size: calc(var(--u) * .5);
  line-height: 1;
}

.game-card-thumb.play .fu-c.e::before { animation: fuIn .28s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
.game-card-thumb.play .fu-c.e         { animation: fuTap .4s calc(var(--i) * 1s) both; }
.game-card-thumb.play .fu-s           { animation: fuMet .4s calc(var(--i) * 1s) both; }
.game-card-thumb.play .mini-fu        { animation: fuPop .5s var(--end) both; }

@keyframes fuIn  { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes fuTap { from { background: #fff0d4; } to { background: #fffbf5; } }
@keyframes fuMet { 0%, 100% { transform: scale(1); } 45% { transform: scale(1.45); } }
@keyframes fuPop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.07); } }

/* mini: nurikabe (nu) — pale teal grid lines, the yellow clue cell and island fill and the
   blue sea, all from nurikabe.html. Islands fill first, each growing out from its number,
   then the sea floods around them, which is the order the puzzle is actually solved in. */
.mini-nu {
  --u: 24px;
  display: grid;
  grid-template-columns: repeat(6, var(--u));
  grid-template-rows: repeat(6, var(--u));
  border-top: 2px solid #99f6e4;
  border-left: 2px solid #99f6e4;
  font-size: calc(var(--u) * .52);
  font-weight: 800;
}
.nu-c {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  border-right: 2px solid #99f6e4;
  border-bottom: 2px solid #99f6e4;
  background: #fff;
}
.nu-c.k {
  background: #fef9c3;
  color: #92400e;
  font-weight: 900;
  box-shadow: inset 0 0 0 2px #fbbf24;
}
.nu-c::before { content: ''; position: absolute; inset: 0; opacity: 0; }
.nu-c.i::before { background: linear-gradient(135deg, #fef08a, #fde047); box-shadow: inset 0 0 0 2px #f59e0b; }
.nu-c.w::before { background: linear-gradient(135deg, #38bdf8, #0ea5e9); box-shadow: inset 0 0 0 2px #0284c7; }

.game-card-thumb.play .nu-c.i::before,
.game-card-thumb.play .nu-c.w::before { animation: nuIn .25s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .mini-nu        { animation: nuPop .5s var(--end) both; }

@keyframes nuIn  { to { opacity: 1; } }
@keyframes nuPop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.07); } }

/* mini: masyu (ma) — faint grey lattice, the blue loop and the gradient black and white
   pearls, all from masyu.html. Same drawn-loop mechanism as Slitherlink, which is why this
   block is short. Each pearl pulses as the loop reaches it; the game does not do that, it
   is the card's way of showing that the loop has to visit every pearl. */
.mini-ma { --u: 26px; line-height: 0; }
.ma-svg { display: block; width: calc(var(--u) * 5.4); height: calc(var(--u) * 5.4); }
.ma-g { fill: none; stroke: #e2e8f0; stroke-width: 1; }
.ma-d { fill: #cbd5e1; }
.ma-l {
  fill: none;
  stroke: #2563eb;
  stroke-width: 3.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: var(--len);
  stroke-dashoffset: var(--len);
}
.ma-p { transform-box: fill-box; transform-origin: center; }
.ma-p.w { fill: url(#maW); stroke: #64748b; stroke-width: 1.6; }
.ma-p.b { fill: url(#maB); stroke: #0f172a; stroke-width: 1.3; }

.game-card-thumb.play .ma-l    { animation: maDraw 2.7s ease-in-out both; }
.game-card-thumb.play .ma-p    { animation: maHit .4s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .mini-ma { animation: maPop .5s var(--end) both; }

@keyframes maDraw { to { stroke-dashoffset: 0; } }
@keyframes maHit  { 0%, 100% { transform: scale(1); } 45% { transform: scale(1.32); } }
@keyframes maPop  { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.07); } }

/* mini: queens (qu) — the game's pastel region palette, its thick region borders and its
   crown pop, from queens.html. A region brightens as its crown lands, which is the card's
   way of showing the one-crown-per-region rule; the game only brightens on hover. */
.mini-qu {
  --u: 24px;
  display: grid;
  grid-template-columns: repeat(6, var(--u));
  grid-template-rows: repeat(6, var(--u));
  border: 2.5px solid rgba(0, 0, 0, .45);
}
.qu-c {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(0, 0, 0, .1);
  background: var(--rg);
  font-size: calc(var(--u) * .68);
  line-height: 1;
}
.qu-c.bt { border-top: 2.5px solid rgba(0, 0, 0, .45); }
.qu-c.bb { border-bottom: 2.5px solid rgba(0, 0, 0, .45); }
.qu-c.bl { border-left: 2.5px solid rgba(0, 0, 0, .45); }
.qu-c.br { border-right: 2.5px solid rgba(0, 0, 0, .45); }
.qu-c i {
  font-style: normal;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .35));
  transform: scale(0);
}

.game-card-thumb.play .qu-c i  { animation: quPop .3s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .qu-c.f  { animation: quLit .5s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .mini-qu { animation: quBoard .5s var(--end) both; }

@keyframes quPop   { 0% { transform: scale(0); } 60% { transform: scale(1.25); } 100% { transform: scale(1); } }
@keyframes quLit   { 0%, 100% { filter: none; } 40% { filter: brightness(1.18); } }
@keyframes quBoard { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.07); } }

/* mini: gomoku (gm) — brown board, amber cells and the purple and orange radial stones,
   all from gomoku.html, including its place curve and its winning-line pulse. */
.mini-gm {
  --u: 13px;
  display: grid;
  grid-template-columns: repeat(9, var(--u));
  grid-template-rows: repeat(9, var(--u));
  gap: 2px;
  padding: 6px;
  background: #a16207;
  border-radius: 7px;
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, .15);
}
.gm-c {
  position: relative;
  background: linear-gradient(145deg, #fbbf24, #f59e0b);
  border-radius: 2px;
  box-shadow: inset 0 0 0 1px rgba(120, 53, 15, .25);
}
.gm-c.p1::after, .gm-c.p2::after {
  content: '';
  position: absolute;
  top: 8%; left: 8%; width: 84%; height: 84%;
  border-radius: 50%;
  box-shadow: inset 0 -1px 3px rgba(0, 0, 0, .25), 0 1px 2px rgba(0, 0, 0, .2);
  transform: scale(0);
}
.gm-c.p1::after { background: radial-gradient(circle at 35% 35%, #a78bfa, #7c3aed 60%, #5b21b6); }
.gm-c.p2::after { background: radial-gradient(circle at 35% 35%, #fdba74, #f97316 60%, #c2410c); }

.game-card-thumb.play .gm-c.p1::after,
.game-card-thumb.play .gm-c.p2::after { animation: gmPlace .28s cubic-bezier(.25, .1, .25, 1.3) calc(var(--i) * 1s) both; }
/* A winning stone is also .p1, so this must stay after that rule to add the pulse. */
.game-card-thumb.play .gm-c.w::after {
  animation: gmPlace .28s cubic-bezier(.25, .1, .25, 1.3) calc(var(--i) * 1s) both,
             gmWin .45s ease-in-out var(--end) 2;
}
.game-card-thumb.play .mini-gm { animation: gmPop .5s var(--end) both; }

@keyframes gmPlace { from { transform: scale(0); opacity: .5; } to { transform: scale(1); opacity: 1; } }
@keyframes gmWin   { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.16); } }
@keyframes gmPop   { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.06); } }

/* mini: hashi (ha) — pale lattice dots, purple single and darker double bridges, and the
   amber islands that turn green once their bridge count is met, all from hashi.html. The
   green is the game's own satisfied state, not an invention. Bridges draw themselves with
   stroke-dashoffset, the third card on that mechanism after Slitherlink and Masyu. */
.mini-ha { --u: 26px; line-height: 0; }
.ha-svg { display: block; width: calc(var(--u) * 5.4); height: calc(var(--u) * 5.4); }
.ha-d { fill: #f3e8ff; }
.ha-b {
  fill: none;
  stroke-linecap: round;
  stroke-dasharray: var(--len);
  stroke-dashoffset: var(--len);
}
.ha-b.s { stroke: #a855f7; stroke-width: 3; }
.ha-b.d { stroke: #7c3aed; stroke-width: 2.5; }
.ha-i { fill: url(#haI); stroke: #d97706; stroke-width: 2; }
.ha-i.ok { fill: url(#haOk); stroke: #16a34a; }
.ha-n { font: 900 11px 'Segoe UI', system-ui, sans-serif; text-anchor: middle; fill: #fff; }

.game-card-thumb.play .ha-b    { animation: haDraw .25s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .ha-i.ok { animation: haOk .35s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .mini-ha { animation: haPop .5s var(--end) both; }

@keyframes haDraw { to { stroke-dashoffset: 0; } }
@keyframes haOk   { from { opacity: 0; } to { opacity: 1; } }
@keyframes haPop  { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.06); } }

/* mini: light up (lu) — the dark wall gradient with its white number, the pale yellow lit
   cell and the brighter bulb cell, all from light-up.html. Bulbs pop in and their beams
   light outward cell by cell, so you can see each bulb's reach. */
.mini-lu {
  --u: 24px;
  display: grid;
  grid-template-columns: repeat(6, var(--u));
  grid-template-rows: repeat(6, var(--u));
  border-top: 2px solid #e5e7eb;
  border-left: 2px solid #e5e7eb;
  font-weight: 800;
}
.lu-c {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  border-right: 2px solid #e5e7eb;
  border-bottom: 2px solid #e5e7eb;
  background: #fff;
}
.lu-c.k {
  background: linear-gradient(145deg, #374151, #1f2937);
  border-color: #111827;
  color: #fff;
  font-weight: 900;
  font-size: calc(var(--u) * .46);
}
.lu-c.l::before, .lu-c.b::before { content: ''; position: absolute; inset: 0; opacity: 0; }
.lu-c.l::before { background: #fef9c3; }
.lu-c.b::before { background: linear-gradient(135deg, #fef08a, #fde047); box-shadow: inset 0 0 0 2px #f59e0b; }
.lu-c i {
  position: relative;
  z-index: 1;
  font-style: normal;
  font-size: calc(var(--u) * .58);
  line-height: 1;
  transform: scale(0);
}

.game-card-thumb.play .lu-c.l::before,
.game-card-thumb.play .lu-c.b::before { animation: luOn .2s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .lu-c i         { animation: luBulb .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
.game-card-thumb.play .mini-lu        { animation: luPop .5s var(--end) both; }

@keyframes luOn   { to { opacity: 1; } }
@keyframes luBulb { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes luPop  { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.07); } }

/* mini: binary puzzle (bi) — pale teal cell borders, pink 0s and blue 1s with the deeper
   shades the game uses for givens, all from binary.html. */
.mini-bi {
  --u: 24px;
  display: grid;
  grid-template-columns: repeat(6, var(--u));
  grid-template-rows: repeat(6, var(--u));
  gap: 2px;
  font-size: calc(var(--u) * .6);
  font-weight: 900;
}
.bi-c {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  border: 2px solid #ccfbf1;
  border-radius: 3px;
  background: #fff;
}
/* The colour sits on a ::before so an entered cell can fade from blank to pink or blue,
   while a given is simply there at full strength from the start. */
.bi-c::before { content: ''; position: absolute; inset: -2px; border-radius: 3px; }
.bi-c.z { color: #be185d; }
.bi-c.o { color: #1d4ed8; }
.bi-c.z::before { background: linear-gradient(135deg, #fce7f3, #fbcfe8); box-shadow: inset 0 0 0 2px #f9a8d4; }
.bi-c.o::before { background: linear-gradient(135deg, #dbeafe, #bfdbfe); box-shadow: inset 0 0 0 2px #93c5fd; }
.bi-c.g.z { color: #9d174d; }
.bi-c.g.o { color: #1e3a8a; }
.bi-c.g.z::before { background: linear-gradient(135deg, #fbb5d5, #f9a8d4); }
.bi-c.g.o::before { background: linear-gradient(135deg, #a5c8fd, #93c5fd); }
.bi-c.e::before { opacity: 0; }
.bi-c i { position: relative; z-index: 1; font-style: normal; }
.bi-c.e i { transform: scale(0); }

.game-card-thumb.play .bi-c.e::before { animation: biOn .3s ease-out calc(var(--i) * 1s) both; }
.game-card-thumb.play .bi-c.e i       { animation: biIn .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
.game-card-thumb.play .mini-bi        { animation: biPop .5s var(--end) both; }

@keyframes biOn  { to { opacity: 1; } }
@keyframes biIn  { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes biPop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.07); } }

/* mini: shikaku (sk2) — pale pink grid lines, the game's rectangle palette at its own 55%
   opacity, and the numbers sitting above the overlays, all from shikaku.html. Rectangles are
   absolutely positioned over the grid, sized in whole cells from --u. */
.mini-sk2 {
  --u: 24px;
  position: relative;
  display: grid;
  grid-template-columns: repeat(6, var(--u));
  grid-template-rows: repeat(6, var(--u));
  border-top: 1px solid #f0d0e6;
  border-left: 1px solid #f0d0e6;
  font-size: calc(var(--u) * .5);
  font-weight: 800;
  color: #2d1b4e;
}
.sk2-c {
  position: relative;
  z-index: 2;
  display: flex; align-items: center; justify-content: center;
  border-right: 1px solid #f0d0e6;
  border-bottom: 1px solid #f0d0e6;
}
.sk2-r {
  position: absolute;
  z-index: 1;
  left: calc(var(--x) * var(--u));
  top: calc(var(--y) * var(--u));
  width: calc(var(--w) * var(--u));
  height: calc(var(--h) * var(--u));
  border-radius: 6px;
  background: var(--rc);
  opacity: 0;
}

.game-card-thumb.play .sk2-r    { animation: sk2In .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
.game-card-thumb.play .mini-sk2 { animation: sk2Pop .5s var(--end) both; }

@keyframes sk2In  { from { opacity: 0; transform: scale(.7); } to { opacity: .55; transform: scale(1); } }
@keyframes sk2Pop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.07); } }

/* mini: ataxx (ax) — indigo board, translucent cells, dark walls and the yellow and red
   radial pieces, all from ataxx.html, including its spinning capture. Like Reversi, a piece
   here can change hands several times, so the builder writes an animation list into --anim
   and the fills are `forwards` only so a later capture cannot back-fill its colour. */
.mini-ax {
  --u: 15px;
  --a1: radial-gradient(circle at 35% 35%, #fef9c3, #facc15 60%, #eab308);
  --a2: radial-gradient(circle at 35% 35%, #fecaca, #f87171 60%, #ef4444);
  display: grid;
  grid-template-columns: repeat(7, var(--u));
  grid-template-rows: repeat(7, var(--u));
  gap: 3px;
  padding: 6px;
  background: linear-gradient(145deg, #6366f1, #4f46e5);
  border-radius: 7px;
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, .12);
}
.ax-c {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  border-radius: 3px;
  background: rgba(255, 255, 255, .1);
}
.ax-c.w { background: #1e293b; border-radius: 2px; }
.ax-p {
  position: absolute;
  width: 82%; height: 82%;
  border-radius: 50%;
  background: var(--bg);
  box-shadow: inset 0 -2px 4px rgba(0, 0, 0, .15), 0 1px 3px rgba(0, 0, 0, .15);
}
.ax-p.n { opacity: 0; }                 /* arrives during play, so hidden until its move */

.game-card-thumb.play .ax-p    { animation: var(--anim); }
.game-card-thumb.play .mini-ax { animation: axPop .5s var(--end) both; }

@keyframes axIn  { from { transform: scale(0); opacity: .5; } to { transform: scale(1); opacity: 1; } }
@keyframes axOut { from { transform: scale(1); opacity: 1; } to { transform: scale(0); opacity: 0; } }
@keyframes axTo1 {
  0%   { transform: scale(1) rotate(0deg); background: var(--a2); }
  49%  { background: var(--a2); }
  50%  { transform: scale(1.3) rotate(180deg); }
  51%  { background: var(--a1); }
  100% { transform: scale(1) rotate(360deg); background: var(--a1); }
}
@keyframes axTo2 {
  0%   { transform: scale(1) rotate(0deg); background: var(--a1); }
  49%  { background: var(--a1); }
  50%  { transform: scale(1.3) rotate(180deg); }
  51%  { background: var(--a2); }
  100% { transform: scale(1) rotate(360deg); background: var(--a2); }
}
@keyframes axPop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.06); } }

/* mini: hex (hx) — the rhombus of pointy-top hexagons, the pale empty fill and the purple
   and pink players, all from hex.html, plus the coloured edges that say which two sides
   belong to whom. Without those edges a Hex board is unreadable, so they are not decoration. */
.mini-hx { --u: 14px; line-height: 0; }
.hx-svg { display: block; width: calc(var(--u) * 10.6); height: auto; }
.hx-h { fill: #e0e7ff; stroke: #94a3b8; stroke-width: 1.5; transform-box: fill-box; transform-origin: center; }
.hx-h.p1 { fill: #7c3aed; stroke: #5b21b6; stroke-width: 2; }
.hx-h.p2 { fill: #ec4899; stroke: #be185d; stroke-width: 2; }
.hx-h.n { opacity: 0; }
.hx-e { fill: none; stroke-width: 3.5; stroke-linecap: round; }
.hx-e.p1 { stroke: #7c3aed; }
.hx-e.p2 { stroke: #ec4899; }

.game-card-thumb.play .hx-h.n { animation: hxIn .3s cubic-bezier(.25, .1, .25, 1.4) calc(var(--i) * 1s) both; }
/* A winning hex is also .n, so this must stay after that rule to add the pulse. */
.game-card-thumb.play .hx-h.w {
  animation: hxIn .3s cubic-bezier(.25, .1, .25, 1.4) calc(var(--i) * 1s) both,
             hxWin .45s ease-in-out var(--end) 2;
}
.game-card-thumb.play .mini-hx { animation: hxPop .5s var(--end) both; }

@keyframes hxIn  { from { opacity: 0; transform: scale(0); } to { opacity: 1; transform: scale(1); } }
@keyframes hxWin { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.18); } }
@keyframes hxPop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.06); } }

/* mini: kenken (kk) — pale teal cell lines, the dark teal cage borders, the small cage label
   in its top-left corner and the teal player digit, all from kenken.html. Cage borders are
   inset box-shadows rather than thicker borders, so nothing shifts the grid. */
.mini-kk {
  --u: 30px;
  display: grid;
  grid-template-columns: repeat(4, var(--u));
  grid-template-rows: repeat(4, var(--u));
  border: 2.5px solid #134e4a;
  font-size: calc(var(--u) * .5);
  font-weight: 800;
}
.kk-c {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  border-right: 1px solid #ccf0eb;
  border-bottom: 1px solid #ccf0eb;
  background: #fff;
  color: #0d9488;
}
.kk-c.br { box-shadow: inset -2.5px 0 0 #134e4a; }
.kk-c.bb { box-shadow: inset 0 -2.5px 0 #134e4a; }
.kk-c.br.bb { box-shadow: inset -2.5px 0 0 #134e4a, inset 0 -2.5px 0 #134e4a; }
.kk-c b {
  position: absolute;
  top: 1px; left: 3px;
  font-size: calc(var(--u) * .3);
  font-weight: 800;
  color: #0f766e;
  line-height: 1;
}
.kk-c::before { content: attr(data-v); transform: scale(0); }

.game-card-thumb.play .kk-c::before { animation: kkIn .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
.game-card-thumb.play .mini-kk      { animation: kkPop .5s var(--end) both; }

@keyframes kkIn  { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes kkPop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.07); } }

/* mini: circle sudoku (cs) — four rings, four 90-degree wedges, the cream hub and the
   orange ring and wedge borders, with every digit coloured by its value, which is what
   circle-sudoku.html does to givens and player digits alike. Drawn in the game's own
   400-unit space, so the radii in the builder are the radii the game uses. */
/* A round panel: the square one wasted its corners and cost the board 14px of diameter. */
.mini-cs { --u: 40px; line-height: 0; border-radius: 50%; padding: 4px; }
.cs-svg { display: block; width: calc(var(--u) * 4); height: auto; }
.cs-hub { fill: #fff7ed; stroke: #ea580c; stroke-width: 2.5; }
.cs-c { fill: #fff; stroke: #fdba74; stroke-width: 1.5; }
/* The game's same-number tint: its own given colour vanishes at thumbnail size. */
.cs-c.g { fill: #fff1e6; }
.cs-f { fill: #f97316; opacity: 0; }                /* the outer ring's flash */
.cs-r { fill: none; stroke: #ea580c; stroke-width: 3; }
.cs-w { stroke: #ea580c; stroke-width: 2.5; }
/* Twice the game's 18 units, because a ring is only 15px thick on a thumbnail. */
.cs-n {
  font: 800 36px 'Segoe UI', system-ui, sans-serif;
  text-anchor: middle;
  dominant-baseline: central;
  transform-box: fill-box;
  transform-origin: center;
}
.cs-n.g { font-weight: 900; }
.cs-n.n1 { fill: #e53e3e; }
.cs-n.n2 { fill: #3182ce; }
.cs-n.n3 { fill: #38a169; }
.cs-n.n4 { fill: #d69e2e; }
.cs-n.p { opacity: 0; }                             /* a digit the solve fills in */

.game-card-thumb.play .cs-n.p   { animation: csIn .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
.game-card-thumb.play .cs-f     { animation: csGlow .6s ease-out var(--flash) forwards; }
.game-card-thumb.play .mini-cs  { animation: csPop .55s var(--end) both; }

@keyframes csIn   { from { opacity: 0; transform: scale(.4); } to { opacity: 1; transform: scale(1); } }
@keyframes csGlow { 0%, 100% { opacity: 0; } 45% { opacity: .5; } }
@keyframes csPop  { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.06); } }

/* mini: star battle (sb) — the game's pastel region palette, its region borders and its
   star pop, from star-battle.html, in its two-star mode on 9x9. One-star mode would be the
   Queens card again with stars for crowns. Region borders are thinner than the game's
   2.5px, because two of them meet at every boundary and the cells here are only 16px. */
.mini-sb {
  --u: 16px;
  display: grid;
  grid-template-columns: repeat(9, var(--u));
  grid-template-rows: repeat(9, var(--u));
  border: 2.5px solid rgba(0, 0, 0, .45);
}
.sb-c {
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(0, 0, 0, .1);
  background: var(--rg);
  font-size: calc(var(--u) * .78);
  line-height: 1;
}
.sb-c.bt { border-top: 1.5px solid rgba(0, 0, 0, .45); }
.sb-c.bb { border-bottom: 1.5px solid rgba(0, 0, 0, .45); }
.sb-c.bl { border-left: 1.5px solid rgba(0, 0, 0, .45); }
.sb-c.br { border-right: 1.5px solid rgba(0, 0, 0, .45); }
.sb-c i {
  font-style: normal;
  filter: drop-shadow(0 1px 1.5px rgba(0, 0, 0, .35));
  transform: scale(0);
}

/* The twinkle fills forwards only. With "both", its first frame would apply during its
   delay and override the pop's scale(0), showing every star before any has landed. */
.game-card-thumb.play .sb-c i  {
  animation: sbPop .3s ease-out calc(var(--i) * 1s) both,
             sbTwinkle .6s ease-in-out var(--tw) forwards;
}
.game-card-thumb.play .mini-sb { animation: sbBoard .55s var(--end) both; }

@keyframes sbPop     { 0% { transform: scale(0); } 60% { transform: scale(1.25); } 100% { transform: scale(1); } }
@keyframes sbTwinkle { 0%, 100% { transform: scale(1) rotate(0); } 50% { transform: scale(1.3) rotate(18deg); } }
@keyframes sbBoard   { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.06); } }

/* mini: suguru (sr) — the game's pastel block colours chosen by its own greedy rule, its
   dark block outlines, bold dark givens and indigo player digits, from suguru.html. */
.mini-sr {
  --u: 24px;
  position: relative;
  display: grid;
  grid-template-columns: repeat(6, var(--u));
  grid-template-rows: repeat(6, var(--u));
  border: 2px solid #1e1b4b;
  font-size: calc(var(--u) * .58);
}
/* isolation makes each cell its own stacking context, so the flash can sit between the
   pastel background and the digit instead of over the digit */
.sr-c {
  position: relative;
  isolation: isolate;
  display: flex; align-items: center; justify-content: center;
  border-right: 1px solid #dbe1ff;
  border-bottom: 1px solid #dbe1ff;
  background: var(--bk);
  color: #1e1b4b;
  font-weight: 900;
  line-height: 1;
}
.sr-c:nth-child(6n) { border-right: 0; }
.sr-c:nth-child(n+31) { border-bottom: 0; }
/* Block outlines are one SVG path over the grid. Per-cell thick borders left notches
   wherever a thick side met a thin one; the game avoids that with a collapsed table. */
.sr-o { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.sr-o path { fill: none; stroke: #1e1b4b; stroke-width: 2px; stroke-linecap: square; vector-effect: non-scaling-stroke; }
.sr-c.p { color: #4338ca; font-weight: 800; }
.sr-c.p::before { content: attr(data-v); transform: scale(0); }
.sr-c.l::after { content: ''; position: absolute; inset: 0; z-index: -1; background: #6366f1; opacity: 0; }

.game-card-thumb.play .sr-c.p::before { animation: srIn .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
.game-card-thumb.play .sr-c.l::after  { animation: srFlash .65s ease-out var(--fl) forwards; }
.game-card-thumb.play .mini-sr        { animation: srPop .55s var(--end) both; }

@keyframes srIn    { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes srFlash { 0%, 100% { opacity: 0; } 40% { opacity: .35; } }
@keyframes srPop   { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.06); } }

/* mini: nine and twelve men's morris (mm) — the board both pages draw: cream ground, three
   amber rings, brown lines and points, and the blue and orange men. Twelve adds diagonals
   through the corners; everything else is shared. */
.mini-mm { --u: 16px; line-height: 0; }
.mm-svg { display: block; width: calc(var(--u) * 10); height: auto; }
.mm-r { opacity: .35; stroke: none; }
.mm-l { stroke: #92400e; stroke-width: 3; stroke-linecap: round; opacity: .6; }
.mm-d { fill: #92400e; }
.mm-p { transform-box: fill-box; transform-origin: center; transform: scale(0); }
/* the gradient ids carry the card's name, so the builder passes them in */
.mm-p.p1 { fill: var(--g1); }
.mm-p.p2 { fill: var(--g2); }
/* the finished mill, lit under the three men that make it */
.mm-m { stroke: #2563eb; stroke-width: 12; stroke-linecap: round; opacity: 0; }

.game-card-thumb.play .mm-p { animation: mmIn .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
/* The captured man lands like the others, then goes when the mill takes it. The second
   animation fills forwards only, so it cannot mask the arrival during its delay. */
.game-card-thumb.play .mm-p.x {
  animation: mmIn .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both,
             mmOut .35s ease-in var(--cap) forwards;
}
/* The three men of the mill swell as it completes. The line alone reads poorly, because
   the men cover all but the gaps between them. */
.game-card-thumb.play .mm-p.m {
  animation: mmIn .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both,
             mmCheer .55s ease-out var(--mill) forwards;
}
.game-card-thumb.play .mm-m    { animation: mmMill .5s ease-out var(--mill) forwards; }
.game-card-thumb.play .mini-mm { animation: mmPop .5s var(--end) both; }

@keyframes mmIn   { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes mmOut  { from { transform: scale(1); opacity: 1; } to { transform: scale(0); opacity: 0; } }
@keyframes mmMill  { from { opacity: 0; } to { opacity: .6; } }
@keyframes mmCheer { 0%, 100% { transform: scale(1); } 45% { transform: scale(1.22); } }
@keyframes mmPop  { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.05); } }

/* mini: jigsaw sudoku (jg) — the game's region palette and its dark region outlines, with
   near-black givens and cyan player digits. Six irregular regions rather than boxes, which
   is the whole point of the game, so the outlines are an SVG path as in Suguru. */
.mini-jg {
  --u: 24px;
  position: relative;
  display: grid;
  grid-template-columns: repeat(6, var(--u));
  grid-template-rows: repeat(6, var(--u));
  border: 2px solid #083344;
  font-size: calc(var(--u) * .55);
}
.jg-c {
  position: relative;
  isolation: isolate;
  display: flex; align-items: center; justify-content: center;
  border-right: 1px solid rgba(8, 51, 68, .28);
  border-bottom: 1px solid rgba(8, 51, 68, .28);
  background: var(--rg);
  color: #083344;
  font-weight: 800;
  line-height: 1;
}
.jg-c:nth-child(6n) { border-right: 0; }
.jg-c:nth-child(n+31) { border-bottom: 0; }
.jg-o { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.jg-o path { fill: none; stroke: #083344; stroke-width: 2px; stroke-linecap: square; vector-effect: non-scaling-stroke; }
.jg-c.p { color: #0891b2; }
.jg-c.p::before { content: attr(data-v); transform: scale(0); }
/* Each region lights in turn once the grid is full: the card's way of saying that every one
   of them holds 1 to 6. Suguru flashes only its last block, so the two read apart. The wash
   is white, not the accent: a cyan one turned the orange regions brown on its way past. */
.jg-c::after { content: ''; position: absolute; inset: 0; z-index: -1; background: #fff; opacity: 0; }

.game-card-thumb.play .jg-c.p::before { animation: jgIn .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
.game-card-thumb.play .jg-c::after    { animation: jgLit .5s ease-out calc(var(--fl) + var(--g) * .07s) forwards; }
.game-card-thumb.play .mini-jg        { animation: jgPop .55s var(--end) both; }

@keyframes jgIn  { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes jgLit { 0%, 100% { opacity: 0; } 45% { opacity: .65; } }
@keyframes jgPop { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.06); } }

/* mini: killer sudoku (kl) — the game's cage palette, its dark box lines and the small
   magenta cage sum in a corner, from killer-sudoku.html. The grid starts completely empty,
   which is the game: the sums carry all the information. */
.mini-kl {
  --u: 32px;
  display: grid;
  grid-template-columns: repeat(4, var(--u));
  grid-template-rows: repeat(4, var(--u));
  border: 2.5px solid #4a044e;
  font-size: calc(var(--u) * .46);
  font-weight: 800;
}
.kl-c {
  position: relative;
  isolation: isolate;
  display: flex; align-items: center; justify-content: center;
  padding-top: calc(var(--u) * .12);
  border-right: 1px solid rgba(74, 4, 78, .26);
  border-bottom: 1px solid rgba(74, 4, 78, .26);
  background: var(--cg);
  color: #86198f;
}
.kl-c:nth-child(4n) { border-right: 0; }
.kl-c:nth-child(n+13) { border-bottom: 0; }
/* the 2x2 box lines, drawn once by the cell on the left or above, as KenKen does */
.kl-c.br { box-shadow: inset -2.5px 0 0 #4a044e; }
.kl-c.bb { box-shadow: inset 0 -2.5px 0 #4a044e; }
.kl-c.br.bb { box-shadow: inset -2.5px 0 0 #4a044e, inset 0 -2.5px 0 #4a044e; }
.kl-c b {
  position: absolute;
  top: 0; left: 2px;
  font-size: calc(var(--u) * .3);
  font-weight: 900;
  line-height: 1.1;
}
.kl-c::before { content: attr(data-v); transform: scale(0); }
/* the finished cage: its sum ticks as the last digit lands */
.kl-c.f b { transform-origin: left center; }

.game-card-thumb.play .kl-c::before { animation: klIn .3s cubic-bezier(.34, 1.56, .64, 1) calc(var(--i) * 1s) both; }
.game-card-thumb.play .kl-c.f b     { animation: klTick .5s ease-out var(--tick) forwards; }
.game-card-thumb.play .mini-kl      { animation: klPop .55s var(--end) both; }

@keyframes klIn   { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes klTick { 0%, 100% { transform: scale(1); color: #86198f; } 45% { transform: scale(1.5); color: #22c55e; } }
@keyframes klPop  { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.06); } }

/* mini: drop merge (dm) — the game's green tile ladder, its dashed empty slots and its
   merge pop, from drop-merge.html. Nothing slides sideways here: a tile falls down one
   column and merges only with the tile directly beneath it. */
.mini-dm {
  --t: 30px; --g: 4px; --st: calc(var(--t) + var(--g));
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, var(--t));
  grid-template-rows: repeat(5, var(--t));
  gap: var(--g);
  padding: var(--g);
  /* White with grey dashed slots, as the game has it. A mint board matched the 2 tile's
     own colour exactly and swallowed every 2 on the board. */
  background: #fff;
  border-radius: 7px;
}
.dm-s { border: 2px dashed #d1d5db; border-radius: .4rem; }
/* Tiles are placed by row and column index, so falling is a change of --r. */
.dm-t {
  position: absolute;
  width: var(--t); height: var(--t);
  left: calc(var(--g) + var(--c) * var(--st));
  top: calc(var(--g) + var(--r) * var(--st));
  display: flex; align-items: center; justify-content: center;
  border-radius: .4rem;
  font-size: calc(var(--t) * .38);
  font-weight: 900;
  background: var(--bg);
  color: var(--fg);
}
.dm-t.small { font-size: calc(var(--t) * .3); }

/* The dropped tile falls, then each merge result pops in as the pair below it vanishes.
   A tile can be two of these at once: the dropped one falls and is then eaten, and every
   merge result but the last is made and later eaten. Both have to sit in one animation
   list, because a second `animation` declaration would replace the first rather than add
   to it. --i times the arrival, --e the moment the tile is eaten. */
.game-card-thumb.play .dm-t.fall  { animation: dmFall var(--fd) cubic-bezier(.45, 0, 1, 1) var(--i) both; }
.game-card-thumb.play .dm-t.made  { animation: dmMade .28s ease-out var(--i) both; }
.game-card-thumb.play .dm-t.eaten { animation: dmEaten .18s ease-in var(--e) forwards; }
.game-card-thumb.play .dm-t.fall.eaten {
  animation: dmFall var(--fd) cubic-bezier(.45, 0, 1, 1) var(--i) both,
             dmEaten .18s ease-in var(--e) forwards;
}
.game-card-thumb.play .dm-t.made.eaten {
  animation: dmMade .28s ease-out var(--i) both,
             dmEaten .18s ease-in var(--e) forwards;
}
.game-card-thumb.play .mini-dm    { animation: dmPop .55s var(--end) both; }

@keyframes dmFall  { from { top: calc(var(--g) + var(--r0) * var(--st)); } to { top: calc(var(--g) + var(--r) * var(--st)); } }
@keyframes dmMade  { 0% { opacity: 0; transform: scale(.5); } 60% { opacity: 1; transform: scale(1.18); } 100% { opacity: 1; transform: scale(1); } }
@keyframes dmEaten { to { opacity: 0; transform: scale(.6); } }
@keyframes dmPop   { 0%, 100% { transform: scale(1); } 40% { transform: scale(1.06); } }

/* Phone board sizes. The thumb's own height stays in index.html with the card layout. */
@media (max-width: 640px) {
  .mini { --u: 22px; }
  .mini-sky { --u: 24px; }
  .mini-sudo { --u: 15px; }
  .mini-kaku { --u: 25px; }
  .mini-code { --p: calc(var(--u) * .86); }
  .mini-2048 { --t: 28px; }
  .mini-bs { --u: 18px; }
  .mini-mj { --tw: 27px; --th: 32px; }
  .mini-sl { --u: 23px; }
  .mini-fr { --u: 16px; }
  .mini-ms { --u: 19px; }
  .mini-tt { --u: 21px; }
  .mini-sg { --u: 18px; }
  .mini-s4 { --u: 26px; }
  .mini-s6 { --u: 20px; }
  .mini-hi { --u: 22px; }
  .mini-su { --u: 34px; }
  .mini-db { --u: 29px; }
  .mini-st { --u: 11px; }
  .mini-rv { --u: 13px; }
  .mini-fu { --u: 22px; }
  .mini-nu { --u: 20px; }
  .mini-ma { --u: 23px; }
  .mini-qu { --u: 20px; }
  .mini-gm { --u: 11px; }
  .mini-ha { --u: 23px; }
  .mini-lu { --u: 20px; }
  .mini-bi { --u: 20px; }
  .mini-sk2 { --u: 20px; }
  .mini-ax { --u: 13px; }
  .mini-hx { --u: 12px; }
  .mini-kk { --u: 26px; }
  .mini-cs { --u: 37px; }
  .mini-sb { --u: 14px; }
  .mini-sr { --u: 21px; }
  .mini-mm { --u: 14px; }
  .mini-jg { --u: 21px; }
  .mini-kl { --u: 28px; }
  .mini-dm { --t: 26px; }
}
