# Editorial Portfolio Preloader → Stacked Clip-Path Hero Reveal

## Goal
Build a full-screen photographer/creative-director **landing-page intro**. On load, a bottom-right **odometer counter** made of four stacked digit reels rolls from `000%` up to `100%` while a thin black **progress bar** fills, all sitting on a flat **chrome-yellow** page. When the "load" completes, a stack of **7 full-bleed editorial photos wipe in one-by-one** via a staggered `clip-path` reveal (right edge → left), the whole hero **scales up to 1.3**, the top nav **drops down** into place, and a giant surname **headline rises letter-by-letter** out of a clip mask. Everything plays automatically once on page load — no scroll, no hover, no click. Total run ≈ 12.5 s.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use **`gsap`** (npm) only — no GSAP plugins, no smooth-scroll library. Import `import gsap from "gsap";`. The whole thing is a set of independent load-triggered `gsap.to` / `gsap.set` tweens (not one master timeline); each is scheduled by an explicit `delay`. Wrap the logic in a `DOMContentLoaded` listener. The per-character text split of the headline is done with a small **hand-rolled function** (wrap each character in a `<span>`) — NOT the SplitText plugin.

## Layout / HTML
Two stacked layers over a `.hero` section. Class/tag names are load-bearing — the JS queries them.

```html
<section class="hero">
  <div class="pre-loader">
    <p>Loading</p>

    <div class="counter">
      <div class="digit-1">
        <div class="num">0</div>
        <div class="num offset">1</div>
      </div>
      <div class="digit-2">
        <div class="num">0</div><div class="num">1</div><div class="num">2</div>
        <div class="num">3</div><div class="num">4</div><div class="num">5</div>
        <div class="num">6</div><div class="num">7</div><div class="num">8</div>
        <div class="num">9</div><div class="num">0</div>
      </div>
      <div class="digit-3">
        <div class="num">0</div><div class="num">1</div><div class="num">2</div>
        <div class="num">3</div><div class="num">4</div><div class="num">5</div>
        <div class="num">6</div><div class="num">7</div><div class="num">8</div>
        <div class="num">9</div>
      </div>
      <div class="digit-4">%</div>
    </div>

    <div class="progress-bar"></div>
  </div>

  <div class="hero-imgs">
    <img src="<photo 1>" alt="" />
    <img src="<photo 2>" alt="" />
    <img src="<photo 3>" alt="" />
    <img src="<photo 4>" alt="" />
    <img src="<photo 5>" alt="" />
    <img src="<photo 6>" alt="" />
    <img src="<photo 7>" alt="" />
  </div>
</section>

<div class="website-content">
  <nav>
    <div class="logo"><p>Logo</p></div>
    <div class="site-info"><p>(Photographer, creative director, filmmaker)</p></div>
    <div class="menu"><p>Menu</p></div>
  </nav>

  <div class="header"><h1>Marlow</h1></div>
</div>
```

The `.digit-1` and `.digit-2` reels are authored fully in HTML; the `.digit-3` reel is authored with only its first 10 nums and then **extended by JS** (see GSAP setup). `<h1>` holds a single surname word (use a neutral placeholder like `Marlow` — an arbitrary 6-letter surname; the character count drives the reveal stagger). Do not use any real client brand.

## Styling
Single flat palette:
- **`#ebdc0b`** — chrome/acid yellow: the full-page background (`html, body`), the nav text color, and the headline color.
- **`#000`** — black: default text (the `Loading` label + counter digits sit black-on-yellow) and the progress-bar fill.

Global reset: `* { margin:0; padding:0; box-sizing:border-box }`. `html, body { width:100vw; height:100vh; overflow:hidden; background:#ebdc0b }`.

Typography (two display faces; if the exact fonts are unavailable, substitute a **bold condensed display serif** for the loader/nav and a **very thin, light, wide sans** for the headline):
- Loader label, counter and nav use family **"Timmons NY"** (a tall condensed display face).
- Headline `<h1>` uses family **"PP Neue World"**, weight **200** (ultra-thin).

Element specifics (initial states matter — the animation depends on them):

