/* ---------------------------------------------------------------------------
   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 same was true inside this repo: ulric and fold each
   carried their own IntersectionObserver in layout.php and scrub carried a
   third in scrub.js. This file and motion.js are now the only copy.

   The one rule this file exists to enforce: MOTION NEVER GATES CONTENT.
   Every hidden state below is scoped to html.fx, 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.

   Why html.fx and not html.js: html.js means "scripts are running", and the
   themes' navigation depends on it (a menu that hides itself must know a
   button can open it again). Motion needs a separate word, because motion
   surrenders in cases where scripts are perfectly healthy: reduced motion,
   ?nofx=1, no IntersectionObserver. Surrendering on html.js would have dumped
   the whole navigation inline for every reduced-motion reader.

   Vocabulary
     .fade            · reveals on scroll (adds .in)
     .reveal          · the same thing, the name the themes already use
     .stagger         · parent; its .fade children come in on a 90ms ladder
     .img-ease        · image settle, added BY SCRIPT only (never authored)
     [data-settle]    · asks for the settle on an element script would skip
     [data-nosettle]  · asks script to leave this image alone
     [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)

   Browser floor: Chrome 79 and equivalents (the studio gets read from a Tesla
   car browser). Nothing here needs a feature newer than that, and the two
   that are newer, clip-path on a replaced element and the independent scale
   property, sit behind @supports with the plain opacity fade underneath.
--------------------------------------------------------------------------- */

/* ---------- scroll reveal ---------- */
html.fx .fade,
html.fx .reveal {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity .7s cubic-bezier(.2, .6, .2, 1), transform .7s cubic-bezier(.2, .6, .2, 1);
}
html.fx .fade.in,
html.fx .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.fx [data-motion-skip] .fade,
html.fx .splide__list .fade,
html.fx .splide__slide .fade { opacity: 1; transform: none; transition: none; }

/* ---------- image settle ---------- */
/* Armed by script only: with no JS the class is never added and the photo is
   simply there. The settle is a clip and a hair of scale, never a translate,
   for one concrete reason: transform on an image would collide with the theme
   sheets that already animate images on hover, and transform:none would win
   the tie and kill the hover. clip-path and the independent scale property
   compose with transform instead of replacing it, and script strips both
   classes once the settle is done, so nothing lingers on the element. */
html.fx .img-ease {
  opacity: 0;
  transition: opacity .85s cubic-bezier(.2, .6, .2, 1),
              clip-path .85s cubic-bezier(.2, .6, .2, 1),
              scale .85s cubic-bezier(.2, .6, .2, 1);
}
html.fx .img-ease.img-in { opacity: 1; }

/* Chrome 55+ / Safari 9.1+. Older engines get the opacity fade alone. */
@supports (clip-path: inset(1px)) {
  html.fx .img-ease { clip-path: inset(0 0 13% 0); }
  html.fx .img-ease.img-in { clip-path: inset(0 0 0 0); }
}
/* Chrome 104+ / Safari 14.1+. Below that there is simply no scale, which is
   the point: the settle still reads, it just settles flat. */
@supports (scale: 1) {
  html.fx .img-ease { scale: 1.022; }
  html.fx .img-ease.img-in { scale: 1; }
}

/* ---------- 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.fx .fade,
  html.fx .reveal,
  html.fx .img-ease,
  .flip .fw,
  .flip .fw-em { opacity: 1 !important; transform: none !important;
    clip-path: none !important; scale: 1 !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;
  clip-path: none !important; scale: 1 !important; }
.no-fx *, .no-fx *::before, .no-fx *::after { animation: none !important; }

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