/* ============================================================================
   RIPVault — Open Pack motion module
   Phase 1 prototype. Goal: validate EMOTIONAL PACING, not final visuals.
   Everything is token-driven + state-driven so beats slot in and assets swap.

   STATE MODEL:  .op-stage[data-beat="<id>"]  drives which beat is active.
   Beats so far:  idle   (next: charge, pause, tear, burst, rise, flip, verdict, afterglow)
   ASSET SWAP:   replace .op-pack__art--placeholder with <img class="op-pack__art">.
   ============================================================================ */

/* ---- MOTION TOKENS (seed of the reusable RIPVault Motion Design System) ---- */
:root{
  --mo-dur-fast: 120ms;
  --mo-dur-base: 240ms;
  --mo-dur-slow: 400ms;
  --mo-dur-slower: 600ms;
  --mo-dur-cinematic: 900ms;
  --mo-dur-epic: 1400ms;

  --mo-ease-standard:   cubic-bezier(.2, 0, 0, 1);
  --mo-ease-decelerate: cubic-bezier(.05, .7, .1, 1);
  --mo-ease-accelerate: cubic-bezier(.3, 0, .8, .15);
  --mo-ease-back-out:   cubic-bezier(.34, 1.56, .64, 1);
  --mo-ease-anticipate: cubic-bezier(.36, 0, .66, -.56);
  --mo-ease-breathe:    cubic-bezier(.37, 0, .63, 1); /* sine-like in/out for idle */

  /* RIPVault energy accent */
  --op-accent: #ea580c;
}

/* ---- STAGE: dark cinematic space, soft key from upper-left ---- */
.op-stage{
  /* per-run controls (JS sets these) */
  --op-rate: 1;            /* 1 = normal, 0.5 = half-speed (for studying pacing) */
  --op-play: running;      /* running | paused */
  /* idle timing */
  --op-breathe-dur: 4200ms;
  --op-sheen-dur: 7200ms;

  position: relative;
  width: 100%;
  min-height: 560px;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background:
    radial-gradient(120% 100% at 34% 24%, #16161d 0%, #0c0c11 46%, #070709 100%);
  isolation: isolate;
}

/* ambient orange glow behind the pack (breathes with it) */
.op-ambient{
  position: absolute;
  width: 460px; height: 460px;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(234,88,12,.22), rgba(234,88,12,0) 70%);
  filter: blur(8px);
  z-index: 1;
  pointer-events: none;
}

/* ---- PACK ---- */
.op-pack{
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* when beat animations stop (e.g. at the pause), ease to rest instead of snapping */
  transition: transform calc(var(--mo-dur-slow) / var(--op-rate)) var(--mo-ease-decelerate);
}

/* floor reflection / contact shadow */
.op-pack__shadow{
  position: absolute;
  bottom: -38px;
  width: 150px; height: 26px;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(0,0,0,.55), rgba(0,0,0,0) 72%);
  z-index: -1;
  transition: opacity calc(var(--mo-dur-slow) / var(--op-rate)) var(--mo-ease-standard);
}

/* the body is the moving element (breathing) — wraps the swappable art */
.op-pack__body{
  position: relative;
  width: 184px; height: 304px;
  border-radius: 16px;
  overflow: hidden;
  box-shadow:
    0 34px 64px rgba(0,0,0,.62),
    inset 0 1px 0 rgba(255,255,255,.06),
    inset 0 -40px 60px rgba(0,0,0,.45);
  will-change: transform;
  transition:
    transform calc(var(--mo-dur-slow) / var(--op-rate)) var(--mo-ease-decelerate),
    opacity   calc(var(--mo-dur-slow) / var(--op-rate)) var(--mo-ease-standard);
}

/* ===== SWAPPABLE PACK ART (replace this block with <img class="op-pack__art">) ===== */
.op-pack__art{ position:absolute; inset:0; width:100%; height:100%; object-fit: cover; }
.op-pack__art--placeholder{
  background:
    linear-gradient(150deg, #20202a 0%, #15151c 52%, #0e0e13 100%);
}
.op-pack__art--placeholder::before{ /* foil seam stripe */
  content:""; position:absolute; left:0; right:0; top:54px; height:2px;
  background: linear-gradient(90deg, transparent, rgba(234,88,12,.6), transparent);
}
.op-pack__logo{
  position:absolute; top:18px; left:0; right:0; text-align:center;
  font: 500 13px/1 system-ui, sans-serif; letter-spacing:.22em;
  color: #cfcfd6; opacity:.85;
}
.op-pack__art--placeholder::after{ /* faint vertical foil accent */
  content:""; position:absolute; top:70px; bottom:24px; left:50%; width:2px;
  transform: translateX(-50%);
  background: linear-gradient(180deg, rgba(234,88,12,.5), rgba(234,88,12,.05));
}
/* ===== /SWAPPABLE PACK ART ===== */

/* roaming foil glint (sits above the art) */
.op-pack__sheen{
  position:absolute; top:-20%; bottom:-20%; left:-40%;
  width: 60px;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.18) 40%, rgba(255,255,255,.32) 50%, rgba(255,255,255,.18) 60%, transparent);
  transform: rotate(8deg) translateX(-160%);
  pointer-events:none;
  mix-blend-mode: screen;
}

