/* ===== Showcase carousel: focus-stop animation (scan + HUD data) =====
   Adds on top of the base .scene/.a3d/.card rules already defined in index.html.
   Loaded only for progressive enhancement; if this file or its JS fails to load,
   the base CSS keyframe rotation in index.html still runs. */

.scene{position:relative}

/* JS has taken over rotation: kill the CSS keyframe animation so our
   manually-driven `rotate` value isn't fought by the animation engine. */
.a3d.js-driven{animation:none}

/* ===== Mobile: flat filmstrip instead of the curved 3D ring =====
   JS (carousel-showcase.js) adds `.flat-mode` and, from then on, sets each
   card's `transform` directly every animation frame (a plain translateX —
   see applyFlatPositions). The base `.card` rule in index.html carries a
   1.15s transition on `transform` (meant for the desktop ring, whose
   per-card transform is otherwise static — only the parent ring's `rotate`
   animates there); left in place here, that transition would fight every
   one of those ~60/sec inline updates, each restarting a new 1.15s ease
   toward the latest position, so cards would visibly lag/smear behind
   their target instead of tracking the spin 1:1. Dropping `transform` from
   the transition list (keeping `filter`, so the dim/undim crossfade above
   still applies) lets the translateX apply immediately each frame, exactly
   like the ring's own JS-driven `rotate` property already does. */
.a3d.flat-mode .card{transition:filter 1.15s cubic-bezier(.4,0,.2,1)}

/* ===== Hidden until every clip is decoded, then reveal together =====
   Every card starts fully invisible via real `opacity`, not a `filter`
   dim — a <video> paints its decoded frame the instant decoding finishes,
   entirely on its own schedule (independent of fetch/network timing, and
   easily staggered across 16 clips decoding near-simultaneously on a
   device with limited concurrent hardware decoders). A filter dim only
   darkens that frame, it doesn't hide it, so whichever cards decoded
   first would still visibly pop in ahead of the rest. True `opacity:0` is
   airtight regardless of decode timing — see carousel-showcase.js, which
   only flips the whole ring to `.carousel-ready` once every single card
   has confirmed a decoded frame, all fading in together in one
   synchronized reveal with zero decode work left to stagger.
   No CSS transition is declared on `.card` itself for this: JS applies a
   temporary inline `transition` only for that one reveal, specifically so
   `.card.is-focused{opacity:0}` (further down) keeps its own instant,
   untransitioned snap on every later focus cycle instead of fighting
   this fade. */
.card{
  opacity:0;
}
.a3d.carousel-ready .card{
  opacity:1;
}

/* ===== Per-card empty-box placeholder while loading =====
   A separate sibling element per card (created in carousel-showcase.js,
   right before its card in DOM order), not a style on `.card` itself —
   a <video> can't show a background behind its own decoded frame since
   opacity/filter both apply to the whole element, content and background
   together, so there is no way to keep a card's real frame hidden while
   also showing a placeholder box on that same element. This box mirrors
   its card's exact position (the same --i/--ba/--ringD 3D transform
   formula as the base .card rule in index.html; kept in step on mobile by
   applyFlatPositions in the JS) and simply fades out once the ring
   reveals, in lockstep with its card fading in. */
.card-loading-box{
  --ba:1turn/var(--n);
  --ringD:calc((.5*var(--w) + .5em)/tan(.5*var(--ba)));
  grid-area:1/1;
  width:var(--w);
  aspect-ratio:4/4;
  border-radius:calc(var(--w) * 0.067);
  backface-visibility:hidden;
  border:1px solid rgba(255,255,255,0.1);
  box-shadow:0 25px 60px rgba(0,0,0,0.55);
  transform:
    rotatey(calc(var(--i)*var(--ba)))
    translatez(calc(-1 * var(--ringD)))
    scale(0.95);
  background:linear-gradient(120deg,
    rgba(1,228,160,.05) 20%,
    rgba(1,228,160,.16) 50%,
    rgba(1,228,160,.05) 80%);
  background-size:220% 220%;
  animation:cardLoadingSheen 3.4s ease-in-out infinite;
  transition:opacity 480ms ease;
  pointer-events:none;
}
@keyframes cardLoadingSheen{
  0%{background-position:0% 50%}
  50%{background-position:100% 50%}
  100%{background-position:0% 50%}
}
.a3d.carousel-ready .card-loading-box{
  opacity:0;
  animation:none;
}

