/* ==========================================================================
   motion.css — every transition and animation on the site, and nothing else.

   The whole file sits inside one prefers-reduced-motion: no-preference block.
   That is not decoration: a visitor who has asked their operating system for
   less motion gets the finished layout with no movement at all, because the
   movement is never declared for them in the first place rather than being
   declared and then cancelled.

   CssConventionTests.No_transition_or_animation_is_declared_outside_a_reduced_
   motion_guard walks every stylesheet and fails the build if a transition
   appears anywhere else — including above the guard in this file.

   KÂĞIT ÜSTÜNDE CAM. The client asked for animation in the same sentence as
   the comps, so this file is half the brief rather than a finish on it. Four
   kinds of movement, and every one of them is about the page being a physical
   surface with objects on it:

     · THE SOLIDS TURN. Five glass objects around the hero, each on its own
       long cycle, each tumbling on a different axis. Nothing else on the page
       moves by itself.
     · OBJECTS LIFT. A card under the pointer rises off the sheet and its cast
       spreads — which is exactly what happens when you pick something up.
     · THE SHEET ANSWERS THE POINTER. The hero stage parallaxes a few pixels
       against the cursor, so the depth you can see is depth you can move.
     · THINGS ARRIVE. One orchestrated page-load sequence and one scroll
       reveal, both rising off the sheet rather than sliding up the page.

   Everything runs on transform, opacity and filter — nothing here animates a
   layout property, a width or a background-position.
   ========================================================================== */

