# Landing Page Reveal — Counter Preloader + Clip-Path Hero Unmask

## Goal
Build a full-screen editorial landing hero with a cinematic **preloader-to-hero reveal** that plays automatically once on page load (~8.5 seconds total). A giant number in the lower-left ticks `0 → 100` while it scales up and a thin horizontal progress bar draws itself across the screen. When the count finishes, the number wipes out digit by digit; then a hidden hero background image is unmasked by an animated **clip-path polygon** — collapsed to a single center point, opening to a small centered rectangle, then expanding to the full viewport — with a custom `hop` ease, while the image itself de-zooms from `2x` to `1x`. Masked **SplitText** finishes the sequence: the huge hero headline slides in character by character from the right, and the nav links and footer labels rise up from behind masks. One single GSAP timeline drives everything.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) plus the GSAP plugins **`CustomEase`** and **`SplitText`**. 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)` and fire the whole sequence on `DOMContentLoaded`.

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

```
<div class="preloader-counter">
  <h1>0</h1>
</div>

<nav>
  <div class="nav-logo"><a href="#">Canon</a></div>
  <div class="nav-links">
    <a href="#">Index</a>
    <a href="#">Collection</a>
    <a href="#">Material</a>
    <a href="#">Process</a>
    <a href="#">Info</a>
  </div>
</nav>

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

  <div class="header"><h1>Canon</h1></div>

  <div class="hero-footer">
    <p>Permanence</p>
    <p>Craftsmanship</p>
    <p>Expression</p>
  </div>

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

Notes:
- `.preloader-counter` is a **fixed** overlay element that lives outside `.hero`; it holds the big counting number and is `.remove()`d from the DOM at the end.
- Use **"Canon"** as the neutral placeholder brand/name (used both in the nav logo and as the hero headline).
- `.hero-bg` holds a single full-bleed `<img>`; the `.progress` div is a fill nested inside `.progress-bar` (two stacked bars).

## Styling
Fonts (Google Fonts): **DM Sans** (variable, ital + opsz + wght) is the only web font. Import it. The big `h1`s declare `font-family: "LK_Ternima", "DM Sans", sans-serif` — `LK_Ternima` is an unavailable custom display face, so in practice everything renders in **DM Sans** (keep the fallback chain but do not attempt to load the missing font). `h1 { line-height: 1 }`.

Palette (CSS custom properties):
- `--base-100: #fff` (all text / progress fill / white)
- `--base-200: #3a3a3a` (progress-bar track color — dark grey)
- `--base-300: #0f0f0f` (page background — near-black)

Global:
- `* { margin:0; padding:0; box-sizing:border-box }`.
- `body { background: var(--base-300); color: var(--base-100) }`.
- `img { width:100%; height:100%; object-fit:cover }`.
- `a, p { text-decoration:none; color: var(--base-100); font-family:"DM Sans"; font-size:0.85rem; font-weight:500; line-height:1 }`.

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

- `.preloader-counter`: `position:fixed; top:50svh; left:2rem; transform: translateY(-50%) scale(0.25); transform-origin:left bottom; will-change:transform; z-index:2`. Its `h1`: `font-size: clamp(2.5rem, 25vw, 25rem)`. (It starts tiny at scale `0.25`, anchored by its bottom-left corner, and grows to full size.)

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

- `.hero`: `position:relative; width:100%; height:100svh; overflow:hidden`.

- `.hero-bg`: `position:absolute; inset:0; width:100%; height:100%; z-index:-1; will-change:clip-path`. **Initial** `clip-path: polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%)` (all four corners collapsed onto the center point → fully hidden).

- `.hero-bg img`: `position:absolute; top:50%; left:50%; transform: translate(-50%, -50%) scale(2); will-change:transform` (centered, zoomed to `2x`).

- `.header`: `position:absolute; bottom:4rem; width:100%; padding:2rem`. Its `h1`: `font-size: clamp(5rem, 18.5vw, 20rem)` (huge, bottom-left).

- `.hero-footer`: `position:absolute; bottom:2rem; width:100%; padding:2rem; display:flex; justify-content:space-between; align-items:flex-start` (three labels spread across the bottom).

- `.progress-bar`: `position:absolute; left:2rem; bottom:6rem; width:calc(100% - 4rem); height:1.5px; background:var(--base-200); transform-origin:left; transform:scaleX(0); will-change:transform; overflow:hidden` (the dark track, starts collapsed).
- `.progress-bar .progress`: `position:absolute; width:100%; height:100%; background:var(--base-100); transform-origin:left; transform:scaleX(0); will-change:transform` (the white fill inside the track, also starts collapsed).

