# Sci-Fi Terminal Preloader Button → Hero Clip-Path Reveal

## Goal
Build a **self-playing terminal-styled preloader** that fills a circular SVG "loading" button, then waits for a **click to wipe the whole preloader away and reveal a hero headline**. On load: monospace status lines rise into view from behind masks (SplitText), a thin **circular SVG outline draws itself while the whole ring rotates 270°**, and a second **progress stroke fills in randomized, jittery stages** like a loader stalling and jumping. When it finishes, the centered logo fades out and an **"Engage" label rises up** — the button is now armed. **Clicking** the button scales the black preloader down, **unwinds both SVG strokes off the ring**, swaps the label from "Engage" up-and-out to "Access Granted" in, then **wipes the black preloader and a white revealer panel leftward via `clip-path`** while the hero **scales up from 0.75 → 1** and its headline **words stagger up out of masks**. The star effect is the **self-drawing + rotating + randomly-stepping SVG loader ring**, followed by the **synchronized left clip-path wipe → hero word reveal**.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) plus the GSAP plugins **`SplitText`** and **`CustomEase`**. No smooth-scroll library and no scroll interaction at all — the intro is a load-triggered timeline and the reveal is a `click`-triggered timeline. Imports:
```js
import gsap from "gsap";
import { SplitText } from "gsap/SplitText";
import CustomEase from "gsap/CustomEase";
```
Register once at module top: `gsap.registerPlugin(SplitText, CustomEase)`. Immediately create two custom eases:
```js
CustomEase.create("hop",   "0.9, 0, 0.1, 1");
CustomEase.create("glide", "0.8, 0, 0.2, 1");
```
All logic runs inside `document.addEventListener("DOMContentLoaded", ...)`.

## Layout / HTML
Three stacked fixed/relative layers, painted back-to-front by z-index:

- **`.preloader-backdrop`** (fixed, full-viewport, **white** bg, gray text, `z-index: 0`) — a decorative terminal HUD sitting behind everything. Flex column, `justify-content: space-between`, so its content pins to top and bottom. It holds two `.pb-row`s (each `display:flex; justify-content:space-between; padding:1.5rem`):
  - Top `.pb-row`: five `.pb-col`s of monospace `<p>` lines — e.g. a repeated call-sign stacked 5×, a sector/id pair, a material/status pair, then a `.pb-col` containing **`#pb-logo`** (a small `<img>` with a dashed border), then a decorative glyph line.
  - Bottom `.pb-row`: six `.pb-col`s of short status `<p>` lines (single-word and two-line labels, a code like "F-9"), aligned to `flex-end`.
- **`.preloader`** (fixed, full-viewport, **black** bg, white text, `z-index: 2`, `overflow` visible) — the active layer. Flex column, `justify-content: space-between`. Its initial `clip-path` is the **full rectangle** `polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)`. Contains:
  - `.p-row` with a single `<p>` "Initiating".
  - `.p-row` with a `.p-col` (two `.p-sub-col` groups of stacked `<p>` labels, `gap: 6rem`, aligned `flex-end`) and a second `.p-col` with a `<p>` code (e.g. "PX-17").
  - `.preloader-btn-container` — **absolutely centered, 20rem × 20rem**, the click target. Inside, all absolutely centered on the same point:
    - `#pbc-logo` — a white `<img>` logo mark, `4rem × 4rem`.
    - `#pbc-label` — `<p>` "Engage".
    - `#pbc-outro-label` — `<p>` "Access Granted".
    - `.pbc-svg-strokes` — an `<svg viewBox="0 0 320 320">` containing **two concentric `<circle>`s**, `cx=160 cy=160 r=155 stroke-width=2`:
      - `.stroke-track` — dark stroke (`#2b2b2b`).
      - `.stroke-progress` — white stroke (`#fff`).
      Both start with `stroke-dasharray` and `stroke-dashoffset` equal to the circumference (≈974) so nothing is drawn.
- **`.hero`** (relative, full-viewport, **black** bg, white text, `z-index` below preloader) — flex-centered, `text-align:center`, and **starts `transform: scale(0.75)`**. Contains:
  - `.preloader-revealer` — an absolutely positioned **white** panel covering the hero, with initial `clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)` (full rectangle → hero hidden behind white).
  - `<h1>` headline — e.g. "The system is now visible", width 90%.

Keep all copy neutral/fictional sci-fi terminal text (call-signs, phase labels, "Engage", "Access Granted"). No real brand or client names.

## Styling
Google Fonts: **Barlow Condensed** (weights 100–900, italics available) and **Geist Mono** (100–900).

