/* ===========================================================
   OnFinance Motion layer

   Deliberately small.

   An earlier version of this file drove a whole scrollytelling
   layer from animation-timeline: headings that gained weight as
   they crossed the screen, cards that wiped in, numerals and spec
   chips and rules that assembled themselves. The engineering was
   sound and the idea did not work, for a reason worth writing
   down so it is not tried again.

   A scroll-triggered entrance is invisible by construction. It
   finishes before the reader's eye settles on the thing it
   animated, so the effect is never actually seen. What does get
   seen is the failure mode. A heading interpolating its weight
   from 340 to 800 as you scroll is indistinguishable from a
   webfont loading late, so the one time the effect registered at
   all, it registered as a bug.

   That is an effect class that can only cost. It is gone.

   What is left is motion the visitor causes, which they are
   already looking at because they just touched it, plus two
   continuous things that are obviously deliberate because they
   never stop: the lender marquee and a slow parallax inside the
   fixed-ratio art frames.
=========================================================== */

/* ---- Cross-document View Transitions (MPA) ----
   Pure progressive enhancement: unsupported browsers simply
   navigate normally with zero behaviour change. */
@media (prefers-reduced-motion: no-preference){
  @view-transition{ navigation: auto; }
}
::view-transition-old(root),
::view-transition-new(root){ animation-duration: var(--duration-slow); }

/* Resting state for every entrance hook is plain visibility. The
   whole system below layers on top of this, never under it. */
.reveal{ opacity: 1; transform: none; }

/* ---- Hero background drift ----
   The gradient field behind the hero only, never any content.
   A background that shifts slowly cannot be mistaken for text
   that failed to render. */
@media (prefers-reduced-motion: no-preference){
  @supports (animation-timeline: scroll()){
    .hero::before{
      animation: hero-drift linear both;
      animation-timeline: scroll(root);
      animation-range: 0 60vh;
    }
  }
}
@keyframes hero-drift{ from{ transform: translateY(0) scale(1); } to{ transform: translateY(6%) scale(1.06); } }

/* ---- Parallax inside the editorial art frames ----
   The image is scaled slightly beyond its fixed-ratio frame and
   drifts as the frame crosses the viewport. Fixed-ratio frames
   only, so nothing ever shows a gap. This one survives the cull
   because it never starts or finishes, it simply tracks the
   scroll, so there is no moment where it looks half-done. */
@media (prefers-reduced-motion: no-preference){
  @supports (animation-timeline: view()){
    .art-frame--landscape img,
    .art-frame--wide img{
      scale: 1.2;
      animation: art-drift linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
      /* Pin this to its own compositor layer.
         Without it the browser re-rasterises the image on every scroll
         frame, and because these are highly textured paper-craft photos
         scaled to 1.2, resampling at fractional offsets produced a
         visible shimmer as the frame crossed the viewport. Promoting it
         makes the drift a pure GPU transform of an already-rendered
         texture, so nothing is redrawn while it moves.

         The other half of that fix was in the markup. These frames used
         to also carry data-tilt, which put a pointer-driven rotateX/
         rotateY and transform-style: preserve-3d on the parent. A
         scroll-driven transform nested inside a preserve-3d parent that
         is itself clipped and rounded cannot be cached, so the two
         effects fought each other every frame. Tilt belongs on small
         cards, not on a large drifting photo. */
      will-change: transform;
      backface-visibility: hidden;
    }
  }
}
@keyframes art-drift{ from{ transform: translateY(-7%); } to{ transform: translateY(7%); } }

/* ---- Microinteractions ---- */
.tap-scale{ transition: transform var(--duration-instant) var(--ease-standard); }
.tap-scale:active{ transform: scale(0.97); }

/* ---- Scroll-snap hero story (short, deliberate, proximity-based)
   Layout rather than animation. Proximity snap so it never fights
   normal scrolling. */
.story{
  display: grid;
  gap: var(--space-sm);
  grid-auto-flow: column;
  grid-auto-columns: min(85%, 380px);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding-block-end: var(--space-xs);
  scrollbar-width: thin;
}
.story > *{ scroll-snap-align: center; }
@media (min-width: 900px){
  .story{ grid-auto-flow: row; grid-template-columns: repeat(3,1fr); overflow: visible; scroll-snap-type: none; }
}

/* ---- FAQ answers glide open ----
   The visitor tapped it, so they are looking straight at it. */
@supports (interpolate-size: allow-keywords){
  :root{ interpolate-size: allow-keywords; }
  @media (prefers-reduced-motion: no-preference){
    .faq-item::details-content{
      block-size: 0; overflow: clip;
      transition: block-size var(--duration-base) var(--ease-out-quart),
                  content-visibility var(--duration-base) allow-discrete;
    }
    .faq-item[open]::details-content{ block-size: auto; }
  }
}

