# Counter Curtain Hero Reveal — Load Counter, Rotating Word, Clip-Path Curtain & Flip-Expanding Frame

## Goal
Build a landing-page intro. A dark preloader shows the brand name top-left and a **rotating word** top-right (Studios → Season → Chamber → Archive → Vision), a **three-digit counter** bottom-left that runs 000 → 100, a caption bottom-right and a thin **iridescent progress line**. During the same 3 seconds a small **glass photo frame** in the hero (hidden under the preloader) **cycles through ten photos** and **travels from the section's left edge to its slot** inside the second headline row. Then: the preloader text **blurs and fades**, the panel **wipes upward** with a custom "hop" ease, the three headline rows **slide their words in from their masks** (row 1 and 3 from the left, row 2 from the right), the caption and nav fade/slide in, and the frame **expands with GSAP Flip until it fills the whole section** behind the type, which is `mix-blend-mode: difference` so it stays readable on any photo.

## Tech
Vanilla HTML/CSS/JS with ES module imports. `gsap` (npm) with **`CustomEase`**, **`SplitText`** and **`Flip`**. Font: **Hanken Grotesk** (500).

## Layout / HTML
```html
<div class="cc">
  <div class="cc-preloader" aria-hidden="true">
    <div class="cc-preloader__header">
      <div class="cc-preloader__row"><h1>Underlume</h1></div>
      <div class="cc-preloader__row"><h1 class="cc-preloader__word">Studios</h1></div>
    </div>
    <div class="cc-preloader__footer">
      <div class="cc-preloader__counter"><h1>000</h1></div>
      <div class="cc-preloader__copy"><p>Currently developing</p></div>
    </div>
    <span class="cc-preloader__bar"></span>
  </div>

  <nav class="cc-nav"><p>Underlume</p><p>Menu</p></nav>

  <section class="cc-hero">
    <div class="cc-hero__header">
      <div class="cc-hero__row"><h1>Everything</h1></div>
      <div class="cc-hero__row">
        <h1>Beneath</h1>
        <div class="cc-hero__frame"><!-- ten <img> --></div>
      </div>
      <div class="cc-hero__row"><h1>The surface</h1></div>
    </div>
    <div class="cc-hero__footer"><p>Seen and unseen</p></div>
  </section>
</div>
```

## Styling
`:root`: `--cc-ink:#f3f2ee`, `--cc-paper:#f3f2ee`, `--cc-preloader:#1b1f22`, `--cc-line:rgba(255,255,255,.22)`, `--cc-frame-w:275px`, `--cc-frame-h:150px`.

