# Landing Page Reveal — Revealer Wipe → Image Stack → Flip-to-Corner Intro

## Goal
Build a full-screen editorial fashion landing hero with a cinematic **load-triggered intro** (~8.6 s, plays once). Two white panels (top + bottom) split apart with a custom `hop` ease to reveal a full-bleed image; a deck of 8 images scales down from `1.5×` and fades in one after another so they cascade into place; then **GSAP Flip** shrinks the final three "main" images from full-screen down to a small stacked cluster of thumbnails in the bottom-left corner. Simultaneously the hero furniture slides in — the logo, nav links, address and a **SplitType**-split heading all rise up from behind clip-path masks, and a desaturated team image in the bottom-right unmasks upward. Everything is orchestrated by nested GSAP timelines that fire automatically on `DOMContentLoaded`.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) plus the GSAP plugins **`Flip`** and **`CustomEase`**, and **`split-type`** (npm, imported as `SplitType`) for the line-splitting. No smooth-scroll library — the page does not scroll during the intro; it is a pure load-triggered timeline. Register with `gsap.registerPlugin(Flip, CustomEase)` and fire the whole sequence on `DOMContentLoaded`.

Imports:
```js
import gsap from "gsap";
import { Flip } from "gsap/Flip";
import { CustomEase } from "gsap/CustomEase";
import SplitType from "split-type";
gsap.registerPlugin(Flip, CustomEase);
```

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

```
<div class="container">
  <div class="revealers">
    <div class="revealer r-1"></div>   <!-- top white panel -->
    <div class="revealer r-2"></div>   <!-- bottom white panel -->
  </div>

  <div class="images">
    <div class="img"><img src="..." alt="" /></div>          <!-- 1: first shown -->
    <div class="img"><img src="..." alt="" /></div>          <!-- 2 -->
    <div class="img"><img src="..." alt="" /></div>          <!-- 3 -->
    <div class="img"><img src="..." alt="" /></div>          <!-- 4 -->
    <div class="img"><img src="..." alt="" /></div>          <!-- 5 -->
    <div class="img main"><img src="..." alt="" /></div>     <!-- 6: Flip target -->
    <div class="img main"><img src="..." alt="" /></div>     <!-- 7: Flip target -->
    <div class="img main"><img src="..." alt="" /></div>     <!-- 8: Flip target -->
  </div>

  <div class="hero-content">
    <div class="site-logo">
      <div class="word"><h1>Arc</h1></div>
      <div class="word"><h1>Worldwide<sup>&copy;</sup></h1></div>
    </div>

    <div class="nav">
      <div class="nav-item"><p>About</p></div>
      <div class="nav-item"><p>Work</p></div>
      <div class="nav-item"><p>Journal</p></div>
      <div class="nav-item"><p>Contact</p></div>
    </div>

    <div class="team-img"><img src="..." alt="" /></div>   <!-- reuse image 3 -->

    <div class="site-info">
      <div class="row">
        <div class="col"><div class="line"><p>Featured Works</p></div></div>
        <div class="col">
          <h2>Arc is a contemporary fashion brand redefining elegance with timeless designs and innovative aesthetics.</h2>
        </div>
      </div>
      <div class="row">
        <div class="col"></div>
        <div class="col">
          <div class="address">
            <div class="line"><p>Arc Studio</p></div>
            <div class="line"><p>Riverstone Building</p></div>
            <div class="line"><p>- 28 Orchard Lane</p></div>
            <div class="line"><p>N1 4DX</p></div>
          </div>
          <div class="socials">
            <div class="line"><p>SayHi@Arc.com</p></div>
            <br />
            <div class="line"><p>Instagram</p></div>
            <div class="line"><p>LinkedIn</p></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
```

Notes:
- `.images` holds exactly **8** `.img` wrappers. The **last three** (indices 5,6,7) also carry class `main` — those are the Flip targets that survive into the corner stack; the first five are `.remove()`d from the DOM mid-sequence.
- `.team-img` reuses the **same photo as image 3** (bottom-right, rendered grayscale).
- Use **"Arc" / "Worldwide"** as the neutral placeholder brand. Copyright superscript `©` after "Worldwide".

## Styling
Fonts: the design calls for **"PP Neue Montreal"** (body + all `h1`/`h2`) and **"Apercu Mono Pro"** (the small `.site-info p` labels). These are custom faces — if unavailable, fall back to a clean geometric sans (e.g. Helvetica Neue / Inter) for the display type and a monospace for the small labels. Keep the family names in the CSS with sensible fallbacks.

