/* ============================================================
   Case Study V2
   Full-bleed, scroll-driven layout.
   Uses CSS scroll-driven animations (Chrome 115+, Safari 18+).
   Falls back gracefully for older browsers.
   ============================================================ */

:root {
    --cs-bg:          #0d0d0f;
    --cs-text-color:  #adadad;
    --cs-heading-color: #ffffff;
    --cs-image-background: var(--cs-bg);
    --cs-text-width:  600px;
    --cs-pad-x:       clamp(32px, 8vw, 120px);
    --cs-pad-y:       clamp(60px, 8vw, 120px);
    --cs-dim-opacity: 1;
    --cs-dim-color:   var(--cs-bg);
}


/* ============================================================
   Base
   ============================================================ */

article.cs {
    background: var(--cs-bg);
    color: var(--cs-text-color);
    margin-bottom: 0;
    font-family: D-DIN, sans-serif;
}

article.cs h1,
article.cs h3,
article.cs p {
    margin-top: 0;
}

article.cs h3, article.cs h1 {
    color: var(--cs-heading-color);
}

article.cs h3 {
    font-size: 42px;
    font-weight: 400;
    line-height: 1.07;
    letter-spacing: -0.03em;
    margin-bottom: 0.5em;
}

/* Single snap anchor at the very top of the article so the page loads without
   jumping. Hero and program have no snap — they scroll freely. Snapping only
   engages once you reach the first content section. */
article.cs {
    scroll-snap-align: start;
}

/* Snap targets for all content sections */
.cs-image,
.cs-image-text,
.cs-before-after,
.cs-multi-2,
.cs-multi-3,
.cs-zoom-pan,
.cs-text {
    scroll-snap-align: start;
}


/* ============================================================
   HERO
   Full bleed image, title sits at the bottom.
   ============================================================ */

.cs-hero {
    position: relative;
    height: 60svh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    padding: calc(var(--cs-pad-y) * 2) var(--cs-pad-x) var(--cs-pad-y);
    color: var(--cs-heading-color);
}

.cs-hero > img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    object-position: top center;
    z-index: 0;
}

/* Gradient dim — solid behind heading + tagline, fades to transparent above */
.cs-hero::after {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(to top, var(--cs-dim-color) 0%, var(--cs-dim-color) 47%, color-mix(in srgb, var(--cs-dim-color), transparent 40%) 78%, transparent 100%);
    z-index: 1;
}

.cs-hero > * {
    position: relative;
    z-index: 2;
}

.cs-hero h1 {
    margin: 0 0 0.15em;
}

.cs-hero .cs-tagline {
    opacity: 0.75;
    margin: 0;
}


/* ============================================================
   PROGRAM OVERVIEW — 4 columns
   ============================================================ */

.cs-program {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: clamp(24px, 3vw, 48px);
    padding: clamp(12px, 1.5vw, 20px) var(--cs-pad-x) var(--cs-pad-y);
}

.cs-program h3 {
    font-size: 24px;
    letter-spacing: 0.1em;
    font-weight: 400;
    margin-bottom: 0.75em;
    min-height: 2lh;
    display: flex;
    align-items: flex-end;
}

.cs-program p {
    font-size: 16px;
    line-height: 1.5;
    opacity: 0.75;
}

