# Landing Page Reveal — Grid Shuffle to Hero Zoom Intro

## Goal

Build an autoplay landing-page intro (runs once on page load, no scroll): a full-screen black overlay reveals a gradient-fill logo and staggered project/location lists, then a centered 3×3 image grid clips open and rapidly shuffles through random editorial photos. The outer cells clip shut again, the center "hero" cell scales up 4× into a large framed hero image, two banner images pop in and rotate outward to its sides, the nav drops in from the top, and masked word-by-word text reveals push in the intro copy and title.

## Tech

Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) plus the GSAP plugin `CustomEase`, and `split-type` (npm package, `SplitType` class — NOT GSAP's SplitText). No smooth-scroll library needed (the page never scrolls).

## Layout / HTML

Single page, `<body>` containing in this order:

1. `div.overlay` — full-screen fixed black overlay with three flex columns:
   - `div.projects` containing `div.projects-header` with two `<p>`: `Project` and `Director`. (Rows are appended by JS.)
   - `div.loader` containing `h1.logo-line-1` with text `Nova` and `h1.logo-line-2` with text `Vice` (stacked two-line logo).
   - `div.locations` containing `div.locations-header` with one `<p>`: `Location`. (Rows appended by JS.)
2. `div.image-grid` — three `div.grid-row`, each with three `div.img` wrappers, each wrapping an `<img>`. The middle cell of the middle row additionally has class `hero-img` (i.e. `div.img.hero-img`). Fill the 9 `<img>` `src`s with the first 9 images of the pool (image 5 in the hero cell).
3. `nav` — three flex sections: `div.links` with two anchors `Index`, `Work`; `div.nav-logo` with one anchor containing `Nova<br />Vice`; another `div.links` with anchors `About`, `Contact`.
4. `div.banner-img.banner-img-1` and `div.banner-img.banner-img-2`, each wrapping an `<img>` (use image 7 and image 16 of the pool).
5. `div.intro-copy` with two `<h3>`: `Creative Solutions` and `Impactful Results`.
6. `div.title` with one `<h1>`: `Crafting bold experiences`.

Also create a small data module `projects.js` exporting `projectsData`: an array of ~16 objects `{ name, director, location }` with fictional film-production entries (e.g. `{ name: "Lunar Eclipse", director: "Amelia Crawford", location: "Toronto, ON" }`; some directors styled like `"Sophia // Chen"`, some locations like `"Elevation Studios - Denver"`).

On DOMContentLoaded, JS appends to `.projects` one `div.project-item` per entry (two `<p>`: name, director) and to `.locations` one `div.location-item` per entry (one `<p>`: location).

## Styling

- Reset `* { margin:0; padding:0; box-sizing:border-box }`. Body background `#e3e3db`, body font a neue-grotesque sans (`"PP Neue Montreal", sans-serif`).
- All `<p>` and `<a>`: uppercase, monospace (`"Akkurat Mono", monospace`), `font-size: 0.7rem`; anchors `color: #000`, no underline.
- Display type (`.loader h1`, `.nav-logo a`, `.intro-copy h3`, `.title h1`): condensed poster font (`"Druk", sans-serif`), italic, uppercase, `line-height: 0.9`.
- `.overlay`: `position: fixed; inset from top-left; width 100vw; height 100svh; padding 2em; background #000; color #fff; display flex; gap 2em; overflow hidden`. Its three children (`.projects`, `.loader`, `.locations`) each `flex: 1; display flex; flex-direction column; justify-content center; gap 1em`. `.loader` centers items with `gap: 0`. `.locations` centers items; `.locations-header` and `.location-item` are `width: 50%`.
- `.loader h1`: `font-size 2.5rem; text-align center`, and the gradient-fill trick that lets GSAP "fill up" the text by animating `background-position`:
  ```
  -webkit-text-fill-color: transparent;
  background-clip: text;
  background-image: linear-gradient(0deg, #3a3a3a, #3a3a3a 50%, #fff 0);
  background-size: 100% 200%;
  background-position: 0% 100%;
  color: #3a3a3a;
  ```
- `.projects-header`, `.project-item`, `.locations-header`, `.location-item`: `display flex; gap 2em; opacity 0` (hidden until animated). Children of the projects rows get `flex: 1` (two equal columns). `.project-item` and `.location-item` start `color: #4f4f4f` (dim gray, later flashed to white).
- `.image-grid`: `position fixed; top 50%; left 50%; transform translate(-50%, -50%); width 30%; aspect-ratio 1; display flex; flex-direction column; gap 1em; z-index 2`. Each `.grid-row`: `width 100%; display flex; gap 1em`. Each `.img`: `position relative; flex 1; aspect-ratio 1;` and the crucial initial clip: `clip-path: polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)` (collapsed to the top edge — invisible). Global `img { width 100%; height 100%; object-fit: cover }`.
- `nav`: `position fixed; width 100vw; padding 1em; display flex; gap 2em`, its three children `flex: 1`; `.links` spreads anchors with `justify-content: space-around`; `.nav-logo` centered, its anchor `font-size 1.75rem; font-weight bolder`.
- `.banner-img`: `position absolute; top 45%; left 50%; transform translate(-50%, -50%) scale(0); width 20%; aspect-ratio 4/5` (hidden at scale 0, both stacked at center; GSAP later animates `left` and `rotate`).
- `.intro-copy`: `position absolute; top 45%; transform translateY(-50%); width 100%; padding 0 8em; display flex; justify-content space-between; align-items center`. `h3` at `font-size 1.5rem`.
- `.title`: `position absolute; bottom 10%; left 50%; transform translateX(-50%)`. `h1` at `font-size 3.5rem`.
- `.intro-copy h3` and `.title h1`: `font-weight 500; color #000; position relative;` and `clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%)` so the split words can slide up from below the mask. Their `.word` spans (created by SplitType): `display inline-block; position relative; will-change transform; margin-right 0.1rem`.
- Media query `max-width: 900px`: hide `.projects`, `.locations`, `.intro-copy`, `.banner-img`; absolutely center `.loader`; `.title` becomes full-width, centered, `bottom 20%`, `h1` 2.5rem; `.image-grid` grows to `width 75%` with `0.5em` gaps (rows `width 95%; justify-content space-around`).

## GSAP effect (exhaustive)

Everything runs on `DOMContentLoaded`. Register `CustomEase` and create the signature ease used almost everywhere:

```js
CustomEase.create("hop", "0.9, 0, 0.1, 1");
```

Split text with SplitType (words only): `new SplitType(".intro-copy h3", { types: "words", absolute: false })` and the same for `".title h1"`. Keep references — you animate `splitInstance.words`.

Element sets: `gridImages` = all `.img` (9); `heroImage` = `.img.hero-img`; `images` = the 8 grid cells excluding the hero.

Image pool: an array of 35 image URLs (`img1 … img35`). Helper `getRandomImageSet()` shuffles a copy of the pool (`sort(() => 0.5 - Math.random())`) and returns the first 9.

**Initial states** (`gsap.set`): `nav` → `y: "-125%"`; all intro-copy words and all title words → `y: "110%"` (below their clip masks).

**Three timelines, all created at once so they run in parallel from t=0:**

### 1) overlayTimeline (the black loader screen)

1. `.logo-line-1` → `{ backgroundPosition: "0% 0%", color: "#fff", duration: 1, ease: "none", delay: 0.5 }` — the gradient fill wipes the first logo line from dark gray to white, bottom to top. Its `onComplete` fires an independent `gsap.to(".logo-line-2", { backgroundPosition: "0% 0%", color: "#fff", duration: 1, ease: "none" })` so line 2 fills right after line 1.
2. `[".projects-header", ".project-item"]` → `{ opacity: 1, duration: 0.15, stagger: 0.075, delay: 1 }` — the project list types in row by row.
3. At the same position (`"<"`): `[".locations-header", ".location-item"]` → same `{ opacity: 1, duration: 0.15, stagger: 0.075 }`.
4. `.project-item` → `{ color: "#fff", duration: 0.15, stagger: 0.075 }` — rows flash from gray to white in sequence; at `"<"` the same for `.location-item`.
5. `[".projects-header", ".project-item"]` → `{ opacity: 0, duration: 0.15, stagger: 0.075 }` — lists stagger back out; at `"<"` the same for the locations.
6. `.overlay` → `{ opacity: 0, duration: 0.5, delay: 1.5 }` — the whole black overlay fades away (the loader logo stays visible longer because the overlay fade is delayed; the logo itself is faded separately below).

### 2) imagesTimeline (grid reveal → shuffle → hero zoom)

1. All `.img` → `{ clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", duration: 1, delay: 2.5, stagger: 0.05, ease: "hop" }` — each cell wipes open top-to-bottom. Its `onStart` schedules, via `setTimeout(..., 1000)`:
   - `startImageRotation()` (see below), and
   - `gsap.to(".loader", { opacity: 0, duration: 0.3 })` (the logo fades out as the shuffle begins).
2. `images` (the 8 non-hero cells) → `{ clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)", duration: 1, delay: 2.5, stagger: 0.05, ease: "hop" }` — outer cells wipe shut again (collapse back to the top edge), leaving only the hero cell visible.
3. `.hero-img` → `{ y: -50, duration: 1, ease: "hop" }` — the hero cell nudges up 50px.
4. `.hero-img` → `{ scale: 4, clipPath: "polygon(20% 10%, 80% 10%, 80% 90%, 20% 90%)", duration: 1.5, ease: "hop" }` — the center cell blows up 4× while its clip contracts to an inset frame (20%/10% margins), producing the "zoom into hero" feel. Its `onStart` fires three parallel tweens:
   - `.hero-img img` → `{ scale: 1, duration: 1.5, ease: "hop" }` (the inner image was pre-scaled to 2 during the last shuffle cycle, so it settles down as the wrapper scales up — a counter-zoom).
   - `.banner-img` → `{ scale: 1, delay: 0.5, duration: 0.5 }` (both banners pop in from scale 0 at screen center).
   - `nav` → `{ y: "0%", duration: 1, ease: "hop", delay: 0.25 }` (nav drops in).
5. At `"<"`: `.banner-img-1` → `{ left: "40%", rotate: -20, duration: 1.5, delay: 0.5, ease: "hop" }` and `.banner-img-2` → `{ left: "60%", rotate: 20, duration: 1.5, ease: "hop" }` — the two banner cards slide out from behind the hero and tilt outward like fanned polaroids.

**startImageRotation()** — the rapid shuffle: loop `cycle` from 0 to 19 (20 cycles). For each cycle schedule a zero-duration tween (`gsap.to({}, { duration: 0, delay: cycle * 0.15, onComplete })`) whose callback assigns a fresh `getRandomImageSet()` to the 9 grid `<img>` elements — so all 9 cells swap images every 150ms for 3 seconds. On the LAST cycle only, the hero cell instead gets its final fixed hero image (image 5 of the pool) and `gsap.set(".hero-img img", { scale: 2 })` pre-zooms the inner image so step 4 above can counter-animate it back to 1.

### 3) textTimeline (masked word reveals)

1. Title words (`titleHeading.words`) → `{ y: "0%", duration: 1, stagger: 0.1, delay: 9.5, ease: "power3.out" }` — words slide up from `110%` into view behind the h1's clip mask, one every 0.1s. The 9.5s delay lands this right after the hero zoom finishes.
2. At `"<"`: intro-copy words (`introCopy.words`) → `{ y: "0%", duration: 1, stagger: 0.1, delay: 0.25, ease: "power3.out" }` — the two side headings follow 0.25s later.

Total choreography lasts ~11s from load.

## Assets / images

A pool of **35 editorial fashion portraits** (moody studio photography: dramatic close-up faces, silhouettes on saturated backgrounds, avant-garde styling). All are used as square crops via `object-fit: cover`, so any aspect works, but roughly square-to-portrait sources look best:

- Images 1–9: initial grid cells (image 5 sits in the center hero cell).
- Image 5: also the final hero image the zoom locks onto.
- Images 7 and 16: also the two banner images (displayed at 4:5).
- The whole pool of 35 feeds the random shuffle.

Reference them in an array like `` Array.from({ length: 35 }, (_, i) => `./images/img${i + 1}.jpeg`) `` (adjust the path to wherever the images live).

## Behavior notes

- Pure intro animation: no ScrollTrigger, no user input; everything autoplays once per load.
- Under 900px wide, the side lists, intro copy and banners are hidden; only the logo fill, grid reveal/shuffle, hero zoom, nav and title reveal run.
- The overlay stays in the DOM after fading (opacity 0); that's fine for a demo.
- Brand text is the fictional "Nova / Vice"; keep all names/locations fictional.
