# Editorial Fullscreen Food Menu — Right-Docked Panel Expands to Full-Bleed Columns with Rising Split-Letter Reveal

## Goal
Build a magazine-style navigation overlay. A tiny fixed **"Menu"** button sits over a full-viewport food photo. Clicking it makes a **narrow cream panel docked to the right edge expand across the whole screen** into **five equal vertical columns**, while **giant condensed-serif labels reveal letter-by-letter, rising up from below** each column with a staggered climb. Clicking **"Close"** reverses everything. Once open, **hovering a column reveals a food image** (a clip-path box growing from a center point to full rectangle) and **swaps the label** between a dark version sliding out and a muted duplicate sliding in. The star effect is the combination of the **CustomEase "hop" panel-width expansion** and the **per-letter `power3.out` staggered vertical reveal** of oversized rotated type.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use `gsap` (npm) plus the GSAP plugin **`CustomEase`**. No ScrollTrigger, no SplitText (the letter splitting is done by a small hand-written helper, not the plugin), no smooth-scroll library — there is **no scroll interaction at all**; the whole thing is a fixed-position click-toggled overlay. Import as:
```js
import gsap from "gsap";
import { CustomEase } from "gsap/CustomEase";
gsap.registerPlugin(CustomEase);
```
Ship `index.html`, `styles.css`, and an ES-module `script.js` (`<script type="module" src="./script.js">`).

## Layout / HTML
```
.menu                              (fixed, docked TOP-RIGHT, width:20vw, height:100vh, display:flex, z-index:2)
  .menu-item            × 5        (flex:1 vertical column; one per nav entry)
    .menu-item-index
      <p>01</p>                     (the index number, stays horizontal)
      <p>Why</p>                    (the short word label, rotated -90deg)
    .menu-link.menu-link-main
      <p>Why</p>                    (giant condensed-serif label — dark)
    .menu-link.menu-link-hover
      <p>Why</p>                    (identical giant label — muted taupe, the hover duplicate)
    .menu-img
      <img>                         (per-column food image, hidden until hover)
  .menu-close                       (fixed TOP-RIGHT, z-index:10)
    <p>Close</p>

.container                          (full-viewport landing view; hero food photo as CSS background)
  .menu-open                        (fixed TOP-LEFT, z-index:1)
    <p>Menu</p>
```
The five items, in order (`index number` / `word label` — the **same word is repeated three times per item**: once in `.menu-item-index`, once in `.menu-link-main`, once in `.menu-link-hover`):

1. `01` / **Why**
2. `02` / **Who**
3. `03` / **What**
4. `04` / **How**
5. `05` / **Join**

No brand, no logo — the only text on screen is `Menu`, `Close`, the five numbers, and the five words. Keep these neutral labels.

## Styling

**Global / typography**
- `* { margin:0; padding:0; box-sizing:border-box; }`
- `html, body { width:100%; height:100%; font-family:"PP Neue Montreal"; }` — a neutral grotesque for all the small UI text (self-host or substitute any clean sans such as Inter/Helvetica Neue).
- `img { width:100%; height:100%; object-fit:cover; }`
- `p { text-transform:uppercase; font-size:12px; font-weight:500; line-height:100%; }` — this governs every small label (Menu, Close, index numbers, words).

**Color palette (only three colors)**
- Cream / off-white **`#eee5d2`** — the `.menu-item` background AND the `.menu-open` ("Menu") text.
- Muted taupe **`#a39b89`** — the `.menu-close` text, the `.menu-item-index` text, the column border (`rgba(163,155,137,0.35)`), and the hover label (`rgba(163,155,137,0.85)`).
- Dark espresso **`#2c221d`** — the main giant label text.

**Landing view**
- `.container { width:100%; height:100%; background:url(<hero>) no-repeat 50% 50%; background-size:cover; }` — full-bleed hero food render behind everything.

**Toggle buttons (Menu / Close) — clip-path is the reveal mask**
- `.menu-open { position:fixed; top:0; left:0; z-index:1; color:#eee5d2; }`
- `.menu-close { position:fixed; top:0; right:0; z-index:10; color:#a39b89; }`
- Shared: `.menu-open, .menu-close { margin:0.75em; padding:0.75em; clip-path:polygon(0 0, 100% 0, 100% 100%, 0% 100%); cursor:pointer; }` — the rectangular `clip-path` acts as an **overflow mask**: a `<p>` translated vertically out of the padding box is clipped, giving a text-slide reveal.
- `.menu-open p, .menu-close p { position:relative; }`

**The panel & columns**
- `.menu { position:fixed; top:0; right:0; width:20vw; height:100vh; display:flex; z-index:2; }` — starts as a slim right-edge strip; GSAP animates its `width`.
- `.menu-item { position:relative; flex:1; height:100%; display:flex; justify-content:center; background:#eee5d2; border-left:1.5px solid rgba(163,155,137,0.35); clip-path:polygon(0 0, 100% 0, 100% 100%, 0% 100%); margin-left:-1px; }` — the full-rectangle `clip-path` masks the oversized type that overflows the column.
- `.menu-item-index { height:100%; padding:1.5em 0.5em 2em 0.5em; display:flex; flex-direction:column; justify-content:space-between; align-items:center; color:#a39b89; }`
- `.menu-item-index p:nth-child(2) { position:relative; transform:rotate(-90deg); }` — the word label at the bottom of the index column reads vertically.

