# Team Coverflow Carousel

> A 3D coverflow carousel of team portraits with name/role swaps, dots, and arrow navigation — polished about-page people showcase.

**Definition:** Team Coverflow Carousel is a sliders 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: Sliders
- Stack: HTML, CSS, JavaScript, and GSAP
- Added: 2026-08-06
- Canonical page: https://html.bosconvertpro.com/collection/team-coverflow-carousel
- Full-page preview: https://html.bosconvertpro.com/collection/team-coverflow-carousel/full
- Live demo: https://html.bosconvertpro.com/demos/team-coverflow-carousel/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/team-coverflow-carousel/index.md
- License: https://html.bosconvertpro.com/terms

## How to use
1. Open https://html.bosconvertpro.com/collection/team-coverflow-carousel/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/team-coverflow-carousel.
3. Paste the files into your project and adjust timing, easing, and content as needed.

## FAQ
### What is Team Coverflow Carousel?

Team Coverflow Carousel is a sliders UI animation from collections.html. A 3D coverflow carousel of team portraits with name/role swaps, dots, and arrow navigation — polished about-page people showcase.

### What is Team Coverflow Carousel built with?

Team Coverflow Carousel 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 Team Coverflow Carousel in my project?

Open the animation page (https://html.bosconvertpro.com/collection/team-coverflow-carousel), 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/team-coverflow-carousel/index.html.

### Is Team Coverflow Carousel free to use?

Yes. The Team Coverflow Carousel 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>Team Coverflow Carousel</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link rel="preconnect" href="https://images.unsplash.com" />
    <link
      href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600;700&family=Manrope:wght@400;500;600;700&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="./styles.css" />
  </head>
  <body>
    <div class="bg-glow" aria-hidden="true"></div>

    <main class="page">
      <header class="page-header">
        <p class="eyebrow">Meet the people</p>
        <h1 class="about-title">Our Team</h1>
      </header>

      <section
        class="carousel"
        aria-roledescription="carousel"
        aria-label="Team members"
      >
        <button
          class="nav-arrow nav-arrow--prev"
          type="button"
          aria-label="Previous member"
        >
          <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
            <path
              d="M14.5 6.5 9 12l5.5 5.5"
              stroke="currentColor"
              stroke-width="2"
              stroke-linecap="round"
              stroke-linejoin="round"
            />
          </svg>
        </button>

        <div class="carousel-stage" data-stage>
          <div class="carousel-track" data-track></div>
        </div>

        <button
          class="nav-arrow nav-arrow--next"
          type="button"
          aria-label="Next member"
        >
          <svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
            <path
              d="M9.5 6.5 15 12l-5.5 5.5"
              stroke="currentColor"
              stroke-width="2"
              stroke-linecap="round"
              stroke-linejoin="round"
            />
          </svg>
        </button>
      </section>

      <div class="member-info" aria-live="polite">
        <div class="member-info__inner">
          <h2 class="member-name" data-name></h2>
          <p class="member-role" data-role></p>
        </div>
      </div>

      <div class="dots" data-dots role="tablist" aria-label="Team members"></div>
    </main>

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

## Source — styles.css
````css
*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  --bg: #f6f3ee;
  --bg-elevated: #ffffff;
  --text: #1c1917;
  --text-soft: #3f3a36;
  --text-muted: #8a847c;
  --accent: #9a7b5a;
  --accent-deep: #6f5640;
  --accent-soft: rgba(154, 123, 90, 0.14);
  --border: rgba(28, 25, 23, 0.08);
  --border-strong: rgba(28, 25, 23, 0.12);
  --shadow-sm: 0 4px 16px rgba(28, 25, 23, 0.06);
  --shadow-md: 0 18px 40px rgba(28, 25, 23, 0.1);
  --shadow-lg: 0 28px 60px rgba(28, 25, 23, 0.14);
  --radius: 22px;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --font-display: "Cormorant Garamond", "Times New Roman", Georgia, serif;
  --font-body: "Manrope", system-ui, -apple-system, sans-serif;
}

