# Cursor with Marquee Effect

> A custom cursor that expands into a scrolling marquee pill on hover — playful pointer feedback for CTAs and labels.

**Definition:** Cursor with Marquee Effect is a hover 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: Hover
- Stack: HTML, CSS, JavaScript, and GSAP
- Added: 2026-08-03
- Canonical page: https://html.bosconvertpro.com/collection/cursor-with-marquee-effect
- Full-page preview: https://html.bosconvertpro.com/collection/cursor-with-marquee-effect/full
- Live demo: https://html.bosconvertpro.com/demos/cursor-with-marquee-effect/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/cursor-with-marquee-effect/index.md
- License: https://html.bosconvertpro.com/terms

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

## FAQ
### What is Cursor with Marquee Effect?

Cursor with Marquee Effect is a hover UI animation from collections.html. A custom cursor that expands into a scrolling marquee pill on hover — playful pointer feedback for CTAs and labels.

### What is Cursor with Marquee Effect built with?

Cursor with Marquee Effect 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 Cursor with Marquee Effect in my project?

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

### Is Cursor with Marquee Effect free to use?

Yes. The Cursor with Marquee Effect 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" />
    <meta
      name="description"
      content="Cursor with Marquee Effect — custom cursor that expands into a scrolling marquee pill on hover (GSAP)"
    />
    <title>Cursor with Marquee Effect</title>

    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link rel="preconnect" href="https://cdn.jsdelivr.net" />
    <link rel="preconnect" href="https://cdn.prod.website-files.com" />

    <link
      href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;0,600;1,500&family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600&display=swap"
      rel="stylesheet"
    />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/lenis@1.2.3/dist/lenis.css"
    />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body class="body is--cursor">
    <!-- Custom cursor: follows pointer, expands into marquee text on hover targets -->
    <div data-cursor-marquee-status="" class="cursor-marquee" aria-hidden="true">
      <div class="cursor-marquee__card">
        <span data-cursor-marquee-text-target="" class="cursor-marquee__text-span"
          >Hello world</span
        >
        <span
          data-cursor-marquee-text-target=""
          class="cursor-marquee__text-span is--duplicate"
          >Hello world</span
        >
      </div>
    </div>

    <header class="page-header">
      <div class="page-header__copy">
        <p class="page-header__eyebrow">Signature collection</p>
        <h1 class="page-header__title">Tropical <em>bar menu</em></h1>
      </div>
      <p class="page-header__hint">Hover a card · marquee cursor</p>
    </header>

    <main class="demo-grid">
      <div class="demo-grid__list">
        <a data-cursor-marquee-text="Mojito" href="#" class="demo-grid-card">
          <div class="demo-grid-card__img">
            <div class="demo-grid-card__img-before"></div>
            <img
              class="demo-grid-card__cover-img"
              src="https://cdn.prod.website-files.com/68ac357de306d9051427968d/68ac47e798a14dae2a3228bc_Colorful%20Beverage%20Display.avif"
              alt="Mojito"
              loading="lazy"
              draggable="false"
            />
          </div>
          <div class="demo-grid-card__content">
            <div class="demo-grid-card__meta">
              <span class="demo-grid-card__label">Classic</span>
              <h2 class="demo-grid-card__text">Mojito</h2>
            </div>
            <p class="demo-grid-card__text is--price">€10</p>
          </div>
        </a>

        <a data-cursor-marquee-text="Floaty" href="#" class="demo-grid-card">
          <div class="demo-grid-card__img">
            <div class="demo-grid-card__img-before"></div>
            <img
              class="demo-grid-card__cover-img"
              src="https://cdn.prod.website-files.com/68ac357de306d9051427968d/68ac48bc7cbc26b40b8847d0_Tropical%20Beach%20Scene.avif"
              alt="Floaty"
              loading="lazy"
              draggable="false"
            />
          </div>
          <div class="demo-grid-card__content">
            <div class="demo-grid-card__meta">
              <span class="demo-grid-card__label">House special</span>
              <h2 class="demo-grid-card__text">Floaty</h2>
            </div>
            <p class="demo-grid-card__text is--price">€25</p>
          </div>
        </a>

        <a data-cursor-marquee-text="No cap" href="#" class="demo-grid-card">
          <div class="demo-grid-card__img">
            <div class="demo-grid-card__img-before"></div>
            <img
              class="demo-grid-card__cover-img"
              src="https://cdn.prod.website-files.com/68ac357de306d9051427968d/68ac48bc7cbc26b40b8847b0_Floating%20Cap%20and%20Flowers.avif"
              alt="No cap"
              loading="lazy"
              draggable="false"
            />
          </div>
          <div class="demo-grid-card__content">
            <div class="demo-grid-card__meta">
              <span class="demo-grid-card__label">Signature</span>
              <h2 class="demo-grid-card__text">No cap</h2>
            </div>
            <p class="demo-grid-card__text is--price">€35</p>
          </div>
        </a>

        <a data-cursor-marquee-text="Pina colada" href="#" class="demo-grid-card">
          <div class="demo-grid-card__img">
            <div class="demo-grid-card__img-before"></div>
            <img
              class="demo-grid-card__cover-img"
              src="https://cdn.prod.website-files.com/68ac357de306d9051427968d/68ac48bc5209c8b81118aa41_Tropical%20Drink%20Scene.avif"
              alt="Pina colada"
              loading="lazy"
              draggable="false"
            />
          </div>
          <div class="demo-grid-card__content">
            <div class="demo-grid-card__meta">
              <span class="demo-grid-card__label">Creamy</span>
              <h2 class="demo-grid-card__text">Pina colada</h2>
            </div>
            <p class="demo-grid-card__text is--price">€12</p>
          </div>
        </a>

        <a data-cursor-marquee-text="Orange juice" href="#" class="demo-grid-card">
          <div class="demo-grid-card__img">
            <div class="demo-grid-card__img-before"></div>
            <img
              class="demo-grid-card__cover-img"
              src="https://cdn.prod.website-files.com/68ac357de306d9051427968d/68ac48bcf430da4defc75795_Man%20with%20Yellow%20Drink.avif"
              alt="Orange juice"
              loading="lazy"
              draggable="false"
            />
          </div>
          <div class="demo-grid-card__content">
            <div class="demo-grid-card__meta">
              <span class="demo-grid-card__label">Fresh</span>
              <h2 class="demo-grid-card__text">Orange juice</h2>
            </div>
            <p class="demo-grid-card__text is--price">€5</p>
          </div>
        </a>

        <a data-cursor-marquee-text="Mojito Mocktail" href="#" class="demo-grid-card">
          <div class="demo-grid-card__img">
            <div class="demo-grid-card__img-before"></div>
            <img
              class="demo-grid-card__cover-img"
              src="https://cdn.prod.website-files.com/68ac357de306d9051427968d/68ac48bcc0860ff14438d69a_Tropical%20Drinks%20on%20Bar.avif"
              alt="Mojito Mocktail"
              loading="lazy"
              draggable="false"
            />
          </div>
          <div class="demo-grid-card__content">
            <div class="demo-grid-card__meta">
              <span class="demo-grid-card__label">Zero proof</span>
              <h2 class="demo-grid-card__text">Mojito Mocktail</h2>
            </div>
            <p class="demo-grid-card__text is--price">€8</p>
          </div>
        </a>

        <a data-cursor-marquee-text="Reflection Mocktail" href="#" class="demo-grid-card">
          <div class="demo-grid-card__img">
            <div class="demo-grid-card__img-before"></div>
            <img
              class="demo-grid-card__cover-img"
              src="https://cdn.prod.website-files.com/68ac357de306d9051427968d/68ac48bc85b333944374497c_Orange%20Cocktails%20in%20Water.avif"
              alt="Reflection Mocktail"
              loading="lazy"
              draggable="false"
            />
          </div>
          <div class="demo-grid-card__content">
            <div class="demo-grid-card__meta">
              <span class="demo-grid-card__label">Zero proof</span>
              <h2 class="demo-grid-card__text">Reflection Mocktail</h2>
            </div>
            <p class="demo-grid-card__text is--price">€12</p>
          </div>
        </a>
      </div>
    </main>

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

