/* Motion system for the public site.
 *
 * Two rules govern everything here:
 *
 * 1. NOTHING IS EVER HIDDEN UNLESS THE SCRIPT IS ALIVE. Every "start" state is
 *    behind `html.motion`, a class set by a tiny blocking script in <head>. If
 *    the JS fails to load, is blocked, or the browser is old, the page renders
 *    exactly as it did before — fully visible. A reveal system that leaves the
 *    site blank when a file 404s is not worth having.
 *
 * 2. NO ELEMENT IS EVER ADDED OR MOVED. The inline CMS keys every editable node
 *    by its position in the element tree (accounts/content.py::_structural_key),
 *    so a wrapper <div> would orphan every saved edit beneath it. Motion is
 *    applied to elements that already exist, through classes and custom
 *    properties only.
 *
 * The vocabulary is deliberately small — one easing curve, one rise distance,
 * one stagger step — because a consistent curve is what makes motion read as
 * authored rather than decorated.
 */

:root {
  /* The single easing for anything that enters or settles. */
  --motion-ease: cubic-bezier(.16, 1, .3, 1);
  /* A softer curve for long, ambient movement (hover drift, parallax). */
  --motion-ease-soft: cubic-bezier(.35, .9, .5, 1);
  --motion-rise: 28px;
  --motion-dur: .9s;
  --motion-dur-clip: 1.1s;
  --motion-stagger: 70ms;
}

/* ---------------------------------------------------------------- reveal --
 * Blocks fade up as they come into view. The transition lives outside the
 * html.motion guard so that removing the class mid-life settles smoothly
 * rather than snapping.
 */
[data-motion="rise"] {
  transition: opacity var(--motion-dur) var(--motion-ease),
              transform var(--motion-dur) var(--motion-ease);
  transition-delay: var(--motion-delay, 0ms);
}
html.motion [data-motion="rise"] {
  opacity: 0;
  transform: translate3d(0, var(--motion-rise), 0);
}
html.motion [data-motion="rise"].is-in {
  opacity: 1;
  transform: none;
}

/* ------------------------------------------------------------ photo clip --
 * A photograph un-crops into place instead of fading. On a site carried by
 * its pictures this reads as the image arriving, not the page loading.
 * Applied to the FRAME, never the <img> — the frame already exists and
 * already clips its contents.
 */
html.motion [data-motion="clip"] {
  clip-path: inset(12% 8%);
}
html.motion [data-motion="clip"].is-in {
  /* `none`, deliberately — NOT inset(0).
   *
   * inset(0) clips nothing, but a clip-path that clips nothing still costs.
   * Blink represents an ancestor clip as a node in the compositor's property
   * tree, so a composited descendant underneath it stays cheap. WebKit does
   * not: it keeps the frame on a mask / re-raster path for as long as the
   * clip-path exists. The .photo-image inside is composited on purpose (see
   * `will-change` under "hover drift") and spends six seconds scaling on
   * hover, so under a live clip-path Safari re-rasterises it against whole
   * device pixels every frame and the zoom visibly steps instead of gliding.
   * Chrome never showed it, because Chrome never took that path.
   *
   * So the reveal runs as an ANIMATION rather than a transition: the default
   * fill-mode drops the element back to this `none` the instant the animation
   * ends. `backwards` holds the start frame through --motion-delay, so a
   * staggered sibling still waits its turn. The last keyframe is inset(0),
   * which is what `none` looks like — nothing snaps at the handover. */
  clip-path: none;
  animation: motion-unclip var(--motion-dur-clip) var(--motion-ease)
             var(--motion-delay, 0ms) 1 normal backwards;
}
@keyframes motion-unclip {
  from { clip-path: inset(12% 8%); }
  to   { clip-path: inset(0); }
}

/* ------------------------------------------------------------ hero intro --
 *
 * Every hero rule below is scoped to :not(.gsap-on). When the GSAP layer runs
 * it owns the hero's opacity AND transform outright. Sharing them was a real
 * bug: GSAP records a tween's start value from the computed style, so it read
 * the opacity:0 set here for the entrance and then animated 0 -> 0, leaving
 * the headline permanently invisible.
 * The hero copy arrives in depth: each layer starts a little lower and a
 * little later than the one above it. --motion-delay and --motion-depth are
 * set per child by the script, so no markup carries a hard-coded order.
 *
 * After the intro lands, these same two properties are driven by scroll (see
 * the parallax note below). The transition is therefore REMOVED once the page
 * has settled — leaving it on would make the parallax lag a second behind the
 * scroll, which reads as broken rather than smooth.
 */
.hero-copy > *,
.post-copy > * {
  transition: opacity 1.1s var(--motion-ease), transform 1.1s var(--motion-ease);
  transition-delay: var(--motion-delay, 0ms);
}
html.motion .hero-copy > *,
html.motion .post-copy > * {
  opacity: 0;
  transform: translate3d(0, 22px, 0);
  will-change: transform, opacity;
}
html.motion body.is-ready .hero-copy > *,
html.motion body.is-ready .post-copy > * {
  opacity: var(--motion-hero-fade, 1);
  transform: translate3d(0, calc(var(--motion-hero-shift, 0px) * var(--motion-depth, 1)), 0);
}
html.motion body.is-settled .hero-copy > *,
html.motion body.is-settled .post-copy > * {
  transition: none;
}

