# Fullscreen Work Carousel — Fling-Away / Clip-Path Reveal Slide Transitions

## Goal
Build a fullscreen, one-slide-at-a-time portfolio "work" carousel. Each mouse-wheel tick or touch swipe fires a dramatic GSAP transition: the **outgoing slide shrinks to 25%, rotates 30°, fades out and flings itself two viewport-heights off screen**, while (starting mid-flight) the **incoming slide flies in from the opposite edge inside a cropped clip-path window that opens up to the full frame**. As the new slide lands, its title words and every info line are revealed with **masked SplitText staggers** rising from below. The carousel loops infinitely in both directions.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) plus the GSAP plugin **`SplitText`** (`import { SplitText } from "gsap/SplitText"`, then `gsap.registerPlugin(SplitText)`). No other libraries, no smooth-scroll lib. Ship `index.html`, `styles.css`, and a `<script type="module" src="./script.js">`.

## Layout / HTML
The static HTML is minimal — the body contains only an empty stage:

```html
<body>
  <div class="slider"></div>
</body>
```

All slides are **built at runtime in JS** from a data array of 4 slide objects, each with: `slideTitle`, `slideDescription`, `slideUrl`, `slideTags` (array of 4 strings), `slideImg` (image path). Use exactly this fictional content:

1. **Monochrome Signal** — "A stripped-back visual experience blending luxury fashion with streetwear edge. Designed for bold statements and minimal distractions." — url `/projects/monochrome-signal` — tags: Monochrome, Editorial, Fashion, Visual Identity.
2. **Mecha Muse** — "An experimental microsite blurring the line between human and machine. Cinematic visuals and deep red hues evoke a futuristic mythos." — url `/projects/mecha-muse` — tags: Cyberpunk, Experimental, 3D Layers, Concept Design.
3. **Neon Bloom** — "A surreal fusion of light, shadow, and sound. This project celebrates contrast and silhouette in a dreamlike digital space." — url `/projects/neon-bloom` — tags: Surreal, Lightplay, Immersive, Visual Narrative.
4. **Chromawave** — "A glossy, synth-infused interface for creators at the edge of music and fashion. Perfect for launch drops or digital showrooms." — url `/projects/chromawave` — tags: Futuristic, Glassmorphism, Music, Creative Tech.

A `createSlide(slideIndex)` function (1-based index) builds this exact DOM for one slide:

```html
<div class="slide">
  <div class="slide-img"><img src="…" alt=""></div>
  <div class="slide-header">
    <div class="slide-title"><h1>Monochrome Signal</h1></div>
    <div class="slide-description"><p>…description…</p></div>
    <div class="slide-link"><a href="…">View Project</a></div>
  </div>
  <div class="slide-info">
    <div class="slide-tags">
      <p>Tags</p>
      <p>Monochrome</p><p>Editorial</p><p>Fashion</p><p>Visual Identity</p>
    </div>
    <div class="slide-index-wrapper">
      <p>01</p><p>/</p><p>04</p>
    </div>
  </div>
</div>
```

The index counter is the current slide number and the total, both zero-padded to 2 digits (`padStart(2, "0")`), separated by a `/`. Only ONE `.slide` exists in the DOM at rest; during a transition the outgoing and incoming slides briefly coexist.

