# Scroll Driven Diagonal Card Stack Animation

> Pinned team section where portrait cards glide diagonally into center with grayscale-to-color shifts and word-by-word story reveals.

**Definition:** Scroll Driven Diagonal Card Stack Animation is a scroll UI animation from collections.html. It is built with HTML, CSS, JavaScript, and GSAP, ships with a live preview, and is free to copy into personal and commercial projects.

## Facts
- Category: Scroll
- Stack: HTML, CSS, JavaScript, and GSAP
- Added: 2026-09-06
- Canonical page: https://html.bosconvertpro.com/collection/scroll-driven-diagonal-card-stack-animation
- Full-page preview: https://html.bosconvertpro.com/collection/scroll-driven-diagonal-card-stack-animation/full
- Live demo: https://html.bosconvertpro.com/demos/scroll-driven-diagonal-card-stack-animation/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/scroll-driven-diagonal-card-stack-animation/index.md
- License: https://html.bosconvertpro.com/terms

## How to use
1. Open https://html.bosconvertpro.com/collection/scroll-driven-diagonal-card-stack-animation/full to preview the animation in full screen.
2. Copy index.html, styles.css, and script.js from the code tabs on https://html.bosconvertpro.com/collection/scroll-driven-diagonal-card-stack-animation.
3. Paste the files into your project and adjust timing, easing, and content as needed.

## FAQ
### What is Scroll Driven Diagonal Card Stack Animation?

Scroll Driven Diagonal Card Stack Animation is a scroll UI animation from collections.html. Pinned team section where portrait cards glide diagonally into center with grayscale-to-color shifts and word-by-word story reveals.

### What is Scroll Driven Diagonal Card Stack Animation built with?

Scroll Driven Diagonal Card Stack Animation is built with HTML, CSS, JavaScript, and GSAP. The source for every file is shown on the animation page and can be copied straight into your project.

### How do I use Scroll Driven Diagonal Card Stack Animation in my project?

