/* ============================================================
   CROWN & CORE - SCROLL REVEAL / MOTION
   ============================================================ */

[data-reveal] {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out);
  will-change: opacity, transform;
}
[data-reveal].is-visible {
  opacity: 1;
  transform: translateY(0);
}

[data-reveal-group] > * {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out);
}
[data-reveal-group].is-visible > * { opacity: 1; transform: translateY(0); }
[data-reveal-group].is-visible > *:nth-child(1) { transition-delay: 0ms; }
[data-reveal-group].is-visible > *:nth-child(2) { transition-delay: 90ms; }
[data-reveal-group].is-visible > *:nth-child(3) { transition-delay: 180ms; }
[data-reveal-group].is-visible > *:nth-child(4) { transition-delay: 270ms; }
[data-reveal-group].is-visible > *:nth-child(5) { transition-delay: 360ms; }
[data-reveal-group].is-visible > *:nth-child(6) { transition-delay: 450ms; }

@media (prefers-reduced-motion: reduce) {
  [data-reveal], [data-reveal-group] > * {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}

/* ============================================================
   INTRO CURTAIN (home page)
   ------------------------------------------------------------
   A full-bleed cover of the hero's own valley footage + the footer's dark
   scrim, with the full logo centred on it. The logo blurs away, then the
   cover shrinks into the hero's framed background window and cross-fades out.

   The handoff is the whole trick: at the end of the shrink, .intro__frame has
   been sized by main.js to .hero's exact box, main.js has seeked the hero's
   video to this one's playhead so both are showing the same frame, and
   .intro__photo carries the hero's exact starting treatment (opacity var,
   clip-path inset(13%),
   scale(1.08), same object-position) with the hero's own scrim
   faded in over it. The two stacks are therefore pixel-identical when the
   curtain finally fades, so there is no visible cut - it reads as one
   continuous shrink.

   Gated on .intro-active, set on <html> by an inline script in the page head
   (see index.html): no JS, reduced motion, or a repeat visit within the same
   session and the curtain never renders at all. ============================ */
.intro { display: none; }

.intro-active .intro {
  display: block;
  position: fixed;
  inset: 0;
  z-index: 200;              /* above .site-header */
  background: #000;
}
/* Belt and braces alongside the inline overflow main.js sets on <body>: the
   page must not scroll underneath the curtain, or the hero would be measured
   (and handed off to) at the wrong scroll offset. Lifted at .intro-revealed -
   the moment the shrink lands - rather than at .intro-active's removal, so
   the curtain's own fade-out still has its element to fade. */
.intro-active:not(.intro-revealed),
.intro-active:not(.intro-revealed) body { overflow: hidden; }

.intro__frame {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #000;
  /* top/height are animated to .hero's measured rect; the photo inside is
     object-fit: cover, so it re-crops as the box narrows exactly the way the
     hero's own background is cropped at the destination size. */
  transition:
    top 1100ms var(--ease-out),
    height 1100ms var(--ease-out);
}
/* The curtain's own copy of .hero__loop (layout.css): same two boxes, same
   division of labour, so the loop-seam blur reads identically on both while
   they are cross-fading. */
.intro__loop {
  position: absolute;
  inset: 0;
  z-index: 1;
}
.intro__loop.is-seam {
  opacity: var(--loop-dim, 1);
}
/* Mirrors .hero__loop.is-seam .hero__photo in layout.css: blur + push-in on the
   photo so its own clip-path (the shrink window) contains them instead of the
   blur bleeding out past the window. main.js holds --loop-zoom at 1 once the
   curtain is shrinking. */
.intro__loop.is-seam .intro__photo {
  filter: blur(var(--loop-blur, 0px));
  scale: var(--loop-zoom, 1);
}
.intro__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--hero-photo-pos);
  clip-path: inset(0%);
  transform: scale(1);
  /* Starts hidden over .intro__frame's black: main.js only adds .is-cover-in
     (and only then starts the sequence) once the photograph has actually
     decoded, so a cold load opens on a considered fade up from black rather
     than a blank rectangle that suddenly pops. */
  opacity: 0;
  transition:
    opacity 900ms var(--ease-out),
    clip-path 1100ms var(--ease-out),
    transform 1100ms var(--ease-out);
}
.intro.is-cover-in .intro__photo { opacity: 1; }
/* Two stacked scrims cross-fading: the footer's treatment on arrival, the
   hero's on departure. Separate elements because background-image itself
   can't be transitioned. */