## Source — styles.css
````css
/* ==========================================================================
   Cursor with Marquee Effect — premium light theme
   ========================================================================== */

:root {
  --bg: #f4f1ec;
  --bg-soft: #ebe6de;
  --surface: #fffcf8;
  --ink: #1a1814;
  --ink-muted: #6b6560;
  --ink-soft: #9a948c;
  --line: rgba(26, 24, 20, 0.12);
  --line-strong: rgba(26, 24, 20, 0.28);
  --ease-premium: cubic-bezier(0.22, 1, 0.36, 1);
}

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

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

html.lenis,
html.lenis body {
  height: auto;
}

.lenis.lenis-smooth {
  scroll-behavior: auto !important;
}

.lenis.lenis-smooth [data-lenis-prevent] {
  overscroll-behavior: contain;
}

.lenis.lenis-stopped {
  overflow: hidden;
}

body {
  margin: 0;
  min-height: 100%;
  color: var(--ink);
  background-color: var(--bg);
  font-family: "DM Sans", system-ui, -apple-system, sans-serif;
  font-size: 16px;
  font-weight: 500;
  line-height: 1.45;
}

/* Soft ambient wash — warm light, not flat white */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -2;
  background:
    radial-gradient(ellipse 80% 60% at 15% 10%, rgba(255, 236, 210, 0.55), transparent 55%),
    radial-gradient(ellipse 70% 50% at 90% 20%, rgba(220, 232, 240, 0.45), transparent 50%),
    radial-gradient(ellipse 60% 40% at 50% 100%, rgba(235, 220, 200, 0.35), transparent 55%),
    var(--bg);
  pointer-events: none;
}