Open the animation page (https://html.bosconvertpro.com/collection/scroll-driven-diagonal-card-stack-animation), preview it live, then copy index.html, styles.css, and script.js from the code tabs. Paste the files into your project and adjust timing, easing, and content as needed. A standalone demo runs at https://html.bosconvertpro.com/demos/scroll-driven-diagonal-card-stack-animation/index.html.

### Is Scroll Driven Diagonal Card Stack Animation free to use?

Yes. The Scroll Driven Diagonal Card Stack Animation demo code may be used in personal and commercial projects. You may not resell or redistribute the collection itself as a competing template pack. Full terms: https://html.bosconvertpro.com/terms.

## Source — index.html
````html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Our Team — Scroll-Driven Diagonal Card Stack Animation</title>
  <meta name="description" content="Our Team scroll animation: a pinned 300vh section where portrait cards glide diagonally into center, the active card shifts from grayscale to color, story text reveals word by word, an odometer counter tracks progress, all on buttery Lenis smooth scroll with GSAP ScrollTrigger — plus a swipeable carousel on mobile." />
  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  <link
    href="https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;500;600&family=Instrument+Serif:ital@0;1&display=swap"
    rel="stylesheet"
  />
  <link rel="stylesheet" href="./styles.css" />
</head>
<body>
  <section id="team" class="team-section">
    <!-- 01 / Full-screen title : "Driven by Creative Curiosity" -->
    <div class="full-screen-media">
      <div class="full-screen-media__title" data-title>
        <h2 aria-label="Crafted by Curious Minds"><span data-title-line>Crafted by</span><br /><span data-title-line>Curious Minds</span></h2>
      </div>
    </div>

    <!-- 02 / DESKTOP : sticky 300vh diagonal stack (visible >= 1200px) -->
    <div class="team" id="teamDesktop" aria-label="Our team">
      <div class="team__content" id="teamContent">
        <div class="team__index" aria-hidden="true">
          <span>4</span><i></i><span>Our team</span>
        </div>

        <div class="team__cards" id="teamCards" style="--total-items:4; --progress:0;">
          <div class="team__card" style="--index:0" data-card="0">
            <div class="team__scene">
              <div class="team__obj" data-obj>
                <img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=1200&auto=format&fit=crop" alt="Team member portrait" draggable="false" />
              </div>
            </div>
          </div>
          <div class="team__card" style="--index:1" data-card="1">
            <div class="team__scene">
              <div class="team__obj" data-obj>
                <img src="https://images.unsplash.com/photo-1568602471122-7832951cc4c5?q=80&w=1200&auto=format&fit=crop" alt="Team member portrait" draggable="false" />
              </div>
            </div>
          </div>
          <div class="team__card" style="--index:2" data-card="2">
            <div class="team__scene">
              <div class="team__obj" data-obj>
                <img src="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?q=80&w=1200&auto=format&fit=crop" alt="Team member portrait" draggable="false" />
              </div>
            </div>
          </div>
          <div class="team__card" style="--index:3" data-card="3">
            <div class="team__scene">
              <div class="team__obj" data-obj>
                <img src="https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?q=80&w=1200&auto=format&fit=crop" alt="Team member portrait" draggable="false" />
              </div>
            </div>
          </div>
        </div>

        <div class="team__info" aria-live="polite">
          <div class="team__descs" id="teamDescs">
            <div class="desc-item" data-desc="0">
              <div class="desc-content" data-desc-content><p>We began as two designers with one belief — great work comes from people who genuinely care about the details.</p></div>
            </div>
            <div class="desc-item" data-desc="1">
              <div class="desc-content" data-desc-content><p>Our studio grew around that obsession. A small team with senior hands — every pixel argued over, nothing handed off.</p></div>
            </div>
            <div class="desc-item" data-desc="2">
              <div class="desc-content" data-desc-content><p>Today we partner with founders and brands worldwide — shaping identities, sites and stories built to last.</p></div>
            </div>
            <div class="desc-item" data-desc="3">
              <div class="desc-content" data-desc-content><p>The tools will keep changing. The standard won&rsquo;t — thoughtful work, made with care, delivered with pride.</p></div>
            </div>
          </div>

          <div class="team__counter" aria-hidden="true">
            <div class="counter-digit">
              <div class="counter-viewport">
                <div class="counter-strip" data-tens>
                  <span class="counter-item">0</span>
                  <span class="counter-item">1</span>
                </div>
              </div>
            </div>
            <div class="counter-digit">
              <div class="counter-viewport">
                <div class="counter-strip" data-units>
                  <span class="counter-item">0</span><span class="counter-item">1</span><span class="counter-item">2</span><span class="counter-item">3</span><span class="counter-item">4</span><span class="counter-item">5</span><span class="counter-item">6</span><span class="counter-item">7</span><span class="counter-item">8</span><span class="counter-item">9</span>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 03 / MOBILE + TABLET carousel (hidden >= 1200px) -->
    <div class="m-team" id="mTeam">
      <div class="m-team__top">
        <p class="sec-index"><span>4</span><i></i><span>Our team</span></p>
      </div>
      <div class="m-team__detail">
        <div class="m-carousel" style="--carousel-index:0" id="mCarousel">
          <div class="m-carousel__track" id="mTrack">
            <div class="m-carousel__slide"><img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=1080&auto=format&fit=crop" alt="Team member portrait" /></div>
            <div class="m-carousel__slide"><img src="https://images.unsplash.com/photo-1568602471122-7832951cc4c5?q=80&w=1080&auto=format&fit=crop" alt="Team member portrait" /></div>
            <div class="m-carousel__slide"><img src="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?q=80&w=1080&auto=format&fit=crop" alt="Team member portrait" /></div>
            <div class="m-carousel__slide"><img src="https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?q=80&w=1080&auto=format&fit=crop" alt="Team member portrait" /></div>
          </div>
        </div>
        <div class="m-para" id="mPara">
          <div class="m-para__stack">
            <div class="m-para__item is-active" data-mpara="0"><p>We began as two designers with one belief — great work comes from people who genuinely care about the details.</p></div>
            <div class="m-para__item" data-mpara="1" aria-hidden="true"><p>Our studio grew around that obsession. A small team with senior hands — every pixel argued over, nothing handed off.</p></div>
            <div class="m-para__item" data-mpara="2" aria-hidden="true"><p>Today we partner with founders and brands worldwide — shaping identities, sites and stories built to last.</p></div>
            <div class="m-para__item" data-mpara="3" aria-hidden="true"><p>The tools will keep changing. The standard won&rsquo;t — thoughtful work, made with care, delivered with pride.</p></div>
          </div>
        </div>
      </div>
      <div class="m-controls">
        <button type="button" class="m-btn" id="mPrev" aria-label="Previous member" disabled>
          <svg viewBox="0 0 12 12" width="12" height="12" fill="none"><path d="M7.5 1.5 3 6l4.5 4.5" stroke="currentColor" stroke-width="1.2"/></svg>
        </button>
        <div class="m-lines" id="mLines">
          <button type="button" class="m-line is-active" aria-label="Go to member 1" aria-current="true"></button>
          <button type="button" class="m-line" aria-label="Go to member 2"></button>
          <button type="button" class="m-line" aria-label="Go to member 3"></button>
          <button type="button" class="m-line" aria-label="Go to member 4"></button>
        </div>
        <button type="button" class="m-btn m-btn--right" id="mNext" aria-label="Next member">
          <svg viewBox="0 0 12 12" width="12" height="12" fill="none"><path d="M4.5 1.5 9 6l-4.5 4.5" stroke="currentColor" stroke-width="1.2"/></svg>
        </button>
      </div>
    </div>
  </section>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/lenis@1.1.14/dist/lenis.min.js"></script>
  <script src="./script.js"></script>
</body>
</html>
````

## Source — styles.css
````css
/* Our Team — light premium edition
   300vh sticky diagonal stack (desktop), 100vh title, mobile carousel.
   Easing = easeOutQuart. */

*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }

:root{
  --easeOutQuart: cubic-bezier(0.165, 0.84, 0.44, 1);
  --brand-yellow:#1c1917;
  --brand-dark-blue:#f6f3ee;
  --text-secondary:rgba(28,25,23,0.62);
  --text-disabled:rgba(28,25,23,0.32);
  --card-bg:#e9e2d4;
  --line-soft:rgba(28,25,23,0.14);
  --gap-x:1.6rem;
  --padding-x:1.6rem;
  --font-sans:"Inter Tight", -apple-system, "Helvetica Neue", Arial, sans-serif;
  --font-italic:"Instrument Serif", Georgia, serif;
}

html{ font-size:max(2.6666666667vw,1px); }
@media(min-width:768px){ html{ font-size:max(1.3020833333vw,1px); } }
@media(min-width:1200px){ html{ font-size:max(0.5208333333vw,1px); } }

html, body{
  -webkit-font-smoothing:antialiased;
  background:
    radial-gradient(ellipse 70% 45% at 50% 30%, rgba(255,255,255,0.9), transparent 70%),
    radial-gradient(ellipse 40% 30% at 85% 15%, rgba(212,196,172,0.30), transparent 60%),
    var(--brand-dark-blue);
  color:var(--brand-yellow);
  font-family:var(--font-sans);
  overscroll-behavior:none;
}
html::-webkit-scrollbar, body::-webkit-scrollbar{ display:none; }
body{ overflow-x:clip; }
img{ display:block; max-width:100%; }
button{ font:inherit; color:inherit; background:none; border:none; cursor:pointer; }

html.lenis, html.lenis body{ height:auto; }
.lenis.lenis-smooth{ scroll-behavior:auto !important; }
.lenis.lenis-stopped{ overflow:hidden; }

.sec-index, .team__index{
  display:flex; align-items:center; gap:1.2rem;
  font-family:var(--font-italic); font-style:italic; font-weight:500;
  font-size:1.2rem; line-height:1; letter-spacing:normal;
}
.sec-index i, .team__index i{
  display:block; width:2rem; height:.1rem; background:var(--brand-yellow);
}

/* ============ section ============ */
.team-section{ position:relative; max-width:100vw; }

/* ---- 01 title : Team_fullScreenMedia ---- */
.full-screen-media{ position:relative; width:100%; height:100svh; }
.full-screen-media__title{
  position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  width:100%; text-align:center;
  font-weight:600; line-height:1em; letter-spacing:-.03em;
  font-size:3.2rem; max-width:15.5rem;
}
@media(min-width:768px){ .full-screen-media__title{ font-size:4.8rem; max-width:21.5rem; } }
@media(min-width:1200px){ .full-screen-media__title{ display:flex; justify-content:center; font-size:8.8rem; max-width:64.5rem; } }
.full-screen-media__title h2{ font:inherit; }
.full-screen-media__title .word{ display:inline-block; will-change:transform, opacity, filter; }
.full-screen-media__title [data-title-line]{ display:inline-block; white-space:nowrap; }

/* ---- 02 desktop sticky stack : Team_team ---- */
.team{ position:relative; width:100%; height:auto; display:none; }
@media(min-width:1200px){ .team{ display:block; height:300svh; } }
.team__content{
  position:sticky; top:0; left:0; width:100vw; height:100svh;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
}
.team__index{ display:none; }

