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

## Goal
Build a full-viewport editorial studio landing hero with a **preloader-to-hero reveal** that plays automatically once on page load (~7 seconds total). A tiny centered number, masked inside a small clipping window, slides up into view and rapidly ticks `0 → 100` with irregular random jumps. The instant it hits 100 it slides up and out of its window, and that same moment fires the reveal: a scaled-down (`0.7`), clip-path-collapsed hero opens from its bottom edge upward while scaling up to full size; the dark overlay covering it wipes upward off the top; the bottom hero image strip de-zooms from `2x` to `1x`; and the giant `38.5vw` headline's per-character `<span>`s rise up from far below into place with a stagger. Everything is driven by chained standalone GSAP tweens (no master timeline), stitched together with `onComplete` / `onStart` callbacks, using a `CustomEase` named `hop` plus `power3`/`power4` eases.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) plus the single GSAP plugin **`CustomEase`** (`import { CustomEase } from "gsap/CustomEase"`). No smooth-scroll library — the page does not scroll (`.container` is `overflow: hidden`, exactly one viewport). Register the plugin with `gsap.registerPlugin(CustomEase)` and run the whole sequence inside a `DOMContentLoaded` listener.

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

```
<div class="container">
  <div class="counter">
    <p>0</p>
  </div>

  <section class="hero">
    <div class="overlay"></div>

    <nav>
      <div class="nav-col">
        <div class="nav-items"><a href="#">zayno</a></div>
        <div class="nav-items"><p>digital studio</p></div>
      </div>
      <div class="nav-col">
        <div class="nav-items">
          <a href="#">work</a><a href="#">studio</a><a href="#">contact</a>
        </div>
        <div class="nav-items">
          <a href="#">twitter</a><a href="#">instagram</a>
        </div>
        <div class="nav-items"><p>toronto, ca</p></div>
      </div>
    </nav>

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

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

Notes:
- `.container` is the single 100vw × 100vh clipping frame (`overflow: hidden`); everything is absolutely positioned inside it.
- `.counter` holds one `<p>0</p>` — this same element is later mutated by the ticker and also GSAP-animated.
- Use **"zayno"** as the neutral placeholder studio name (both the nav logo and the hero headline). Nav text is neutral studio boilerplate: `digital studio`, `work / studio / contact`, `twitter / instagram`, `toronto, ca`.
- `.hero` wraps four stacked layers in DOM order: `.overlay` (dark cover, top of stack), `nav`, `.header` (giant word), `.hero-img` (bottom image strip).

## Styling
Fonts: body declares `font-family: "Test Söhne"`; the headline declares `.header h1 { font-family: "Blanquotey" }`. **Both are proprietary faces that are NOT bundled and NOT loaded** (no `@font-face`, no `<link>`) — declare the exact family names but do not attempt to fetch them; the browser falls back to its default (serif) for both, which is how the original renders. Optionally append a generic fallback.

Palette:
- Background `#ebebeb` (light warm grey) — the page/body.
- Text / overlay `#1a1a1a` (near-black).

Global:
- `* { margin:0; padding:0; box-sizing:border-box }`.
- `html, body { width:100%; height:100%; font-family:"Test Söhne"; background:#ebebeb; color:#1a1a1a }`.
- `img { width:100%; height:100%; object-fit:cover }`.
- `p, a { text-decoration:none; font-size:13px; color:#1a1a1a }`.

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

- `.container`: `width:100vw; height:100vh; overflow:hidden`.

- `.counter`: `position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:40px; height:20px; text-align:center; z-index:0`. **Initial** `clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%)` (full rectangle — a fixed 40×20px window that clips its content).
- `.counter p`: `position:relative; display:block; transform:translateY(20px)` (pushed down exactly one window-height, so it sits just BELOW the window and is hidden).

- `.hero`: `position:absolute; top:50%; left:50%; transform:translate(-50%,-50%) scale(0.7); width:100vw; height:100vh; will-change:transform; z-index:1`. **Initial** `clip-path: polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)` (all four corners collapsed onto the BOTTOM edge → zero height → hidden). So the hero starts shrunk to `0.7` and clipped to nothing.

- `.overlay`: `position:absolute; top:0; left:0; width:100%; height:100%; background:#1a1a1a; z-index:2`. **Initial** `clip-path: polygon(0 100%, 100% 100%, 100% 0, 0 0)` (full rectangle — a solid dark panel covering the entire hero; it is the top-most layer inside `.hero`).

- `nav`: `position:absolute; top:0; left:0; width:100%; padding:1em; display:flex` (no z-index → sits below the overlay, revealed when the overlay wipes). `.nav-col { flex:1; display:flex }`. `.nav-items { flex:1; display:flex; flex-direction:column }`. `.nav-col:nth-child(2) .nav-items:last-child p { text-align:right }`.

