# Full-Screen Work Carousel — Fly-Off Exit + Clip-Path Entry + SplitText Reveals

## Goal

Build a full-screen, scroll-hijacked work/portfolio carousel (one slide visible at a time). On wheel or touch swipe, the current slide shrinks, rotates and flies off screen while the next slide enters from the opposite edge inside an animated `clip-path` polygon that expands to full screen; then the slide's title words and every text line (description, tags, index, link) reveal upward through SplitText masks with staggered power4 eases.

## Tech

Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) plus the GSAP plugin `SplitText` (`import { SplitText } from "gsap/SplitText"`). No other libraries. Do NOT register ScrollTrigger — scrolling is fully hijacked with native `wheel`/`touch` listeners.

## Data

Create a `slides.js` module that default-exports an array of 4 slide objects, each with:

- `slideTitle` (string, e.g. "Second Skin", "Half Light", "Sharp Shoulder", "Under Veil")
- `slideDescription` (one sentence, ~20 words, editorial tone about the project)
- `slideUrl` (e.g. `/work/second-skin`)
- `slideTags` (array of 4 short tags, e.g. `["Leather", "Studio", "Still life", "AW25"]`)
- `slideImg` (path to the slide's full-bleed image)

## Layout / HTML

`<body>` contains only:

```
.slider                     ← fixed-size viewport container
  .slide                    ← the FIRST slide, hard-coded in the HTML (matches slides[0])
    .slide-img > img        ← full-bleed image
    .slide-header
      .slide-title > h1     ← project title
      .slide-description > p
      .slide-link > a       ← text "View Project", href = slideUrl
    .slide-info
      .slide-tags           ← <p>Tags</p> followed by one <p> per tag
      .slide-index-wrapper  ← <p id="slide-index">01</p> <p>/</p> <p id="total-slide-count">04</p>
```

All subsequent slides are built entirely in JS with `document.createElement`, reproducing this exact structure (the index `<p>` shows the current slide number zero-padded to 2 digits, e.g. `02`, and the total count `04`).

## Styling

- Google Fonts: **DM Sans** (body/headings) and **DM Mono** (all small text). Global reset (`* { margin:0; padding:0; box-sizing:border-box }`).
- `img { width:100%; height:100%; object-fit:cover }`.
- All `h1, p, a`: `text-transform: uppercase; color: #fff`. Links have no underline.
- `h1`: DM Sans, `font-size: 5rem`, `font-weight: 600`, `letter-spacing: -0.1rem`.
- `p, a`: DM Mono, `font-size: 0.9rem`, `font-weight: 500`, `letter-spacing: -0.01rem`.
- `.slider`: `position: relative; width: 100vw; height: 100svh; background-color: #000; overflow: hidden`.
- `.slide` and `.slide-img`: `position: absolute; top: 0; left: 0; width: 100vw; height: 100svh`. `.slide { will-change: transform }`.
- `.slide-header`: absolute, `bottom: 2rem; left: 50%; transform: translate(-50%, 0%)`, `width: 75%`, centered text, flex column, `align-items: center; gap: 1rem; z-index: 1`.
- `.slide-description`: `width: 60%; text-align: center; margin-bottom: 1rem`.
- `.slide-info`: absolute, `left: 0; bottom: 2rem; width: 100vw; padding: 0 2rem`, flex `justify-content: space-between; align-items: flex-end`.
- `.slide-tags`: flex column; its first `<p>` ("Tags") has `margin-bottom: 1rem`.
- `.slide-index-wrapper`: flex row; each `<p>` inside is `width: 2rem; text-align: center`.
- `.line, .word` (SplitText output): `position: relative; display: inline-block; will-change: transform`.
- `@media (max-width: 1000px)`: `h1 { font-size: 2rem; letter-spacing: 0 }`, `p { font-size: 0.8rem }`, `.slide-header { top: 50%; bottom: unset; transform: translate(-50%, -50%); width: 90% }`, `.slide-description { width: 100% }`.

## GSAP effect (be exact)

Everything runs inside `DOMContentLoaded`. State: `currentSlide = 1`, `isAnimating = false`, `scrollAllowed = true`, `lastScrollTime = 0`, `totalSlides = slides.length`.

### Input → trigger

- `window` `wheel` listener with `{ passive: false }` + `e.preventDefault()`; `direction = e.deltaY > 0 ? "down" : "up"`.
- Touch: on `touchstart` record `touchStartY` and set `isTouchActive = true`; on `touchmove` (`{ passive: false }`, `preventDefault()`), if the vertical delta exceeds **50px**, set `isTouchActive = false` and fire with `direction = delta > 0 ? "down" : "up"`; `touchend` resets `isTouchActive`.
- A `handleScroll(direction)` gate ignores input while `isAnimating || !scrollAllowed`, and also throttles: ignore if `Date.now() - lastScrollTime < 1000` ms; otherwise update `lastScrollTime` and run the transition.

### Slide counter (infinite wrap)

`direction === "down"` → `currentSlide = currentSlide === totalSlides ? 1 : currentSlide + 1`; `"up"` → wrap the other way.

### Direction-dependent values

- Exit translate: `exitY = direction === "down" ? "-200vh" : "200vh"`.
- Entry start translate: `entryY = direction === "down" ? "100vh" : "-100vh"` (new slide comes from the bottom when scrolling down).
- Entry start clip-path:
  - down: `polygon(20% 20%, 80% 20%, 80% 100%, 20% 100%)` (a 60%-wide window anchored to the bottom edge)
  - up: `polygon(20% 0%, 80% 0%, 80% 80%, 20% 80%)` (anchored to the top edge)

### 1) Exit tween (current slide)

`gsap.to(currentSlideElement, { scale: 0.25, opacity: 0, rotation: 30, y: exitY, duration: 2, ease: "power4.inOut", force3D: true, onComplete: () => currentSlideElement.remove() })` — the slide simultaneously shrinks to a quarter size, fades out, tilts 30° clockwise and flies two viewport-heights off screen.

### 2) Entry (new slide) — starts after a 750 ms `setTimeout`

1. Build the new slide element from the data, then `gsap.set(newSlide, { y: entryY, clipPath: entryClipPath, force3D: true })` and append it to `.slider`.
2. Run SplitText on it:
   - Title `h1`: `SplitText.create(h1, { type: "words", wordsClass: "word", mask: "words" })`.
   - Every `p` and `a` in the slide: `SplitText.create(el, { type: "lines", linesClass: "line", mask: "lines", reduceWhiteSpace: false })`.
3. `gsap.set([all .word, all .line], { y: "100%", force3D: true })` — all text starts hidden below its mask.
4. Container tween: `gsap.to(newSlide, { y: 0, clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", duration: 1.5, ease: "power4.out", force3D: true })` — the slide travels to center while the polygon window expands to the full rectangle.
5. In that tween's `onStart`, build a `gsap.timeline()` for the text reveals (all `y: "100%"` → `"0%"`, all `ease: "power4.out"`, all `duration: 1`):
   - `.slide-title .word` with `stagger: 0.1`, placed at absolute time `0.75` on the timeline.
   - `.slide-tags .line` with `stagger: 0.1`, position `"-=0.75"` (overlaps the title reveal).
   - `.slide-index-wrapper .line` with `stagger: 0.1`, position `"<"` (same start as tags).
   - `.slide-description .line` with `stagger: 0.1`, position `"<"`.
   - `.slide-link .line` (no stagger), position `"-=1"`.
6. In the container tween's `onComplete`: `isAnimating = false`, and after a 100 ms timeout set `scrollAllowed = true` and `lastScrollTime = Date.now()`.

Net result: exit and entry overlap (exit lasts 2 s, entry starts at 0.75 s), so for a moment both slides are moving in opposite directions.

## Assets / images

4 full-bleed, cinematic fashion/editorial portraits (moody studio lighting, futuristic styling), one per slide, displayed full-screen with `object-fit: cover` — landscape ~16:9 or larger works best. Each slide pairs its image with its title/description/tags. No logos or brand marks.

## Behavior notes

- The first slide is static HTML and its text is NOT split — SplitText only runs on slides created during transitions.
- The carousel loops infinitely in both directions.
- Native page scroll never happens: the page is exactly one viewport tall and wheel/touch events are prevented.
- Works on touch devices via the 50px swipe threshold; the responsive breakpoint at 1000px recenters the header vertically.