/* Subtle paper grain */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  opacity: 0.35;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
  mix-blend-mode: multiply;
}

/* Hide system cursor — custom marquee cursor takes over */
.body.is--cursor,
.body.is--cursor * {
  cursor: none !important;
}

img {
  max-width: 100%;
  display: block;
  user-select: none;
  -webkit-user-drag: none;
}

a {
  color: inherit;
  text-decoration: none;
}

h1,
h2,
h3,
p {
  margin: 0;
}

/* --------------------------------------------------------------------------
   Page header
   -------------------------------------------------------------------------- */

.page-header {
  max-width: 80em;
  margin: 0 auto;
  padding: 2.5rem 5vw 0;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem;
}

.page-header__eyebrow {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 0.5rem;
}

.page-header__title {
  font-family: "Cormorant Garamond", "Times New Roman", serif;
  font-size: clamp(2rem, 4.5vw, 3.25rem);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--ink);
}

.page-header__title em {
  font-style: italic;
  font-weight: 500;
  color: var(--ink-muted);
}

.page-header__hint {
  display: none;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--line);
}

@media (hover: hover) and (pointer: fine) {
  .page-header__hint {
    display: inline-flex;
  }
}

/* --------------------------------------------------------------------------
   Demo grid
   -------------------------------------------------------------------------- */

.demo-grid {
  display: flex;
  flex-flow: wrap;
  justify-content: center;
  align-items: flex-start;
  min-height: auto;
  padding: 3rem 5vw 8vw;
}

.demo-grid__list {
  display: flex;
  flex-flow: wrap;
  gap: clamp(1.25rem, 2.5vw, 2rem);
  width: 100%;
  max-width: 80em;
}

/* --------------------------------------------------------------------------
   Premium cards — sharp, bordered, white content panel
   Hover motion is driven by GSAP (see script.js) for buttery easing
   -------------------------------------------------------------------------- */

