# Scale Slider

> A geometric scale conveyor of images with drag, autoplay, and keyboard control — distinctive carousel for product and editorial hero strips.

**Definition:** Scale Slider 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-03
- Canonical page: https://html.bosconvertpro.com/collection/scale-slider
- Full-page preview: https://html.bosconvertpro.com/collection/scale-slider/full
- Live demo: https://html.bosconvertpro.com/demos/scale-slider/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/scale-slider/index.md
- License: https://html.bosconvertpro.com/terms

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

## FAQ
### What is Scale Slider?

Scale Slider is a sliders UI animation from collections.html. A geometric scale conveyor of images with drag, autoplay, and keyboard control — distinctive carousel for product and editorial hero strips.

### What is Scale Slider built with?

Scale Slider 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 Scale Slider in my project?

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

### Is Scale Slider free to use?

Yes. The Scale Slider 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>Scale Slider</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=Inter:wght@400;500;600&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <section class="scroll-cover" aria-hidden="true">
      <div class="scroll-cover__hint">
        <span class="scroll-cover__text">Scroll</span>
        <div class="scroll-cover__line"></div>
      </div>
    </section>

    <section class="stage" id="animation-section">
      <div
        class="scale-slider"
        data-scale-slider
        role="group"
        aria-label="Image slider"
        aria-roledescription="carousel"
        tabindex="0"
      >
        <div class="scale-slider__scene" data-scale-slider-scene>
          <div class="scale-slider__card" data-scale-slider-card>
            <img
              class="scale-slider__img"
              src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=1600&q=85"
              alt="Portrait of a woman with soft natural light"
              draggable="false"
              loading="eager"
            />
          </div>
          <div class="scale-slider__card" data-scale-slider-card>
            <img
              class="scale-slider__img"
              src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=1600&q=85"
              alt="Portrait of a man looking toward the camera"
              draggable="false"
              loading="lazy"
            />
          </div>
          <div class="scale-slider__card" data-scale-slider-card>
            <img
              class="scale-slider__img"
              src="https://images.unsplash.com/photo-1529626455594-4ff0802cfb7e?auto=format&fit=crop&w=1600&q=85"
              alt="Fashion portrait with warm golden tones"
              draggable="false"
              loading="lazy"
            />
          </div>
          <div class="scale-slider__card" data-scale-slider-card>
            <img
              class="scale-slider__img"
              src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=1600&q=85"
              alt="Close-up portrait with soft smile"
              draggable="false"
              loading="lazy"
            />
          </div>
          <div class="scale-slider__card" data-scale-slider-card>
            <img
              class="scale-slider__img"
              src="https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?auto=format&fit=crop&w=1600&q=85"
              alt="Editorial fashion portrait"
              draggable="false"
              loading="lazy"
            />
          </div>
          <div class="scale-slider__card" data-scale-slider-card>
            <img
              class="scale-slider__img"
              src="https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&w=1600&q=85"
              alt="Portrait against a textured wall"
              draggable="false"
              loading="lazy"
            />
          </div>
          <div class="scale-slider__card" data-scale-slider-card>
            <img
              class="scale-slider__img"
              src="https://images.unsplash.com/photo-1488426862026-3ee34a7d66df?auto=format&fit=crop&w=1600&q=85"
              alt="Portrait with dramatic side lighting"
              draggable="false"
              loading="lazy"
            />
          </div>
          <div class="scale-slider__card" data-scale-slider-card>
            <img
              class="scale-slider__img"
              src="https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=1600&q=85"
              alt="Studio fashion portrait"
              draggable="false"
              loading="lazy"
            />
          </div>
          <div class="scale-slider__card" data-scale-slider-card>
            <img
              class="scale-slider__img"
              src="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=1600&q=85"
              alt="Portrait of a man in soft daylight"
              draggable="false"
              loading="lazy"
            />
          </div>
          <div class="scale-slider__card" data-scale-slider-card>
            <img
              class="scale-slider__img"
              src="https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&w=1600&q=85"
              alt="Portrait with natural outdoor light"
              draggable="false"
              loading="lazy"
            />
          </div>
          <div class="scale-slider__card" data-scale-slider-card>
            <img
              class="scale-slider__img"
              src="https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?auto=format&fit=crop&w=1600&q=85"
              alt="Portrait with clean background"
              draggable="false"
              loading="lazy"
            />
          </div>
          <div class="scale-slider__card" data-scale-slider-card>
            <img
              class="scale-slider__img"
              src="https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?auto=format&fit=crop&w=1600&q=85"
              alt="Street-style portrait in soft focus"
              draggable="false"
              loading="lazy"
            />
          </div>
        </div>

        <!-- 3D liquid glass control pin -->
        <div class="glass-pin-wrap">
          <div class="glass-pin" role="group" aria-label="Slide controls">
            <div class="glass-pin__shine" aria-hidden="true"></div>
            <button
              class="glass-pin__btn"
              data-scale-slider-prev
              type="button"
              aria-label="Previous"
            >
              <svg class="glass-pin__icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
                <path
                  d="M10 3L5 8L10 13"
                  stroke="currentColor"
                  stroke-width="1.5"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
              </svg>
            </button>
            <button
              class="glass-pin__btn"
              data-scale-slider-next
              type="button"
              aria-label="Next"
            >
              <svg class="glass-pin__icon" viewBox="0 0 16 16" fill="none" aria-hidden="true">
                <path
                  d="M6 3L11 8L6 13"
                  stroke="currentColor"
                  stroke-width="1.5"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                />
              </svg>
            </button>
          </div>
        </div>
      </div>
    </section>

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

