# Inline Spot Hover Cards — Dots in a Headline that Open into Pointer-Tilting Glass Cards

## Goal
Build a centred, heavy condensed headline (six lines) where three of the lines contain an **inline square dot** between two words. Each dot is a folded **photo card**: on hover it **grows from 0.4em to an 18×14rem card**, its photo **fades in while sharpening from a blur**, an **iridescent glass edge** lights up, and a per-frame loop makes the card **drift toward the pointer** (clamped to a 25px radius) and **tilt on both axes** (clamped to ±20°) while the photo inside **moves the opposite way**, like looking through a lens. Leaving shrinks it back to a dot that gently "breathes". Desktop only (≥1000px). GSAP core.

## Tech
Vanilla HTML/CSS/JS with ES module imports. `gsap` (npm), no plugins. Font: **Barlow Condensed** 900.

## Layout / HTML
```html
<div class="is">
  <section class="is-stage">
    <h2 class="is-headline">
      <span class="is-line">We frame the</span>
      <span class="is-line">
        <span class="is-word">Worlds</span>
        <span class="is-spot"><span class="is-spot__card"><img src="…" alt="" /></span></span>
        <span class="is-word">reality</span>
      </span>
      <span class="is-line">Was too</span>
      <span class="is-line"><span class="is-word">Small</span><span class="is-spot">…</span><span class="is-word">to</span></span>
      <span class="is-line">Ever hold</span>
      <span class="is-line"><span class="is-word">On</span><span class="is-spot">…</span><span class="is-word">to</span></span>
    </h2>
    <p class="is-hint">Hover the dots</p>
  </section>
</div>
```

## Styling
`:root`: `--is-paper:#eef0ea`, `--is-ink:#121316`, `--is-line:rgba(255,255,255,.5)`, `--is-card-w:18rem`, `--is-card-h:14rem`, `--is-hue:0deg` (register with `@property` as an `<angle>`).

- `.is` — `min-height:100svh; background:var(--is-paper); color:var(--is-ink); font-family:"Barlow Condensed"`. `.is-stage` — `min-height:100svh; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2rem; overflow:hidden; padding:2rem 1rem`.
- `.is-headline` — `text-transform:uppercase; font-size:clamp(3rem,9vw,8.5rem); font-weight:900; line-height:.82; letter-spacing:-.02em`. `.is-line` — `display:flex; align-items:center; justify-content:center; white-space:nowrap`.
- `.is-spot` — `position:relative; flex-shrink:0; width:.4em; height:.4em; margin:0 .2em; transform:translateY(.05em); perspective:800px; cursor:pointer`.
- `.is-spot__card` — `position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:.4em; height:.4em; border-radius:.04em; background:var(--is-ink); overflow:hidden; animation: is-breathe 2.6s ease-in-out infinite` where `is-breathe` pulses a `box-shadow: 0 0 0 0 rgba(18,19,22,.28)` → `0 0 0 .18em rgba(18,19,22,0)`. The animation is removed on hover / `.is-open`.
- `.is-spot__card img` — `position:absolute; inset:0; object-fit:cover; pointer-events:none; transform:scale(1.4); opacity:0; filter:blur(10px)`.
- `.is-spot__card::before` — iridescent 1.5px ring: `conic-gradient(from var(--is-hue), rgba(255,255,255,.15), rgba(178,196,255,.85) 80deg, rgba(255,184,226,.7) 160deg, rgba(255,255,255,.15) 220deg, rgba(160,255,228,.8) 300deg, rgba(255,255,255,.15))`, masked to the border with `mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite: exclude`, `opacity:0` → `1` under `.is-spot.is-open` (0.5s), `z-index:2`. `::after` — a glass sheen `linear-gradient(160deg, rgba(255,255,255,.28), transparent 45%)`, same opacity toggle, `z-index:1`.
- `.is-hint` — `.8rem`, 500, `letter-spacing:.16em`, uppercase, `opacity:.45`.
- `@media (max-width:999px)`: `.is-spot { cursor:default; pointer-events:none }`, hide the hint. `prefers-reduced-motion`: no breathing.