- `.cc` — `min-height:100svh; background:var(--cc-paper); font-family:"Hanken Grotesk"`. All `h1, p`: `margin:0; text-transform:uppercase; font-weight:500; letter-spacing:-.02em; line-height:.85`; `h1 { font-size: clamp(2.5rem, 10vw, 15rem) }`, `p { font-size:.85rem; letter-spacing:.02em; line-height:1 }`.
- `.cc-preloader` — `position:fixed; inset:0; z-index:20; display:flex; flex-direction:column; justify-content:space-between; padding:2.5rem; color:var(--cc-ink); background: radial-gradient(120vmax 60vmax at 50% 120%, rgba(255,255,255,.08), transparent 60%), var(--cc-preloader); clip-path:polygon(0% 0%,100% 0%,100% 100%,0% 100%); overflow:hidden`. Second header row is `justify-content:flex-end`; footer is `display:flex; justify-content:space-between; align-items:flex-end`; counter uses `font-variant-numeric:tabular-nums`.
- `.cc-preloader__bar` — `position:absolute; left:2.5rem; right:2.5rem; bottom:1.4rem; height:1px; background:linear-gradient(90deg, rgba(255,255,255,.9), rgba(178,196,255,.9), rgba(255,184,226,.8), rgba(160,255,228,.8)); transform:scaleX(0); transform-origin:left; opacity:.8`.
- `.cc-nav` — `position:fixed; inset:0 0 auto 0; z-index:5; display:flex; justify-content:space-between; padding:2.5rem; color:#fff; mix-blend-mode:difference`.
- `.cc-hero` — `position:relative; height:100svh; padding:2.5rem; display:flex; align-items:flex-end; overflow:hidden; isolation:isolate; background:var(--cc-paper)` (the background inside the isolated group is what makes `difference` work). `.cc-hero__header` — `position:relative; z-index:1; width:100%; display:flex; flex-direction:column`. Rows: `flex:1; display:flex; align-items:center`; rows 1 and 3 `justify-content:flex-end`; row 2 `gap:2.5rem`. Row `h1`: `color:#fff; mix-blend-mode:difference`.
- `.cc-hero__frame` — `position:relative; flex-shrink:0; width:var(--cc-frame-w); height:var(--cc-frame-h); border-radius:10px; overflow:hidden; box-shadow:0 0 0 1px var(--cc-line), 0 30px 60px -30px rgba(0,0,0,.5); z-index:2`. Its images are `position:absolute; inset:0; object-fit:cover; opacity:0`, `.is-active { opacity:1 }`. A `::after` gradient (`transparent 55% → rgba(0,0,0,.35)`) fades in over 1s when the frame has `.is-full`.
- `.cc-hero__frame.is-full` — `position:absolute; inset:0; width:auto; height:auto; border-radius:0; box-shadow:none; z-index:0`.
- `.cc-hero__footer` — `position:absolute; left:2.5rem; bottom:2.5rem; z-index:1`; its `p` is white, `mix-blend-mode:difference`, `opacity:0`.
- `.cc-word` (SplitText word class) — `position:relative`.
- `@media (max-width:1000px)`: `--cc-frame-w:100px; --cc-frame-h:56px`, paddings `1.5rem`, row-2 gap `1rem`, footer `bottom:25svh`.

## GSAP effect (be exact)
`gsap.registerPlugin(CustomEase, SplitText, Flip)`; `CustomEase.create("cc-hop", "0.8, 0, 0.1, 1")`. Build in `mount(config)` → `destroy()`.

```js
const DEFAULTS = { startDelay: .5, loadDuration: 3, curtainDuration: 1, wordDuration: 1.25, expandDuration: 1.25, imageLoops: 3 };
const ROTATING = ["Studios", "Season", "Chamber", "Archive", "Vision"];
```

**Prep**
- Split each hero `h1` with `SplitText.create(h, { type:"words", mask:"words", wordsClass:"cc-word" })`; set words of row 2 to `xPercent: 100`, rows 1 and 3 to `xPercent: -100`.
- `gsap.set(nav, { y: -300 })`; preloader `clipPath` full, `display:flex`; the three fading blocks `opacity:1, filter:"blur(0px)"`; bar `scaleX:0`; footer `opacity:0`; first image `.is-active`; counter `000`; word `Studios`.
- Frame start offset: `pad = parseFloat(getComputedStyle(hero).paddingLeft)`; `leftOffset = pad + hero.getBoundingClientRect().left - frame.getBoundingClientRect().left`; `gsap.set(frame, { x: leftOffset })` — the frame begins flush with the section's left padding.

**Renderers** driven by plain objects:
- `counter.p` → `textContent = String(round(p)).padStart(3, "0")`.
- `wordCycle.p` → `ROTATING[round(p)]` (only update when the index changes).
- `imageCycle.p` → `round(p) % images.length` toggles `.is-active`.

**Expand** — `state = Flip.getState(frame)`; add `.is-full`; `gsap.set(frame, { x: 0 })`; `Flip.from(state, { duration: expandDuration, ease: "cc-hop", absolute: true })`.