- `img` — `position:absolute; width:100%; height:100%; object-fit:cover;` and the **critical initial clip-path** `clip-path: polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)` (all four points collapsed onto the right edge → the image is an invisible zero-width sliver pinned right).
- `.hero` — `width:100vw; height:100vh; padding:3em` (the 3em padding leaves a yellow border framing the photos).
- `.hero-imgs` — `position:relative; width:100%; height:100%; overflow:hidden; z-index:0` (all 7 images stack absolutely inside it).
- `.pre-loader` — `position:fixed; top:0; right:0; width:200%; height:100%; padding:2em; display:flex; justify-content:flex-end; align-items:flex-end; gap:0.5em; overflow:hidden; z-index:2`. It is anchored to the **bottom-right**; the label + counter + progress bar sit in a row at the bottom-right corner (the 200% width overflows off-screen left).
- `.pre-loader p` — `width:max-content; text-transform:uppercase; font-family:"Timmons NY"; font-size:60px; line-height:60px`.
- `.counter` — `height:100px; display:flex; font-family:"Timmons NY"; font-size:100px; font-weight:400; line-height:150px; clip-path: polygon(0 0, 100% 0, 100% 100px, 0 100px)`. The clip-path is a **100px-tall mask window**; each `.num` is 150px tall (line-height), so only one digit shows through the window at a time — this is what turns the reels into an odometer.
- `.digit-1, .digit-2, .digit-3, .digit-4` — `position:relative; top:-15px` (optical centering inside the 100px clip window).
- `.offset` — `position:relative; right:-7.5px` (kerning nudge for the narrow "1" glyph).
- `.progress-bar` — `position:relative; top:-15px; width:0%; height:4px; background:#000` (a thin black bar that starts at zero width).
- `.website-content` — `position:absolute; top:0; left:0; width:100%; height:100%; z-index:1` (sits **above** the images).
- `nav` — `position:fixed; top:0; width:100%; display:flex; padding:2em`. `nav > div { flex:1; font-family:"Timmons NY"; font-size:36px; font-weight:lighter; color:#ebdc0b; text-transform:uppercase }`. `.site-info { text-align:center }`, `.menu { text-align:right }` → logo left, credits centered, menu right.
- `.header` — `position:absolute; top:50%; left:50%; transform:translate(-50%,-50%)`.
- `h1` — `text-transform:uppercase; font-family:"PP Neue World"; font-size:20vw; font-weight:200; color:#ebdc0b; clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%)`. The clip-path is a **rectangular mask** that hides anything pushed below the headline box.
- `h1 span` (produced by the JS split) — `position:relative; top:400px` (every character starts **400px below** its resting line, hidden under the h1 clip mask).

## GSAP effect (be exhaustive)

Everything runs inside one `DOMContentLoaded` handler. There is no timeline object — each behavior is an independent `gsap.to` scheduled by `delay`. `ease` strings assume core GSAP eases (`power2/3/4.inOut/out`).

### Setup (before the tweens)
1. `gsap.set("nav", { y: -150 })` — park the whole nav 150px above the top edge.
2. Grab `const digit1 = ".digit-1"`, `digit2 = ".digit-2"`, `digit3 = ".digit-3"`.
3. **Headline split** — hand-rolled: read `.header h1` innerText, split into characters, wrap each char in `<span>…</span>`, join, and set as the h1's innerHTML. (`Marlow` → 6 spans. Each span inherits the CSS `top:400px`.)
4. **Extend the units reel** — the `.digit-3` reel authored with 10 nums (`0`–`9`) is grown by JS: append **two more full `0–9` runs** (nested loop `i<2`, `j<10`, each a `<div class="num">j</div>`), then append **one final `<div class="num">0</div>`**. Result: `.digit-3` ends with **31 nums** total, and its last num is `0`.

Final resting readout of the four reels (hundreds/tens/units/%) is **`100%`** (digit-1→`1`, digit-2→`0`, digit-3→`0`, digit-4 is the static `%`).

### A — Odometer counter roll (three reels)
Helper:
```js
function animate(digit, duration, delay = 1) {
  const numHeight = digit.querySelector(".num").clientHeight; // = 150px (line-height)
  const totalDistance = (digit.querySelectorAll(".num").length - 1) * numHeight;
  gsap.to(digit, { y: -totalDistance, duration, delay, ease: "power2.inOut" });
}
```
Each reel translates up by `(numCount − 1) × 150px` so its **last** num lands in the clip window. Calls, in order:
- `animate(digit3, 5)` — units reel (31 nums): `y: -4500`, duration **5**, delay **1** (default), `power2.inOut`.
- `animate(digit2, 6)` — tens reel (11 nums): `y: -1500`, duration **6**, delay **1**, `power2.inOut`.
- `animate(digit1, 2, 5)` — hundreds reel (2 nums): `y: -150`, duration **2**, delay **5**, `power2.inOut`.

So the units and tens reels start rolling at t=1 (units settles at t≈6, tens at t≈7); the hundreds reel flips `0→1` between t=5 and t=7. All three land at ≈100% by t≈7.

