# Landing Page Reveal — Progress-Bar Preloader to Gliding-Image-Row Hero

## Goal
Build a full-screen editorial landing hero with a cinematic **preloader-to-hero reveal** that plays automatically once on page load (~7.5 seconds total). First a thin white **progress bar** draws itself left-to-right across the top of a solid dark overlay, then retracts to the right; the dark overlay then **wipes upward** via an animated clip-path. Underneath, **five small tilted image thumbnails** — pre-parked far off-screen to the left — **glide in with a custom ease to form a centered horizontal row**. The row then **splits apart**: the two left thumbnails fly off-screen left, the two right thumbnails fly off-screen right, while the **center thumbnail simultaneously scales up, un-rotates and un-rounds into a full-bleed hero background**. Finally, masked **SplitText** lines finish the sequence: the nav, the big headline paragraph and the footer contact links all **rise up line by line** from behind masks. One single GSAP timeline drives the entire sequence.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) plus the GSAP plugins **`SplitText`** and **`CustomEase`**. No smooth-scroll library — the page does not scroll during the intro; it is a pure load-triggered timeline. Register the plugins with `gsap.registerPlugin(CustomEase, SplitText)`. Run everything inside `document.addEventListener("DOMContentLoaded", …)` **wrapped in `document.fonts.ready.then(…)`** so the geometry math and SplitText run only after the web font has loaded.

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

```html
<div class="preloader-overlay">
  <div class="preloader"></div>
</div>

<nav>
  <div class="nav-logo">
    <a href="#">Foundry &amp; Form<br />Industrial Design Consultancy</a>
  </div>
  <div class="nav-items">
    <a href="#">Work</a>
    <a href="#">Catalogue</a>
    <a href="#">About</a>
  </div>
</nav>

<section class="hero">
  <div class="intro-img"><img src="..." alt="" /></div>
  <div class="intro-img"><img src="..." alt="" /></div>
  <div class="intro-img hero-img"><img src="..." alt="" /></div>
  <div class="intro-img"><img src="..." alt="" /></div>
  <div class="intro-img"><img src="..." alt="" /></div>

  <div class="hero-content">
    <div class="hero-header">
      <h1>
        We design objects that carry the weight of their own conviction,
        where every curve and joint exists not for beauty but because the
        material demanded it.
      </h1>
    </div>
    <div class="hero-social">
      <p>Say Hello</p>
      <a href="#">info@foundryandform.com</a>
      <a href="#">View Enquiries</a>
    </div>
  </div>
</section>
```

Notes:
- Exactly **5** `.intro-img` wrappers, each holding one `<img>`. They are all absolutely stacked at full-viewport size (see CSS); JS scales/positions them into a row. The **3rd** one additionally has the class **`.hero-img`** — it is the one that becomes the full-bleed hero background.
- `.preloader` is a thin bar nested inside a full-screen `.preloader-overlay` panel.
- Use **"Foundry & Form — Industrial Design Consultancy"** as the neutral placeholder brand, `info@foundryandform.com` as the neutral contact. No real brand names.
- `.hero-content` (headline paragraph + contact block) sits above the images.

## Styling
Font (single web font): **DM Sans** (variable, ital + opsz + wght) via Google Fonts — `@import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap");`. `body { font-family: "DM Sans", sans-serif }`.

Palette (only two colors):
- `#fff` — all text, the progress bar (white).
- `#0f0f0f` — the preloader-overlay panel (near-black).

Global:
- `* { margin:0; padding:0; box-sizing:border-box }`.
- `h1 { color:#fff; font-size:3rem; font-weight:400; letter-spacing:-1%; line-height:1.1 }`.
- `a, p { color:#fff; text-decoration:none; font-weight:400; letter-spacing:-1%; display:block }`.
- `img { width:100%; height:100%; object-fit:cover }`.

Key elements and their **initial states** (the animation depends on these exactly):

- `nav`: `position:fixed; top:0; width:100%; padding:2rem; display:flex; justify-content:space-between; align-items:flex-start; z-index:2`. `.nav-items`: `display:flex; gap:4rem`.

