/* ==========================================================================
   Scanova feature-preview harness
   --------------------------------------------------------------------------
   A looping, screenshot-based "here's the new feature" animation built only
   from stacked images + CSS keyframes. No video, no GIF, no runtime library.

   Every preview shares one timeline. A preview declares its own numbers as
   custom properties on .sp-preview (see dark-mode/index.html), so retiming or
   re-aiming a preview never means touching keyframes.

   DOM contract
   ------------
   .sp-preview                    outer frame, clips the zoom, sets aspect ratio
     .sp-stage                    scaled/zoomed layer
       img.sp-frame.is-base       the "before" screenshot
       img.sp-frame.is-target     the "after" screenshot, crossfaded in
       .sp-cursor-track           full-size layer, translated to aim the cursor
         .sp-cursor-scale         counter-scales so the cursor ignores the zoom
           .sp-click-ring         click feedback, centred on the cursor tip
           svg.sp-cursor-arrow    the pointer, tip at the element origin

   Timeline (fractions of --sp-duration, shared by every keyframe block below)
   --------------------------------------------------------------------------
     0     -> 7.78%   hold the base frame, cursor parked
     7.78  -> 17.78%  zoom in toward the control
     17.78 -> 26.67%  cursor travels to the control
     26.67 -> 28.89%  press + click ring
     28.89 -> 37.78%  zoom back out to the full screen
     35.00 -> 41.67%  crossfade base -> target (overlaps the last of the zoom
                      out, so the change reads as a response to the click)
     41.67 -> 68.89%  hold the target frame
     68.89 -> 71.11%  second press + click ring (no zoom this time)
     71.11 -> 77.78%  crossfade target -> base
     77.78 -> 86.67%  cursor drifts back to its parked position
     86.67 -> 100%    hold, then loop
   ========================================================================== */

.sp-preview {
  /* ---- tunables: override these per preview ---- */
  --sp-duration: 9s;
  --sp-aspect: 1920 / 1091;   /* shipping frame dimensions, per optimize.py */
  --sp-zoom: 1.9;             /* how far we push in on the control */
  --sp-zoom-inv: 0.5263;      /* 1 / --sp-zoom, keeps the cursor a fixed size */
  --sp-target-x: 96.25%;      /* the control, as a fraction of the screenshot */
  --sp-target-y: 4.23%;
  --sp-park-x: 78%;           /* where the cursor idles between loops */
  --sp-park-y: 30%;
  --sp-cursor-size: 22px;
  --sp-ring-size: 38px;
  --sp-ring-color: 216 60 45; /* Scanova red, as R G B for rgb()/alpha use */
  --sp-radius: 12px;

  position: relative;
  width: 100%;
  aspect-ratio: var(--sp-aspect);
  overflow: hidden;
  border-radius: var(--sp-radius);
  background: #f6f7f9;
  /* Frame it lightly so it reads as a product surface, not a raw image.
     Drop this rule in-app if the modal already provides a container. */
  box-shadow:
    0 0 0 1px rgb(0 0 0 / 8%),
    0 12px 32px -12px rgb(16 24 40 / 22%);
  /* Hint the compositor: this element animates for its whole life. */
  contain: paint;
}

/* --- the zoomed layer ---------------------------------------------------- */

.sp-stage {
  position: absolute;
  inset: 0;
  /* Scaling about the control itself means the control never moves on screen
     during the zoom — the surrounding UI grows away from it. No jump cut. */
  transform-origin: var(--sp-target-x) var(--sp-target-y);
  animation: sp-zoom var(--sp-duration) infinite;
  will-change: transform;
}

/* --- the two screenshots ------------------------------------------------- */

.sp-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* Guards against the two captures differing by a pixel or two. */
  object-position: center;
}

.sp-frame.is-target {
  opacity: 0;
  animation: sp-crossfade var(--sp-duration) infinite;
  will-change: opacity;
}

/* --- cursor ------------------------------------------------------------- */

/* Full-size layer, so its percentage translate resolves against the stage.
   Cheaper and smoother than animating left/top. */
.sp-cursor-track {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  animation: sp-cursor-move var(--sp-duration) infinite;
  will-change: transform;
  pointer-events: none;
}

.sp-cursor-scale {
  position: absolute;
  top: 0;
  left: 0;
  animation: sp-cursor-counterscale var(--sp-duration) infinite;
  transform-origin: 0 0;
}

.sp-cursor-arrow {
  display: block;
  width: var(--sp-cursor-size);
  height: auto;
  transform-origin: 0 0;
  animation: sp-cursor-press var(--sp-duration) infinite;
  filter: drop-shadow(0 1px 2px rgb(0 0 0 / 35%));
  /* The arrow's tip is at viewBox 0,0 — i.e. exactly at this element's origin,
     which is what puts the tip on the control rather than near it. That leaves
     the outer half of the stroke outside the box, so it must not be clipped. */
  overflow: visible;
}

