/* ============================================================
   Chewtoy's Dugout — motion · "Night Pitch" round 2 (2026-09-23)

   THE ROOM MOVES. Owner, 2026-09-23: "go crazy on what you can do with
   it, with animations … make it professional but also follow the same
   kind of theme". Every value this file spends is declared in the
   "Motion, round 2" run at the foot of tokens.css; every behaviour that
   needs a pointer or a clock is js/ctd-motion.js. Between them they are
   the whole layer, so it can be dialled from two files or dropped by
   dequeuing two handles.

   WHAT IS IN HERE, in page order:
     1  the switch                html[data-ctd-motion]
     2  arrival                   the cover's castings; page heads
     3  nameplates                dropping onto their cross members
     4  reveal, cont.             the trust icons drawing themselves
     5  the pointer light         a disc of lamplight under the cursor
     6  sparks                    off a gold casting when it is struck
     7  embers                    the hero floor, painted by the script
     8  the foil pass             light running across the gold headline
     9  chrome                    popovers, drawer, dialogs, toasts, badge
    10  scroll-linked depth       parallax, photo drift, slide depth,
                                  the reading bar — CSS scroll timelines
    11  page transitions          one page handing over to the next
    12  reduced motion            all of it, cancelled

   FOUR RULES, and every rule below obeys them:

   NOTHING IS HIDDEN BY CSS ALONE. Anything that starts invisible does so
   under html[data-ctd-motion], which an inline script in header.php sets
   before first paint and only when the visitor has not asked for reduced
   motion. No script, no attribute, no hidden content — the same contract
   ctd-reveal.js keeps.

   ARRIVAL ANIMATIONS FILL BACKWARDS, NOT BOTH. The from-keyframe holds
   through the delay; the end keyframe is the element's own resting style,
   so when the animation finishes the element drops out of it entirely.
   A fill of `both` on a transform would keep a stacking context on every
   headline for the life of the page — and pin a `transform` that a hover
   rule elsewhere could then never move.

   ARRIVAL IS ONCE PER PAGE. Rules for surfaces a script re-renders — the
   page heads, the shop grids, the basket and checkout, the nameplates — sit
   under html[data-ctd-motion]:not([data-ctd-arrived]). js/ctd-motion.js sets
   data-ctd-arrived once the page has finished arriving (and clears it for a
   soft navigation's new content), so WooCommerce swapping the basket after
   "Update cart", or a filter re-showing a card, never plays the entrance
   again. The hero keeps the plain switch: nothing re-renders it.

   RESPONSE IS STILL THE 2026-09-03 VOCABULARY. A control you are aiming
   at answers in colour, shadow and a couple of pixels, fast. What is new
   here is arrival, presence and continuity — never a hover that makes you
   wait for it.
   ============================================================ */


/* ============================================================
   2 · ARRIVAL
   ============================================================ */
@keyframes ctd-arrive-rise {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}
/* A mark stamped in: in big, landing with a jolt (the order-received tick).
   Never on anything carrying its own transform — see the note on the corner
   braces below. */
@keyframes ctd-arrive-bolt {
  0%   { opacity: 0; transform: scale(1.7); }
  55%  { opacity: 1; transform: scale(0.94); }
  78%  { transform: scale(1.03); }
  100% { opacity: 1; transform: none; }
}
/* A casting rising onto the page and settling on its own weight. */
@keyframes ctd-arrive-cast {
  0%   { opacity: 0; transform: translateY(22px); }
  60%  { opacity: 1; transform: translateY(-3px); }
  100% { opacity: 1; transform: none; }
}

/* ---- the home cover's castings ----
   The buttons are castings, so they land like castings — and the row is the
   thing in the column, so the row does not move (§14); its two castings do. */
html[data-ctd-motion] .ctd-home__cta > .ctd-btn {
  animation: ctd-arrive-cast calc(var(--t-enter) * 0.9) var(--ease-out) calc(var(--t-step) * 8) backwards;
}
html[data-ctd-motion] .ctd-home__cta > .ctd-btn + .ctd-btn { animation-delay: calc(var(--t-step) * 9); }

/* THE CORNER BRACES DO NOT MOVE (owner, 2026-09-23: "I dont want the corner
   brace things rotating"). They had a clamp-down arrival here, and it read as
   a spin: the four gussets are ONE drawing mirrored per corner with
   transform: scaleX(-1) / scaleY(-1) / scale(-1, -1) (ornament.css), and a
   keyframe on transform replaces that mirror, so three corners started the
   wrong way round and flipped into place. No animation at all is the answer,
   not a repaired one — the frame corners are bolted, which is what
   ornament.css has always said of them. They travel with the panel as it
   slides in, and that is the only motion they get. */

/* ---- every other page's head ----
   The same rise, on the eyebrow → title → lede of the builders hub, the shop
   landing and archives, the reading shell and the team-set buy column. Order
   comes off the child index through one custom property, so a head that
   gains a line picks up the next beat without a new rule.
   QUICKER THAN THE HERO, on purpose: the home page can take a second to
   make an entrance, but on a product or a basket page the customer came for
   the facts, and the last line of the buy column has to be readable inside
   ~700ms. Two-thirds of the hero's beat, on two-thirds of its duration. */