@media (prefers-reduced-motion: no-preference) {

  /* ------------------------------------------------------------------ reveal
     Scroll-reveal, driven by the single IntersectionObserver in site.js.

     Gated on .js, set by an inline script in <head>: without it, a visitor with
     JavaScript disabled or still loading would be left staring at a page of
     permanently invisible sections. The hidden state only ever exists where
     something is guaranteed to come along and un-hide it.

     It arrives by rising off the sheet and settling — a small lift and a small
     scale, together. A dozen blocks sliding up by 24px is the default reveal of
     every template there is; the difference here is that the block also grows
     into place, which is what the eye reads as "this came toward me" rather
     than "this scrolled". */
  .js .reveal {
    opacity: 0;
    transform: translate3d(0, 2rem, 0) scale(0.985);
    transition:
      opacity var(--dur-slow) var(--ease-out-expo),
      transform var(--dur-slow) var(--ease-out-expo);
  }

  .js .reveal.is-visible {
    opacity: 1;
    transform: translate3d(0, 0, 0) scale(1);
  }

  /* Staggered children, so a grid resolves in sequence rather than as one
     block. Four steps and then it repeats: past about 160ms of delay the last
     item feels late rather than sequenced. */
  .js .reveal[data-reveal-step] { transition-delay: calc(var(--reveal-i, 0) * 70ms); }

  /* ------------------------------------------------------- staggered lists
     `data-reveal-stagger` sits on four of the page's biggest blocks — the
     founders, the results board, the stat strip and the modules catalogue —
     and NOTHING READ IT. The only rule that ever existed matched
     `data-reveal-step` against a --reveal-i that no view sets any more, so all
     four arrived as one lump, which is the exact thing the attribute was added
     to prevent. This is that attribute finally doing its job.

     The container stops animating itself and hands the movement to its
     children, 70ms apart, cycling every six so a list of any length never has a
     last item arriving a second late. nth-child rather than a custom property,
     so a view only has to say a list IS staggered and never has to number it.

     `translate`, NOT `transform`, and that is load-bearing rather than
     stylistic: .pin carries a rotateY tilt of its own and .founder__plate a
     perspective, both in `transform`. A transform here would overwrite them —
     the cards would arrive square and stay square, and the pinboard's whole
     collage would quietly flatten. The independent translate/scale/rotate
     properties compose with `transform` instead of replacing it, which is what
     they exist for. */
  .js .reveal[data-reveal-stagger] {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .js .reveal[data-reveal-stagger] > * {
    opacity: 0;
    translate: 0 1.5rem;
    transition:
      opacity var(--dur-slow) var(--ease-out-expo),
      translate var(--dur-slow) var(--ease-out-expo);
  }

  .js .reveal[data-reveal-stagger].is-visible > * {
    opacity: 1;
    translate: 0 0;
  }

  .js .reveal[data-reveal-stagger].is-visible > :nth-child(6n + 2) { transition-delay: 70ms; }
  .js .reveal[data-reveal-stagger].is-visible > :nth-child(6n + 3) { transition-delay: 140ms; }
  .js .reveal[data-reveal-stagger].is-visible > :nth-child(6n + 4) { transition-delay: 210ms; }
  .js .reveal[data-reveal-stagger].is-visible > :nth-child(6n + 5) { transition-delay: 280ms; }
  .js .reveal[data-reveal-stagger].is-visible > :nth-child(6n) { transition-delay: 350ms; }

  /* A ROW THAT ARRIVES ON ITS OWN. The rules above gate the whole list on the
     container, so a list taller than the screen has finished arriving before
     the reader has scrolled to the half of it they cannot see. site.js enrols
     those rows individually; this is what it enrols them into.

     `:not(.is-visible)` is not decoration either — it is what carries this past
     the container's own `.is-visible > *` rule, which is otherwise the more
     specific of the two and would show the row the moment the list arrived.

     NO DELAY, and stated for both states because the nth-child steps above go on
     matching a row once it is visible. Those steps sequence a group that all
     arrived at the same instant. These rows did not: each one crosses the line
     when the reader scrolls it there, so the sequence is already being played
     by the reader, and a delay on top of it only makes the row they are looking
     straight at wait a third of a second before it moves. */
  .js .reveal[data-reveal-stagger] > .reveal-item:not(.is-visible) {
    opacity: 0;
    translate: 0 1.5rem;
  }

  .js .reveal[data-reveal-stagger] > .reveal-item,
  .js .reveal[data-reveal-stagger].is-visible > .reveal-item {
    transition-delay: 0s;
  }

  /* ------------------------------------------------------- the ambient light
     Three enormous soft sources drifting behind the page on their own cycles.
     Deliberately co-prime durations: 34, 27 and 41 seconds never resynchronise,
     so the light never pulses in a way the eye can lock onto. */
  .hero__blob--1 { animation: drift-a 34s var(--ease-out-expo) infinite alternate; }
  .hero__blob--2 { animation: drift-b 27s var(--ease-out-expo) infinite alternate; }
  .hero__blob--3 { animation: drift-c 41s var(--ease-out-expo) infinite alternate; }

  @keyframes drift-a {
    from { translate: 0 0; scale: 1; }
    to { translate: -8% 6%; scale: 1.14; }
  }

  @keyframes drift-b {
    from { translate: 0 0; scale: 1.08; }
    to { translate: 10% -7%; scale: 0.94; }
  }

  @keyframes drift-c {
    from { translate: 0 0; scale: 0.96; }
    to { translate: -6% -9%; scale: 1.2; }
  }

  /* --------------------------------------------------------- THE SOLIDS TURN

     The page's signature movement, and the one the comps are actually asking
     for: five glass objects tumbling very slowly around the hero.

     Five cycles, all co-prime (17/23/29/19/31 seconds), so the group never
     returns to its opening arrangement and there is no frame the eye can catch
     it repeating. Each object turns on a different pair of axes, because five
     things rotating the same way is a carousel.

     `backwards` fill and a negative delay on three of them, so the group is
     already mid-cycle at first paint rather than all starting square-on
     together — the single detail that separates this from a CSS demo. */
  .prop--1 { animation: tumble-a 17s ease-in-out infinite alternate; }
  .prop--2 { animation: tumble-b 23s ease-in-out -6s infinite alternate; }
  .prop--3 { animation: tumble-c 29s ease-in-out infinite alternate; }
  .prop--4 { animation: tumble-b 19s ease-in-out -11s infinite alternate; }
  .prop--5 { animation: tumble-a 31s ease-in-out -4s infinite alternate; }

  @keyframes tumble-a {
    from { transform: translate3d(0, 0, 0) rotate3d(1, 0.4, 0.2, -14deg); }
    to { transform: translate3d(6%, -14%, 0) rotate3d(1, 0.4, 0.2, 20deg); }
  }

  @keyframes tumble-b {
    from { transform: translate3d(0, 0, 0) rotate3d(0.3, 1, 0.4, 18deg); }
    to { transform: translate3d(-9%, 11%, 0) rotate3d(0.3, 1, 0.4, -22deg); }
  }

  @keyframes tumble-c {
    from { transform: translate3d(0, 0, 0) rotate3d(0.5, 0.5, 1, -10deg); }
    to { transform: translate3d(4%, 13%, 0) rotate3d(0.5, 0.5, 1, 16deg); }
  }

  /* --------------------------------------------------------- the two screens
     The hero's pair breathe toward and away from each other in z. Very slow and
     very small: this is ambient motion and it has to survive being looked at
     for a minute without becoming a fidget. */
  .hero__entrance {
    animation: screen-near 18s var(--ease-out-expo) infinite alternate;
  }

  .hero__entrance--brand {
    animation: screen-far 22s var(--ease-out-expo) infinite alternate;
  }

  @keyframes screen-near {
    from { transform: rotateY(var(--tilt-a)) translateZ(var(--depth-1)); }
    to { transform: rotateY(calc(var(--tilt-a) + 2deg)) translateZ(var(--depth-2)); }
  }

  @keyframes screen-far {
    from { transform: rotateY(var(--tilt-b)) translateZ(calc(-1 * var(--depth-2))); }
    to { transform: rotateY(calc(var(--tilt-b) - 2deg)) translateZ(0); }
  }

  /* The light in the gap. Breathes on a third cycle so the gap never looks like
     it is keyed to either screen. */
  .hero__subject::after { animation: gap-light 13s ease-in-out infinite alternate; }

  @keyframes gap-light {
    from { opacity: 0.5; scale: 0.9; }
    to { opacity: 0.85; scale: 1.12; }
  }

  /* ------------------------------------------------- THE POINTER PARALLAX
     site.js writes --px and --py onto the stage — the pointer's offset from its
     centre, normalised to -1..1 — and the whole stage leans against it.

     A transition rather than an animation, because the input is continuous:
     the values change on every frame the pointer moves, and a long ease on the
     transition is what turns a jittery cursor track into something that reads
     as weight. Small numbers on purpose; a stage that swings a long way with
     the mouse is a gimmick, and one that moves six pixels is a surface. */
  .hero__subject {
    transform: translate3d(calc(var(--px, 0) * 10px), calc(var(--py, 0) * 8px), 0);
    transition: transform 900ms var(--ease-out-expo);
  }

  /* The solids lean twice as far as the screens they surround, which is the
     whole of how parallax states distance. Applied to the stage's own
     perspective origin so the group turns rather than slides. */
  .hero__subject { perspective-origin: calc(50% + var(--px, 0) * 8%) calc(45% + var(--py, 0) * 8%); }

  /* ------------------------------------------------------------ index rule */
  .section.is-active .index-line::before { animation: mark-pulse 3.2s ease-in-out infinite; }

  @keyframes mark-pulse {
    0%, 100% { box-shadow: 0 0 0 2px var(--accent-veil); }
    50% { box-shadow: 0 0 0 6px var(--accent-bright-veil); }
  }

  /* --------------------------------------------------------------- header
     Two states, both set by site.js: .is-scrolled once the page has moved at
     all, and .is-tucked while the reader is moving DOWN past 240px. Tucking on
     direction rather than on position is what stops the bar flickering in and
     out around a single threshold.

     The capsule is what transitions, not the header: the header is a
     transparent sticky box at rest, and the object that appears inside it when
     the sheet moves is the thing with a background, a radius and a cast. */
  .site-header { transition: translate var(--dur-base) var(--ease-out-expo); }

  .site-header__inner {
    transition:
      background-color var(--dur-base) ease,
      box-shadow var(--dur-base) ease,
      padding var(--dur-base) var(--ease-out-expo);
  }

  .site-header.is-tucked { translate: 0 -110%; }

  /* Never while a panel anchored to it is open. */
  .site-header:focus-within.is-tucked,
  .has-menu-open .site-header.is-tucked { translate: 0 0; }

  /* ------------------------------------------------------------ the curtain
     Each measured line of the hero headline rises out from behind its own
     clipped block, one after another. --l is the line index, set by site.js. */
  .hero__title.is-split .line__inner {
    display: block;
    animation: line-rise 1s var(--ease-out-expo) backwards;
    animation-delay: calc(var(--l, 0) * 90ms);
  }

  @keyframes line-rise {
    from { translate: 0 105%; }
    to { translate: 0 0; }
  }

  /* ------------------------------------------------------------- marquee
     Translated by exactly -50%, which is why _Marquee.cshtml renders its list
     twice: at the halfway point the second copy is exactly where the first
     began, so the loop has no seam to hide. */
  .marquee__track { animation: marquee-run 42s linear infinite; }

  /* Slows rather than stops on hover: a hard stop reads as a bug, and a reader
     who has paused on a word still wants to finish it. */
  .marquee:hover .marquee__track { animation-duration: 110s; }

  /* One copy, not half the track. Half only lands on a copy boundary when there
     are exactly two of them; --marquee-copies is what the view actually rendered,
     so this stays seamless whatever that number is. */
  @keyframes marquee-run {
    from { transform: translateX(0); }
    to { transform: translateX(calc(-100% / var(--marquee-copies))); }
  }

  /* --------------------------------------------------------------- buttons */
  .btn {
    transition:
      color var(--dur-fast) ease,
      border-color var(--dur-fast) ease,
      box-shadow var(--dur-base) var(--ease-out-expo),
      transform var(--dur-base) var(--ease-out-expo);
  }

  .btn::before { transition: scale var(--dur-base) var(--ease-out-expo); }

  .btn__icon { transition: translate var(--dur-fast) var(--ease-spring); }
  .btn:hover .btn__icon { translate: 0.22em 0; }

  /* Rises off the sheet, and the cast under it spreads to match. Both halves
     matter: a button that moves without its shadow changing reads as a sticker
     sliding rather than as an object being lifted. */
  .btn:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); }
  .btn:active { transform: translateY(0); box-shadow: var(--shadow-soft); }

  .text-link { transition: background-size var(--dur-base) var(--ease-out-expo); }

  /* --------------------------------------------------------------- objects
     Every white card on the page lifts under the pointer, and every one of them
     lifts by the same amount, from the same token. A page where each card
     invents its own hover is a page of components rather than a design. */
  .folder,
  .module-entry,
  .result-card__plate,
  .two-sided__half,
  .accordion__item,
  .module-nav__link,
  .pin,
  .social-link,
  .shelf-btn {
    transition:
      transform var(--dur-base) var(--ease-out-expo),
      box-shadow var(--dur-base) var(--ease-out-expo),
      background-color var(--dur-fast) ease,
      color var(--dur-fast) ease;
  }

  .folder::after { transition: opacity var(--dur-base) ease; }

  /* A card lifts under the pointer. `hover: hover`, not a width — a small
     window on a desktop has a pointer and a large tablet does not, and the
     width was only ever standing in for the question.

     The translateZ and the rotateY(0deg) that used to be here are gone with the
     fan they undid. The deck was a stack of cards turned in Y under a shared
     perspective, so straightening one was the hover; it runs sideways now, sits
     square to the reader already, and a translateZ with no perspective ancestor
     is a no-op that reads as though depth were still doing something. */
  @media (hover: hover) {
    .folder:hover { transform: translateY(calc(-1 * var(--lift-hover))); }

    .module-entry:hover {
      transform: translateY(calc(-1 * var(--lift-hover))) translateZ(var(--depth-1)) rotateY(0deg);
    }
  }

  @media (min-width: 48em) {
    .pin:hover { transform: translateY(calc(-1 * var(--lift-hover))) rotateY(0deg); }
  }

  @media (min-width: 56em) {
    .two-sided__half:hover { transform: translateY(-6px) rotateY(0deg); }
  }

  .module-nav__link:hover,
  .accordion__item:hover { transform: translateY(-3px); }

  .social-link:hover,
  .shelf-btn:hover { transform: translateY(-2px); }

  /* The icon tile turns a few degrees when its card is picked up — the one
     piece of movement inside a card, and it is what makes the flood feel like
     an object rather than a printed square.
     Not the numbered tiles. A pictogram can be knocked askew and still be
     itself; a numeral set on a slant is a numeral that has been printed wrong,
     and eight of them down a column all leaning the same way turned the spine
     into a row of things that had slipped. */
  .icon-tile:not(.icon-tile--no) { transition: rotate var(--dur-base) var(--ease-spring); }
  .folder:hover .icon-tile:not(.icon-tile--no),
  .module-entry:hover .icon-tile { rotate: -6deg; }

  /* The numbered tile lights instead of turning: the node on the track the
     pointer is over is the one that is lit. */
  .icon-tile--no { transition: background-color var(--dur-fast) ease; }
  .folder:hover .icon-tile--no { background-color: var(--accent-bright); }

  /* The arrow at the end of a card travels toward the edge it points at, and
     takes the ink as it goes. Same gesture as the one inside a button, so a
     reader who has learned it once has learned it everywhere. */
  .folder__go { transition: translate var(--dur-base) var(--ease-spring), color var(--dur-fast) ease; }
  .folder:hover .folder__go { translate: 0.3em 0; color: var(--ink); }

  .founder__plate,
  .module-head__cover,
  .phone {
    transition: transform var(--dur-slow) var(--ease-out-expo);
  }

  @media (min-width: 56em) {
    .founder:hover .founder__plate {
      transform: perspective(var(--persp-near)) rotateY(0deg) translateY(calc(-1 * var(--lift-hover)));
    }
  }

  .frame,
  .media-frame,
  .lang-switcher__toggle,
  .lang-switcher__option {
    transition: border-color var(--dur-base) ease, background-color var(--dur-base) ease, color var(--dur-base) ease;
  }

  /* ---------------------------------------------------------- ghost words */
  .ghost-word,
  .footer__ghost { animation: ghost-drift 46s linear infinite alternate; }

  @keyframes ghost-drift {
    from { translate: 0 -50%; }
    to { translate: -4% -50%; }
  }

  /* ------------------------------------------------------------ form label */
  .field__label {
    transition: translate var(--dur-base) var(--ease-out-expo),
                scale var(--dur-base) var(--ease-out-expo),
                color var(--dur-fast) ease;
  }

  .field__input { transition: border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease; }

  /* ------------------------------------------------------------- accordion
     interpolate-size lets a details element animate from its closed height to
     `auto`, which was not expressible in CSS at all until very recently. A
     browser without it simply opens instantly. */
  @supports (interpolate-size: allow-keywords) {
    .accordion__item {
      interpolate-size: allow-keywords;
    }

    .accordion__item::details-content {
      block-size: 0;
      overflow: hidden;
      transition: block-size var(--dur-base) var(--ease-out-expo),
                  content-visibility var(--dur-base) allow-discrete;
    }

    .accordion__item[open]::details-content { block-size: auto; }
  }

  .accordion__icon { transition: rotate var(--dur-base) var(--ease-spring); }

  /* --------------------------------------------------------------- lightbox */
  .lightbox { transition: opacity var(--dur-base) ease; }
  .lightbox__figure { transition: transform var(--dur-base) var(--ease-out-expo); }

  /* ------------------------------------------------------------ menu morph
     TWO bars crossing into an X. The rule that was here moved three bars —
     rotate, fade, rotate — against markup that only ever rendered two, so the
     close control was a single diagonal stroke. The travel is half the gap plus
     half a bar (5px + 1.5px)/2, which is what puts both bars on the centre line
     before they turn. */
  .menu-toggle__bar { transition: translate var(--dur-fast) ease, rotate var(--dur-fast) var(--ease-spring); }

  .menu-toggle[aria-expanded="true"] .menu-toggle__bar:nth-child(1) { translate: 0 3.25px; rotate: 45deg; }
  .menu-toggle[aria-expanded="true"] .menu-toggle__bar:nth-child(2) { translate: 0 -3.25px; rotate: -45deg; }

  /* The overlay's links arrive in sequence rather than as a block. --i is set
     per item by the Navigation view component, so the order is the navigation's
     own and not a guess about DOM position. */
  .menu-overlay.is-open .menu-overlay__list li {
    animation: menu-item-in 620ms var(--ease-out-expo) backwards;
    animation-delay: calc(var(--i, 0) * 55ms + 80ms);
  }

  @keyframes menu-item-in {
    from { opacity: 0; translate: -0.75rem 0; }
    to { opacity: 1; translate: 0 0; }
  }

  .menu-overlay__link { transition: color var(--dur-fast) ease, translate var(--dur-base) var(--ease-out-expo); }
  .menu-overlay__link:hover { translate: 0.5rem 0; }

  /* -------------------------------------------------------------- site nav */
  .site-nav a { transition: color var(--dur-fast) ease; }
  .site-nav a::after { transition: scale var(--dur-base) var(--ease-out-expo); transform-origin: 0 50%; }
  .site-nav a:hover::after { scale: 1 1; }

  /* --------------------------------------------------------- page entrance
     One orchestrated arrival rather than a dozen scattered ones: the headline,
     the lead, the actions and the stage come in on the same curve, a beat
     apart. It is the only sequence on the site that is not scroll-driven,
     because it is the only one that happens before the reader has done
     anything. */
  .hero__copy > * { animation: rise var(--dur-slow) var(--ease-out-expo) backwards; }
  .hero__copy > :nth-child(1) { animation-delay: 60ms; }
  .hero__copy > :nth-child(2) { animation-delay: 140ms; }
  .hero__copy > :nth-child(3) { animation-delay: 220ms; }
  .hero__copy > :nth-child(4) { animation-delay: 300ms; }
  .hero__copy > :nth-child(5) { animation-delay: 380ms; }

  @keyframes rise {
    from { opacity: 0; translate: 0 1.2rem; }
    to { opacity: 1; translate: 0 0; }
  }

  /* The stage arrives last and from further away, so the solids are already
     turning by the time the headline has finished settling. */
  .hero__subject { animation: stage-in 1.6s var(--ease-out-expo) backwards; animation-delay: 200ms; }

  @keyframes stage-in {
    from { opacity: 0; scale: 0.9; }
    to { opacity: 1; scale: 1; }
  }

  /* ------------------------------------------------------------ the shelf
     Smooth scrolling belongs here rather than beside the shelf's layout: the
     arrows call scrollBy, and a reader who has asked for less motion should get
     the jump, not the glide. Declared on the element rather than on :root so
     nothing else on the site inherits a behaviour it never asked for. */
  .shelf { scroll-behavior: smooth; }

  .result-card__plate::before { transition: opacity var(--dur-base) var(--ease-out-expo); }

  .result-card__plate:hover,
  .result-card__plate:focus-visible {
    transform: translateY(calc(-1 * var(--lift-hover)));
    box-shadow: var(--shadow-lift);
  }

  .video-plate__poster { transition: opacity var(--dur-base) var(--ease-out-expo); }
  .video-plate__play { transition: transform var(--dur-base) var(--ease-spring); }
  .video-plate__launch:hover .video-plate__play { transform: scale(1.08); }

  /* -------------------------------------------------- PANELS THAT DO NOT POP

     Two things on this site appear from nothing: the language panel and the
     cookie notice. Both were switched with `display`, which cannot be
     transitioned, so they snapped into place — the one piece of the interface
     that still moved like a 2015 dropdown.

     `transition-behavior: allow-discrete` plus `@starting-style` is what makes
     that expressible at all: the discrete property is held until the rest of
     the transition finishes on the way out, and @starting-style supplies the
     "before it existed" values on the way in. A browser without either simply
     shows the panel instantly, which is what it did before.

     Transform-origin at the top, because the panel hangs from the button that
     opened it and a menu that grows from its own middle reads as a modal. */
  /* The closed values, scoped to `.js` — without them the exit has nowhere to
     transition TO and the panel would still vanish on the frame it closes.
     Inside the guard on purpose: a reduced-motion visitor gets the panel at its
     natural opacity, which is the finished state rather than a hidden one. */
  .js .lang-switcher__panel {
    opacity: 0;
    translate: 0 -0.5rem;
    scale: 0.96;
  }

  .lang-switcher__panel {
    transition:
      opacity var(--dur-fast) var(--ease-out-expo),
      translate var(--dur-base) var(--ease-out-expo),
      scale var(--dur-base) var(--ease-out-expo),
      display var(--dur-base) allow-discrete;
    transform-origin: 100% 0;
  }

  .js .lang-switcher__panel.is-open {
    opacity: 1;
    translate: 0 0;
    scale: 1;
  }

  @starting-style {
    .js .lang-switcher__panel.is-open {
      opacity: 0;
      translate: 0 -0.5rem;
      scale: 0.96;
    }
  }

  /* The cookie notice arrives from below the fold rather than being there when
     the page paints — it is the least important thing on the first screen and
     should be the last thing to ask for attention. */
  .cookie-banner { animation: notice-in 700ms var(--ease-out-expo) 900ms backwards; }

  @keyframes notice-in {
    from { opacity: 0; translate: 0 1.5rem; }
    to { opacity: 1; translate: 0 0; }
  }

  /* --------------------------------------------------- SCROLL-DRIVEN DEPTH

     The last piece of the comps' third dimension, and the one that only exists
     while the reader is moving: the hero stage and the two engraved ghost words
     travel at a different rate from the page they are on.

     A scroll-driven animation rather than a scroll listener, so it runs on the
     compositor and costs nothing on the main thread — there is no JavaScript
     behind any of this. Behind `@supports` because `animation-timeline` is not
     everywhere yet, and a browser without it simply gets the static position,
     which is the finished layout rather than a broken one.

     `view()` and not `scroll()`: the timeline is each element's own passage
     through the viewport, so the effect is the same whatever else is on the
     page above it. */
  @supports (animation-timeline: view()) {
    /* TWO animations on the stage, not one. The entrance has to be restated
       here or this rule replaces it outright and the stage stops arriving —
       `animation` is a shorthand, and the later declaration wins the whole
       property. They compose because they touch different properties: the
       entrance moves opacity and scale, the drift moves translate. */
    .hero__subject {
      animation:
        stage-in 1.6s var(--ease-out-expo) 200ms backwards,
        stage-drift linear both;
      animation-timeline: auto, view();
      animation-range: normal, entry 0% exit 100%;
    }

    @keyframes stage-drift {
      from { translate: 0 3%; }
      to { translate: 0 -6%; }
    }

    /* The ghost words run AGAINST the scroll, which is what makes them read as
       lying further back than the type in front of them.

       This replaces the ambient drift rather than adding to it, deliberately:
       both animate `translate`, and two animations on one property do not
       compose — the last one simply wins, and the first would be dead weight
       nobody could see. Scroll-driven is the better of the two anyway: it is
       movement the reader is causing. */
    .ghost-word,
    .footer__ghost {
      animation: ghost-parallax linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }

    @keyframes ghost-parallax {
      from { translate: 7% -50%; }
      to { translate: -11% -50%; }
    }
  }

  /* ------------------------------------------------------ view transitions */
  ::view-transition-old(page) { animation: fade-out var(--dur-fast) ease both; }
  ::view-transition-new(page) { animation: fade-in var(--dur-base) var(--ease-out-expo) both; }

  @keyframes fade-out {
    to { opacity: 0; }
  }

  @keyframes fade-in {
    from { opacity: 0; translate: 0 0.75rem; }
    to { opacity: 1; translate: 0 0; }
  }
}

/* ==========================================================================
   ON PAPER

   The reveal's hidden state is a screen affordance, and it is the one thing in
   this file that hides content rather than moving it. Nothing scrolls on a
   printed page, so no observer ever arrives to un-hide it: every block below
   the first screen would come out of the printer as blank paper — and now that
   the reveal is enrolled site-wide, that is most of every page rather than a
   dozen blocks on one.

   Outside the reduced-motion guard on purpose. The guard asks whether the
   reader wants movement; this asks whether movement is even possible, and the
   answer on paper is no whichever way they answered the first question.

   Stated for the staggered lists as well, because their children's rule is the
   more specific one and would otherwise survive this.
   ========================================================================== */

@media print {
  .js .reveal {
    opacity: 1;
    transform: none;
  }

  .js .reveal[data-reveal-stagger] > *,
  .js .reveal[data-reveal-stagger] > .reveal-item:not(.is-visible) {
    opacity: 1;
    translate: none;
  }
}