/* cards — exact original math:
   --rel-pos = index - progress * last-index
   --offset-step = 100% + stack-gap
   translateX(offset-x) translateY(offset-y) => diagonal */
.team__cards{
  --last-index:calc(var(--total-items) - 1);
  --width:50.4rem; --height:55rem; --stack-gap:3.2rem;
  width:var(--width); height:var(--height);
  display:grid; grid-template-columns:1fr; grid-template-rows:1fr;
}
.team__cards > *{ grid-column:1/-1; grid-row:1/-1; }
.team__card{
  --offset-step:calc(100% + var(--stack-gap, 3.2rem));
  --rel-pos:calc(var(--index) - var(--progress, 0) * var(--last-index));
  --offset-x:calc(var(--offset-step) * var(--rel-pos));
  --offset-y:calc(var(--offset-step) * var(--rel-pos));
  width:var(--width); height:var(--height);
  position:relative; will-change:transform;
  transform:translateX(var(--offset-x)) translateY(var(--offset-y)) translateZ(0);
}
.team__scene{ position:relative; width:100%; height:100%; overflow:clip; background:var(--card-bg); border-radius:1.2rem; box-shadow:0 2.4rem 6rem rgba(28,25,23,0.14); }
.team__obj{
  position:relative; width:100%; height:100%; will-change:transform;
  transform:translateX(calc(var(--scroll-velocity, 0) * .9px)) translateY(calc(var(--scroll-velocity, 0) * .9px)) translateZ(0) scale(1.1);
}
.team__obj img{
  width:100%; height:100%; object-fit:cover;
  filter:grayscale(100%); opacity:.3; will-change:filter, opacity;
  user-select:none; pointer-events:none;
}

/* descriptions — absolute right like original (left:150rem top:26.7rem) */
.team__info{ position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index:1; }
.team__descs{
  position:absolute; left:150rem; top:26.7rem; width:24rem; text-align:left;
  display:grid; grid-template-columns:1fr; grid-template-rows:1fr;
  font-size:1.8rem; font-weight:400; line-height:1.4em; letter-spacing:-.036rem;
}
.team__descs > *{ grid-column:1/-1; grid-row:1/-1; }
.desc-item .word{ display:inline-block; will-change:opacity; }
.desc-content{ opacity:0; will-change:opacity; }

/* giant counter bottom-left */
.team__counter{
  position:absolute; bottom:7.6rem; left:18rem;
  display:flex; font-size:24rem; font-weight:600; line-height:.9em; letter-spacing:-.72rem;
}
.counter-digit{ display:inline-block; vertical-align:top; }
.counter-viewport{ overflow:hidden; height:.9em; line-height:.9em; }
.counter-strip{ will-change:transform; }
.counter-item{ display:flex; align-items:center; justify-content:flex-start; height:.9em; line-height:.9em; }

/* ---- 03 mobile / tablet : styles_mobile ---- */
.m-team{ width:100%; padding:12rem var(--padding-x); display:flex; flex-direction:column; gap:4rem; }
@media(min-width:768px){ .m-team{ padding:14.7rem var(--padding-x) 3.2rem; gap:2.9rem; } }
@media(min-width:1200px){ .m-team{ display:none; } }
.m-team__top{ width:100%; display:flex; align-items:center; justify-content:center; }
.m-team__detail{ width:100%; display:flex; flex-direction:column; align-items:center; gap:3.2rem; }
@media(min-width:768px){ .m-team__detail{ gap:10.9rem; } }

.m-carousel{
  --carousel-index:0; --slide-gap:0.8rem;
  position:relative; width:calc(100% + var(--slide-gap)); margin-right:calc(var(--slide-gap) * -1);
  overflow:hidden;
}
@media(min-width:768px){ .m-carousel{ --slide-gap:1.2rem; } }
.m-carousel__track{
  display:flex; gap:var(--slide-gap); width:100%;
  transform:translateX(calc(var(--carousel-index) * -100%));
  transition:transform 1.4s var(--easeOutQuart); will-change:transform;
}
.m-carousel__slide{ position:relative; flex:0 0 calc(100% - var(--slide-gap)); width:calc(100% - var(--slide-gap)); aspect-ratio:343/374; overflow:hidden; background:var(--card-bg); border-radius:1.2rem; box-shadow:0 1.6rem 4rem rgba(28,25,23,0.12); }
@media(min-width:768px){ .m-carousel__slide{ aspect-ratio:522/569; } }
.m-carousel__slide img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }

.m-para{ font-size:1.4rem; line-height:140%; letter-spacing:-.028rem; padding-right:6rem; width:100%; }
@media(min-width:768px){ .m-para{ font-size:1.6rem; letter-spacing:-.032rem; padding-right:14rem; } }
.m-para__stack{ display:grid; }
.m-para__item{
  grid-area:1/1; opacity:0; visibility:hidden; pointer-events:none;
  transform:translateY(.8rem);
  transition:opacity .35s var(--easeOutQuart), transform .35s var(--easeOutQuart), visibility 0s var(--easeOutQuart) .35s;
  will-change:opacity, transform;
}
.m-para__item.is-active{
  opacity:1; visibility:visible; pointer-events:auto; transform:translateY(0);
  transition:opacity .35s var(--easeOutQuart), transform .35s var(--easeOutQuart), visibility 0s var(--easeOutQuart) 0s;
}

.m-controls{ width:100%; display:flex; align-items:center; justify-content:space-between; }
.m-btn{
  width:4.8rem; height:4.8rem; border:1px solid var(--brand-yellow);
  display:flex; align-items:center; justify-content:center;
  transition:opacity .2s var(--easeOutQuart); will-change:opacity;
}
.m-btn:disabled{ opacity:.3; cursor:default; }
.m-lines{ display:flex; align-items:center; justify-content:center; gap:.4rem; }
.m-line{ width:1.2rem; height:1px; background:rgba(28,25,23,0.25); transition:width .2s var(--easeOutQuart), background-color .2s var(--easeOutQuart); }
.m-line.is-active{ width:2rem; background:var(--brand-yellow); }

@media (prefers-reduced-motion: reduce){
  .m-carousel__track, .m-para__item, .team__card, .team__obj{ transition:none !important; }
}
````

## Source — script.js
````javascript
/* Our Team — light premium edition (html/css/js + gsap + lenis)
   Desktop: 100vh title + 300vh sticky diagonal card stack, scrubbed --progress,
   grayscale active card, word-stagger descriptions, odometer counter, velocity parallax.
   Mobile: carousel + crossfade paragraphs + square controls + progress lines. */
