Instruction

Webflow Template User Guide
GSAP Setup & Main Functions
All scripts go in Webflow Page Settings → Before </body> tag (or in Site Settings → Custom Code → Footer Code if global).

Webflow Site Settings → GSAP must have ScrollTriggrt enabled. Inertia optional (script has fallback).
1. MAIN CURSOR
Custom mouse-follower cursor with smooth lerp trailing. Requires .cursor-core (the moving dot) in the DOM; .cursor-pointer is optional. Call window._cursorPause() / window._cursorResume() from other scripts to hide/show the pointer (e.g. on link hover).
<script>
window.Webflow = window.Webflow || [];
window.Webflow.push(function () {

  // ==================================================
  // MAIN CURSOR
  // ==================================================

  if (window._cursorCoreInit) return;
  window._cursorCoreInit = true;

  var cursor = document.querySelector(".cursor-core");
  var pointer = document.querySelector(".cursor-pointer");

  if (!cursor) return;

  var mouseX = -100;
  var mouseY = -100;

  var x = -100;
  var y = -100;

  var paused = false;

  // initial
  gsap.set(cursor, {
    xPercent: -50,
    yPercent: -50,
    x: x,
    y: y
  });

  // smooth follow
  gsap.ticker.add(function () {

    x += (mouseX - x) * 0.15;
    y += (mouseY - y) * 0.15;

    gsap.set(cursor, {
      x: x,
      y: y
    });

  });

  // track mouse
  window.addEventListener("mousemove", function (e) {

    mouseX = e.clientX;
    mouseY = e.clientY;

  });

  // =========================================
  // HIDE POINTER ONLY
  // =========================================

  window._cursorPause = function () {

    paused = true;

    if (!pointer) return;

    gsap.to(pointer, {
      opacity: 0,
      duration: 0.2,
      overwrite: true
    });

  };

  // =========================================
  // SHOW POINTER AGAIN
  // =========================================

  window._cursorResume = function () {

    paused = false;

    if (!pointer) return;

    gsap.to(pointer, {
      opacity: 1,
      duration: 0.2,
      overwrite: true
    });

  };

});
</script>
2. COUNTER ANIMATION
Animates numbers counting up when scrolled into view. Needs .counter-item wrappers each containing a .counter-number element with the target number as its text. Triggers via ScrollTrigger unless already in viewport on load.
<script>
window.Webflow = window.Webflow || [];
window.Webflow.push(function () {

  // ==================================================
  // COUNTER ANIMATION
  // ==================================================

  if (window._counterInit) return;
  window._counterInit = true;

  if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") {
    console.error("[Counter] GSAP or ScrollTrigger not found.");
    return;
  }

  gsap.registerPlugin(ScrollTrigger);

  const counters = document.querySelectorAll(".counter-item");

  if (!counters.length) {
    console.warn("[Counter] No .counter-item elements found.");
    return;
  }

  function isInViewport(el) {
    const rect = el.getBoundingClientRect();
    return rect.top < window.innerHeight && rect.bottom > 0;
  }

  function runCounter(numberEl, targetNumber, index) {
    const obj = { val: 0 };
    gsap.to(obj, {
      val: targetNumber,
      duration: 4,
      delay: index * 0.15,
      ease: "power2.out",
      onUpdate: function () {
        numberEl.textContent = Math.round(obj.val);
      },
      onComplete: function () {
        numberEl.textContent = targetNumber;
      }
    });
  }

  // Read all target values BEFORE modifying anything in the DOM
  const items = [];

  counters.forEach(function (wrap, index) {
    const numberEl = wrap.querySelector(".counter-number");

    if (!numberEl) {
      console.warn("[Counter] .counter-number not found at index:", index);
      return;
    }

    const targetNumber = parseInt(numberEl.textContent.trim().replace(/\D/g, ""), 10);

    if (isNaN(targetNumber) || targetNumber === 0) {
      console.warn("[Counter] Invalid value at index:", index, "| value:", numberEl.textContent);
      return;
    }

    // Store reference and target, then reset display to 0
    items.push({ wrap, numberEl, targetNumber, index });
    numberEl.textContent = "0";
  });

  // Set up animations only after all targets are stored
  items.forEach(function ({ wrap, numberEl, targetNumber, index }) {
    if (isInViewport(wrap)) {
      setTimeout(function () {
        runCounter(numberEl, targetNumber, index);
      }, 100);
    } else {
      ScrollTrigger.create({
        trigger: wrap,
        start: "top 85%",
        once: true,
        onEnter: function () {
          runCounter(numberEl, targetNumber, index);
        }
      });
    }
  });

});
</script>
3. LENIS SMOOTH SCROLL (DESKTOP)
Loads Lenis from a CDN and enables smooth scrolling, desktop only (min-width: 992px). No markup requirements. Exposes window.lenis for other scripts to control scroll (e.g. window.lenis.scrollTo(...)).
<script>
window.Webflow = window.Webflow || [];
window.Webflow.push(function () {

  // ==================================================
  // LENIS SMOOTH SCROLL (DESKTOP)
  // ==================================================

  // ── Guard against double-init ──────────────────────────
  if (window._lenisScrollInit) return;
  window._lenisScrollInit = true;

  // desktop only
  const isDesktop = window.matchMedia("(min-width: 992px)").matches;
  if (!isDesktop) return;

  const lenisScript = document.createElement("script");
  lenisScript.src = "https://unpkg.com/lenis@1.3.21/dist/lenis.min.js";

  lenisScript.onload = function () {

    if (typeof Lenis === "undefined") {
      console.error("[LenisScroll] Lenis failed to load.");
      return;
    }

    const lenis = new Lenis({
      lerp: 0.07,          // the smaller, the smoother
      smoothWheel: true,
      wheelMultiplier: 0.8,
      autoRaf: false,
      // Force document-level scroll measurement. Without this, an
      // overflow:hidden element earlier in the DOM (e.g. a sticky
      // section wrapper) can cause Lenis to under-measure total
      // scrollHeight, cutting off content at the end of the page
      // (footer) even though native scroll renders it fine.
      wrapper: window,
      content: document.documentElement
    });

    function raf(time) {
      lenis.raf(time);
      requestAnimationFrame(raf);
    }

    requestAnimationFrame(raf);

    // so it can be called from other scripts
    window.lenis = lenis;

    // ── Fix: keep scroll limit in sync with actual document height ──
    // ResizeObserver on body catches any layout change that affects
    // total scrollHeight, including the case where an overflow:hidden
    // sticky wrapper earlier in the DOM causes Lenis to under-measure
    // the page on first init. This covers font swaps, late layout
    // shifts, and the sticky-wrap case together, instead of relying
    // on a fixed timeout guess.
    if (typeof ResizeObserver !== "undefined") {
      const resizeObserver = new ResizeObserver(function () {
        lenis.resize();
      });
      resizeObserver.observe(document.body);
    } else {
      // Fallback for browsers without ResizeObserver support
      setTimeout(function () {
        lenis.resize();
      }, 500);
    }

    // ── Fix: keep scroll limit correct on viewport resize ──
    window.addEventListener("resize", function () {
      lenis.resize();
    });

  };

  document.body.appendChild(lenisScript);

});
</script>
4. PARTNER LOGO MARQUEE
Infinite auto-scrolling logo strip. Requires a .partner-logo-wrap container with logo <img> children; the script builds a duplicated track internally for seamless looping. Pauses on hover.
<script>
window.Webflow = window.Webflow || [];
window.Webflow.push(function () {

  // ==================================================
  // PARTNER LOGO MARQUEE
  // ==================================================

  // ── Guard against double-init ──────────────────────────
  if (window._partnerMarqueeInit) return;

  // ── DOM check ──────────────────────────────────────────
  // .partner-logo-wrap is the VIEWPORT: it already has overflow:hidden in
  // its own CSS. It must stay static — it is the clipping window, not the
  // element that moves. A separate inner track div is created below to
  // hold the logos and be the thing GSAP translates.
  const viewport = document.querySelector(".partner-logo-wrap");
  if (!viewport) {
    console.warn("[PartnerMarquee] .partner-logo-wrap not found.");
    return;
  }

  const TRACK_CLASS = "partner-marquee-track";

  function waitForImages() {
    const imgs = Array.from(viewport.querySelectorAll("img"));
    if (imgs.length === 0) return Promise.resolve();

    return Promise.all(
      imgs.map((img) => {
        // Flipping loading to "eager" before it has started fetching
        // tells the browser to fetch it immediately instead of waiting
        // for the image to scroll near the viewport.
        if (img.loading === "lazy") img.loading = "eager";

        if (img.complete) return Promise.resolve();

        return new Promise((resolve) => {
          img.addEventListener("load", resolve, { once: true });
          img.addEventListener("error", resolve, { once: true });
        });
      })
    );
  }

  // ── Build (or rebuild) the seamless loop ───────────────
  function start() {
    if (window._partnerMarqueeInit) return;

    if (typeof gsap === "undefined") {
      console.error("[PartnerMarquee] GSAP not found.");
      return;
    }

    let track = viewport.querySelector(":scope > ." + TRACK_CLASS);

    if (!track) {
      // First run: build the track from the original .logo-wrap children.
      const originalChildren = Array.from(viewport.children);

      if (originalChildren.length === 0) {
        console.warn("[PartnerMarquee] .partner-logo-wrap has no logo children.");
        return;
      }

      // Read the gap Webflow already applies to .partner-logo-wrap so the
      // track reproduces the same spacing between logos.
      const computedGap = getComputedStyle(viewport).columnGap;

      // Lock each logo's natural rendered width BEFORE moving it into a new
      // flex context, so flex-basis recalculation can't shrink it. Safe to
      // do here because waitForImages() guarantees every logo has finished
      // loading by the time start() runs.
      originalChildren.forEach((child) => {
        const naturalWidth = child.getBoundingClientRect().width;
        child.style.flex = "0 0 auto";
        child.style.width = naturalWidth + "px";
      });

      // Build the track div that will actually be translated.
      track = document.createElement("div");
      track.className = TRACK_CLASS;
      track.style.display = "flex";
      track.style.flexWrap = "nowrap";
      track.style.alignItems = "center";
      track.style.columnGap = computedGap;
      track.style.width = "max-content";

      // Move the original logos into the track (appendChild moves existing
      // nodes, it does not clone them).
      originalChildren.forEach((child) => track.appendChild(child));

      // Clone the set once more inside the track so the loop has no seam.
      originalChildren.forEach((child) => {
        const clone = child.cloneNode(true);
        clone.setAttribute("aria-hidden", "true");
        track.appendChild(clone);
      });

      viewport.appendChild(track);
    }

    let marqueeTween;

    function buildMarquee() {
      if (marqueeTween) {
        marqueeTween.kill();
        gsap.set(track, { x: 0 });
      }

      // Half of the track's total width = width of one full logo set.
      const distance = track.scrollWidth / 2;

      marqueeTween = gsap.to(track, {
        x: -distance,
        duration: distance / 50, // adjust divisor to tune speed (px per second)
        ease: "none",
        repeat: -1,
      });
    }

    buildMarquee();

    // Setup succeeded — now safe to mark as initialized.
    window._partnerMarqueeInit = true;

    // ── Hover pause / resume ───────────────────────────────
    // Listeners on the viewport (the visible, static, hoverable box).
    viewport.addEventListener("mouseenter", () => {
      if (marqueeTween) marqueeTween.pause();
    });

    viewport.addEventListener("mouseleave", () => {
      if (marqueeTween) marqueeTween.resume();
    });

    // ── Rebuild on resize (debounced) so distance stays accurate ──
    let resizeTimeout;
    window.addEventListener("resize", () => {
      clearTimeout(resizeTimeout);
      resizeTimeout = setTimeout(buildMarquee, 250);
    });
  }

  // ── Only build the marquee once every logo image has actually loaded ──
  // (resolves near-instantly on a warm cache/refresh, waits properly on a
  // cold cache regardless of browser-specific lazy-load timing).
  waitForImages().then(start);

});
</script>
5. CARD SWIPE (MOBILE TOUCH SLIDER)
Touch-swipeable card carousel, active only between 360–479px width. Requires .process-list-wrap containing .single-process-wrap cards, and optionally .swipe-indicator-wrap .swipe-indicator-line (sibling element) as a progress bar.
<script>
window.Webflow = window.Webflow || [];
window.Webflow.push(function () {

  // ==================================================
  // CARD SWIPE (MOBILE TOUCH SLIDER)
  // ==================================================

  // ── Guard against double-init ──────────────────────────
  if (window._cardSwipeInit) return;
  window._cardSwipeInit = true;

  // ── GSAP availability check ────────────────────────────
  if (typeof gsap === "undefined") {
    console.error("[CardSwipe] GSAP not found.");
    return;
  }

  // ── Config ──────────────────────────────────────────────
  const MOBILE_MIN_WIDTH = 360;
  const MOBILE_MAX_WIDTH = 479;   // matches the CSS max-width:479px breakpoint used on
                                  // .process-list-wrap / .single-process-wrap / .swipe-indicator-wrap
  const SWIPE_DISTANCE_RATIO = 0.2;   // % of card width needed to trigger a step
  const SWIPE_VELOCITY_THRESHOLD = 0.5; // px/ms, for fast flicks below the distance ratio
  const DIRECTION_LOCK_THRESHOLD = 8; // px, before we decide horizontal vs vertical intent

  // ── DOM check ───────────────────────────────────────────
  // .process-list-wrap has overflow:clip at mobile (by design), so it acts as
  // the fixed "viewport". The individual repeated cards (.single-process-wrap)
  // are its direct children before this script runs.
  const viewport = document.querySelector('.process-list-wrap');
  const cards = viewport
    ? Array.from(viewport.children).filter(function (el) {
        return el.classList.contains('single-process-wrap');
      })
    : [];

  if (!viewport || cards.length === 0) {
    console.warn('[CardSwipe] .process-list-wrap or .single-process-wrap cards not found.');
    return;
  }

  // .swipe-indicator-wrap lives OUTSIDE .process-list-wrap (a sibling of it,
  // under whatever their shared parent is), so it is never moved into the
  // draggable track and stays fixed while cards are swiped. Looked up via
  // viewport.parentElement instead of a hardcoded parent class name, so this
  // keeps working even if the section wrapper's class changes later. The
  // animated fill element is .swipe-indicator-line, nested inside
  // .swipe-indicator-line-wrap.
  const activeIndicator = viewport.parentElement
    ? viewport.parentElement.querySelector('.swipe-indicator-wrap .swipe-indicator-line')
    : null;

  if (!activeIndicator) {
    console.warn('[CardSwipe] .swipe-indicator-line not found outside .process-list-wrap scope — swipe will still work, indicator will not update.');
  }

  const totalCards = cards.length;

  // ── State ───────────────────────────────────────────────
  let track = null;
  let cardWidth = 0;
  let gapPx = 0;
  let currentIndex = 0;
  let baseX = 0;
  let isSwipeActive = false;
  let isDragging = false;
  let directionLock = null; // 'x' | 'y' | null
  let startX = 0;
  let startY = 0;
  let startTime = 0;

  // ── Helpers ─────────────────────────────────────────────
  function checkMobileRange() {
    const w = window.innerWidth;
    return w >= MOBILE_MIN_WIDTH && w <= MOBILE_MAX_WIDTH;
  }

  function buildTrack() {
    track = document.createElement('div');
    track.style.display = 'flex';
    track.style.flexFlow = 'row';
    track.style.willChange = 'transform';

    viewport.style.position = viewport.style.position || 'relative';
    viewport.style.overflow = 'hidden';

    cards.forEach(function (card) {
      track.appendChild(card);
    });

    viewport.appendChild(track);
  }

  function teardownTrack() {
    if (!track) return;

    cards.forEach(function (card) {
      card.style.width = '';
      card.style.flex = '';
      viewport.appendChild(card);
    });

    track.remove();
    track = null;
  }

  function measure() {
    // Measure against the viewport, not the track, since the track's
    // own width is derived from the cards and would create a circular reference.
    cardWidth = viewport.getBoundingClientRect().width;
    gapPx = parseFloat(window.getComputedStyle(viewport).columnGap) || 0;

    cards.forEach(function (card) {
      card.style.width = cardWidth + 'px';
      card.style.flex = '0 0 auto';
    });

    track.style.columnGap = gapPx + 'px';
  }

  function updateIndicator(index) {
    if (!activeIndicator) return;
    const percent = ((index + 1) / totalCards) * 100;
    gsap.to(activeIndicator, { width: percent + '%', duration: 0.3, ease: 'power2.out' });
  }

  function goToIndex(index, animate) {
    currentIndex = Math.max(0, Math.min(totalCards - 1, index));
    const targetX = -currentIndex * (cardWidth + gapPx);

    if (animate === false) {
      gsap.set(track, { x: targetX });
    } else {
      gsap.to(track, { x: targetX, duration: 0.4, ease: 'power3.out' });
    }

    baseX = targetX;
    updateIndicator(currentIndex);
  }

  // ── Touch handlers ──────────────────────────────────────
  function onTouchStart(e) {
    isDragging = true;
    directionLock = null;
    startX = e.touches[0].clientX;
    startY = e.touches[0].clientY;
    startTime = Date.now();
    gsap.killTweensOf(track);
  }

  function onTouchMove(e) {
    if (!isDragging) return;

    const currentX = e.touches[0].clientX;
    const currentY = e.touches[0].clientY;
    const deltaX = currentX - startX;
    const deltaY = currentY - startY;

    if (directionLock === null) {
      if (Math.abs(deltaX) > DIRECTION_LOCK_THRESHOLD || Math.abs(deltaY) > DIRECTION_LOCK_THRESHOLD) {
        directionLock = Math.abs(deltaX) > Math.abs(deltaY) ? 'x' : 'y';
      }
    }

    if (directionLock === 'y') {
      // Vertical intent: let the page scroll natively, abort the drag.
      isDragging = false;
      return;
    }

    if (directionLock === 'x') {
      // Horizontal intent: take over the gesture, block page scroll.
      e.preventDefault();
      gsap.set(track, { x: baseX + deltaX });
    }
  }

  function onTouchEnd(e) {
    if (!isDragging || directionLock !== 'x') {
      isDragging = false;
      directionLock = null;
      return;
    }

    isDragging = false;

    const endX = (e.changedTouches && e.changedTouches[0].clientX) || startX;
    const deltaX = endX - startX;
    const elapsed = Math.max(Date.now() - startTime, 1);
    const velocity = Math.abs(deltaX) / elapsed;

    const passedDistance = Math.abs(deltaX) > cardWidth * SWIPE_DISTANCE_RATIO;
    const passedVelocity = velocity > SWIPE_VELOCITY_THRESHOLD;

    let nextIndex = currentIndex;
    if (passedDistance || passedVelocity) {
      nextIndex = deltaX < 0 ? currentIndex + 1 : currentIndex - 1;
    }

    goToIndex(nextIndex);
    directionLock = null;
  }

  // ── Enable / disable swipe mode ─────────────────────────
  function enableSwipe() {
    if (isSwipeActive) return;
    isSwipeActive = true;

    buildTrack();
    measure();
    goToIndex(0, false);

    track.addEventListener('touchstart', onTouchStart, { passive: true });
    track.addEventListener('touchmove', onTouchMove, { passive: false });
    track.addEventListener('touchend', onTouchEnd);
    track.addEventListener('touchcancel', onTouchEnd);
  }

  function disableSwipe() {
    if (!isSwipeActive) return;
    isSwipeActive = false;

    if (track) {
      track.removeEventListener('touchstart', onTouchStart);
      track.removeEventListener('touchmove', onTouchMove);
      track.removeEventListener('touchend', onTouchEnd);
      track.removeEventListener('touchcancel', onTouchEnd);
    }

    teardownTrack();
    currentIndex = 0;
  }

  function handleResize() {
    const nowMobile = checkMobileRange();

    if (nowMobile && !isSwipeActive) {
      enableSwipe();
    } else if (!nowMobile && isSwipeActive) {
      disableSwipe();
    } else if (nowMobile && isSwipeActive) {
      // Still in range, but dimensions may have changed (e.g. rotation).
      measure();
      goToIndex(currentIndex, false);
    }
  }

  // ── Init ────────────────────────────────────────────────
  if (checkMobileRange()) {
    enableSwipe();
  }

  window.addEventListener('resize', handleResize);

});
</script>
6. CARD SLIDER (MOBILE)
Button-driven (not touch) card slider, active only ≤479px. Runs two independent instances: .team-member-list / .team-card-wrap, and .testimonial-card / .card-testimonial. Both share the same .all-arrow-team prev/next buttons.
<script>
window.Webflow = window.Webflow || [];
window.Webflow.push(function () {

  // ==================================================
  // CARD SLIDER (MOBILE)
  // ==================================================

  // Guard: prevent double-init
  if (window._cardSliderInit) return;
  window._cardSliderInit = true;

  // Guard: only active on mobile (max 479px)
  if (!window.matchMedia("(max-width: 479px)").matches) return;

  // GSAP availability check
  if (typeof gsap === "undefined") {
    console.error("[CardSlider] GSAP not found. Make sure GSAP is loaded before this script.");
    return;
  }

  // Reusable slider factory: same arrow classes, different wrap/card targets
  function initCardSlider(config) {
    var label = config.label;
    var wrapSelector = config.wrapSelector;
    var cardSelector = config.cardSelector;

    // DOM check
    var wrap = document.querySelector(wrapSelector);
    if (!wrap) {
      console.warn("[" + label + "] " + wrapSelector + " not found.");
      return;
    }

    var cards = Array.from(wrap.querySelectorAll(cardSelector));
    if (cards.length === 0) {
      console.warn("[" + label + "] No " + cardSelector + " found inside " + wrapSelector + ".");
      return;
    }

    var btnPrev = document.querySelector(".all-arrow-team .arrow-team-wrap:first-child");
    var btnNext = document.querySelector(".all-arrow-team .arrow-team-wrap:last-child");

    if (!btnPrev || !btnNext) {
      console.warn("[" + label + "] Arrow buttons not found.");
      return;
    }

    // Setup: overflow hidden on wrap so off-frame cards are invisible
    gsap.set(wrap, {
      overflow: "hidden",
      position: "relative"
    });

    var totalCards = cards.length;
    var currentIndex = 0;
    var isAnimating = false;
    var containerWidth = wrap.offsetWidth;

    // Set all cards to absolute positioning, stacked at the same origin.
    // Only the active card is at x:0. All others are pushed off-frame (x: containerWidth).
    function initPositions() {
      containerWidth = wrap.offsetWidth;
      cards.forEach(function (card, i) {
        gsap.set(card, {
          position: "absolute",
          top: 0,
          left: 0,
          width: containerWidth,
          flexShrink: 0,
          // All inactive cards start off-frame to the right
          x: i === 0 ? 0 : containerWidth
        });
      });
      // Give wrap an explicit height so it doesn't collapse after cards go absolute
      gsap.set(wrap, { height: cards[0].offsetHeight });
    }

    initPositions();

    // Navigation function
    function goTo(newIndex) {
      if (isAnimating || newIndex === currentIndex) return;
      isAnimating = true;

      containerWidth = wrap.offsetWidth;

      // Determine slide direction:
      // +1 = sliding left (next), -1 = sliding right (prev)
      var direction;
      if (newIndex > currentIndex) {
        direction = 1;
      } else {
        direction = -1;
      }

      // Detect wraparound cases
      if (currentIndex === totalCards - 1 && newIndex === 0) direction = 1;
      if (currentIndex === 0 && newIndex === totalCards - 1) direction = -1;

      var cardOut = cards[currentIndex];
      var cardIn  = cards[newIndex];

      // Place incoming card just off-frame in the correct direction
      gsap.set(cardIn, { x: direction * containerWidth });

      var tl = gsap.timeline({
        onComplete: function () {
          // Push the outgoing card fully off-frame and keep it there.
          gsap.set(cardOut, { x: -direction * containerWidth });
          isAnimating = false;
          currentIndex = newIndex;
        }
      });

      // Outgoing card slides out
      tl.to(cardOut, {
        x: -direction * containerWidth,
        duration: 0.5,
        ease: "power2.inOut"
      }, 0);

      // Incoming card slides in
      tl.to(cardIn, {
        x: 0,
        duration: 0.5,
        ease: "power2.inOut"
      }, 0);
    }

    // Button listeners
    btnNext.addEventListener("click", function () {
      var nextIndex = (currentIndex + 1) % totalCards;
      goTo(nextIndex);
    });

    btnPrev.addEventListener("click", function () {
      var prevIndex = (currentIndex - 1 + totalCards) % totalCards;
      goTo(prevIndex);
    });

    // Resize handler: recalculate on orientation change
    window.addEventListener("resize", function () {
      if (!window.matchMedia("(max-width: 479px)").matches) return;
      containerWidth = wrap.offsetWidth;
      // Re-pin active card at x:0, push all others off-frame
      cards.forEach(function (card, i) {
        gsap.set(card, {
          width: containerWidth,
          x: i === currentIndex ? 0 : containerWidth
        });
      });
      gsap.set(wrap, { height: cards[currentIndex].offsetHeight });
    });
  }

  // Instance 1: existing team card slider (unchanged target)
  initCardSlider({
    label: "TeamCardSlider",
    wrapSelector: ".team-member-list",
    cardSelector: ".team-card-wrap"
  });

  // Instance 2: testimonial card slider (new target, same arrow classes)
  initCardSlider({
    label: "TestimonialCardSlider",
    wrapSelector: ".testimonial-card",
    cardSelector: ".card-testimonial"
  });

});
</script>