.demo-grid-card {
  display: flex;
  flex-flow: column;
  width: calc(50% - clamp(0.625rem, 1.25vw, 1rem));
  color: var(--ink);
  text-decoration: none;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 0;
  box-shadow: none;
  padding: 0;
  overflow: hidden;
  will-change: opacity, border-color;
  transition: border-color 0.7s var(--ease-premium);
}

.demo-grid-card.is-active {
  border-color: var(--line-strong);
  z-index: 1;
}

.demo-grid-card__img {
  position: relative;
  width: 100%;
  overflow: hidden;
  border-radius: 0;
  background: var(--bg-soft);
  border-bottom: 1px solid var(--line);
  transition: border-color 0.7s var(--ease-premium);
}

.demo-grid-card.is-active .demo-grid-card__img {
  border-bottom-color: var(--line-strong);
}

/* Soft light wash on image (GSAP fades this in) */
.demo-grid-card__img-overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    rgba(255, 252, 248, 0) 40%,
    rgba(26, 24, 20, 0.12) 100%
  );
  opacity: 0;
  will-change: opacity;
}

/* 3:4 aspect */
.demo-grid-card__img-before {
  padding-top: 125%;
}

.demo-grid-card__cover-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.001);
  will-change: transform, filter;
  filter: saturate(0.9) contrast(1.02) brightness(0.98);
  backface-visibility: hidden;
}

.demo-grid-card__content {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.25rem;
  padding: 1.15rem 1.15rem 1.2rem;
  background: #fff;
  will-change: transform;
}

.demo-grid-card__meta {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  min-width: 0;
}

.demo-grid-card__label {
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-soft);
  will-change: color, letter-spacing;
}

.demo-grid-card__text {
  color: currentColor;
  margin: 0;
  font-family: "Cormorant Garamond", "Times New Roman", serif;
  font-size: clamp(1.4em, 2.8vw, 1.9em);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.1;
  will-change: letter-spacing;
}

.demo-grid-card__text.is--price {
  --underline-scale: 0;
  position: relative;
  flex-shrink: 0;
  align-self: flex-end;
  font-family: "DM Sans", system-ui, sans-serif;
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  opacity: 1;
  color: var(--ink-muted);
  padding: 0 0 0.35rem;
  border-radius: 0;
  background: none;
  border: 0;
  line-height: 1;
  will-change: color;
}

/* Underline scale driven by GSAP via --underline-scale */
.demo-grid-card__text.is--price::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: var(--ink);
  transform: scaleX(var(--underline-scale));
  transform-origin: left center;
  will-change: transform;
}

/* --------------------------------------------------------------------------
   Custom cursor marquee (dark pill on light bg for contrast)
   -------------------------------------------------------------------------- */

.cursor-marquee {
  z-index: 100;
  pointer-events: none;
  justify-content: center;
  align-items: center;
  display: flex;
  position: fixed;
  top: 0;
  left: 0;
  will-change: transform;
}

.cursor-marquee__card {
  color: #faf8f5;
  background-color: #1a1814;
  justify-content: center;
  align-items: center;
  display: flex;
  position: absolute;
  overflow: hidden;
  transition: all 0.4s cubic-bezier(0.75, 0, 0.25, 1);
  transform: translateY(0%) rotate(0.001deg);
  clip-path: inset(calc(50% - 0.25em) round 50em);
  will-change: clip-path;
  opacity: 0;
  box-shadow:
    0 4px 16px rgba(26, 24, 20, 0.18),
    0 0 0 1px rgba(255, 255, 255, 0.06) inset;
}

[data-cursor-marquee-status="active"] .cursor-marquee__card {
  clip-path: inset(0 round 50em);
  transform: translateY(-25%) rotate(0.001deg);
}

[data-cursor-marquee-status="not-active"] .cursor-marquee__card,
[data-cursor-marquee-status="active"] .cursor-marquee__card {
  opacity: 1;
}

@keyframes translateXCursor {
  to {
    transform: translateX(-100%);
  }
}