Split-piece init states (these classes are produced by SplitText below; the CSS pre-hides them):
- `.word, .char, .digit { position:relative; will-change:transform }`.
- `.header h1 .char { transform: translateX(100%) }` (each headline char parked one char-width to the **right**, inside its mask).
- `nav a .word, .hero-footer p .word { transform: translateY(100%) }` (each nav/footer word parked one line **below**, inside its mask).

## GSAP effect (be exact)

### Setup
```js
gsap.registerPlugin(CustomEase, SplitText);
CustomEase.create("hop", "0.9, 0, 0.1, 1");   // steep symmetric in-out — used for the whole hero unmask

// Helper: split + assign a class + wrap each piece in an overflow-hidden mask
const splitText = (selector, type, className) =>
  SplitText.create(selector, {
    type: type,                 // "chars" | "words"
    [`${type}Class`]: className, // charsClass / wordsClass = className
    mask: type,                  // mask each char/word so translated pieces are clipped
  });

const headerSplit = splitText(".header h1", "chars", "char");   // → .char (masked chars)
const navSplit    = splitText("nav a", "words", "word");        // → .word (masked words)
const footerSplit = splitText(".hero-footer p", "words", "word"); // → .word (masked words)

const counterProgress  = document.querySelector(".preloader-counter h1");
const counterContainer = document.querySelector(".preloader-counter");
const counter = { value: 0 };  // plain object we tween and read in onUpdate
```

