# Fan Deck Overlay Menu — Clip-Path Panel, Glass Shelf & Fanned Image Cards

## Goal
Build a click-to-open fullscreen overlay menu. When the pill button in the top-right is pressed, a dark panel **wipes up from the bottom** (animated `clip-path` polygon) while a soft violet light blooms behind it. A thin **glass shelf** rises near the bottom, and **four frosted-glass image cards** climb out of it — blurred, tilted and slightly small — straighten up, then **fan out** left/right with a centre-out stagger into a row of four with a gentle arc and alternating rotation. With the menu open, moving the pointer **parallaxes** the cards (outer cards drift more) and a **specular highlight** follows the cursor across each card's glass; hovering a card scales it up a touch. Closing gathers the fan back into a stack, drops the cards into the shelf, dims the light and wipes the panel back down. Everything is GSAP.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) — core only, no plugins. Fonts: **DM Sans** (weights 400–600). One `script.js`, one `styles.css`, one `index.html`.

## Layout / HTML
Wrap everything in `<div class="fd">`. Class names are load-bearing.

```html
<div class="fd">
  <nav class="fd-nav">
    <a class="fd-brand" href="#">Lumen</a>
    <button class="fd-toggle" aria-expanded="false" aria-controls="fd-menu">
      <span class="fd-toggle__mask"><span class="fd-toggle__label">Menu</span></span>
    </button>
  </nav>

  <div class="fd-menu" id="fd-menu" aria-hidden="true">
    <div class="fd-menu__glow"></div>
    <div class="fd-deck">
      <!-- ×4: Work / Studio / Journal / Contact, indices 01–04 -->
      <a class="fd-card" href="#">
        <span class="fd-card__inner">
          <img src="…" alt="" />
          <span class="fd-card__label"><span class="fd-card__index">01</span>Work</span>
        </span>
      </a>
    </div>
    <div class="fd-dock"></div>
    <p class="fd-menu__foot"><span>hello@lumen.studio</span><span>Est. 2019</span></p>
  </div>

  <section class="fd-hero">
    <h1>Slide one out</h1>
    <p>A four-card deck lives behind the menu. Open it and the cards rise, settle and fan into place.</p>
  </section>
</div>
```

## Styling
Declare the palette as prefixed custom properties on `:root` so an editor can override them: `--fd-ink:#14161b`, `--fd-paper:#f3f1ec`, `--fd-overlay:#15171d`, `--fd-glow:#7b6cf6`, `--fd-glass:rgba(255,255,255,.08)`, `--fd-line:rgba(255,255,255,.18)`, `--fd-card-w:clamp(150px,20vw,300px)`, `--fd-hue:0deg`. Register `--fd-hue` with `@property { syntax:"<angle>"; inherits:true; initial-value:0deg }` so it can be animated smoothly.

- `.fd` — `position:relative; min-height:100svh; background:var(--fd-paper); color:var(--fd-ink); font-family:"DM Sans"`.
- `.fd-nav` — `position:fixed; inset:0 0 auto 0; z-index:100; display:flex; justify-content:space-between; align-items:center; padding:1.5rem 2rem`. When `.fd.is-open`, the nav text turns light (`#f3f1ec`).
- `.fd-toggle` — pill: `border:1px solid rgba(20,22,27,.14); background:rgba(255,255,255,.45); backdrop-filter:blur(14px) saturate(1.3); border-radius:999px; padding:.7rem 1.1rem; font-size:.85rem; font-weight:500; text-transform:uppercase; letter-spacing:.02em`; `:active` scales to .95. When open: `color:#f3f1ec; border-color:var(--fd-line); background:var(--fd-glass)`. `.fd-toggle__mask` is `display:inline-block; overflow:hidden` so the label can slide vertically.
- `.fd-menu` — `position:fixed; inset:0; z-index:10; background:var(--fd-overlay); overflow:hidden; color:#f3f1ec; pointer-events:none` (→ `auto` when open); initial `clip-path: polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)` (a zero-height sliver at the bottom).
- `.fd-menu__glow` — the only background element: `position:absolute; left:50%; bottom:-20%; width:70vmax; aspect-ratio:1; transform:translateX(-50%); border-radius:50%; background:radial-gradient(circle at 50% 40%, var(--fd-glow) 0%, transparent 60%); filter:blur(60px); opacity:.55`.
- `.fd-deck` — `position:absolute; inset:0; display:grid; place-items:center`. Each `.fd-card` is `position:absolute; width:var(--fd-card-w); aspect-ratio:5/4; transform-origin:50% 120%` (so rotation pivots below the card, like a hand of cards).
- `.fd-card__inner` — the glass: `display:block; width:100%; height:100%; padding:6px; border-radius:18px; background:var(--fd-glass); border:1px solid var(--fd-line); backdrop-filter:blur(18px) saturate(1.4); box-shadow:0 30px 60px -30px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.22); overflow:hidden`. The `img` fills it with `object-fit:cover; border-radius:12px`.
  - `::after` = **iridescent edge**: `inset:0; border-radius:inherit; padding:1px; background:conic-gradient(from var(--fd-hue), transparent 0deg, rgba(178,196,255,.7) 70deg, rgba(255,184,226,.55) 140deg, transparent 200deg, rgba(160,255,228,.6) 280deg, transparent 360deg)`, clipped to the 1px ring with `mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite: exclude` (`-webkit-mask-composite: xor`); `opacity:.55; pointer-events:none`.
  - `::before` = **specular highlight**: `radial-gradient(120px 120px at var(--fd-mx,50%) var(--fd-my,0%), rgba(255,255,255,.22), transparent 70%)`, `mix-blend-mode:screen`, `opacity:0` → `1` on `.fd-card:hover` (0.4s transition), `z-index:1`.