.intro__scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  transition: opacity 1100ms var(--ease-out);
}
.intro__scrim--footer {
  /* Was a straight mirror of .footer__bg::after, but that scrim is tuned for
     a static section background, not a full-bleed cover the visitor stares
     at for ~2s before the hero underneath - at those alphas (up to 0.92) the
     curtain read as near-black while the hero it hands off to sits at a
     fraction of that (photo opacity var(--hero-photo-o), scrim capped
     around 0.56 - see .intro__scrim--hero / hero__bg::before), so the shrink
     landed as a sudden brightening instead of a handoff. Lightened to keep
     the same vignette shape - top lighter, bottom darker, warm glow behind
     the logo - while sitting much closer to the hero's own brightness. */
  background:
    radial-gradient(ellipse 65% 60% at 14% 10%, rgba(var(--glow-rgb), 0.14), transparent 65%),
    linear-gradient(180deg, rgba(20, 20, 20, 0.62) 0%, rgba(20, 20, 20, 0.42) 38%, rgba(20, 20, 20, 0.7) 100%);
}
.intro__scrim--hero {
  /* Mirrors .hero__bg::before in layout.css - including its 80% alphas. The
     curtain lands on the hero's exact treatment, so these stops are not free to
     drift from the ones over there. */
  opacity: 0;
  background:
    linear-gradient(90deg, rgba(20,20,20,0.53) 0%, rgba(20,20,20,0.16) 46%, rgba(20,20,20,0.26) 74%, rgba(20,20,20,0.21) 100%),
    linear-gradient(180deg, rgba(20,20,20,0.56) 0%, rgba(20,20,20,0.13) 34%, rgba(20,20,20,0.24) 100%);
}

.intro__logo {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 3;
  width: min(400px, 64vw);
  height: auto;
  opacity: 0;
  filter: blur(12px);
  transform: translate(-50%, -50%) scale(0.94);
  transition:
    opacity 900ms var(--ease-out),
    filter 900ms var(--ease-out),
    transform 900ms var(--ease-out);
}
.intro.is-logo-in .intro__logo {
  opacity: 1;
  filter: blur(0);
  transform: translate(-50%, -50%) scale(1);
}
/* Declared after .is-logo-in so it wins on source order while both classes
   are present (JS adds, never removes, so the logo can't snap back). */
.intro.is-logo-out .intro__logo {
  opacity: 0;
  filter: blur(16px);
  transform: translate(-50%, -50%) scale(1.06);
  transition-duration: 700ms;
}

/* Shrink: the destination state is the hero background's own starting state
   (--hero-clip: 13%, --hero-zoom: 1.08, opacity: var(--hero-photo-o)).
   Declared after .is-cover-in so its opacity wins on source order - both
   selectors carry the same specificity and both classes are present here. */
.intro.is-shrinking .intro__photo {
  opacity: var(--hero-photo-o);
  clip-path: inset(13%);
  transform: scale(1.08);
  transition-duration: 1100ms;
}
@media (max-width: 1299px) {
  /* Below the hero's own pin/reveal breakpoint (layout.css), the hero no
     longer has a zoomed-in starting state to hand off to - it sits flat at
     --hero-clip: 0%, --hero-zoom: 1 the whole time. Left alone, the curtain
     would still zoom its photo in to the old 13%/1.08 landing state here,
     which no longer matches what's underneath: a visible mismatch/pop right
     as the cross-fade lands, instead of the seamless handoff this is meant
     to be. Matching the hero's flat state removes that zoom entirely. */
  .intro.is-shrinking .intro__photo {
    clip-path: inset(0%);
    transform: scale(1);
  }
}

/* The consent banner is part of the page, not the overture - it would
   otherwise sit on top of the curtain from the first frame. Held back until
   the reveal, then it fades in on its own existing transition. */
.intro-active:not(.intro-revealed) .consent {
  opacity: 0;
  pointer-events: none;
}
.intro.is-shrinking .intro__scrim--footer { opacity: 0; }
.intro.is-shrinking .intro__scrim--hero { opacity: 1; }

.intro.is-leaving {
  opacity: 0;
  pointer-events: none;
  transition: opacity 550ms var(--ease-out);
}

/* Hero entrance (loads immediately, not scroll-triggered) */
.hero-enter {
  opacity: 0;
  transform: translateY(14px);
  animation: heroEnter var(--dur-slow) var(--ease-out) forwards;
}
/* While the curtain is up the hero copy must not have already played itself
   out behind it. Pausing (rather than re-declaring the animation) keeps the
   --1..--4 stagger intact: main.js adds .intro-revealed as the shrink lands
   and the whole staggered entrance then runs from the top, in step with the
   curtain's fade rather than a beat behind it. */
.intro-active:not(.intro-revealed) .hero-enter { animation-play-state: paused; }
.hero-enter--1 { animation-delay: 60ms; }
.hero-enter--2 { animation-delay: 160ms; }
.hero-enter--3 { animation-delay: 260ms; }
.hero-enter--4 { animation-delay: 360ms; }

@keyframes heroEnter {
  to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .hero-enter { animation: none; opacity: 1; transform: none; }
}

/* `animation-fill-mode: forwards` keeps the animation "active" indefinitely,
   which keeps the element on its own compositing layer for the life of the
   page - and at a fractional device pixel ratio (125%/150% Windows display
   scaling) that layer's backing store doesn't align with the physical pixel
   grid, so the text inside reads visibly softer than normally-painted DOM
   text. main.js adds this class on the element's `animationend`, once the
   slide-up has actually finished, dropping it back to normal painting. */
.hero-enter.hero-enter--settled { animation: none; opacity: 1; transform: none; }

/* Crest mark subtle float */
.brand__mark, .crest-hero {
  transition: transform var(--dur-slow) var(--ease-out);
}

