# Interactive Logo Depth Tunnel

## Goal
Build a full-viewport hero that frames a centered monogram logo inside a **tunnel of concentric, logo-shaped holes** receding into depth. Five solid, purple, full-bleed layers each have the **same glyph silhouette punched out of them** at progressively smaller sizes (via `mask-composite: subtract`), stacked with a sixth layer that holds the real logo image at the very center. As the pointer moves over the hero, every layer **trails the cursor** — but each one reads a progressively *older* cursor position from a frame-buffer (an 8-frame stagger per layer) and eases toward it with a slow lerp. The result is a soft, cascading parallax "wormhole": the inner logo leads, the outer rings lag behind like a comet tail, and the whole tunnel swings toward wherever the mouse is. No scroll, no click — pure `mousemove` + a per-frame ticker.

## Tech
Vanilla HTML/CSS/JS with an ES-module entry (`<script type="module">`). **Only dependency is `gsap` (npm)** — no plugins, no ScrollTrigger, no Lenis, no Three.js. The motion uses `gsap.ticker.add(...)` for the per-frame loop, `gsap.utils.toArray(...)` to collect the layers, and `gsap.set(...)` to write transforms. Runs in a fresh Vite project with a single import: `import gsap from "gsap"`.

## Layout / HTML
One hero section containing **six sibling `.depth-layer` divs**. The first five each wrap a `.depth-mask`; the sixth wraps a `.logo` with the `<img>`:

```html
<section class="hero">
  <div class="depth-layer"><div class="depth-mask"></div></div>
  <div class="depth-layer"><div class="depth-mask"></div></div>
  <div class="depth-layer"><div class="depth-mask"></div></div>
  <div class="depth-layer"><div class="depth-mask"></div></div>
  <div class="depth-layer"><div class="depth-mask"></div></div>
  <div class="depth-layer">
    <div class="logo"><img src="/path/to/logo.png" alt="" /></div>
  </div>
</section>
<script type="module" src="./script.js"></script>
```

DOM order matters: the JS collects `.depth-layer` in document order and derives each layer's lag from its index, so the **logo layer must be last** (index 5). Key classes the CSS/JS depend on: `.hero` (event target + measurement box), `.depth-layer` (the translated elements), `.depth-mask` (the punched shape), `.logo`/`.logo img` (centered mark).

## Styling

**Global reset**: `* { margin:0; padding:0; box-sizing:border-box; }`.