/* cinematic vignette on top of everything */
.op-vignette{
  position:absolute; inset:0; z-index:5; pointer-events:none;
  background: radial-gradient(130% 110% at 50% 46%, transparent 52%, rgba(0,0,0,.55) 100%);
}

/* ============================================================================
   BEAT: IDLE  — calm, premium, breathing. (KF-01)
   "This is mine." Unhurried. The breath + a slow drifting glint.
   ============================================================================ */
.op-stage[data-beat="idle"] .op-pack__body,
.op-stage[data-beat="intent"] .op-pack__body,
.op-stage[data-beat="charge"] .op-pack__body{
  animation: op-breathe calc(var(--op-breathe-dur) / var(--op-rate)) var(--mo-ease-breathe) infinite;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="idle"] .op-pack__shadow,
.op-stage[data-beat="intent"] .op-pack__shadow,
.op-stage[data-beat="charge"] .op-pack__shadow{
  animation: op-shadow calc(var(--op-breathe-dur) / var(--op-rate)) var(--mo-ease-breathe) infinite;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="idle"] .op-ambient,
.op-stage[data-beat="intent"] .op-ambient,
.op-stage[data-beat="charge"] .op-ambient{
  animation: op-ambient calc(var(--op-breathe-dur) / var(--op-rate)) var(--mo-ease-breathe) infinite;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="idle"] .op-pack__sheen{
  animation: op-sheen calc(var(--op-sheen-dur) / var(--op-rate)) var(--mo-ease-standard) infinite;
  animation-play-state: var(--op-play);
}

@keyframes op-breathe{
  0%, 100% { transform: translateY(3px)  scale(1.000); }
  50%      { transform: translateY(-7px) scale(1.015); }
}
@keyframes op-shadow{ /* pack floats up -> shadow tightens + fades */
  0%, 100% { transform: scaleX(1);   opacity: .30; }
  50%      { transform: scaleX(.82); opacity: .16; }
}
@keyframes op-ambient{
  0%, 100% { opacity: .10; transform: scale(1);    }
  50%      { opacity: .17; transform: scale(1.06); }
}
@keyframes op-sheen{
  0%   { transform: rotate(8deg) translateX(-160%); opacity: 0;   }
  18%  { opacity: .5; }
  50%  { opacity: .32; }
  82%  { opacity: 0; }
  100% { transform: rotate(8deg) translateX(420%);  opacity: 0;   }
}

/* ============================================================================
   CAMERA + INTENT ELEMENTS (base styles)
   ============================================================================ */
.op-camera{
  position: relative; z-index: 2;
  display: flex; align-items: center; justify-content: center;
  transform-origin: 50% 42%;             /* focus slightly above center, toward the seam */
  transform: scale(1) translateY(0);
  transition: transform calc(var(--mo-dur-cinematic) / var(--op-rate)) var(--mo-ease-decelerate);
  will-change: transform;
}
/* rim light on the pack edges (fades in on intent) */
.op-pack__rim{
  position: absolute; inset: 0; border-radius: 16px; pointer-events: none;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.10), inset 0 0 22px rgba(255,180,120,.18);
  opacity: 0;
  transition: opacity calc(var(--mo-dur-cinematic) / var(--op-rate)) var(--mo-ease-standard);
}
/* glint pooled at the seam (fades in on intent; roaming sheen calms) */
.op-pack__seamglow{
  position: absolute; left: 0; right: 0; top: 44px; height: 30px; pointer-events: none;
  background: radial-gradient(60% 100% at 50% 50%, rgba(255,200,140,.6), rgba(234,88,12,0) 70%);
  mix-blend-mode: screen; opacity: 0;
  transition: opacity calc(var(--mo-dur-cinematic) / var(--op-rate)) var(--mo-ease-standard);
}
/* tighter vignette layer that fades in on intent */
.op-vignette--tight{
  z-index: 5;
  background: radial-gradient(112% 96% at 50% 44%, transparent 40%, rgba(0,0,0,.72) 100%);
  opacity: 0;
  transition: opacity calc(var(--mo-dur-cinematic) / var(--op-rate)) var(--mo-ease-standard);
}
/* PAUSE darkening — heavy dark with a tight transparent pocket at the seam */
.op-vignette--pause{
  z-index: 5;
  background: radial-gradient(46% 34% at 50% 40%, transparent 0%, rgba(0,0,0,.5) 50%, rgba(0,0,0,.92) 100%);
  opacity: 0;
  transition: opacity calc(var(--mo-dur-slow) / var(--op-rate)) var(--mo-ease-standard);
}
/* commit affordance: "Hold to rip" */
.op-affordance{
  position: absolute; z-index: 6; bottom: 44px; left: 50%;
  transform: translate(-50%, 14px);
  display: flex; align-items: center; gap: 10px;
  opacity: 0; pointer-events: none;
  transition:
    opacity   calc(var(--mo-dur-slower) / var(--op-rate)) var(--mo-ease-standard),
    transform calc(var(--mo-dur-slower) / var(--op-rate)) var(--mo-ease-decelerate);
  transition-delay: calc(var(--mo-dur-base) / var(--op-rate)); /* arrives just after the push starts */
}
.op-affordance__ring{
  width: 13px; height: 13px; border-radius: 50%;
  border: 2px solid var(--op-accent);
  box-shadow: 0 0 10px rgba(234,88,12,.45);
  transition: transform calc(var(--mo-dur-base) / var(--op-rate)) var(--mo-ease-standard);
}
.op-affordance__label{
  font: 500 12px/1 system-ui, sans-serif; letter-spacing: .16em; text-transform: uppercase;
  color: #e7d9c8;
}