## Source — styles.css
````css
/* ==========================================================================
   Scale Slider — premium light + 3D liquid glass pin
   ========================================================================== */

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

:root {
  /* Premium light palette — ivory, pearl, soft champagne */
  --bg-base: #f4f1ec;
  --bg-soft: #faf8f5;
  --bg-mid: #ebe6de;
  --bg-warm: #f0e8dc;
  --bg-glow: rgba(212, 180, 140, 0.22);
  --bg-glow-cool: rgba(160, 180, 200, 0.18);
  --bg-glow-rose: rgba(220, 190, 190, 0.14);
  --ink: #1c1916;
  --ink-muted: rgba(28, 25, 22, 0.38);
  --accent: #b8956a;
  --focus: #a67c52;

  --font: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
    sans-serif;
}

html {
  scroll-behavior: smooth;
}

body {
  font-family: var(--font);
  color: var(--ink);
  background:
    radial-gradient(100% 80% at 15% 0%, var(--bg-glow) 0%, transparent 50%),
    radial-gradient(80% 60% at 90% 90%, var(--bg-glow-cool) 0%, transparent 48%),
    radial-gradient(60% 50% at 50% 50%, var(--bg-glow-rose) 0%, transparent 55%),
    linear-gradient(165deg, var(--bg-soft) 0%, var(--bg-base) 42%, var(--bg-mid) 100%);
  background-color: var(--bg-base);
  background-attachment: fixed;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* --------------------------------------------------------------------------
   Scroll cover
   -------------------------------------------------------------------------- */

.scroll-cover {
  position: relative;
  min-height: 100vh;
}

.scroll-cover__hint {
  position: absolute;
  bottom: 1.5rem;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  pointer-events: none;
}

.scroll-cover__text {
  font-size: 0.6875rem;
  font-weight: 500;
  color: var(--ink-muted);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.scroll-cover__line {
  width: 1px;
  height: 2rem;
  background: rgba(28, 25, 22, 0.12);
  position: relative;
  overflow: hidden;
}

.scroll-cover__line::after {
  content: "";
  position: absolute;
  top: -100%;
  left: 0;
  width: 100%;
  height: 100%;
  background: linear-gradient(
    180deg,
    transparent,
    var(--accent),
    transparent
  );
  animation: scrollHint 1.6s ease-in-out infinite;
}

@keyframes scrollHint {
  0% {
    top: -100%;
  }
  100% {
    top: 100%;
  }
}

/* --------------------------------------------------------------------------
   Stage
   -------------------------------------------------------------------------- */

.stage {
  min-height: 100vh;
  width: 100%;
  padding: 0;
}

/* --------------------------------------------------------------------------
   Scale Slider
   -------------------------------------------------------------------------- */

.scale-slider {
  position: relative;
  width: 100%;
  height: 100vh;
  overflow: hidden;
  touch-action: pan-y;
  outline: none;
  cursor: grab;
  /* Light premium field */
  background:
    radial-gradient(
      100% 80% at 50% 0%,
      rgba(255, 255, 255, 0.7) 0%,
      transparent 55%
    ),
    radial-gradient(
      90% 70% at 20% 100%,
      rgba(212, 180, 140, 0.16) 0%,
      transparent 50%
    ),
    radial-gradient(
      70% 55% at 85% 30%,
      rgba(170, 190, 210, 0.14) 0%,
      transparent 48%
    ),
    linear-gradient(180deg, #faf8f5 0%, #f2ede6 45%, #e8e2d8 100%);
}

.scale-slider:active {
  cursor: grabbing;
}

.scale-slider:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}

.scale-slider__scene {
  position: absolute;
  inset: 0;
}

.scale-slider__card {
  position: absolute;
  left: 0;
  bottom: 0;
  display: none;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border-radius: 4px;
  transform-origin: 0 100%;
  will-change: transform;
  backface-visibility: hidden;
  box-shadow:
    0 18px 48px rgba(40, 30, 20, 0.16),
    0 6px 16px rgba(40, 30, 20, 0.1),
    0 0 0 1px rgba(255, 255, 255, 0.35);
}

.scale-slider__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 22%;
  transform: scale(1);
  transform-origin: center;
  will-change: transform;
  backface-visibility: hidden;
  user-select: none;
  -webkit-user-select: none;
  -webkit-user-drag: none;
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   3D Liquid glass pin (controls)
   -------------------------------------------------------------------------- */

/* Wrapper holds outer shadow so the pill can safely clip its glass layers */
.glass-pin-wrap {
  position: absolute;
  left: 50%;
  bottom: 40px;
  z-index: 20;
  transform: translateX(-50%);
  /* Soft outer drop + contact shadow (never clipped by the pill) */
  filter: drop-shadow(0 14px 28px rgba(50, 40, 30, 0.14))
    drop-shadow(0 4px 10px rgba(50, 40, 30, 0.1));
}

.glass-pin {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px;
  border-radius: 999px;
  isolation: isolate;
  /* Clip shine / refraction so nothing spills past the pill */
  overflow: hidden;
  /* Helps Safari clip backdrop + children to radius */
  -webkit-mask-image: -webkit-radial-gradient(white, black);
  mask-image: radial-gradient(white, black);

  /* Frosted glass body */
  background:
    linear-gradient(
      155deg,
      rgba(255, 255, 255, 0.72) 0%,
      rgba(255, 255, 255, 0.38) 38%,
      rgba(255, 255, 255, 0.18) 72%,
      rgba(245, 240, 232, 0.28) 100%
    );
  border: 1px solid rgba(255, 255, 255, 0.75);
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.35),
    inset 0 1.5px 1px rgba(255, 255, 255, 0.95),
    inset 0 -2px 4px rgba(120, 100, 80, 0.1),
    inset 0 0 20px rgba(255, 255, 255, 0.35);

  -webkit-backdrop-filter: blur(28px) saturate(1.85) brightness(1.12);
  backdrop-filter: blur(28px) saturate(1.85) brightness(1.12);
}

/* Specular highlight band — liquid sheen (clipped by overflow) */
.glass-pin::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(
    112deg,
    rgba(255, 255, 255, 0.75) 0%,
    rgba(255, 255, 255, 0.28) 18%,
    rgba(255, 255, 255, 0.04) 36%,
    transparent 52%,
    rgba(200, 175, 145, 0.08) 100%
  );
  z-index: 0;
}