@media (max-width: 900px) {
    .cs-program {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (max-width: 500px) {
    .cs-program {
        grid-template-columns: 1fr;
    }
}


/* ============================================================
   TEXT ONLY
   ============================================================ */

.cs-text {
    padding: var(--cs-pad-y) var(--cs-pad-x);
    max-width: calc(var(--cs-text-width) + var(--cs-pad-x) * 2);
}


/* ============================================================
   IMAGE ONLY
   Full bleed, one viewport tall. Scales in on scroll entry.
   ============================================================ */

.cs-image-only {
    height: 100svh;
    overflow: hidden;
}

.cs-image-only > img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}


/* ============================================================
   IMAGE + TEXT
   Sticky full-bleed image. Image scales in as section enters.
   Text scrolls in from below over the image.
   Image dims to black as text enters.
   ============================================================ */

.cs-image-text {
    position: relative;
    view-timeline-name: --image-text-view;
    view-timeline-axis: block;
    /* scroll-snap-align: start inherited from global section rule */
}

/* Text snap target — direct child, no wrapper.
   Section = 100svh sticky + 100svh page + 150svh dwell = 350svh.
   contain range = 250svh.
   contain 0%  → image snap
   contain 40% → text snap (100/250)
   contain 100% → dwell end */
.cs-image-text-page {
    height: 100svh;
    scroll-snap-align: start;
    scroll-snap-stop: always;
    pointer-events: none;
}

/* Text: absolute at bottom of sticky wrap, slides in from below.
   overflow:hidden on .cs-image-wrap clips the translateY(100%) start.
   Section = 100svh sticky + 100svh page = 200svh, contain range = 100svh.
   Text and dim animate over the full contain range (image snap → text snap). */
.cs-image-text .cs-text-block {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    padding: var(--cs-pad-y) var(--cs-pad-x) 20px;
    max-width: calc(var(--cs-text-width) + var(--cs-pad-x) * 2);
    z-index: 2;
    view-timeline-name: none;
    animation: cs-text-slide-in linear both;
    animation-timeline: --image-text-view;
    animation-range: contain 0% contain 100%;
}

/* Gradient dim — fully override base .cs-dim (which uses --text-view).
   Same gradient style as carousel. */
.cs-image-text .cs-dim {
    background: linear-gradient(to bottom, transparent 0%, var(--cs-dim-color) 40%, var(--cs-dim-color) 100%);
    opacity: 0;
    animation: cs-dim-in linear both;
    animation-timeline: --image-text-view;
    animation-range: contain 0% contain 100%;
}

/* Sticky image container */
.cs-image-wrap {
    position: sticky;
    top: 0;
    height: 100svh;
    overflow: hidden;
    z-index: 0;
    background: var(--cs-image-background);
}

.cs-image-wrap > img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

/* Black overlay — dims in as text enters, undims as text exits.
   width: 100vw + right: auto breaks out of .cs-text-block's max-width. */
.cs-dim {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: auto;
    width: 100vw;
    background: var(--cs-dim-color);
    opacity: 0;
    pointer-events: none;
    z-index: -1;
    animation: cs-dim-in linear forwards, cs-dim-out linear forwards;
    animation-timeline: --text-view, --text-view;
    animation-range: entry 0% entry 65%, exit 0% exit 100%;
}

/* Text block — scrolls in from below over the sticky image */
.cs-text-block {
    position: relative;
    z-index: 1;
    padding: var(--cs-pad-y) var(--cs-pad-x);
    max-width: calc(var(--cs-text-width) + var(--cs-pad-x) * 2);
    /* Creates the timeline that drives .cs-dim and its own entrance */
    view-timeline-name: --text-view;
    view-timeline-axis: block;
    /* Slide up and fade in as block enters the viewport */
    animation: cs-text-in linear both;
    animation-timeline: --text-view;
    animation-range: entry 0% entry 45%;
}


/* Dwell spacer — keeps section tall enough for image to remain
   sticky through the full text exit + a beat after.
   Dwell time after text exits = spacer height - 100svh.
   At 150svh: ~50svh of bright-image dwell before image scrolls off. */
.cs-dwell {
    height: 150svh;
}


/* ============================================================
   Keyframes
   ============================================================ */

@keyframes cs-dim-in {
    to { opacity: var(--cs-dim-opacity); }
}

@keyframes cs-dim-out {
    from { opacity: var(--cs-dim-opacity); }
    to { opacity: 0; }
}

@keyframes cs-text-in {
    from {
        opacity: 0;
        transform: translateY(52px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}


/* ============================================================
   Fallback — browsers without scroll-driven animation support
   ============================================================ */

@supports not (animation-timeline: scroll()) {
    .cs-dim {
        animation: none;
        opacity: 0.5; /* Static partial dim so text stays legible */
    }

    .cs-text-block {
        animation: none;
        opacity: 1;
        transform: none;
    }
}


/* ============================================================
   Reduced motion
   ============================================================ */

@media (prefers-reduced-motion: reduce) {
    .cs-text-block {
        animation: none;
        transform: none;
        opacity: 1;
    }

    .cs-dim {
        animation: none;
        opacity: 0.5;
    }
}


/* ============================================================
   Mobile
   ============================================================ */

@media (max-width: 600px) {
    article.cs h1 {
        font-size: 64px;
        line-height: 1;
    }

    article.cs h3 {
        font-size: 28px;
        line-height: 1.1;
    }

    .cs-image-wrap > img,
    .cs-carousel-track > img {
        object-position: top center;
    }

    .cs-hero::after {
        background: linear-gradient(to top, var(--cs-dim-color) 0%, var(--cs-dim-color) 60%, transparent 100%);
    }
}


/* ============================================================
   BEAT SPACERS
   Reusable scroll-beat elements that create named view timelines,
   decoupling image transitions from text/dim arrival.
     .cs-beat      → --beat-view    (before-after, multi-2, multi-3)
     .cs-beat-2    → --beat-2-view  (multi-3 second transition)
     .cs-zoom-beat → --zoom-beat-view (zoom-pan)
   ============================================================ */

.cs-beat {
    height: 80svh;
    view-timeline-name: --beat-view;
    view-timeline-axis: block;
}

/* Snap dwell — brief exit spacer after the last image snap.
   No snap point here; the carousel-scroll end-snap (scroll-snap-align: end)
   holds the final state. This spacer just gives the section enough height
   for image + text to begin scrolling off together before the section ends. */
.cs-snap-dwell {
    height: 20svh;
}

.cs-beat-2 {
    height: 80svh;
    view-timeline-name: --beat-2-view;
    view-timeline-axis: block;
}

.cs-zoom-beat {
    height: 100svh;
    view-timeline-name: --zoom-beat-view;
    view-timeline-axis: block;
}


/* ============================================================
   BEFORE / AFTER
   Swipe completes on .cs-beat, then text + dim arrive.
   Structure:
     <section class="cs-before-after">
       <div class="cs-image-wrap">
         <img src="before.png">
         <img class="cs-after-img" src="after.png">
         <div class="cs-swipe-line" aria-hidden="true"></div>
         <div class="cs-dim"></div>
       </div>
       <div class="cs-beat" aria-hidden="true"></div>
       <div class="cs-text-block">…</div>
       <div class="cs-dwell" aria-hidden="true"></div>
     </section>
   ============================================================ */

.cs-before-after {
    position: relative;
    view-timeline-name: --before-after-view;
    view-timeline-axis: block;
}

/* Two snap pages:
   Section = 100svh sticky + 100svh page-1 + 100svh page-2 = 300svh.
   contain range = 200svh.
     contain 0%   → image snap (before)
     contain 50%  → page-1 snap → swipe complete
     contain 100% → page-2 snap → text + dim in */
.cs-before-after-page {
    height: 100svh;
    scroll-snap-align: start;
    scroll-snap-stop: always;
    pointer-events: none;
}

/* After image sits on top, clipped to reveal left → right.
   Swipe over contain 0%→50%. */
.cs-before-after .cs-after-img {
    position: absolute;
    inset: 0;
    clip-path: inset(0 100% 0 0);
    animation: cs-swipe-reveal linear forwards;
    animation-timeline: --before-after-view;
    animation-range: contain 0% contain 50%;
}

/* Divider line tracks the reveal edge */
.cs-swipe-line {
    position: absolute;
    inset-block: 0;
    left: 0;
    width: 2px;
    background: rgba(255, 255, 255, 0.7);
    pointer-events: none;
    animation: cs-swipe-line-move linear forwards;
    animation-timeline: --before-after-view;
    animation-range: contain 0% contain 50%;
}

/* Text + dim animate in contain 50%→100% (after swipe complete) */
.cs-before-after .cs-text-block {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    padding: var(--cs-pad-y) var(--cs-pad-x) 20px;
    max-width: calc(var(--cs-text-width) + var(--cs-pad-x) * 2);
    z-index: 2;
    view-timeline-name: none;
    animation: cs-text-slide-in linear both;
    animation-timeline: --before-after-view;
    animation-range: contain 50% contain 100%;
}

.cs-before-after .cs-dim {
    background: linear-gradient(to bottom, transparent 0%, var(--cs-dim-color) 40%, var(--cs-dim-color) 100%);
    opacity: 0;
    animation: cs-dim-in linear both;
    animation-timeline: --before-after-view;
    animation-range: contain 50% contain 100%;
}

@keyframes cs-swipe-reveal {
    from { clip-path: inset(0 100% 0 0); }
    to   { clip-path: inset(0 0% 0 0); }
}

@keyframes cs-swipe-line-move {
    from { left: 0; }
    to   { left: 100%; }
}


/* ============================================================
   CROSS-FADE (2 images)
   Cross-fade happens on .cs-beat. Text lives inside .cs-image-wrap
   so image and text leave the viewport together.
   Text and dim fade in after the transition completes.
   Structure:
     <section class="cs-multi-2">
       <div class="cs-image-wrap">
         <img class="cs-img-1" src="img1.png">
         <img class="cs-img-2" src="img2.png">
         <div class="cs-dim"></div>
         <div class="cs-text-block">…</div>
       </div>
       <div class="cs-beat" aria-hidden="true"></div>
       <div class="cs-snap-dwell" aria-hidden="true"></div>
     </section>
   ============================================================ */

.cs-multi-2 {
    position: relative;
    timeline-scope: --beat-view;
    view-timeline-name: --section-enter;
    view-timeline-axis: block;
}

.cs-multi-2 .cs-img-1 {
    animation: cs-fade-out linear forwards;
    animation-timeline: --beat-view;
    animation-range: entry 20% entry 80%;
}

/* Second image stacks on top, starts invisible */
.cs-multi-2 .cs-img-2 {
    position: absolute;
    inset: 0;
    opacity: 0;
    animation: cs-fade-in linear forwards;
    animation-timeline: --beat-view;
    animation-range: entry 20% entry 80%;
}

/* Text: absolute at bottom of sticky image-wrap, fades in with first image */
.cs-multi-2 .cs-text-block {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    padding: var(--cs-pad-y) var(--cs-pad-x);
    max-width: calc(var(--cs-text-width) + var(--cs-pad-x) * 2);
    view-timeline-name: none;
    z-index: 2;
    animation: cs-text-in linear both;
    animation-timeline: --section-enter;
    animation-range: entry 0% entry 8%;
}

/* Gradient dim covers only behind the text + soft fade above it */
.cs-multi-2 .cs-dim {
    background: linear-gradient(to bottom, transparent 0%, var(--cs-dim-color) 50%, var(--cs-dim-color) 100%);
    animation: cs-dim-in linear forwards;
    animation-timeline: --section-enter;
    animation-range: entry 0% entry 8%;
}

@keyframes cs-fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

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


/* ============================================================
   CROSS-FADE (3 images)
   Two beats drive two transitions. Text lives inside .cs-image-wrap
   so image and text leave the viewport together.
   Text and dim fade in after the last transition completes.
   Structure:
     <section class="cs-multi-3">
       <div class="cs-image-wrap">
         <img class="cs-img-1" src="img1.png">
         <img class="cs-img-2" src="img2.png">
         <img class="cs-img-3" src="img3.png">
         <div class="cs-dim"></div>
         <div class="cs-text-block">…</div>
       </div>
       <div class="cs-beat"   aria-hidden="true"></div>
       <div class="cs-beat-2" aria-hidden="true"></div>
       <div class="cs-snap-dwell" aria-hidden="true"></div>
     </section>
   ============================================================ */

.cs-multi-3 {
    position: relative;
    timeline-scope: --beat-view, --beat-2-view;
    view-timeline-name: --section-enter;
    view-timeline-axis: block;
}

/* Second and third images stack on top of the first */
.cs-multi-3 .cs-img-2,
.cs-multi-3 .cs-img-3 {
    position: absolute;
    inset: 0;
    opacity: 0;
}

/* img1: fades out on beat-1 */
.cs-multi-3 .cs-img-1 {
    animation: cs-fade-out linear forwards;
    animation-timeline: --beat-view;
    animation-range: entry 20% entry 80%;
}

/* img2: fades in on beat-1, fades out on beat-2 (second animation wins cascade) */
.cs-multi-3 .cs-img-2 {
    animation: cs-fade-in linear forwards, cs-fade-out linear forwards;
    animation-timeline: --beat-view, --beat-2-view;
    animation-range: entry 20% entry 80%, entry 20% entry 80%;
}

/* img3: fades in on beat-2 */
.cs-multi-3 .cs-img-3 {
    animation: cs-fade-in linear forwards;
    animation-timeline: --beat-2-view;
    animation-range: entry 20% entry 80%;
}

/* Text: absolute at bottom of sticky image-wrap, fades in with first image */
.cs-multi-3 .cs-text-block {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    padding: var(--cs-pad-y) var(--cs-pad-x);
    max-width: calc(var(--cs-text-width) + var(--cs-pad-x) * 2);
    view-timeline-name: none;
    z-index: 2;
    animation: cs-text-in linear both;
    animation-timeline: --section-enter;
    animation-range: entry 0% entry 8%;
}

/* Gradient dim covers only behind the text + soft fade above it */
.cs-multi-3 .cs-dim {
    background: linear-gradient(to bottom, transparent 0%, var(--cs-dim-color) 50%, var(--cs-dim-color) 100%);
    animation: cs-dim-in linear forwards;
    animation-timeline: --section-enter;
    animation-range: entry 0% entry 8%;
}


/* ============================================================
   CAROUSEL (2 or 3 images)
   Images slide horizontally; scroll snap pauses at each image.
   Auto-adapts to 2 or 3 children via :has().

   Snap geometry — section is both the view-timeline source and the panel-1
   snap target. Pages are direct children (no wrapper) for N-1 transitions.

   3-image section = 300svh (100svh image-wrap + 2×100svh pages).
   contain range = 200svh:
     contain 0%   → section snap → image 1 → text + dim animate in
     contain 25%  → text fully in
     contain 50%  → page 1 snap  → image 2
     contain 100% → page 2 snap  → image 3

   Text lives inside cs-image-wrap so it enters and exits with
   the sticky container. overflow:hidden on image-wrap clips
   text's translateY(100%) start position.

   Structure:
     <section class="cs-carousel">
       <div class="cs-image-wrap">
         <div class="cs-carousel-track">
           <img src="img1.png">
           <img src="img2.png">
           <img src="img3.png">  ← optional third
         </div>
         <div class="cs-dim"></div>
         <div class="cs-text-block">…</div>
       </div>
       <div class="cs-carousel-page" aria-hidden="true"></div>  ← snap: image 2
       <div class="cs-carousel-page" aria-hidden="true"></div>  ← snap: image 3 (3-img only)
     </section>
   ============================================================ */

/* Scroll snap — mandatory so every snap target is always hit.
   Applied to both html and body: Chrome scrolls on html,
   Safari scrolls on body. */
html, body {
    scroll-snap-type: y mandatory;
}

/* Section is the view-timeline source and the panel-1 snap target.
   Section = 100svh (image-wrap) + N-1 × 100svh (pages) = N×100svh.
   contain range = N×100svh − 100svh = (N−1)×100svh.

   3-image: section = 300svh, contain range = 200svh.
     contain 0%   → panel 1 (section snap)
     contain 50%  → panel 2 (page 1 snap)
     contain 100% → panel 3 (page 2 snap) */
.cs-carousel {
    position: relative;
    view-timeline-name: --carousel-view;
    view-timeline-axis: block;
    scroll-snap-align: start;
    scroll-snap-stop: always;
}

/* One page per transition (N-1 pages for N images).
   Direct children of .cs-carousel — no wrapper. */
.cs-carousel-page {
    height: 100svh;
    scroll-snap-align: start;
    scroll-snap-stop: always;
    pointer-events: none;
}

/* Track lays all images out in a horizontal strip */
.cs-carousel-track {
    display: flex;
    width: 200%;
    height: 100%;
    flex-shrink: 0;
}

.cs-carousel-track:has(> img:nth-child(3)) {
    width: 300%;
}

.cs-carousel-track > img {
    width: 50%;
    height: 100%;
    object-fit: contain;
    display: block;
    flex-shrink: 0;
}

.cs-carousel-track > img.cs-top {
    object-position: top center;
}

.cs-carousel-track:has(> img:nth-child(3)) > img {
    width: 33.333%;
}

/* Slide animation: contain range covers the scroll distance between snaps.
   3-image contain 0%/50%/100% = panel 1/2/3. Keyframes match exactly. */
.cs-carousel-track {
    animation: cs-carousel-slide-2 linear both;
    animation-timeline: --carousel-view;
    animation-range: contain 0% contain 100%;
}

.cs-carousel:has(.cs-carousel-track > img:nth-child(3)) .cs-carousel-track {
    animation-name: cs-carousel-slide-3;
}

/* Text sits at absolute bottom of the sticky image-wrap.
   Starts at translateY(100%) — clipped by image-wrap overflow:hidden.
   Slides in during panel 1 dwell (contain 0%–25%). */
.cs-carousel .cs-text-block {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    padding: var(--cs-pad-y) var(--cs-pad-x) 20px;
    max-width: calc(var(--cs-text-width) + var(--cs-pad-x) * 2);
    z-index: 2;
    animation: cs-text-slide-in linear both;
    animation-timeline: --carousel-view;
    animation-range: entry 50% contain 0%;
}

@keyframes cs-text-slide-in {
    from { transform: translateY(100%); }
    to   { transform: translateY(0); }
}

/* Gradient dim fades in as text slides up */
.cs-carousel .cs-dim {
    background: linear-gradient(to bottom, transparent 0%, color-mix(in srgb, var(--cs-dim-color), transparent 40%) 20%, var(--cs-dim-color) 40%, var(--cs-dim-color) 100%);
    animation: cs-dim-in linear forwards;
    animation-timeline: --carousel-view;
    animation-range: entry 50% contain 0%;
}

/* Variant: text and dim animate in after the first image snaps (contain 0%–25%)
   rather than during the approach. Add alongside .cs-carousel on the section. */
.cs-carousel.cs-text-on-snap .cs-text-block,
.cs-carousel.cs-text-on-snap .cs-dim {
    animation-range: contain 0% contain 25%;
}

/* Variant: text between image snaps.
   Requires 2 cs-carousel-page elements (3 total snap positions).
   Section = 100svh + 2×100svh = 300svh. contain range = 200svh.
     contain 0%   → section snap: 1st image, no text
     contain 50%  → snap 1: text fully in over 1st image
     contain 100% → snap 2: 2nd image in, text stays → both exit together */
.cs-carousel.cs-text-between .cs-text-block {
    animation: cs-text-between-snaps linear both;
    animation-timeline: --carousel-view;
    animation-range: contain 0% contain 100%;
}

.cs-carousel.cs-text-between .cs-dim {
    animation: cs-dim-between-snaps linear both;
    animation-timeline: --carousel-view;
    animation-range: contain 0% contain 100%;
}

.cs-carousel.cs-text-between .cs-carousel-track {
    animation: cs-carousel-between-2 linear both;
    animation-timeline: --carousel-view;
    animation-range: contain 0% contain 100%;
}

@keyframes cs-text-between-snaps {
    0%   { transform: translateY(100%); }
    40%  { transform: translateY(0); }
    100% { transform: translateY(0); }
}

@keyframes cs-dim-between-snaps {
    0%   { opacity: 0; }
    40%  { opacity: var(--cs-dim-opacity); }
    100% { opacity: var(--cs-dim-opacity); }
}

@keyframes cs-carousel-between-2 {
    0%   { transform: translateX(0); }
    50%  { transform: translateX(0); }
    100% { transform: translateX(-50%); }
}

@keyframes cs-carousel-slide-2 {
    from { transform: translateX(0); }
    to   { transform: translateX(-50%); }
}

@keyframes cs-carousel-slide-3 {
    0%   { transform: translateX(0); }
    50%  { transform: translateX(-33.333%); }
    100% { transform: translateX(-66.667%); }
}


/* ============================================================
   ZOOM AND PAN
   Zoom + first half of pan happen during .cs-zoom-beat.
   Text + dim arrive when pan is roughly half done.
   Structure:
     <section class="cs-zoom-pan">
       <div class="cs-image-wrap">
         <img src="image.png">
         <div class="cs-dim"></div>
       </div>
       <div class="cs-zoom-beat" aria-hidden="true"></div>
       <div class="cs-text-block">…</div>
       <div class="cs-dwell" aria-hidden="true"></div>
     </section>
   ============================================================ */

.cs-zoom-pan {
    position: relative;
    timeline-scope: --text-view, --zoom-beat-view;
}

.cs-zoom-pan .cs-image-wrap > img {
    object-fit: cover;
    transform-origin: left center;
    animation: cs-zoom-then-pan linear both;
    animation-timeline: --zoom-beat-view;
    animation-range: entry 0% entry 100%;
}

/* Phase 1 (0–40%): zoom in. Phase 2 (40–100%): pan left. */
@keyframes cs-zoom-then-pan {
    0%   { transform: scale(1)   translateX(0); }
    40%  { transform: scale(1.5) translateX(0); }
    100% { transform: scale(1.5) translateX(-33%); }
}


/* ============================================================
   Fallback — new patterns
   ============================================================ */

@supports not (animation-timeline: scroll()) {
    .cs-before-after .cs-after-img { animation: none; clip-path: none; }
    .cs-swipe-line { display: none; }

    .cs-multi-2 .cs-img-1 { animation: none; opacity: 0; }
    .cs-multi-2 .cs-img-2 { animation: none; opacity: 1; }
    .cs-multi-2 .cs-text-block { animation: none; opacity: 1; transform: none; }

    .cs-multi-3 .cs-img-1,
    .cs-multi-3 .cs-img-2 { animation: none; opacity: 0; }
    .cs-multi-3 .cs-img-3 { animation: none; opacity: 1; }
    .cs-multi-3 .cs-text-block { animation: none; opacity: 1; transform: none; }

    .cs-zoom-pan .cs-image-wrap > img { animation: none; transform: none; object-fit: contain; }

    .cs-carousel-track { animation: none; transform: translateX(-50%); }
    .cs-carousel:has(.cs-carousel-track > img:nth-child(3)) .cs-carousel-track {
        transform: translateX(-66.667%);
    }
    .cs-carousel .cs-text-block { animation: none; opacity: 1; transform: none; }
    .cs-carousel.cs-text-between .cs-carousel-track { transform: translateX(-50%); }
}


/* ============================================================
   Reduced motion — new patterns
   ============================================================ */

@media (prefers-reduced-motion: reduce) {
    .cs-before-after .cs-after-img { animation: none; clip-path: inset(0 50% 0 0); }
    .cs-swipe-line { display: none; }

    .cs-multi-2 .cs-img-1 { animation: none; opacity: 0; }
    .cs-multi-2 .cs-img-2 { animation: none; opacity: 1; }
    .cs-multi-2 .cs-text-block { animation: none; opacity: 1; transform: none; }

    .cs-multi-3 .cs-img-1,
    .cs-multi-3 .cs-img-2 { animation: none; opacity: 0; }
    .cs-multi-3 .cs-img-3 { animation: none; opacity: 1; }
    .cs-multi-3 .cs-text-block { animation: none; opacity: 1; transform: none; }

    .cs-zoom-pan .cs-image-wrap > img { animation: none; transform: none; }

    .cs-carousel-track { animation: none; transform: translateX(-50%); }
    .cs-carousel:has(.cs-carousel-track > img:nth-child(3)) .cs-carousel-track {
        transform: translateX(-66.667%);
    }
    .cs-carousel .cs-text-block { animation: none; opacity: 1; transform: none; }
    .cs-carousel.cs-text-between .cs-carousel-track { transform: translateX(-50%); }
}
