/* Entrance transitions. The class sits on .frame-content INSIDE the clipped
   media frame; the IntersectionObserver adds .in-view to the SLIDE, so every
   reveal rule is the descendant form `.in-view .transition-*`. Putting these
   transforms on the slide box itself deformed the seamless strip (scale/
   translate on unentered slides opened dark seams between images).

   --entrance-ms / --entrance-ease are written per slide by PortfolioItem:
   short and snappy for a slide that is already on screen when it reveals,
   long and EVEN for one arriving on the drifting conveyor, where the fade has
   to cover enough travel to be seen. The easing has to change with the
   duration — ease-out-expo is 88% done after 30% of its time, so stretching
   the duration alone still crams the whole visible fade into the first ~20px.
   The fallbacks here are what applies anywhere outside the carousel. */
.transition-fade-slide-up {
  opacity: 0;
  transform: translateY(24px) scale(0.98);
  transition: opacity var(--entrance-ms, 500ms) var(--entrance-ease, var(--ease-out-expo)),
              transform var(--entrance-ms, 500ms) var(--entrance-ease, var(--ease-out-expo));
}

.in-view .transition-fade-slide-up {
  opacity: 1;
  transform: none;
}

/* The 動畫 tab's second option. This rule was missing entirely, so the
   choice was unreachable — PortfolioItem hardcoded fade-slide-up. */
.transition-fade {
  opacity: 0;
  transition: opacity var(--entrance-ms, 500ms) var(--entrance-ease, var(--ease-out-expo));
}

.in-view .transition-fade {
  opacity: 1;
}

/* Additional image entrances, all following the same base-hidden /
   .in-view-revealed pattern the IntersectionObserver drives. */
.transition-slide-left {
  opacity: 0;
  transform: translateX(48px);
  transition: opacity var(--entrance-ms, 500ms) var(--entrance-ease, var(--ease-out-expo)),
              transform var(--entrance-ms, 500ms) var(--entrance-ease, var(--ease-out-expo));
}

.transition-slide-right {
  opacity: 0;
  transform: translateX(-48px);
  transition: opacity var(--entrance-ms, 500ms) var(--entrance-ease, var(--ease-out-expo)),
              transform var(--entrance-ms, 500ms) var(--entrance-ease, var(--ease-out-expo));
}

.transition-zoom-in {
  opacity: 0;
  transform: scale(1.08);
  transition: opacity var(--entrance-ms, 500ms) var(--entrance-ease, var(--ease-out-expo)),
              transform var(--entrance-ms, 500ms) var(--entrance-ease, var(--ease-out-expo));
}

.transition-zoom-out {
  opacity: 0;
  transform: scale(0.92);
  transition: opacity var(--entrance-ms, 500ms) var(--entrance-ease, var(--ease-out-expo)),
              transform var(--entrance-ms, 500ms) var(--entrance-ease, var(--ease-out-expo));
}

.in-view .transition-slide-left,
.in-view .transition-slide-right,
.in-view .transition-zoom-in,
.in-view .transition-zoom-out {
  opacity: 1;
  transform: none;
}

/* Held for a single frame while PortfolioItem.replay() rewinds a slide that
   is coming round again. The base state has to land INSTANTLY — with the
   normal duration still in force, removing .in-view would just start a slow
   fade-out and the re-add would have nothing left to animate from. */
.carousel-slide.is-replaying .frame-content,
.carousel-slide.is-replaying .text-overlay {
  transition-duration: 0s;
  transition-delay: 0s;
}

/* --- 疊字 entrance (the 動畫 tab's 文字 row). These five classes were being
   emitted by PortfolioItem with no CSS behind them anywhere, so every choice
   in that row did exactly nothing on the published page. They ride the
   slide's own .in-view, a beat behind the picture, so the words read as
   landing ON the photo rather than being baked into it. "none" deliberately
   has no rule: no base state, nothing to reveal, always visible. --- */
.text-anim-fade-up {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 450ms var(--ease-out-expo) 250ms,
              transform 450ms var(--ease-out-expo) 250ms;
}

.text-anim-fade {
  opacity: 0;
  transition: opacity 450ms var(--ease-out-expo) 250ms;
}

.text-anim-zoom {
  opacity: 0;
  transform: scale(0.86);
  transition: opacity 450ms var(--ease-out-expo) 250ms,
              transform 450ms var(--ease-out-expo) 250ms;
}

.text-anim-blur {
  opacity: 0;
  filter: blur(8px);
  transition: opacity 450ms ease 250ms, filter 450ms ease 250ms;
}

.in-view .text-anim-fade-up,
.in-view .text-anim-fade,
.in-view .text-anim-zoom,
.in-view .text-anim-blur {
  opacity: 1;
  transform: none;
  filter: none;
}

/* --- The editor's 動畫 tab preview. Same problem as .text-anim-*: the markup
   and the ▶ 重播預覽 button existed, the CSS did not, so picking an entrance
   showed nothing happening and the feature read as broken. Keyframes rather
   than transitions here because the preview is REMOUNTED to replay it (see
   the key= on .anim-preview) — there is no class flip to transition on. --- */