/* Bottom refraction + caustic tint (clipped) */
.glass-pin::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 50%;
  border-radius: 0 0 999px 999px;
  pointer-events: none;
  background:
    radial-gradient(
      80% 100% at 50% 100%,
      rgba(201, 168, 124, 0.2) 0%,
      transparent 70%
    ),
    linear-gradient(
      180deg,
      transparent 0%,
      rgba(160, 180, 200, 0.08) 100%
    );
  z-index: 0;
}

/* Specular blob — stays inside the pill */
.glass-pin__shine {
  position: absolute;
  top: -30%;
  left: -8%;
  width: 55%;
  height: 90%;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(
    ellipse at 40% 40%,
    rgba(255, 255, 255, 0.7) 0%,
    rgba(255, 255, 255, 0.18) 38%,
    transparent 68%
  );
  filter: blur(2px);
  z-index: 0;
  mix-blend-mode: soft-light;
  animation: glassShimmer 5.5s ease-in-out infinite;
}

@keyframes glassShimmer {
  0%,
  100% {
    transform: translate(0, 0) scale(1);
    opacity: 0.65;
  }
  50% {
    transform: translate(10%, 6%) scale(1.06);
    opacity: 1;
  }
}

/* 3D glass buttons */
.glass-pin__btn {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex: 0 0 auto;
  padding: 0;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.7);
  border-radius: 999px;
  background:
    linear-gradient(
      165deg,
      rgba(255, 255, 255, 0.85) 0%,
      rgba(255, 255, 255, 0.35) 42%,
      rgba(255, 255, 255, 0.15) 100%
    );
  color: rgba(40, 32, 24, 0.78);
  cursor: pointer;
  box-shadow:
    0 6px 14px rgba(50, 40, 30, 0.1),
    0 2px 4px rgba(50, 40, 30, 0.06),
    inset 0 1.5px 0 rgba(255, 255, 255, 0.95),
    inset 0 -2px 3px rgba(120, 100, 80, 0.12),
    inset 0 0 12px rgba(255, 255, 255, 0.4);
  -webkit-backdrop-filter: blur(12px) saturate(1.4);
  backdrop-filter: blur(12px) saturate(1.4);
  transition:
    background 0.3s ease,
    border-color 0.3s ease,
    box-shadow 0.3s ease,
    transform 0.25s cubic-bezier(0.22, 1, 0.36, 1),
    color 0.25s ease;
}

