# Spotlight List Hover Cards — Handwritten Label Swap, Proximity Row Growth & Elastic Glass Cards

## Goal
Build a three-section page whose middle section is a dark, rounded **panel** with a centred list of six titles. When the pointer **enters the list**, four **glass-framed portrait cards** parked in the panel's corners **pop from scale 0 with an elastic ease**. When the pointer **hovers a title**: the bold uppercase title **squashes, blurs and fades out** while a **handwritten version scales in** (back-out ease); the hovered **row grows** by 50px and every other row **shrinks in proportion to its distance** (weights `1/|i−j|`), so the list breathes around the cursor; and the four cards **load that title's images** and **reshuffle** to new random offsets/rotations while following the hovered row's vertical position. Leaving the list scales the cards away and restores the rows. A soft light inside the panel follows the pointer. GSAP with `CustomEase`.

## Tech
Vanilla HTML/CSS/JS with ES module imports. `gsap` (npm) + **`CustomEase`**. Fonts: **Anton** (titles), **Caveat** 600 (handwritten), **DM Sans** 500 (body). No smooth-scroll library.

## Layout / HTML
```html
<div class="sl">
  <section class="sl-intro"><h1>The Sketchbook</h1></section>

  <section class="sl-spotlight">
    <div class="sl-panel">
      <p class="sl-panel__eyebrow">Explore the collection</p>
      <div class="sl-stage">
        <div class="sl-cards" aria-hidden="true">
          <img class="sl-card sl-card--1" data-rotation="-10" alt="" />
          <img class="sl-card sl-card--2" data-rotation="10" alt="" />
          <img class="sl-card sl-card--3" data-rotation="8" alt="" />
          <img class="sl-card sl-card--4" data-rotation="-8" alt="" />
        </div>
        <div class="sl-list">
          <!-- ×6 -->
          <a class="sl-item" href="#">
            <span class="sl-label">
              <span class="sl-label__default">Drawn and dressed</span>
              <span class="sl-label__alt">Drawn and dressed</span>
            </span>
          </a>
          …Sketch the look · Palette of style · Muse in ink · Runway reimagined · Bold brushstroke…
        </div>
      </div>
    </div>
  </section>

  <section class="sl-outro"><h1>Stay iconic</h1></section>
</div>
```

## Styling
`:root`: `--sl-paper:#f2f0eb`, `--sl-ink:#151618`, `--sl-panel:#101114`, `--sl-panel-ink:#f4f2ee`, `--sl-glow:#6f7cff`, `--sl-line:rgba(255,255,255,.22)`, `--sl-card-w:clamp(90px,15vw,210px)`.

- `.sl` — `background:var(--sl-paper); color:var(--sl-ink); font-family:"DM Sans"`. `h1` — Anton, `clamp(2.6rem,8vw,11rem)`, uppercase, `line-height:1`. Sections: `position:relative; min-height:100svh; padding:1.5rem; overflow:hidden`; intro/outro centre their `h1`.
- `.sl-panel` — `position:relative; min-height:calc(100svh - 3rem); padding:8rem 2rem; border-radius:2.5rem; background:var(--sl-panel); color:var(--sl-panel-ink); display:flex; flex-direction:column; justify-content:center; align-items:center; text-align:center; gap:2rem; overflow:hidden; isolation:isolate`. Its `::before` is the pointer light: `radial-gradient(38rem 38rem at var(--sl-mx,50%) var(--sl-my,50%), color-mix(in srgb, var(--sl-glow) 22%, transparent), transparent 70%)`, `opacity:0` → `1` with `.is-lit` (0.8s).
- `.sl-panel__eyebrow` — `.8rem`, weight 500, `letter-spacing:.14em`, uppercase, `opacity:.6`.
- `.sl-stage` — `position:relative; width:100%; display:flex; justify-content:center; align-items:center; z-index:1`. `.sl-list` — column, centred, `z-index:1`. `.sl-item` — `width:100%; display:flex; justify-content:center; align-items:center` (JS sets its height).
- `.sl-label` — `display:grid; place-items:center`; both spans share `grid-area:1/1; white-space:nowrap`. `.sl-label__default` — Anton `clamp(1.6rem,6vw,8rem)`, uppercase, `line-height:1`. `.sl-label__alt` — Caveat 600 `clamp(2.2rem,8vw,10.5rem)`, `line-height:.7; letter-spacing:-.03em; transform:translateY(12%); opacity:0`.
- `.sl-cards` — `position:absolute; inset:0; z-index:2; pointer-events:none`. `.sl-card` — `position:absolute; width:var(--sl-card-w); aspect-ratio:2/3; object-fit:cover; border-radius:1rem; border:1px solid var(--sl-line); padding:5px; background:rgba(255,255,255,.06); box-shadow:0 30px 60px -28px rgba(0,0,0,.8), inset 0 1px 0 rgba(255,255,255,.3); transform:scale(0)`. Corners: card-1 `top:7.5%; left:7.5%`, card-2 `top:5%; right:7.5%`, card-3 `bottom:12.5%; left:12.5%`, card-4 `bottom:15%; right:12.5%`.
- `@media (max-width:1000px)`: section padding 1rem, panel `padding:6rem 1rem; border-radius:1.5rem`.

## GSAP effect (be exact)
`gsap.registerPlugin(CustomEase)`; `CustomEase.create("sl-hop", "0.35, 0.75, 0, 1")`. Build in `mount(config)` → `destroy()`.