Color tokens:
- `--base-100: #fff` (white)
- `--base-200: #7a7a7a` (mid gray)
- `--base-300: #000` (black)

Type:
- `h1`: Barlow Condensed, **uppercase**, `font-size: clamp(5rem, 15vw, 15rem)`, weight **800**, `letter-spacing: -2%`, `line-height: 0.8`.
- `p`: Geist Mono, **uppercase**, `font-size: 0.75rem`, weight 500, `line-height: 1` (button labels `#pbc-label`/`#pbc-outro-label` are `0.9rem`).
- `.preloader-backdrop` is white bg + `--base-200` gray text; `.preloader` and `.hero` are black bg + white text.

**Split-piece initial state (load-bearing for the masked reveals):**
```css
h1 .word,
p  .line {
  position: relative;
  transform: translateY(100%);   /* every split line/word starts one line below */
  will-change: transform;
}
```

Key positioning / will-change (the animation depends on these):
- `.preloader` → `will-change: transform, clip-path`.
- `.preloader-btn-container` → `position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:20rem; height:20rem`.
- `.pbc-svg-strokes`, `#pbc-logo`, `#pbc-label`, `#pbc-outro-label` → all `position:absolute; top:50%; left:50%; transform:translate(-50%,-50%)` (perfectly stacked). `.pbc-svg-strokes` and its `svg` are `width:100%; height:100%; will-change:transform`.
- `#pbc-logo` → `4rem × 4rem`. `#pb-logo` → `2.5rem × 2.5rem`, `padding:0.25rem`, `border:1px dashed var(--base-200)`.
- `.hero` → `transform: scale(0.75); will-change:transform`.
- `.hero .preloader-revealer` → `position:absolute; inset:0; width:100%; height:100%; background:var(--base-100); will-change:clip-path`.

Responsive `@media (max-width:1000px)`: hide the top `.pb-row`'s 1st, 2nd, and 5th `.pb-col`s (`display:none`) to declutter the HUD on narrow screens. Nothing else about the animation changes.

## GSAP effect (exhaustive)

### Setup (once, inside DOMContentLoaded)
```js
let preloaderComplete = false;

const preloaderTexts   = document.querySelectorAll(".preloader p");
const preloaderBtn     = document.querySelector(".preloader-btn-container");
const btnOutlineTrack  = document.querySelector(".stroke-track");
const btnOutlineProgress = document.querySelector(".stroke-progress");
const svgPathLength    = btnOutlineTrack.getTotalLength();   // ≈ 973.9 for r=155
```
- `gsap.set([btnOutlineTrack, btnOutlineProgress], { strokeDasharray: svgPathLength, strokeDashoffset: svgPathLength })` — normalize both circles to the measured circumference and hide them (nothing drawn).
- **SplitText**: for **each** `.preloader p`, `new SplitText(p, { type: "lines", linesClass: "line", mask: "lines" })` (masked lines). For the hero, `new SplitText(".hero h1", { type: "words", wordsClass: "word", mask: "words" })` (masked words). The `mask` option wraps each piece in a clipped element so the `translateY(100%)` start state hides it fully.

### Intro timeline (auto-plays on load)
`const introTl = gsap.timeline({ delay: 1 });` — a **1s initial delay**, then:

1. **Status lines rise.** `.to(".preloader .p-row p .line", { y: "0%", duration: 0.75, ease: "power3.out", stagger: 0.1 })` — every masked line inside the two `.p-row`s slides up into view, 0.1s apart. (Only `.p-row` lines here — the button labels are split too but animated later.)
2. **Track draws itself.** `.to(btnOutlineTrack, { strokeDashoffset: 0, duration: 2, ease: "hop" }, "<")` — position `"<"` = starts together with step 1. The dark ring outline draws from nothing to a full circle over 2s.
3. **Whole ring rotates.** `.to(".pbc-svg-strokes svg", { rotation: 270, duration: 2, ease: "hop" }, "<")` — also starts with step 1; the SVG spins 0° → 270° while it draws, so the stroke appears to sweep around.
4. **Randomized progress fill.** Build stops:
   ```js
   const progressStops = [0.2, 0.25, 0.85, 1].map((base, i) =>
     i === 3 ? 1 : base + (Math.random() - 0.5) * 0.1);   // last is exactly 1; others jitter ±0.05
   ```
   For each `stop` (index `i`), append:
   ```js
   introTl.to(btnOutlineProgress, {
     strokeDashoffset: svgPathLength - svgPathLength * stop,  // fills to `stop` fraction
     duration: 0.75,
     ease: "glide",
     delay: i === 0 ? 0.3 : 0.3 + Math.random() * 0.2,        // stalls between jumps
   });
   ```
   These are sequential (default end-of-timeline position). Net feel: the white progress stroke jumps to ~20%, pauses, nudges to ~25%, pauses, leaps to ~85%, then snaps to 100% — a stuttering, "real loader" fill.
