/* ProudSite motion system v1.1 (first-party; REFERENCE/motion-plan/PLAN.md).
   Rules: content is visible by default; every hiding style sits behind BOTH
   prefers-reduced-motion: no-preference AND html.ps-boot:not(.ps-reduce):not(.ps-nojs); effects animate transform and
   opacity, which browsers CAN run on the compositor (measured per template, not guaranteed);
   no scroll listeners. Declared exceptions are listed in PLAN.md (class B). */

/* ---- 1. Tokens ------------------------------------------------------------------------- */
:root {
  --ps-dur-xs: 120ms;
  --ps-dur-s: 200ms;
  --ps-dur-m: 420ms;
  --ps-dur-l: 700ms;
  --ps-dur-ambient: 40s;
  --ps-stagger: 60ms;
  --ps-dist-s: 8px;
  --ps-dist-m: 24px;
  --ps-dist-l: 48px;
  --ps-parallax-max: 40px;
  --ps-tilt-max: 6deg;
  --ps-magnet-max: 8px;
  --ps-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ps-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --ps-ease-spring: cubic-bezier(0.22, 1, 0.36, 1);
  --ps-ease-spring-soft: cubic-bezier(0.34, 1.3, 0.64, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
  :root {
    --ps-ease-spring: linear(0, 0.062, 0.184, 0.328, 0.46, 0.579, 0.674, 0.754, 0.814, 0.862, 0.898, 0.925, 0.946, 0.96, 0.971, 0.979, 0.985, 0.989, 0.992, 0.994, 1);
    --ps-ease-spring-soft: linear(0, 0.065, 0.218, 0.396, 0.571, 0.724, 0.85, 0.939, 0.999, 1.035, 1.052, 1.055, 1.051, 1.042, 1.032, 1.023, 1.014, 1.008, 1.003, 1, 1);
  }
}

/* Motion runs only after a successful boot with motion allowed. Otherwise (the effective preference is
   'reduce' -> html.ps-reduce; the boot script never ran -> no .ps-boot, which also covers an unread saved
   choice; the module never arrived -> .ps-nojs) this stills ALL CSS motion, the template's own included:
   hover transitions, menus, icon spins and smooth scrolling (Codex motion r2 #1, r6 #1). */
:is(html.ps-reduce, html.ps-nojs, html:not(.ps-boot)), :is(html.ps-reduce, html.ps-nojs, html:not(.ps-boot))::before, :is(html.ps-reduce, html.ps-nojs, html:not(.ps-boot))::after, :is(html.ps-reduce, html.ps-nojs, html:not(.ps-boot)) *, :is(html.ps-reduce, html.ps-nojs, html:not(.ps-boot)) *::before, :is(html.ps-reduce, html.ps-nojs, html:not(.ps-boot)) *::after { transition-duration: 0s !important; transition-delay: 0s !important; animation-duration: 0s !important; animation-delay: 0s !important; scroll-behavior: auto !important; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 0s !important; transition-delay: 0s !important; animation-duration: 0s !important; animation-delay: 0s !important; scroll-behavior: auto !important; }
}

/* ---- 2. Page transitions: in ps-vt.css (linked only by templates whose motion is not 'off', so an
   'off' template has no @view-transition even with scripts disabled; Codex motion r3 #2). */

/* ---- 3. Scroll reveals (scroll-driven, compositor; IntersectionObserver fallback in JS) -- */
/* Text reveals SLIDE ONLY: a scroll-linked fade would leave text part-transparent, below the contrast
   minimum, if the visitor stops scrolling mid-reveal (found by the template-concepts contrast review).
   Opacity fades (ps-fade, data-ps="fade") are for images and decoration only; motion-check enforces it. */