.sp-click-ring {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--sp-ring-size);
  height: var(--sp-ring-size);
  /* Centred on the cursor tip, which sits at the element origin. */
  margin: calc(var(--sp-ring-size) / -2) 0 0 calc(var(--sp-ring-size) / -2);
  border-radius: 50%;
  border: 2.5px solid rgb(var(--sp-ring-color) / 90%);
  background: radial-gradient(
    circle,
    rgb(var(--sp-ring-color) / 28%) 0%,
    rgb(var(--sp-ring-color) / 0%) 70%
  );
  /* White halo keeps the ring legible against the dark frame too. */
  box-shadow: 0 0 0 2px rgb(255 255 255 / 30%);
  opacity: 0;
  animation: sp-click-ring var(--sp-duration) infinite;
}

/* ==========================================================================
   Keyframes
   Timing functions are declared *inside* keyframes: each one governs the
   segment that starts at that keyframe, which is how one animation gets
   per-segment easing.
   ========================================================================== */

@keyframes sp-zoom {
  0%      { transform: scale(1);              animation-timing-function: linear; }
  7.78%   { transform: scale(1);              animation-timing-function: cubic-bezier(.32, 0, .24, 1); }
  17.78%  { transform: scale(var(--sp-zoom)); animation-timing-function: linear; }
  28.89%  { transform: scale(var(--sp-zoom)); animation-timing-function: cubic-bezier(.32, 0, .24, 1); }
  37.78%  { transform: scale(1); }
  100%    { transform: scale(1); }
}

/* Mirrors sp-zoom exactly, so the pointer stays the same on-screen size while
   the UI behind it grows. */
@keyframes sp-cursor-counterscale {
  0%      { scale: 1;                   animation-timing-function: linear; }
  7.78%   { scale: 1;                   animation-timing-function: cubic-bezier(.32, 0, .24, 1); }
  17.78%  { scale: var(--sp-zoom-inv);  animation-timing-function: linear; }
  28.89%  { scale: var(--sp-zoom-inv);  animation-timing-function: cubic-bezier(.32, 0, .24, 1); }
  37.78%  { scale: 1; }
  100%    { scale: 1; }
}

@keyframes sp-cursor-move {
  0%      { transform: translate(var(--sp-park-x), var(--sp-park-y));     animation-timing-function: linear; }
  17.78%  { transform: translate(var(--sp-park-x), var(--sp-park-y));     animation-timing-function: cubic-bezier(.42, 0, .22, 1); }
  26.67%  { transform: translate(var(--sp-target-x), var(--sp-target-y)); animation-timing-function: linear; }
  77.78%  { transform: translate(var(--sp-target-x), var(--sp-target-y)); animation-timing-function: cubic-bezier(.4, 0, .3, 1); }
  86.67%  { transform: translate(var(--sp-park-x), var(--sp-park-y)); }
  100%    { transform: translate(var(--sp-park-x), var(--sp-park-y)); }
}

@keyframes sp-cursor-press {
  0%,
  26.11%  { scale: 1; }
  27.22%  { scale: 0.82; }
  28.89%  { scale: 1; }
  68.33%  { scale: 1; }
  69.44%  { scale: 0.82; }
  71.11%  { scale: 1; }
  100%    { scale: 1; }
}

@keyframes sp-click-ring {
  0%,
  26.60%  { opacity: 0;   scale: 0.30; animation-timing-function: cubic-bezier(.2, .7, .3, 1); }
  26.67%  { opacity: 0.85; scale: 0.30; }
  32.78%  { opacity: 0;   scale: 1; }
  68.80%  { opacity: 0;   scale: 0.30; animation-timing-function: cubic-bezier(.2, .7, .3, 1); }
  68.89%  { opacity: 0.85; scale: 0.30; }
  75.00%  { opacity: 0;   scale: 1; }
  100%    { opacity: 0;   scale: 1; }
}

@keyframes sp-crossfade {
  0%,
  35.00%  { opacity: 0; animation-timing-function: cubic-bezier(.4, 0, .2, 1); }
  41.67%  { opacity: 1; }
  71.11%  { opacity: 1; animation-timing-function: cubic-bezier(.4, 0, .2, 1); }
  77.78%  { opacity: 0; }
  100%    { opacity: 0; }
}

/* ==========================================================================
   Paused state — set by the IntersectionObserver in the page, so a preview
   scrolled out of view stops burning frames.
   ========================================================================== */

.sp-preview.is-paused .sp-stage,
.sp-preview.is-paused .sp-frame.is-target,
.sp-preview.is-paused .sp-cursor-track,
.sp-preview.is-paused .sp-cursor-scale,
.sp-preview.is-paused .sp-cursor-arrow,
.sp-preview.is-paused .sp-click-ring {
  animation-play-state: paused;
}

/* ==========================================================================
   Reduced motion — no zoom, no cursor, no loop. Show the payoff frame as a
   still, which is what the animation was selling anyway.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .sp-stage,
  .sp-frame.is-target,
  .sp-cursor-track,
  .sp-cursor-scale,
  .sp-cursor-arrow,
  .sp-click-ring {
    animation: none !important;
  }

  .sp-frame.is-target { opacity: 1; }
  .sp-cursor-track    { display: none; }
}