/* ============================================================================
   CHARGE elements (base) — foil shimmer + rising energy motes
   ============================================================================ */
.op-pack__foil{
  position: absolute; top: -20%; bottom: -20%; left: -50%; width: 84px;
  background: linear-gradient(90deg, transparent, rgba(255,210,150,.30) 42%, rgba(255,255,255,.55) 50%, rgba(255,210,150,.30) 58%, transparent);
  transform: rotate(8deg) translateX(-160%);
  opacity: 0; pointer-events: none; mix-blend-mode: screen;
}
.op-particles{ position: absolute; inset: -24px 0 0 0; pointer-events: none; z-index: 3; }
.op-particle{
  position: absolute; top: 62%; width: 3px; height: 3px; border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255,205,150,.95), rgba(234,88,12,0));
  opacity: 0;
}

/* ============================================================================
   BEAT: INTENT (KF-02) — camera push + commit affordance
   "Okay. I'm doing this." Slow push-in; glint pools at the seam; rim lights;
   vignette tightens; 'Hold to rip' rises in and gives one gentle pulse.
   (rim / vignette / affordance persist into CHARGE)
   ============================================================================ */
.op-stage[data-beat="intent"] .op-camera{ transform: scale(1.15) translateY(14px); }
.op-stage[data-beat="intent"] .op-pack__seamglow{ opacity: .85; }

.op-stage[data-beat="intent"] .op-pack__rim,
.op-stage[data-beat="charge"] .op-pack__rim{ opacity: 1; }
.op-stage[data-beat="intent"] .op-vignette--tight,
.op-stage[data-beat="charge"] .op-vignette--tight{ opacity: 1; }
.op-stage[data-beat="intent"] .op-affordance,
.op-stage[data-beat="charge"] .op-affordance{ opacity: 1; transform: translate(-50%, 0); }
.op-stage[data-beat="intent"] .op-affordance__ring,
.op-stage[data-beat="charge"] .op-affordance__ring{
  animation: op-ring-pulse calc(2400ms / var(--op-rate)) var(--mo-ease-breathe) 700ms infinite;
  animation-play-state: var(--op-play);
}
@keyframes op-ring-pulse{
  0%, 100% { transform: scale(1);    box-shadow: 0 0 10px rgba(234,88,12,.45); }
  50%      { transform: scale(1.18); box-shadow: 0 0 16px rgba(234,88,12,.72); }
}

/* ============================================================================
   BEAT: CHARGE (KF-03) — foil shimmer sweeps, seam energy builds, motes lift,
   push continues, near-imperceptible handheld energy.  "It's waking up…"
   ============================================================================ */
.op-stage[data-beat="charge"] .op-camera{ transform: scale(1.22) translateY(16px); }
.op-stage[data-beat="charge"] .op-pack{
  animation: op-handheld calc(3200ms / var(--op-rate)) ease-in-out infinite;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="charge"] .op-pack__foil{
  opacity: 1;
  animation: op-foil calc(1900ms / var(--op-rate)) var(--mo-ease-standard) infinite;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="charge"] .op-pack__seamglow{
  opacity: 1;
  animation: op-seambuild calc(2200ms / var(--op-rate)) var(--mo-ease-breathe) infinite;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="charge"] .op-particle{
  animation: op-rise calc(2600ms / var(--op-rate)) ease-in infinite;
  animation-play-state: var(--op-play);
}
@keyframes op-foil{
  0%   { transform: rotate(8deg) translateX(-160%); }
  100% { transform: rotate(8deg) translateX(440%); }
}
@keyframes op-handheld{
  0%   { transform: translate(0, 0); }
  25%  { transform: translate(0.8px, -0.6px); }
  50%  { transform: translate(-0.6px, 0.5px); }
  75%  { transform: translate(0.5px, 0.7px); }
  100% { transform: translate(0, 0); }
}
@keyframes op-seambuild{
  0%, 100% { opacity: .7; transform: scaleX(.92); }
  50%      { opacity: 1;  transform: scaleX(1.08); }
}
@keyframes op-rise{
  0%   { transform: translateY(0) scale(.6); opacity: 0; }
  15%  { opacity: .9; }
  100% { transform: translateY(-130px) scale(1); opacity: 0; }
}

/* ============================================================================
   BEAT: PAUSE (KF-04) — the held breath. THE secret beat.
   Everything that was moving stops; the camera locks tight on the seam; the
   frame darkens; the seam pools bright-hot and throbs ONCE.  "Here it comes—"
   (charge animations stop automatically on the data-beat switch; pack + body
    ease to rest via their transitions; affordance freezes — no more pulse.)
   ============================================================================ */