@keyframes ps-fade-up { from { translate: 0 var(--ps-dist-m); } }
@keyframes ps-fade { from { opacity: 0; } }
@keyframes ps-wipe { from { scale: 1 1; } to { scale: 1 0; } }
@keyframes ps-wipe-x { from { scale: 1 1; } to { scale: 0 1; } }
@keyframes ps-settle { from { scale: 1.08; } to { scale: 1; } }
@keyframes ps-parallax { from { translate: 0 calc(var(--ps-parallax-max) * -1); } to { translate: 0 var(--ps-parallax-max); } }
@keyframes ps-grow-x { from { scale: 0 1; } to { scale: 1 1; } }
@keyframes ps-shadow-in { from { opacity: 0; } to { opacity: 1; } }

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
  /* Longhands only: Chrome (148) rejects a timeline inside the `animation` shorthand, and the CSS minifier
     merges a full shorthand + animation-timeline into exactly that, silently killing the effect. */
    html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="fade-up"] { animation-name: ps-fade-up; animation-timing-function: var(--ps-ease-out); animation-fill-mode: both; animation-timeline: view(); animation-range: entry 0% entry 70%; }
    html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="fade"] { animation-name: ps-fade; animation-timing-function: linear; animation-fill-mode: both; animation-timeline: view(); animation-range: entry 0% entry 70%; }
    /* Stagger: each child's range starts a little later (--i set at build time). Time delays do not apply to scroll timelines. */
    html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="stagger"] > * { animation-name: ps-fade-up; animation-timing-function: var(--ps-ease-out); animation-fill-mode: both; animation-timeline: view(); animation-range: entry calc(var(--i, 0) * 6%) entry calc(60% + var(--i, 0) * 6%); }
    /* Wipe: the frame's cover panel shrinks away (transform only, so it stays on the GPU). */
    html.ps-boot:not(.ps-reduce):not(.ps-nojs) :is([data-ps~="wipe"], [data-ps~="wipe-x"])::after { content: ""; position: absolute; inset: -1px; /* past the edge: no hairline at anti-aliased corners */ background: var(--ps-wipe-cover, Canvas); transform-origin: 50% 0; animation-name: ps-wipe; animation-timing-function: var(--ps-ease-in-out); animation-fill-mode: both; animation-timeline: view(); animation-range: entry 10% cover 40%; pointer-events: none; }
    html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="wipe-x"]::after { transform-origin: 100% 50%; animation-name: ps-wipe-x; }
    html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="settle"] > img { animation-name: ps-settle; animation-timing-function: linear; animation-fill-mode: both; animation-timeline: view(); animation-range: entry 0% cover 60%; }
    html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="parallax"] > img { scale: 1.12; animation-name: ps-parallax; animation-timing-function: linear; animation-fill-mode: both; animation-timeline: view(); animation-range: cover; }
    html.ps-boot:not(.ps-reduce):not(.ps-nojs) .ps-progress { animation-name: ps-grow-x; animation-timing-function: linear; animation-fill-mode: both; animation-timeline: scroll(root); }
    html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="condense"]::after { animation-name: ps-shadow-in; animation-timing-function: linear; animation-fill-mode: both; animation-timeline: scroll(root); animation-range: 0 120px; }
  }
}
/* Wipe frames hold the image and its cover panel. */
:is([data-ps~="wipe"], [data-ps~="wipe-x"]) { position: relative; overflow: clip; }
/* Focus wins over reveals: a focused element (or one containing focus) is shown at once (plan C14). */
html [data-ps]:focus-within, html [data-ps~="stagger"] > :focus-within, html:focus-within .ps-words[data-ps~="rise"] .ps-wi { animation: none !important; transition: none !important; opacity: 1 !important; translate: none !important; }
html :is([data-ps~="wipe"], [data-ps~="wipe-x"]):focus-within::after { display: none !important; }
/* Frames that hold settle/parallax images clip the overflow. */
[data-ps~="settle"], [data-ps~="parallax"] { overflow: clip; }
[data-ps~="settle"] > img, [data-ps~="parallax"] > img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* Fallback (no scroll timelines): ps-motion.js adds .ps-io to <html> and .ps-in to each element
   as it enters. Elements already on screen are marked before .ps-io is set, so nothing flashes.
   Text slides only, exactly as on the native path: it is never shown part-transparent (concepts r4 #5). */
@media (prefers-reduced-motion: no-preference) {
  html.ps-boot.ps-io:not(.ps-reduce):not(.ps-nojs) :is([data-ps~="fade-up"], [data-ps~="stagger"] > *):not(.ps-in) { translate: 0 var(--ps-dist-m); }
  html.ps-boot.ps-io:not(.ps-reduce):not(.ps-nojs) [data-ps~="fade"]:not(.ps-in) { opacity: 0; }
  html.ps-boot.ps-io:not(.ps-reduce):not(.ps-nojs) :is([data-ps~="fade-up"], [data-ps~="stagger"] > *) { transition: translate var(--ps-dur-m) var(--ps-ease-out); transition-delay: calc(var(--i, 0) * var(--ps-stagger)); }
  html.ps-boot.ps-io:not(.ps-reduce):not(.ps-nojs) [data-ps~="fade"] { transition: opacity var(--ps-dur-m) var(--ps-ease-out); }
}

/* Reading progress bar and condensing header shadow. */
.ps-progress { position: fixed; inset: 0 0 auto 0; height: 3px; background: var(--ps-progress, currentColor); transform-origin: 0 50%; scale: 0 1; z-index: 100; pointer-events: none; }
@media (prefers-reduced-motion: reduce) { .ps-progress { display: none; } }
html.ps-reduce .ps-progress { display: none; }
@supports not (animation-timeline: scroll()) { .ps-progress { display: none; } }
[data-ps~="condense"] { position: sticky; }
[data-ps~="condense"]::after { content: ""; position: absolute; inset: auto 0 -12px 0; height: 12px; pointer-events: none; background: linear-gradient(rgba(0, 0, 0, 0.12), transparent); opacity: 0; }

/* ---- 4. Hero entrance (time-based; movement only, never hides the LCP element) ---------- */
@keyframes ps-rise { from { translate: 0 var(--ps-dist-m); } }
@media (prefers-reduced-motion: no-preference) {
  /* The LCP heading and anything containing it stay still (Codex motion r1 #4, plan C1). */
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="hero"] > :not(h1, :has(h1)) { animation: ps-rise var(--ps-dur-l) var(--ps-ease-spring) both; animation-delay: calc(var(--i, 0) * 80ms); }
}