html[data-ctd-motion]:not([data-ctd-arrived]) :is(
  .ctd-hub__head,
  .ctd-shop__head > .ctd-stack,
  .ctd-shop__head--archive,
  .ctd-doc__head,
  .ctd-product__summary
) > * {
  animation: ctd-arrive-rise calc(var(--t-enter) * 0.66) var(--ease-out) backwards;
  animation-delay: calc(var(--t-step) * 0.66 * var(--ctd-n, 0));
}
html[data-ctd-motion]:not([data-ctd-arrived]) :is(.ctd-hub__head, .ctd-shop__head > .ctd-stack, .ctd-shop__head--archive, .ctd-doc__head, .ctd-product__summary) > :nth-child(2) { --ctd-n: 1.5; }
html[data-ctd-motion]:not([data-ctd-arrived]) :is(.ctd-hub__head, .ctd-shop__head > .ctd-stack, .ctd-shop__head--archive, .ctd-doc__head, .ctd-product__summary) > :nth-child(3) { --ctd-n: 3; }
html[data-ctd-motion]:not([data-ctd-arrived]) :is(.ctd-hub__head, .ctd-shop__head > .ctd-stack, .ctd-shop__head--archive, .ctd-doc__head, .ctd-product__summary) > :nth-child(4) { --ctd-n: 4.5; }
html[data-ctd-motion]:not([data-ctd-arrived]) :is(.ctd-hub__head, .ctd-shop__head > .ctd-stack, .ctd-shop__head--archive, .ctd-doc__head, .ctd-product__summary) > :nth-child(5) { --ctd-n: 6; }
html[data-ctd-motion]:not([data-ctd-arrived]) :is(.ctd-hub__head, .ctd-shop__head > .ctd-stack, .ctd-shop__head--archive, .ctd-doc__head, .ctd-product__summary) > :nth-child(n + 6) { --ctd-n: 7; }
/* ---- the funnel: basket, checkout, order received ----
   Same quick rise. The basket's lines come in one after another under the
   title and the summary slides in from its own side, so the total — the
   thing the page is for — lands last and is the one that settles. */
@keyframes ctd-arrive-side {
  from { opacity: 0; transform: translateX(28px); }
  to   { opacity: 1; transform: none; }
}
html[data-ctd-motion]:not([data-ctd-arrived]) :is(.cart-ledger-header__title-block, .checkout-ledger-header__title-block, .thankyou-hero__copy) > * {
  animation: ctd-arrive-rise calc(var(--t-enter) * 0.66) var(--ease-out) backwards;
}
html[data-ctd-motion]:not([data-ctd-arrived]) :is(.cart-ledger-header__title-block, .checkout-ledger-header__title-block, .thankyou-hero__copy) > :nth-child(2) {
  animation-delay: calc(var(--t-step) * 0.66);
}
html[data-ctd-motion]:not([data-ctd-arrived]) .cart-items > .cart-item,
html[data-ctd-motion]:not([data-ctd-arrived]) .checkout-fields {
  animation: ctd-arrive-rise calc(var(--t-enter) * 0.66) var(--ease-out) backwards;
  animation-delay: calc(var(--t-step) * 0.66 * (var(--ctd-n, 0) + 1));
}
.cart-items > .cart-item:nth-child(2) { --ctd-n: 1; }
.cart-items > .cart-item:nth-child(3) { --ctd-n: 2; }
.cart-items > .cart-item:nth-child(4) { --ctd-n: 3; }
.cart-items > .cart-item:nth-child(n + 5) { --ctd-n: 4; }
html[data-ctd-motion]:not([data-ctd-arrived]) :is(.cart-sidebar, .checkout-sidebar) {
  animation: ctd-arrive-side calc(var(--t-enter) * 0.8) var(--ease-out) calc(var(--t-step) * 2) backwards;
}
/* Stacked under the lines (1000px and down, css/v3/woo.css) the summary has no
   side of its own to come from, and the 28px slide carries it past a phone's
   24px gutter: the basket and checkout were 4px wider than the screen for the
   length of the arrival. There it rises like the lines above it. */
@media (max-width: 1000px) {
  html[data-ctd-motion]:not([data-ctd-arrived]) :is(.cart-sidebar, .checkout-sidebar) {
    animation-name: ctd-arrive-rise;
  }
}
/* The thank-you mark is stamped: in big, landing hard. */
html[data-ctd-motion]:not([data-ctd-arrived]) .thankyou-hero__icon {
  animation: ctd-arrive-bolt calc(var(--t-enter) * 0.8) var(--ease-out) backwards;
}

/* ---- the 404: a fumble ----
   The numeral is dropped, bounces twice and settles, like a ball spilled on
   a turnover. It squashes on each landing and never tilts. */
@keyframes ctd-fumble {
  0%   { opacity: 0; transform: translateY(-64px); }
  34%  { opacity: 1; transform: translateY(0) scale(1.04, 0.9); }
  50%  { transform: translateY(-20px) scale(1); }
  64%  { transform: translateY(0) scale(1.02, 0.95); }
  78%  { transform: translateY(-6px); }
  90%  { transform: translateY(0) scale(1.01, 0.98); }
  100% { opacity: 1; transform: none; }
}
html[data-ctd-motion] .ctd-doc--404 .ctd-doc__code {
  display: inline-block; transform-origin: 50% 100%;
  animation: ctd-fumble calc(var(--t-enter) * 1.4) var(--ease-out) backwards;
}

/* ---- the shop grids ----
   The team picker and the product grid deal their cards in, a beat apart
   for the first row or two; anything further down has long landed by the
   time it is scrolled to, and the stagger is capped so the thirtieth card
   does not wait three seconds. */
html[data-ctd-motion]:not([data-ctd-arrived]) :is(.team-set-card, li.product-card) {
  animation: ctd-arrive-rise calc(var(--t-enter) * 0.66) var(--ease-out) backwards;
  animation-delay: calc(var(--t-step) * 0.5 * var(--ctd-n, 0) + var(--t-step) * 2);
}
:is(.team-set-card, li.product-card):nth-child(2) { --ctd-n: 1; }
:is(.team-set-card, li.product-card):nth-child(3) { --ctd-n: 2; }
:is(.team-set-card, li.product-card):nth-child(4) { --ctd-n: 3; }
:is(.team-set-card, li.product-card):nth-child(5) { --ctd-n: 4; }
:is(.team-set-card, li.product-card):nth-child(6) { --ctd-n: 5; }
:is(.team-set-card, li.product-card):nth-child(7) { --ctd-n: 6; }
:is(.team-set-card, li.product-card):nth-child(n + 8) { --ctd-n: 7; }