html {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

body {
  min-height: 100vh;
  min-height: 100dvh;
  font-family: var(--font-body);
  color: var(--text);
  background: var(--bg);
  overflow-x: hidden;
  overflow-y: auto;
}

.bg-glow {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  background:
    radial-gradient(ellipse 75% 50% at 50% 38%, rgba(255, 255, 255, 0.9), transparent 70%),
    radial-gradient(ellipse 45% 35% at 15% 85%, rgba(212, 196, 172, 0.28), transparent 60%),
    radial-gradient(ellipse 40% 30% at 88% 18%, rgba(230, 220, 205, 0.45), transparent 55%),
    linear-gradient(180deg, #faf8f5 0%, #f6f3ee 50%, #f0ebe3 100%);
}

.page {
  position: relative;
  z-index: 1;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: clamp(1.5rem, 4vw, 3rem) 1.25rem clamp(2rem, 5vw, 3.5rem);
  user-select: none;
}

.page-header {
  text-align: center;
  margin-bottom: clamp(0.5rem, 2vw, 1rem);
}

.eyebrow {
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 0.55rem;
}

.about-title {
  font-family: var(--font-display);
  font-size: clamp(3rem, 9vw, 5.75rem);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 0.95;
  color: var(--text);
}

.carousel {
  width: 100%;
  max-width: 1100px;
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: clamp(0.5rem, 2vw, 1.25rem);
  margin-top: clamp(0.75rem, 2vw, 1.5rem);
}

.carousel-stage {
  position: relative;
  height: clamp(340px, 48vw, 440px);
  perspective: 1400px;
  perspective-origin: 50% 50%;
  overflow: visible;
  touch-action: pan-y;
  cursor: grab;
}

.carousel-stage.is-dragging {
  cursor: grabbing;
}

.carousel-track {
  width: 100%;
  height: 100%;
  position: relative;
  transform-style: preserve-3d;
}

.card {
  position: absolute;
  top: 50%;
  left: 50%;
  width: clamp(200px, 28vw, 280px);
  height: clamp(280px, 38vw, 380px);
  margin: 0;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--bg-elevated);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-sm), var(--shadow-md);
  cursor: pointer;
  will-change: transform, opacity, filter;
  transform-origin: center center;
  outline: none;
  transition:
    border-color 0.35s var(--ease),
    box-shadow 0.35s var(--ease);
}

.card:focus-visible {
  border-color: rgba(154, 123, 90, 0.45);
  box-shadow:
    0 0 0 3px var(--accent-soft),
    var(--shadow-md);
}

.card.is-center {
  border-color: var(--border-strong);
  box-shadow:
    var(--shadow-sm),
    var(--shadow-lg),
    0 0 0 1px rgba(255, 255, 255, 0.8) inset;
  cursor: default;
}

.card__media {
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  pointer-events: none;
  will-change: filter, transform;
}

.card__shine {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    145deg,
    rgba(255, 255, 255, 0.22) 0%,
    transparent 40%,
    transparent 100%
  );
  opacity: 0;
  transition: opacity 0.5s var(--ease);
}

.card.is-center .card__shine {
  opacity: 1;
}

.nav-arrow {
  flex-shrink: 0;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  background: rgba(255, 255, 255, 0.72);
  color: var(--text-soft);
  display: grid;
  place-items: center;
  cursor: pointer;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  box-shadow: 0 4px 14px rgba(28, 25, 23, 0.05);
  transition:
    background 0.3s var(--ease),
    border-color 0.3s var(--ease),
    transform 0.3s var(--ease),
    color 0.3s var(--ease),
    box-shadow 0.3s var(--ease);
}

.nav-arrow svg {
  width: 20px;
  height: 20px;
}

.nav-arrow:hover {
  background: #ffffff;
  border-color: rgba(154, 123, 90, 0.35);
  color: var(--accent-deep);
  box-shadow: 0 8px 22px rgba(28, 25, 23, 0.08);
  transform: scale(1.06);
}