```js
const DEFAULTS = { hoverGrowth: 50, cardXRange: 100, cardYJitter: 50, cardRotationJitter: 20, cardFollow: .75, cardsDuration: 1, labelDuration: .25, entryBlur: 6 };
const HEADROOM = 10; // px added to the tallest row
```

**Row height baseline** — `measure()`: clear inline heights, `baseHeight = round(max(item.offsetHeight) + HEADROOM)`, `gsap.set(items, { height: baseHeight })`. Run at mount, on `resize`, and again after `document.fonts.ready`.

**Card init** — each card stores `baseRotation = parseFloat(dataset.rotation)`, loads `item_1_card_{n}.jpg`, and is set to `{ rotation: baseRotation, scale: 0, filter: "blur(6px)" }`.

**Label swap** — `animateLabel(el, { opacity, scaleX, scaleY, blur })` fires three overlapping tweens with `overwrite:"auto"`: `opacity` (duration `labelDuration*.4`, `delay:.025`, ease `sl-hop`), `scaleX/scaleY` (`labelDuration`, `ease:"back.out(1.75)"`), `filter: blur(px)` (`labelDuration`, `sl-hop`). On enter: default → `{0, .75, 1.25, 1}` and alt → `{1, 1, 1, 0}`; on leave, the inverse.

**Cards follow the row** — `placeCards(item, n)`: `followY = (itemCenterY − listCenterY) * cardFollow` (from `getBoundingClientRect`); each card gets `src = item_${n}_card_${i}.jpg` and tweens `{ x: random(−cardXRange, cardXRange), y: followY + random(−cardYJitter, cardYJitter), rotation: baseRotation + random(−cardRotationJitter, cardRotationJitter), duration: cardsDuration, ease: "elastic.out(1, 0.5)", overwrite: "auto" }`.

**List enter/leave** — `mouseenter` on `.sl-list`: cards `{ scale: 1, filter: "blur(0px)", duration: .75, ease: "elastic.out(1, 0.6)", overwrite: "scale,filter" }`. `mouseleave`: cards `{ scale: 0, filter: "blur(6px)", duration: .5, ease: "power3.out" }` and all rows `{ height: baseHeight, duration: .35, ease: "power3.out" }`.

**Row enter** (index `idx`) — swap labels, `placeCards(item, idx + 1)`, then `weights[i] = i === idx ? 0 : 1 / |i − idx|`, `total = Σ weights`; row `i` height → `idx ? baseHeight + hoverGrowth : round(baseHeight − hoverGrowth * weights[i] / total)`, `duration: .5, ease: "power2.out", overwrite: "auto"`. **Row leave** — swap labels back (heights are restored only when leaving the list).

**Panel light** — `pointermove` on the panel writes `--sl-mx/--sl-my` as percentages; `pointerenter/leave` toggle `.is-lit`.

**destroy()** — remove listeners, kill tweens, `clearProps:"all"` on cards, rows and label spans, remove `.is-lit` and the two variables.

## Assets / images
Twenty-four portrait photos (2:3 crop; ~600px wide is fine), four per list item, each set with a consistent mood (studio fashion, monochrome portraits, fabric in motion, soft editorial, street style, colour studio). File names `item_{1–6}_card_{1–4}.jpg`.

## Behavior notes
- Hover-only by design; on touch devices the list is a plain list and the cards never appear.
- Cards are `<img>` with padding and a translucent background, which is what draws the glass frame around the photo.
- Row heights are animated in px, so the list never reflows the page: total height is constant (growth on one row is paid for by the others).
- No `:root` rules beyond the prefixed variables; the panel is self-contained and can be dropped between any two sections.

## Images

This component ships with 24 reference assets, served publicly.
Use them as-is to reproduce the demo faithfully, then swap in your own — the layout expects the
same aspect ratios.

```
https://motionprompts.dev/c/spotlight-list-hover/item_1_card_1.jpg
https://motionprompts.dev/c/spotlight-list-hover/item_1_card_2.jpg
https://motionprompts.dev/c/spotlight-list-hover/item_1_card_3.jpg
https://motionprompts.dev/c/spotlight-list-hover/item_1_card_4.jpg
https://motionprompts.dev/c/spotlight-list-hover/item_2_card_1.jpg
https://motionprompts.dev/c/spotlight-list-hover/item_2_card_2.jpg
… 18 more under https://motionprompts.dev/c/spotlight-list-hover/
```

They are hotlinkable for prototyping. For anything you ship, replace them: they are licensed for
demonstration of this component, not for redistribution.

## Using this outside its demo page

This component is written as a complete page — that is how the demo is meant to look. If you are dropping it into an existing project, or combining it with other components, these are the things it declares at document level and that you need to move or reconcile first.

- **Palette on `:root`** — `--sl-paper`, `--sl-ink`, `--sl-panel`, `--sl-panel-ink`, `--sl-glow`, `--sl-line`, `--sl-card-w`. These names are not namespaced and they collide: `--ink` is defined by 164 of the 219 components in this catalogue, `--paper` by 94, `--muted` by 80, each with different values — and they will also collide with whatever your own project defines. Move them onto the component's wrapper (`.my-section { --ink: … }`) or rename them with a prefix.
- **Rules on `html, body`** — the demo owns the whole document, so these set the page background, typography and resets. Dropped into an existing project they restyle the entire page, not just this section. Re-target them at the component's wrapper before using it.