- `.header`: `position:absolute; width:100%; top:-5%; left:0; z-index:1`. **Static** `clip-path: polygon(0 100%, 100% 100%, 100% 0, 0 0)` (full rectangle — this one is NEVER animated; it just masks the overflow of the rising spans).
- `.header h1`: `font-family:"Blanquotey"; font-size:38.5vw; font-weight:lighter; text-align:center; line-height:100%` (one enormous centered word spanning the viewport).
- `.header h1 span`: `position:relative; display:inline-block; transform:translateY(500px)` (each character parked 500px below its final position, hidden well under the `.header` clip window).

- `.hero-img`: `position:absolute; bottom:0; width:100%; height:35vh; overflow:hidden; z-index:0` (a full-width strip pinned to the bottom, 35vh tall).
- `.hero-img img`: `transform:scale(2)` (the image starts zoomed to 2×, cover-cropped).

Z-index stack inside `.hero` (matters for the reveal to read right): `.hero-img` (0) & `nav` (auto) < `.header` (1) < `.overlay` (2). The dark overlay covers everything until it wipes up.

## GSAP effect (be exact)

### Setup
```js
gsap.registerPlugin(CustomEase);
CustomEase.create(
  "hop",
  "M0,0 C0.29,0 0.348,0.05 0.422,0.134 0.494,0.217 0.484,0.355 0.5,0.5 0.518,0.662 0.515,0.793 0.596,0.876 0.701,0.983 0.72,0.987 1,1 "
);
```
Note: `hop` is defined in **SVG-path form** (not the 4-number cubic-bezier shorthand). Reproduce this exact path string — it is a springy, slightly wavy accelerate-then-settle in-out curve used for the two clip-path animations.

**Manual character split** (NOT the SplitText plugin): a small helper wraps every character of `.header h1` in its own `<span>`. Take the element's `innerText`, `split("")`, map each char to `` `<span>${char === " " ? "&nbsp;&nbsp;" : char}</span>` `` (a space becomes two `&nbsp;`), `join("")`, and write it back as `innerHTML`. Run this on `.header h1` before animating. (For the word "zayno" this yields 5 spans, no spaces.)

There is **no master timeline** — the effect is a chain of independent `gsap.to()` calls wired together through callbacks. Sequence:

### 1 — Counter number slides into its window (fires on load, `delay: 1`)
```js
gsap.to(".counter p", {
  y: 0,
  duration: 1,
  ease: "power3.out",
  delay: 1,
  onComplete: () => animateCounter(),
});
```
After a **1s** initial delay, the `<p>` slides from `translateY(20px)` up to `y: 0` over **1s** with `power3.out`, sliding "0" up into the 20px clip window. On complete it launches the ticker.

### 2 — The random-increment ticker (`animateCounter`, plain JS, NOT a GSAP tween)
A hand-rolled counter, not a tween:
```js
const el = document.querySelector(".counter p");
let currentValue = 0;
const updateInterval = 300;   // ms between ticks
const maxDuration   = 2000;   // ms total counting window
const endValue      = 100;
const startTime = Date.now();

const updateCounter = () => {
  const elapsed = Date.now() - startTime;
  if (elapsed < maxDuration) {
    currentValue = Math.min(currentValue + Math.floor(Math.random() * 30) + 5, endValue);
    el.textContent = currentValue;
    setTimeout(updateCounter, updateInterval);
  } else {
    el.textContent = endValue;                 // snap to 100
    setTimeout(() => {
      gsap.to(el, {
        y: -20,
        duration: 1,
        ease: "power3.inOut",
        onStart: () => revealLandingPage(),
      });
    }, -500);                                   // negative delay → fires next tick
  }
};
updateCounter();
```
So the number jumps upward by a random `5–34` every **300ms**, clamped at 100, for **2000ms** (~7 irregular ticks). When the 2s window closes it snaps to `100`, then the same `<p>` tweens `y: 0 → -20` (slides up and out the TOP of its window) over **1s** `power3.inOut`, and its `onStart` triggers the whole page reveal at the same instant.

### 3 — The reveal (`revealLandingPage`) — one outer tween whose `onStart` fires four parallel tweens
```js
const revealLandingPage = () => {
  gsap.to(".hero", {
    clipPath: "polygon(0% 100%, 100% 100%, 100% 0%, 0% 0%)",
    duration: 2,
    ease: "hop",
    onStart: () => {
      gsap.to(".hero", {
        transform: "translate(-50%, -50%) scale(1)",
        duration: 2.25, ease: "power3.inOut", delay: 0.25,
      });
      gsap.to(".overlay", {
        clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",
        duration: 2, delay: 0.5, ease: "hop",
      });
      gsap.to(".hero-img img", {
        transform: "scale(1)",
        duration: 2.25, ease: "power3.inOut", delay: 0.25,
      });
      gsap.to(".header h1 span", {
        y: 0,
        stagger: 0.1,
        duration: 2, ease: "power4.inOut", delay: 0.75,
      });
    },
  });
};
```
Breaking down each simultaneous tween (all start at reveal time `t0`, offset by their own `delay`):