.nav-arrow:active {
  transform: scale(0.96);
}

.nav-arrow:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.member-info {
  text-align: center;
  margin-top: clamp(1.5rem, 3.5vw, 2.5rem);
  min-height: 5.5rem;
  display: flex;
  align-items: flex-start;
  justify-content: center;
}

.member-info__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
}

.member-name {
  font-family: var(--font-display);
  font-size: clamp(2rem, 4.5vw, 2.85rem);
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--text);
  line-height: 1.15;
}

.member-name::before,
.member-name::after {
  content: "";
  display: inline-block;
  vertical-align: middle;
  width: clamp(28px, 6vw, 56px);
  height: 1px;
  margin: 0 0.85rem;
  background: linear-gradient(
    90deg,
    transparent,
    rgba(154, 123, 90, 0.45),
    transparent
  );
}

.member-role {
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.dots {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-top: clamp(1.25rem, 3vw, 2rem);
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 999px;
  border: none;
  padding: 0;
  background: rgba(28, 25, 23, 0.14);
  cursor: pointer;
  transition:
    background 0.35s var(--ease),
    width 0.4s var(--ease),
    transform 0.3s var(--ease);
}

.dot:hover {
  background: rgba(28, 25, 23, 0.28);
}

.dot.is-active {
  width: 28px;
  background: var(--accent);
}

.dot:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

@media (max-width: 768px) {
  .carousel {
    position: relative;
    display: block;
    max-width: 100%;
  }

  .carousel-stage {
    width: 100%;
    margin-bottom: 4.25rem;
  }

  .nav-arrow {
    width: 44px;
    height: 44px;
    position: absolute;
    bottom: 0;
    z-index: 20;
  }

  .nav-arrow--prev {
    left: calc(50% - 52px);
    transform: translateX(-50%);
  }

  .nav-arrow--next {
    left: calc(50% + 52px);
    transform: translateX(-50%);
  }

  .nav-arrow:hover {
    transform: translateX(-50%) scale(1.06);
  }

  .nav-arrow:active {
    transform: translateX(-50%) scale(0.96);
  }

  .member-name::before,
  .member-name::after {
    width: 24px;
    margin: 0 0.5rem;
  }
}

@media (max-width: 480px) {
  .card {
    width: 180px;
    height: 250px;
  }

  .carousel-stage {
    height: 300px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .card,
  .nav-arrow,
  .dot,
  .card__shine {
    transition: none !important;
  }
}
````

## Source — script.js
````javascript
(() => {
  "use strict";

  if (typeof gsap === "undefined") return;

  gsap.config({ force3D: true, nullTargetWarn: false });
  gsap.ticker.lagSmoothing(500, 33);

  const MEMBERS = [
    {
      name: "Emily Kim",
      role: "Founder",
      image:
        "https://images.unsplash.com/photo-1573497019940-1c28c88b4f3e?q=80&w=900&auto=format&fit=crop",
    },
    {
      name: "Michael Steward",
      role: "Creative Director",
      image:
        "https://images.unsplash.com/photo-1568602471122-7832951cc4c5?q=80&w=900&auto=format&fit=crop",
    },
    {
      name: "Emma Rodriguez",
      role: "Lead Developer",
      image:
        "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=900&auto=format&fit=crop&q=80",
    },
    {
      name: "Julia Gimmel",
      role: "UX Designer",
      image:
        "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=900&auto=format&fit=crop&q=80",
    },
    {
      name: "Lisa Anderson",
      role: "Marketing Manager",
      image:
        "https://images.unsplash.com/photo-1655249481446-25d575f1c054?w=900&auto=format&fit=crop&q=80",
    },
    {
      name: "James Wilson",
      role: "Product Manager",
      image:
        "https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?q=80&w=900&auto=format&fit=crop",
    },
  ];

  const SLOTS = {
    center: { x: 0, z: 0, scale: 1.08, opacity: 1, gray: 0, zIndex: 30 },
    near: { x: 0.72, z: -120, scale: 0.9, opacity: 0.88, gray: 0.85, zIndex: 20 },
    far: { x: 1.35, z: -260, scale: 0.78, opacity: 0.55, gray: 1, zIndex: 10 },
    hidden: { x: 1.9, z: -400, scale: 0.65, opacity: 0, gray: 1, zIndex: 1 },
  };

  const DURATION = 0.85;
  const EASE = "power3.out";
  const TEXT_DURATION = 0.45;
  const SWIPE_THRESHOLD = 48;
  const DRAG_DEADZONE = 8;

  class CardCarousel {
    constructor(root = document) {
      this.root = root;
      this.stage = root.querySelector("[data-stage]");
      this.track = root.querySelector("[data-track]");
      this.dotsRoot = root.querySelector("[data-dots]");
      this.nameEl = root.querySelector("[data-name]");
      this.roleEl = root.querySelector("[data-role]");
      this.prevBtn = root.querySelector(".nav-arrow--prev");
      this.nextBtn = root.querySelector(".nav-arrow--next");

      if (!this.stage || !this.track) return;

      this.members = MEMBERS;
      this.total = this.members.length;
      this.index = 0;
      this.busy = false;
      this.cards = [];
      this.dots = [];
      this.resizeTimer = null;

      this.build();
      this.bind();
      this.mount();
    }

    prefersReducedMotion() {
      return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    }

    spacing() {
      const width = window.innerWidth;
      if (width <= 480) return 130;
      if (width <= 768) return 160;
      return 210;
    }

    wrap(value) {
      return ((value % this.total) + this.total) % this.total;
    }

    shortestOffset(i) {
      let delta = i - this.index;
      if (delta > this.total / 2) delta -= this.total;
      if (delta < -this.total / 2) delta += this.total;
      return delta;
    }

    resolveSlot(offset) {
      if (offset === 0) return { key: "center", dir: 0 };
      const abs = Math.abs(offset);
      const dir = offset > 0 ? 1 : -1;
      if (abs === 1) return { key: "near", dir };
      if (abs === 2) return { key: "far", dir };
      return { key: "hidden", dir };
    }

    slotTransform(key, dir, spacing) {
      const slot = SLOTS[key];
      return {
        x: slot.x * dir * spacing,
        z: slot.z,
        scale: slot.scale,
        opacity: slot.opacity,
        gray: slot.gray,
        zIndex: slot.zIndex,
      };
    }

    build() {
      const fragment = document.createDocumentFragment();

      this.members.forEach((member, i) => {
        const card = document.createElement("article");
        card.className = "card";
        card.dataset.index = String(i);
        card.tabIndex = 0;

        const media = document.createElement("div");
        media.className = "card__media";

        const img = document.createElement("img");
        img.src = member.image;
        img.alt = member.name;
        img.draggable = false;
        img.loading = i === 0 ? "eager" : "lazy";
        media.appendChild(img);

        const shine = document.createElement("div");
        shine.className = "card__shine";
        shine.setAttribute("aria-hidden", "true");

        card.append(media, shine);
        fragment.appendChild(card);
      });

      this.track.appendChild(fragment);
      this.cards = gsap.utils.toArray(".card", this.track);

      if (!this.dotsRoot) return;

      this.dotsRoot.replaceChildren();
      this.members.forEach((member, i) => {
        const dot = document.createElement("button");
        dot.type = "button";
        dot.className = "dot";
        dot.setAttribute("role", "tab");
        dot.setAttribute("aria-label", member.name);
        dot.setAttribute("aria-selected", "false");
        dot.addEventListener("click", () => this.goTo(i));
        this.dotsRoot.appendChild(dot);
      });

      this.dots = gsap.utils.toArray(".dot", this.dotsRoot);
    }

    cardState(card, props, key) {
      return {
        transform: {
          xPercent: -50,
          yPercent: -50,
          x: props.x,
          z: props.z,
          scale: props.scale,
          opacity: props.opacity,
          force3D: true,
        },
        image: {
          filter: `grayscale(${props.gray * 100}%)`,
          scale: key === "center" ? 1.02 : 1,
        },
      };
    }

    applyMeta(card, key, zIndex, hidden) {
      card.dataset.slot = key;
      card.classList.toggle("is-center", key === "center");
      card.style.zIndex = String(zIndex);
      card.setAttribute("aria-hidden", hidden ? "true" : "false");
      card.tabIndex = hidden ? -1 : 0;
    }

    layout(animate = true) {
      const spacing = this.spacing();
      const reduce = this.prefersReducedMotion();
      const shouldAnimate = animate && !reduce;

      this.cards.forEach((card, i) => {
        const offset = this.shortestOffset(i);
        const { key, dir } = this.resolveSlot(offset);
        const props = this.slotTransform(key, dir, spacing);
        const img = card.querySelector("img");
        const wasHidden = card.dataset.slot === "hidden";
        const nowHidden = key === "hidden";
        const { transform, image } = this.cardState(card, props, key);

        this.applyMeta(card, key, props.zIndex, nowHidden);

        if (!shouldAnimate) {
          gsap.set(card, transform);
          if (img) gsap.set(img, image);
          return;
        }

        gsap.killTweensOf(card);
        if (img) gsap.killTweensOf(img);

        if (wasHidden && !nowHidden) {
          gsap.set(card, {
            xPercent: -50,
            yPercent: -50,
            x: props.x * 1.15,
            z: props.z - 40,
            scale: props.scale * 0.92,
            opacity: 0,
            force3D: true,
          });
          gsap.to(card, { ...transform, duration: DURATION, ease: EASE, overwrite: "auto" });
          if (img) gsap.to(img, { ...image, duration: DURATION, ease: EASE });
          return;
        }

        if (!wasHidden && nowHidden) {
          gsap.to(card, {
            opacity: 0,
            scale: props.scale,
            z: props.z,
            duration: DURATION * 0.55,
            ease: "power2.in",
            overwrite: "auto",
            onComplete: () => gsap.set(card, transform),
          });
          if (img) {
            gsap.to(img, { ...image, duration: DURATION * 0.55, ease: "power2.in" });
          }
          return;
        }

        gsap.to(card, { ...transform, duration: DURATION, ease: EASE, overwrite: "auto" });
        if (img) gsap.to(img, { ...image, duration: DURATION, ease: EASE, overwrite: "auto" });
      });
    }

    updateInfo(animate = true) {
      const member = this.members[this.index];
      if (!member || !this.nameEl || !this.roleEl) return;

      const reduce = this.prefersReducedMotion();
      const els = [this.nameEl, this.roleEl];

      if (!animate || reduce) {
        this.nameEl.textContent = member.name;
        this.roleEl.textContent = member.role;
        gsap.set(els, { opacity: 1, y: 0 });
        return;
      }

      gsap
        .timeline({ defaults: { ease: "power2.out" } })
        .to(els, {
          opacity: 0,
          y: 10,
          duration: TEXT_DURATION * 0.55,
          stagger: 0.04,
          onComplete: () => {
            this.nameEl.textContent = member.name;
            this.roleEl.textContent = member.role;
          },
        })
        .to(els, {
          opacity: 1,
          y: 0,
          duration: TEXT_DURATION,
          stagger: 0.06,
        });
    }

    updateDots() {
      this.dots.forEach((dot, i) => {
        const active = i === this.index;
        dot.classList.toggle("is-active", active);
        dot.setAttribute("aria-selected", active ? "true" : "false");
      });
    }

    goTo(index, { force = false } = {}) {
      if (this.busy && !force) return;

      const next = this.wrap(index);
      if (next === this.index && !force) return;

      this.busy = true;
      this.index = next;
      this.layout(true);
      this.updateInfo(true);
      this.updateDots();

      gsap.delayedCall(DURATION * 0.75, () => {
        this.busy = false;
      });
    }

    next() {
      this.goTo(this.index + 1);
    }

    prev() {
      this.goTo(this.index - 1);
    }

    bindDrag() {
      let startX = 0;
      let dragging = false;
      let moved = false;
      let pointerId = null;

      const onDown = (event) => {
        if (event.pointerType === "mouse" && event.button !== 0) return;
        dragging = true;
        moved = false;
        startX = event.clientX;
        pointerId = event.pointerId;
        this.stage.classList.add("is-dragging");
        this.stage.setPointerCapture?.(pointerId);
      };

      const onMove = (event) => {
        if (!dragging) return;
        if (Math.abs(event.clientX - startX) > DRAG_DEADZONE) moved = true;
      };

      const onUp = (event) => {
        if (!dragging) return;
        dragging = false;
        this.stage.classList.remove("is-dragging");

        const delta = event.clientX - startX;
        if (Math.abs(delta) >= SWIPE_THRESHOLD) {
          delta < 0 ? this.next() : this.prev();
        }

        if (pointerId != null) {
          this.stage.releasePointerCapture?.(pointerId);
          pointerId = null;
        }

        if (moved) {
          this.stage.dataset.suppressClick = "1";
          requestAnimationFrame(() => {
            delete this.stage.dataset.suppressClick;
          });
        }
      };

      this.stage.addEventListener("pointerdown", onDown);
      this.stage.addEventListener("pointermove", onMove);
      this.stage.addEventListener("pointerup", onUp);
      this.stage.addEventListener("pointercancel", onUp);
    }

    bind() {
      this.prevBtn?.addEventListener("click", () => this.prev());
      this.nextBtn?.addEventListener("click", () => this.next());

      this.cards.forEach((card, i) => {
        card.addEventListener("click", () => {
          if (this.stage.dataset.suppressClick) return;
          if (i !== this.index) this.goTo(i);
        });

        card.addEventListener("keydown", (event) => {
          if (event.key !== "Enter" && event.key !== " ") return;
          event.preventDefault();
          if (i !== this.index) this.goTo(i);
        });
      });

      document.addEventListener("keydown", (event) => {
        if (event.key === "ArrowLeft") {
          event.preventDefault();
          this.prev();
        } else if (event.key === "ArrowRight") {
          event.preventDefault();
          this.next();
        }
      });

      window.addEventListener("resize", () => {
        clearTimeout(this.resizeTimer);
        this.resizeTimer = setTimeout(() => this.layout(false), 120);
      });

      this.bindDrag();
    }

    intro() {
      this.layout(false);

      const timeline = gsap.timeline({ delay: 0.05 });

      this.cards.forEach((card, i) => {
        const depth = Math.abs(this.shortestOffset(i));
        const opacity = Number(gsap.getProperty(card, "opacity"));

        timeline.fromTo(
          card,
          { y: 40, opacity: 0 },
          { y: 0, opacity, duration: 0.9, ease: "power3.out" },
          0.04 + depth * 0.07
        );
      });

      if (this.nameEl && this.roleEl) {
        gsap.set([this.nameEl, this.roleEl], { opacity: 0, y: 16 });
        timeline.to(
          [this.nameEl, this.roleEl],
          { opacity: 1, y: 0, duration: 0.7, stagger: 0.08, ease: "power2.out" },
          0.3
        );
      }

      if (this.dots.length) {
        gsap.set(this.dots, { opacity: 0, scale: 0.6 });
        timeline.to(
          this.dots,
          { opacity: 1, scale: 1, duration: 0.45, stagger: 0.04, ease: "power2.out" },
          0.45
        );
      }
    }

    mount() {
      const member = this.members[0];
      if (this.nameEl) this.nameEl.textContent = member.name;
      if (this.roleEl) this.roleEl.textContent = member.role;
      this.updateDots();

      if (this.prefersReducedMotion()) {
        this.layout(false);
        this.updateInfo(false);
        return;
      }

      this.intro();
    }
  }

  const start = () => new CardCarousel();

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", start, { once: true });
  } else {
    start();
  }
})();
````