.cursor-marquee__text-span {
  white-space: nowrap;
  transform-origin: 0;
  padding: 0.55em 0.65em;
  font-family: "DM Sans", system-ui, sans-serif;
  font-size: 1.15em;
  font-weight: 500;
  letter-spacing: 0.01em;
  line-height: 1;
  display: block;
  position: relative;
  animation: translateXCursor 10s linear infinite paused;
  transition: opacity 0.15s ease-in-out 0.25s;
  opacity: 0;
}

[data-cursor-marquee-status="active"] .cursor-marquee__text-span {
  transition: opacity 0.15s ease-in-out 0s;
  opacity: 1;
}

.cursor-marquee__text-span.is--duplicate {
  position: absolute;
  left: 100%;
}

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

@media screen and (max-width: 767px) {
  .page-header {
    flex-direction: column;
    align-items: flex-start;
    padding-top: 2rem;
  }

  .demo-grid {
    padding-top: 2rem;
  }

  .demo-grid-card {
    width: 100%;
  }

  .demo-grid-card__text {
    font-size: 1.5em;
  }
}

@media screen and (max-width: 479px) {
  .demo-grid-card__content {
    align-items: flex-start;
  }
}

/* Touch devices: show normal cursor */
@media (hover: none), (pointer: coarse) {
  .body.is--cursor,
  .body.is--cursor * {
    cursor: auto !important;
  }

  .cursor-marquee {
    display: none;
  }
}
````

## Source — script.js
````javascript
/**
 * Cursor with Marquee Effect
 *
 * - GSAP quickTo for smooth cursor follow
 * - Clip-path pill expands on [data-cursor-marquee-text] hover
 * - CSS marquee speed scales with text length
 */

function initCursorMarqueeEffect(lenis) {
  const hoverOutDelay = 0.4; // seconds before pausing marquee after leave
  const followDuration = 0.4; // seconds — how smoothly cursor follows pointer
  const speedMultiplier = 5; // higher = faster marquee (duration = length / multiplier)

  const cursor = document.querySelector("[data-cursor-marquee-status]");
  if (!cursor) return;

  const targets = cursor.querySelectorAll("[data-cursor-marquee-text-target]");

  // Center the fixed cursor on the pointer (GSAP owns transform; keep % offsets)
  gsap.set(cursor, { xPercent: -50, yPercent: -50 });

  const xTo = gsap.quickTo(cursor, "x", {
    duration: followDuration,
    ease: "power3",
  });
  const yTo = gsap.quickTo(cursor, "y", {
    duration: followDuration,
    ease: "power3",
  });

  let pauseTimeout = null;
  let activeEl = null;
  let lastX = 0;
  let lastY = 0;

  function playFor(el) {
    if (!el) return;
    if (pauseTimeout) clearTimeout(pauseTimeout);

    const text = el.getAttribute("data-cursor-marquee-text") || "";
    // Duration scales with string length so long labels still read well
    const sec = (text.length || 1) / speedMultiplier;

    targets.forEach((t) => {
      t.textContent = text;
      t.style.animationPlayState = "running";
      t.style.animationDuration = sec + "s";
    });

    cursor.setAttribute("data-cursor-marquee-status", "active");
    activeEl = el;
  }

  function pauseLater() {
    cursor.setAttribute("data-cursor-marquee-status", "not-active");

    if (pauseTimeout) clearTimeout(pauseTimeout);

    // Wait for card collapse transition, then pause CSS animation
    pauseTimeout = setTimeout(() => {
      targets.forEach((t) => {
        t.style.animationPlayState = "paused";
      });
    }, hoverOutDelay * 1000);

    activeEl = null;
  }

  function checkTarget() {
    const el = document.elementFromPoint(lastX, lastY);
    // Ignore the cursor itself (pointer-events: none) — hit test real page elements
    const hit = el && el.closest("[data-cursor-marquee-text]");

    if (hit !== activeEl) {
      if (activeEl) pauseLater();
      if (hit) playFor(hit);
    }
  }

  window.addEventListener(
    "pointermove",
    (e) => {
      lastX = e.clientX;
      lastY = e.clientY;
      xTo(lastX);
      yTo(lastY);
      checkTarget();
    },
    { passive: true }
  );

  // Re-check on scroll (element under cursor may change without pointermove)
  const onScroll = () => {
    xTo(lastX);
    yTo(lastY);
    checkTarget();
  };

  if (lenis) {
    lenis.on("scroll", onScroll);
  } else {
    window.addEventListener("scroll", onScroll, { passive: true });
  }

  // Brief delay so the first paint doesn’t flash the expanded state
  setTimeout(() => {
    cursor.setAttribute("data-cursor-marquee-status", "not-active");
  }, 500);
}