- `.fd-card__label` — glass chip bottom-left: `position:absolute; left:14px; bottom:14px; padding:.45em .8em; border-radius:999px; background:rgba(10,12,16,.42); border:1px solid rgba(255,255,255,.14); backdrop-filter:blur(10px); font-size:.85rem; font-weight:500; z-index:2`; the index span is `font-size:.65em; opacity:.6`.
- `.fd-dock` — the shelf: `position:absolute; left:50%; bottom:17%; width:min(56vw,720px); height:12px; transform:translateX(-50%); border-radius:999px; background:linear-gradient(180deg, rgba(255,255,255,.16), rgba(255,255,255,.05)); border:1px solid var(--fd-line); box-shadow:0 20px 40px -18px rgba(0,0,0,.7), inset 0 1px 0 rgba(255,255,255,.3); z-index:3`.
- `.fd-menu__foot` — `position:absolute; left/right:2rem; bottom:1.6rem; display:flex; justify-content:space-between; font-size:.75rem; letter-spacing:.04em; text-transform:uppercase; opacity:.55`.
- `.fd-hero` — centred column, `min-height:100svh`; `h1` `clamp(2.4rem,7vw,7rem)`, weight 500, `letter-spacing:-.03em; line-height:.95`; `p` max 34ch, opacity .65.
- `@media (max-width:1000px)`: `--fd-card-w:min(58vw,260px)`, cards `aspect-ratio:5/3.4`, dock `width:70vw; bottom:10%`, nav/foot padding 1.25rem.

## GSAP effect (be exact)
Write a `mount(config)` function that builds everything and returns a `destroy()`; call it on `DOMContentLoaded` with these defaults:

```js
const DEFAULTS = { openDuration: 1, fanAngle: 7, spacing: 108, rise: 140, entryBlur: 14, parallax: 14, hoverScale: 1.05 };
```

Constants: `CLOSED = "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)"`, `OPEN = "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)"`, `mid = (cards.length - 1) / 2`, `mq = matchMedia("(max-width: 1000px)")`.

**Initial state (`gsap.set`)** — menu `clipPath: CLOSED`; cards `{ yPercent: rise, xPercent: 0, rotation: 14, scale: .92, x: 0, y: 0, filter: "blur(14px)" }`; glow `{ yPercent: 40, scale: .8, opacity: 0 }`; dock `{ yPercent: 240, opacity: 0 }`; foot `{ opacity: 0, y: 10 }`.

**Fan targets are function-based** so they re-evaluate on every open (`timeline.invalidate()` before `restart()`):
- `fanX(i) = mq.matches ? 0 : (i - mid) * spacing` → `xPercent` (108% of card width between centres → -162, -54, 54, 162).
- `fanY(i) = mq.matches ? (i - mid) * 62 : Math.abs(i - mid) * 7` → `yPercent` (a shallow arc on desktop, a vertical stack on mobile).
- `fanR(i) = mq.matches ? (i - mid) * 2.5 : (i - mid) * fanAngle` → `rotation` (−10.5°, −3.5°, 3.5°, 10.5°).