.op-stage[data-beat="pause"] .op-camera{ transform: scale(1.28) translateY(20px); }
.op-stage[data-beat="pause"] .op-pack__rim{ opacity: .8; }
.op-stage[data-beat="pause"] .op-vignette--tight{ opacity: 1; }
.op-stage[data-beat="pause"] .op-vignette--pause{ opacity: 1; }
.op-stage[data-beat="pause"] .op-affordance{ opacity: .9; transform: translate(-50%, 0); }
.op-stage[data-beat="pause"] .op-pack__seamglow{
  opacity: 1;
  animation: op-throb calc(1300ms / var(--op-rate)) var(--mo-ease-standard) 1 both;
  animation-play-state: var(--op-play);
}
@keyframes op-throb{
  0%   { opacity: .85; transform: scale(1);    filter: brightness(1);    }
  35%  { opacity: 1;   transform: scale(1.30); filter: brightness(1.7);  }
  100% { opacity: 1;   transform: scale(1.06); filter: brightness(1.28); }
}

/* ============================================================================
   TEAR elements (base) — jagged rip, flash, foil fragments
   ============================================================================ */
.op-pack__rip{
  position: absolute; left: 6px; right: 6px; top: 38px; height: 18px; z-index: 1;
  background: linear-gradient(180deg, rgba(255,240,210,0), rgba(255,248,228,.95) 50%, rgba(255,240,210,0));
  clip-path: polygon(0 45%, 8% 20%, 16% 55%, 24% 22%, 33% 58%, 42% 24%, 50% 56%, 58% 22%, 67% 58%, 76% 24%, 84% 55%, 92% 20%, 100% 48%, 100% 62%, 0 64%);
  opacity: 0; transform: scaleY(.15); transform-origin: center; mix-blend-mode: screen; pointer-events: none;
}
.op-pack__flash{
  position: absolute; left: 50%; top: 34px; width: 170px; height: 170px;
  margin-left: -85px; margin-top: -85px; z-index: 3; border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255,255,255,.95), rgba(255,232,196,.5) 36%, rgba(255,200,140,0) 70%);
  opacity: 0; transform: scale(.3); mix-blend-mode: screen; pointer-events: none;
}
.op-debris{ position: absolute; left: 0; right: 0; top: 42px; height: 0; z-index: 4; pointer-events: none; }
.op-shard{
  position: absolute; left: 50%; top: 0; width: 7px; height: 3px; margin-left: -3px; border-radius: 1px;
  background: linear-gradient(90deg, #ffd9a0, #ea580c); opacity: 0;
}

/* ============================================================================
   BEAT: TEAR (KF-05) — rupture / release.  "HERE IT IS—!"
   Camera impact kick; flash bursts from the seam; foil fragments fling out;
   the jagged rip opens. One-shot, fast, decisive.
   ============================================================================ */
.op-stage[data-beat="tear"] .op-camera{
  animation: op-camkick calc(340ms / var(--op-rate)) var(--mo-ease-standard) 1 both;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="tear"] .op-pack__rip{
  animation: op-rip calc(420ms / var(--op-rate)) var(--mo-ease-decelerate) 1 both;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="tear"] .op-pack__flash{
  animation: op-flash calc(620ms / var(--op-rate)) var(--mo-ease-standard) 1 both;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="tear"] .op-shard{
  animation: op-shard calc(560ms / var(--op-rate)) var(--mo-ease-decelerate) 1 both;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="tear"] .op-vignette--pause{ opacity: 0; }
.op-stage[data-beat="tear"] .op-vignette--tight{ opacity: .5; }

@keyframes op-camkick{
  0%   { transform: scale(1.28) translateY(20px); }
  16%  { transform: scale(1.34) translateY(12px) translateX(-4px) rotate(-.4deg); }
  34%  { transform: scale(1.30) translateY(17px) translateX(4px) rotate(.3deg); }
  60%  { transform: scale(1.315) translateY(15px); }
  100% { transform: scale(1.30) translateY(16px); }
}
@keyframes op-rip{
  0%   { opacity: 0;   transform: scaleY(.12); }
  18%  { opacity: 1;   transform: scaleY(1);   }
  100% { opacity: .92; transform: scaleY(2.6); }
}
@keyframes op-flash{
  0%   { opacity: 0; transform: scale(.3);   }
  10%  { opacity: 1; transform: scale(1.05); }
  100% { opacity: 0; transform: scale(2.6);  }
}
@keyframes op-shard{
  0%   { transform: translate(0,0) rotate(0deg); opacity: 0; }
  12%  { opacity: 1; }
  100% { transform: translate(var(--dx,0), var(--dy,-60px)) rotate(var(--rot,120deg)); opacity: 0; }
}

/* ============================================================================
   BURST elements (base) — white-hot core, volumetric rays, radiating sparks
   ============================================================================ */
.op-core{
  position: absolute; left: 50%; top: 42%; width: 180px; height: 180px;
  margin: -90px 0 0 -90px; border-radius: 50%; z-index: 1;
  background: radial-gradient(closest-side, rgba(255,255,255,.95), rgba(255,224,180,.5) 40%, rgba(255,180,120,0) 72%);
  opacity: 0; transform: scale(.4); mix-blend-mode: screen; pointer-events: none;
  transition:
    opacity   calc(var(--mo-dur-slow) / var(--op-rate)) var(--mo-ease-standard),
    transform calc(var(--mo-dur-slow) / var(--op-rate)) var(--mo-ease-standard);
}
.op-rays{
  position: absolute; left: 50%; top: 42%; width: 360px; height: 360px;
  margin: -180px 0 0 -180px; z-index: 1;
  background: repeating-conic-gradient(from 0deg, rgba(255,205,150,0) 0deg 4deg, rgba(255,205,150,.18) 4deg 5.5deg, rgba(255,205,150,0) 5.5deg 10deg);
  -webkit-mask: radial-gradient(closest-side, transparent 16%, #000 32%, transparent 80%);
          mask: radial-gradient(closest-side, transparent 16%, #000 32%, transparent 80%);
  opacity: 0; mix-blend-mode: screen; pointer-events: none;
}
.op-burst{ position: absolute; left: 50%; top: 42%; width: 0; height: 0; z-index: 5; pointer-events: none; }
.op-spark{
  position: absolute; left: 0; top: 0; width: 5px; height: 5px; margin: -2.5px; border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255,240,210,.95), rgba(234,88,12,0));
  opacity: 0;
}

/* ============================================================================
   BEAT: BURST (KF-06) — particles bloom, core whites-out then cools, rays
   spread, the pack shell dissolves, camera pulls back.  "Whoa—"
   The cooled core glow is left behind for the card to rise from (KF-07).
   ============================================================================ */
.op-stage[data-beat="burst"] .op-camera{ transform: scale(1.12) translateY(0); }
.op-stage[data-beat="burst"] .op-pack__body,
.op-stage[data-beat="reveal"] .op-pack__body,
.op-stage[data-beat="flip"] .op-pack__body,
.op-stage[data-beat="verdict"] .op-pack__body,
.op-stage[data-beat="afterglow"] .op-pack__body{ opacity: 0; transform: translateY(10px) scale(.96); }
.op-stage[data-beat="burst"] .op-pack__shadow,
.op-stage[data-beat="reveal"] .op-pack__shadow,
.op-stage[data-beat="flip"] .op-pack__shadow,
.op-stage[data-beat="verdict"] .op-pack__shadow,
.op-stage[data-beat="afterglow"] .op-pack__shadow{ opacity: 0; }
.op-stage[data-beat="burst"] .op-core{
  animation: op-coreburst calc(900ms / var(--op-rate)) var(--mo-ease-decelerate) 1 both;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="burst"] .op-rays{
  animation: op-raysin calc(1100ms / var(--op-rate)) var(--mo-ease-standard) 1 both;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="burst"] .op-spark{
  animation: op-spark calc(820ms / var(--op-rate)) var(--mo-ease-decelerate) 1 both;
  animation-play-state: var(--op-play);
}
@keyframes op-coreburst{
  0%   { opacity: 0;  transform: scale(.4);   }
  18%  { opacity: 1;  transform: scale(1.25); }  /* white-hot bloom */
  100% { opacity: .8; transform: scale(1);    }  /* cools to a steady soft glow */
}
@keyframes op-raysin{
  0%   { opacity: 0;   transform: scale(.6) rotate(-8deg); }
  25%  { opacity: .7;  }
  100% { opacity: .32; transform: scale(1.1) rotate(6deg); }
}
@keyframes op-spark{
  0%   { transform: translate(0,0) scale(1);   opacity: 0; }
  10%  { opacity: 1; }
  100% { transform: translate(var(--dx,0), var(--dy,0)) scale(.5); opacity: 0; }
}

/* ============================================================================
   CARD (slab) — rises from the burst glow, flips to reveal. Modular: the
   .op-slab__art block is the swap point for the real graded-card image.
   ============================================================================ */
.op-card{
  position: absolute; left: 50%; top: 42%;
  height: clamp(300px, 54vh, 450px); width: auto; aspect-ratio: 660 / 860; /* HERO slab — scales with viewport, 660:860 */
  z-index: 3; perspective: 1100px;
  opacity: 0; transform: translate(-50%, -50%) translateY(120px) scale(.9);
  transition:
    transform calc(var(--mo-dur-cinematic) / var(--op-rate)) var(--mo-ease-decelerate),
    opacity   calc(var(--mo-dur-cinematic) / var(--op-rate)) var(--mo-ease-standard);
}
.op-card__inner{
  position: absolute; inset: 0; transform-style: preserve-3d;
  transition: transform calc(720ms / var(--op-rate)) var(--mo-ease-back-out); /* flip: weight + overshoot */
}
.op-card__face{
  position: absolute; inset: 0; border-radius: 12px; overflow: hidden;
  -webkit-backface-visibility: hidden; backface-visibility: hidden;
  box-shadow: 0 24px 48px rgba(0,0,0,.6);
}
.op-card__back{
  background: linear-gradient(150deg, #1c1c24, #101015);
  border: 1px solid #2a2a33; display: grid; place-items: center;
}
.op-card__back::after{
  content: "RIPVAULT"; color: #3c3c47;
  font: 500 11px/1 system-ui, sans-serif; letter-spacing: .2em;
}
.op-card__front{
  transform: rotateY(180deg);
  background: #0c0c10; border: 1px solid #2a2a33;
}
.op-slab__grade{
  position: absolute; top: 6px; left: 6px; right: 6px; height: 22px;
  background: #16161c; border-radius: 4px; color: #cfcfd6;
  font: 500 10px/22px system-ui, sans-serif; letter-spacing: .12em; text-align: center;
}
/* Real graded-card slab image (660x860 framed WebP straight from the open result) */
.op-slab__art{
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; border-radius: 12px;
  background: linear-gradient(160deg, #23232c, #15151b); /* fallback while the image loads */
}
.op-fall{ position: absolute; left: 50%; top: 18%; width: 200px; height: 210px; margin-left: -100px; z-index: 4; pointer-events: none; }
.op-fallmote{
  position: absolute; top: 0; width: 3px; height: 3px; border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255,225,180,.85), rgba(234,88,12,0)); opacity: 0;
}

/* ============================================================================
   BEAT: REVEAL (KF-07) — the slab rises from the glow, face-DOWN, hovering and
   slowly turning; backlight halo; motes drift down. "What is it—" (micro-eternity).
   The card stays risen through flip / verdict / afterglow.
   ============================================================================ */
.op-stage[data-beat="reveal"] .op-card,
.op-stage[data-beat="flip"] .op-card,
.op-stage[data-beat="verdict"] .op-card,
.op-stage[data-beat="afterglow"] .op-card{ opacity: 1; transform: translate(-50%, -50%) translateY(0) scale(1); }

.op-stage[data-beat="reveal"] .op-camera{ transform: scale(1.16) translateY(-6px); }
.op-stage[data-beat="reveal"] .op-card__inner{
  animation: op-cardhover calc(3600ms / var(--op-rate)) var(--mo-ease-breathe) infinite;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="reveal"] .op-core,
.op-stage[data-beat="flip"] .op-core{ opacity: .7; transform: scale(1.1); }
.op-stage[data-beat="reveal"] .op-fallmote{
  animation: op-fall calc(3400ms / var(--op-rate)) linear infinite;
  animation-play-state: var(--op-play);
}
@keyframes op-cardhover{
  0%, 100% { transform: translateY(0)    rotateY(-12deg); }
  50%      { transform: translateY(-8px) rotateY(12deg);  }
}
@keyframes op-fall{
  0%   { transform: translateY(-20px); opacity: 0;  }
  15%  { opacity: .8; }
  85%  { opacity: .8; }
  100% { transform: translateY(170px); opacity: 0;  }
}

/* ============================================================================
   BEAT: FLIP (KF-08) — the slab turns face-UP with weight + overshoot; a light
   rake sweeps the face as it comes round; camera locks to front. "..."
   ============================================================================ */
.op-card__glare{
  position: absolute; top: -10%; bottom: -10%; left: -40%; width: 56px;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.55), transparent);
  transform: rotate(10deg) translateX(-220%); opacity: 0;
  mix-blend-mode: screen; pointer-events: none;
}
.op-stage[data-beat="flip"] .op-card__inner,
.op-stage[data-beat="verdict"] .op-card__inner,
.op-stage[data-beat="afterglow"] .op-card__inner{ transform: rotateY(180deg); }
.op-stage[data-beat="flip"] .op-camera{ transform: scale(1.18) translateY(-4px); }
.op-stage[data-beat="flip"] .op-card__glare{
  animation: op-glare calc(640ms / var(--op-rate)) var(--mo-ease-standard) calc(180ms / var(--op-rate)) 1 both;
  animation-play-state: var(--op-play);
}
@keyframes op-glare{
  0%   { transform: rotate(10deg) translateX(-220%); opacity: 0;  }
  30%  { opacity: .9; }
  100% { transform: rotate(10deg) translateX(360%);  opacity: 0;  }
}