## Styling
- Google Fonts: **"DM Sans"** (body / h1) and **"DM Mono"** (all `p` and `a`). Global reset (`* { margin:0; padding:0; box-sizing:border-box }`).
- All `h1`, `p`, `a` are `text-transform: uppercase; color: #fff`.
- `h1`: `font-size: 5rem; font-weight: 600; letter-spacing: -0.1rem` (DM Sans).
- `p, a`: DM Mono, `font-size: 0.9rem; font-weight: 500; letter-spacing: -0.01rem; text-decoration: none`.
- `img`: `width: 100%; height: 100%; object-fit: cover`.
- `.slider`: `position: relative; width: 100vw; height: 100svh; background-color: #000; overflow: hidden` — a black fullscreen stage that clips the flying slides.
- `.slide` and `.slide-img`: `position: absolute; top: 0; left: 0; width: 100vw; height: 100svh`. `.slide` gets `will-change: transform`.
- `.slide-header`: `position: absolute; bottom: 2rem; left: 50%; transform: translate(-50%, 0); width: 75%; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 1rem; z-index: 1` — title, description and link stacked and centered near the bottom of the frame.
- `.slide-description`: `width: 60%; text-align: center; margin-bottom: 1rem`.
- `.slide-info`: `position: absolute; left: 0; bottom: 2rem; width: 100vw; padding: 0 2rem; display: flex; justify-content: space-between; align-items: flex-end` — tags bottom-left, index counter bottom-right.
- `.slide-tags`: `display: flex; flex-direction: column`; its first `p` (the "Tags" label) gets `margin-bottom: 1rem`.
- `.slide-index-wrapper`: `display: flex`; each `p` inside is `width: 2rem; text-align: center`.
- **Critical for the text masks:** `.line, .word { position: relative; display: inline-block; will-change: transform; }` — these are the classes SplitText assigns; combined with SplitText's `mask` option they let the text rise out of clipped wrappers.
- Mobile (`max-width: 1000px`): `h1` drops to `2rem` / `letter-spacing: 0`; `p` to `0.8rem`; `.slide-header` moves to dead center (`top: 50%; bottom: unset; transform: translate(-50%, -50%); width: 90%`); `.slide-description` becomes `width: 100%`.

## GSAP effect (exhaustive)

Everything runs inside `DOMContentLoaded`. State variables: `currentSlide = 1`, `isAnimating = false`, `scrollAllowed = true`, `lastScrollTime = 0`, plus `touchStartY` / `isTouchActive` for touch. `totalSlides = 4`.

### SplitText setup — `splitText(slide)`
Run on every freshly created slide, AFTER it is in the DOM:
- The `h1` inside `.slide-title` → `SplitText.create(h1, { type: "words", wordsClass: "word", mask: "words" })` — word-level split with masked wrappers.
- **Every** `p` and `a` in the slide (description, "Tags" label, each tag, the three index chunks, the "View Project" link) → `SplitText.create(el, { type: "lines", linesClass: "line", mask: "lines", reduceWhiteSpace: false })` — line-level split with masked wrappers.

### Initial load animation
1. Create slide 1, append to `.slider`, run `splitText`.
2. `gsap.set` on ALL `.word` and `.line` elements: `y: "100%"` (with `force3D: true`) — every word/line hidden below its mask.
3. `gsap.to` the same collection: `y: "0%"`, `duration: 1`, `ease: "power4.out"`, `stagger: 0.1`, `force3D: true` — the whole slide's text cascades up on load.

### Slide transition — `animateSlide(direction)` ("down" = next, "up" = previous)
Guard: return if `isAnimating || !scrollAllowed`; then set both flags to block re-entry. Update `currentSlide` with wraparound (after 4 comes 1; before 1 comes 4).