/* ---- Statement weight ----
   The statement paragraphs used to interpolate a weight axis on
   scroll, the same trick that made the headings look broken. They
   now simply sit at the weight they were always heading towards.
   font-variation-settings inherits and outranks font-weight, so the
   emphasised words restate their own axis or they would be dragged
   back to the light weight of the sentence around them. */
.statement{ font-variation-settings: "wght" 330, "opsz" 40; }
.statement strong,
.statement em{ font-variation-settings: "wght" 800, "opsz" 40; }

@media (prefers-reduced-motion: reduce){
  .hero::before,
  .art-frame--landscape img,
  .art-frame--wide img{ animation: none !important; transform: none !important; }
  .tap-scale{ transition: none; }
}


/* ===========================================================
   Entrances, fourth attempt, different physics.

   The three previous versions were scroll-driven: the animation's
   clock was scroll position, so a reader could stop halfway
   through one and sit there looking at a half-made page. Weight
   still shifting, cards still clipped, headings at half opacity.
   Every complaint traced back to that one property.

   These are time-driven and merely *started* by visibility.
   js/motion.js watches with an IntersectionObserver, and the
   moment an element first enters the viewport it plays a fixed
   460ms entrance and is finished. There is no scroll position at
   which anything can be caught half-done, because the clock is
   not the scroll.

   Fail-safe by construction, in three layers.
   1. Resting CSS is fully visible. The hidden state is a class
      the script only puts on elements that are below the fold at
      arm time, so no JS means no hiding, ever.
   2. The animation fills backwards, not both. If it never runs,
      the element is simply there. The both fill is what left a
      600px hole in the homepage in version two.
   3. Nothing above the fold is armed, so the first paint of every
      page is always complete.

   This is the same pattern as the mobile menu rows, which is the
   one piece of motion that has survived review.
=========================================================== */

@media screen and (prefers-reduced-motion: no-preference){
  .io-wait{ opacity: 0; }
  .io-in{
    animation: io-rise 460ms var(--ease-out-quart) backwards;
    animation-delay: var(--iod, 0ms);
  }
  @keyframes io-rise{
    from{ opacity: 0; transform: translateY(18px); }
  }

  /* Full-bleed artwork is uncovered by an edge instead of fading. */
  .io-in.wipe{ animation-name: io-wipe; animation-duration: 620ms; }
  @keyframes io-wipe{
    from{ opacity: 0; clip-path: inset(0 100% 0 0 round var(--radius-lg)); }
    25%{ opacity: 1; }
    to{ clip-path: inset(0 round var(--radius-lg)); }
  }

  /* Big figures land with a little weight. The count-up in
     motion.js runs on the same trigger, so the number is still
     rolling while the tile settles. */
  .io-in.figure-pop{ animation-name: io-pop; }
  @keyframes io-pop{
    from{ opacity: 0; transform: scale(0.86); transform-origin: left bottom; }
  }
}

/* ---- On-load entrance for hero content ----
   @starting-style transitions from first render, so the top of the
   page arrives rather than being switched on. Resting state is
   visible: if the transition never runs the heading is simply
   there. Never applied to buttons, so it cannot fight the tactile
   transitions. */
@supports (transition-behavior: allow-discrete){
  @media screen and (prefers-reduced-motion: no-preference){
    .enter{
      transition: opacity 640ms var(--ease-out-quart), transform 640ms var(--ease-out-quart);
      transition-delay: calc(var(--ei, 0) * 80ms);
    }
    @starting-style{
      .enter{ opacity: 0; transform: translateY(16px); }
    }
  }
}

/* ---- The red thread of progress ----
   Back by request. A fine thread grows across the top of the
   viewport as you read, tipped with a gold bead. This one is
   scroll-scrubbed on purpose: it is a progress indicator, so
   tracking the scroll is its meaning, and a progress bar cannot
   be mistaken for content that failed to load. */
.thread-progress{
  position: fixed; inset-block-start: 0; inset-inline: 0; z-index: 200;
  block-size: 3px; pointer-events: none;
  background: linear-gradient(90deg, var(--brand-red-600), var(--brand-red-500));
  box-shadow: 0 0 8px rgba(200, 29, 35, 0.45);
  transform-origin: 0 50%; transform: scaleX(0);
}
.thread-progress::after{
  content: ""; position: absolute; inset-inline-end: -2px; inset-block-start: 50%;
  inline-size: 7px; block-size: 7px; border-radius: 50%;
  background: var(--sand-500); transform: translateY(-50%);
}
@media (prefers-reduced-motion: no-preference){
  @supports (animation-timeline: scroll()){
    .thread-progress{ animation: thread-grow linear both; animation-timeline: scroll(root); }
  }
}
@keyframes thread-grow{ from{ transform: scaleX(0); } to{ transform: scaleX(1); } }