**Palette** — a single monochrome purple family, all `hsl(270, 56%, L%)`:
- `.hero` background: `hsl(270, 56%, 65%)`.
- Mask fill colors (the solid rectangle each hole is cut from), per layer 1→5: `50%`, `55%`, `60%`, `52.5%`, `55%`.
- Per-layer drop-shadow color (`filter: drop-shadow(0 0 1rem hsl(270,56% L%))`), layers 1→5: `25%`, `30%`, `35%`, `27.5%`, `75%`. (Layer 5's very light `75%` shadow reads as a bright rim/glow around the innermost ring.)

**`.hero`**: `position:relative; width:100%; height:100svh; overflow:hidden` (overflow hidden clips the oversized layers as they translate).

**`.depth-layer`**: `position:absolute; width:250%; height:250%; top:-75%; left:-75%; will-change:transform`. Each layer is **2.5× the viewport and offset up-left by 75%**, i.e. centered but massively oversized so it can be translated in any direction without ever exposing an edge inside the hero.

**`.depth-mask`**: `width:100%; height:100%`. Its fill is a solid `background: hsl(270,56%,L%)` (per-layer above). The cutout is done with a **dual mask + subtract**:
```css
-webkit-mask:
  linear-gradient(#fff, #fff),
  url("/path/to/mask.svg") center / var(--size) no-repeat;
-webkit-mask-composite: subtract;
mask:
  linear-gradient(#fff, #fff),
  url("/path/to/mask.svg") center / var(--size) no-repeat;
mask-composite: subtract;
```
The first mask layer (`linear-gradient(#fff,#fff)`) shows the entire rectangle; the second (the SVG glyph silhouette, centered, sized to `var(--size)`) is **subtracted**, punching a logo-shaped transparent hole through the solid color. Larger `--size` = larger hole.

**Per-layer `--size` and `z-index`** (this is what builds the tunnel — bigger holes on top, smaller holes behind, so you look *through* nested cutouts down to the logo):

| layer (nth-child) | z-index | mask fill L | `--size` (hole) | drop-shadow L |
|---|---|---|---|---|
| 1 | 7 | 50%   | **90%**   | 25%  |
| 2 | 6 | 55%   | **67.5%** | 30%  |
| 3 | 5 | 60%   | **45%**   | 35%  |
| 4 | 4 | 52.5% | **27.5%** | 27.5%|
| 5 | 3 | 55%   | **15%**   | 75%  |
| 6 (logo) | 2 | — | — | — |

So the front-most layer (z 7) has the biggest hole (90%) and the innermost visible ring (z 3) has the smallest (15%), with the logo image sitting behind everything at z 2, framed dead-center in the shrinking tunnel of holes.

**`.logo`**: `width:100%; height:100%; display:flex; justify-content:center; align-items:center`.
**`.logo img`**: `width:200px; height:auto; object-fit:contain`.

**Responsive** `@media (max-width:1000px)`: `.depth-layer` grows to `width:500%; height:500%; top:-200%; left:-200%` — even more oversized on small screens (layers translate a larger fraction of the viewport there, so they need more bleed to never reveal an edge).

## The effect (be exhaustive — this is the whole component)

There is **no timeline and no tween**. Motion is a hand-rolled frame-buffer + lerp running inside `gsap.ticker`. Reproduce every constant.

### Tunable constants (exact)
```
SENSITIVITY   = 0.3    // max pointer displacement as a fraction of hero size
LERP          = 0.04   // easing factor toward the sampled target, per frame
STAGGER_DELAY = 8      // frames of lag added per layer step
```
Derived: `totalDepthLayers = 6`; `BUFFER_SIZE = totalDepthLayers * STAGGER_DELAY + 1 = 49`.

### 1. Pointer tracking (normalized, centered on the hero)
Keep `const mouse = { x: 0, y: 0 }`.
- On `.hero` `mousemove`: read `rect = hero.getBoundingClientRect()`, then
  `mouse.x = ((e.clientX - rect.left) / rect.width  - 0.5) * 2`,
  `mouse.y = ((e.clientY - rect.top)  / rect.height - 0.5) * 2`.
  Range **−1..1**, with **0,0 at the hero center**.
- On `.hero` `mouseleave`: reset `mouse.x = 0; mouse.y = 0` (the tunnel eases back to rest/center).

### 2. Per-layer state
`const depthLayers = gsap.utils.toArray(".depth-layer")` (document order). Map to state objects:
```
layers[i] = {
  el: node,
  delay: (totalDepthLayers - 1 - i) * STAGGER_DELAY,  // i0→40, i1→32, i2→24, i3→16, i4→8, i5→0
  current: { x: 0, y: 0 },
}
```
Note the delay is **inverted vs. DOM index**: the **logo layer (index 5) has delay 0** (leads, most responsive), and the **front-most big-hole layer (index 0) has delay 40** (lags most). Also keep `const cursorTrail = []` — a rolling buffer of recent target positions.

### 3. The ticker loop (`gsap.ticker.add(() => { ... })`) — the star
Every frame:
1. `const rect = hero.getBoundingClientRect()`.
2. **Push the current target** onto the trail:
   `cursorTrail.push({ x: mouse.x * rect.width * SENSITIVITY, y: mouse.y * rect.height * SENSITIVITY })`.
   (At the extreme corner the target is ±30% of the hero's width/height.)
3. **Cap the buffer**: `if (cursorTrail.length > BUFFER_SIZE) cursorTrail.shift()` (drop the oldest).
4. For **each** layer:
   - `const trailIndex = Math.max(0, cursorTrail.length - 1 - layer.delay)` — sample a position `delay` frames in the past (clamped to the oldest entry while the buffer is still filling).
   - `const targetPos = cursorTrail[trailIndex]`.
   - Ease toward it: `layer.current.x += (targetPos.x - layer.current.x) * LERP` (and `.y` likewise).
   - Write it: `gsap.set(layer.el, { x: layer.current.x, y: layer.current.y })`.

### Why it looks the way it does
- **Two-stage lag.** Each layer first samples an *older* cursor target (the 8-frame-per-step stagger), then *eases* toward that stale target at only 4%/frame. Both effects compound: inner rings snap toward the cursor almost immediately while outer rings drift in behind them, producing a fluid caterpillar/comet-tail cascade rather than a rigid parallax.
- **Direction.** Positive `mouse.x` (cursor right of center) → positive target x → layers translate **right, toward the cursor**; same for y. The whole tunnel leans toward the pointer, and because the holes are concentric you get a convincing "looking down a shaft" perspective shift.
- **Rest state.** With the pointer outside the hero, targets are `0,0`; every layer lerps back to center and the tunnel settles perfectly aligned.
- **Softness.** Nothing is ever tweened to completion — the lerp means the layers are *always chasing*, so motion feels weighty and floaty, and stops the instant the target stabilizes.

## Assets / images
- **1 logo image** (`logo.png`): a bold, high-contrast **monogram / short wordmark** (e.g. two uppercase letters) in **solid white on a fully transparent background**, on a **square ~1:1 canvas** (~2800×2800). Displayed small (`width:200px`) dead-center on the innermost layer. Any simple, chunky mark works — no brand logos; use a neutral placeholder monogram.
- **1 SVG mask** (`mask.svg`): an SVG whose **`viewBox` matches the logo canvas (0 0 2800 2800)** containing a **single solid-fill (`#000`) silhouette path** of the same glyph. This is the shape subtracted from all five layers to cut the tunnel holes, so **it must match the logo's dominant silhouette** — if you swap the logo, regenerate this mask to the same shape. A bold, closed letterform path (thick strokes, no thin details, so the cutout reads clearly at every size from 90% down to 15%) works best.

## Behavior notes
- **Desktop / pointer only.** The effect is driven entirely by `mousemove`; touch devices that don't emit `mousemove` simply show the static, centered tunnel — that's acceptable, there's no touch fallback in the original.
- **Infinite, no trigger.** The ticker runs continuously from load; there is no start/stop, no `ScrollTrigger`, and **no reduced-motion guard** in the original. The tunnel is idle (centered) until the pointer enters the hero.
- **Perf.** `will-change: transform` on every layer; transforms are written with `gsap.set` (no layout thrash). The oversized 250%/500% layers plus `overflow:hidden` on `.hero` are what let layers translate freely without exposing edges.