5. **Logo fades out.** `.to("#pbc-logo", { opacity: 0, duration: 0.35, ease: "power1.out" }, "-=0.25")` — overlaps back 0.25s into the last progress jump.
6. **Button shrinks.** `.to(preloaderBtn, { scale: 0.9, duration: 1.5, ease: "hop" }, "-=0.5")` — the whole button container eases from scale 1 → 0.9.
7. **"Engage" label rises + arm the button.** `.to("#pbc-label .line", { y: "0%", duration: 0.75, ease: "power3.out", onComplete: () => { preloaderComplete = true; } }, "-=0.75")` — the masked "Engage" text slides up where the logo was, and its `onComplete` flips `preloaderComplete = true` so the click handler will now fire.

### Exit timeline (click `.preloader-btn-container`)
Listener on `preloaderBtn`: bail if `!preloaderComplete`; otherwise set `preloaderComplete = false` (one-shot) and run `const exitTl = gsap.timeline();`:

1. **Preloader scales down.** `.to(".preloader", { scale: 0.75, duration: 1.25, ease: "hop" })`.
2. **Both strokes unwind off the ring.** `.to([btnOutlineTrack, btnOutlineProgress], { strokeDashoffset: -svgPathLength, duration: 1.25, ease: "hop" }, "<")` — offset goes to **negative** circumference, so both circles animate out of view (unraveling), starting together with step 1.
3. **"Engage" exits up.** `.to("#pbc-label .line", { y: "-100%", duration: 0.75, ease: "power3.out" }, "-=1.25")` — the current label slides up and out of its mask.
4. **"Access Granted" enters.** `.to("#pbc-outro-label .line", { y: "0%", duration: 0.75, ease: "power3.out" }, "-=0.75")` — the outro label rises up into the same spot right behind it.
5. **Preloader wipes left.** `.to(".preloader", { clipPath: "polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)", duration: 1.5, ease: "hop" })` — the two right-hand corners collapse onto the left edge, so the black preloader wipes off to the left.
6. **Revealer wipes left (reveals hero).** `.to(".preloader-revealer", { clipPath: "polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)", duration: 1.5, ease: "hop", onComplete: () => gsap.set(".preloader", { display: "none" }) }, "-=1.45")` — starts 1.45s before the previous ends, i.e. **nearly in lockstep** with the preloader wipe. The white revealer covering the hero collapses to the left too, uncovering the black hero + headline; on complete the now-empty preloader layer is hidden.
7. **Hero scales up.** `.to(".hero", { scale: 1, duration: 1.25, ease: "hop" })` — hero eases 0.75 → 1 (settles to full size as it's revealed).
8. **Headline words stagger in.** `.to(".hero h1 .word", { y: "0%", duration: 1, ease: "glide", stagger: 0.05 }, "-=1.75")` — each masked word slides up into place 0.05s apart, overlapping back 1.75s so the words are rising while the hero is still scaling/revealing.

### Eases used
- **`hop`** = `CustomEase "0.9, 0, 0.1, 1"` — a hard, near-symmetric ease used for the ring draw/rotate, the scale changes, the stroke unwind, and the clip-path wipes.
- **`glide`** = `CustomEase "0.8, 0, 0.2, 1"` — softer S-curve for the progress fill and the final headline words.
- `power3.out` for the masked text rises, `power1.out` for the logo fade.

## Assets / images
Two logo images (a third dark variant may exist but is unused by the component), all **~1:1 square PNGs on transparent background**, each a small **monochrome geometric emblem** (e.g. a fragmented/split-hexagon glyph):
1. **Neutral/gray mark** — used as `#pb-logo`, the tiny dashed-bordered badge in the white backdrop HUD (`2.5rem`).
2. **White mark** — used as `#pbc-logo`, centered inside the circular button (`4rem`), and fades out mid-intro.

Provide simple, high-contrast marks; exact glyph is not important, aspect 1:1.

## Behavior notes
- **Two triggers only:** the intro is `load`-driven (1s delay, ~6s total), then it **waits for a user click**. The `preloaderComplete` boolean gates the click — the exit can't run until "Engage" has finished rising, and it's reset to `false` on click so the exit fires exactly once.
- No ScrollTrigger, no Lenis, no scroll interaction anywhere.
- The whole thing is percentage/`clip-path`/`vw`-based, so it scales fluidly; the `≤1000px` media query only hides three decorative HUD columns.
- Reduced-motion is **not** handled in the original — the animation always plays.