/* A Full Squad's three steps come up one after another, the way the kit
   goes together (the photo strip that did this went with the 2026-09-28
   rework of the page). */
html[data-ctd-motion]:not([data-ctd-arrived]) .ctd-product__steps > * {
  animation: ctd-arrive-rise calc(var(--t-enter) * 0.66) var(--ease-out) backwards;
  animation-delay: calc(var(--t-step) * 0.66 * (var(--ctd-n, 0) + 1));
}
html[data-ctd-motion] .ctd-product__steps > :nth-child(2) { --ctd-n: 1; }
html[data-ctd-motion] .ctd-product__steps > :nth-child(n + 3) { --ctd-n: 2; }


/* ============================================================
   3 · NAMEPLATES DROP ONTO THEIR CROSS MEMBERS
   A plate hangs on the steel, so it arrives the way a hung thing does:
   dropped, over-travelling, swinging back and settling — and only then do
   its rivets go in. The plate's own `transform: translateY(-50%)` is what
   hangs it on the member, so everything here moves the INDIVIDUAL
   translate and scale properties, which compose with that transform
   instead of replacing it.

   The whole run plays once on load for every plate; js/ctd-motion.js arms
   the ones below the fold (`.is-armed` stops it and hides them) and swaps
   in `.is-hung` as each one scrolls into view, which starts it again from
   zero. Arming only ever touches a plate nobody can see yet.
   ============================================================ */
/* THE EVENT LOOK (2026-09-28): a plate is a heading between two gold rules
   now, with no member to drop onto, so it RISES into place the way the page
   heads do. The name and the timing stay, so the arming in js/ctd-motion.js
   is untouched. */
@keyframes ctd-plate-hang {
  0%   { opacity: 0; translate: 0 16px; }
  100% { opacity: 1; translate: 0 0; }
}
@keyframes ctd-rivet-in {
  from { opacity: 0; transform: scale(0); }
  to   { opacity: 1; transform: none; }
}
html[data-ctd-motion]:not([data-ctd-arrived]) .ctd-plate {
  animation: ctd-plate-hang var(--t-hang) var(--ease-out) calc(var(--t-step) * 6) backwards;
}
html[data-ctd-motion]:not([data-ctd-arrived]) .ctd-plate__rivet {
  animation: ctd-rivet-in calc(var(--t-enter) * 0.5) var(--ease-spring) backwards;
  animation-delay: calc(var(--t-step) * 6 + var(--t-hang) * 0.5 + var(--ctd-r, 0) * 70ms);
}
.ctd-plate__rivet--tr { --ctd-r: 1; }
.ctd-plate__rivet--br { --ctd-r: 2; }
.ctd-plate__rivet--bl { --ctd-r: 3; }
html[data-ctd-motion] .ctd-plate.is-armed { animation: none; opacity: 0; }
html[data-ctd-motion] .ctd-plate.is-armed .ctd-plate__rivet { animation: none; opacity: 0; }
/* The scroll-triggered drop is spelled out in full rather than borrowing the
   load rule, because that one stops applying once the page has arrived and
   a plate reached by scrolling has to drop whenever it is reached. */
html[data-ctd-motion] .ctd-plate.is-hung {
  animation: ctd-plate-hang var(--t-hang) var(--ease-out) backwards;
}
html[data-ctd-motion] .ctd-plate.is-hung .ctd-plate__rivet {
  animation: ctd-rivet-in calc(var(--t-enter) * 0.5) var(--ease-spring) backwards;
  animation-delay: calc(var(--t-hang) * 0.5 + var(--ctd-r, 0) * 70ms);
}
/* The builder's plates are drawn by its own render (ui/team/render.js), which
   runs again when the team catalogue lands — a hang keyed to insertion would
   drop the same plate twice on a slow connection. The step change already
   carries them in (.v2-step-anim), so inside the app they simply stand. */
html[data-ctd-motion] #builderApp .ctd-plate,
html[data-ctd-motion] #builderApp .ctd-plate__rivet { animation: none; }


/* ============================================================
   4 · REVEAL, CONTINUED
   The rise itself lives with the rest of the reveal in ornament.css. This
   is what rides on it: the trust strip's icons DRAW themselves as their
   item arrives. stroke-dasharray is inherited, so setting it on the <svg>
   reaches the paths inside the sprite's <use> shadow tree. 160 is longer
   than any single stroke in the sprite, which is all a dash needs to be.
   Both the arriving (.is-in) and the settled (.is-shown) state carry the
   same animation name, so the class swap ctd-reveal.js does at the end of
   the rise does not restart or cut it.
   ============================================================ */
@keyframes ctd-draw { from { stroke-dashoffset: 160; } to { stroke-dashoffset: 0; } }
html[data-ctd-motion] .ctd-trust__item:is(.is-in, .is-shown) .ctd-i {
  stroke-dasharray: 160;
  animation: ctd-draw calc(var(--t-enter) * 1.6) var(--ease-out) backwards;
  animation-delay: calc(var(--ctd-i, 0) * var(--t-step) + var(--t-step) * 2);
}


/* ============================================================
   5 · THE POINTER LIGHT
   A disc of lamplight that follows the cursor across a card. js/ctd-motion.js
   drops one `.ctd-spot` into each target on first hover and moves the disc
   inside it; the wrapper clips it to the card. It sits UNDER the card's
   content (z-index -1 inside an isolated card) so it lights the ground the
   copy stands on and never the photo or the letters themselves — a product
   photo is never tinted, which is an owner rule, not a preference.

   Fine pointers only. On a touch screen there is no cursor to follow, and
   a light stuck wherever the last tap landed is a smudge.
   ============================================================ */