Palette: dead simple — background `#fff`, all text `#000`. The revealer panels are also `#fff`.

Global:
- `* { margin:0; padding:0; box-sizing:border-box }`.
- `html, body { width:100%; height:100%; font-family:"PP Neue Montreal"; background:#fff }`.
- `img { width:100%; height:100%; object-fit:cover }`.

Containers:
- `.container`: `position:relative; width:100vw; height:100vh; overflow:hidden`.
- `.revealers`: `position:fixed; inset:0; width:100vw; height:100vh; display:flex; flex-direction:column; z-index:2`. Each `.revealer`: `flex:1; width:100%; background:#fff` (so the two panels each fill 50% of the viewport height). **Initial** `clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)` (full rectangle — both panels fully opaque and covering everything).
- `.images`: `position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:100%; height:100%; transform-origin:center center; will-change:transform`.
- `.img`: `position:absolute; top:50%; left:50%; transform:translate(-50%,-50%) scale(1.5); width:100%; height:100%; opacity:0; will-change:transform`. **`.img:first-child { opacity:1 }`** (only the first image is visible at the start; all start zoomed to `1.5×`).

Stacked (post-Flip) state — these classes are added by the JS, but the CSS must define them:
- `.images.stacked-container`: `position:fixed; left:2em; bottom:2em; width:auto; height:auto; display:flex; flex-direction:column-reverse; align-items:flex-start; gap:1em; transform:none; will-change:transform`.
- `.img.stacked`: `position:relative; width:150px; height:100px; transform:none; top:auto; left:auto; opacity:1; will-change:transform`.

Hero furniture:
- `.hero-content`: `position:relative; width:100%; height:100%`.
- `.site-logo`: `position:absolute; top:2em; left:2em; display:flex; gap:1em`. `.site-logo h1`: `color:#000; font-size:5vw; font-weight:500; line-height:1; letter-spacing:-0.01em`. `.site-logo h1 sup`: `position:absolute; top:-0.125em; font-size:2rem`.
- `.nav`: `position:absolute; right:0; width:50%; padding:2em; display:flex; justify-content:flex-end; gap:1em`. `.nav .nav-item p`: `font-size:16px; font-weight:500`.
- `.team-img`: `position:absolute; right:2em; bottom:2em; width:40%; height:50%`. **Initial** `clip-path: polygon(0 100%, 100% 100%, 100% 100%, 0% 100%)` (collapsed onto the bottom edge → hidden). `.team-img img { filter:saturate(0) }` (grayscale).
- `.site-info`: `position:absolute; bottom:2em; left:2em; width:50%; height:50%; display:flex; flex-direction:column; justify-content:space-between`. `.site-info .row`: `display:flex; gap:2em`; `.row .col { flex:1 }`. `.site-info h2`: `font-size:25px; font-weight:500; line-height:1.25`. `.site-info p`: `text-transform:uppercase; font-family:"Apercu Mono Pro"; font-size:11px; font-weight:500; color:#000; line-height:1.25`. The second row's second col is `display:flex` so `.address` and `.socials` sit side by side (each `flex:1`).

Mask / initial hidden states (critical — the reveal depends on these):
- `.word, .nav-item, .line { clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%) }` — each is an overflow-clipping mask window.
- `.word h1, .nav-item p, .line p { position:relative; will-change:transform; transform:translateY(100%) }` — the text sits one full line **below** its mask (hidden).
- `.site-info h2 .line span { display:block; transform:translateY(100%) }` — the split heading lines also start pushed one line down inside their masks.

## GSAP effect (be exact)

### Setup
```js
document.addEventListener("DOMContentLoaded", () => {
  CustomEase.create("hop",  "M0,0 C0.355,0.022 0.448,0.079 0.5,0.5 0.542,0.846 0.615,1 1,1");
  CustomEase.create("hop2", "M0,0 C0.078,0.617 0.114,0.716 0.255,0.828 0.373,0.922 0.561,1 1,1");
```
- `hop` — a slightly "sticky" ease-in-out (hangs near the middle, then snaps to the end). Used for the revealer wipe, the Flip, and the team-image unmask.
- `hop2` — an ease-out-ish curve that leaps early then settles. Used for the masked-text rise.