/* ---- 5. Headline words (split at build time by <Words>; real text for screen readers) ----- */
.ps-words { display: inline; }
.ps-word { display: inline-block; }
.ps-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* Movement only, never opacity: a headline is usually the LCP element and must be visible at first paint
   (Codex motion review r1, finding 4). */
@keyframes ps-word { from { translate: 0 0.35em; } }
@media (prefers-reduced-motion: no-preference) {
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) .ps-words[data-ps~="words"] .ps-word { animation: ps-word var(--ps-dur-l) var(--ps-ease-spring) both; animation-delay: calc(var(--i, 0) * var(--ps-stagger)); }
}

/* ---- 6. Marquee (pause button + pauses on hover, focus and off-screen) -------------------- */
/* Static (no JS, failed boot or module, reduced motion): one plain wrapped list, nothing clipped,
   nothing scrolls, no Pause button. With motion on, the strip is a single clipped row FROM FIRST PAINT
   (so starting the animation shifts nothing); it moves only once its Pause button is wired (.ps-ready).
   The button sits outside the clipped viewport, so it never moves or scrolls away (Codex motion r4 #2). */
.ps-marquee .ps-marquee-track > .ps-marquee-list { display: flex; flex-wrap: wrap; }
.ps-marquee .ps-marquee-track > .ps-marquee-list > * { white-space: normal; overflow-wrap: anywhere; }
.ps-marquee .ps-marquee-track > .ps-marquee-dup, .ps-marquee > .ps-marquee-btn { display: none; }
@keyframes ps-marquee { to { translate: -50% 0; } }
@media (prefers-reduced-motion: no-preference) {
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) .ps-marquee:not(.ps-static) { display: flex; align-items: center; gap: 12px; }
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) .ps-marquee:not(.ps-static) > .ps-marquee-viewport { flex: 1 1 auto; min-width: 0; overflow: clip; }
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) .ps-marquee:not(.ps-static) .ps-marquee-track { display: flex; width: max-content; }
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) .ps-marquee:not(.ps-static) .ps-marquee-track > .ps-marquee-list { flex-wrap: nowrap; flex: none; }
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) .ps-marquee:not(.ps-static) .ps-marquee-track > .ps-marquee-list > * { white-space: nowrap; }
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) .ps-marquee:not(.ps-static) .ps-marquee-track > .ps-marquee-dup { display: flex; }
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) .ps-marquee:not(.ps-static) > .ps-marquee-btn { display: inline-block; flex: none; }
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) .ps-marquee.ps-ready:not(.ps-static) .ps-marquee-track { animation: ps-marquee var(--ps-dur-ambient) linear infinite; }
  /* No :focus-within pause: the only focusable item is the Pause/Play button, and pressing Play
     would otherwise leave the strip paused while that button keeps focus. */
  /* Same specificity as the moving rule plus the state (its `animation` shorthand would otherwise reset play-state). */
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) .ps-marquee.ps-ready:not(.ps-static):is(:hover, .ps-paused, .ps-offscreen) .ps-marquee-track { animation-play-state: paused; }
}