- `.preloader-overlay`: `position:fixed; top:0; width:100%; height:100svh; background-color:#0f0f0f; z-index:10`. **Initial** `clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)` (full rectangle — covers the whole viewport).
- `.preloader-overlay .preloader`: `position:absolute; top:0; width:100%; height:0.5rem; background-color:#fff; transform:scaleX(0); transform-origin:left; will-change:transform` (a thin white bar pinned to the very top, starting collapsed, growing from the left).

- `.hero`: `position:relative; width:100%; height:100svh; overflow:hidden`.
- `.intro-img`: `position:absolute; top:0; left:0; width:100%; height:100%; overflow:hidden; border-radius:0.5rem; transform-origin:center center; will-change:transform` (each wrapper is the full viewport, all five stacked; JS scales them down to thumbnails and moves them via `x`).

- `.hero-content`: `position:absolute; top:0; left:0; width:100%; height:100svh; padding:15svh 2rem 15svh 2rem; display:flex; flex-direction:column; justify-content:space-between; z-index:2`.
- `.hero-header`: `width:60%`.

- `.line`: `position:relative; will-change:transform` (this class is produced by SplitText; see below).

## GSAP effect (be exact)

### Setup
```js
import gsap from "gsap";
import SplitText from "gsap/SplitText";
import CustomEase from "gsap/CustomEase";
gsap.registerPlugin(CustomEase, SplitText);

CustomEase.create("hop",   "0.9, 0, 0.1, 1");   // steeper in-out — preloader retract + overlay wipe
CustomEase.create("glide", "0.8, 0, 0.2, 1");   // softer in-out — the whole image-row choreography
```

### Compute the image-row geometry (before the timeline)
Each `.intro-img` is full-viewport-sized and scaled to `0.2`, positioned by `x`-translation to form a horizontal centered row of 5 thumbnails with a fixed 40px gap. Because `transform-origin` is `center center`, a thumbnail's on-screen center sits at `innerWidth/2 + x`; the target `x` is therefore `desiredCenter − innerWidth/2`.

```js
const introImages = document.querySelectorAll(".intro-img");
const introImgScale = 0.2;
const introImgGap = 40;                                // px between thumbnails
const introImgRotations = [-15, 5, -7.5, 10, -2.5];    // per-index tilt in degrees

const introImgScaledWidth = window.innerWidth * introImgScale;
const introImgRowWidth     = introImgScaledWidth * 5 + introImgGap * 4;   // 5 thumbs + 4 gaps
const introImgCenteredX    = (window.innerWidth - introImgRowWidth) / 2;  // left edge of the centered row
const introImgOffScreenX   = introImgCenteredX - window.innerWidth * 1.3; // same row, shoved 1.3 vw to the left

introImages.forEach((img, i) => {
  const centeredX =
    introImgCenteredX + i * (introImgScaledWidth + introImgGap) + introImgScaledWidth / 2 - window.innerWidth / 2;
  const offScreenX =
    introImgOffScreenX + i * (introImgScaledWidth + introImgGap) + introImgScaledWidth / 2 - window.innerWidth / 2;

  gsap.set(img, {
    scale: introImgScale,          // 0.2
    x: offScreenX,                 // parked far off-screen left, in row order
    rotation: introImgRotations[i],
    borderRadius: "2.5rem",        // overrides the CSS 0.5rem while it's a thumbnail
  });

  img.dataset.centeredX = centeredX; // stash the row target for later
});
```

### Split the text into masked lines (before the timeline)
```js
SplitText.create("nav a, .hero-header h1, .hero-social p, .hero-social a", {
  type: "lines",
  linesClass: "line",   // each line element gets class "line"
  mask: "lines",        // wrap each line in an overflow-hidden mask
  autoSplit: true,      // re-split on font-load / resize
});
gsap.set(".line", { y: "125%" });  // park every line 125% below its mask (hidden)
```