/* Button top facet highlight — clipped by button overflow */
.glass-pin__btn::before {
  content: "";
  position: absolute;
  top: 2px;
  left: 18%;
  right: 18%;
  height: 38%;
  border-radius: 999px;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.9) 0%,
    rgba(255, 255, 255, 0.15) 100%
  );
  opacity: 0.85;
}

.glass-pin__btn:hover {
  background:
    linear-gradient(
      165deg,
      rgba(255, 255, 255, 0.95) 0%,
      rgba(255, 255, 255, 0.5) 45%,
      rgba(255, 248, 240, 0.28) 100%
    );
  border-color: rgba(255, 255, 255, 0.95);
  color: rgba(28, 22, 16, 0.9);
  box-shadow:
    0 8px 18px rgba(50, 40, 30, 0.12),
    0 3px 6px rgba(50, 40, 30, 0.08),
    0 0 0 1px rgba(184, 149, 106, 0.18),
    inset 0 1.5px 0 rgba(255, 255, 255, 1),
    inset 0 -2px 3px rgba(120, 100, 80, 0.1),
    inset 0 0 16px rgba(255, 255, 255, 0.5);
  transform: scale(1.05);
}

.glass-pin__btn:active {
  transform: scale(0.96);
  box-shadow:
    0 2px 6px rgba(50, 40, 30, 0.1),
    inset 0 2px 4px rgba(80, 60, 40, 0.12),
    inset 0 1px 0 rgba(255, 255, 255, 0.7);
}

.glass-pin__btn:focus-visible {
  outline: 2px solid rgba(166, 124, 82, 0.85);
  outline-offset: 2px;
}

.glass-pin__icon {
  position: relative;
  z-index: 1;
  display: block;
  width: 16px;
  height: 16px;
  filter: drop-shadow(0 1px 0 rgba(255, 255, 255, 0.5));
}

/* --------------------------------------------------------------------------
   Responsive
   -------------------------------------------------------------------------- */

@media (max-width: 767px) {
  .scale-slider {
    height: 100svh;
  }

  .glass-pin-wrap {
    bottom: 24px;
  }

  .glass-pin {
    padding: 7px;
    gap: 8px;
  }

  .glass-pin__btn {
    width: 40px;
    height: 40px;
  }
}

/* Reduced motion: still usable, calmer glass */
@media (prefers-reduced-motion: reduce) {
  .scroll-cover__line::after,
  .glass-pin__shine {
    animation: none;
  }

  html {
    scroll-behavior: auto;
  }
}
````