/* Center "still loading" indicator — the AgenticEye eye mark, spinning in
   place (same brand treatment as .cta-eye further down the page). Purely
   an overlay: it never touches the ring's own visibility or transform, so
   it has no bearing on the ring's natural spin above. JS fades it out and
   removes it once every clip has a frame ready (or, for reduced-motion
   users and the true no-JS case, removes/never shows it at all and
   reveals the ring immediately instead — see revealImmediately() in
   carousel-showcase.js), per the opacity:0 default below. */
.carousel-loader{
  position:absolute;
  top:50%;left:50%;
  translate:-50% -50%;
  width:64px;height:64px;
  z-index:20;
  opacity:0;
  transition:opacity .5s ease;
  pointer-events:none;
}
.scene.carousel-loading .carousel-loader{opacity:1}
.loader-eye-icon,.loader-eye-icon-back{
  position:absolute;inset:0;
  width:64px;height:64px;
  transform-origin:center center;
}
.loader-eye-icon{
  z-index:1;
  mix-blend-mode:exclusion;
  filter:drop-shadow(0 0 14px rgba(1,228,160,0.4));
  animation:ctaEyeScan 7s cubic-bezier(.45,0,.4,1) infinite;
}
.loader-eye-icon-back{
  z-index:0;
  opacity:.35;
  animation:ctaEyeScanBack 10s cubic-bezier(.45,0,.4,1) infinite;
}
@media (prefers-reduced-motion: reduce){
  .carousel-loader{display:none}
}

/* Dim the rest of the ring while one card is focused. Darkened via
   brightness (still fully opaque), not opacity — opacity would let the
   scene's own background show through each card, reading as "see-through"
   rather than "dimmed". Plain filter only — no blur() here. blur() forces
   the browser to rasterize each filtered element into its own layer,
   which is real GPU cost times 15 simultaneously-animating cards;
   brightness/saturate are cheap, mostly compositor-only operations and
   read almost the same at this size. */
.a3d.has-focus .card:not(.is-focused){filter:brightness(.32) saturate(.7)}
/* The "focused" card in the ring itself just goes invisible in place — the
   thing that actually grows and glows is a flat 2D clone (.carousel-focus-
   clone, below), rendered in .scene's normal stacking order, *outside*
   the 3D ring entirely. Earlier attempts made the real card win the ring's
   preserve-3d depth-sort via a translateZ push, but that either broke its
   on-screen position (pushing the focused card itself) or required pushing
   *all* other cards back by multiples of the ring's own radius — which
   visually shrank/hid half the ring and made the whole thing look like it
   was falling apart. A flat clone sidesteps the 3D depth-sort problem
   completely: normal 2D stacking always wins. */
.card.is-focused{opacity:0}

.card{
  --fs:1;
  transform:
    rotatey(calc(var(--i)*var(--ba)))
    translatez(calc(-1 * var(--ringD)))
    scale(calc(var(--fs) * 0.95));
  /* the 0.95 factor above leaves a small gap to each neighboring card */
  /* filter's duration/easing matches the focus-clone's own 1.15s
     cubic-bezier shrink exactly (JS fires them in the same instant — see
     hideFocusFx in carousel-showcase.js), so the ring re-lights at the
     same pace the focused card takes to shrink back into place.
     Deliberately no opacity transition here — the card should snap to
     fully opaque the instant `is-focused` is removed. If it faded in over
     the same .3s the clone fades out over, there'd be a brief window where
     *both* are semi-transparent at once, letting the scene's background
     show through both layers — a visible flash right at the handoff. The
     clone (pixel-identical, sitting on top) still fades its own glow out
     smoothly; the now-instantly-opaque card underneath means that fade
     never reveals anything but the same image. */
  transition:transform 1.15s cubic-bezier(.4,0,.2,1),filter 1.15s cubic-bezier(.4,0,.2,1);
}

