# Block Reveal Page Transition (pixel-grid flicker cover-and-reveal)

## Goal
Build a client-side "fake router" for a small four-page site where **navigating between pages plays a full-screen grid of small dark blocks that flickers in with a random stagger to fully cover the viewport, swaps the page content underneath while covered, then flickers the blocks back out to reveal the new page**. The star effect is the two-phase random-stagger opacity flicker of a procedurally generated 60px block grid acting as a transition curtain. Trigger is a click on the fixed bottom nav links.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) only — **no GSAP plugins, no ScrollTrigger, no SplitText, no Lenis**. There is no scroll interaction at all. Import as:
```js
import gsap from "gsap";
```
No `gsap.registerPlugin` call is needed.

## Layout / HTML
```
.transition-grid            (fixed full-viewport overlay; JS fills it with blocks)

nav                         (fixed bottom-center pill row)
  a[data-route="/"]         "Homebase"
  a[data-route="/gateway"]  "Gateway"
  a[data-route="/station"]  "Station"
  a[data-route="/colony"]   "Colony"

section.page.active[data-route="/"]          (Homebase — first page, visible on load)
  .section-bg > img         (full-bleed background image)
  .section-header > h1      "Homebase"
section.page[data-route="/gateway"]          (Gateway)
  .section-bg > img
  .section-header > h1      "Gateway"
section.page[data-route="/station"]          (Station)
  .section-bg > img
  .section-header > h1      "Station"
section.page[data-route="/colony"]           (Colony)
  .section-bg > img
  .section-header > h1      "Colony"
```
- The `.transition-grid` starts **empty** in the HTML — JS builds and appends all the block `<div>`s.
- Each nav link carries a `data-route` matching a `section.page`'s `data-route`. Only one `section.page` has class `active` at a time.
- Use neutral fictional page names (four sci-fi place labels like Homebase / Gateway / Station / Colony). No real brands.

## Styling
Fonts:
- Display: **SCHABO** (condensed uppercase display face, via `@import url("https://fonts.cdnfonts.com/css/schabo")`), used for `h1`.
- UI: **DM Mono** (Google Fonts, weights 300/400/500 + italics), used for nav links.

Color tokens:
- `--base-100: #fcfcfc` (near-white text)
- `--base-200: #3f3f3f` (mid-grey page background, seen only briefly)
- `--base-300: #0f0f0f` (near-black — the transition blocks AND the nav pill background)
- `body { background-color: var(--base-200); color: var(--base-100); }`

Type:
- `h1`: `text-transform: uppercase; font-family:"SCHABO", sans-serif; font-size: clamp(6rem, 15vw, 12rem); font-weight:500; line-height:0.9;`
- `a`: `text-transform: uppercase; color: var(--base-100); font-family:"DM Mono", monospace; font-size:0.8rem; font-weight:450; line-height:1; text-decoration:none;`
- `img { width:100%; height:100%; object-fit: cover; }`

Key structural CSS (load-bearing):
- `nav`: `position: fixed; bottom: 5rem; left: 50%; transform: translateX(-50%); display:flex; gap:0.25rem; z-index:2;`
- `nav a`: `padding:0.5rem 0.75rem; background-color: var(--base-300); border-radius:0.2rem; transition: transform 200ms ease-out;` and `nav a:active { transform: scale(0.9); }` (a small press feedback on click).
- `section`: `position:relative; width:100%; height:100svh; display:flex; justify-content:center; align-items:center; overflow:hidden;`
- **Fake-router visibility:** `section.page { display:none; }` and `section.page.active { display:flex; }` — only the active page is in the layout; the others are removed entirely.
- `.section-bg`: `position:absolute; width:100%; height:100%; z-index:-1;` (image sits behind the centered `h1`).
- `.transition-grid`: `position:fixed; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index:100; overflow:hidden;` — it sits above everything (nav is z-index 2, grid is z-index 100) and never blocks clicks.
- `.transition-block`: `position:absolute; background-color: var(--base-300); opacity:0;` — every block is absolutely positioned by inline `left`/`top`/`width`/`height` from JS and starts fully transparent.

## GSAP effect (exhaustive)

### Constants & grid generation
- `const BLOCK_SIZE = 60;` — each block is a 60×60px square.
- `createTransitionGrid()` builds the curtain:
  - `gridWidth = window.innerWidth`, `gridHeight = window.innerHeight`.
  - `columns = Math.ceil(gridWidth / BLOCK_SIZE)`.
  - `rows = Math.ceil(gridHeight / BLOCK_SIZE) + 1` (one extra row so the bottom edge is always covered).
  - `offsetX = (gridWidth - columns * BLOCK_SIZE) / 2` and `offsetY = (gridHeight - rows * BLOCK_SIZE) / 2` — centers the grid (offsets are ≤ 0, so the grid slightly overhangs to guarantee full coverage).
  - Nested loop `row` × `col`: create `div.transition-block`, set inline `width/height = 60px`, `left = col*60 + offsetX`, `top = row*60 + offsetY`, append to `.transition-grid`, and push into a `blocks[]` array.
  - Before generating, clear the grid: `transitionGrid.innerHTML = ""` and reset `blocks = []`.
  - After building, `gsap.set(blocks, { opacity: 0 })`.