/* The photograph settles from a slight over-scale, so the first thing the
 * visitor sees is the picture easing into focus rather than snapping in. */
html.motion:not(.gsap-on) .hero-photo .photo-image,
html.motion:not(.gsap-on) .post-hero .post-photo .photo-image {
  transform: scale(1.06);
  transition: transform 1.6s var(--motion-ease-soft);
}
html.motion:not(.gsap-on) body.is-ready .hero-photo .photo-image,
html.motion:not(.gsap-on) body.is-ready .post-hero .post-photo .photo-image {
  transform: scale(1);
}

/* --------------------------------------------------------- hero parallax --
 * Another Escape moves the hero's TEXT layers at different rates and fades
 * them out, leaving the photograph still. Doing it that way round is not just
 * a style choice: translating a full-bleed photo instead would drag its edge
 * into view unless it were scaled up enough to hide the gap, and at this
 * hero's proportions that costs real sharpness. --motion-hero-shift and
 * --motion-hero-fade are written onto the copy container each frame.
 */

/* ------------------------------------------------------------ hover drift --
 * Another Escape's asymmetry: the zoom creeps in over seconds and settles back
 * quickly. Slow enough that it is felt rather than noticed.
 */
.photo-stage .photo-image,
.post-photo .photo-image,
.role-photo .photo-image,
.featured-photo .photo-image,
.article-photo .photo-image {
  transform: scale(1);
  transform-origin: 50% 50%;
  transition: transform 1.2s var(--motion-ease-soft);
  /* These images sit inside a parent that is overflow:hidden and, during its
     entrance only, clip-path'd. Scaling them without a compositing layer makes
     the browser repaint through that clip every frame. A plain rectangular
     overflow clip is something every compositor handles cheaply; the clip-path
     is not, which is why it is now dropped the moment the entrance finishes.
     `backface-visibility: hidden` used to sit here too. It is a legacy iOS
     flicker hack, redundant next to will-change, and in WebKit it rasterises
     the layer once and GPU-scales it, so the picture softens through the zoom
     and pops back sharp at the end — motion the eye reads as a jump. */
  will-change: transform;
}
@media (hover: hover) {
  html.motion .photo-stage:hover .photo-image,
  html.motion .post-photo:hover .photo-image,
  html.motion .post-card:hover .photo-image,
  html.motion .role-photo:hover .photo-image,
  html.motion .role-card:hover .photo-image,
  html.motion .featured-photo:hover .photo-image,
  html.motion .featured-card:hover .photo-image,
  html.motion .article-photo:hover .photo-image {
    transform: scale(1.06);
    transition-duration: 6s;
  }
}

/* ------------------------------------------------------------------ links --
 * A rule that wipes in from the left. ::after on an anchor that already
 * exists — no extra element, and it does not touch the anchor's text, which
 * the CMS owns.
 */
@media (hover: hover) {
  html.motion .main-nav a,
  html.motion .footer-nav a,
  html.motion .more-link {
    position: relative;
  }
  html.motion .main-nav a::after,
  html.motion .footer-nav a::after,
  html.motion .more-link::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: -.18em;
    height: 1px;
    background: currentColor;
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform .55s var(--motion-ease);
    pointer-events: none;
  }
  html.motion .main-nav a:hover::after,
  html.motion .main-nav a:focus-visible::after,
  html.motion .footer-nav a:hover::after,
  html.motion .footer-nav a:focus-visible::after,
  html.motion .more-link:hover::after,
  html.motion .more-link:focus-visible::after {
    transform: scaleX(1);
  }
  /* The current page is already underlined by aria-current; leaving the sweep
     on it too would read as a hover that never ends. */
  html.motion .main-nav a[aria-current="page"]::after {
    display: none;
  }
}

/* Buttons lift a little, on the same curve as everything else. */
@media (hover: hover) {
  html.motion .button,
  html.motion .header-cta {
    transition: transform .45s var(--motion-ease), background .45s var(--motion-ease),
                color .45s var(--motion-ease), border-color .45s var(--motion-ease);
  }
  html.motion .button:hover,
  html.motion .header-cta:hover {
    transform: translateY(-2px);
  }
}

/* -------------------------------------------------------- reduced motion --
 * Not a softening — a full stop. Anyone who has asked their system for less
 * motion gets the page with none: no offsets, no clipping, no drift.
 */
@media (prefers-reduced-motion: reduce) {
  html.motion [data-motion="rise"],
  html.motion .hero-copy > *,
  html.motion .post-copy > *,
  html.motion body.is-ready .hero-copy > *,
  html.motion body.is-ready .post-copy > * {
    opacity: 1 !important;
    transform: none !important;
  }
  html.motion [data-motion="clip"] {
    clip-path: none !important;
  }
  html.motion .hero-photo .photo-image,
  html.motion .post-hero .post-photo .photo-image,
  html.motion body.is-ready .hero-photo .photo-image,
  html.motion body.is-ready .post-hero .post-photo .photo-image {
    transform: none !important;
  }
  html.motion .photo-stage:hover .photo-image,
  html.motion .post-card:hover .photo-image,
  html.motion .role-card:hover .photo-image,
  html.motion .featured-card:hover .photo-image,
  html.motion .button:hover,
  html.motion .header-cta:hover {
    transform: none !important;
  }
  html.motion *,
  html.motion *::before,
  html.motion *::after {
    transition-duration: .01ms !important;
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
  }
}