/* ---- 7. Micro-interactions ---------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="press"] { transition: scale var(--ps-dur-s) var(--ps-ease-spring-soft); }
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="press"]:active { scale: 0.97; transition-duration: var(--ps-dur-xs); }
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="lift"] { transition: translate var(--ps-dur-s) var(--ps-ease-out); position: relative; }
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="lift"]::before { content: ""; position: absolute; inset: 0; border-radius: inherit; box-shadow: 0 18px 40px -18px rgba(0, 0, 0, 0.35); opacity: 0; transition: opacity var(--ps-dur-s) var(--ps-ease-out); pointer-events: none; z-index: -1; }
  @media (hover: hover) {
    html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="lift"]:hover { translate: 0 -4px; }
    html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="lift"]:hover::before { opacity: 1; }
  }
  /* Magnet and tilt: JS writes --ps-mx/--ps-my (px) and --ps-rx/--ps-ry (deg) only. */
  /* The magnet host (the link) stays still so its hit area never moves; its inner .ps-mag layer moves. */
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="magnet"] > .ps-mag { pointer-events: none; display: inline-flex; translate: var(--ps-mx, 0px) var(--ps-my, 0px); transition: translate var(--ps-dur-m) var(--ps-ease-spring-soft); }
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="tilt"] { transform: perspective(800px) rotateX(var(--ps-rx, 0deg)) rotateY(var(--ps-ry, 0deg)); transition: transform var(--ps-dur-m) var(--ps-ease-spring); }
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) :is([data-ps~="magnet"] > .ps-mag, [data-ps~="tilt"].ps-tracking) { transition-duration: 80ms; }
}

/* ---- 8. Menus, dialogs, popovers (enter and exit with @starting-style) -------------------- */
@media (prefers-reduced-motion: no-preference) {
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="pop"] { transition: opacity var(--ps-dur-s) var(--ps-ease-out), translate var(--ps-dur-s) var(--ps-ease-out), display var(--ps-dur-s) allow-discrete, overlay var(--ps-dur-s) allow-discrete; }
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="pop"]:is([hidden], dialog:not([open]), [popover]:not(:popover-open)) { opacity: 0; translate: 0 calc(var(--ps-dist-s) * -1); }
  @starting-style { html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="pop"] { opacity: 0; translate: 0 calc(var(--ps-dist-s) * -1); } }
}

/* ---- 9. Accordion (<details>; user-initiated, so not counted as layout shift) ------------- */
/* The panel opens at once (no height animation: that is layout, plan C8); its content fades and rises. */
@keyframes ps-panel { from { opacity: 0; translate: 0 calc(var(--ps-dist-s) * -1); } }
@media (prefers-reduced-motion: no-preference) {
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) details[data-ps~="accordion"][open] > :not(summary) { animation: ps-panel var(--ps-dur-m) var(--ps-ease-out); }
}

/* ---- 10. Grain (static texture; painted once, never animated) ----------------------------- */
/* A layer BEHIND the host's content (z-index -1 inside the host's own stacking context): it darkens only the
   host's background, so text drawn on it and opaque cards inside it are untouched (concepts r4 #4).
   Put grain on a WRAPPER: it owns ::before, so motion-check rejects it alongside lift, wipe or condense
   on the same element (Codex motion r7 #1). */
[data-ps~="grain"] { position: relative; isolation: isolate; }
[data-ps~="grain"]::before { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0.07; mix-blend-mode: multiply; z-index: -1;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }

/* ---- 11. Images fade in once decoded (JS adds .ps-loaded) --------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  html.ps-boot.ps-js:not(.ps-reduce):not(.ps-nojs) img[data-ps~="img-in"]:not(.ps-loaded) { opacity: 0; }
  html.ps-boot.ps-js:not(.ps-reduce):not(.ps-nojs) img[data-ps~="img-in"] { transition: opacity var(--ps-dur-m) var(--ps-ease-out); }
}

/* ---- 12. In-page links glide ------------------------------------------------------------ */
html { scroll-padding-top: var(--ps-scroll-pad, 0px); } /* WCAG 2.4.11: targets not hidden under a sticky header (plan C7) */
@media (prefers-reduced-motion: no-preference) { html.ps-boot:not(.ps-reduce):not(.ps-nojs) { scroll-behavior: smooth; } }

/* ---- 14. Editorial tier (reverse-engineered from award sites; v1.1) --------------------------- */
/* clip: the photo opens from a smaller inset window to the full frame as it arrives (Stripe/Odyssee).
   Declared paint exception: clip-path is not a compositor property; measured by the long-frame gate.
   Photos only. Set --ps-clip-r to the frame's corner radius. */
@keyframes ps-clip { from { clip-path: inset(12% 9% round var(--ps-clip-r, 0px)); } to { clip-path: inset(0 round var(--ps-clip-r, 0px)); } }
/* tint: the photo starts washed in a brand tint and warms to full colour as it arrives (an overlay fades). */
[data-ps~="tint"] { position: relative; }
@keyframes ps-untint { from { opacity: 0.85; } to { opacity: 0; } }
/* drift: the photo moves slowly inside its frame (oversized by 14%). hero-drift: the same for a hero photo,
   driven by page scroll from exactly 0, so at load the photo is at its final size and place (LCP rule). */
[data-ps~="drift"], [data-ps~="hero-drift"], [data-ps~="zoom-hover"] { overflow: clip; }
@keyframes ps-drift { from { translate: 0 -6%; } to { translate: 0 6%; } }
@keyframes ps-hero-drift { to { translate: 0 8%; scale: 1.2; } } /* grows faster than it travels: never a gap, even in a pinned panel */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="clip"] { animation-name: ps-clip; animation-timing-function: var(--ps-ease-out); animation-fill-mode: both; animation-timeline: view(); animation-range: entry 0% cover 42%; }
    html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="tint"]::after { content: ""; position: absolute; inset: 0; background: var(--ps-tint, #5f6f96); mix-blend-mode: color; pointer-events: none; animation-name: ps-untint; animation-timing-function: linear; animation-fill-mode: both; animation-timeline: view(); animation-range: entry 10% cover 40%; } /* full colour before the photo reaches the centre */
    html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="drift"] > img { scale: 1.14; animation-name: ps-drift; animation-timing-function: linear; animation-fill-mode: both; animation-timeline: view(); animation-range: cover; }
    html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="hero-drift"] > img { animation-name: ps-hero-drift; animation-timing-function: linear; animation-fill-mode: both; animation-timeline: scroll(root); animation-range: 0 100vh; }
    /* stack: full-height panels pin in turn; each next panel slides up over the last. No focusable content inside
       (motion-check): a covered panel's controls would be hidden under the next. */
    /* Pinned only once the module has confirmed every panel fits the viewport (.ps-tall otherwise: zoom, short
       screens), and never while anything inside has focus, so text and focus can never sit under the next panel. */
    html.ps-boot:not(.ps-reduce):not(.ps-nojs).ps-js [data-ps~="stack"]:not(.ps-tall):not(:focus-within) > * { position: sticky; top: 0; }
    html.ps-boot:not(.ps-reduce):not(.ps-nojs).ps-js [data-ps~="stack"]:not(.ps-tall):not(:focus-within) > * + * { box-shadow: 0 -24px 48px -24px rgb(0 0 0 / 0.35); }
    /* rail (LTR only; RTL keeps the wrapped grid): a pinned horizontal photo gallery. Travel uses the pin's own
       width (container units), not the viewport. */
    html.ps-boot.ps-js:not(.ps-reduce):not(.ps-nojs) [data-ps~="rail"]:dir(ltr) { view-timeline-name: --ps-rail; height: var(--ps-rail-len, 260vh); }
    html.ps-boot.ps-js:not(.ps-reduce):not(.ps-nojs) [data-ps~="rail"]:dir(ltr) > .ps-rail-pin { position: sticky; top: 0; height: 100svh; overflow: clip; display: flex; align-items: center; container-type: inline-size; }
    html.ps-boot.ps-js:not(.ps-reduce):not(.ps-nojs) [data-ps~="rail"]:dir(ltr) .ps-rail-track { flex: none; flex-wrap: nowrap; justify-content: flex-start; width: max-content; animation-name: ps-rail; animation-timing-function: linear; animation-fill-mode: both; animation-timeline: --ps-rail; animation-range: contain 0% contain 100%; }
  }
}
@keyframes ps-rail { to { translate: calc(-100% + 100cqw) 0; } }
/* rail static (reduced, no JS, unsupported, RTL): a wrapped grid of photos; nothing scrolls sideways. */
[data-ps~="rail"] .ps-rail-track { display: flex; flex-wrap: wrap; justify-content: center; gap: 18px; padding: 0 5vw; }
[data-ps~="rail"] .ps-rail-track > * { flex: none; }
/* zoom-hover: a slow push-in on a photo card (pointer devices). */
@media (prefers-reduced-motion: no-preference) {
  html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="zoom-hover"] img { transition: scale var(--ps-dur-l) var(--ps-ease-out); }
  @media (hover: hover) { html.ps-boot:not(.ps-reduce):not(.ps-nojs) [data-ps~="zoom-hover"]:hover img { scale: 1.05; } }
}
/* rise: headline words slide up from behind their own line ONCE when the heading comes into view (JS adds
   .ps-in), and always finish, whether or not scrolling continues. Without the module: plain text. */