## Source — script.js
````javascript
/**
 * Scale Slider
 * Continuous geometric scale conveyor — drag, autoplay, arrows, keyboard.
 * Requires GSAP + Draggable (+ optional InertiaPlugin, CustomEase).
 */
(function () {
  "use strict";

  const EASE_NAME = "scaleSliderInOut";

  function waitForGsap(cb, attempt) {
    attempt = attempt || 0;
    if (typeof gsap !== "undefined") {
      cb();
      return;
    }
    if (attempt < 150) {
      setTimeout(function () {
        waitForGsap(cb, attempt + 1);
      }, 50);
      return;
    }
    console.error("[Scale Slider] GSAP failed to load");
  }

  function registerPlugins() {
    const plugins = [];
    if (typeof CustomEase !== "undefined") plugins.push(CustomEase);
    if (typeof Draggable !== "undefined") plugins.push(Draggable);
    if (typeof InertiaPlugin !== "undefined") plugins.push(InertiaPlugin);
    if (plugins.length) gsap.registerPlugin.apply(gsap, plugins);

    if (typeof CustomEase !== "undefined") {
      CustomEase.create("scaleSlider", "M0,0 C0.3,0.9 0.1,1 1,1");
      CustomEase.create(EASE_NAME, "M0,0 C0.7,0 0.16,1 1,1");
    }
  }

  /**
   * @param {HTMLElement} root
   */
  function createScaleSlider(root) {
    const scene = root.querySelector("[data-scale-slider-scene]");
    if (!scene) return null;

    const cards = Array.prototype.slice
      .call(scene.querySelectorAll("[data-scale-slider-card]"))
      .map(function (el) {
        return { el: el, img: el.querySelector("img") };
      })
      .filter(function (c) {
        return c.img;
      });

    const count = cards.length;
    if (count < 3) return null;

    const prevBtn = root.querySelector("[data-scale-slider-prev]");
    const nextBtn = root.querySelector("[data-scale-slider-next]");

    // Config (data attributes with sensible defaults)
    const direction =
      root.dataset.direction === "ltr" ? "ltr" : "rtl";
    const countMode = root.dataset.count || "auto";
    const minScale = Math.min(
      0.8,
      Math.max(0.05, parseFloat(root.dataset.minScale || "0.3"))
    );
    const autoplaySpeed = parseFloat(root.dataset.autoplay || "0.0016");
    const dragFactor = parseFloat(root.dataset.drag || "0.0012");
    const scrollFactor = parseFloat(root.dataset.scroll || "0.0002");
    const focusY = parseFloat(root.dataset.focusY || "0.22");
    const radius = Math.max(0, parseFloat(root.dataset.radius || "4"));
    const parallax = Math.max(0, parseFloat(root.dataset.parallax || "0.08"));
    const dirSign = direction === "ltr" ? -1 : 1;
    const reducedMotion = window.matchMedia(
      "(prefers-reduced-motion: reduce)"
    ).matches;

    // Layout state
    let sceneW = 1;
    let sceneH = 1;
    let cardSize = 100;
    let visibleCount = 5;
    let ratio = 1.35;
    let extent = 1;
    let offset = 0;

    // Progress state
    let progress = 0;
    let dragDelta = 0;
    let lastScrollY = window.scrollY || 0;
    let scrollMomentum = 0;
    let active = true;
    let tween = null;
    let resizeTimer = null;
    let dragProxy = null;

    const ease =
      typeof CustomEase !== "undefined" ? EASE_NAME : "power3.inOut";

    // ---- Layout helpers ----------------------------------------------------

    function sizeAt(slot) {
      // Geometric scale along the conveyor
      return offset + extent * Math.pow(ratio, progress - Math.floor(progress) - slot);
    }

    function pickVisibleCount() {
      if (countMode !== "auto") {
        return Math.max(3, Math.min(count - 1, parseInt(countMode, 10) || 5));
      }
      const aspect = sceneW / sceneH;
      if (aspect >= 2.2) return 6;
      if (aspect >= 1.5) return 5;
      if (aspect >= 1) return 4;
      return 3;
    }

    function layout() {
      const w = scene.clientWidth;
      const h = scene.clientHeight;
      if (!w || !h) return;

      sceneW = w;
      sceneH = h;
      visibleCount = pickVisibleCount();
      ratio = Math.pow(1 / minScale, 1 / (visibleCount - 1));
      extent = sceneW / (1 - minScale);
      offset = -minScale * extent;
      cardSize = (extent * (ratio - 1) + 1.5) * (1 + parallax);

      const sizePx = cardSize.toFixed(1) + "px";
      for (let i = 0; i < count; i++) {
        cards[i].el.style.width = sizePx;
        cards[i].el.style.height = sizePx;
      }
      render();
    }

    function render() {
      const base = Math.floor(progress);

      for (let i = 0; i < count; i++) {
        const card = cards[i];
        const slot = ((i - base) % count + count) % count;
        const s0 = sizeAt(slot);
        const s1 = sizeAt(slot + 1);
        const span = Math.max(s0 - s1, 1) + 1.5;
        const x =
          direction === "ltr" ? sceneW - s0 : s1 - 1.5;
        const scale = span / cardSize;

        card.el.style.zIndex = String(visibleCount - slot);
        card.el.style.transform =
          "translate3d(" +
          x.toFixed(1) +
          "px,0,0) scale(" +
          scale.toFixed(5) +
          ")";

        if (parallax > 0 && slot < visibleCount) {
          const centerX = x + span / 2;
          let t =
            direction === "ltr"
              ? (sceneW - centerX) / sceneW
              : centerX / sceneW;
          t = t < 0 ? 0 : t > 1 ? 1 : t;
          const imgScale = 1 + parallax * t;
          card.img.style.transform = "scale(" + imgScale.toFixed(4) + ")";
        }
      }
    }

    // ---- Motion ------------------------------------------------------------

    function killTween() {
      if (!tween) return;
      if (tween === demoTl) {
        demoTl.pause();
      } else {
        tween.kill();
      }
      tween = null;
    }

    function animateTo(target) {
      killTween();
      const proxy = { t: progress };
      const duration = Math.min(1.1, 0.45 + 0.12 * Math.abs(target - progress));

      tween = gsap.to(proxy, {
        t: target,
        duration: duration,
        ease: ease,
        onUpdate: function () {
          progress = proxy.t;
          render();
        },
        onComplete: function () {
          tween = null;
        },
      });
    }

    function nearestLoopIndex(index) {
      const wrapped = ((index % count) + count) % count;
      return wrapped + count * Math.round((progress - wrapped) / count);
    }

    // Progress decreases as the conveyor advances (matches continuous autoplay).
    function stepForward() {
      animateTo(Math.round(progress) - 1);
    }

    function stepBack() {
      animateTo(Math.round(progress) + 1);
    }

    function goTo(index) {
      animateTo(nearestLoopIndex(index));
    }

    // Continuous autoplay + scroll + drag accumulation
    function onTick() {
      if (!active) return;

      const y = window.scrollY || 0;
      const dy = y - lastScrollY;
      lastScrollY = y;

      if (tween) {
        dragDelta = 0;
        return;
      }

      scrollMomentum = 0.9 * scrollMomentum + Math.abs(dy) * scrollFactor;
      progress +=
        dragDelta * dragFactor * dirSign -
        (reducedMotion ? 0 : autoplaySpeed) -
        scrollMomentum;
      dragDelta = 0;
      render();
    }

    // Demo timeline (for external attract / preview tools)
    const demoProxy = { t: 0 };
    const demoTl = gsap.timeline({
      paused: true,
      onComplete: function () {
        if (tween === demoTl) tween = null;
      },
      onInterrupt: function () {
        if (tween === demoTl) tween = null;
      },
    });

    function buildDemo() {
      function sync() {
        if (tween !== demoTl && demoTl.isActive()) tween = demoTl;
        progress = demoProxy.t;
        render();
      }

      demoTl.call(
        function () {
          demoProxy.t = progress;
          if (tween && tween !== demoTl) tween.kill();
          tween = demoTl;
        },
        null,
        0
      );

      demoTl.to(
        demoProxy,
        {
          t: function () {
            return Math.round(demoProxy.t) - 1;
          },
          duration: 0.6,
          ease: ease,
          onUpdate: sync,
          data: { label: "Conveyor advances one card" },
        },
        0.01
      );

      demoTl.to(
        demoProxy,
        {
          t: "+=0",
          duration: 0.7,
          ease: "none",
          data: { label: "Holds on the featured card" },
        }
      );

      demoTl.to(
        demoProxy,
        {
          t: function () {
            return Math.round(demoProxy.t) - 1;
          },
          duration: 0.6,
          ease: ease,
          onUpdate: sync,
          data: { label: "Advances to the next card" },
        }
      );
    }

    // ---- Events ------------------------------------------------------------

    function onKeydown(e) {
      if (e.key === "ArrowLeft") {
        e.preventDefault();
        // Matches conveyor: ltr left = reverse, rtl left = advance
        direction === "ltr" ? stepBack() : stepForward();
      } else if (e.key === "ArrowRight") {
        e.preventDefault();
        direction === "ltr" ? stepForward() : stepBack();
      }
    }

    function onVisibility() {
      active = !document.hidden;
    }

    // ---- Init --------------------------------------------------------------

    buildDemo();

    if (!root.getAttribute("tabindex")) {
      root.setAttribute("tabindex", "0");
    }

    root.addEventListener("keydown", onKeydown);
    // Prev reverses the conveyor; Next advances (same direction as autoplay).
    if (prevBtn) prevBtn.addEventListener("click", stepBack);
    if (nextBtn) nextBtn.addEventListener("click", stepForward);

    const ro = new ResizeObserver(function () {
      clearTimeout(resizeTimer);
      resizeTimer = setTimeout(layout, 150);
    });
    ro.observe(scene);
    document.addEventListener("visibilitychange", onVisibility);

    const objectPos = "50% " + (100 * focusY).toFixed(1) + "%";
    for (let i = 0; i < count; i++) {
      cards[i].img.style.objectPosition = objectPos;
      if (radius > 0) cards[i].el.style.borderRadius = radius + "px";
    }

    layout();

    for (let i = 0; i < count; i++) {
      cards[i].el.style.display = "block";
    }

    // Drag
    if (typeof Draggable !== "undefined") {
      const proxyEl = document.createElement("div");
      dragProxy = Draggable.create(proxyEl, {
        type: "x",
        trigger: scene,
        inertia: typeof InertiaPlugin !== "undefined",
        allowContextMenu: true,
        dragClickables: true,
        onDragStart: function () {
          killTween();
        },
        onDrag: function () {
          dragDelta += this.deltaX;
        },
        onThrowUpdate: function () {
          dragDelta += this.deltaX;
        },
      })[0];
    }

    gsap.ticker.add(onTick);
    gsap.ticker.lagSmoothing(0);

    return {
      destroy: function () {
        active = false;
        gsap.ticker.remove(onTick);
        clearTimeout(resizeTimer);
        demoTl.kill();
        if (tween && tween !== demoTl) tween.kill();
        if (dragProxy) dragProxy.kill();
        ro.disconnect();
        document.removeEventListener("visibilitychange", onVisibility);
        root.removeEventListener("keydown", onKeydown);
        if (prevBtn) prevBtn.removeEventListener("click", stepBack);
        if (nextBtn) nextBtn.removeEventListener("click", stepForward);
      },
      next: stepForward,
      prev: stepBack,
      goTo: goTo,
      demo: function () {
        if (tween && tween !== demoTl) return null;
        demoTl.restart(true, false);
        return demoTl;
      },
    };
  }

  // ---- Boot ----------------------------------------------------------------

  waitForGsap(function () {
    registerPlugins();

    const instances = [];
    document.querySelectorAll("[data-scale-slider]").forEach(function (el) {
      const api = createScaleSlider(el);
      if (api) instances.push(api);
    });

    window.ScaleSlider = {
      destroy: function () {
        instances.forEach(function (i) {
          i.destroy();
        });
      },
      next: function () {
        instances.forEach(function (i) {
          i.next();
        });
      },
      prev: function () {
        instances.forEach(function (i) {
          i.prev();
        });
      },
      goTo: function (index) {
        instances.forEach(function (i) {
          i.goTo(index);
        });
      },
      demo: function () {
        return instances[0] ? instances[0].demo() : null;
      },
    };
  });
})();
````