.ctd-spot {
  position: absolute; inset: 0; z-index: -1;
  overflow: hidden; border-radius: inherit; pointer-events: none;
  opacity: 0; transition: opacity var(--t-base) var(--ease);
}
.ctd-spot__disc {
  position: absolute; left: 0; top: 0;
  width: var(--spot-size); height: var(--spot-size);
  margin: calc(var(--spot-size) / -2) 0 0 calc(var(--spot-size) / -2);
  border-radius: 50%;
  background: var(--fx-spot);
  transform: translate3d(var(--mx, 0px), var(--my, 0px), 0);
}
.ctd-card--interactive, .product-card { isolation: isolate; }
.ctd-card--interactive:hover > .ctd-spot,
.product-card:hover > .ctd-spot,
.team-set-card__link:hover > .ctd-spot,
.ctd-poster:hover > .ctd-spot { opacity: 1; }
/* On a poster the light is a torch passing over paper: warmer, faster to
   fall off, and held inside the sheet's margin so it never spills past a
   torn edge onto the boarding. The body goes up a layer so the light slides
   between the sheet (the module's svg, z 0) and the print on it. */
.ctd-poster > .ctd-spot { inset: 12px; z-index: 1; border-radius: var(--r-1); }
.ctd-poster > .ctd-spot > .ctd-spot__disc { background: var(--fx-spot-paper); }
.ctd-poster > .ctd-poster__body { z-index: 2; }


/* ============================================================
   6 · SPARKS
   Strike a gold casting and it throws sparks. js/ctd-motion.js builds a
   handful of these in one fixed layer at the point of contact and flies
   each on the Web Animations API; this is only what one looks like.
   ============================================================ */
.ctd-sparks { position: fixed; left: 0; top: 0; width: 0; height: 0; z-index: var(--z-toast); pointer-events: none; }
.ctd-spark {
  position: absolute; left: 0; top: 0;
  width: 6px; height: 6px; margin: -3px 0 0 -3px; border-radius: 50%;
  background: var(--fx-spark); box-shadow: var(--fx-spark-glow);
}


/* ============================================================
   7 · EMBERS
   The hero floor gives off embers — warm ones rising, a few cold violet
   motes drifting — painted on one canvas by js/ctd-motion.js, which also
   stops painting whenever the hero is off screen or the tab is hidden.
   Behind everything in the hero's own stacking context, over the moon.
   ============================================================ */
.ctd-embers {
  position: absolute; inset: 0; z-index: -1;
  width: 100%; height: 100%; pointer-events: none;
  opacity: 0; transition: opacity 1.2s var(--ease);
}
.ctd-embers.is-lit { opacity: 1; }


/* ============================================================
   8 · THE FOIL PASS
   ============================================================ */
/* The cover's gold casting catches the light every --t-foil. It moves the
   casting's own sheen layer (components.css), which is why the second
   position in each pair is the face, held still. A hover takes the sheen
   over, so the idle pass stands down while the pointer is on it. */
@keyframes ctd-cta-foil {
  0%, 12%   { background-position: -120% 0, 0 0; }
  26%, 100% { background-position: 220% 0, 0 0; }
}
html[data-ctd-motion] .ctd-home__cta > .ctd-btn--primary::after {
  animation: ctd-cta-foil var(--t-foil) var(--ease-in-out) calc(var(--t-step) * 6 + var(--t-enter)) infinite;
}
html[data-ctd-motion] .ctd-home__cta > .ctd-btn--primary:hover::after { animation: none; }


/* ============================================================
   9 · CHROME — popovers, drawer, dialogs, toasts, the basket badge
   ============================================================ */
