# Liquid Glass Carousel

> A Three.js carousel with a refractive liquid-glass overlay, odometer index, and caption swaps — immersive image galleries.

**Definition:** Liquid Glass 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-30
- Canonical page: https://html.bosconvertpro.com/collection/liquid-glass-carousel
- Full-page preview: https://html.bosconvertpro.com/collection/liquid-glass-carousel/full
- Live demo: https://html.bosconvertpro.com/demos/liquid-glass-carousel/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/liquid-glass-carousel/index.md
- License: https://html.bosconvertpro.com/terms

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

## FAQ
### What is Liquid Glass Carousel?

Liquid Glass Carousel is a sliders UI animation from collections.html. A Three.js carousel with a refractive liquid-glass overlay, odometer index, and caption swaps — immersive image galleries.

### What is Liquid Glass Carousel built with?

Liquid Glass 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 Liquid Glass Carousel in my project?

Open the animation page (https://html.bosconvertpro.com/collection/liquid-glass-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/liquid-glass-carousel/index.html.

### Is Liquid Glass Carousel free to use?

Yes. The Liquid Glass 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>Liquid Glass 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 rel="preconnect" href="https://cdn.jsdelivr.net" />
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="./styles.css" />
  </head>
  <body>
    <section data-glass-carousel class="glass-carousel">
      <canvas
        aria-hidden="true"
        data-glass-carousel-canvas
        class="glass-carousel__canvas"
      ></canvas>

      <div class="glass-carousel__collection">
        <div data-glass-carousel-list class="glass-carousel__list">
          <div data-glass-carousel-item class="glass-carousel__item">
            <img
              src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&auto=format&fm=jpg&fit=crop&w=1600&q=80"
              alt="Sun breaking over a mountain ridge in golden light"
              crossorigin="anonymous"
              referrerpolicy="no-referrer"
              data-glass-carousel-image
              class="glass-carousel__item-img"
              draggable="false"
            />
            <div data-glass-carousel-content class="glass-carousel__item-content">
              <p class="glass-carousel__item-p">First Light</p>
              <p class="glass-carousel__item-p is--faded">Alpine Horizon Diary</p>
            </div>
          </div>

          <div data-glass-carousel-item class="glass-carousel__item">
            <img
              src="https://images.unsplash.com/photo-1480714378408-67cf0d13bc1b?ixlib=rb-4.0.3&auto=format&fm=jpg&fit=crop&w=1600&q=80"
              alt="City skyline across a harbor at blue hour"
              crossorigin="anonymous"
              referrerpolicy="no-referrer"
              data-glass-carousel-image
              class="glass-carousel__item-img"
              draggable="false"
            />
            <div data-glass-carousel-content class="glass-carousel__item-content">
              <p class="glass-carousel__item-p">Steel Hour</p>
              <p class="glass-carousel__item-p is--faded">Harbor Grid Study</p>
            </div>
          </div>

          <div data-glass-carousel-item class="glass-carousel__item">
            <img
              src="https://images.unsplash.com/photo-1426604966848-d7adac402bff?ixlib=rb-4.0.3&auto=format&fm=jpg&fit=crop&w=1600&q=80"
              alt="Granite valley with a river winding through pine forest"
              crossorigin="anonymous"
              referrerpolicy="no-referrer"
              data-glass-carousel-image
              class="glass-carousel__item-img"
              draggable="false"
            />
            <div data-glass-carousel-content class="glass-carousel__item-content">
              <p class="glass-carousel__item-p">Granite Gate</p>
              <p class="glass-carousel__item-p is--faded">Valley Floor Record</p>
            </div>
          </div>

          <div data-glass-carousel-item class="glass-carousel__item">
            <img
              src="https://images.unsplash.com/photo-1519904981063-b0cf448d479e?ixlib=rb-4.0.3&auto=format&fm=jpg&fit=crop&w=1600&q=80"
              alt="Colorful canal houses reflected in still water"
              crossorigin="anonymous"
              referrerpolicy="no-referrer"
              data-glass-carousel-image
              class="glass-carousel__item-img"
              draggable="false"
            />
            <div data-glass-carousel-content class="glass-carousel__item-content">
              <p class="glass-carousel__item-p">Painted Quay</p>
              <p class="glass-carousel__item-p is--faded">Lagoon Color Notes</p>
            </div>
          </div>

          <div data-glass-carousel-item class="glass-carousel__item">
            <img
              src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?ixlib=rb-4.0.3&auto=format&fm=jpg&fit=crop&w=1600&q=80"
              alt="Fog drifting across green mountain slopes"
              crossorigin="anonymous"
              referrerpolicy="no-referrer"
              data-glass-carousel-image
              class="glass-carousel__item-img"
              draggable="false"
            />
            <div data-glass-carousel-content class="glass-carousel__item-content">
              <p class="glass-carousel__item-p">Soft Range</p>
              <p class="glass-carousel__item-p is--faded">Cloud Line Journal</p>
            </div>
          </div>

          <div data-glass-carousel-item class="glass-carousel__item">
            <img
              src="https://images.unsplash.com/photo-1493976040374-85c8e12f0c0e?ixlib=rb-4.0.3&auto=format&fm=jpg&fit=crop&w=1600&q=80"
              alt="Hot air balloons rising over a desert valley at dawn"
              crossorigin="anonymous"
              referrerpolicy="no-referrer"
              data-glass-carousel-image
              class="glass-carousel__item-img"
              draggable="false"
            />
            <div data-glass-carousel-content class="glass-carousel__item-content">
              <p class="glass-carousel__item-p">Lift Off</p>
              <p class="glass-carousel__item-p is--faded">Cappadocia Field Set</p>
            </div>
          </div>

          <div data-glass-carousel-item class="glass-carousel__item">
            <img
              src="https://images.unsplash.com/photo-1439066615861-d1af74d74000?ixlib=rb-4.0.3&auto=format&fm=jpg&fit=crop&w=1600&q=80"
              alt="Still lake reflecting pine forest and mountain light"
              crossorigin="anonymous"
              referrerpolicy="no-referrer"
              data-glass-carousel-image
              class="glass-carousel__item-img"
              draggable="false"
            />
            <div data-glass-carousel-content class="glass-carousel__item-content">
              <p class="glass-carousel__item-p">Mirror Bay</p>
              <p class="glass-carousel__item-p is--faded">Quiet Water Study</p>
            </div>
          </div>
        </div>
      </div>

      <div
        aria-live="polite"
        data-glass-carousel-caption
        class="glass-carousel__caption"
      >
        <div class="glass-carousel__caption-line">
          <div data-caption-title class="glass-carousel__caption-track"></div>
        </div>
        <div class="glass-carousel__caption-line">
          <div data-caption-sub class="glass-carousel__caption-track"></div>
        </div>
      </div>
      <div class="glass-carousel__controls">
        <button
          type="button"
          class="glass-carousel__nav is--prev"
          data-glass-carousel-prev
          aria-label="Previous slide"
        >
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
            <path
              d="M14.5 5.5 8.5 12l6 6.5"
              fill="none"
              stroke="currentColor"
              stroke-width="1.75"
              stroke-linecap="round"
              stroke-linejoin="round"
            />
          </svg>
        </button>

        <div data-glass-carousel-counter class="glass-carousel__counter">
          <span data-odo-current class="glass-carousel__odo" aria-hidden="true"></span>
          <span class="glass-carousel__odo-slash">/</span>
          <span data-odo-total class="glass-carousel__odo-total">07</span>
        </div>

        <button
          type="button"
          class="glass-carousel__nav is--next"
          data-glass-carousel-next
          aria-label="Next slide"
        >
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
            <path
              d="M9.5 5.5 15.5 12l-6 6.5"
              fill="none"
              stroke="currentColor"
              stroke-width="1.75"
              stroke-linecap="round"
              stroke-linejoin="round"
            />
          </svg>
        </button>
      </div>
    </section>

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

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

:root {
  --paper: #f3eee7;
  --ink: #2c2824;
  --surface: #fffcf8;
  --line: #3a322a14;
  --shadow: #4a382814;
  --hover: #efe8df;
}

html,
body {
  margin: 0;
  width: 100%;
  height: 100%;
  background: var(--paper);
  color: var(--ink);
  font-family: Inter, Arial, sans-serif;
  font-size: 16px;
  line-height: 1.2;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  overflow: clip;
}

img {
  max-width: none;
}

.glass-carousel {
  background-color: var(--paper);
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100svh;
  min-height: 100svh;
  display: flex;
  position: relative;
  overflow: clip;
}

.glass-carousel__canvas {
  width: 100%;
  height: 100%;
  display: block;
  position: absolute;
  inset: 0;
  z-index: 1;
  touch-action: pan-y;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

[data-glass-carousel-wheel="all"] canvas {
  touch-action: none;
}

[data-glass-carousel-pointer="grab"] canvas {
  cursor: grab;
}

[data-glass-carousel-pointer="grabbing"] canvas {
  cursor: grabbing;
}

.glass-carousel__collection {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.glass-carousel__list {
  scroll-snap-type: x mandatory;
  justify-content: flex-start;
  align-items: center;
  padding-left: 2em;
  padding-right: 2em;
  display: flex;
  position: absolute;
  inset: 0;
  overflow-x: auto;
  gap: 2.5em;
  scrollbar-width: none;
}

.glass-carousel__list::-webkit-scrollbar {
  display: none;
}

[data-glass-carousel="canvas"] [data-glass-carousel-list] {
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.glass-carousel__item {
  scroll-snap-align: center;
  flex: none;
  height: 35%;
}

[data-glass-carousel-item] [data-glass-carousel-content] {
  visibility: hidden;
}

.glass-carousel__item-img {
  object-fit: cover;
  height: 100%;
  display: block;
}

.glass-carousel__item-p {
  margin-top: 0;
  margin-bottom: 0;
  font-family: Inter, Arial, sans-serif;
  font-size: 1em;
  font-weight: 400;
  line-height: 1.2;
}

.glass-carousel__item-p.is--faded {
  opacity: 0.6;
}

.glass-carousel__caption {
  pointer-events: none;
  text-align: center;
  padding-left: 1em;
  padding-right: 1em;
  position: absolute;
  top: 15%;
  left: 50%;
  z-index: 5;
  transform: translate(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1em;
}

.glass-carousel__caption-line {
  position: relative;
  height: 1.25em;
  line-height: 1.25em;
}

.glass-carousel__caption-track {
  position: relative;
  display: block;
  height: 1.25em;
  min-width: 1em;
}

.glass-carousel__caption-track .glass-carousel__item-p {
  position: absolute;
  left: 50%;
  top: 0;
  margin: 0;
  white-space: nowrap;
  text-align: center;
  line-height: 1.25em;
  transform: translateX(-50%);
  will-change: filter, opacity;
}

.glass-carousel__controls {
  position: absolute;
  bottom: 12%;
  left: 50%;
  z-index: 6;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75em;
  transform: translateX(-50%);
  pointer-events: auto;
}

.glass-carousel__counter {
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.35em;
  text-align: center;
  background-color: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  min-width: 5.5em;
  padding: 0.7em 1.15em;
  font-size: 1em;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  box-shadow: 0 8px 24px var(--shadow);
}

.glass-carousel__odo {
  display: inline-flex;
  align-items: stretch;
  height: 1.2em;
  overflow: hidden;
}

.glass-carousel__odo-digit {
  position: relative;
  display: block;
  width: 0.66em;
  height: 1.2em;
  overflow: hidden;
}

.glass-carousel__odo-track {
  display: flex;
  flex-direction: column;
  align-items: center;
  will-change: transform;
}

.glass-carousel__odo-num {
  display: flex;
  flex: 0 0 1.2em;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 1.2em;
  font-variant-numeric: tabular-nums;
}

.glass-carousel__odo-slash {
  opacity: 0.45;
  line-height: 1.2em;
}

.glass-carousel__odo-total {
  line-height: 1.2em;
  font-variant-numeric: tabular-nums;
}

.glass-carousel__nav {
  position: relative;
  z-index: 6;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 2.85em;
  height: 2.85em;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  box-shadow: 0 8px 24px var(--shadow);
  transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}

.glass-carousel__nav svg {
  display: block;
  width: 1.15em;
  height: 1.15em;
}

.glass-carousel__nav:hover {
  background: var(--hover);
  box-shadow: 0 10px 28px var(--shadow);
}

.glass-carousel__nav:active {
  transform: scale(0.94);
}

.glass-carousel__nav:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

@media (max-width: 768px) {
  .glass-carousel__controls {
    bottom: 8%;
    gap: 0.5em;
  }

  .glass-carousel__nav {
    width: 2.5em;
    height: 2.5em;
  }
}

@media (prefers-reduced-motion: reduce) {
  .glass-carousel__caption,
  .glass-carousel__counter {
    display: none;
  }
}
````

## Source — script.js
````javascript
const ODO_CELL = 1.2;

class OdometerCounter {
  constructor(root, { digits = 2 } = {}) {
    this.root = root;
    this.digitCount = digits;
    this.value = 0;
    this.tracks = [];
    this.reelTimeline = null;
    this.mount();
    this.set(0, { animate: false });
  }

  mount() {
    this.root.textContent = "";
    this.tracks = [];
    for (let column = 0; column < this.digitCount; column += 1) {
      const digit = document.createElement("span");
      digit.className = "glass-carousel__odo-digit";
      const track = document.createElement("span");
      track.className = "glass-carousel__odo-track";
      for (let cycle = 0; cycle < 3; cycle += 1) {
        for (let n = 0; n <= 9; n += 1) {
          const cell = document.createElement("span");
          cell.className = "glass-carousel__odo-num";
          cell.textContent = String(n);
          track.appendChild(cell);
        }
      }
      digit.appendChild(track);
      this.root.appendChild(digit);
      this.tracks.push(track);
    }
  }

  set(value, { animate = true, direction = 1 } = {}) {
    const next = Math.max(0, Math.floor(value));
    const prev = this.value;
    const nextDigits = this.toDigits(next);
    const prevDigits = this.toDigits(prev);

    if (this.reelTimeline) {
      this.reelTimeline.kill();
      this.reelTimeline = null;
    }

    if (!animate) {
      nextDigits.forEach((digit, index) => {
        gsap.set(this.tracks[index], { y: this.offsetForSlot(10 + digit) });
      });
      this.value = next;
      return;
    }

    this.reelTimeline = gsap.timeline({
      onComplete: () => {
        nextDigits.forEach((digit, index) => {
          gsap.set(this.tracks[index], { y: this.offsetForSlot(10 + digit) });
        });
        this.reelTimeline = null;
      },
    });

    nextDigits.forEach((digit, index) => {
      if (digit === prevDigits[index]) {
        gsap.set(this.tracks[index], { y: this.offsetForSlot(10 + digit) });
        return;
      }

      const fromSlot = 10 + prevDigits[index];
      let toSlot;
      if (direction >= 0) {
        toSlot = digit >= prevDigits[index] ? 10 + digit : 20 + digit;
      } else {
        toSlot = digit <= prevDigits[index] ? 10 + digit : digit;
      }

      gsap.set(this.tracks[index], { y: this.offsetForSlot(fromSlot) });
      this.reelTimeline.to(
        this.tracks[index],
        {
          y: this.offsetForSlot(toSlot),
          duration: 0.7,
          ease: "power3.inOut",
        },
        index * 0.05
      );
    });

    this.value = next;
  }

  toDigits(value) {
    return String(value)
      .padStart(this.digitCount, "0")
      .slice(-this.digitCount)
      .split("")
      .map((d) => Number(d));
  }

  offsetForSlot(slot) {
    return `${-slot * ODO_CELL}em`;
  }
}

function blurCaptionLine(track, text, faded, _direction, delay) {
  if (!track) return;

  const restOpacity = faded ? 0.6 : 1;
  const incoming = document.createElement("p");
  incoming.className = faded
    ? "glass-carousel__item-p is--faded"
    : "glass-carousel__item-p";
  incoming.textContent = text;

  const outgoing =
    track.querySelector("p.is--current") || track.querySelector("p");

  if (!outgoing) {
    incoming.classList.add("is--current");
    track.appendChild(incoming);
    gsap.set(incoming, { autoAlpha: restOpacity, filter: "blur(0px)" });
    track.style.width = `${incoming.offsetWidth}px`;
    return;
  }

  if (outgoing.textContent === text) return;

  gsap.killTweensOf(track.querySelectorAll("p"));

  outgoing.classList.remove("is--current");
  outgoing.classList.add("is--outgoing");
  incoming.classList.add("is--current");
  track.appendChild(incoming);

  const width = Math.max(outgoing.offsetWidth, incoming.offsetWidth);
  track.style.width = `${width}px`;

  gsap.set(incoming, {
    autoAlpha: 0,
    filter: "blur(16px)",
  });

  gsap
    .timeline({
      delay,
      onComplete: () => {
        outgoing.remove();
        gsap.set(incoming, { autoAlpha: restOpacity, filter: "blur(0px)" });
        track.style.width = `${incoming.offsetWidth}px`;
      },
    })
    .to(
      outgoing,
      {
        autoAlpha: 0,
        filter: "blur(16px)",
        duration: 0.35,
        ease: "power2.in",
      },
      0
    )
    .to(
      incoming,
      {
        autoAlpha: restOpacity,
        filter: "blur(0px)",
        duration: 0.55,
        ease: "power2.out",
      },
      0.1
    );
}

async function initLiquidGlassCarousel() {
  const wrappers = document.querySelectorAll("[data-glass-carousel]");
  if (!wrappers.length) return;

  const THREE = await import(
    "https://cdn.jsdelivr.net/npm/three@0.184.0/build/three.module.min.js"
  );

  const panelHeight = 0.35;
  const panelGap = 40;
  const wheel = "horizontal";

  const lensWidth = 1;
  const lensHeight = 0.6;
  const lensRotation = 90;
  const lensWidthNarrow = 1;
  const lensHeightNarrow = 0.7;
  const lensRotationNarrow = 90;
  const lensColor = "#c4a484";
  const lensGlow = 4;
  const lensRing = 1;
  const lensRingRadius = 0.49;
  const lensRingWidth = 0.01;
  const lensRimLine = 1;
  const lensNova = 0.1;
  const lensDispersion = 11;
  const lensRimWave = 0.3;
  const lensZoom = 0.4;
  const lensVignette = 0;
  const lensShimmer = true;

  const lensNovaSize = 12;
  const lensShimmerFreq = 12;
  const lensShimmerSpeed = 3.5;
  const lensShimmerDepth = 0.12;
  const lensRimStart = 0.578;
  const lensRimFreq1 = 2;
  const lensRimFreq2 = 1;
  const lensRimLinePos = 0.488;
  const lensRimLineWidth = 0.003;
  const lensVignetteSize = 0.3;
  const lensSamples = 16;
  const textureDetail = 1.5;
  const panelWidthMax = 0.78;
  const narrowBreakpoint = 768;
  const repeats = 4;
  const ease = 0.09;
  const wheelSpeed = 1.4;
  const dragSpeed = 1.6;
  const touchSpeed = 1;
  const touchEase = 0.22;
  const friction = 0.865;
  const snapIdle = 120;
  const snapEase = 0.05;
  const shrinkMax = 60;
  const shrinkAttack = 0.25;
  const shrinkDecay = 0.06;
  const clickSlop = 6;
  const touchClickSlop = 12;
  const flickIdle = 90;

  const vertexShader = `
    varying vec2 vUv;
    void main(){
      vUv = uv;
      gl_Position = vec4(position.xy, 0.0, 1.0);
    }
  `;

  const fragmentShader = `
    #define PI 3.14159265
    precision highp float;
    varying vec2 vUv;
    uniform sampler2D uTex;
    uniform vec2  uRes;
    uniform vec2  uCenter;
    uniform float uSizeX;
    uniform float uSizeY;
    uniform float uAspect;
    uniform float uZoom;
    uniform float uDispersion;
    uniform float uGlow;
    uniform float uWhiteGlow;
    uniform float uNovaSize;
    uniform float uBlueRing;
    uniform float uRingRadius;
    uniform float uRingWidth;
    uniform float uShimmer;
    uniform float uShimmerFreq;
    uniform float uShimmerSpeed;
    uniform float uShimmerDepth;
    uniform float uTime;
    uniform float uRimStart;
    uniform float uRimTangential;
    uniform float uRimFreq1;
    uniform float uRimFreq2;
    uniform vec3  uBlueColor;
    uniform float uRimLine;
    uniform float uRimLinePos;
    uniform float uRimLineWidth;
    uniform float uVignette;
    uniform float uVignetteSize;
    uniform float uRotation;
    uniform int   uSamples;

    const int MAX_SAMPLES = 16;

    vec3 discLens(vec2 center, float aspectCorrect, out float outA) {
      vec2 p = (vUv - center);
      p.x *= aspectCorrect;
      float ca = cos(uRotation), sa = sin(uRotation);
      p = mat2(ca, -sa, sa, ca) * p;
      vec2 halfSize = vec2(uSizeX, uSizeY);
      float dist = length(p / halfSize);
      outA = 0.0;

      float maskND = dist;
      if (maskND > 1.0) return vec3(0.0);

      float shapeND = clamp(maskND, 0.0, 1.0);
      float nd = clamp(dist, 0.0, 1.0);
      vec2  offset = vUv - center;
      vec2  radialDir = normalize(offset + 1e-6);
      vec2  tangentDir = vec2(-radialDir.y, radialDir.x);
      float angle = atan(p.y, p.x);

      float pull = uZoom * 0.30 * (nd * nd);
      float rimStrength = smoothstep(uRimStart, 1.0, nd);
      float fluidWave = sin(angle * uRimFreq1) * 0.55 + sin(angle * uRimFreq2) * 0.25;
      float rScreen = (uSizeX + uSizeY) * 0.5;
      vec2  rimOff = tangentDir * fluidWave * rimStrength * rScreen * uRimTangential;

      vec2 baseUV = center + offset * (1.0 - pull) + rimOff;

      float rimMask = smoothstep(0.55, 1.0, nd);
      vec2  dispDir = offset * uDispersion * 0.004 * rimMask;
      int N = uSamples;
      if (N < 2) N = 2;
      if (N > MAX_SAMPLES) N = MAX_SAMPLES;
      vec3 col = vec3(0.0);
      vec3 caW = vec3(0.0);
      for (int i = 0; i < MAX_SAMPLES; i++) {
        if (i >= N) break;
        float t = float(i) / float(N - 1);
        vec2 sUV = baseUV + dispDir * (t - 0.5);
        vec3 s = texture2D(uTex, sUV).rgb;
        vec3 w = vec3(
          exp(-pow((t - 0.00) / 0.38, 2.0)),
          exp(-pow((t - 0.50) / 0.38, 2.0)),
          exp(-pow((t - 1.00) / 0.38, 2.0))
        );
        col += s * w;
        caW += w;
      }
      col /= max(caW, vec3(0.001));

      col *= mix(0.91, 1.0, smoothstep(0.0, 0.38, shapeND));

      float r2 = shapeND * shapeND * 0.25;
      float gs = max(uNovaSize * uGlow * 0.003, 0.004);
      float nova = exp(-r2 / gs) + exp(-r2 / (gs * 7.0)) * 0.18;
      nova *= uWhiteGlow * (uGlow / 17.0) * 1.15;
      col += vec3(nova);

      float dC = shapeND * 0.5;
      float tR = clamp(uRingRadius, 0.1, 0.49);
      float rW = max(uRingWidth, 0.003);
      float ring = exp(-pow((dC - tR) / rW, 2.0));
      ring *= uBlueRing * (uGlow / 17.0) * 1.8;
      if (uShimmer > 0.5) ring *= sin(angle * uShimmerFreq + uTime * uShimmerSpeed) * uShimmerDepth + (1.0 - uShimmerDepth);
      float ringAura = exp(-pow((dC - tR) / (rW * 6.0), 2.0)) * 0.28 * uBlueRing * (uGlow / 17.0);
      col += uBlueColor * (ring + ringAura);
      col += vec3(exp(-pow((dC - uRimLinePos) / max(uRimLineWidth, 0.0001), 2.0)) * uRimLine);

      outA = smoothstep(1.0, 0.93, maskND);
      return col;
    }

    void main(){
      vec3 base = texture2D(uTex, vUv).rgb;
      vec3 outc = base;

      float a = 0.0;
      vec3 c = discLens(uCenter, uAspect, a);
      outc = mix(outc, c, a);

      if (uVignette > 0.001) {
        vec2 vc = vUv - 0.5;
        vc.x *= uAspect;
        float d = length(vc) / max(uVignetteSize, 0.0001);
        float vig = 1.0 - uVignette * smoothstep(0.5, 1.0, d);
        outc *= clamp(vig, 0.0, 1.0);
      }

      gl_FragColor = vec4(outc, 1.0);
    }
  `;

  function backgroundOf(element) {
    let node = element;
    while (node && node !== document.documentElement) {
      const color = getComputedStyle(node).backgroundColor;
      if (color && color !== "transparent" && color !== "rgba(0, 0, 0, 0)") {
        return color;
      }
      node = node.parentElement;
    }
    return "#f3eee7";
  }

  function setupInstance(wrapper) {
    const mount = wrapper.querySelector("[data-glass-carousel-canvas]");
    const items = Array.from(
      wrapper.querySelectorAll("[data-glass-carousel-item]")
    );
    if (!mount || !items.length) return null;
    if (wrapper.getAttribute("data-glass-carousel") === "canvas") return null;

    const counterEl = wrapper.querySelector("[data-glass-carousel-counter]");
    const captionTitleTrack = wrapper.querySelector("[data-caption-title]");
    const captionSubTrack = wrapper.querySelector("[data-caption-sub]");
    const odoRoot = wrapper.querySelector("[data-odo-current]");
    const odoTotal = wrapper.querySelector("[data-odo-total]");
    const odometer = odoRoot ? new OdometerCounter(odoRoot, { digits: 2 }) : null;
    const prevBtn = wrapper.querySelector("[data-glass-carousel-prev]");
    const nextBtn = wrapper.querySelector("[data-glass-carousel-next]");
    const total = items.length;
    let W = Math.max(1, mount.clientWidth);
    let H = Math.max(1, mount.clientHeight);
    let narrow = W < narrowBreakpoint;
    let panelH = 0;
    let renderer;

    try {
      renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: false,
        canvas: mount,
      });
    } catch {
      return null;
    }

    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
    renderer.setSize(W, H, false);
    renderer.setClearColor(new THREE.Color(backgroundOf(wrapper)), 1);

    const scene = new THREE.Scene();
    const camera = new THREE.OrthographicCamera(
      -W / 2,
      W / 2,
      H / 2,
      -H / 2,
      -100,
      100
    );
    camera.position.z = 10;

    const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
    const sources = items.map((item) => ({
      tex: null,
      aspect: 1,
      image:
        item.querySelector("[data-glass-carousel-image]") ||
        item.querySelector("img"),
    }));

    function fitToPanel(image) {
      const cap = Math.round(
        panelH * Math.min(window.devicePixelRatio || 1, 2) * textureDetail
      );
      const height = image.naturalHeight || image.height;
      const width = image.naturalWidth || image.width;
      if (!height || height <= cap) return image;
      const scaled = document.createElement("canvas");
      scaled.width = Math.max(1, Math.round(width * (cap / height)));
      scaled.height = cap;
      scaled.getContext("2d").drawImage(image, 0, 0, scaled.width, scaled.height);
      return scaled;
    }

    function adoptTexture(source) {
      const image = source.image;
      if (!image || !image.naturalWidth || !image.naturalHeight) return;
      if (source.tex) return;
      source.aspect = image.naturalWidth / image.naturalHeight;
      measurePanel();

      const tex = new THREE.Texture(fitToPanel(image));
      tex.minFilter = THREE.LinearMipmapLinearFilter;
      tex.magFilter = THREE.LinearFilter;
      tex.generateMipmaps = true;
      tex.anisotropy = maxAnisotropy;
      tex.colorSpace = THREE.SRGBColorSpace;
      tex.needsUpdate = true;
      source.tex = tex;
      measurePanel();
      recomputeTotal();
      if (!userInteracted) {
        scroll = centerForIndex(0);
        target = scroll;
      }
    }

    function bindTextures() {
      sources.forEach((source) => {
        if (!source.image) return;
        const img = source.image;
        if (img.complete && img.naturalWidth) {
          adoptTexture(source);
          return;
        }
        img.addEventListener("load", () => adoptTexture(source), { once: true });
        img.addEventListener(
          "error",
          () => {
            console.warn("Glass Carousel: image failed to load", img.src);
          },
          { once: true }
        );
      });
    }

    function measurePanel() {
      let widest = 1;
      for (let i = 0; i < sources.length; i++) {
        widest = Math.max(widest, sources[i].aspect);
      }
      panelH = Math.min(H * panelHeight, (W * panelWidthMax) / widest);
    }

    measurePanel();

    function slotWidth(index) {
      return sources[index].aspect * panelH + panelGap;
    }

    let offsets = [];
    let totalWidth = 0;

    function recomputeTotal() {
      offsets = [];
      let acc = 0;
      for (let i = 0; i < sources.length; i++) {
        offsets.push(acc);
        acc += slotWidth(i);
      }
      totalWidth = acc;
    }

    recomputeTotal();

    function slotCenter(index) {
      return offsets[index] + slotWidth(index) / 2 - panelGap / 2;
    }

    function centerForIndex(index) {
      const loop = Math.floor(index / total);
      const s = ((index % total) + total) % total;
      return slotCenter(s) + loop * totalWidth;
    }

    function nearestIndex(value) {
      if (!totalWidth) return 0;
      let best = 0;
      let bestDist = Infinity;
      for (let i = 0; i < total; i++) {
        const center = slotCenter(i);
        const k = Math.round((value - center) / totalWidth);
        const dist = Math.abs(center + k * totalWidth - value);
        if (dist < bestDist) {
          bestDist = dist;
          best = i + k * total;
        }
      }
      return best;
    }

    function sourceIndex(value) {
      return ((nearestIndex(value) % total) + total) % total;
    }

    const panelGeometry = new THREE.PlaneGeometry(1, 1);
    const pool = [];
    for (let r = 0; r < repeats; r++) {
      for (let i = 0; i < total; i++) {
        const mat = new THREE.MeshBasicMaterial({
          color: 0xdddddd,
          transparent: true,
        });
        const mesh = new THREE.Mesh(panelGeometry, mat);
        mesh.visible = false;
        scene.add(mesh);
        pool.push({ mesh, mat, srcIndex: i, bound: false });
      }
    }

    let scroll = centerForIndex(0);
    let target = scroll;
    let userInteracted = false;
    let velocity = 0;
    let prevScroll = 0;
    let scrollEnergy = 0;
    let lastInput = performance.now();
    let snapped = false;
    let lastCenter = -1;

    const dpr = renderer.getPixelRatio() || 1;
    const bufferW = () => Math.max(1, Math.round(W * dpr));
    const bufferH = () => Math.max(1, Math.round(H * dpr));
    let rt = new THREE.WebGLRenderTarget(bufferW(), bufferH());
    const lensScene = new THREE.Scene();
    const lensCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
    const lensUniforms = {
      uTex: { value: rt.texture },
      uRes: { value: new THREE.Vector2(bufferW(), bufferH()) },
      uCenter: { value: new THREE.Vector2(0.5, 0.5) },
      uSizeX: { value: (narrow ? lensWidthNarrow : lensWidth) * (W / H) },
      uSizeY: { value: (narrow ? lensHeightNarrow : lensHeight) * (W / H) },
      uRotation: { value: 0 },
      uAspect: { value: W / H },
      uZoom: { value: lensZoom },
      uDispersion: { value: lensDispersion },
      uGlow: { value: lensGlow },
      uWhiteGlow: { value: lensNova },
      uNovaSize: { value: lensNovaSize },
      uBlueRing: { value: lensRing },
      uRingRadius: { value: lensRingRadius },
      uRingWidth: { value: lensRingWidth },
      uShimmer: { value: lensShimmer ? 1 : 0 },
      uShimmerFreq: { value: lensShimmerFreq },
      uShimmerSpeed: { value: lensShimmerSpeed },
      uShimmerDepth: { value: lensShimmerDepth },
      uTime: { value: 0 },
      uRimStart: { value: lensRimStart },
      uRimTangential: { value: lensRimWave },
      uRimFreq1: { value: lensRimFreq1 },
      uRimFreq2: { value: lensRimFreq2 },
      uBlueColor: { value: new THREE.Color(lensColor) },
      uRimLine: { value: lensRimLine },
      uRimLinePos: { value: lensRimLinePos },
      uRimLineWidth: { value: lensRimLineWidth },
      uVignette: { value: lensVignette },
      uVignetteSize: { value: lensVignetteSize },
      uSamples: { value: lensSamples },
    };

    const lensMat = new THREE.ShaderMaterial({
      uniforms: lensUniforms,
      vertexShader,
      fragmentShader,
    });
    const lensQuad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), lensMat);
    lensScene.add(lensQuad);

    function applyLook() {
      renderer.setClearColor(new THREE.Color(backgroundOf(wrapper)), 1);
      const active = sourceIndex(scroll);
      measurePanel();
      recomputeTotal();
      scroll = centerForIndex(active);
      target = scroll;
      lensUniforms.uAspect.value = W / H;
      lensUniforms.uSizeX.value =
        (narrow ? lensWidthNarrow : lensWidth) * (W / H);
      lensUniforms.uSizeY.value =
        (narrow ? lensHeightNarrow : lensHeight) * (W / H);
      lensUniforms.uRotation.value =
        ((narrow ? lensRotationNarrow : lensRotation) * Math.PI) / 180;
    }

    let panelRects = [];
    let centeredPanel = null;

    function layout() {
      panelRects = [];
      centeredPanel = null;
      let centeredDist = Infinity;
      const half = W / 2;
      const buffer = panelH;

      pool.forEach((p, poolIdx) => {
        const rep = Math.floor(poolIdx / total);
        const i = p.srcIndex;
        const src = sources[i];
        let x = slotCenter(i) - scroll;
        x = ((x % totalWidth) + totalWidth) % totalWidth;
        x += (rep - Math.floor(repeats / 2)) * totalWidth;
        if (x > half + totalWidth) x -= totalWidth * repeats;
        const centerX = x;

        if (centerX < -half - buffer || centerX > half + buffer) {
          p.mesh.visible = false;
          return;
        }

        const shrink = 1 - 0.25 * scrollEnergy;
        const h = panelH * shrink;
        const w = src.aspect * panelH * shrink;

        if (src.tex && !p.bound) {
          p.mat.map = src.tex;
          p.mat.color.set(0xffffff);
          p.mat.needsUpdate = true;
          p.bound = true;
        }

        p.mesh.visible = true;
        p.mesh.position.set(centerX, 0, 0);
        p.mesh.scale.set(w, h, 1);

        const sx = centerX + W / 2;
        const sy = H / 2;
        panelRects.push({
          left: sx - w / 2,
          right: sx + w / 2,
          top: sy - h / 2,
          bottom: sy + h / 2,
          poolIdx,
          srcIndex: i,
          centerX,
        });

        if (Math.abs(centerX) < centeredDist) {
          centeredDist = Math.abs(centerX);
          centeredPanel = { srcIndex: i, poolIdx };
        }
      });
    }

    const el = mount;
    wrapper.setAttribute("data-glass-carousel-wheel", wheel);

    function rectOf(px, py) {
      const bounds = el.getBoundingClientRect();
      const x = px - bounds.left;
      const y = py - bounds.top;
      for (let i = 0; i < panelRects.length; i++) {
        const r = panelRects[i];
        if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r;
      }
      return null;
    }

    let dragging = false;
    let dragPointerId = null;
    let dragLastX = 0;
    let dragDist = 0;
    let dragVel = 0;
    let dragMoveT = 0;
    let suppressClick = false;
    let dragPointerType = "mouse";
    let lastPointerX = NaN;
    let lastPointerY = NaN;
    let pointerInside = false;
    let lastPointerType = "mouse";
    let pointerState = "";

    function setPointer(v) {
      if (v === pointerState) return;
      pointerState = v;
      wrapper.setAttribute("data-glass-carousel-pointer", v);
    }

    function setHover(on) {
      setPointer(dragging ? "grabbing" : on ? "grab" : "");
    }

    function refreshHover() {
      if (!pointerInside || lastPointerType !== "mouse") return;
      if (!Number.isFinite(lastPointerX)) return;
      setHover(rectOf(lastPointerX, lastPointerY) !== null);
    }

    function onWheel(e) {
      if (wheel === "off") return;
      const sideways = Math.abs(e.deltaX) > Math.abs(e.deltaY);
      if (wheel === "horizontal" && !sideways) return;
      e.preventDefault();
      userInteracted = true;
      target += (sideways ? e.deltaX : e.deltaY) * wheelSpeed;
      lastInput = performance.now();
      snapped = false;
    }

    function onPointerDown(e) {
      suppressClick = false;
      if (dragging) return;
      if (e.button !== 0 && e.pointerType === "mouse") return;
      dragging = true;
      dragPointerId = e.pointerId;
      dragPointerType = e.pointerType || "mouse";
      try {
        el.setPointerCapture(e.pointerId);
      } catch {}
      dragLastX = e.clientX;
      lastPointerX = e.clientX;
      lastPointerY = e.clientY;
      dragDist = 0;
      dragVel = 0;
      dragMoveT = performance.now();
      setHover(false);
      velocity = 0;
      userInteracted = true;
      snapped = false;
      lastInput = dragMoveT;
    }

    function onPointerMove(e) {
      if (dragging && e.pointerId === dragPointerId) {
        const sens = dragPointerType === "mouse" ? dragSpeed : touchSpeed;
        const dx = e.clientX - dragLastX;
        dragLastX = e.clientX;
        dragDist += Math.abs(dx);
        target -= dx * sens;
        dragVel = dragVel * 0.6 + -dx * sens * 0.4;
        dragMoveT = performance.now();
        lastInput = dragMoveT;
        snapped = false;
      }
      lastPointerX = e.clientX;
      lastPointerY = e.clientY;
      lastPointerType = e.pointerType || "mouse";
      pointerInside = true;
      if (e.pointerType !== "mouse") return;
      setHover(rectOf(e.clientX, e.clientY) !== null);
    }

    function onPointerUp(e) {
      if (!dragging) return;
      if (e && dragPointerId !== null && e.pointerId !== dragPointerId) return;
      dragging = false;
      if (dragPointerId !== null) {
        try {
          el.releasePointerCapture(dragPointerId);
        } catch {}
        dragPointerId = null;
      }
      velocity =
        performance.now() - dragMoveT > flickIdle ? 0 : dragVel;
      dragVel = 0;
      lastInput = performance.now();
      snapped = false;
      suppressClick =
        dragDist >
        (dragPointerType === "mouse" ? clickSlop : touchClickSlop);
      if (dragPointerType === "mouse") {
        setHover(rectOf(lastPointerX, lastPointerY) !== null);
      } else {
        setHover(false);
      }
    }

    function onEnter(e) {
      pointerInside = true;
      lastPointerType = e.pointerType || "mouse";
    }

    function onLeave() {
      pointerInside = false;
      setHover(false);
    }

    function onClick(e) {
      if (suppressClick) {
        suppressClick = false;
        return;
      }
      const hit = rectOf(e.clientX, e.clientY);
      if (!hit) return;
      if (centeredPanel && hit.poolIdx === centeredPanel.poolIdx) {
        followLink(hit.srcIndex, e);
        return;
      }
      userInteracted = true;
      velocity = 0;
      target = centerForIndex(nearestIndex(scroll + hit.centerX));
      snapped = true;
      setHover(false);
    }

    function goBy(step) {
      userInteracted = true;
      velocity = 0;
      target = centerForIndex(nearestIndex(scroll) + step);
      snapped = true;
      lastInput = performance.now();
    }

    function onPrev(e) {
      e.preventDefault();
      e.stopPropagation();
      goBy(-1);
    }

    function onNext(e) {
      e.preventDefault();
      e.stopPropagation();
      goBy(1);
    }

    function followLink(srcIndex, e) {
      const item = items[srcIndex];
      const link =
        item &&
        (item.matches("a[href]") ? item : item.querySelector("a[href]"));
      if (!link) return;
      if (e.metaKey || e.ctrlKey || e.shiftKey || link.target === "_blank") {
        window.open(link.href, link.target || "_blank", "noopener");
        return;
      }
      link.click();
    }

    function showActive(index, direction = 1, animate = true) {
      if (odoTotal) {
        odoTotal.textContent = String(total).padStart(2, "0");
      }
      if (odometer) {
        odometer.set(index + 1, { animate, direction });
      } else if (counterEl) {
        counterEl.textContent = `${String(index + 1).padStart(2, "0")}/${String(total).padStart(2, "0")}`;
      }

      const content = items[index].querySelector(
        "[data-glass-carousel-content]"
      );
      if (!content) return;
      const lines = content.querySelectorAll(".glass-carousel__item-p");
      const title = lines[0] ? lines[0].textContent : "";
      const sub = lines[1] ? lines[1].textContent : "";

      if (!animate) {
        if (captionTitleTrack) {
          captionTitleTrack.replaceChildren();
          blurCaptionLine(captionTitleTrack, title, false, direction, 0);
        }
        if (captionSubTrack) {
          captionSubTrack.replaceChildren();
          blurCaptionLine(captionSubTrack, sub, true, direction, 0);
        }
        return;
      }

      blurCaptionLine(captionTitleTrack, title, false, direction, 0);
      blurCaptionLine(captionSubTrack, sub, true, direction, 0.06);
    }

    el.addEventListener("wheel", onWheel, { passive: false });
    el.addEventListener("pointerdown", onPointerDown);
    el.addEventListener("pointermove", onPointerMove);
    el.addEventListener("pointerup", onPointerUp);
    el.addEventListener("pointercancel", onPointerUp);
    el.addEventListener("pointerenter", onEnter);
    el.addEventListener("pointerleave", onLeave);
    el.addEventListener("click", onClick);
    if (prevBtn) prevBtn.addEventListener("click", onPrev);
    if (nextBtn) nextBtn.addEventListener("click", onNext);

    let raf = 0;

    function tick() {
      if (!wrapper.isConnected) return destroy();
      if (!dragging) {
        target += velocity;
        velocity *= friction;
        if (Math.abs(velocity) < 0.05) velocity = 0;
        if (!snapped && performance.now() - lastInput > snapIdle) {
          target = centerForIndex(nearestIndex(scroll));
          snapped = true;
        }
      }

      const follow =
        dragging && dragPointerType !== "mouse"
          ? touchEase
          : snapped
            ? snapEase
            : ease;
      scroll += (target - scroll) * follow;

      const ci = sourceIndex(scroll);
      if (ci !== lastCenter) {
        let direction = 1;
        if (lastCenter >= 0) {
          const forward = (ci - lastCenter + total) % total;
          const backward = (lastCenter - ci + total) % total;
          direction = forward <= backward ? 1 : -1;
        }
        lastCenter = ci;
        showActive(ci, direction, true);
      }

      const rawSpeed = scroll - prevScroll;
      prevScroll = scroll;
      const norm = Math.min(1, Math.abs(rawSpeed) / Math.max(1, shrinkMax));
      const k = norm > scrollEnergy ? shrinkAttack : shrinkDecay;
      scrollEnergy += (norm - scrollEnergy) * k;

      layout();
      refreshHover();
      lensUniforms.uTime.value = performance.now() * 0.001;
      renderer.setRenderTarget(rt);
      renderer.render(scene, camera);
      renderer.setRenderTarget(null);
      renderer.render(lensScene, lensCam);

      raf = requestAnimationFrame(tick);
    }

    function onResize() {
      const nextW = mount.clientWidth;
      const nextH = mount.clientHeight;
      if (!nextW || !nextH) return;
      if (nextW === W && nextH === H) return;
      W = nextW;
      H = nextH;
      renderer.setSize(W, H, false);
      camera.left = -W / 2;
      camera.right = W / 2;
      camera.top = H / 2;
      camera.bottom = -H / 2;
      camera.updateProjectionMatrix();
      rt.setSize(bufferW(), bufferH());
      lensUniforms.uRes.value.set(bufferW(), bufferH());
      narrow = W < narrowBreakpoint;
      applyLook();
    }

    const resizeObserver = new ResizeObserver(onResize);
    resizeObserver.observe(mount);
    wrapper.setAttribute("data-glass-carousel", "canvas");
    bindTextures();
    applyLook();
    showActive(0, 1, false);
    lastCenter = 0;
    tick();

    function destroy() {
      cancelAnimationFrame(raf);
      resizeObserver.disconnect();
      el.removeEventListener("wheel", onWheel);
      el.removeEventListener("pointerdown", onPointerDown);
      el.removeEventListener("pointermove", onPointerMove);
      el.removeEventListener("pointerup", onPointerUp);
      el.removeEventListener("pointercancel", onPointerUp);
      el.removeEventListener("pointerenter", onEnter);
      el.removeEventListener("pointerleave", onLeave);
      el.removeEventListener("click", onClick);
      if (prevBtn) prevBtn.removeEventListener("click", onPrev);
      if (nextBtn) nextBtn.removeEventListener("click", onNext);
      renderer.dispose();
      rt.dispose();
      lensQuad.geometry.dispose();
      lensMat.dispose();
      panelGeometry.dispose();
      pool.forEach((p) => p.mat.dispose());
      sources.forEach((s) => {
        if (s.tex) s.tex.dispose();
      });
      wrapper.setAttribute("data-glass-carousel", "");
    }

    return { destroy };
  }

  const instances = [];
  const mm = gsap.matchMedia();
  mm.add("(prefers-reduced-motion: no-preference)", () => {
    wrappers.forEach((wrapper) => {
      const instance = setupInstance(wrapper);
      if (instance) instances.push(instance);
    });
    return () => {
      instances.forEach((instance) => instance.destroy());
      instances.length = 0;
    };
  });
}

document.addEventListener("DOMContentLoaded", () => {
  initLiquidGlassCarousel();
});
````