(() => {
  "use strict";
  if (typeof gsap === "undefined") return;
  gsap.registerPlugin(ScrollTrigger);
  gsap.config({ nullTargetWarn: false });
  gsap.ticker.lagSmoothing(0);

  /* ---------- Lenis smooth scroll (like fromanother.love) ---------- */
  let lenis = null;
  if (typeof Lenis !== "undefined" && !reduceMotionSafe()) {
    lenis = new Lenis({
      duration: 1.15,
      easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
      smoothWheel: true,
      wheelMultiplier: 1,
      touchMultiplier: 1.5
    });
    lenis.on("scroll", ScrollTrigger.update);
    gsap.ticker.add((time) => lenis.raf(time * 1000));
    gsap.ticker.lagSmoothing(0);
  }
  function reduceMotionSafe() {
    return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  }

  const EASE = "power3.out";
  const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  const isDesktopMQ = window.matchMedia("(min-width: 1200px)");

  /* ---------- word splitter (mirrors SplitText words/lines behavior) ---------- */
  function splitWords(el) {
    const words = [];
    const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
    const nodes = [];
    while (walker.nextNode()) nodes.push(walker.currentNode);
    nodes.forEach((node) => {
      const frag = document.createDocumentFragment();
      node.textContent.split(/(\s+)/).forEach((part) => {
        if (!part) return;
        if (/^\s+$/.test(part)) { frag.appendChild(document.createTextNode(" ")); return; }
        const s = document.createElement("span");
        s.className = "word";
        s.textContent = part;
        frag.appendChild(s);
        words.push(s);
      });
      node.parentNode.replaceChild(frag, node);
    });
    return words;
  }

  /* ---------- 01 title intro : blur words in (like Words WORDS_BLUR) ---------- */
  const titleEl = document.querySelector("[data-title] h2");
  let titleWords = [];
  if (titleEl) {
    titleWords = splitWords(titleEl);
    gsap.set(titleWords, { opacity: 0, filter: "blur(10px)" });
    gsap.to(titleWords, {
      opacity: 1, filter: "blur(0px)", duration: 0.5, ease: EASE, stagger: 0.02,
      scrollTrigger: { trigger: ".full-screen-media", start: "top 75%", once: true }
    });
    // Title drifts/fades as the sticky stack takes over (matches global crossfade)
    if (!reduceMotion) {
      gsap.to(".full-screen-media__title", {
        yPercent: -30, opacity: 0, ease: "none",
        scrollTrigger: { trigger: "#teamDesktop", start: "top bottom", end: "top 25%", scrub: true }
      });
    }
  }

  /* ---------- 02 desktop sticky stack ---------- */
  const team = document.getElementById("teamDesktop");
  const cardsWrap = document.getElementById("teamCards");
  const cards = gsap.utils.toArray(".team__card");
  const descs = gsap.utils.toArray(".desc-item");
  const descContents = gsap.utils.toArray("[data-desc-content]");
  const descWords = descContents.map((c) => splitWords(c.querySelector("p") || c));

  const TOTAL = 4;              // members
  const OFFSET = 0;             // first card starts centered (visible at scroll start)
  const PROCESSED = TOTAL + OFFSET; // 4 (processedTotalItems)
  const LAST = PROCESSED - 1;   // 3

  let current = -99;            // currentMemberIndex (can be negative pre-entry)
  let prev = -99;
  let hideTimers = [];

  // initial states
  descWords.forEach((w) => gsap.set(w, { opacity: 0 }));
  gsap.set(descContents, { opacity: 0 });

  function setCounter(n) {
    const clamped = Math.max(0, Math.min(TOTAL - 1, n));
    const display = clamped + 1;                 // 1..4
    const tens = Math.floor(display / 10);       // 0
    const units = display % 10;                  // 1..4
    const tensEl = document.querySelector("[data-tens]");
    const unitsEl = document.querySelector("[data-units]");
    if (!tensEl || !unitsEl) return;
    if (reduceMotion) {
      gsap.set(tensEl, { y: `-${tens * 100}%` });
      gsap.set(unitsEl, { y: `-${units * 10}%` });
      return;
    }
    gsap.to(tensEl, { y: `-${tens * 100}%`, duration: 0.4, ease: "power2.out", overwrite: true });
    gsap.to(unitsEl, { y: `-${units * 10}%`, duration: 0.4, ease: "power2.out", overwrite: true });
  }

  function activateCard(i, animate = true) {
    cards.forEach((card, k) => {
      const img = card.querySelector("img");
      const on = i === k;
      if (!img) return;
      gsap.killTweensOf(img);
      if (!animate || reduceMotion) {
        gsap.set(img, { filter: on ? "grayscale(0%)" : "grayscale(100%)", opacity: on ? 1 : 0.3 });
        return;
      }
      gsap.to(img, {
        filter: on ? "grayscale(0%)" : "grayscale(100%)",
        opacity: on ? 1 : 0.3,
        duration: 1.4, ease: EASE, overwrite: true
      });
    });
  }

  function activateDesc(i) {
    hideTimers.forEach(clearTimeout);
    hideTimers = [];
    descs.forEach((d, k) => {
      const content = d.querySelector("[data-desc-content]");
      const words = descWords[k] || [];
      if (!content) return;
      gsap.killTweensOf(content);
      gsap.killTweensOf(words);
      if (i === k) {
        // reveal: container visible, words snap in fast with tight stagger
        hideTimers.push(setTimeout(() => {
          gsap.set(content, { opacity: 1 });
          gsap.fromTo(words,
            { opacity: 0 },
            { opacity: 1, duration: 0.45, ease: EASE, stagger: 0.015, delay: 0.02 * k + 0.05, overwrite: true });
        }, 60));
      } else if (prev === k) {
        gsap.to(content, { opacity: 0, duration: 0.3, ease: EASE, overwrite: true });
      } else {
        gsap.set(words, { opacity: 0 });
        gsap.set(content, { opacity: 0 });
      }
    });
  }

  function setActive(raw) {
    if (raw === current) return;
    prev = current;
    current = raw;
    const clampedView = raw >= 0 && raw < TOTAL ? raw : -1;
    activateCard(clampedView, true);
    if (clampedView >= 0) { activateDesc(clampedView); setCounter(clampedView); }
    else {
      // pre-entry: all dim, hide descs
      descContents.forEach((c, k) => {
        const w = descWords[k] || [];
        gsap.killTweensOf(c); gsap.killTweensOf(w);
        gsap.set(w, { opacity: 0 }); gsap.set(c, { opacity: 0 });
      });
    }
  }

  // z-order so centered card sits on top (original stacks by DOM; this keeps motion clean)
  function paintDepth(progress) {
    cards.forEach((card) => {
      const idx = parseFloat(getComputedStyle(card).getPropertyValue("--index")) || 0;
      const rel = idx - progress * LAST;
      card.style.zIndex = String(100 - Math.round(Math.abs(rel) * 10));
    });
  }

  // scroll velocity parallax on inner .obj (translate .9px * velocity, scale 1.1)
  let targetVel = 0, smoothVel = 0;
  gsap.ticker.add(() => {
    smoothVel += (targetVel - smoothVel) * 0.12;
    targetVel *= 0.9;
    if (Math.abs(smoothVel) < 0.01 && Math.abs(targetVel) < 0.01) {
      if (smoothVel !== 0 && cardsWrap) { smoothVel = 0; cardsWrap.style.setProperty("--scroll-velocity", "0"); }
      return;
    }
    if (cardsWrap) cardsWrap.style.setProperty("--scroll-velocity", smoothVel.toFixed(2).toString());
  });

  if (team && cardsWrap) {
    ScrollTrigger.create({
      trigger: team,
      start: "top top",
      end: "bottom bottom",
      scrub: 1.2,
      invalidateOnRefresh: true,
      onUpdate(self) {
        const p = self.progress;
        cardsWrap.style.setProperty("--progress", p.toFixed(4).toString());
        paintDepth(p);
        targetVel = gsap.utils.clamp(-30, 30, self.getVelocity() / -250);
        setActive(Math.round(p * LAST) - OFFSET);
      }
    });
    // init paint
    paintDepth(0);
    setActive(Math.round(0) - OFFSET);
    setCounter(0);
    // first card pre-lights once it approaches center (no forced active at load)
    ScrollTrigger.refresh();
  }

  /* ---------- 03 mobile / tablet carousel (exact original timing) ---------- */
  const mTrack = document.getElementById("mTrack");
  const mCarousel = document.getElementById("mCarousel");
  const mPrev = document.getElementById("mPrev");
  const mNext = document.getElementById("mNext");
  const mLines = Array.from(document.querySelectorAll("#mLines .m-line"));
  const mParas = Array.from(document.querySelectorAll("[data-mpara]"));
  let mIndex = 0;
  const mMax = 3;

  function paintMobile() {
    if (mCarousel) mCarousel.style.setProperty("--carousel-index", String(mIndex));
    mParas.forEach((p, k) => {
      const on = k === mIndex;
      p.classList.toggle("is-active", on);
      if (on) p.removeAttribute("aria-hidden"); else p.setAttribute("aria-hidden", "true");
    });
    mLines.forEach((l, k) => {
      const on = k === mIndex;
      l.classList.toggle("is-active", on);
      if (on) l.setAttribute("aria-current", "true"); else l.removeAttribute("aria-current");
    });
    if (mPrev) mPrev.disabled = mIndex <= 0;
    if (mNext) mNext.disabled = mIndex >= mMax;
  }
  function mGo(i) { mIndex = Math.max(0, Math.min(mMax, i)); paintMobile(); }
  if (mPrev) mPrev.addEventListener("click", () => mGo(mIndex - 1));
  if (mNext) mNext.addEventListener("click", () => mGo(mIndex + 1));
  mLines.forEach((l, k) => l.addEventListener("click", () => mGo(k)));
  // swipe
  let sx = null;
  if (mTrack) {
    mTrack.addEventListener("touchstart", (e) => { sx = e.touches[0].clientX; }, { passive: true });
    mTrack.addEventListener("touchend", (e) => {
      if (sx == null) return;
      const dx = e.changedTouches[0].clientX - sx;
      sx = null;
      if (dx > 50) mGo(mIndex - 1); else if (dx < -50) mGo(mIndex + 1);
    }, { passive: true });
  }
  document.addEventListener("keydown", (e) => {
    if (isDesktopMQ.matches) return;
    if (e.key === "ArrowLeft") mGo(mIndex - 1);
    if (e.key === "ArrowRight") mGo(mIndex + 1);
  });
  paintMobile();

  // refresh triggers after images settle + after Lenis takes over scroll
  window.addEventListener("load", () => {
    if (lenis) lenis.resize();
    ScrollTrigger.refresh();
  });
})();
````