**Timeline** (`delay: startDelay`, `L = loadDuration`):
1. `0` — `counter { p: 100, duration: L, ease: "none", onUpdate }`.
2. `0` — bar `scaleX: 1`, `L`, `ease:"none"`.
3. `0` — frame `x: 0`, `L`, `ease:"none"`.
4. `0` — `wordCycle { p: ROTATING.length - 1, duration: L, ease:"none", onUpdate }`.
5. `0` — `imageCycle { p: images.length * imageLoops - 1, duration: L, ease:"none", onUpdate }` (three full laps over ten photos).
6. `"+=0.3"` — header, counter and copy `{ opacity: 0, filter: "blur(10px)", duration: .35, stagger: .04 }`; `"<"` bar `opacity: 0`, .2s.
7. next — preloader `clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)"`, `curtainDuration`, `ease: "cc-hop"`, `onComplete` → `display:none`.
8. `"-=0.5"` — all words `xPercent: 0`, `wordDuration`, `power3.out`, `onComplete: expand`.
9. `"<"` — footer `opacity: 1`, 1s, `power3.out`.
10. `"<"` — nav `y: 0`, 1s, `power3.out`.

**destroy()** — kill timeline/tweens, revert splits, remove `.is-full`, `clearProps` on nav/preloader/bar/footer/frame/fading blocks, reset images/counter/word.

## Assets / images
Ten landscape photos (≈16:9, ≥1200px — the last one becomes the full-section background, so it should be the strongest): a coherent set of landscapes and architecture in a muted palette. Photos 1–9 are only ever seen at 275×150 for a fraction of a second each.

## Behavior notes
- The whole thing is a one-shot intro of ≈7s; the preloader is `position:fixed` and removed from view at the end.
- `mix-blend-mode: difference` on the headline needs a painted background inside the same stacking context (`.cc-hero` has `isolation:isolate` and its own background) — without it the text renders white on white.
- Flip with `absolute:true` lets the frame leave the flex row without the row collapsing; the `Beneath` heading keeps its place because it precedes the frame.
- Mobile shrinks the frame to 100×56 and the paddings; the sequence is otherwise identical.

## Images

This component ships with 10 reference assets, served publicly.
Use them as-is to reproduce the demo faithfully, then swap in your own — the layout expects the
same aspect ratios.

```
https://motionprompts.dev/c/counter-curtain-hero-reveal/img1.jpg
https://motionprompts.dev/c/counter-curtain-hero-reveal/img10.jpg
https://motionprompts.dev/c/counter-curtain-hero-reveal/img2.jpg
https://motionprompts.dev/c/counter-curtain-hero-reveal/img3.jpg
https://motionprompts.dev/c/counter-curtain-hero-reveal/img4.jpg
https://motionprompts.dev/c/counter-curtain-hero-reveal/img5.jpg
… 4 more under https://motionprompts.dev/c/counter-curtain-hero-reveal/
```

They are hotlinkable for prototyping. For anything you ship, replace them: they are licensed for
demonstration of this component, not for redistribution.

## Using this outside its demo page

This component is written as a complete page — that is how the demo is meant to look. If you are dropping it into an existing project, or combining it with other components, these are the things it declares at document level and that you need to move or reconcile first.

- **Palette on `:root`** — `--cc-ink`, `--cc-paper`, `--cc-preloader`, `--cc-line`, `--cc-frame-w`, `--cc-frame-h`. These names are not namespaced and they collide: `--ink` is defined by 164 of the 219 components in this catalogue, `--paper` by 94, `--muted` by 80, each with different values — and they will also collide with whatever your own project defines. Move them onto the component's wrapper (`.my-section { --ink: … }`) or rename them with a prefix.
- **Rules on `html, body`** — the demo owns the whole document, so these set the page background, typography and resets. Dropped into an existing project they restyle the entire page, not just this section. Re-target them at the component's wrapper before using it.
- **Full-screen overlay** — a fixed element covers the viewport (a loader or transition). Only one may exist per page and it must remove itself when done. If your page already has one, keep that and drop this; otherwise the second silently hides the first.