.ps-words[data-ps~="rise"] .ps-word { overflow: clip; vertical-align: top; padding-bottom: 0.14em; margin-bottom: -0.14em; }
.ps-wi { display: inline-block; }
@media (prefers-reduced-motion: no-preference) {
  html.ps-boot:not(.ps-reduce):not(.ps-nojs).ps-js .ps-words[data-ps~="rise"]:not(.ps-in) .ps-wi { translate: 0 108%; }
  html.ps-boot:not(.ps-reduce):not(.ps-nojs).ps-js .ps-words[data-ps~="rise"] .ps-wi { transition: translate var(--ps-dur-l) var(--ps-ease-out); transition-delay: calc(var(--i, 0) * 70ms); }
}

/* ---- 13. Print: every effect target and descendant fully shown and still (Codex motion r2 #5) ---- */
@media print {
  *, *::before, *::after { animation: none !important; transition: none !important; }
  /* translate/scale and transform in SEPARATE rules: the minifier otherwise merges all three into
     "transform:none" and silently drops the translate/scale resets. */
  [data-ps], [data-ps] *, .ps-mag { opacity: 1 !important; translate: none !important; scale: none !important; }
  [data-ps~="rail"] { height: auto !important; } [data-ps~="rail"] > .ps-rail-pin { position: static !important; height: auto !important; overflow: visible !important; display: block !important; } [data-ps~="rail"] .ps-rail-track { flex-wrap: wrap !important; width: auto !important; }
  [data-ps~="stack"] > * { position: relative !important; top: auto !important; } /* unpinned, but still the anchor for positioned photos inside */ [data-ps~="clip"] { clip-path: none !important; } [data-ps~="tint"]::after { display: none !important; }
  [data-ps~="tilt"], [data-ps~="tilt"] * { transform: none !important; }
  :is([data-ps~="wipe"], [data-ps~="wipe-x"])::after, .ps-progress, .ps-marquee-dup, .ps-marquee-btn, .ps-reduce-btn { display: none !important; }
  .ps-marquee { display: block !important; }
  .ps-marquee > .ps-marquee-viewport { overflow: visible !important; }
  .ps-marquee-track { display: block !important; width: auto !important; animation: none !important; }
  /* :not(.ps-marquee-dup): the copy is also a .ps-marquee-list, and an equal-specificity !important rule here would re-show it. */
  .ps-marquee-list:not(.ps-marquee-dup) { display: flex !important; flex-wrap: wrap !important; width: 100% !important; max-width: 100% !important; white-space: normal !important; }
  .ps-marquee-list > * { white-space: normal !important; flex: 0 1 auto !important; }
}

/* ---- 14. Number counter (JS animates text; final number is the real text) ---------------- */
[data-ps~="count"] { font-variant-numeric: tabular-nums; }
.ps-count-box { display: inline-grid; font: inherit; }
.ps-count-box > * { grid-area: 1 / 1; font: inherit; color: inherit; }
.ps-count-ghost { visibility: hidden; }