**SplitType on the heading, then re-wrap each line in a mask.** Split `.site-info h2` into **lines**, then for every produced line replace it with a `<div class="line"><span>…</span></div>` wrapper (so it picks up the `.line` clip-path mask and the `span { translateY(100%) }` init):
```js
const splitH2 = new SplitType(".site-info h2", { types: "lines" });
splitH2.lines.forEach((line) => {
  const text = line.textContent;
  const wrapper = document.createElement("div");
  wrapper.className = "line";
  const span = document.createElement("span");
  span.textContent = text;
  wrapper.appendChild(span);
  line.parentNode.replaceChild(wrapper, line);
});
```

Three timelines:
```js
const mainTl     = gsap.timeline();
const revealerTl = gsap.timeline();
const scaleTl    = gsap.timeline();
```

### `revealerTl` — the two white panels split apart (both start together, `"<"`)
```js
revealerTl
  .to(".r-1", {
    clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",   // bottom edge collapses UP to the top → wipes up
    duration: 1.5, ease: "hop",
  })
  .to(".r-2", {
    clipPath: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)", // top edge collapses DOWN to the bottom → wipes down
    duration: 1.5, ease: "hop",
  }, "<");   // simultaneous
```
The top panel wipes upward and the bottom panel wipes downward at the same time, splitting open like a horizontal shutter over 1.5 s to reveal the image deck behind.

### `scaleTl` — image deck scales in and cascades
```js
scaleTl.to(".img:first-child", { scale: 1, duration: 2, ease: "power4.inOut" });

const images = document.querySelectorAll(".img:not(:first-child)"); // the other 7
images.forEach((img) => {
  scaleTl.to(img, {
    opacity: 1, scale: 1, duration: 1.25, ease: "power3.out",
  }, ">-0.95");   // start 0.95s BEFORE the previous tween ends → heavy overlap
});
```
- The already-visible **first** image de-zooms `scale 1.5 → 1` over **2 s** (`power4.inOut`).
- Each of the other **7** images fades `opacity 0 → 1` and de-zooms `1.5 → 1` over **1.25 s** (`power3.out`), each inserted at `">-0.95"` (0.95 s before the previous one finishes). Because duration 1.25 − 0.95 = **~0.3 s effective step**, the images cascade in quick succession, each layer landing on top of the last. `scaleTl` total ≈ **4.1 s**.

### `mainTl` — the master sequence
```js
mainTl
  .add(revealerTl)              // t = 0 .. 1.5
  .add(scaleTl, "-=1.25")       // inserted at 0.25 → images start scaling while panels still opening
  .add(() => { /* Flip, see below */ })   // fires at ~t = 4.35 (end of scaleTl)
  .to(".word h1, .nav-item p, .line p, .site-info h2 .line span", {
    y: 0, duration: 3, ease: "hop2", stagger: 0.1, delay: 1.25,
  })
  .to(".team-img", {
    clipPath: "polygon(0% 100%, 100% 100%, 100% 0%, 0% 0%)",
    duration: 2, ease: "hop", delay: -4.75,
  });
```

**Placement math** (position params / delays resolve to absolute times):
- `revealerTl` at **0** → spans [0, 1.5].
- `scaleTl` at `"-=1.25"` → inserted at **0.25** → spans [0.25, ~4.35]. So the deck begins de-zooming while the revealer shutter is still opening.
- The Flip callback is appended at the end (**~4.35 s**) — a zero-duration `.add(fn)`.
- The masked-text `.to(...)` is appended at ~4.35 with `delay: 1.25` → animates **[5.6, 8.6]**.
- The team-image `.to(...)` is appended at the new end (~8.6) with `delay: -4.75` → pulled back to **[3.85, 5.85]** (overlaps the Flip and the text rise).

Total runtime ≈ **8.6 s**.

### The Flip callback (the signature move) — full-screen images → corner thumbnail stack
Fires at ~t = 4.35. It captures the current geometry of the three `.main` images (currently full-bleed and centered), reparents/restyles them into a small bottom-left column, then Flips from the old geometry to the new:
```js
() => {
  // 1. Drop the first five images out of the DOM — only the 3 .main survive.
  document.querySelectorAll(".img:not(.main)").forEach((img) => img.remove());

  // 2. Snapshot the .main images while they're still full-screen.
  const state = Flip.getState(".main");

  // 3. Restyle the container + images into the stacked-corner layout.
  const imagesContainer = document.querySelector(".images");
  imagesContainer.classList.add("stacked-container");        // → fixed bottom-left, column-reverse, gap 1em
  document.querySelectorAll(".main").forEach((img, i) => {
    img.classList.add("stacked");                            // → position:relative, 150×100px
    img.style.order = i;
    gsap.set(".img.stacked", { clearProps: "transform,top,left" });
  });

  // 4. Animate from the snapshot to the new layout.
  return Flip.from(state, {
    duration: 2,
    ease: "hop",
    absolute: true,
    stagger: { amount: -0.3 },   // reverse-order stagger, 0.3s total spread
  });
}
```
Visual result: the three overlapping full-screen images simultaneously **shrink and fly down to the bottom-left corner**, landing as a tidy vertical stack of three `150×100` thumbnails (gap `1em`, `column-reverse` order), the reverse stagger (`amount: -0.3`) making the last one lead. `absolute: true` lets them animate freely from their overlapping full-bleed positions. Runs 2 s, independent of `mainTl` (the returned tween is standalone).