### Timeline
Everything runs on **one** timeline with a 1s lead delay:
```js
const tl = gsap.timeline({ delay: 1 });
```
Position params below are `"<"` (align to the start of the previously-added tween), `"<N"` (N seconds after that start), or a **string label** (`"spread"` — created at the current timeline end the first time it's used, then reused).

**1 — Progress bar draws in** (default position → `t = 0`)
```js
tl.to(".preloader", {
  scaleX: 1,
  duration: 1.5,
  ease: "glide",
  onComplete: () => gsap.set(".preloader", { transformOrigin: "right" }),
});
```
The white bar grows `scaleX 0 → 1` from the left edge over 1.5s. Its `onComplete` flips `transform-origin` to `right` so the next tween collapses it toward the right.

**2 — Progress bar retracts** (default position → `t = 1.5`, sequential)
```js
tl.to(".preloader", { scaleX: 0, duration: 1.25, ease: "hop" });
```
The bar shrinks `scaleX 1 → 0` back toward the **right** edge over 1.25s.

**3 — Dark overlay wipes upward** (position `"<0.75"` → `t = 2.25`)
```js
tl.to(".preloader-overlay", {
  clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",
  duration: 1,
  ease: "hop",
}, "<0.75");
```
Starting 0.75s after the retract began, the overlay's clip-path collapses its **bottom edge up to the top** (both bottom coords `100% → 0%`), so the near-black panel wipes up and off, uncovering the hero beneath.

**4 — Five thumbnails glide into a centered row** (loop, each at `"<0.025"`)
```js
introImages.forEach((img) => {
  tl.to(img, {
    x: parseFloat(img.dataset.centeredX),
    duration: 1.5,
    ease: "glide",
  }, "<0.025");
});
```
Each image tweens `x` from its off-screen-left position to its row slot over 1.5s (`glide`). The `"<0.025"` chains them: the first starts 0.025s after step 3's start (≈ `t = 2.275`), and each subsequent image starts 0.025s after the previous one (`2.300, 2.325, 2.350, 2.375`) — a tight cascade so the row assembles left-to-right.

**5 — Left pair flies off-screen left** (position label `"spread"` → created at `t ≈ 3.875`)
```js
tl.to(".intro-img:nth-child(1), .intro-img:nth-child(2)",
  { x: "-100vw", duration: 1.5, ease: "glide" }, "spread");
```
The 1st and 2nd thumbnails slide out to `x: -100vw`. The `"spread"` label does not yet exist, so GSAP creates it at the **current end of the timeline** (right after the glide-in loop finishes, ≈ `t = 3.875`) and places this tween there.

**6 — Right pair flies off-screen right** (position `"spread"` → `t ≈ 3.875`)
```js
tl.to(".intro-img:nth-child(4), .intro-img:nth-child(5)",
  { x: "100vw", duration: 1.5, ease: "glide" }, "spread");
```
The 4th and 5th thumbnails slide out to `x: 100vw`, starting together with step 5 (the `"spread"` label now exists).

**7 — Center thumbnail expands into the hero** (position `"<"` → same start as step 6, `t ≈ 3.875`)
```js
tl.to(".hero-img", {
  scale: 1,
  x: 0,
  rotation: 0,
  borderRadius: 0,
  duration: 1.5,
  ease: "glide",
}, "<");
```
Simultaneously with the pairs flying apart, the 3rd/center image (`.hero-img`) `scale 0.2 → 1`, `x → 0`, `rotation → 0`, `borderRadius 2.5rem → 0` — it blooms from a small tilted thumbnail into the full-bleed, upright, square-cornered hero background over 1.5s.

**8 — Nav lines rise in** (position `"<1"` → `t ≈ 4.875`)
```js
tl.to("nav .line", { y: "0%", duration: 1, stagger: 0.1, ease: "power3.out" }, "<1");
```
1s after the hero-expand began, every nav line (the 2-line logo + the 3 nav items) rises from `y:125%` to `0%` inside its mask, staggered `0.1s`, `power3.out`.

**9 — Headline lines rise in** (position `"<"` → same start as step 8, `t ≈ 4.875`)
```js
tl.to(".hero-header .line", { y: "0%", duration: 1, stagger: 0.1, ease: "power3.out" }, "<");
```
The big headline paragraph reveals line by line the same way, starting together with the nav.

**10 — Footer contact lines rise in** (position `"<0.25"` → `t ≈ 5.125`)
```js
tl.to(".hero-social .line", { y: "0%", duration: 1, stagger: 0.1, ease: "power3.out" }, "<0.25");
```
0.25s after the headline started, the "Say Hello" label, the email and the "View Enquiries" link rise up line by line, staggered `0.1s`, `power3.out`.

### Timeline summary (absolute seconds, after the 1s lead delay)
| t (s) | what |
|------|------|
| 0–1.5   | progress bar draws in `scaleX 0→1` from left (`glide`); on end → `transform-origin:right` |
| 1.5–2.75 | progress bar retracts `scaleX 1→0` toward right (`hop`) |
| 2.25–3.25 | dark overlay clip-path wipes upward off screen (`hop`) |
| 2.275–3.875 | 5 thumbnails glide `x: offscreen→row`, chained `+0.025` each (`glide`) |
| 3.875–5.375 | thumbs 1&2 → `x:-100vw`, thumbs 4&5 → `x:100vw` (`glide`) |
| 3.875–5.375 | center `.hero-img` `scale 0.2→1`, `x→0`, `rotation→0`, `radius→0` (`glide`) |
| 4.875–~6 | nav lines + headline lines rise `y:125%→0%`, stagger 0.1 (`power3.out`) |
| 5.125–~6.5 | footer contact lines rise `y:125%→0%`, stagger 0.1 (`power3.out`) |

Total runtime ≈ **7.5s** including the 1s lead delay.

### Ease reference
- `glide` = `CustomEase.create("glide", "0.8, 0, 0.2, 1")` — the softer in-out used for the progress-bar **draw-in** (step 1) and the entire image-row choreography (steps 4–7).
- `hop` = `CustomEase.create("hop", "0.9, 0, 0.1, 1")` — the steeper in-out used for the progress-bar **retract** (step 2) and the overlay **wipe** (step 3).
- All three SplitText line reveals (nav, headline, footer) use `power3.out`.

## Assets / images
**Five** moody editorial images, `object-fit: cover` (aspect ratio is flexible since each is cover-cropped — each wrapper is full-viewport, so as a `0.2` thumbnail it reads as a small viewport-aspect tile, and the center one ends full-bleed). The set here mixes four landscape frames with one portrait frame; only the count and order matter to the motion. What the current images actually show:
- **Image 1** — exits to the **left**. Landscape. A warm sepia-toned macro close-up of an eyebrow, brow bone and lash line; dominant colors are monochrome browns and beige skin tones.
- **Image 2** — exits to the **left**. Landscape. A centered waist-up portrait of a young man with dark curly hair in an olive-green suit and tie, shot against a soft painterly backdrop; dominant colors are olive green, warm peach/apricot and pale teal.
- **Image 3 (`.hero-img`)** — the star: it becomes the fullscreen hero background. Portrait orientation, cover-cropped to full-bleed. A high-contrast black-and-white side profile of a woman with a short dark bob in a black blazer against a pale off-white ground; dominant colors are grayscale black, white and light grey.
- **Image 4** — exits to the **right**. Landscape. A minimal product-style still of a matte silver twisted-torus (wavy metal ring) sculpture resting on a flat neutral grey surface; dominant colors are brushed silver and mid grey.
- **Image 5** — exits to the **right**. Landscape. A moody still-life of purple bearded iris blooms with yellow-and-white throats against a deep glossy red tiled backdrop; dominant colors are violet-purple, oxblood red and touches of green and yellow.

Each thumbnail is pre-tilted by index with rotations `[-15, 5, -7.5, 10, -2.5]` degrees. If you swap in your own images, keep the roles by position (two exit left, center is the hero, two exit right) — texture/portrait/still-life all work. If you have fewer than five, repeat to reach five. No brand logos.

## Behavior notes
- **Autoplay once** on load (`DOMContentLoaded` → `document.fonts.ready`); no scroll, hover or click triggers. The page does not scroll during the intro.
- The geometry (`window.innerWidth`) is measured once at load, so the row is centered for the initial viewport size; `autoSplit: true` keeps the SplitText lines valid across font-load/resize.
- Uses `100svh` (small viewport height) so mobile browser chrome doesn't clip the full-height overlay/hero.
- Keep the `will-change` hints (`transform` on `.preloader`, `.intro-img`, `.line`) — they matter for smooth transform and clip-path animation.
- **Responsive** (`@media max-width: 1000px`): `.nav-items` becomes a right-aligned vertical column (`flex-direction:column; align-items:flex-end; gap:0`); `.hero-content` padding drops to `15svh 2rem 2rem 2rem`; `.hero-header` widens to `width:100%`. The animation itself is unchanged — only layout adapts.