@keyframes ctd-pop-in {
  from { opacity: 0; transform: translateY(-8px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}
@keyframes ctd-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes ctd-slide-in-r {
  from { opacity: 0; transform: translateX(24px); }
  to   { opacity: 1; transform: none; }
}
@keyframes ctd-panel-in-r { from { transform: translateX(100%); } to { transform: none; } }
@keyframes ctd-dialog-in {
  from { opacity: 0; transform: translateY(16px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}
@keyframes ctd-toast-in {
  0%   { opacity: 0; transform: translateY(18px) scale(0.94); }
  60%  { opacity: 1; transform: translateY(-3px) scale(1.01); }
  100% { opacity: 1; transform: none; }
}
@keyframes ctd-bump {
  0%   { transform: scale(1);    box-shadow: var(--fx-bump-ring); }
  30%  { transform: scale(1.45); }
  55%  { transform: scale(0.92); }
  100% { transform: scale(1);    box-shadow: var(--fx-bump-ring-out); }
}

/* The account menu is un-hidden by header.js, so it animates on the way in.
   The builder group and the mini-cart are opacity/visibility panels; their
   own transitions in chrome.css carry them, and the menu rows inside the
   builder group step in behind the panel. */
html[data-ctd-motion] .ctd-account__menu:not([hidden]) {
  transform-origin: top right;
  animation: ctd-pop-in calc(var(--t-base) * 1.3) var(--ease-out);
}
html[data-ctd-motion] .ctd-navgroup:is(:hover, :focus-within) .ctd-menu > li {
  animation: ctd-slide-in-r calc(var(--t-base) * 1.6) var(--ease-out) backwards;
}
html[data-ctd-motion] .ctd-navgroup:is(:hover, :focus-within) .ctd-menu > li:nth-child(2) { animation-delay: 40ms; }
html[data-ctd-motion] .ctd-navgroup:is(:hover, :focus-within) .ctd-menu > li:nth-child(3) { animation-delay: 80ms; }

/* The phone drawer: the scrim fades, the panel slides in off the edge and
   its rows follow it one after another. */
html[data-ctd-motion] .ctd-drawer:not([hidden]) { animation: ctd-fade-in var(--t-base) var(--ease); }
html[data-ctd-motion] .ctd-drawer:not([hidden]) .ctd-drawer__panel {
  animation: ctd-panel-in-r calc(var(--t-enter) * 0.45) var(--ease-out);
}
html[data-ctd-motion] .ctd-drawer:not([hidden]) .ctd-drawer__nav > * {
  animation: ctd-slide-in-r calc(var(--t-enter) * 0.6) var(--ease-out) backwards;
  animation-delay: calc(120ms + var(--ctd-n, 0) * 32ms);
}
.ctd-drawer__nav > :nth-child(2) { --ctd-n: 1; }  .ctd-drawer__nav > :nth-child(3) { --ctd-n: 2; }
.ctd-drawer__nav > :nth-child(4) { --ctd-n: 3; }  .ctd-drawer__nav > :nth-child(5) { --ctd-n: 4; }
.ctd-drawer__nav > :nth-child(6) { --ctd-n: 5; }  .ctd-drawer__nav > :nth-child(7) { --ctd-n: 6; }
.ctd-drawer__nav > :nth-child(n + 8) { --ctd-n: 7; }

/* Every shared dialog (js/ctd-ui.js) rises into place over a fading scrim. */
html[data-ctd-motion] .ctd-overlay:not([hidden]) { animation: ctd-fade-in var(--t-base) var(--ease); }
html[data-ctd-motion] .ctd-overlay:not([hidden]) .ctd-dialog {
  animation: ctd-dialog-in calc(var(--t-enter) * 0.4) var(--ease-out);
}

/* A toast is created on demand, so its entrance plays exactly once. */
html[data-ctd-motion] .ctd-toasts .ctd-toast {
  animation: ctd-toast-in calc(var(--t-enter) * 0.55) var(--ease-out);
}

/* ARROWS LEAD. Anything you can press that carries a right arrow steps the
   arrow forward on hover, and a left arrow steps back — site-wide, one rule,
   keyed off the sprite reference so no template needs a class for it. */
.ctd-i:has(> use[href="#i-arrow-right"]),
.ctd-i:has(> use[href="#i-arrow-left"]) { transition: transform var(--t-base) var(--ease-out); }
:is(a, button, [role="button"]):hover .ctd-i:has(> use[href="#i-arrow-right"]) { transform: translateX(3px); }
:is(a, button, [role="button"]):hover .ctd-i:has(> use[href="#i-arrow-left"]) { transform: translateX(-3px); }

/* The header search opens as a wipe out of its own button rather than
   appearing whole beside it. */
@keyframes ctd-search-open {
  from { opacity: 0; clip-path: inset(0 0 0 100%); }
  to   { opacity: 1; clip-path: inset(-8px -8px -8px -8px); }
}
html[data-ctd-motion] .ctd-search:not([hidden]) .ctd-input {
  animation: ctd-search-open calc(var(--t-base) * 1.4) var(--ease-out) backwards;
}

/* The phone tab bar's lit edge draws out from the middle on arrival. */
@keyframes ctd-tab-lit { from { transform: scaleX(0); } to { transform: none; } }
html[data-ctd-motion] .ctd-tabbar__item--on::before {
  animation: ctd-tab-lit calc(var(--t-enter) * 0.6) var(--ease-out) calc(var(--t-step) * 3) backwards;
}

/* Footer links lean toward the pointer, a couple of pixels — the list reads
   as a set of things you can pull rather than a block of small print. */
.ctd-footer ul a { display: inline-block; transition: color var(--t-fast) var(--ease), transform var(--t-base) var(--ease-out); }
.ctd-footer ul a:hover { transform: translateX(3px); }

/* FAQ answers open and close instead of snapping. ::details-content plus
   interpolate-size is what lets a <details> body animate to its natural
   height with no script and no measured max-height; a browser without them
   keeps the native snap, which is still correct. content-visibility is
   transitioned discretely so the closing answer stays painted until its
   height has run out, instead of vanishing on the first frame. */
@supports selector(::details-content) {
  @media (prefers-reduced-motion: no-preference) {
    details.ctd-details { interpolate-size: allow-keywords; }
    details.ctd-details::details-content {
      block-size: 0; overflow: hidden; opacity: 0;
      transition: block-size var(--t-base) var(--ease-out), opacity var(--t-base) var(--ease),
                  content-visibility var(--t-base) allow-discrete;
    }
    details.ctd-details[open]::details-content { block-size: auto; opacity: 1; }
  }
}

/* The basket badge takes an item: a jolt and a gold ring leaving it.
   js/ctd-motion.js adds .is-bump when the count goes UP, and takes it off
   again at animationend so the next item can play it too. */
.ctd-header .cart-count.is-bump,
.ctd-header .ctd-count.is-bump {
  animation: ctd-bump var(--t-bump) var(--ease-out);
}


/* ============================================================
   9b · THE BUILDERS AND THE PRODUCT PAGE — feedback, and printing in
   The builder is a tool and spends half the effect budget: nothing here
   is ambient. Each rule answers something the customer just did — moved
   a step, changed the price, opened the review — and the one showpiece,
   printing in, is about the product itself: the sprues in the review and
   on a team-set page appear the way the real ones do, layer on layer
   under a curing light. The drawings themselves are untouched; only a
   clip moves across them.

   The selectors carry html[data-ctd-motion] for weight as well as for the
   switch: builder.css loads after this file and restates .v2-step-anim.
   ============================================================ */
html[data-ctd-motion] .v2-step-anim {
  animation: ctd-step-rise calc(var(--t-enter) * 0.55) var(--ease-out);
}
@keyframes ctd-step-rise {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

/* A price that changes says so: a jolt, a flash of bone and the price
   bloom, then back. js/ctd-motion.js adds .is-tick to #barTotal and the
   review total when their text changes, and removes it at animationend. */
@keyframes ctd-tick {
  0%   { transform: scale(1); }
  30%  { transform: scale(1.14); color: var(--bone-100); text-shadow: var(--fx-glow-price); }
  100% { transform: scale(1); }
}
.is-tick { display: inline-block; transform-origin: 50% 70%; animation: ctd-tick var(--t-bump) var(--ease-out); }

/* PRINTING IN. The drawing is uncovered from the bottom edge up while the
   curing line climbs it, then the line goes out. The clip is on the
   drawing, the line is on its frame, so a caption under a frame is never
   clipped — it is simply passed over. */
@keyframes ctd-print {
  from { clip-path: inset(100% -2px -2px -2px); }
  to   { clip-path: inset(-2px -2px -2px -2px); }
}
@keyframes ctd-print-line {
  0%   { top: 100%; opacity: 0; }
  8%   { opacity: 1; }
  88%  { opacity: 1; }
  100% { top: 0%; opacity: 0; }
}
html[data-ctd-motion] .ctd-sheet__art.is-printing { position: relative; }
html[data-ctd-motion] .ctd-sheet__art.is-printing > svg {
  animation: ctd-print var(--t-print) var(--ease-in-out) backwards;
  animation-delay: calc(var(--ctd-n, 0) * 60ms);
}
html[data-ctd-motion] .ctd-sheet__art.is-printing::after {
  content: ""; position: absolute; left: -6px; right: -6px; height: 2px; top: 100%;
  border-radius: var(--r-pill); pointer-events: none; opacity: 0;
  background: var(--fx-uv-line); box-shadow: var(--fx-uv-glow);
  animation: ctd-print-line var(--t-print) var(--ease-in-out) backwards;
  animation-delay: calc(var(--ctd-n, 0) * 60ms);
}

/* A skill card you press gives under the finger: pressing is
   feedback, and it is the builder's one allowed movement under the pointer.
   (The filter chips already seat like castings — builder.css §20.) */
.skill-card:active { transform: translateY(1px) scale(0.98); }


/* ============================================================
   10 · SCROLL-LINKED DEPTH
   CSS scroll timelines, so this costs no script and runs on the
   compositor. A browser without them gets the page as it was, which is
   why every rule is inside the @supports and nothing outside depends on
   it. Each one moves an individual transform property (translate, scale)
   so it composes with whatever hover `transform` the element already has.
   ============================================================ */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* Photos drift inside their frames as the frame crosses the screen —
       the frame moves with the page and the picture a little slower, which
       is what makes it read as a window rather than a sticker. Scaled up a
       touch first so the drift never shows an edge. */
    @keyframes ctd-drift {
      from { translate: 0 calc(var(--t-drift-y) * -1); }
      to   { translate: 0 var(--t-drift-y); }
    }
    :is(.ctd-card__media, .product-card__image) img {
      scale: 1.1;
      animation: ctd-drift linear both;
      animation-timeline: view();
      animation-range: cover;
    }

    /* Carousel slides take depth as they pass through the frame: full size
       and fully lit at rest, smaller and dimmer as a swipe carries them off.
       view(inline) reads the slide against its own track, so at a snapped
       rest every slide sits at the middle of its timeline — 100%, 1. */
    @keyframes ctd-slide-depth {
      0%        { opacity: 0.15; scale: 0.84; }
      45%, 55%  { opacity: 1;    scale: 1; }
      100%      { opacity: 0.15; scale: 0.84; }
    }
    .ctd-sheet__art {
      animation: ctd-slide-depth linear both;
      animation-timeline: view(inline);
    }
  }
}

/* The reading bar: document pages get a gold rule across the top of the
   window that fills as you read. Scroll timeline only, same reasoning. */
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    @keyframes ctd-read { from { transform: scaleX(0); } to { transform: scaleX(1); } }
    /* scaleX(0) at rest as well as at the start: a page too short to scroll
       has an INACTIVE scroll timeline, the animation then applies nothing,
       and without this the bar sat full width across the top of the 404. */
    .ctd-doc::before {
      content: ""; position: fixed; left: 0; right: 0; top: 0; z-index: var(--z-toast);
      height: 3px; pointer-events: none; transform-origin: 0 50%; transform: scaleX(0);
      background: var(--fx-progress); box-shadow: var(--fx-progress-glow);
      animation: ctd-read linear both;
      animation-timeline: scroll(root);
    }
  }
}


/* ============================================================
   11 · PAGE TRANSITIONS
   A full page load cross-fades instead of blinking to white: the leaving
   page sinks and dims, the arriving one rises into it. The header carries
   its own name, so it is matched across the two pages and simply stays
   put while the page under it changes. js/shell-router.js drives the same
   pseudo-elements for its soft navigations through
   document.startViewTransition(), so both kinds of navigation look alike.
   ============================================================ */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
}
.ctd-header { view-transition-name: ctd-header; }
@keyframes ctd-vt-out { to { opacity: 0; transform: translateY(-14px); } }
@keyframes ctd-vt-in  { from { opacity: 0; transform: translateY(22px); } }
::view-transition-old(root) { animation: ctd-vt-out var(--t-vt, 340ms) var(--ease-in-out, ease) both; }
::view-transition-new(root) { animation: ctd-vt-in calc(var(--t-vt, 340ms) * 1.35) var(--ease-out, ease-out) both; }


