# Clayboan Scroll Gallery — Clip-Path Reveal + Char-by-Char Titles

## Goal
Build a full-page vertical scroll gallery (portfolio style) with Lenis smooth scroll where each tall "work" section is scrubbed by GSAP ScrollTrigger. As a section enters the viewport, its full-bleed image's `clip-path` morphs from an angular slanted polygon into a full rectangle (a diagonal wipe-open reveal); as the section leaves, the clip-path morphs again into a bottom-slanted polygon (a diagonal wipe-close). Meanwhile the big white project title, centered over the image, animates in character by character: SplitText masks every char and each char slides up from below its mask, with each character bound to its OWN scroll window so the reveal cascades left-to-right as you scroll.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) plus the GSAP plugins **`ScrollTrigger`** and **`SplitText`**, and **`lenis`** for smooth scroll. Register with `gsap.registerPlugin(ScrollTrigger, SplitText)`. Run everything inside a `DOMContentLoaded` handler.

## Layout / HTML
Semantic structure (class names are load-bearing — the JS/CSS query them):

```
<section class="hero">
  <h1>Beyond the limits</h1>
</section>

<!-- repeat this block 5 times, one per project -->
<section class="work-item">
  <div class="work-item-img"><img src="..." alt="" /></div>
  <div class="work-item-name">
    <h1>Carbon Edge</h1>
  </div>
</section>

<section class="outro">
  <h1>Back to base</h1>
</section>
```

- Exactly **5** `.work-item` sections between the hero and the outro.
- Project titles, in order: **Carbon Edge**, **Velocity Grid**, **Aeroform**, **Mach Horizon**, **Titan Rail**.
- Hero copy: "Beyond the limits". Outro copy: "Back to base". No nav, no footer — just these 7 stacked sections.

## Styling
Font (Google Fonts): **Inter Tight** (variable weight import). Body: `font-family: "Inter Tight", sans-serif; background: #fcfcfc; color: #141414`.

Global:
- `* { margin:0; padding:0; box-sizing:border-box }`.
- `img { width:100%; height:100%; object-fit:cover }`.
- `h1 { text-transform:uppercase; text-align:center; font-size:5rem; font-weight:550; line-height:1 }` (weight 550 works because Inter Tight is a variable font).
- `section { position:relative; width:100vw; overflow:hidden }`.

Sections:
- `.hero, .outro { height:100svh; display:flex; justify-content:center; align-items:center; padding:2rem }` — dark text on the off-white page background.
- `.work-item { height:150svh }` — each project section is 1.5 viewport heights tall, which creates the scroll runway for the scrubbed animations.
- `.work-item-img { position:absolute; width:100%; height:100%; will-change:clip-path }` with **initial** `clip-path: polygon(25% 25%, 75% 40%, 100% 100%, 0% 100%)` (an irregular quadrilateral: top edge slanted downward left-to-right, full bottom). The image fills the whole 150svh section behind the title.
- `.work-item-name { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:100%; z-index:1 }` — the title is centered in the section, layered above the image.
- `.work-item-name h1 { color:#fff }` (white title over the photo).
- Media query `max-width: 1000px`: all `h1` (including `.work-item-name h1`) drop to `font-size: 2.5rem`.

## GSAP effect (be exact)

### Lenis + ScrollTrigger wiring
```js
const lenis = new Lenis();
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => { lenis.raf(time * 1000); });
gsap.ticker.lagSmoothing(0);
```
Lenis drives the scroll; GSAP's ticker drives Lenis (time is in seconds, Lenis wants ms, hence `* 1000`).

### Per work item
Loop `gsap.utils.toArray(".work-item")`. For each `item`, grab `img = item.querySelector(".work-item-img")` and `nameH1 = item.querySelector(".work-item-name h1")`, then create THREE kinds of scrubbed ScrollTriggers:

**1. Title chars — one ScrollTrigger per character.**
```js
const split = SplitText.create(nameH1, { type: "chars", mask: "chars" });
gsap.set(split.chars, { y: "125%" });
```
`mask: "chars"` wraps every char in its own overflow-clipping mask element, so `y: "125%"` hides each char fully below its own line box. Then, for each `char` at `index`:
```js
ScrollTrigger.create({
  trigger: item,
  start: `top+=${index * 25 - 250} top`,
  end:   `top+=${index * 25 - 100} top`,
  scrub: 1,
  animation: gsap.fromTo(char, { y: "125%" }, { y: "0%", ease: "none" }),
});
```
- Each character gets its own 150px scroll window, offset 25px later per index — that per-index offset is what produces the left-to-right cascade (it is a scroll-position stagger, not a time stagger).
- The negative base offsets (`-250` / `-100`) mean the first characters start revealing BEFORE the section top reaches the viewport top (i.e. while the section is still entering), and scrubbing back down reverses them.
- `scrub: 1` gives the chars a ~1s catch-up smoothing.

**2. Image reveal on enter.**
```js
ScrollTrigger.create({
  trigger: item,
  start: "top bottom",
  end: "top top",
  scrub: 0.5,
  animation: gsap.fromTo(
    img,
    { clipPath: "polygon(25% 25%, 75% 40%, 100% 100%, 0% 100%)" },
    { clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", ease: "none" }
  ),
});
```
While the section travels from "top hits viewport bottom" to "top hits viewport top", the slanted top edge of the polygon unfolds up into the full rectangle — the photo wipes open diagonally. `ease: "none"` + scrub keeps it perfectly tied to scroll; `scrub: 0.5` adds a half-second smoothing lag.

**3. Image conceal on exit.**
```js
ScrollTrigger.create({
  trigger: item,
  start: "bottom bottom",
  end: "bottom top",
  scrub: 0.5,
  animation: gsap.fromTo(
    img,
    { clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)" },
    { clipPath: "polygon(0% 0%, 100% 0%, 75% 60%, 25% 75%)", ease: "none" }
  ),
});
```
While the section's bottom travels from viewport bottom to viewport top (the section leaving upward), the BOTTOM edge folds up into a slanted polygon (full top edge kept, bottom collapsing to points at 60%/75% height) — the mirror-image wipe-close of the entrance.

Important: both image triggers animate the same element's `clipPath` in separate non-overlapping scroll ranges (enter range ends where the section is fully on screen; exit range starts when its bottom reaches the viewport bottom), so they hand off cleanly. All polygons have 4 points so the morph interpolates smoothly. No pinning anywhere — the sections scroll naturally; the 150svh height is what stretches the effect out.

## Assets / images
**5 full-bleed photographs**, one per work item, each filling a 150svh × 100vw section with `object-fit: cover` — so use large landscape-or-taller images (roughly 4:3 to 3:4 works; they'll be cropped by cover). They should read as a matched editorial set: dramatic, dark, industrial "engineering & speed" photography — think close-ups of aircraft engines, turbine intakes, streamlined land-speed vehicles, high-speed train noses and angular jet fuselages, mostly monochrome/desaturated with hard studio lighting on dark backgrounds. White or near-white machine bodies against black work best under the white titles.

## Behavior notes
- Everything is **scroll-driven and fully scrubbed** (reversible): scrolling back down plays every reveal backwards. Nothing autoplays on load except that the hero is static text.
- No pinning, no scroll hijacking beyond Lenis smoothing.
- Uses `100svh`/`150svh` so mobile browser chrome doesn't cause jumps.
- Responsive is handled purely by the `max-width: 1000px` font-size drop; the clip-path and char animations are percentage/offset based and work at any size.
- Keep `will-change: clip-path` on `.work-item-img` — the clip morphs are the heaviest part.