/* The flat clone that actually grows, glows, and carries the scan + HUD.
   JS sizes/positions it to match the real card's on-screen rect, then
   toggles `.active` to scale it up from there — see GROW_SCALE in
   carousel-showcase.js, which must match the 1.6 below. */
.carousel-focus-clone{
  position:absolute;
  object-fit:cover;
  border-radius:12em;
  z-index:10;
  opacity:0;
  will-change:transform;
  pointer-events:none;
  transform:scale(1);
  transform-origin:center center;
  /* Deliberately no opacity transition — same reasoning as .card's own
     (see carousel-showcase.css above it, and hideFocusFx in the JS): the
     clone is pixel-identical to the real card underneath, so it must
     appear/disappear in the same instant the real card disappears/
     reappears, or there's a brief window where both are simultaneously
     semi-transparent, letting the scene's background flash through both
     layers. The glow doesn't need opacity's help to fade smoothly — its
     own box-shadow transition below already fades it out independently,
     well before shrink even finishes. */
  transition:transform 1.15s cubic-bezier(.4,0,.2,1),box-shadow .5s ease;
}
.carousel-focus-clone.active{
  transform:scale(1.85);
  /* A subtle edge glow plus a grounding drop shadow — softer/dimmer than
     the original so it reads as a faint highlight, not a bright halo. */
  box-shadow:
    0 0 20px rgba(1,228,160,0.28),
    0 30px 70px rgba(0,0,0,0.6);
}

/* Video-only "return" tint: while a focused video clip shrinks back into
   the ring, a fully solid (not blended, not translucent) color box sits
   exactly over the clone — see .carousel-return-tint below. A CSS
   `filter` on the video itself can't produce a genuinely flat, uniform
   color (it's always a function of the underlying frame's own pixels, so
   it reads as a colorized/graded video rather than a solid color) — a
   plain opaque box positioned to match the clone exactly is the only way
   to get real flat color here. No mix-blend-mode anywhere in this. */