Everything runs on **one** timeline: `const tl = gsap.timeline();`. Position params below are **absolute times** (numbers) or `"<"` (align to the previously-added tween's start).

### 1 — Counter ticks up (`t = 0`, the timeline's first tween)
```js
tl.to(counter, {
  value: 100,
  duration: 3,
  ease: "power3.out",
  onUpdate: () => { counterProgress.textContent = Math.floor(counter.value); },
  onComplete: () => { /* digit wipe-out, see below */ },
});
```
The number counts `0 → 100` over **3s**, decelerating (`power3.out`), written as an integer each frame.

**onComplete (fires at `t ≈ 3`) — digit wipe-out:** split the now-`"100"` number into masked chars (`splitText(counterProgress, "chars", "digit")`), then:
```js
gsap.to(counterSplit.chars, {
  x: "-100%",
  duration: 0.75,
  ease: "power3.out",
  stagger: 0.1,
  delay: 1,                 // waits 1s after the count finishes → starts ~t=4
  onComplete: () => counterContainer.remove(),  // yank the whole counter out of the DOM
});
```
Each digit of "100" slides **left** out of its mask (`x: -100%`), staggered `0.1s`, so the number wipes away character by character; then the entire `.preloader-counter` element is removed.

### 2 — Counter scales up (parallel, `"<"` → `t = 0`)
```js
tl.to(counterContainer, { scale: 1, duration: 3, ease: "power3.out" }, "<");
```
The whole counter grows from its CSS `scale(0.25)` to `scale(1)` over the same 3s, anchored at its bottom-left (`transform-origin: left bottom`), so the number swells up from the corner as it counts.

### 3 — Progress-bar track draws in (parallel, `"<"` → `t = 0`)
```js
tl.to(".progress-bar", { scaleX: 1, duration: 3, ease: "power3.out" }, "<");
```
The dark track (`.progress-bar`) grows `scaleX 0 → 1` from the left edge over the same 3s — a thin line drawing itself across the screen in lockstep with the count.

### 4 — Hero image unmask, part 1: point → small rectangle (`t = 4.5`)
```js
tl.to(".hero-bg", {
  clipPath: "polygon(35% 35%, 65% 35%, 65% 65%, 35% 65%)",
  duration: 1.5,
  ease: "hop",
}, 4.5);
```
The `.hero-bg` clip-path opens from the collapsed center point to a **small centered rectangle** (30%×30% of the viewport) — the image first appears as a small window in the middle.

### 5 — Image de-zoom, part 1 (parallel, `"<"` → `t = 4.5`)
```js
tl.to(".hero-bg img", { scale: 1.5, duration: 1.5, ease: "hop" }, "<");
```
Simultaneously the image scales from `2 → 1.5`.

### 6, 7, 8 — Full unmask + de-zoom finish + progress fill (all at `t = 6`)
```js
tl.to(".hero-bg", {
  clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)",
  duration: 2, ease: "hop",
}, 6);
tl.to(".hero-bg img", { scale: 1, duration: 2, ease: "hop" }, 6);
tl.to(".progress",    { scaleX: 1, duration: 2, ease: "hop" }, 6);
```
- `.hero-bg` clip-path expands from the small rectangle out to the **full viewport** (`0%,0% … 100%,100%`) over 2s — the image blooms open to full-bleed.
- `.hero-bg img` finishes de-zooming `1.5 → 1` over the same 2s, so the image settles to its natural framing as it fills the frame.
- `.progress` (the white fill inside the track) sweeps `scaleX 0 → 1` from the left over the same 2s — a white line racing across the already-drawn dark track.

### 9 — Hero headline slides in char by char (`t = 7`)
```js
tl.to(".header h1 .char", {
  x: "0%",
  duration: 1,
  ease: "power4.out",
  stagger: 0.075,
}, 7);
```
Each headline character slides from `translateX(100%)` (parked right, hidden by its mask) into place, left to right, staggered `0.075s`, with a snappy `power4.out`.

### 10, 11 — Nav + footer words rise up (both at `t = 7.5`)
```js
tl.to("nav a .word",        { y: "0%", duration: 1, ease: "power4.out", stagger: 0.075 }, 7.5);
tl.to(".hero-footer p .word", { y: "0%", duration: 1, ease: "power4.out", stagger: 0.075 }, 7.5);
```
All nav-link words and all footer-label words slide up from `translateY(100%)` (below, hidden by their masks) into view, staggered `0.075s`, `power4.out`. Both start together at `t = 7.5`.

### Timeline summary (absolute seconds)
| t (s) | what |
|------|------|
| 0–3  | counter `0→100` (power3.out), counter scales `0.25→1`, progress track draws `scaleX 0→1` |
| ~3   | count done → onComplete splits digits |
| ~4–5 | digits wipe left (`x:-100%`, stagger 0.1) then `.preloader-counter` removed |
| 4.5–6 | hero clip-path point→small rect + image `2→1.5` (`hop`) |
| 6–8  | hero clip-path small-rect→full + image `1.5→1` + white progress fill `scaleX 0→1` (`hop`) |
| 7–8  | headline chars slide in from right (stagger 0.075, power4.out) |
| 7.5–8.5 | nav + footer words rise up (stagger 0.075, power4.out) |

Total runtime ≈ **8.5s**.

### Ease reference
- `hop` = `CustomEase.create("hop", "0.9, 0, 0.1, 1")` — steep symmetric in-out used for the entire hero clip-path unmask and image de-zoom.
- Counter, counter-scale, progress-track, and the digit wipe use `power3.out`.
- All SplitText slide-ins (headline chars, nav words, footer words) use `power4.out`.

## Assets / images
**One** hero background image (`hero.jpg`), used as the full-bleed backdrop (`object-fit: cover`, centered, initially `scale(2)`). It is the single subject of the clip-path reveal and the `2x → 1x` de-zoom.

The real asset is a **landscape (~3:2), classical Baroque oil painting** — a mythological figure scene of two nude figures in soft chiaroscuro: a reclining, sleeping figure on draped white linen at the lower left, and a seated winged figure leaning toward them at the right, with faint classical architecture and a distant sky in the background. Dominant colors are **warm and low-key**: creamy flesh tones and off-white drapery lit against deep browns and shadowed reds, with a muted blue-grey sky in the upper right. The overall feel is dim, painterly, and old-master. Its warm mid-tones and dark edges let the near-black `#0f0f0f` page and white type sit well against it (the cover-crop centers on the figures). Aspect ratio is flexible since it is cover-cropped; if unavailable, any dark, warm-toned painterly or editorial full-bleed image works.

## Behavior notes
- **Autoplay once** on load (`DOMContentLoaded`); no scroll, hover, or click triggers. The page does not scroll during the intro.
- The `.preloader-counter` element is permanently `.remove()`d from the DOM once its digits wipe out — do not rely on it existing afterward.
- Uses `100svh` (small viewport height) so mobile browser chrome doesn't clip the hero.
- Keep the `will-change` hints (`clip-path` on `.hero-bg`, `transform` on the image, counter, progress bars, and split pieces) — they matter for smooth clip-path and transform animation.
- **Responsive** (`@media max-width: 1000px`): `.nav-links` becomes a right-aligned vertical column (`flex-direction:column; align-items:flex-end; gap:0.5rem`); `.header` re-anchors to vertical center (`bottom:unset; top:50svh; transform:translateY(-50%)`, centered) and its `h1` drops to `font-size:4rem`. The animation itself is unchanged — only layout adapts.