**Open timeline** (`paused`, defaults `ease: "power3.out"`):
1. `0` — menu `clipPath → OPEN`, duration `openDuration` (1s).
2. `0` — glow `{ yPercent: 0, scale: 1, opacity: 1 }`, duration `openDuration * 1.5`, `power2.out`.
3. `0.18` — dock `{ yPercent: 0, opacity: 1 }`, 0.8s.
4. `0.28` — cards `{ yPercent: 0, rotation: -8, scale: 1, filter: "blur(0px)" }`, 0.9s, `stagger: 0.045` (they rise as a leaning stack).
5. `0.92` — cards `{ xPercent: fanX, yPercent: fanY, rotation: fanR }`, 1s, `ease: "power3.inOut"`, `stagger: { each: 0.035, from: "center" }`.
6. `1.1` — foot `{ opacity: 1, y: 0 }`, 0.6s.
7. `0` — root `{ "--fd-hue": "360deg", duration: 6, ease: "none", repeat: -1 }` (the iridescent edges slowly rotate while open).

**Close timeline** (`paused`, defaults `ease: "power3.inOut"`):
1. `0` — foot `{ opacity: 0, y: 10 }`, 0.3s.
2. `0` — cards `{ xPercent: 0, yPercent: 0, rotation: -8 }`, 0.55s, `stagger: { each: 0.03, from: "edges" }` (the fan closes from the outside in).
3. `0.38` — cards `{ yPercent: rise, rotation: 14, scale: .92, filter: "blur(14px)" }`, 0.55s, `stagger: 0.03`, `ease: "power3.in"`.
4. `0.55` — dock `{ yPercent: 240, opacity: 0 }`, 0.45s, `power3.in`.
5. `0.6` — glow `{ yPercent: 40, scale: .8, opacity: 0 }`, 0.6s.
6. `0.72` — menu `clipPath → CLOSED`, 0.7s.

**Toggle** — `setOpen(next)`: toggles `.is-open` on `.fd`, `aria-expanded`/`aria-hidden`, swaps the label (see below) and either `closeTl.pause(0); openTl.invalidate().restart()` or `openTl.pause(); closeTl.invalidate().restart()`. `Escape` closes.

**Label swap** — a tiny timeline: label `yPercent → -110` (0.25s, `power2.in`), set `textContent` to "Close"/"Menu", then `fromTo` `yPercent 110 → 0` (0.35s, `power3.out`).

**Pointer parallax** (only when open and not mobile) — on `pointermove` over the window, `nx = clientX/innerWidth - .5`, `ny = clientY/innerHeight - .5`; for each card `depth = 1 + |i - mid| * .35`, then `quickTo(card,"x")(nx * parallax * depth)` and `quickTo(card,"y")(ny * parallax * .6 * depth)` with `{ duration: .8, ease: "power3" }`. `x/y` are independent of the `xPercent/yPercent` fan, so they stack cleanly.

**Card hover** — `pointermove` on a card writes `--fd-mx/--fd-my` (percent position inside the card) onto `.fd-card__inner` for the specular highlight; `pointerenter` tweens the inner to `scale: hoverScale` (0.6s `power3.out`, `overwrite:"auto"`), `pointerleave` back to 1.

**destroy()** — remove all listeners, kill both timelines and any tweens, `clearProps:"all"` on menu/glow/dock/foot/label/cards/inners, remove `--fd-hue`, reset classes/aria/label.

## Assets / images
Four landscape photos (≈3:2, ~600×400 is enough) that read as *sections of a studio site*: a workspace, an interior, a stack of books, two people talking. Muted, natural light; they sit inside frosted frames so avoid busy backgrounds.

## Behavior notes
- The overlay is `position:fixed` and covers the page: keep `.fd-nav` above it (`z-index:100`) so the toggle stays reachable.
- All fan geometry is in `xPercent/yPercent/rotation`; pointer parallax lives in `x/y`. Don't mix them.
- Mobile (<1000px): cards stack vertically with a slight rotation, no pointer parallax; touch still opens/closes.
- `prefers-reduced-motion`: disable the highlight transition; the timelines still run (they are short).
- Nothing in the component is `:root`-scoped except the prefixed variables, so it can be dropped into an existing page.

## Images

This component ships with 4 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/fan-deck-overlay-menu/contact.jpg
https://motionprompts.dev/c/fan-deck-overlay-menu/journal.jpg
https://motionprompts.dev/c/fan-deck-overlay-menu/studio.jpg
https://motionprompts.dev/c/fan-deck-overlay-menu/work.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`** — `--fd-ink`, `--fd-paper`, `--fd-overlay`, `--fd-glow`, `--fd-glass`, `--fd-line`, `--fd-card-w`, `--fd-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.
- **Full-screen overlay** — a fixed element covers the viewport (a loader or transition). Only one may exist per page and it must remove itself when done. If your page already has one, keep that and drop this; otherwise the second silently hides the first.