.carousel-return-tint{
  position:absolute;
  pointer-events:none;
  overflow:hidden;
  opacity:0;
  z-index:11;
  transition:opacity .35s ease;
}
.carousel-return-tint.active{opacity:1}
.carousel-return-tint.rc-green{background:#01E4A0}
.carousel-return-tint.rc-blue{background:#00B8FF}
.carousel-return-tint.rc-purple{background:#A855F7}
.carousel-return-tint.rc-amber{background:#F5A623}
@media (max-width:768px){
  .carousel-focus-clone{border-radius:3em}
  /* The clone grows symmetrically around the card's own center
     (transform-origin:center center) — at this scale, the mobile .scene
     height in index.html was bumped up alongside it so the grown clone
     still fits inside .scene's overflow:hidden bounds without clipping
     top/bottom (see the comment on .scene there). positionOverlays() in
     the JS reads the clone's actual rendered rect, so the scan/HUD
     overlays track this size automatically — nothing else needs to change
     to match it. */
  .carousel-focus-clone.active{transform:scale(1.7)}
}

/* Green scanning sweep overlaid on the focused card. A subtle dark-green
   wash sits under the scan lines/HUD to slightly darken the image and
   sell the "being scanned" look — kept faint, and now that this element is
   sized/clipped precisely to the image's own bounds (see positionOverlays
   in JS), it reads as a tint *on* the photo rather than a rectangle
   floating behind it. */
.carousel-scan{
  position:absolute;
  pointer-events:none;
  /* border-radius is set inline by JS to exactly match the card's own
     live computed radius — always a pixel-perfect 1:1 match, whatever
     the card's radius happens to be. */
  overflow:hidden;
  opacity:0;
  /* fade in gently, but disappear quickly on the way out — otherwise it
     visibly lags behind the card as it shrinks back down */
  transition:opacity .5s ease;
  z-index:11;
  background:
    linear-gradient(rgba(1,228,160,0.16) 1px, transparent 1px),
    linear-gradient(90deg, rgba(1,228,160,0.16) 1px, transparent 1px),
    rgba(1,228,160,0.22);
  background-size:2.6em 2.6em, 2.6em 2.6em, auto;
  background-position:center, center, center;
}
.carousel-scan.active{opacity:1}
/* The two sweeping lines live on their own layer, separate from
   .carousel-scan's tint/grid background, so only the lines get faded near
   the top/bottom edge — the tint/grid stay at full strength edge-to-edge.
   Without this fade, each line goes from fully hidden to fully visible
   within ~5px of travel (a handful of milliseconds at its speed), which
   reads as an abrupt pop right as it crosses the edge instead of a smooth
   entrance. border-radius:inherit keeps its rounded corners matched to
   the parent's own JS-set radius. */
.carousel-scan-lines{
  position:absolute;inset:0;
  overflow:hidden;
  border-radius:inherit;
  -webkit-mask-image:linear-gradient(to bottom, transparent 0, #000 20px, #000 calc(100% - 20px), transparent 100%);
  mask-image:linear-gradient(to bottom, transparent 0, #000 20px, #000 calc(100% - 20px), transparent 100%);
}
/* One line, travelling the full edge-to-edge span from bottom to top.
   Animation only runs while `.active` is present, so it restarts fresh
   every cycle instead of playing its iterations once and staying finished
   forever. The bar itself is a vertical gradient, solid at its leading
   (top) edge and fading to transparent at its trailing (bottom) edge, so
   it reads as a comet-like sweep rather than a hard-edged bar. */
.carousel-scan-lines::after{
  content:'';
  position:absolute;
  left:0;right:0;
  height:48px;
  /* Resting position matches the sweep's own end position (see
     carouselScanSweepUp below) — otherwise, the instant the animation is
     removed along with `.active`, the line snaps back to this default spot
     and briefly flashes at the top edge again before the overlay finishes
     fading out. */
  top:-48px;
  background:linear-gradient(to bottom, var(--accent), transparent);
}
.carousel-scan.active .carousel-scan-lines::after{
  /* linear, not ease-in-out — easing would decelerate to a near-standstill
     right at the end, reading as a pause instead of one steady sweep.
     Single pass, bottom to top only, no reverse/repeat. */
  animation:carouselScanSweepUp 4s linear 1;
  animation-fill-mode:forwards;
}
@keyframes carouselScanSweepUp{
  0%{top:100%}
  100%{top:-48px}
}

/* HUD data readouts, overlaid directly on the focused image */
.carousel-hud{
  position:absolute;
  display:flex;
  flex-direction:column;
  gap:.35rem;
  pointer-events:none;
  opacity:0;
  overflow:hidden; /* JS caps max-width to the image's bounds; clip anything that still doesn't fit */
  transform:translateY(4px);
  transition:opacity .35s ease,transform .35s ease;
  font-family:'Space Mono',monospace;
  font-size:.95rem;
  letter-spacing:.03em;
  color:var(--accent);
  z-index:12;
}
.carousel-hud.active{opacity:1;transform:translateY(0)}
.carousel-hud.left{align-items:flex-start;text-align:left}
.carousel-hud.right{align-items:flex-end;text-align:right}
.carousel-hud .hud-line{
  white-space:nowrap;
  opacity:.9;
}

@media (max-width:768px){
  /* Previously hidden outright on mobile — brought back to match desktop,
     now that the focused card itself has more room to carry it (see the
     reduced 1.4x mobile grow scale above). Smaller type + tighter line gap
     than desktop so it still comfortably fits the smaller card. */
  .carousel-hud{font-size:.68rem;gap:.22rem}
}

@media (prefers-reduced-motion: reduce){
  .card{animation:none}
  .carousel-scan,.carousel-hud,.card.is-focused,.carousel-focus-clone{display:none !important}
}