## GSAP effect (be exact)
Build in `mount(config)` → `destroy()`.

```js
const DEFAULTS = { tiltMax: 20, driftMax: 25, smoothing: .075, openDuration: .75, closeDuration: .5, cardWidth: 18, cardHeight: 14 };
const OPEN = { width: "18rem", height: "14rem", borderRadius: ".6rem" };   // from cardWidth/cardHeight
const DOT  = { width: ".4em", height: ".4em", borderRadius: ".04em" };
const CENTER = { x: 0, y: 0, rotateX: 0, rotateY: 0, xPercent: -50, yPercent: -50 };
```

- At mount start a slow hue rotation: `gsap.to(root, { "--is-hue": "360deg", duration: 8, ease: "none", repeat: -1 })`.
- Per spot keep `live = {x,y,tx,ty}` and `aim = {x,y,tx,ty}`, `hovering = false`, `frame = null`.

**Tracking loop** (added to `gsap.ticker` while hovering): each tick `live.k += (aim.k − live.k) * smoothing` for the four values; `gsap.set(card, { x: live.x, y: live.y, rotateX: live.tx, rotateY: live.ty })`; `gsap.set(image, { x: −live.x, y: −live.y })`.

**mouseenter** (desktop only) — `hovering = true`, add `.is-open`, zero `live`/`aim`, `gsap.set(card, CENTER)`, `gsap.set(image, { x:0, y:0 })`, start the loop, then `gsap.to(card, { ...OPEN, duration: openDuration, ease: "power3.out", overwrite: "auto" })` and `gsap.to(image, { opacity: 1, scale: 1.08, filter: "blur(0px)", duration: openDuration * .8, ease: "power2.out", overwrite: "auto" })`.

**mousemove** — from the spot's centre `(cx, cy)`: `dx, dy = client − centre`; if `hypot > driftMax` scale both down to the radius → `aim.x/aim.y`. Tilt from the **card's** current bounds: `aim.ty = clamp((clientX − cx) / (cardW/2)) * −tiltMax`, `aim.tx = clamp((clientY − cy) / (cardH/2)) * tiltMax` (clamp to ±1).

**mouseleave** — `hovering = false`, remove `.is-open`, zero the tilt aims, remove the ticker callback, `gsap.to(card, { ...DOT, x:0, y:0, rotateX:0, rotateY:0, duration: closeDuration, ease: "power3.out", overwrite:"auto", onComplete })` where `onComplete` (if not re-hovered) does `gsap.set(card, { clearProps: "width,height,borderRadius", ...CENTER })` and resets the image; plus `gsap.to(image, { opacity: 0, scale: 1.4, filter: "blur(10px)", duration: closeDuration * .5, ease: "power2.in" })`.

**destroy()** — remove listeners and any ticker callbacks, kill the hue tween and card/image tweens, `clearProps:"all"`, remove `.is-open` and `--is-hue`.

## Assets / images
Three landscape photos (≈3:2, ~600px) with a clear subject in the centre — a forest, a building, a sky — since only the middle of each is visible in the 18×14rem card and the crop shifts a little with the pointer.

## Behavior notes
- Everything about the dot's size is in `em`, so it scales with the headline; the open card is in `rem`, so it is the same size on every line.
- `perspective` lives on the `.is-spot` wrapper; the card rotates inside it.
- The hover branch is guarded by `innerWidth >= 1000` on enter, move and leave; below that the dots are inert decoration.
- Only the prefixed variables are global.

## Images

This component ships with 3 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/inline-spot-hover-cards/img1.jpg
https://motionprompts.dev/c/inline-spot-hover-cards/img2.jpg
https://motionprompts.dev/c/inline-spot-hover-cards/img3.jpg
```

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`** — `--is-paper`, `--is-ink`, `--is-line`, `--is-card-w`, `--is-card-h`, `--is-hue`. 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 `*, *::before, *::after`, `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.
