/* Sacrifice effects (fx.js): the reveal on the alternate's card, and the swap and burn over the page.
   Every effect ends early on a tap, and has a still version for devices that ask for less motion (.calm). */

/* ---------------------------------------------------------------- the overlay */
.fx { position: fixed; inset: 0; z-index: 80; display: grid; place-items: center; align-content: center; gap: 26px; padding: 16px; cursor: pointer;
  background: rgba(2, 4, 8, .93); animation: fx-in .3s ease both; -webkit-tap-highlight-color: transparent; }
.fx.out { animation: fx-out .38s ease forwards; }
.fx-burn { background: radial-gradient(55% 45% at 50% 55%, rgba(255, 96, 24, .26), transparent 70%), rgba(2, 4, 8, .93); }
.fx-swap { background: radial-gradient(55% 45% at 50% 50%, rgba(255, 8, 203, .2), transparent 70%), radial-gradient(80% 60% at 50% 60%, rgba(107, 43, 217, .22), transparent 70%), rgba(2, 4, 8, .93); }
.fx-canvas { position: fixed; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 2; }
.fx-stage { position: relative; z-index: 1; width: min(300px, 68vw); aspect-ratio: 1; }
.fx-card { position: absolute; inset: 0; }
.fx-card .nft { width: 100%; height: 100%; border: 5px solid var(--fg); border-radius: 22px; transform: rotate(-2deg); box-shadow: 0 30px 60px -24px rgba(0, 0, 0, .8); }
.fx-cap { position: relative; z-index: 3; margin: 0; max-width: 90vw; text-align: center; font: 400 clamp(22px, 5vw, 30px)/1.15 var(--marker); color: #ffb36b;
  transform: rotate(-3deg); text-shadow: 0 0 18px rgba(255, 110, 20, .6); animation: fx-cap .5s .25s cubic-bezier(.2, 1.4, .4, 1) both; }
.fx-swap .fx-cap { color: var(--pink-2); text-shadow: 0 0 18px rgba(255, 8, 203, .6); animation-delay: 1.35s; }
.fx-swap.arrive .fx-cap { animation-delay: .75s; }
.fx-skip { position: fixed; bottom: max(16px, env(safe-area-inset-bottom)); left: 0; right: 0; z-index: 3; text-align: center; font: 600 12px/1 var(--body); letter-spacing: .14em;
  text-transform: uppercase; color: rgba(255, 255, 255, .38); animation: fx-in .4s 1s both; }
/* the void behind every swap, and the fire behind every burn */
.fx-burn::before, .fx-swap::before { content: ""; position: absolute; inset: -6%; z-index: 0; pointer-events: none; opacity: .5;
  background: center / cover no-repeat; animation: fx-bg 3.2s ease-out both; }
.fx-burn::before { background-image: url(/sacrifice-assets/void-red.webp); }
.fx-swap::before { background-image: url(/sacrifice-assets/void-purple.webp); }
.fx-burn.heavy::before { opacity: .72; }
.fx.calm::before { animation: none; }
@keyframes fx-bg { from { transform: scale(1.18) rotate(-4deg); } to { transform: scale(1) rotate(0deg); } }
@keyframes fx-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes fx-out { to { opacity: 0; } }
@keyframes fx-cap { from { opacity: 0; transform: rotate(-3deg) scale(.7); } to { opacity: 1; transform: rotate(-3deg) scale(1); } }