/**
 * Premium card hover — staggered, long-duration easing
 * Image ken-burns, content lift, price underline, sibling dim
 */
function initPremiumCardHover() {
  const list = document.querySelector(".demo-grid__list");
  if (!list || typeof gsap === "undefined") return;

  const cards = gsap.utils.toArray(".demo-grid-card");
  if (!cards.length) return;

  // Inject image overlays once (for soft gradient wash on hover)
  cards.forEach((card) => {
    const imgWrap = card.querySelector(".demo-grid-card__img");
    if (imgWrap && !imgWrap.querySelector(".demo-grid-card__img-overlay")) {
      const overlay = document.createElement("div");
      overlay.className = "demo-grid-card__img-overlay";
      overlay.setAttribute("aria-hidden", "true");
      imgWrap.appendChild(overlay);
    }
  });

  const prefersReduced = window.matchMedia(
    "(prefers-reduced-motion: reduce)"
  ).matches;

  // Skip fancy motion on touch / reduced-motion
  if (prefersReduced || window.matchMedia("(hover: none)").matches) {
    cards.forEach((card) => {
      card.addEventListener("click", (e) => e.preventDefault());
    });
    return;
  }

  const easeIn = "power3.out";
  const easeOut = "power3.inOut";

  cards.forEach((card) => {
    const img = card.querySelector(".demo-grid-card__cover-img");
    const overlay = card.querySelector(".demo-grid-card__img-overlay");
    const content = card.querySelector(".demo-grid-card__content");
    const label = card.querySelector(".demo-grid-card__label");
    const title = card.querySelector(".demo-grid-card__text:not(.is--price)");
    const price = card.querySelector(".demo-grid-card__text.is--price");

    // Underline proxy lives on the card so kill/restart stays continuous
    const state = { underline: 0 };

    gsap.set(img, {
      scale: 1.001,
      filter: "saturate(0.9) contrast(1.02) brightness(0.98)",
    });
    if (overlay) gsap.set(overlay, { opacity: 0 });
    if (content) gsap.set(content, { y: 0 });
    if (price) {
      gsap.set(price, { color: "#6b6560" });
      price.style.setProperty("--underline-scale", "0");
    }

    let hoverTl = null;

    function setUnderline(value) {
      state.underline = value;
      if (price) {
        price.style.setProperty("--underline-scale", String(value));
      }
    }

    function enter() {
      if (hoverTl) hoverTl.kill();

      card.classList.add("is-active");
      list.classList.add("is-hovering");

      // Dim siblings — put focus on the active card
      cards.forEach((sibling) => {
        gsap.to(sibling, {
          opacity: sibling === card ? 1 : 0.4,
          duration: sibling === card ? 0.7 : 0.9,
          ease: sibling === card ? easeIn : easeOut,
          overwrite: "auto",
        });
      });

      hoverTl = gsap.timeline({ defaults: { overwrite: "auto" } });

      if (img) {
        hoverTl.to(
          img,
          {
            scale: 1.065,
            filter: "saturate(1.06) contrast(1.04) brightness(1)",
            duration: 1.2,
            ease: easeIn,
          },
          0
        );
      }

      if (overlay) {
        hoverTl.to(overlay, { opacity: 1, duration: 0.9, ease: easeIn }, 0);
      }

      if (content) {
        hoverTl.to(content, { y: -5, duration: 0.8, ease: easeIn }, 0.04);
      }

      if (label) {
        hoverTl.to(
          label,
          {
            color: "#1a1814",
            letterSpacing: "0.2em",
            duration: 0.75,
            ease: easeIn,
          },
          0.06
        );
      }

      if (title) {
        hoverTl.to(
          title,
          { letterSpacing: "-0.005em", duration: 0.85, ease: easeIn },
          0.05
        );
      }

      if (price) {
        hoverTl.to(
          price,
          { color: "#1a1814", duration: 0.7, ease: easeIn },
          0.08
        );
        hoverTl.to(
          state,
          {
            underline: 1,
            duration: 0.75,
            ease: easeIn,
            onUpdate: () => setUnderline(state.underline),
          },
          0.1
        );
      }
    }

    function leave() {
      if (hoverTl) hoverTl.kill();

      card.classList.remove("is-active");

      if (!cards.some((c) => c.classList.contains("is-active"))) {
        list.classList.remove("is-hovering");
      }

      cards.forEach((sibling) => {
        gsap.to(sibling, {
          opacity: 1,
          duration: 0.95,
          ease: easeOut,
          overwrite: "auto",
        });
      });

      hoverTl = gsap.timeline({ defaults: { overwrite: "auto" } });

      if (img) {
        hoverTl.to(
          img,
          {
            scale: 1.001,
            filter: "saturate(0.9) contrast(1.02) brightness(0.98)",
            duration: 1.1,
            ease: easeOut,
          },
          0
        );
      }

      if (overlay) {
        hoverTl.to(overlay, { opacity: 0, duration: 0.75, ease: easeOut }, 0);
      }

      if (content) {
        hoverTl.to(content, { y: 0, duration: 0.75, ease: easeOut }, 0);
      }

      if (label) {
        hoverTl.to(
          label,
          {
            color: "#9a948c",
            letterSpacing: "0.16em",
            duration: 0.7,
            ease: easeOut,
          },
          0
        );
      }

      if (title) {
        hoverTl.to(
          title,
          { letterSpacing: "-0.02em", duration: 0.75, ease: easeOut },
          0
        );
      }

      if (price) {
        hoverTl.to(
          price,
          { color: "#6b6560", duration: 0.65, ease: easeOut },
          0
        );
        hoverTl.to(
          state,
          {
            underline: 0,
            duration: 0.6,
            ease: easeOut,
            onUpdate: () => setUnderline(state.underline),
          },
          0
        );
      }
    }

    card.addEventListener("pointerenter", enter);
    card.addEventListener("pointerleave", leave);
    card.addEventListener("click", (e) => e.preventDefault());
  });
}

/**
 * Buttery smooth page scroll via Lenis + GSAP ticker
 */
function initSmoothScroll() {
  if (typeof Lenis === "undefined") return null;

  const prefersReduced = window.matchMedia(
    "(prefers-reduced-motion: reduce)"
  ).matches;
  if (prefersReduced) return null;

  const lenis = new Lenis({
    duration: 1.4,
    easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
    orientation: "vertical",
    gestureOrientation: "vertical",
    smoothWheel: true,
    wheelMultiplier: 0.85,
    touchMultiplier: 1.35,
    infinite: false,
  });

  // Drive Lenis with GSAP's RAF for perfectly synced frames
  gsap.ticker.add((time) => {
    lenis.raf(time * 1000);
  });
  gsap.ticker.lagSmoothing(0);

  return lenis;
}

// Initialize when DOM is ready
document.addEventListener("DOMContentLoaded", function () {
  const lenis = initSmoothScroll();
  initCursorMarqueeEffect(lenis);
  initPremiumCardHover();

  if (lenis) window.__lenis = lenis;
});
````