/* Perspective capability marquee (home hero peek strip)
   -----------------------------------------------------
   A 3D-tilted ticker whose items pull into focus - shedding blur and
   gaining opacity - as they cross the strip's focal centre, then recede
   and soften toward the edges. Ported from a Remotion/React reference to
   vanilla JS + CSS: the tilt, the type and the edge fades live here; the
   scroll and the per-item depth-of-field are driven per frame by main.js
   (see the "Perspective capability marquee" block there). Colours are the
   site's own - muted paper text on charcoal, gold brand accent for the
   hairline dot between items, charcoal for the fades - nothing new added.
   The JS block is skipped for reduced-motion / no-JS visitors, who get the
   flattened static row defined in the media query at the end. */
.pmarquee {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  display: flex;
  align-items: center;
  perspective: 1200px;
  perspective-origin: 50% 50%;
}
.pmarquee__stage {
  width: 100%;
  /* Flat, head-on - no 3D rotation. The depth-of-field blur (driven per
     frame by main.js from each item's distance to the strip centre) still
     provides the focus/recede feel without any perspective tilt. */
  transform: none;
  will-change: transform;
}
.pmarquee__track {
  /* position: relative makes this element the offsetParent for its items,
     so main.js reads each item's centre in the track's own coordinate
     space (offsetLeft) rather than the document's. */
  position: relative;
  display: flex;
  align-items: center;
  width: max-content;
  white-space: nowrap;
  will-change: transform;
  transform: translateX(0);
}
.pmarquee__item {
  display: inline-flex;
  align-items: center;
  font-family: var(--font-body);
  font-size: var(--fs-2xs);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--paper-text-muted);
  /* Sharp defaults - these are the reduced-motion / no-JS resting state;
     main.js overwrites filter + opacity per frame when it's running. */
  filter: blur(0);
  opacity: 1;
}
/* Brand icon sitting in the gap between items - the gold Crown & Core mark
   (assets/img/logo-mark.png). Box uses the icon's own 437:500 aspect ratio
   so it isn't squished; ~16px keeps roughly the same footprint the crown
   separator had. As the item's ::after it inherits that item's per-frame
   blur/opacity, so the marks breathe with the depth-of-field of the words. */
.pmarquee__item::after {
  content: "";
  display: inline-block;
  width: 14px;
  height: 16px;
  margin-inline: clamp(1.6rem, 4vw, 3.4rem);
  background: url("../img/logo-mark.png") center / contain no-repeat;
  opacity: 0.85;
  vertical-align: middle;
}
/* Edge vignettes in the strip's own charcoal: horizontal for the scroll
   in/out, vertical to soften where the tilted type meets the clip edges. */
.pmarquee__fade {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}
.pmarquee__fade--x {
  background: linear-gradient(90deg, var(--charcoal) 0%, transparent 15%, transparent 85%, var(--charcoal) 100%);
}
.pmarquee__fade--y {
  background: linear-gradient(180deg, var(--charcoal) 0%, transparent 28%, transparent 72%, var(--charcoal) 100%);
}
@media (prefers-reduced-motion: reduce) {
  /* Static, fully-legible row - no blur (already flat). */
  .pmarquee__item { filter: none !important; opacity: 1 !important; }
}

/* Number count-up target */
.count-up { font-variant-numeric: tabular-nums; }

/* Image reveal wipe: a curtain (own element, not ::after - .art-panel--photo
   already owns its ::after for the scrim/glow, and the two would fight over
   it) sits above the photo and wipes away on scroll-in. Distinct from the
   plain opacity/translate of [data-reveal] elsewhere - reserved for the
   handful of full photographs, so it reads as a deliberate moment rather
   than a repeated tic. */
.img-wipe {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-md);
}
.img-wipe__curtain {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: var(--charcoal);
  transform-origin: right;
  transition: transform 1150ms var(--ease-in-out);
}
.img-wipe.is-visible .img-wipe__curtain { transform: scaleX(0); }
@media (prefers-reduced-motion: reduce) {
  .img-wipe__curtain { display: none; }
}

/* Image parallax: the photo drifts vertically at a different rate than the
   page scroll for the entire time its panel is near the viewport, so it
   moves at a visibly different pace than the text beside it. The oversized
   inner layer (not the <img> itself, so the existing .art-panel__img hover
   zoom keeps its own transform untouched) is what main.js translates each
   frame, up or down by up to MAX_TRAVEL (141px) - the 23%/146% over-scan
   below is that buffer on both top and bottom, since the drift runs both
   directions depending on whether the panel sits above or below center. */
.img-parallax {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-md);
}
.img-parallax__layer {
  position: absolute;
  left: 0;
  right: 0;
  top: -23%;
  height: 146%;
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .img-parallax__layer { transform: none !important; top: 0; height: 100%; }
}

/* Link underline sweep */
.link-sweep {
  position: relative;
  display: inline-block;
}
.link-sweep::after {
  content: '';
  position: absolute; left: 0; bottom: -2px;
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-base) var(--ease-out);
}
.link-sweep:hover::after { transform: scaleX(1); }