/* ============================================================================
   RARITY SYSTEM (KF-09) — every verdict effect scales from these per-rarity
   tokens, so lighting / particles / color / camera-shake / rim / sound tune
   INDEPENDENTLY. Change a number here to retune one dimension for one tier.
   --rar-sound is reserved for the audio layer (read by JS later; not wired yet).
   ============================================================================ */
.op-stage[data-rarity="common"]{
  --rar-color:#16a34a; --rar-color2:#34d399;
  --rar-glow:.28; --rar-rays:0;   --rar-spark:0;   --rar-shake:0px; --rar-rim:.35; --rar-bloom:1.2; --rar-hold:0ms;   --rar-sound:.25;
}
.op-stage[data-rarity="rare"]{
  --rar-color:#2563eb; --rar-color2:#60a5fa;
  --rar-glow:.55; --rar-rays:.18; --rar-spark:.5;  --rar-shake:2px; --rar-rim:.6;  --rar-bloom:1.5; --rar-hold:0ms;   --rar-sound:.5;
}
.op-stage[data-rarity="epic"]{
  --rar-color:#7c3aed; --rar-color2:#c084fc;
  --rar-glow:.8;  --rar-rays:.36; --rar-spark:.85; --rar-shake:4px; --rar-rim:.85; --rar-bloom:1.8; --rar-hold:0ms;   --rar-sound:.75;
}
.op-stage[data-rarity="mythic"]{
  --rar-color:#ea580c; --rar-color2:#fbbf24;
  --rar-glow:1;   --rar-rays:.6;  --rar-spark:1;   --rar-shake:6px; --rar-rim:1;   --rar-bloom:2.1; --rar-hold:380ms; --rar-sound:1;
}