Direction-dependent constants:
- `exitY` = `"-200vh"` for down, `"200vh"` for up (outgoing slide flies opposite to the incoming one's origin).
- `entryY` = `"100vh"` for down, `"-100vh"` for up.
- `entryClipPath` = down: `polygon(20% 20%, 80% 20%, 80% 100%, 20% 100%)` (a window cropped 20% from left/right and 20% from the top, open at the bottom); up: `polygon(20% 0%, 80% 0%, 80% 80%, 20% 80%)` (mirrored: open at the top, cropped 20% at the bottom).

**Outgoing slide** (the existing `.slide` in the DOM), one tween:
```
gsap.to(currentSlideElement, {
  scale: 0.25, opacity: 0, rotation: 30, y: exitY,
  duration: 2, ease: "power4.inOut", force3D: true,
  onComplete: () => currentSlideElement.remove()
})
```
So it simultaneously shrinks to a quarter size, tilts 30° clockwise, fades to invisible and travels two full viewport heights away over 2 seconds with a strong in-out ease.

**Incoming slide** — inside a `setTimeout(..., 750)` (it starts 0.75 s after the exit tween begins, overlapping it):
1. `createSlide(currentSlide)`, then BEFORE appending: `gsap.set(newSlide, { y: entryY, clipPath: entryClipPath, force3D: true })`.
2. Append to `.slider`, run `splitText(newSlide)`.
3. `gsap.set` all its `.word` and `.line` elements to `y: "100%"` (`force3D: true`).
4. Main entry tween:
```
gsap.to(newSlide, {
  y: 0,
  clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
  duration: 1.5, ease: "power4.out", force3D: true,
  onStart: …text timeline below…,
  onComplete: …unlock below…
})
```
The slide travels one full viewport height into place while the clip-path window expands from the cropped polygon to the full frame — a "window opening" reveal.

**Text reveal timeline** — built in the entry tween's `onStart` (`const tl = gsap.timeline()`), all tweens animate `y` from the pre-set `"100%"` to `"0%"` with `duration: 1`, `ease: "power4.out"`:
- `.slide-title .word` (header words): `stagger: 0.1`, `force3D: true`, placed at absolute position **`0.75`** on the timeline (waits until the slide is mostly in frame).
- `.slide-tags .line`: `stagger: 0.1`, position **`"-=0.75"`** (starts 0.75 s before the previous tween ends, i.e. overlapping the title reveal).
- `.slide-index-wrapper .line`: `stagger: 0.1`, position **`"<"`** (same start as the tags).
- `.slide-description .line`: `stagger: 0.1`, position **`"<"`** (same start again).
- `.slide-link .line` ("View Project"): no stagger, position **`"-=1"`**.

**Unlock** — the entry tween's `onComplete`: set `isAnimating = false`, then after a `setTimeout` of 100 ms set `scrollAllowed = true` and `lastScrollTime = Date.now()`.

### Input handling
- `handleScroll(direction)`: return if `isAnimating || !scrollAllowed`, and also return if `Date.now() - lastScrollTime < 1000` (1-second throttle between transitions); otherwise update `lastScrollTime` and call `animateSlide(direction)`.
- **Wheel**: `window.addEventListener("wheel", handler, { passive: false })`; the handler calls `e.preventDefault()` and maps `e.deltaY > 0` → `"down"`, else `"up"`.
- **Touch**: `touchstart` (passive: false) records `touches[0].clientY` and sets `isTouchActive = true`. `touchmove` (passive: false) calls `e.preventDefault()`; if touch is active and not animating and the vertical delta from the start exceeds **50 px**, deactivate the touch and call `handleScroll` — swipe up (finger moves up, positive difference) = `"down"`, swipe down = `"up"`. `touchend` resets `isTouchActive`.

## Assets / images
4 full-bleed background images, one per slide, each shown at full viewport size with `object-fit: cover` (use generous ~3:2 or 16:9 sources; any large image works since it's cover-cropped). Describe by role — moody, cinematic, editorial fashion/tech portraits that match each project's vibe:
1. Slide 1 ("Monochrome Signal"): black-and-white-leaning studio portrait of a model with a bleach-blond buzzcut, black tank top, layered silver chains and angular black sunglasses on a flat grey backdrop.
2. Slide 2 ("Mecha Muse"): cinematic close-up of a woman wearing a glossy black cyborg helmet with a transparent visor, against a deep red background.
3. Slide 3 ("Neon Bloom"): surreal backlit portrait of a face wreathed in shimmering white feathered/crystalline texture against a dark blue background.
4. Slide 4 ("Chromawave"): futuristic portrait of a platinum-blonde model in a glowing visor with a chrome segmented cybernetic neckpiece on a purple background.

Name them `slide-img-1.jpg` … `slide-img-4.jpg` and reference them from the slide data array. No logos or brand marks.

## Behavior notes
- The carousel is fully wraparound/infinite in both directions; there is no scrollbar and no page scroll — the wheel is hijacked (`preventDefault`) and drives slides only.
- Re-entry is triple-guarded: the `isAnimating` flag, the `scrollAllowed` flag (released 100 ms after a transition completes) and the 1000 ms `lastScrollTime` throttle. A rapid wheel flick fires exactly one transition.
- During a transition both slides are visible at once: the old one spinning/shrinking away while the new one slides in through its opening clip-path window — this overlap (incoming starts at 750 ms of the 2000 ms exit) is the signature look; keep the timings exact.
- Works on touch devices via the 50 px swipe threshold; layout adapts under 1000 px (smaller type, header centered mid-screen).
- Use `100svh` (not `100vh`) for the stage and slide heights so mobile browser chrome doesn't cause cropping.