.anim-preview--fade-slide-up { animation: enter-fade-slide-up 600ms var(--ease-out-expo) both; }
.anim-preview--fade { animation: enter-fade 600ms var(--ease-out-expo) both; }
.anim-preview--slide-left { animation: enter-slide-left 600ms var(--ease-out-expo) both; }
.anim-preview--slide-right { animation: enter-slide-right 600ms var(--ease-out-expo) both; }
.anim-preview--zoom-in { animation: enter-zoom-in 600ms var(--ease-out-expo) both; }
.anim-preview--zoom-out { animation: enter-zoom-out 600ms var(--ease-out-expo) both; }

.anim-text--fade-up { animation: enter-text-fade-up 450ms var(--ease-out-expo) 250ms both; }
.anim-text--fade { animation: enter-fade 450ms var(--ease-out-expo) 250ms both; }
.anim-text--zoom { animation: enter-text-zoom 450ms var(--ease-out-expo) 250ms both; }
.anim-text--blur { animation: enter-text-blur 450ms ease 250ms both; }

@keyframes enter-fade-slide-up {
  from { opacity: 0; transform: translateY(24px) scale(0.98); }
  to { opacity: 1; transform: none; }
}

@keyframes enter-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes enter-slide-left {
  from { opacity: 0; transform: translateX(48px); }
  to { opacity: 1; transform: none; }
}

@keyframes enter-slide-right {
  from { opacity: 0; transform: translateX(-48px); }
  to { opacity: 1; transform: none; }
}

@keyframes enter-zoom-in {
  from { opacity: 0; transform: scale(1.08); }
  to { opacity: 1; transform: none; }
}

@keyframes enter-zoom-out {
  from { opacity: 0; transform: scale(0.92); }
  to { opacity: 1; transform: none; }
}

@keyframes enter-text-fade-up {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: none; }
}

@keyframes enter-text-zoom {
  from { opacity: 0; transform: scale(0.86); }
  to { opacity: 1; transform: none; }
}

@keyframes enter-text-blur {
  from { opacity: 0; filter: blur(8px); }
  to { opacity: 1; filter: none; }
}

/* --- Continuous in-image motion (畫面動態): the picture itself slowly
   moves inside its frame, forever — this is what makes "圖片內的東西會動",
   independent of the one-shot entrance. Applied to the media element, which
   the frame clips. --- */
.motion-kenburns-in {
  animation: motion-kb-in 7s ease-in-out infinite alternate;
}

.motion-kenburns-out {
  animation: motion-kb-out 7s ease-in-out infinite alternate;
}

.motion-drift {
  animation: motion-drift 8s ease-in-out infinite alternate;
}

@keyframes motion-kb-in {
  from { transform: scale(1); }
  to { transform: scale(1.18); }
}

@keyframes motion-kb-out {
  from { transform: scale(1.18); }
  to { transform: scale(1); }
}

@keyframes motion-drift {
  from { transform: scale(1.2) translateX(-4%); }
  to { transform: scale(1.2) translateX(4%); }
}

/* 3D motions — the image rotates in perspective INSIDE its clipped frame
   (scaled up so its corners never expose the frame edge). Rotating the
   frame itself, ALIGNED-card style, would reopen the seams the seamless
   strip just eliminated. Every frame-like container provides perspective. */
.media-frame,
.editable-slide__media,
.canvas-preview {
  perspective: 900px;
}

.motion-tilt-sway {
  animation: motion-tilt-sway 6s ease-in-out infinite alternate;
}

.motion-rotate-float {
  animation: motion-rotate-float 7s ease-in-out infinite alternate;
}

@keyframes motion-tilt-sway {
  from { transform: rotateY(-13deg) rotateX(4deg) scale(1.22); }
  to { transform: rotateY(13deg) rotateX(-4deg) scale(1.22); }
}

@keyframes motion-rotate-float {
  from { transform: rotate(-4deg) scale(1.18) translateY(-2%); }
  to { transform: rotate(4deg) scale(1.18) translateY(2%); }
}

/* Interactive: the image tilts toward the mouse (desktop). --tilt-x/y are
   written by the pointermove handler on the frame; the transition keeps the
   follow smooth and eases the reset on leave. */
.motion-pointer-tilt {
  transform: rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg)) scale(1.15);
  transition: transform 200ms ease-out;
  will-change: transform;
}

/* AI 元件動畫 (see AiPartsLayer.js): each detected object is a feather-edged
   crop of the SAME photo, floating just enough to read as "this thing is
   alive" without drifting off its own cutout — the base photo underneath
   stays a plain still (see item-defaults.js motionClass), so there is never
   a gap to fill behind a moving part. Three offset keyframe variants so
   several parts in one photo don't all breathe in lockstep. */
@keyframes shimmer-sweep {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* Hover/tap micro-interaction already covered by .carousel-btn:hover and
   .carousel-dot.is-active width morph in viewer.css — kept together there
   since they're layout-affecting, not purely decorative. */