**The giant rotated labels**
- `.menu-link { position:absolute; bottom:2.5%; left:55%; transform:translate(-50%,-50%) rotate(-90deg); }` — anchored near the column's bottom, rotated so the huge text reads vertically (bottom-to-top).
- **Third column exception:** `.menu-item:nth-child(3) .menu-link { bottom:7.5%; }` (the longer word "What" needs a bit more room).
- `.menu-link p { position:relative; display:flex; font-family:"OPTICaslon-ExtraCondensed"; font-size:200px; font-weight:lighter; transition:all 0.5s; }` — an **extra-condensed serif display face** at 200px (substitute any narrow high-contrast serif; the extreme condensation is the point). `display:flex` so the per-letter `<span>`s sit in a row.
- `.menu-link p span { position:relative; }`
- `.menu-link-main p { color:#2c221d; }` (dark)
- `.menu-item .menu-link-main, .menu-item .menu-link-hover { transition:all 0.5s; }`
- `.menu-item .menu-link-hover { left:150%; color:rgba(163,155,137,0.85); }` — the duplicate hover label starts parked off to the right.

**Per-column image**
- `.menu-img { position:absolute; width:75%; height:35%; top:25%; left:50%; transform:translate(-50%,-50%); clip-path:polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%); transition:0.5s all cubic-bezier(0.165, 0.84, 0.44, 1); }` — the collapsed-to-a-point `clip-path` hides the image entirely; it lives in the upper portion of the column.

**CSS hover interactions (pure CSS transitions — NOT GSAP)**
- Label swap: `.menu-item:hover .menu-link-main { left:-100%; }` (dark label slides out to the left) and `.menu-item:hover .menu-link-hover { left:50%; }` (taupe duplicate slides in from `150%` to center) — both over the `all 0.5s` transition, a horizontal label cross-swap.
- Image reveal (only once the menu is fully open): `.menu-item.menu-opened:hover .menu-img { clip-path:polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%); }` — the clip-path grows from a center point to a full rectangle over `0.5s cubic-bezier(0.165, 0.84, 0.44, 1)` (easeOutQuart), unmasking the food image. The `.menu-opened` class is only present after the open animation completes (see GSAP §3), so images never reveal in the collapsed strip.

## GSAP effect (exhaustive)

### Setup (runs once on load)
1. Register the plugin and define the signature ease:
   ```js
   CustomEase.create("hop", "M0,0 C0.091,0.543 0.148,0.662 0.277,0.786 0.405,0.909 0.596,0.979 1,1");
   ```
   This "hop" curve rises fast, eases through a soft mid-plateau, then settles — used **only** for the panel-width tween.
2. **Split every giant label into per-character spans** with a hand-rolled helper (no SplitText plugin):
   ```js
   function splitTextIntoSpans(selector){
     document.querySelectorAll(selector).forEach((el) => {
       el.innerHTML = el.innerText.split("")
         .map((ch) => `<span>${ch === " " ? "&nbsp;&nbsp;" : ch}</span>`).join("");
     });
   }
   splitTextIntoSpans(".menu-link p");
   ```
   (A literal space becomes two `&nbsp;`.) Runs on both `.menu-link-main p` and `.menu-link-hover p`.
3. **Initial GSAP states:**
   - `gsap.set(".menu-close p", { y: 40 })` — the "Close" word is pushed 40px down, clipped out of its masked button (hidden on load).
   - `gsap.set(".menu-link p span", { y: 250 })` — every letter is pushed 250px down its own axis, sitting below the column's masked bottom edge (hidden on load).
   - `.menu-open p` is left at its default `y:0` (the "Menu" word is visible on load).
4. State flag `let isMenuOpen = false;`. Wire `click` on **both** `.menu-open` and `.menu-close` to the same `handleMenu` handler.

### The single toggle handler (`handleMenu`)
Every click fires the following tweens **in parallel**. Crucially, `isMenuOpen` is read *before* it flips — it only flips inside the `onComplete` of the align-items tween — so on a given click all ternaries use the pre-click value. `isMenuOpen === false` ⇒ **OPEN targets**; `isMenuOpen === true` ⇒ **CLOSE targets**.

1. **Panel width (the hero move):**
   ```js
   gsap.to(menu, { width: isMenuOpen ? "20vw" : "100vw", duration: 1, ease: "hop" });
   ```
   Open: `20vw → 100vw` (the right strip stretches across the whole viewport, so the five `flex:1` columns each become 20vw wide and fill the screen). Close: `100vw → 20vw`. **This is the only tween using the `"hop"` CustomEase.**

2. **Column content alignment:**
   ```js
   gsap.to(".menu-item", { justifyContent: isMenuOpen ? "center" : "flex-start", duration: 1, ease: "power3.out" });
   ```
   Open: `center → flex-start`. Close: reverse. (Animating a flex keyword — GSAP tweens it as a discrete/step change synced to the 1s duration.)