### Masked text rise (`hop2`, t = 5.6 → 8.6)
```js
.to(".word h1, .nav-item p, .line p, .site-info h2 .line span", {
  y: 0, duration: 3, ease: "hop2", stagger: 0.1, delay: 1.25,
})
```
Every masked text piece — the two logo words (`Arc`, `Worldwide©`), the four nav links, the "Featured Works" / address / socials lines, and each split heading line — slides from `translateY(100%)` (parked below, clipped by its `.word` / `.nav-item` / `.line` mask) up to `y:0`. Duration **3 s**, ease **hop2**, staggered **0.1 s** across all pieces in DOM order.

### Team image unmask (`hop`, t = 3.85 → 5.85)
```js
.to(".team-img", {
  clipPath: "polygon(0% 100%, 100% 100%, 100% 0%, 0% 0%)",
  duration: 2, ease: "hop", delay: -4.75,
})
```
The bottom-right grayscale image reveals from its collapsed bottom edge upward — its two top corners travel from `y:100%` to `y:0%`, so the frame grows up from the bottom to fill its box. Duration **2 s**, ease **hop**.

### Timeline summary (absolute seconds)
| t (s)      | what |
|-----------|------|
| 0 – 1.5   | revealer panels split apart (r-1 up, r-2 down), `hop` |
| 0.25 – 2.25 | first image de-zooms `1.5→1`, `power4.inOut` |
| ~1.3 – 4.35 | other 7 images fade in + de-zoom, ~0.3s cascade, `power3.out` |
| ~4.35 – 6.35 | Flip: 3 main images shrink/fly to bottom-left thumbnail stack, `hop`, reverse stagger |
| 3.85 – 5.85 | team image clip-path unmasks upward, `hop` |
| 5.6 – 8.6 | logo/nav/address/heading lines rise up from masks, `hop2`, stagger 0.1 |

### Ease reference
- `hop`  = `CustomEase.create("hop",  "M0,0 C0.355,0.022 0.448,0.079 0.5,0.5 0.542,0.846 0.615,1 1,1")` — revealer wipe, Flip, team-image unmask.
- `hop2` = `CustomEase.create("hop2", "M0,0 C0.078,0.617 0.114,0.716 0.255,0.828 0.373,0.922 0.561,1 1,1")` — masked text rise.
- Image deck: first image `power4.inOut`; the other seven `power3.out`.

## Assets / images
**8 editorial fashion photographs**, all rendered full-bleed (`object-fit: cover`), so exact aspect ratios are flexible since they are cover-cropped. Roles:
- **Images 1–5** — the hero deck layers that scale in and fade; they are `.remove()`d once the Flip fires, so they're only seen briefly stacking up. Image **2** reads best in **portrait** orientation. Image **3** is **also reused** (grayscale/desaturated) as the bottom-right `.team-img`.
- **Images 6–8** — the three `.main` frames that survive the Flip and become the `150×100` (≈3:2 landscape) corner thumbnails. Image **7** works well roughly **square**.

All are moody, high-fashion editorial portraits (single subject, studio or atmospheric backdrop) with a cohesive elegant, minimal look. No brand marks. If you have fewer than eight, repeat images to fill the deck; the three used in the corner stack (6–8) should be distinct so the final cluster reads as three photos.

## Behavior notes
- **Autoplay once** on `DOMContentLoaded`; no scroll, hover, or click triggers. The page does not scroll during the intro (`.container` is `overflow:hidden`, viewport-sized).
- The first five `.img` wrappers are permanently removed from the DOM once the Flip fires — do not rely on them existing afterward.
- Keep every `will-change` hint (transform on `.images`/`.img`/`.stacked`, and on the masked text pieces) — they matter for smooth transform + clip-path animation.
- The whole piece is viewport-height (`100vh`) full-screen; it is a preloader-style hero, so it is intended to sit above the fold and animate on first paint.