/* ============================================================
   12 · REDUCED MOTION
   Everything in this file is movement, so everything goes — the arrival
   runs to its end state instantly (the tokens are zeroed in tokens.css and
   every arrival is under html[data-ctd-motion], which is never set), and
   the ambient loops are cancelled outright here. The pointer light is a
   light, not motion; it stays.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  .ctd-plate, .ctd-plate__rivet,
  .ctd-home__cta > .ctd-btn--primary::after,
  .ctd-header .cart-count.is-bump, .ctd-header .ctd-count.is-bump,
  .is-tick { animation: none; }
  .skill-card:active, .ctd-footer ul a:hover,
  :is(a, button, [role="button"]):hover .ctd-i:has(> use[href="#i-arrow-right"]),
  :is(a, button, [role="button"]):hover .ctd-i:has(> use[href="#i-arrow-left"]) { transform: none; }
  .ctd-embers { display: none; }
  .ctd-sparks { display: none; }
  .ctd-spot { transition: none; }
}


/* ============================================================
   13 · HELD STILL — the visitor's Pause (WCAG 2.2.2)
   The hero carousel's toggle is the page's pause control, and it pauses
   more than the slides: js/carousel.js sets html[data-ctd-still] (and
   remembers it, and header.php sets it before first paint). This stops
   every AMBIENT LOOP the reduced-motion blocks stop — here and in
   ornament.css — and puts the embers out. It leaves arrivals, hover
   feedback, sparks and the pointer light alone: none runs for five
   seconds, and replaying the arrival on Play would be a bug of its own.

   KEEP THIS LIST IN STEP WITH THE REDUCED-MOTION BLOCKS. A loop added to
   one and not the other is a loop the Pause button cannot stop. Each
   selector is written at a weight that beats the loop's own rule, or
   matches it and wins on source order.
   ============================================================ */
