/* ---------------------------------------------------------------------------
   UlricCMS shared motion. Lives in core/ because the fork ritual rsyncs core/
   and skips themes/, so anything left in a theme cannot be inherited: that is
   how six forks ended up with six hand-copied observers and one fork (mycomuse)
   with none at all.

   The one rule this file exists to enforce: MOTION NEVER GATES CONTENT.
   Every hidden state below is scoped to html.js, and the boot snippet drops
   that class if motion.js has not reported in. So a blocked script, a 404, a
   throw, a browser with no IntersectionObserver, or a reader with JS off all
   land in the same place: the page, fully visible, unanimated.

   Vocabulary
     .fade            · reveals on scroll (adds .in)
     .stagger         · parent; its .fade children come in on a 90ms ladder
     [data-settle]    · image fades up once it has been scrolled near
     [data-flip]      · headline whose words flip up on load (JS wraps them)
     .no-fx           · ?nofx=1 QA mode: everything visible, nothing moving
     [data-motion-skip] · subtree the reveal engine must not touch (carousels)
--------------------------------------------------------------------------- */

/* ---------- scroll reveal ---------- */
html.js .fade,
html.js .reveal {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity .7s cubic-bezier(.2, .6, .2, 1), transform .7s cubic-bezier(.2, .6, .2, 1);
}
html.js .fade.in,
html.js .reveal.in { opacity: 1; transform: none; }

/* Carousel clones are built AFTER the observer has run, so a cloned .fade card
   carries the hidden state with no observer watching it and never comes back.
   Grace stranded 12 sold listings this way. Anything inside a marked subtree
   (or a Splide track) opts out of hiding entirely. */
html.js [data-motion-skip] .fade,
html.js .splide__list .fade,
html.js .splide__slide .fade { opacity: 1; transform: none; transition: none; }

/* ---------- image settle ---------- */
/* Armed by script only: with no JS the attribute is inert and the photo is
   simply there. */
html.js .img-ease {
  opacity: 0;
  transform: translateY(14px) scale(1.012);
  transition: opacity .8s cubic-bezier(.2, .6, .2, 1), transform .8s cubic-bezier(.2, .6, .2, 1);
}
html.js .img-ease.img-in { opacity: 1; transform: none; }

/* ---------- headline word flip ---------- */
/* Same discipline: .flip is added by script after it has wrapped the words, so
   the hidden .fw state can never apply to an unwrapped headline. */
.flip { perspective: 820px; }
.fw { display: inline-block; }
.flip .fw {
  opacity: 0;
  transform: rotateX(86deg);
  transform-origin: 50% 88%;
  animation: ulr-wordflip .62s cubic-bezier(.2, .65, .25, 1) forwards;
  animation-delay: calc(.1s + var(--i, 0) * .07s);
}
.flip .fw-em {
  opacity: 0;
  animation: ulr-emin .7s ease forwards;
  animation-delay: var(--emd, 1s);
}
@keyframes ulr-wordflip { to { opacity: 1; transform: none; } }
@keyframes ulr-emin { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

/* rotateX needs 3D. A browser without transform support ignores the rule and
   the animation, and the opacity keyframe still finishes the job; this states
   the fallback rather than leaving it to luck. */
@supports not (transform: rotateX(1deg)) {
  .flip .fw { opacity: 1; animation: none; }
}

/* ---------- reduced motion ---------- */
/* Reveal first, then stop the world. Order matters: the blanket
   transition:none must not land before .fade has been made visible. */
@media (prefers-reduced-motion: reduce) {
  html.js .fade,
  html.js .reveal,
  html.js .img-ease,
  .flip .fw,
  .flip .fw-em { opacity: 1 !important; transform: none !important; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------- ?nofx=1 : deterministic screenshots and QA ---------- */
.no-fx .fade,
.no-fx .reveal,
.no-fx .img-ease,
.no-fx .fw,
.no-fx .fw-em { opacity: 1 !important; transform: none !important; transition: none !important; }
.no-fx *, .no-fx *::before, .no-fx *::after { animation: none !important; }

/* ---------- the escape hatch ---------- */
/* motion.js removes html.js if it ever fails, and the boot snippet removes it
   if motion.js never reported in. Both land here: no hidden states at all. */
