@keyframes fade-up {
  from {
    opacity: 0;
    transform: translateY(9px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.reveal {
  animation: fade-up 210ms ease both;
}

.reveal--delay-1 {
  animation-delay: 40ms;
}

.reveal--delay-2 {
  animation-delay: 76ms;
}

/* ── Entrance on mount ──────────────────────────────────────────
   For content the router paints in one go. Staggered so a page
   assembles itself rather than snapping into place.             */
@keyframes rise-in {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}

.reveal-up {
  animation: rise-in 280ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

.reveal-up--1 { animation-delay: 40ms; }
.reveal-up--2 { animation-delay: 45ms; }
.reveal-up--3 { animation-delay: 67ms; }
.reveal-up--4 { animation-delay: 89ms; }

/* Children of a staggered container, without a class per item */
.stagger > * {
  animation: rise-in 260ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.stagger > *:nth-child(1) { animation-delay: 60ms; }
.stagger > *:nth-child(2) { animation-delay: 58ms; }
.stagger > *:nth-child(3) { animation-delay: 50ms; }
.stagger > *:nth-child(4) { animation-delay: 67ms; }
.stagger > *:nth-child(5) { animation-delay: 84ms; }
.stagger > *:nth-child(6) { animation-delay: 101ms; }

/* ── Page transitions ───────────────────────────────────────────
   A quick cross-fade with a hair of scale. There was a blur here; a
   full-viewport `filter` re-rasterises the whole layer every frame, which is
   what made navigation feel heavy on a phone no matter how short the duration
   was. Opacity and transform stay on the compositor.                  */
/* Opacity only — no scale.
   The scale was 0.994 leaving and 1.008 entering: under one percent, which is
   below the threshold anyone can actually see. It was not free, though. Any
   element with a transform becomes the containing block for `position: fixed`
   descendants, so for the 200ms this transition ran, every fixed element
   inside a page was positioned against #app-main instead of the viewport.
   The product page's sticky buy bar lives there, and it visibly jumped into
   place after each navigation on a phone — the one control on the most
   important page that must never move. An invisible scale is a bad trade for
   that, so the cross-fade carries the transition alone. */
#app-main {
  transition: opacity 200ms cubic-bezier(0.16, 1, 0.3, 1);
}

#app-main.is-leaving {
  opacity: 0;
  /* Must not exceed PAGE_DEFOCUS_MS in router.js — the router waits exactly
     this long before rendering, so a longer fade is simply dead time. */
  transition-duration: 120ms;
  transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
}

#app-main.is-entering {
  opacity: 0;
  transition: none;
}

/* Warmth blooms through the swap. Opacity only — nothing moves. */
.page-glow {
  position: fixed;
  inset: 0;
  /* Under the header, so the chrome stays crisp while the content shifts */
  z-index: 90;
  pointer-events: none;
  opacity: 0;
  background:
    radial-gradient(circle at 50% 42%, rgba(197, 164, 106, 0.12), transparent 58%),
    radial-gradient(circle at 50% 100%, rgba(154, 122, 69, 0.1), transparent 45%);
  transition: opacity 200ms cubic-bezier(0.16, 1, 0.3, 1);
}

.page-glow.is-on {
  opacity: 1;
  transition-duration: 120ms;
}

/* Motion is decoration; never let it stand between a user and the shop */
@media (prefers-reduced-motion: reduce) {
  .reveal,
  .reveal-up,
  .stagger > * {
    animation: none;
    opacity: 1;
    transform: none;
  }

  #app-main,
  #app-main.is-leaving,
  #app-main.is-entering {
    opacity: 1;
    transform: none;
    filter: none;
    transition: none;
  }

  .page-glow { display: none; }
}

/* ── Touch devices: stop the ambient loops that repaint ─────────────
   Passes 1 and 2 shortened durations and removed the page blur, and the site
   was still slow on a phone. Duration was never the remaining cost: it is
   *which property* the ambient `infinite` loops animate.

   `opacity` and `transform` are handed to the compositor and cost a phone
   almost nothing. `background-position`, `box-shadow` and `filter` cannot be:
   each frame re-rasterises the element on the main thread, for as long as the
   animation runs — which for an `infinite` loop is forever, on screen or not.

   The shop page renders 24 product cards, and every card's gold CTA runs
   `pc-buy-flow` (background-position) *and* `pc-buy-breathe` (box-shadow). That
   is 48 permanently repainting elements before the shopper has scrolled once.
   Nothing else on the page gets a smooth frame after that, which is what made
   navigation and scrolling feel heavy no matter how short the transitions were.

   So on touch devices the repainting decoration is switched off and each
   element is pinned to its resting frame. What is *kept* is everything that
   composites cheaply (drifting opacity, scaling glows, the marquee) and
   everything that carries meaning (spinners, skeletons). The site keeps its
   character; it stops repainting to prove it.

   Desktop is untouched — it has the GPU headroom, and hover states are where
   this decoration was designed to be read anyway.                          */
@media (hover: none) and (pointer: coarse) {
  /* ── Gold CTAs: gradient travel + breathing shadow ────────────────
     Pinned to 50% so the bright band sits centred — the most flattering
     frame of the sweep, and the one the eye reads as "gold" rather than
     the dark edge a default 0% would leave.                              */
  .pc-cta--buy,
  .pc-cta--buy:hover,
  .cx-submit,
  .qa-confirm.is-ready {
    animation: none;
    background-position: 50% center;
  }

  /* This one carries no static box-shadow — it exists only inside the
     keyframe — so the resting frame has to be restored by hand or the
     button loses its lift entirely. */
  .os-btn-primary {
    animation: none;
    background-position: 50% center;
    box-shadow: 0 4px 20px rgba(197, 164, 106, 0.25);
  }

  /* ── Text sheens (background-clip: text) ──────────────────────────
     A gradient moving through glyphs re-rasterises the text every frame,
     which is among the most expensive things a phone can be asked to do
     continuously. Every one of these gradients is flat cream except for a
     band at 50%, so with the sweep stopped the heading simply reads as its
     declared solid colour — no pinning needed.

     The home and contact heroes are no longer in this list: neither title
     carries a sheen any more. Both are plain `.lx-display`, and their
     entrances are the page's own keyframes rather than an `animation`
     shorthand that had to be restated here to keep the rise while dropping
     the loop.                                                            */
  .about-hero__title,
  .trk-title,
  .trk-row--grand strong:last-child {
    animation: none;
  }

  /* The About hero's entrance and its sheen share one `animation` shorthand,
     so the sheen cannot be dropped without naming the entrance again —
     otherwise killing the loop would also kill the rise the title arrives on. */
  .about-hero.is-visible .about-hero__inner > .about-hero__title {
    animation: about-rise 900ms cubic-bezier(0.16, 1, 0.3, 1) 200ms both;
  }

  /* ── Box-shadow pulses ────────────────────────────────────────────
     A spreading ring of shadow repaints the element and everything under
     it. The dot stays; only its heartbeat stops.                        */
  .pp-stamp__dot,
  .tc-stamp__dot {
    animation: none;
  }

  /* ── The terms page's seal ────────────────────────────────────────
     The last two ambient loops that were still running on a phone. The halo
     animates an SVG `fill` and the rim spins a shadowed ring for 24s at a
     time, forever, whether or not the seal is on screen — the same cost the
     dots above were switched off for, on the same rule.

     They were already stopped under `prefers-reduced-motion`; this extends
     that to touch, where the reason is battery rather than preference. Both
     rest on their declared static appearance — the halo keeps its fill, the
     rim its opacity — so the seal still looks like the seal. Desktop keeps
     the motion. */
  .tc-doc__halo,
  .tc-doc__rim {
    animation: none;
  }

  /* Same shorthand problem as the hero titles: keep the arrival, drop the
     endless pulse and the endless gradient flow. */
  .trk-step.is-active .trk-step__dot {
    animation: trk-dot-in 640ms cubic-bezier(0.34, 1.32, 0.44, 1)
               calc(var(--trk-beat) + 110ms) both;
  }
  .trk-step.is-done::before {
    animation: trk-rail 560ms ease-out calc(var(--trk-beat) + 200ms) both;
  }

  /* ── Frosted glass ────────────────────────────────────────────────
     `backdrop-filter` is the most expensive property in this stylesheet
     during a scroll. Unlike a normal paint it cannot be cached: the
     compositor has to read back everything behind the element, blur it, and
     recomposite — every frame that anything underneath moves. Which, while
     scrolling, is every frame.

     Three of them are on screen the entire time on a phone: the sticky
     header (18px), the dock stacked inside it (14px), and the product page's
     fixed buy bar (16px). Plus one per product card, and five more on the
     gallery chrome. That is the frost tax on every swipe.

     What makes it an easy trade is that almost none of it shows. The header
     paints at 84–92% opacity and the bars at 92–94%, so the blur is being
     computed to affect the ~10% of backdrop that reads through. Raising the
     surfaces to near-opaque looks all but identical on a 6-inch screen and
     costs the compositor nothing at all.

     Desktop keeps the glass — it has the GPU headroom and the larger screen
     is where the effect actually reads. */
  /* The blur goes; the colour is the redesign's, not the gold-gradient bar this
     override was originally written against. Near-opaque, so dropping the
     backdrop-filter costs nothing anyone can see — which was the point.

     `.site-header.is-scrolled` in system.css is more specific and still wins
     once the page has moved, so the solid state is unaffected. */
  .site-header {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: rgba(10, 10, 11, 0.97);
  }

  /* `.mobile-dock` was removed with the old two-row phone header, and the
     product page's dock never asks for glass — it is opaque by design. */

  /* These sit over photographs, so they keep a little translucency for depth
     — just enough that the image reads behind them, without the readback. */
  .pc-wish {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: rgba(20, 19, 18, 0.82);
  }

  .lux-select__panel {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }

  /* The blurred background orbs used to be switched off here for touch only.
     They are now static on every device — a blurred surface cannot be moved
     on the compositor, so it re-runs the blur over its whole area every
     frame, and that was the last per-frame re-blur left on the site. The
     declarations are gone from pages.css entirely, so there is nothing left
     to override. `.announce__aura` went the same way: the announcement bar's
     glow is painted into the band's own background now. */
}