- **(a) Hero clip-path opens — `t0`, duration 2, `hop`.** `.hero` clip-path animates from the collapsed bottom edge `polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)` to the full rectangle `polygon(0% 100%, 100% 100%, 100% 0%, 0% 0%)` — the top two vertices travel from `y:100%` up to `y:0%`, so the hero unmasks from the bottom upward. This is the outer tween; its `onStart` fires the other three the same frame.

- **(b) Hero scales up — `t0 + 0.25`, duration 2.25, `power3.inOut`.** `.hero` `transform` goes from `translate(-50%,-50%) scale(0.7)` to `... scale(1)` — the whole hero grows from 70% to full size, staying centered, in near-lockstep with the clip-path opening.

- **(c) Overlay wipes upward off the top — `t0 + 0.5`, duration 2, `hop`.** `.overlay` clip-path animates from the full rectangle `polygon(0 100%, 100% 100%, 100% 0, 0 0)` to the top-edge-collapsed `polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)` — the bottom two vertices travel from `y:100%` up to `y:0%`, so the dark panel retracts upward and off, uncovering the nav, headline and image beneath it.

- **(d) Hero image de-zooms — `t0 + 0.25`, duration 2.25, `power3.inOut`.** `.hero-img img` `transform` goes from `scale(2)` to `scale(1)` — the bottom image strip un-zooms to its natural framing as the overlay clears it.

- **(e) Headline characters rise — `t0 + 0.75`, duration 2, `power4.inOut`, stagger 0.1.** Every `.header h1 span` animates `y: 500px → 0`, left-to-right, each starting **0.1s** after the previous, so the giant word climbs into place letter by letter behind the (static) header mask.

### Timeline summary (approximate absolute seconds from load)
| t (s) | what |
|-------|------|
| 1.0–2.0 | counter `<p>` slides up into its window (`y:20→0`, power3.out) |
| ~2.0–4.0 | random ticker `0 → 100` (jumps of 5–34 every 300ms, over 2000ms) |
| ~4.0 | snap to 100 → counter slides out top (`y:0→-20`, power3.inOut) AND reveal fires (`onStart`) |
| ~4.0–6.0 | hero clip-path opens bottom→full (`hop`) |
| ~4.25–6.5 | hero scales `0.7→1` + hero image de-zooms `2→1` (power3.inOut) |
| ~4.5–6.5 | dark overlay wipes upward off the top (`hop`) |
| ~4.75–6.9 | headline spans rise `y:500→0`, stagger 0.1 (power4.inOut) |

Total runtime ≈ **7s**.

### Ease reference
- `hop` = the SVG-path `CustomEase` above (springy in-out) — used for BOTH clip-path animations (hero open + overlay wipe).
- Counter slide-in uses `power3.out`; counter slide-out uses `power3.inOut`.
- Hero scale + image de-zoom use `power3.inOut`.
- Headline character rise uses `power4.inOut`.

## Assets / images
**One** hero image, used as the bottom full-width strip (`.hero-img`, `height:35vh`, `object-fit:cover`, starting `scale(2)`, de-zooming to `1`). It is a **moody, warm-toned editorial photograph** — low-key, atmospheric, with warm amber/brown tones that sit well against the light-grey page and dark type. Aspect ratio is flexible since it is cover-cropped inside the strip; any dark, warm, editorial full-bleed image works. If unavailable, use any single moody warm-toned photo.

## Behavior notes
- **Autoplay once** on load (`DOMContentLoaded`); no scroll, hover, or click triggers. The page never scrolls — `.container` is exactly one viewport with `overflow: hidden`.
- The counter number and its slide/tick logic operate on the **same** `.counter p` element throughout (slide-in → text-ticker → slide-out); don't split these into separate nodes.
- Keep the `will-change: transform` hint on `.hero` — it matters for the smooth simultaneous scale + clip-path.
- The `.header` clip-path is static (a mask); only the `.overlay` and `.hero` clip-paths animate.
- **Responsive** (`@media max-width: 900px`): the first nav column's second `<p>` becomes right-aligned; the entire second nav column is `display:none`; `.header` re-anchors to `top:30%`; `.hero-img` grows to `height:50vh`. The animation itself is unchanged — only layout adapts.