html[data-ctd-still] .ctd-mist__sheet, html[data-ctd-still] .ctd-mist__sheet.is-up,
html[data-ctd-still] .ctd-mist__drop, html[data-ctd-still] .ctd-mist__drop.is-up,
html[data-ctd-still] .ctd-mist__spin, html[data-ctd-still] .ctd-mist__spin.is-ccw,
html[data-ctd-still] .ctd-mist__sway, html[data-ctd-still] .ctd-mist__wander,
html[data-ctd-still] .ctd-mist__bloom, html[data-ctd-still] .ctd-mist__ghost,
html[data-ctd-still] .ctd-orn__glint, html[data-ctd-still] .ctd-orn__ring,
html[data-ctd-still] .ctd-orn__chain-run, html[data-ctd-still] .ctd-rule__line,
html[data-ctd-still] .ctd-wall--torchlit::after, html[data-ctd-still] .ctd-wall__wet,
html[data-ctd-still] .ctd-edge::before, html[data-ctd-still] .ctd-edge::after,
html[data-ctd-still] .ctd-turf__w,
html[data-ctd-still] .ctd-home__cta > .ctd-btn--primary::after { animation: none; }
/* The rest poses reduced motion gives them: a stopped ring hangs straight,
   a glint is gone rather than frozen mid-flash, a drop stays drawn. */
html[data-ctd-still] .ctd-orn__glint { opacity: 0; }
html[data-ctd-still] .ctd-mist__drop, html[data-ctd-still] .ctd-mist__drop.is-up { opacity: .85; }
html[data-ctd-still] .ctd-embers { display: none; }

/* ============================================================
   14 · THE EVENT LOOK IN MOTION (2026-09-28)
   The owner's calls on the overhaul branch still stand: "good animations
   and animated backgrounds/shimmers", and nothing ambient inside the
   builder. So the ghost's wisps drift, a glint travels the gold rules
   under the bar and the sheet heads, the cover's gold line takes a foil
   pass and its face floats, and the two-still steps cross-fade. All of it
   runs under the pre-paint switch only, stops for the page's Pause
   (html[data-ctd-still], js/carousel.js) and for reduced motion, and none
   of it runs on an app page. The drawn sprues never move.
   The mist itself flows on a canvas over all this (js/ctd-motion.js §10,
   "so it looks like flowing fog"), under the same rules.
   ============================================================ */
@keyframes ctd-ghost-drift {
  from { transform: translate3d(-3.5%, 1.2%, 0) scale(1.02); }
  to   { transform: translate3d(3.5%, -1.8%, 0) scale(1.07); }
}
@keyframes ctd-ghost-breathe { from { opacity: 0.55; } to { opacity: 1; } }
@keyframes ctd-rule-glint {
  0%, 30%   { background-position: -30% 0; }
  70%, 100% { background-position: 130% 0; }
}
@keyframes ctd-step-swap {
  0%, 38%  { opacity: 0; }
  50%, 88% { opacity: 1; }
  100%     { opacity: 0; }
}
@keyframes ctd-face-float { from { transform: translateY(0); } to { transform: translateY(-10px); } }
@keyframes ctd-title-foil {
  0%, 58%   { background-position: 100% 0; }
  88%, 100% { background-position: 0% 0; }
}

html[data-ctd-motion] .ctd-ghost__drift {
  animation: ctd-ghost-drift var(--t-ghost) var(--ease) infinite alternate,
             ctd-ghost-breathe var(--t-ghost-breathe) var(--ease) infinite alternate;
}
html[data-ctd-motion] .ctd-header::after { animation: ctd-rule-glint var(--t-rule-glint) var(--ease) infinite; }
html[data-ctd-motion] .ctd-sheethead::after { animation: ctd-rule-glint var(--t-rule-glint) var(--ease) infinite; animation-delay: 1.2s; }
html[data-ctd-motion] .ctd-home__step-b { animation: ctd-step-swap var(--t-step-swap) var(--ease) infinite; }
html[data-ctd-motion] .ctd-home__step:nth-child(3) .ctd-home__step-b { animation-delay: 1.6s; }
html[data-ctd-motion] .ctd-home__face img { animation: ctd-face-float var(--t-face-float) var(--ease) infinite alternate; }
html[data-ctd-motion] .ctd-home__face::before { animation: ctd-ghost-breathe var(--t-ghost-breathe) var(--ease) infinite alternate; }
/* The foil: the glow moves to a drop-shadow FILTER, because a text-shadow
   paints over background-clipped text and washes the foil out. */
html[data-ctd-motion] .ctd-title--hero .alt {
  background: var(--fx-title-foil); background-size: 320% 100%; background-position: 100% 0;
  -webkit-background-clip: text; background-clip: text; color: transparent;
  text-shadow: none; filter: var(--fx-title-glow-drop);
  animation: ctd-title-foil var(--t-foil) var(--ease) infinite;
}

/* The cover arrives the way the hero did: the badge, the eyebrow, the title,
   the lede and the meta line rise one after another on the --t-step clock;
   the castings keep their own landing (§2). */
html[data-ctd-motion] .ctd-home__cover-copy > * { animation: ctd-arrive-rise var(--t-enter) var(--ease-out) backwards; }
html[data-ctd-motion] .ctd-home__cover-copy > .ctd-home__eyebrow { animation-delay: var(--t-step); }
html[data-ctd-motion] .ctd-home__cover-copy > .ctd-title { animation-delay: calc(var(--t-step) * 2); }
html[data-ctd-motion] .ctd-home__cover-copy > .ctd-lede { animation-delay: calc(var(--t-step) * 4); }
html[data-ctd-motion] .ctd-home__cover-copy > .ctd-home__cta { animation: none; }
html[data-ctd-motion] .ctd-home__cover-copy > .ctd-home__meta { animation-delay: calc(var(--t-step) * 10); }

