/* =============================================================================
   Floulex Studio — motion.css
   The liveness layer. Loaded after site.css.

   RULES THIS FILE OBEYS (see ../../../docs/motion-and-liveness.md):
     · zero dependencies, zero KB of JavaScript — all of it is native CSS
     · every motion is tied to something the visitor DID (scrolled, hovered,
       navigated). Nothing loops forever. Perpetual motion reads as a loading
       skeleton, which is a slop tell.
     · everything is inside @supports / @media guards, so a browser that can't
       do it gets the still page, not a broken one
     · prefers-reduced-motion switches all of it off
   ============================================================================= */


/* ---------------------------------------------------------------------------
   1. CROSS-DOCUMENT VIEW TRANSITIONS
   Page-to-page navigation cross-fades instead of white-flashing, and the
   wordmark holds its position across the change. Chrome/Edge 126+, Safari 18.2+.
   Firefox falls back to a normal navigation. Two rules, no JavaScript.
   --------------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
}

::view-transition-old(root) { animation: vt-out 0.28s cubic-bezier(0.4, 0, 1, 1) both; }
::view-transition-new(root) { animation: vt-in  0.42s cubic-bezier(0, 0, 0.2, 1) both; }
@keyframes vt-out { to { opacity: 0; } }
@keyframes vt-in  { from { opacity: 0; transform: translateY(8px); } }

/* the masthead and wordmark are the same object on every page — let them persist */
.masthead  { view-transition-name: masthead; }
.wordmark  { view-transition-name: wordmark; }
.sticky-cta{ view-transition-name: sticky-cta; }


/* ---------------------------------------------------------------------------
   2. SCROLL-DRIVEN ANIMATION — native, no JS, no scroll listener
   Chrome/Edge 115+, Safari 26+. ~84% of browsers mid-2026.
   Firefox (still flagged) falls back to the IntersectionObserver path in site.js.
   --------------------------------------------------------------------------- */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* --- 2a. photographs drift as they cross the viewport ------------------
       This is the single biggest "alive" win on a photography site: the image
       moves inside its frame while the frame stays put, so the page has depth
       rather than being a flat wall of stills. 3.2% of travel — felt, not seen.
       -------------------------------------------------------------------- */
    .frame img {
      animation: photo-drift linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
      will-change: transform;
    }
    @keyframes photo-drift {
      from { transform: scale(1.07) translateY(-1.6%); }
      to   { transform: scale(1.07) translateY(1.6%); }
    }
    /* hover can't use transform any more (the timeline owns it), so it warms instead */
    .frame:hover img { filter: brightness(1.035) saturate(1.04); }

    /* --- 2b. empty frames breathe on scroll, not on a loop ----------------
       The hatch pattern shifts as the frame passes. Ties the motion to the
       visitor's scroll instead of an infinite shimmer.
       -------------------------------------------------------------------- */
    .frame:not(:has(img))::before {
      animation: hatch-drift linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
    @keyframes hatch-drift {
      from { background-position: 0 0;    opacity: 0.75; }
      50%  {                              opacity: 1; }
      to   { background-position: 34px 0; opacity: 0.75; }
    }

    /* --- 2c. sections arrive under their own steam ------------------------ */
    .reveal {
      opacity: 1;
      transform: none;
      transition: none;
      animation: section-in linear both;
      animation-timeline: view();
      animation-range: entry 4% cover 22%;
    }
    @keyframes section-in {
      from { opacity: 0; transform: translateY(26px); }
      to   { opacity: 1; transform: none; }
    }

    /* --- 2d. a hairline of progress under the masthead --------------------
       On a long landing page this answers "how much is left", which is a real
       question. It is 1px of clay and nothing else.
       -------------------------------------------------------------------- */
    .masthead::after,
    .lp-bar::after {
      content: "";
      position: absolute;
      left: 0; bottom: -1px;
      height: 1px;
      width: 100%;
      background: var(--clay);
      transform-origin: 0 50%;
      animation: progress linear both;
      animation-timeline: scroll(root block);
    }
    @keyframes progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }
  }
}


/* ---------------------------------------------------------------------------
   3. THE HERO HEADLINE LIFTS OUT OF A MASK
   Each line sits in an overflow-hidden block and rises into place. This is the
   deliberate opposite of the "same fade-up on every element" tell — it happens
   ONCE, on the one element that earns it.
   --------------------------------------------------------------------------- */
.ln {
  display: block;
  overflow: hidden;
  /* Fraunces overshoots its box; this stops the mask clipping descenders */
  padding-block: 0.09em;
  margin-block: -0.09em;
}
.js .ln > span {
  display: block;
  animation: lift 1.15s cubic-bezier(0.16, 1, 0.3, 1) both;
  animation-delay: calc(var(--l, 0) * 110ms + 120ms);
}
@keyframes lift { from { transform: translateY(105%); } }

@media (prefers-reduced-motion: reduce) {
  .js .ln > span { animation: none; }
}


/* ---------------------------------------------------------------------------
   4. CURSOR PARALLAX ON THE HERO PHOTOGRAPH
   Desktop pointers only. site.js writes --px / --py; if the JS never runs the
   values default to 0 and nothing moves. Max travel 10px.
   --------------------------------------------------------------------------- */
@media (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .frame--hero {
    --px: 0;
    --py: 0;
  }
  .frame--hero img {
    translate: calc(var(--px) * 10px) calc(var(--py) * 10px);
    transition: translate 0.9s cubic-bezier(0.22, 0.61, 0.36, 1);
  }
}


/* ---------------------------------------------------------------------------
   5. SMALL PURPOSEFUL DETAILS
   Each one says something. None of them are decoration.
   --------------------------------------------------------------------------- */

/* the arrow leads the way out of the button */
.btn .btn__tail { transition: transform 0.35s cubic-bezier(0.22, 0.61, 0.36, 1); }
.btn:hover .btn__tail, .btn:focus-visible .btn__tail { transform: translateX(5px); }

/* the FAQ marker rotates rather than swapping glyphs — continuous, so it reads
   as one control changing state instead of two different characters */
.faq summary::after {
  content: "+" !important;
  transition: transform 0.4s cubic-bezier(0.22, 0.61, 0.36, 1), color 0.3s;
  display: inline-block;
}
.faq details[open] summary::after { transform: rotate(45deg); color: var(--clay); }

/* the eyebrow rule draws itself when its section arrives */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .eyebrow::after {
      transform-origin: 0 50%;
      animation: draw-rule linear both;
      animation-timeline: view();
      animation-range: entry 10% cover 30%;
    }
    @keyframes draw-rule { from { transform: scaleX(0); } to { transform: scaleX(1); } }
  }
}

/* ledger and step rows tick in one after another as the list arrives */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .ledger li, .steps > li, .tier {
      animation: row-in linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 18%;
    }
    @keyframes row-in {
      from { opacity: 0; transform: translateY(14px); }
      to   { opacity: 1; transform: none; }
    }
  }
}

/* a photograph that has just loaded fades in rather than popping */
@media (prefers-reduced-motion: no-preference) {
  .js .frame img:not(.is-loaded) { opacity: 0; }
  .js .frame img.is-loaded { opacity: 1; transition: opacity 0.7s ease-out; }
}