### B — Progress bar (two overlapping tweens on the same element)
```js
gsap.to(".progress-bar", { width: "30%",  duration: 2, ease: "power4.inOut", delay: 7 });
gsap.to(".progress-bar", {
  width: "100%", opacity: 0, duration: 2, ease: "power3.out", delay: 8.5,
  onComplete: () => gsap.set(".pre-loader", { display: "none" }),
});
```
The bar fills `0% → 30%` starting at t=7 (`power4.inOut`, 2 s), then a second tween (starting at t=8.5, `power3.out`, 2 s) drives it on to `100%` **while fading its opacity to 0**; on completion the entire `.pre-loader` is hidden (`display:none`). (Default GSAP overwrite is off, so the two width tweens briefly co-run around t=8.5–9 — that momentary overlap is intended, the net read is "bar fills and dissolves".)

### C — Stacked photo reveal (the star effect)
```js
gsap.to(".hero-imgs > img", {
  clipPath: "polygon(100% 0%, 0% 0%, 0% 100%, 100% 100%)",
  duration: 1, ease: "power4.inOut", stagger: 0.25, delay: 9,
});
```
Each image animates its `clip-path` from the collapsed right-edge sliver `polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)` to the **full rectangle** `polygon(100% 0%, 0% 0%, 0% 100%, 100% 100%)` — i.e. the two left-hand vertices slide from x=100% to x=0%, so each photo **wipes open from the right edge leftward**. Duration 1 s each, `power4.inOut`, **stagger 0.25 s** across the 7 images (last image starts at t≈10.5), all beginning at delay **9**. Because they're stacked absolutely, each new reveal paints over the previous one; the 7th photo is what's left on screen.

### D — Hero zoom-in
```js
gsap.to(".hero", { scale: 1.3, duration: 3, ease: "power3.inOut", delay: 9 });
```
The whole `.hero` (photo stack) scales **1 → 1.3** over 3 s starting at t=9, so the images push in cinematically as they reveal.

### E — Nav drop-in
```js
gsap.to("nav", { y: 0, duration: 1, ease: "power3.out", delay: 11 });
```
Nav slides down from `y:-150` to `y:0` over 1 s at t=11.

### F — Headline rise (letter by letter)
```js
gsap.to("h1 span", { top: "0px", stagger: 0.1, duration: 1, ease: "power3.out", delay: 11 });
```
Each character span animates its CSS `top` from **400px → 0px**, `power3.out`, 1 s, **stagger 0.1 s**, delay **11**. Since `<h1>` has a rectangular `clip-path` mask, the letters emerge from below and rise into the box one after another (`Marlow` → 6 chars, last starts at t≈11.5).

### Approximate timeline (seconds from load)
| t | event |
|---|-------|
| 0 | nav parked at y:-150; reels read `000%`; photos are right-edge slivers |
| 1 | units reel (A, dur 5) + tens reel (A, dur 6) start rolling |
| 5 | hundreds reel (A, dur 2) starts its `0→1` flip |
| ~6 | units reel settles on `0` |
| ~7 | tens + hundreds settle → `100%`; progress bar starts `0→30%` (B) |
| 8.5 | progress bar `30→100%` + fade-out begins (B) |
| 9 | photos begin staggered clip-path reveal (C); hero starts `scale→1.3` (D) |
| ~10.5 | progress bar done → `.pre-loader` hidden; last photo reveal fires |
| 11 | nav drops in (E); headline letters begin rising (F) |
| ~12.5 | headline fully risen; hero settled |

## Assets / images
**7 full-bleed editorial portrait photographs**, cover-cropped (`object-fit: cover`, so aspect ratio is flexible — shot roughly portrait ~2:3). They share a **warm, cinematic rust/orange palette**: a red-haired model against burnt-orange velvet furniture and sunlit concrete, styled with black heels, a vintage cream rotary phone, crystal-chandelier light refractions, and rust-toned gowns/blouses. Any cohesive set of 7 warm editorial fashion/portrait frames works — they are only ever seen as the sequential clip-path reveal, so ordering matters more than exact content (a strong final frame = the 7th, which stays on screen). No brand marks.

## Behavior notes
- **Autoplay once** on `DOMContentLoaded`; no scroll, hover, or click. Whole intro ≈ 12.5 s.
- The counter relies on the rendered `clientHeight` of a `.num` (150px), so the `.digit-3` reel must be extended **before** `animate()` runs (same tick is fine).
- Responsive (`@media (max-width:900px)`): `.pre-loader` padding drops to `1em`; `.counter` font-size → `70px`; `.pre-loader p` → `font-size:40px; line-height:64px`; `.offset` nudge → `right:-5px`. The animation logic is unchanged; the headline stays fluid at `20vw`.
- Keep `body { overflow:hidden }` and `.hero-imgs { overflow:hidden }` so the scaling photos and off-screen loader never spill.
- No reduced-motion handling exists in the reference; add one if desired but it is not part of the reference behavior.