/* A tool gets feedback only: nothing ambient on an app page. */
.ctd-app-page .ctd-ghost__drift,
.ctd-app-page .ctd-header::after,
.ctd-app-page .ctd-sheethead::after { animation: none; }

/* The page's Pause. Written at the loops' own weight, winning on order. */
html[data-ctd-still] .ctd-ghost__drift,
html[data-ctd-still] .ctd-header::after,
html[data-ctd-still] .ctd-sheethead::after,
html[data-ctd-still] .ctd-home__step-b,
html[data-ctd-still] .ctd-home__face img,
html[data-ctd-still] .ctd-home__face::before,
html[data-ctd-still] .ctd-title--hero .alt { animation: none; }

@media (prefers-reduced-motion: reduce) {
  .ctd-ghost__drift, .ctd-header::after, .ctd-sheethead::after,
  .ctd-home__step-b, .ctd-home__face img, .ctd-home__face::before,
  .ctd-title--hero .alt { animation: none; }
}

/* ============================================================
   15 · THE BAR'S BACKGROUND (2026-09-29)
   "Make the header bar more interesting, the background. It should have
   animation or some form of effect going on." Pools of violet and gold
   slide under the row, motes drift the other way and twinkle, and a band of
   lamplight crosses the bar every so often — on top of the glint that already
   runs its rule. Every layer is a transform or a background-position, so
   none of it repaints; the tiles are fixed lengths, so every loop lands on
   itself at any width (chrome.css says why). The timings are unrelated on
   purpose (80s, 46s, 17s, and the rule's 11s): they only line up once in a
   very long while, so the bar never repeats itself where anyone can see.
   Same four cancels as everything above: an app page, the Pause, reduced
   motion — and, being under the switch, no script.
   ============================================================ */
@keyframes ctd-hdr-pools   { from { transform: translate3d(0, 0, 0); } to { transform: translate3d(calc(var(--hdr-pool-tile) * -1), 0, 0); } }
@keyframes ctd-hdr-motes   { from { transform: translate3d(calc(var(--hdr-mote-tile) * -1), 0, 0); } to { transform: translate3d(0, 0, 0); } }
@keyframes ctd-hdr-twinkle { from { opacity: 0.45; } to { opacity: 1; } }
@keyframes ctd-hdr-sheen {
  0%, 46%   { background-position: -40% 0; }
  78%, 100% { background-position: 140% 0; }
}
html[data-ctd-motion] .ctd-header__board::before { animation: ctd-hdr-pools var(--t-hdr-pools) linear infinite; will-change: transform; }
html[data-ctd-motion] .ctd-header__board::after {
  animation: ctd-hdr-motes var(--t-hdr-motes) linear infinite,
             ctd-hdr-twinkle var(--t-hdr-twinkle) ease-in-out infinite alternate;
  will-change: transform;
}
html[data-ctd-motion] .ctd-header::before { animation: ctd-hdr-sheen var(--t-hdr-sheen) ease-in-out infinite; }

.ctd-app-page .ctd-header__board::before,
.ctd-app-page .ctd-header__board::after,
.ctd-app-page .ctd-header::before,
html[data-ctd-still] .ctd-header__board::before,
html[data-ctd-still] .ctd-header__board::after,
html[data-ctd-still] .ctd-header::before { animation: none; will-change: auto; }

@media (prefers-reduced-motion: reduce) {
  .ctd-header__board::before, .ctd-header__board::after, .ctd-header::before { animation: none; will-change: auto; }
}

/* ON A TOUCH SCREEN THE REPAINTING LOOPS REST (2026-10-09). The bar's sheen,
   its rule's glint, the sheet heads' glint and the hero's foil animate
   background-position, so every frame of them is a repaint of a full-width
   strip — on a sticky header that is the whole time the page is open, and it
   was costing phones frames while they scrolled. The pools and motes are
   transforms on the compositor and keep moving; so does the fog. */
@media (pointer: coarse) {
  .ctd-header::before, .ctd-header::after, .ctd-sheethead::after, .ctd-title--hero .alt { animation: none; }
  /* With the foil not moving there is nothing for its clipped-text gradient
     to do, and that recipe — background-clip: text under a drop-shadow
     filter, inside an animated, overflow-clipped parent — is the one Safari
     has drawn as invisible text. A touch screen gets the plain gold line every
     other title's second line is (components.css .ctd-title .alt). */
  html[data-ctd-motion] .ctd-title--hero .alt {
    background: none; -webkit-background-clip: border-box; background-clip: border-box;
    color: var(--gold-300); filter: none; text-shadow: var(--fx-glow-title);
  }
}

/* UNDER A DIALOG THE AMBIENT LOOPS REST (2026-10-01). Nothing behind the
   scrim can be seen, and on a phone the bar's sheen and the title's foil
   (both background-position, so both repaint) and the face's float were
   spending the frames the 360 viewer turns on. Paused rather than removed,
   so each picks up where it stopped when the dialog closes. js/ctd-motion.js
   rests its two canvases the same way (covered()). */
body.ctd-dialog-open .ctd-ghost__drift,
body.ctd-dialog-open .ctd-header::after,
body.ctd-dialog-open .ctd-header::before,
body.ctd-dialog-open .ctd-header__board::before,
body.ctd-dialog-open .ctd-header__board::after,
body.ctd-dialog-open .ctd-sheethead::after,
body.ctd-dialog-open .ctd-home__step-b,
body.ctd-dialog-open .ctd-home__face img,
body.ctd-dialog-open .ctd-home__face::before,
body.ctd-dialog-open .ctd-title--hero .alt { animation-play-state: paused; }