- Call `createTransitionGrid()` once on load, and **rebuild it on `window.resize`** (`window.addEventListener("resize", createTransitionGrid)`).

### The two transition tweens (this is the whole effect)
Two functions, each a single `gsap.to(blocks, …)` over the block array, each with an `onComplete` callback (`next`) to chain the sequence:

**`leave(next)` — cover the screen:**
```js
gsap.to(blocks, {
  opacity: 1,
  duration: 0.05,
  ease: "power2.inOut",
  stagger: { amount: 0.5, from: "random" },
  onComplete: next,
});
```
- Every block animates `opacity: 0 → 1`.
- Per-block `duration` is only **0.05s** (a fast flicker-on), but the whole set is spread across **`stagger: { amount: 0.5, from: "random" }`** — total stagger window of **0.5s**, and `from:"random"` shuffles the order so blocks light up in a scattered, dissolve-in pattern rather than any directional sweep. Net wall-clock time ≈ 0.55s.

**`enter(next)` — reveal the new page:**
```js
gsap.set(blocks, { opacity: 1 });          // ensure fully covered first
gsap.to(blocks, {
  opacity: 0,
  duration: 0.05,
  delay: 0.3,
  ease: "power2.inOut",
  stagger: { amount: 0.5, from: "random" },
  onComplete: next,
});
```
- First force all blocks to `opacity: 1` (fully opaque cover), then tween `opacity: 1 → 0`.
- Same `duration: 0.05`, same `ease: "power2.inOut"`, same `stagger: { amount: 0.5, from: "random" }`, plus a **`delay: 0.3`** — a 0.3s hold on full black before the blocks flicker back out (this pause is what hides the content swap). Net wall-clock time ≈ 0.85s.

### Sequencing (`navigate(route)`)
State: `currentRoute = "/"`, `isTransitioning = false`.
- Guard: if `isTransitioning` or `route === currentRoute`, return (ignore re-clicks and clicks on the current page).
- Set `isTransitioning = true`, then:
  1. `leave(() => { … })` — blocks flicker IN to cover.
  2. In `leave`'s `onComplete`: `showPage(route)` (toggle `.active` so the new `section.page` becomes the only visible page) and `currentRoute = route`. **The swap happens while the screen is fully covered.**
  3. Then call `enter(() => { isTransitioning = false; })` — after the 0.3s hold, blocks flicker OUT to reveal, and re-entrancy unlocks on complete.
- `showPage(route)`: loop all `.page` sections, `page.classList.toggle("active", page.dataset.route === route)`.

### Trigger wiring
For every `nav a`, `addEventListener("click", e => { e.preventDefault(); navigate(link.dataset.route); })`. (No History API / URL change — it is a purely visual fake router.)

## Assets / images
**4 images**, one full-bleed background per page (`.section-bg > img`, `object-fit: cover`), all **16:9 landscape** (~1456×816) to fill the viewport. Role: atmospheric full-screen sci-fi scene behind each page's giant `h1`. Each has a distinct dominant color so the page swap is obvious through the flicker. The four, in nav order:
- **Homebase (`bg_1`):** curved silver/chrome futuristic towers being swallowed by a churning **orange-amber sandstorm** on a desert world; wrecked pods half-buried in the foreground. Dominant colors: burnt orange, tan, glints of steel grey.
- **Gateway (`bg_2`):** a glowing **golden energy portal** (a rectangular doorway with a swirling vortex) framed by two tall rocky spires under a **teal starry night sky**, with turquoise crystals scattered on the ground and stone steps leading up. Dominant colors: deep teal/green sky vs warm gold portal glow.
- **Station (`bg_3`):** a **glass geodesic dome colony** lit up from within, sitting on a red-rock **Mars-like desert**; a huge mottled **orange planet** looms in a green-tinged starry sky. Dominant colors: rust red, amber dome lights, teal-green sky.
- **Colony (`bg_4`):** black **silhouetted spires and towers** of a dense city against an enormous **red-orange sun/star**, with small ships drifting through smoky haze. Dominant colors: fiery red-orange over near-black silhouettes.

Provide 4 distinct landscape images matching these roles, forms and color palettes (no real brands).

## Behavior notes
- **Desktop and mobile** both work; there is no `prefers-reduced-motion` branch and no min-width gate.
- The grid is **regenerated on every resize**, so the block count adapts to the viewport; `gsap.set` re-zeroes opacity each rebuild.
- `.transition-grid` has `pointer-events: none`, so even at full black cover the nav underneath (z-index 2 vs grid z-index 100) can't be clicked — but the `isTransitioning` guard already blocks input during a transition.
- No infinite loops; each animation is one-shot per click. Total transition ≈ 1.4s door-to-door (leave ~0.55s + 0.3s hold + reveal ~0.5s).
- The first page (`/` Homebase) is visible on load with no intro animation; transitions only fire on nav clicks.