/* ---------------------------------------------------------------- burn: rise (flames climb the card, it burns away bottom to top) */
.fx-burn-rise .fx-card .nft { animation: fx-char 2.8s ease-in forwards; }
.fx-burn-rise .fx-card .nft img { animation: fx-away 2.6s cubic-bezier(.5, 0, .75, 1) forwards; }
.fx-burn-rise .fx-card::after { content: ""; position: absolute; left: 3%; right: 3%; height: 16px; bottom: 0; z-index: 1; pointer-events: none; border-radius: 50%;
  background: linear-gradient(transparent, #ffd166 40%, #ff6b2c 60%, transparent); filter: blur(4px); mix-blend-mode: screen; animation: fx-edge 2.6s cubic-bezier(.5, 0, .75, 1) forwards; }
.fx-flames { position: absolute; left: -8%; right: -8%; height: 48%; bottom: -10%; z-index: 1; display: flex; justify-content: center; align-items: flex-end;
  pointer-events: none; filter: blur(1.5px); mix-blend-mode: screen; animation: fx-rise 2.6s cubic-bezier(.5, 0, .75, 1) forwards; }
.fx-flames i { margin: 0 -3%; flex: 0 0 26%; height: 100%; border-radius: 50% 50% 38% 38%; transform-origin: 50% 100%;
  background: radial-gradient(ellipse at 50% 88%, #fff7cf 0 10%, #ffd166 20%, #ff8a1f 42%, rgba(255, 48, 0, .55) 62%, transparent 72%); animation: fx-flick .42s ease-in-out infinite alternate; }
.fx-flames i:nth-child(2) { height: 128%; animation-delay: -.12s; }
.fx-flames i:nth-child(3) { height: 112%; animation-delay: -.3s; flex-basis: 30%; }
.fx-flames i:nth-child(4) { height: 136%; animation-delay: -.2s; }
.fx-flames i:nth-child(5) { height: 92%; animation-delay: -.05s; }
@keyframes fx-char { 25% { border-color: #ffd166; box-shadow: 0 0 60px rgba(255, 122, 0, .75); } 100% { border-color: #3a1a08; box-shadow: 0 0 110px rgba(255, 60, 0, .5); transform: rotate(-2deg) scale(.95); opacity: 0; } }
@keyframes fx-away { 0% { clip-path: inset(0 0 0 0); filter: none; } 35% { filter: sepia(.5) saturate(1.8) brightness(.95); } 100% { clip-path: inset(0 0 100% 0); filter: sepia(1) brightness(.25); } }
@keyframes fx-edge { 0% { bottom: 0; opacity: 0; } 8% { opacity: 1; } 92% { opacity: 1; } 100% { bottom: 100%; opacity: 0; } }
@keyframes fx-rise { 0% { bottom: -10%; opacity: 0; } 10% { opacity: 1; } 88% { opacity: 1; } 100% { bottom: 88%; opacity: 0; } }
@keyframes fx-flick { from { transform: scaleY(.84) scaleX(1.06) translateY(4%); } to { transform: scaleY(1.16) scaleX(.92) translateY(-6%); } }

/* ---------------------------------------------------------------- burn: crack (glowing cracks, then the card breaks and falls) */
.fx-burn-crack .fx-card:not(.split) .nft { animation: fx-heat 1.25s ease-in forwards; }
.fx-cracks { position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%; overflow: visible; pointer-events: none; transform: rotate(-2deg);
  filter: drop-shadow(0 0 4px #ff8a1f) drop-shadow(0 0 10px rgba(255, 90, 0, .8)); }
.fx-cracks path { fill: none; stroke: #ffe39a; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke;
  stroke-dasharray: 160; stroke-dashoffset: 160; animation: fx-draw 1.05s .15s cubic-bezier(.3, 0, .6, 1) forwards; }
.fx-cracks path:nth-child(n+7) { animation-delay: .55s; }
.fx-card.split ~ .fx-cracks { animation: fx-out .25s ease forwards; }
.fx-card.split { transform: rotate(-2deg); }
.fx-card.split .shard { position: absolute; inset: 0; animation: fx-fall 1.35s var(--d) cubic-bezier(.45, 0, .8, .6) forwards; }
.fx-card.split .shard .nft { transform: none; filter: sepia(.8) saturate(1.6) brightness(.8); box-shadow: none; }
@keyframes fx-heat { 0% { filter: none; transform: rotate(-2deg); } 30% { transform: rotate(-2deg) translate(1px, -1px); }
  45% { transform: rotate(-2deg) translate(-1px, 1px); } 60% { transform: rotate(-2deg) translate(1px, 1px); } 75% { transform: rotate(-2deg) translate(-2px, 0); }
  100% { filter: sepia(.8) saturate(1.6) brightness(.8); border-color: #ffd166; box-shadow: 0 0 70px rgba(255, 122, 0, .7); transform: rotate(-2deg) translate(1px, -1px); } }
@keyframes fx-draw { to { stroke-dashoffset: 0; } }
@keyframes fx-fall { 0% { transform: none; opacity: 1; } 12% { transform: translate(calc(var(--dx) * .12), -6px) rotate(calc(var(--r) * .1)); opacity: 1; }
  100% { transform: translate(var(--dx), var(--dy)) rotate(var(--r)) scale(.9); opacity: 0; filter: brightness(.2); } }

/* ---------------------------------------------------------------- burn: combust (heat builds, then it bursts into sparks) */
.fx-burn-combust .fx-card .nft { animation: fx-heat2 1.25s ease-in forwards; }
.fx-burn-combust.heavy .fx-card .nft { animation-duration: 1.5s; }
.fx-burn-combust.burst .fx-card .nft { animation: fx-pop .42s ease-out forwards; }
.fx-burn-combust .fx-stage::after { content: ""; position: absolute; inset: -40%; z-index: 2; pointer-events: none; border-radius: 50%; opacity: 0;
  background: radial-gradient(circle, #fff6d8 0 8%, rgba(255, 196, 92, .9) 18%, rgba(255, 96, 24, .5) 38%, transparent 65%); }
.fx-burn-combust.burst .fx-stage::after { animation: fx-flash .7s ease-out forwards; }
@keyframes fx-heat2 { 0% { filter: none; transform: rotate(-2deg) scale(1); }
  50% { filter: sepia(.4) saturate(1.6) brightness(1.1); box-shadow: 0 0 40px rgba(255, 122, 0, .5); transform: rotate(-1deg) scale(1.02); }
  70% { transform: rotate(-3deg) scale(1.01); } 85% { transform: rotate(-1.5deg) scale(1.04); }
  100% { filter: sepia(.7) saturate(2.2) brightness(1.5); border-color: #ffe39a; box-shadow: 0 0 90px rgba(255, 140, 0, .9); transform: rotate(-2deg) scale(1.05); } }
@keyframes fx-pop { 0% { filter: sepia(.7) saturate(2.2) brightness(1.5); border-color: #ffe39a; transform: rotate(-2deg) scale(1.05); opacity: 1; }
  100% { filter: sepia(1) brightness(3) blur(6px); transform: rotate(-2deg) scale(1.3); opacity: 0; } }
@keyframes fx-flash { 0% { opacity: 0; transform: scale(.3); } 18% { opacity: 1; } 100% { opacity: 0; transform: scale(1.4); } }

/* doomed: bigger, longer, with a shake and a red edge */
.fx-burn.heavy { background: radial-gradient(60% 50% at 50% 55%, rgba(255, 60, 20, .32), transparent 70%), rgba(2, 4, 8, .92); }
.fx-burn.heavy .fx-stage { width: min(340px, 76vw); animation: fx-shake .5s 1s ease-in-out 2; }
.fx-burn.heavy.burst .fx-stage { animation: fx-shake .35s ease-out 1; }
.fx-burn.heavy .fx-cap { color: #ff8a5c; }
.fx-vignette { position: fixed; inset: 0; z-index: -1; pointer-events: none; box-shadow: inset 0 0 160px 40px rgba(255, 40, 10, .45); animation: fx-throb 1s ease-in-out infinite alternate; }
@keyframes fx-shake { 0%, 100% { transform: none; } 20% { transform: translate(-4px, 2px); } 40% { transform: translate(4px, -2px); } 60% { transform: translate(-3px, -2px); } 80% { transform: translate(3px, 2px); } }
@keyframes fx-throb { from { opacity: .55; } to { opacity: 1; } }

/* ---------------------------------------------------------------- burn: wind (it blackens, then blows away as ash, left to right) */
.fx-burn-wind .fx-card .nft { animation: fx-blow 2.4s ease-in forwards; }
.fx-burn-wind .fx-card::after { content: ""; position: absolute; top: -2%; bottom: -2%; width: 12px; left: 0; z-index: 1; pointer-events: none; opacity: 0;
  background: linear-gradient(90deg, transparent, #ffd166, #ff6b2c, transparent); filter: blur(4px); mix-blend-mode: screen; animation: fx-sweep 1.65s .75s linear forwards; }
@keyframes fx-blow { 0% { filter: none; clip-path: inset(-20px -20px -20px 0); }
  31% { filter: sepia(1) saturate(.6) brightness(.38) contrast(1.2); border-color: #46362c; clip-path: inset(-20px -20px -20px 0); transform: rotate(-2deg); }
  100% { filter: sepia(1) saturate(.6) brightness(.3) contrast(1.2); border-color: #46362c; clip-path: inset(-20px -20px -20px 100%); transform: rotate(-2deg) translateX(14px) skewX(-4deg); } }
@keyframes fx-sweep { 0% { left: 0; opacity: 0; } 8% { opacity: 1; } 92% { opacity: 1; } 100% { left: 100%; opacity: 0; } }

/* ---------------------------------------------------------------- swap: portal (pulled into a spinning portal, the new one thrown out) */
.fx-vortex { position: absolute; inset: -32%; border-radius: 50%; pointer-events: none; opacity: 0;
  background: conic-gradient(from 0deg, transparent, rgba(255, 8, 203, .85), transparent 30%, rgba(126, 242, 200, .7), transparent 55%, rgba(185, 166, 255, .85), transparent 80%, rgba(255, 8, 203, .85));
  -webkit-mask: radial-gradient(circle, transparent 24%, #000 30%, #000 52%, transparent 70%); mask: radial-gradient(circle, transparent 24%, #000 30%, #000 52%, transparent 70%);
  filter: blur(2px); animation: fx-vortex 2.4s ease-in-out forwards; }
.fx-swap-portal.arrive .fx-vortex { animation-duration: 1.5s; }
.fx-swap-portal .from .nft { animation: fx-suck 1.1s cubic-bezier(.6, 0, .9, .5) forwards; }
.fx-swap-portal .to .nft { animation: fx-throw 1s 1.1s cubic-bezier(.15, .7, .3, 1.15) both; }
.fx-swap-portal.arrive .to .nft { animation-delay: .4s; }
@keyframes fx-vortex { 0% { opacity: 0; transform: scale(.4) rotate(0); } 15% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; transform: scale(1.1) rotate(720deg); } }
@keyframes fx-suck { 0% { transform: rotate(-2deg) scale(1); filter: none; opacity: 1; } 100% { transform: rotate(540deg) scale(0); filter: blur(4px) brightness(1.6) hue-rotate(60deg); opacity: 0; } }
@keyframes fx-throw { 0% { transform: rotate(-540deg) scale(0); filter: blur(4px) brightness(1.8); opacity: 0; } 30% { opacity: 1; }
  100% { transform: rotate(-2deg) scale(1); filter: none; opacity: 1; box-shadow: 0 30px 70px -20px rgba(255, 8, 203, .6), 0 0 0 1px rgba(255, 8, 203, .4); } }

/* ---------------------------------------------------------------- swap: eclipse (a moon passes over, and the card is different after) */
.fx-moon { position: absolute; z-index: 1; top: -9%; left: -9%; width: 118%; aspect-ratio: 1; border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle at 38% 34%, #1b2233, #0a0d16 62%, #05070c);
  box-shadow: 0 0 0 2px rgba(255, 255, 255, .55), 0 0 40px 10px rgba(255, 8, 203, .65), 0 0 110px 34px rgba(185, 166, 255, .45);
  animation: fx-pass 2.2s cubic-bezier(.45, 0, .55, 1) forwards; }
.fx-swap-eclipse.arrive .fx-moon { animation-duration: 1.6s; }
.fx-swap-eclipse .from .nft { animation: fx-hide 2.2s linear forwards; }
.fx-swap-eclipse .to .nft { animation: fx-show 2.2s linear forwards; }
.fx-swap-eclipse.arrive .to .nft { animation-duration: 1.6s; }
.fx-swap-eclipse .fx-stage { overflow: visible; }
@keyframes fx-pass { 0% { transform: translateX(-150%); opacity: 0; } 10% { opacity: 1; } 50% { transform: translateX(0); } 90% { opacity: 1; } 100% { transform: translateX(150%); opacity: 0; } }
@keyframes fx-hide { 0%, 46% { opacity: 1; } 50%, 100% { opacity: 0; } }
@keyframes fx-show { 0%, 50% { opacity: 0; filter: brightness(1.8); } 54% { opacity: 1; filter: brightness(1.8); }
  100% { opacity: 1; filter: none; box-shadow: 0 30px 70px -20px rgba(255, 8, 203, .6), 0 0 0 1px rgba(255, 8, 203, .4); } }

/* ---------------------------------------------------------------- swap: teleport (beamed out as light, beamed back in as the new one) */
.fx-swap-teleport .fx-stage::before { content: ""; position: absolute; z-index: 1; left: 42%; right: 42%; top: -70%; bottom: -70%; pointer-events: none; opacity: 0;
  background: linear-gradient(90deg, transparent, rgba(126, 242, 200, .55), #fff, rgba(126, 242, 200, .55), transparent); filter: blur(6px); animation: fx-beam 2.4s linear forwards; }
.fx-swap-teleport.arrive .fx-stage::before { animation: fx-beam-in 1.7s linear forwards; }
.fx-swap-teleport .from .nft { animation: fx-out-beam .6s cubic-bezier(.6, 0, .9, .4) forwards; }
.fx-swap-teleport .to .nft { animation: fx-in-beam .75s 1.25s cubic-bezier(.1, .7, .3, 1) both; }
.fx-swap-teleport.arrive .to .nft { animation-delay: .55s; }
@keyframes fx-beam { 0% { opacity: 0; transform: scaleX(.3); } 12% { opacity: 1; transform: scaleX(1.4); } 28% { opacity: 0; transform: scaleX(.2); }
  48% { opacity: 0; transform: scaleX(.2); } 58% { opacity: 1; transform: scaleX(1.4); } 76% { opacity: 0; transform: scaleX(.3); } 100% { opacity: 0; } }
@keyframes fx-beam-in { 0% { opacity: 0; transform: scaleX(.2); } 32% { opacity: 1; transform: scaleX(1.4); } 60% { opacity: 0; transform: scaleX(.3); } 100% { opacity: 0; } }
@keyframes fx-out-beam { 0% { transform: rotate(-2deg); filter: none; opacity: 1; } 45% { transform: rotate(0) scale(1.02, 1); filter: brightness(2.4) saturate(0); }
  100% { transform: scale(.02, 1.25); filter: brightness(4) saturate(0) blur(2px); opacity: 0; } }
@keyframes fx-in-beam { 0% { transform: scale(.02, 1.25); filter: brightness(4) saturate(0) blur(2px); opacity: 0; } 25% { opacity: 1; }
  60% { transform: rotate(0) scale(1.03, 1); filter: brightness(1.8) saturate(.6); }
  100% { transform: rotate(-2deg); filter: none; opacity: 1; box-shadow: 0 30px 70px -20px rgba(255, 8, 203, .6), 0 0 0 1px rgba(255, 8, 203, .4); } }

/* ---------------------------------------------------------------- still versions (reduced motion) */
.fx.calm *, .fx.calm .fx-stage::before, .fx.calm .fx-stage::after, .fx.calm .fx-card::after { animation: none !important; }
.fx.calm .fx-flames, .fx.calm .fx-cracks, .fx.calm .fx-vortex, .fx.calm .fx-moon, .fx.calm .fx-vignette, .fx.calm .fx-skip,
.fx.calm .fx-card::after, .fx.calm .fx-stage::before, .fx.calm .fx-stage::after, .fx.calm .from { display: none; }
.fx-burn.calm .fx-card .nft { filter: sepia(1) brightness(.4); }

/* ---------------------------------------------------------------- reveal: the alternate's card arriving on the choice screen */
.versus .side.alt { animation: none; }
.versus .side.alt .nft.fx-r { animation-delay: var(--fx-delay, 0s); animation-fill-mode: both; }
.versus .side.alt .nft.fx-r::after { content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none; }

/* emerge: it comes spinning out of the portal behind it */
.versus .side.alt .nft.fx-r-emerge { animation-name: fx-r-emerge; animation-duration: 1.3s; animation-timing-function: cubic-bezier(.2, .7, .3, 1.15); }
@keyframes fx-r-emerge { 0%, 18% { transform: scale(0) rotate(-200deg); filter: blur(6px) brightness(2.2); opacity: 0; } 32% { opacity: 1; }
  72% { transform: scale(1.06) rotate(4deg); filter: none; } 100% { transform: rotate(1.5deg); } }

/* flip: it turns over from its back */
.versus .side.alt .nft.fx-r-flip { animation-name: fx-r-flip; animation-duration: 1.3s; animation-timing-function: cubic-bezier(.3, .7, .3, 1); }
.versus .side.alt .nft.fx-r-flip::after { animation: fx-r-back 1.3s steps(1, end) var(--fx-delay, 0s) both;
  background: radial-gradient(circle at 50% 46%, #fff 0 9%, transparent 10%), radial-gradient(circle at 56% 42%, #2a1b4d 0 9%, transparent 10%),
    repeating-linear-gradient(45deg, rgba(255, 255, 255, .06) 0 10px, transparent 10px 20px), linear-gradient(135deg, #ff08cb, #6b2bd9); }
@keyframes fx-r-flip { 0% { transform: perspective(900px) rotateY(180deg) scale(.86); } 30% { transform: perspective(900px) rotateY(180deg) scale(.92) translateY(-8px); }
  65% { transform: perspective(900px) rotateY(-12deg) scale(1.05); } 100% { transform: perspective(900px) rotateY(0) rotate(1.5deg); } }
@keyframes fx-r-back { 0% { opacity: 1; } 47% { opacity: 0; } 100% { opacity: 0; } }

/* iris: it opens out from a point of light */
.versus .side.alt .nft.fx-r-iris { animation-name: fx-r-iris; animation-duration: 1.3s; animation-timing-function: cubic-bezier(.5, 0, .2, 1); }
.versus .side.alt .nft.fx-r-iris img { animation: fx-r-bright 1.3s ease-out var(--fx-delay, 0s) both; }
@keyframes fx-r-iris { 0% { clip-path: circle(0 at 50% 50%); transform: rotate(1.5deg) scale(.92); } 15% { clip-path: circle(4% at 50% 50%); }
  100% { clip-path: circle(75% at 50% 50%); transform: rotate(1.5deg); } }
@keyframes fx-r-bright { 0% { filter: brightness(3) saturate(0); } 100% { filter: none; } }

/* moonrise: it rises out of the dark with a shine across it */
.versus .side.alt .nft.fx-r-moonrise { animation-name: fx-r-rise; animation-duration: 1.3s; animation-timing-function: cubic-bezier(.2, .8, .3, 1.1); }
.versus .side.alt .nft.fx-r-moonrise::after { animation: fx-r-shine 1.3s ease-in-out var(--fx-delay, 0s) both; background: linear-gradient(115deg, transparent 35%, rgba(255, 255, 255, .7) 48%, rgba(255, 122, 217, .4) 52%, transparent 65%); background-size: 300% 100%; }
@keyframes fx-r-rise { 0% { transform: translateY(60px) scale(.8) rotate(-6deg); filter: blur(10px) brightness(.2); opacity: 0; } 50% { filter: blur(0) brightness(.9); opacity: 1; }
  100% { transform: rotate(1.5deg); filter: none; opacity: 1; } }
@keyframes fx-r-shine { 0%, 45% { background-position: 100% 0; } 100% { background-position: 0 0; } }

/* shuffle: it spins through colours like a slot reel and lands */
.versus .side.alt .nft.fx-r-shuffle { animation-name: fx-r-shuffle; animation-duration: 1.5s; animation-timing-function: ease-out; }
.versus .side.alt .nft.fx-r-shuffle img { animation: fx-r-reel 1.5s cubic-bezier(.2, .6, .3, 1) var(--fx-delay, 0s) both; }
.versus .side.alt .nft.fx-r-shuffle::after { animation: fx-r-streak 1.5s ease-out var(--fx-delay, 0s) both; background: repeating-linear-gradient(180deg, rgba(255, 255, 255, .22) 0 3px, transparent 3px 18px); background-size: 100% 36px; }
@keyframes fx-r-shuffle { 0% { transform: rotate(1.5deg) scale(.94); } 70% { transform: rotate(1.5deg) scale(.98); } 85% { transform: rotate(1.5deg) scale(1.05); } 100% { transform: rotate(1.5deg); } }
@keyframes fx-r-reel { 0% { filter: hue-rotate(0) blur(6px) brightness(1.3); transform: translateY(-14%); } 20% { transform: translateY(14%); } 35% { transform: translateY(-10%); }
  50% { transform: translateY(8%); } 65% { filter: hue-rotate(720deg) blur(3px) brightness(1.2); transform: translateY(-4%); } 100% { filter: none; transform: none; } }
@keyframes fx-r-streak { 0% { opacity: 1; background-position: 0 0; } 65% { opacity: .6; background-position: 0 420px; } 100% { opacity: 0; background-position: 0 520px; } }

/* ---------------------------------------------------------------- the test panel (?fx=demo) */
.fx-demo-reveal .versus { display: block; margin: 0; }
.fx-demo-reveal .side { padding: 0; }
.fx-demo-reveal .nft { width: 100%; }
.fx-demo { position: fixed; z-index: 70; right: 12px; bottom: 12px; max-width: 380px; max-height: 46vh; overflow: auto; display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
  padding: 12px; border-radius: 14px; border: 1px solid var(--border-hi); background: var(--card); box-shadow: 0 20px 50px -20px rgba(0, 0, 0, .9); }
.fx-demo b { flex-basis: 100%; font: 800 14px/1 var(--display); letter-spacing: .14em; text-transform: uppercase; color: var(--ok); }
.fx-demo button { padding: 7px 10px; font-size: 13px; }
@media (max-width: 480px) { .fx-demo { left: 8px; right: 8px; bottom: 8px; max-width: none; } }

/* ---------------------------------------------------------------- the portal is the stage */
/* the choice screen sits in Dr. Gloop's lab, and the alternate comes out of the portal itself */
#pDecide { overflow: hidden; background: linear-gradient(180deg, rgba(12, 8, 26, .82), rgba(12, 8, 26, .94) 55%),
  url(/sacrifice-assets/void-portal-900.webp) 50.8% 40% / cover no-repeat, var(--card); }
.portal-stage { position: relative; isolation: isolate; }
.portal-stage::before { content: ""; position: absolute; z-index: -1; inset: -14%; border-radius: 50%; pointer-events: none;
  background: url(/sacrifice-assets/portal-core.webp) center / cover no-repeat;
  box-shadow: 0 0 0 3px rgba(255, 122, 217, .3), 0 0 50px 8px rgba(255, 8, 203, .35), 0 0 120px 30px rgba(107, 43, 217, .3);
  animation: ps-spin 40s linear infinite; }
.portal-stage.doomed::before { background-image: url(/sacrifice-assets/portal-core-red.webp);
  box-shadow: 0 0 0 3px rgba(255, 110, 60, .35), 0 0 50px 8px rgba(255, 50, 20, .45), 0 0 120px 30px rgba(180, 20, 0, .35); }
#pDecide.doomed { background: linear-gradient(180deg, rgba(26, 6, 6, .8), rgba(22, 6, 8, .94) 55%),
  url(/sacrifice-assets/void-portal-red-900.webp) 50.8% 40% / cover no-repeat, var(--card); }
.portal-stage.opening::before { animation: ps-spin 40s linear infinite, ps-open .7s cubic-bezier(.2, .8, .3, 1.2) var(--fx-delay, 0s) both; }
@keyframes ps-spin { to { rotate: 360deg; } }
@keyframes ps-open { from { scale: 0; opacity: 0; } to { scale: 1; opacity: 1; } }

/* the swap's portal is the real one too */
.fx-swap-portal .fx-stage::before { content: ""; position: absolute; inset: -16%; border-radius: 50%; pointer-events: none;
  background: url(/sacrifice-assets/portal-core.webp) center / cover no-repeat; animation: fx-core 2.4s ease-in-out forwards; }
.fx-swap-portal.arrive .fx-stage::before { animation-duration: 1.5s; }
@keyframes fx-core { 0% { scale: 0; rotate: 0deg; opacity: 0; } 16% { scale: 1; opacity: 1; } 84% { scale: 1; opacity: 1; } 100% { scale: .3; rotate: 540deg; opacity: 0; } }

/* ---------------------------------------------------------------- loot drop: a rare alternate */
.portal-stage.loot { --loot: #ffd166; --loot-ink: #1a1200; margin-bottom: 14px; }
.portal-stage.loot-legendary { --loot: #e9e4ff; --loot-ink: #1a1030; }
.portal-stage.loot-gem { --loot: #7ef2c8; --loot-ink: #022; }
.portal-stage.loot::after { content: ""; position: absolute; z-index: -2; inset: -38%; border-radius: 50%; pointer-events: none; opacity: .75;
  background: repeating-conic-gradient(from 0deg, color-mix(in srgb, var(--loot) 60%, transparent) 0deg 5deg, transparent 5deg 15deg);
  -webkit-mask: radial-gradient(circle, #000 22%, transparent 68%); mask: radial-gradient(circle, #000 22%, transparent 68%);
  animation: ps-spin 60s linear infinite reverse; }
.portal-stage.loot.drop::after { animation: ps-spin 60s linear infinite reverse, loot-burst 1.6s ease-out var(--drop-delay, 0s) both; }
.versus .side.alt .portal-stage.loot .nft { box-shadow: 0 0 0 2px var(--loot), 0 0 60px -6px var(--loot), 0 30px 60px -24px rgba(0, 0, 0, .8); }
.loot-tag { position: absolute; z-index: 3; left: 50%; bottom: -16px; transform: translateX(-50%) rotate(-4deg); white-space: nowrap; padding: 7px 14px; border-radius: 10px;
  font: 800 16px/1 var(--display); letter-spacing: .12em; text-transform: uppercase; color: var(--loot-ink); background: var(--loot); border: 2px solid #fff; box-shadow: 0 8px 26px -6px var(--loot); }
.portal-stage.drop .loot-tag { animation: loot-slam .55s cubic-bezier(.2, 1.6, .4, 1) calc(var(--drop-delay, 0s) + .5s) both; }
@keyframes loot-burst { 0% { scale: 0; opacity: 0; } 35% { scale: 1.35; opacity: 1; } 100% { scale: 1; opacity: .75; } }
@keyframes loot-slam { from { transform: translateX(-50%) rotate(-4deg) scale(2.4); opacity: 0; } to { transform: translateX(-50%) rotate(-4deg) scale(1); opacity: 1; } }

/* ---------------------------------------------------------------- the intro (intro.js), first visit only */
.vp { position: fixed; inset: 0; z-index: 90; overflow: hidden; background: #0b0618; animation: fx-in .6s ease both; }
.vp.out { animation: fx-out .45s ease forwards; }
/* a blurred copy fills the screen wherever the scene doesn't reach (tall phones) */
.vp::before { content: ""; position: absolute; inset: -40px; background: url(/sacrifice-assets/void-portal-900.webp) center / cover no-repeat; filter: blur(26px) brightness(.5) saturate(1.3); }
/* the scene is placed so the vortex sits in the middle of the screen, and the camera dives in there */
.vp-scene { position: absolute; left: 50%; top: 50%; width: max(104vw, 186vh); aspect-ratio: 1672 / 941; cursor: pointer; transform-origin: 50.8% 51.2%;
  background: url(/sacrifice-assets/void-portal.webp) center / 100% 100% no-repeat; transform: translate(-50.8%, -51.2%); animation: vp-drift 16s ease-in-out infinite alternate; }
@media (max-aspect-ratio: 1 / 1) { .vp-scene { width: 250vw; -webkit-mask: linear-gradient(transparent, #000 12%, #000 88%, transparent); mask: linear-gradient(transparent, #000 12%, #000 88%, transparent); } }
.vp-swirl { position: absolute; left: 50.8%; top: 51.2%; width: 23.3%; aspect-ratio: 1; border-radius: 50%; pointer-events: none;
  background: url(/sacrifice-assets/portal-core.webp) center / cover no-repeat; transform: translate(-50%, -50%); animation: vp-spin 36s linear infinite; }
.vp-red { position: absolute; inset: 0; pointer-events: none; opacity: 0; transition: opacity .9s ease;
  background: url(/sacrifice-assets/void-portal-red.webp) center / 100% 100% no-repeat; }
.vp-swirl.red { background-image: url(/sacrifice-assets/portal-core-red.webp); opacity: 0; transition: opacity .9s ease; }
.vp.fire .vp-red, .vp.fire .vp-swirl.red { opacity: 1; }
.vp.fire .vp-box { box-shadow: 0 20px 60px -20px rgba(0, 0, 0, .9), inset 0 0 0 1px rgba(255, 70, 30, .6), 0 0 40px -10px rgba(255, 60, 20, .7); }
/* through the portal: the void the camera lands in */
.vp-void { position: absolute; inset: -12%; pointer-events: none; opacity: 0; background: url(/sacrifice-assets/void-purple.webp) 52% 48% / cover no-repeat; }
.vp.enter .vp-void { animation: vp-void 1.9s ease-out forwards; }
@keyframes vp-void { 0%, 38% { opacity: 0; transform: scale(1.7) rotate(-25deg); } 60% { opacity: 1; } 100% { opacity: 1; transform: scale(1) rotate(0deg); } }
.vp-canvas { position: fixed; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 2; }
.vp-skip { position: absolute; z-index: 4; top: max(14px, env(safe-area-inset-top)); right: 14px; padding: 8px 14px; font-size: 13px; border-radius: 999px; background: rgba(0, 0, 0, .5); }
.vp-box { position: absolute; z-index: 3; left: 50%; bottom: max(18px, env(safe-area-inset-bottom)); transform: translateX(-50%); width: min(92vw, 720px); min-height: 124px; cursor: pointer;
  padding: 14px 18px 18px; border-radius: 18px; border: 2px solid rgba(255, 255, 255, .85); background: rgba(10, 6, 24, .84); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  box-shadow: 0 20px 60px -20px rgba(0, 0, 0, .9), inset 0 0 0 1px rgba(255, 8, 203, .35); animation: vp-box .5s .4s cubic-bezier(.2, 1.3, .4, 1) both; }
.vp-who { margin: 0 0 8px; display: inline-block; font: 400 18px/1 var(--marker); color: var(--ok); transform: rotate(-2deg); }
.vp-line { margin: 0; min-height: 2.8em; font: 600 clamp(16px, 2.3vw, 20px)/1.4 var(--body); color: var(--fg); }
.vp-more { position: absolute; right: 16px; bottom: 10px; font-size: 13px; color: var(--primary); animation: vp-bob .7s ease-in-out infinite alternate; }
.vp-enter { width: 100%; margin-top: 12px; padding: 14px; font-size: 18px; }
.vp::after { content: ""; position: absolute; inset: 0; z-index: 3; opacity: 0; pointer-events: none; background: radial-gradient(circle, #fff 0 10%, #ff7ad9 35%, rgba(107, 43, 217, .9) 70%); }
.vp.enter .vp-scene { animation: vp-dive 1.1s cubic-bezier(.55, 0, .9, .35) forwards; }
.vp.enter .vp-box, .vp.enter .vp-skip { transition: opacity .3s ease; opacity: 0; }
.vp.enter::after { animation: vp-flash 1.9s ease-in-out forwards; }
@keyframes vp-drift { from { transform: translate(-50.8%, -51.2%) scale(1); } to { transform: translate(-50.8%, -51.2%) scale(1.05); } }
@keyframes vp-dive { from { transform: translate(-50.8%, -51.2%) scale(1.02); filter: none; } to { transform: translate(-50.8%, -51.2%) scale(7); filter: brightness(1.8) blur(3px); } }
@keyframes vp-spin { to { transform: translate(-50%, -50%) rotate(360deg); } }
@keyframes vp-box { from { opacity: 0; transform: translateX(-50%) translateY(20px); } to { opacity: 1; transform: translateX(-50%); } }
@keyframes vp-bob { from { translate: 0 0; } to { translate: 0 4px; } }
@keyframes vp-flash { 0%, 38% { opacity: 0; } 50% { opacity: .95; } 72%, 100% { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .portal-stage::before, .portal-stage::after, .loot-tag, .vp-scene, .vp-swirl, .vp-box, .vp-more, .fx::before { animation: none !important; }
  .vp-red, .vp-swirl.red { transition: none; }
}