3. **Index column alignment + state flip (drives the `.menu-opened` class):**
   ```js
   gsap.to(".menu-item-index", {
     alignItems: isMenuOpen ? "center" : "flex-start",
     duration: 1, ease: "power3.out",
     onComplete: () => {
       isMenuOpen = !isMenuOpen;
       menuItems.forEach((it) => it.classList.toggle("menu-opened", isMenuOpen));
     },
   });
   ```
   Open: `center → flex-start`. On complete it flips `isMenuOpen` and **adds `.menu-opened`** to every item (enabling the hover image reveal); on close it removes the class. This is the single source of truth for the toggle state.

4. **"Close" button reveal:**
   ```js
   gsap.to(".menu-close p", { y: isMenuOpen ? 40 : 0, duration: 1, ease: "power3.out" });
   ```
   Open: `y 40 → 0` ("Close" slides up into its masked button). Close: `y → 40` (slides back out of view).

5. **"Menu" button hide:**
   ```js
   gsap.to(".menu-open p", { y: isMenuOpen ? 0 : -40, duration: 1, ease: "power3.out" });
   ```
   Open: `y 0 → -40` ("Menu" slides up and out of its mask). Close: `y → 0` (slides back in).

6. **Per-letter staggered label reveal (the signature climb):** loop over each `.menu-item`, grab its letters (`.menu-link p span`), and tween them **per column**:
   ```js
   menuItems.forEach((menuItem) => {
     const letters = menuItem.querySelectorAll(".menu-link p span");
     gsap.to(letters, {
       delay: isMenuOpen ? 0 : 0.25,
       y: isMenuOpen ? 250 : 0,
       duration: 1,
       stagger: isMenuOpen ? -0.075 : 0.075,
       ease: "power3.out",
     });
   });
   ```
   - **Open** (`isMenuOpen === false`): `delay 0.25`, `y 250 → 0` (letters rise up from below the clipped edge into place), `stagger 0.075` (forward — letters appear left-to-right along the rotated baseline). The 0.25s delay lets the panel begin expanding before the type climbs.
   - **Close** (`isMenuOpen === true`): `delay 0`, `y 0 → 250` (letters drop back down out of view), `stagger -0.075` (reverse order). No delay — type retreats immediately.
   - Because both `.menu-link-main` and `.menu-link-hover` spans are selected, both label copies rise/fall together. Each of the five columns runs its own independent stagger in parallel, so all columns reveal simultaneously (each internally left-to-right).

There is no timeline object and no ScrollTrigger — everything is concurrent `gsap.to`/`gsap.set` calls sharing the same 1s duration, with the 0.25s letter delay on open being the only offset.

## Assets / images
Six **glossy, stylized 3D-rendered fast-food "icon" images on a solid near-black background** (studio-lit, high-saturation, single hero object centered). All are `object-fit:cover`, so exact aspect ratio is forgiving — supply roughly **portrait / square** renders (the reveal box is a small landscape-ish window, ~75%×35% of a column). One hero + one per column:

- **hero** (`.container` background, full-bleed): a domed-lid takeaway cup of **iced coffee / frappé with a swirl of whipped topping and chocolate drizzle**, warm caramel tones. Fills the whole landing viewport behind the "Menu" button.
- **img1 — "Why":** a **pink-glazed ring donut with rainbow sprinkles**, three-quarter view.
- **img2 — "Who":** a **frothy glass mug of golden lager** with an overflowing foam head.
- **img3 — "What":** a **glossy cheeseburger** — sesame bun, melted cheese, tomato, lettuce.
- **img4 — "How":** a **red carton of thick-cut french fries** standing upright.
- **img5 — "Join":** a whole **pepperoni pizza**, top-down.

Any cohesive set of playful, saturated 3D food renders on a dark background works — no brands or logos.

## Behavior notes
- **Desktop-oriented** (`mobileSafe:false`). At `max-width:900px` the CSS reflows: `.menu` becomes `flex-direction:column`, labels un-rotate and drop to `font-size:80px` centered (`top/left:50%`, `rotate(0)`), `.menu-link-hover` and `.menu-img` are `display:none`, and the index column centers. The GSAP width tween still runs (20vw→100vw), so on narrow screens the columns stack vertically once expanded.
- No `prefers-reduced-motion` branch; no resize listener; no infinite loops. Every animation is a one-shot, 1-second click-driven toggle.
- The hover label-swap and hover image-reveal are **CSS transitions**, gated by the `.menu-opened` class that GSAP adds/removes on toggle completion — so hovering does nothing while the panel is still the collapsed strip.
- Re-clicking mid-animation is not guarded in the original; the tweens simply retarget. Keep it simple and match that (no re-entrancy lock).
- Fonts: `"PP Neue Montreal"` for UI text and `"OPTICaslon-ExtraCondensed"` for the giant labels. Self-host via `@font-face` if available, otherwise substitute a clean grotesque + an extra-condensed serif display face; the extreme condensation and 200px scale are essential to the look.