/* verdict elements (base — invisible until the verdict beat) */
.op-verdict-glow{
  position: absolute; left: 50%; top: 42%; width: 400px; height: 400px; margin: -200px 0 0 -200px;
  border-radius: 50%; z-index: 2; pointer-events: none; mix-blend-mode: screen;
  background: radial-gradient(closest-side, var(--rar-color2,#fff), var(--rar-color,#fff) 36%, transparent 70%);
  opacity: 0; transform: scale(.3);
}
.op-verdict-rays{
  position: absolute; left: 50%; top: 42%; width: 580px; height: 580px; margin: -290px 0 0 -290px; z-index: 2;
  background: repeating-conic-gradient(from 0deg, transparent 0deg 5deg, var(--rar-color,#fff) 5deg 6deg, transparent 6deg 11deg);
  -webkit-mask: radial-gradient(closest-side, transparent 22%, #000 40%, transparent 82%);
          mask: radial-gradient(closest-side, transparent 22%, #000 40%, transparent 82%);
  opacity: 0; mix-blend-mode: screen; pointer-events: none;
}
.op-vparticles{ position: absolute; left: 50%; top: 42%; width: 0; height: 0; z-index: 5; pointer-events: none; }
.op-vspark{
  position: absolute; left: 0; top: 0; width: 5px; height: 5px; margin: -2.5px; border-radius: 50%;
  background: radial-gradient(closest-side, var(--rar-color2,#fff), transparent); opacity: 0;
}
.op-card__rarglow{
  position: absolute; inset: -2px; border-radius: 13px; pointer-events: none; z-index: 5;
  box-shadow: 0 0 calc(18px + 34px * var(--rar-glow,0)) var(--rar-color, transparent),
              inset 0 0 12px var(--rar-color, transparent);
  opacity: 0; transition: opacity calc(var(--mo-dur-slow) / var(--op-rate)) var(--mo-ease-standard);
}
.op-timeslow{
  position: absolute; inset: 0; z-index: 7; pointer-events: none;
  background: radial-gradient(58% 48% at 50% 42%, transparent 0%, rgba(0,0,0,.72) 100%);
  opacity: 0;
}

/* ============================================================================
   BEAT: VERDICT (KF-09) — rarity bloom. Common: clean confirm · Rare: richer ·
   Epic: dramatic burst · Mythic: time holds, then full hero bloom.
   ============================================================================ */
.op-stage[data-beat="verdict"] .op-card{ transform: translate(-50%, -50%) scale(1.05); }
.op-stage[data-beat="verdict"] .op-card__rarglow,
.op-stage[data-beat="afterglow"] .op-card__rarglow{ opacity: var(--rar-rim,.4); }

.op-stage[data-beat="verdict"] .op-verdict-glow{
  animation: op-vbloom calc(900ms / var(--op-rate)) var(--mo-ease-decelerate) var(--rar-hold,0ms) 1 both;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="verdict"] .op-verdict-rays{
  animation: op-vrays calc(2600ms / var(--op-rate)) linear var(--rar-hold,0ms) 1 both;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="verdict"] .op-vspark{
  animation: op-vspark calc(1400ms / var(--op-rate)) var(--mo-ease-decelerate) var(--rar-hold,0ms) 1 both;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="verdict"] .op-camera{
  animation: op-vcam calc(720ms / var(--op-rate)) var(--mo-ease-standard) var(--rar-hold,0ms) 1 both;
  animation-play-state: var(--op-play);
}
.op-stage[data-beat="verdict"][data-rarity="mythic"] .op-timeslow{
  animation: op-timeslow calc(1100ms / var(--op-rate)) var(--mo-ease-standard) 1 both;
  animation-play-state: var(--op-play);
}
@keyframes op-vbloom{
  0%   { opacity: 0; transform: scale(.3); }
  30%  { opacity: var(--rar-glow); transform: scale(var(--rar-bloom)); }
  100% { opacity: calc(var(--rar-glow) * .68); transform: scale(calc(var(--rar-bloom) * .82)); }
}
@keyframes op-vrays{
  0%   { opacity: 0; transform: rotate(0deg) scale(.7); }
  25%  { opacity: var(--rar-rays); }
  100% { opacity: calc(var(--rar-rays) * .8); transform: rotate(38deg) scale(1.04); }
}
@keyframes op-vspark{
  0%   { transform: translate(0,0) scale(1); opacity: 0; }
  18%  { opacity: var(--rar-spark); }
  100% { transform: translate(var(--dx,0), var(--dy,0)) scale(.4); opacity: 0; }
}
@keyframes op-vcam{
  0%   { transform: scale(1.18) translateY(-4px); }
  18%  { transform: scale(1.21)  translateY(-7px) translateX(calc(var(--rar-shake,0px) * -1)); }
  36%  { transform: scale(1.195) translateY(-5px) translateX(var(--rar-shake,0px)); }
  100% { transform: scale(1.2)   translateY(-6px); }
}
@keyframes op-timeslow{
  0%   { opacity: 0;   }
  22%  { opacity: .85; }
  42%  { opacity: .85; }
  68%  { opacity: 0;   }
  100% { opacity: 0;   }
}

/* ============================================================================
   RESULT panel (afterglow) — value + grade + CTAs (Hold / Trade / Sell back)
   ============================================================================ */
.op-result{
  position: absolute; left: 50%; bottom: 7%; transform: translate(-50%, 18px);
  width: min(400px, 90%); z-index: 8; text-align: center;
  opacity: 0; pointer-events: none;
  transition:
    opacity   calc(var(--mo-dur-slower) / var(--op-rate)) var(--mo-ease-standard),
    transform calc(var(--mo-dur-slower) / var(--op-rate)) var(--mo-ease-decelerate);
  transition-delay: calc(var(--mo-dur-base) / var(--op-rate));
}
.op-result__name{ color: #e7e7ea; font: 500 14px/1.3 system-ui, sans-serif; }
.op-result__meta{ color: var(--rar-color2, #cfcfd6); font: 500 13px/1 system-ui, sans-serif; margin-top: 4px; letter-spacing: .04em; }
.op-result__cta{ display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 14px; }
.op-cta{
  flex: 1; max-width: 104px; padding: 9px 0; border-radius: 9px; cursor: pointer;
  font: 500 12px/1 system-ui, sans-serif; border: 1px solid #2a2a33; background: #16161c; color: #d4d4da;
}
.op-cta--primary{ background: var(--op-accent); border-color: var(--op-accent); color: #fff; }
.op-cta[disabled]{ opacity: .5; cursor: default; }

/* ============================================================================
   INTEGRATION: full-screen reveal overlay + skip control (used by app.js when
   the animation plays the real /api/pack/open result). Self-contained.
   ============================================================================ */
.op-overlay{ position: fixed; inset: 0; z-index: 4000; background: #060608; }
.op-overlay .op-stage{ height: 100%; }
.op-skip{
  position: absolute; top: 16px; right: 16px; z-index: 20;
  background: rgba(20,20,26,.7); color: #cfcfd6; border: 1px solid #2a2a33;
  border-radius: 8px; padding: 7px 14px; font: 500 13px/1 system-ui, sans-serif; cursor: pointer;
}
.op-skip:hover{ background: rgba(30,30,38,.9); }

/* ============================================================================
   BEAT: AFTERGLOW (KF-10) — calm settle. Camera eases back to a product shot;
   the bloom recedes to a soft tier accent (rim + faint halo); value + grade +
   Hold/Trade/Sell-back rise in. "That's mine now. What do I do with it?"
   ============================================================================ */
.op-stage[data-beat="afterglow"] .op-camera{ transform: scale(1) translateY(-34px); }
.op-stage[data-beat="afterglow"] .op-core{ opacity: .3; transform: scale(1.05); }
.op-stage[data-beat="afterglow"] .op-result{ opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }

/* ---- ACCESSIBILITY: respect reduced-motion (brief requirement) ---- */
@media (prefers-reduced-motion: reduce){
  .op-stage[data-beat] .op-pack__body,
  .op-stage[data-beat] .op-pack__shadow,
  .op-stage[data-beat] .op-ambient,
  .op-stage[data-beat] .op-pack__sheen,
  .op-stage[data-beat] .op-affordance__ring,
  .op-stage[data-beat] .op-pack,
  .op-stage[data-beat] .op-pack__foil,
  .op-stage[data-beat] .op-pack__seamglow,
  .op-stage[data-beat] .op-particle,
  .op-stage[data-beat] .op-camera,
  .op-stage[data-beat] .op-pack__rip,
  .op-stage[data-beat] .op-pack__flash,
  .op-stage[data-beat] .op-shard,
  .op-stage[data-beat] .op-core,
  .op-stage[data-beat] .op-rays,
  .op-stage[data-beat] .op-spark,
  .op-stage[data-beat] .op-card__inner,
  .op-stage[data-beat] .op-fallmote,
  .op-stage[data-beat] .op-card__glare,
  .op-stage[data-beat] .op-verdict-glow,
  .op-stage[data-beat] .op-verdict-rays,
  .op-stage[data-beat] .op-vspark,
  .op-stage[data-beat] .op-timeslow{
    animation: none !important;
  }
  .op-camera, .op-pack, .op-pack__body, .op-pack__rim, .op-pack__seamglow,
  .op-affordance__ring, .op-vignette--tight, .op-vignette--pause, .op-affordance,
  .op-card, .op-card__inner, .op-core, .op-card__rarglow, .op-result{
    transition: none !important;
  }
  /* verdict still shows the rarity bloom + rim, just statically (no shake/timeslow) */
  .op-stage[data-beat="verdict"] .op-verdict-glow{ opacity: var(--rar-glow); transform: scale(var(--rar-bloom)); }
  .op-stage[data-beat="verdict"] .op-verdict-rays{ opacity: var(--rar-rays); }
  /* hold the framing without the violent kick */
  .op-stage[data-beat="tear"] .op-camera{ transform: scale(1.30) translateY(16px); }
  .op-stage[data-beat="tear"] .op-pack__rip{ opacity: .9; transform: scaleY(2); }
  /* burst resolves to the cooled glow, no bloom/sparks */
  .op-stage[data-beat="burst"] .op-core{ opacity: .8; transform: scale(1); }
  .op-stage[data-beat="burst"] .op-rays{ opacity: .3; }
}
