# Interactive Calendar Cards — click a grid pad to flood-fill and reveal a card

## Goal
Build a centered column of dark "month" cards on a near-black page. Each card shows its month name and a randomized **contribution-graph style grid of small square pads**, most of them dim, with a handful of brightly colored **active** pads. The star effect: clicking an active pad makes it **scale up 20× to flood-fill the whole card in its color**, and once the card is filled, a detail panel (image + title + paragraph + link) **flies in from below with a staggered `elastic.out` bounce and a random per-item rotation that unwinds to zero**. A **Back** button reverses the whole thing — the panel drops away and the pad shrinks back to a single cell.

## Tech
Vanilla HTML/CSS/JS with ES module imports. Use **`gsap` (npm) only** — **no GSAP plugins, no Lenis, no Three.js, no ScrollTrigger**. All animation is plain `gsap.to` / `gsap.fromTo` / `gsap.set` tweens triggered by click. Ship one `index.html` (`<link rel="stylesheet" href="./styles.css">` + `<script type="module" src="./script.js">`), one `styles.css`, one ES-module `script.js`, and a `data.js` module that default-exports the card data array. Must run in a fresh Vite + npm project.

The revealed link uses an **Ionicons** `arrow-forward-outline` glyph. Load Ionicons in the `<head>` via its ESM web-component script (CDN is fine, it is not an npm dep):
```html
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
```

## Layout / HTML
The static HTML is tiny — everything else is generated by JS. Only a container with a nav lives in markup:
```html
<div class="container">
  <nav>
    <div class="logo"><a href="#">Motionprompts</a></div>
    <div class="nav-items">
      <a href="#">Blog</a>
      <a href="#">Contact</a>
    </div>
  </nav>
</div>
```
`script.js` appends one `.card` per month **into `.container`**, after the nav. The per-card DOM the JS builds is:
```html
<div class="card">
  <div class="card-title"><p>May</p></div>
  <div class="card-content"><!-- empty until a pad is clicked --></div>
  <div class="row"><div class="pad"></div> …7 pads… </div>   <!-- row 0 -->
  <div class="row"><div class="pad"></div> …7 pads… </div>   <!-- row 1 -->
  <div class="row"><div class="pad"></div> …7 pads… </div>   <!-- row 2 -->
  <div class="row"><div class="pad"></div> …7 pads… </div>   <!-- row 3 -->
  <div class="row"><div class="pad"></div> …2–4 pads… </div> <!-- row 4 (random count) -->
</div>
```
When an active pad is clicked, `.card-content` is filled with:
```html
<button>Back</button>
<div class="card-item img"><img src="<detail image>" alt="" /></div>
<div class="card-item copy">
  <h1>Modern Art Evolution</h1>
  <p>Explore the dynamic evolution of modern art…</p>
</div>
<div class="card-item copy link">
  <a href="https://example.com/…">Explore More</a>
  <ion-icon name="arrow-forward-outline"></ion-icon>
</div>
```

### Data (`data.js`)
Default-export an array of single-key month objects; each key's value is an array of item objects `{ img, h1, copy, linkLabel, linkSrc }`. Use 4 months so the demo shows different active-pad counts per card (the number of items in a month === how many active pads that card gets):
- **May** → 4 items, **June** → 1 item, **July** → 3 items, **August** → 2 items.
Copy voice is a neutral art/gallery blurb (titles like "Modern Art Evolution", "Sculpture Innovations", "Abstract Painting Odyssey", "Graphic Design Trends", "Contemporary Photography", "Mixed Media Masterpieces", "Conceptual Art Series", "Street Art Chronicles"; each `copy` is one 1–2 sentence paragraph; `linkLabel` like "Explore More" / "Discover Now" / "Read More"; `linkSrc` a placeholder `https://example.com/...`). No client or third-party brand names.

## Styling
- Reset: `* { margin:0; padding:0; box-sizing:border-box; }`.
- `html, body { width:100%; height:150%; font-family:"Rubik"; background-color:#1a1a1a; }` — note the deliberate **`height:150%`** (leaves scroll room below the stacked cards). Load **Rubik** from Google Fonts (300–900). (Big Shoulders Display may also be linked but is not used for visible text.)
- `img { width:100%; height:100%; object-fit:cover; }`.
- `.container { width:100%; height:100%; }`.
- `nav { width:30%; margin:2em auto 0 auto; padding:2em 0.25em; display:flex; justify-content:space-between; align-items:center; }`. `.nav-items { display:flex; gap:2em; }`. `nav a { text-decoration:none; color:#fff; }`.
- **`.card`**: `position:relative; width:460px; height:400px; margin:2em auto; padding:2em; border-radius:1.5rem; display:flex; flex-direction:column; gap:0.5em; background-color:#272727; overflow:hidden;` — the `overflow:hidden` is essential: it clips the pad as it scales 20× so the enlargement reads as the card flooding with color.
- `.card-title { color:#fff; padding-bottom:1em; }`.
- `.row { width:100%; display:flex; gap:0.5em; }`.
- **`.pad`**: `position:relative; width:50px; height:50px; border-radius:0.5rem; background-color:#333333; z-index:0; pointer-events:none;` — dim grey cells, non-interactive by default.
- **`.pad.active`**: `pointer-events:all; cursor:pointer;` (JS also paints each active pad a bright color inline).
- Decorative dim: `.card .row:nth-child(3) .pad:nth-child(1), …:nth-child(2), …:nth-child(3) { opacity:0.35; }` — because `.card-title` and `.card-content` are the card's first two children, `:nth-child(3)` is the **first pad row**; its first three pads render at 35% opacity (a subtle top-left fade).
- **`.card-content`**: `position:absolute; top:0; left:0; width:100%; height:100%; padding:2em; color:#fff; overflow-y:auto; pointer-events:none; z-index:2; opacity:0;` — the reveal panel, stacked above the pads (z-index 2), starts fully transparent and click-through.
- `button { border:none; outline:none; border-radius:4em; font-family:"Rubik"; padding:0.5em 1em; background-color:#fff; cursor:pointer; }` (the white "Back" pill).
- `.img { width:100%; height:200px; margin:1em 0; border-radius:1em; overflow:hidden; }` (fixed 200px-tall image well).
- `.copy { margin:0.5em 0; padding:1.5em; border-radius:1em; background:#fff; color:#000; }`, `.copy h1 { font-size:20px; font-weight:500; margin-bottom:0.35em; }`, `.copy p { font-size:14px; line-height:150%; color:gray; }`.
- `.copy.link { display:flex; justify-content:space-between; align-items:center; margin:1em 0; }` (label on the left, arrow icon on the right).
- `.card-item { position:relative; }`.
- Media `@media (max-width:900px)`: `nav { width:100%; padding:2em; }`; `.card { width:360px; height:320px; gap:0.25em; }`; `.row { gap:0.25em; }`; `.pad { width:40px; height:40px; }`.

## GSAP effect (be exhaustive)

### Build order (`DOMContentLoaded`)
```js
import gsap from "gsap";
import data from "./data.js";
```
Constants used while building:
```js
const activeColors = ["#5fa5f9", "#e879f9", "#a78bfa", "#2cd4bf"]; // blue, magenta, violet, teal
```
For each month object in `data`: read the month name (its single key) and its `items` array. Create `.card`, then `.card-title` (`<p>${month}</p>`), then an **empty** `.card-content`, then call `generatePads(card, items.length, items)`, then append the card to `.container`.

### Grid generation — `generatePads(card, activePadCount, items)`
```js
const rowsConfig = [7, 7, 7, 7, Math.floor(Math.random() * 3) + 2]; // 5 rows: four of 7, last of 2–4
const clickablePads = [];
rowsConfig.forEach((padCount, rowIndex) => {
  const row = document.createElement("div"); row.classList.add("row");
  for (let i = 0; i < padCount; i++) {
    const pad = document.createElement("div"); pad.classList.add("pad");
    row.appendChild(pad);
    // only the MIDDLE rows are eligible to become active (skip first and last row)
    if (rowIndex !== 0 && rowIndex !== rowsConfig.length - 1) clickablePads.push(pad);
  }
  card.appendChild(row);
});
shuffleArray(clickablePads);                 // Fisher–Yates in place
setActivePads(clickablePads, card, activePadCount, items);
```
So every card has **5 rows**: rows 0–3 hold 7 pads each and the last row holds a random 2–4 pads. The **eligible ("clickable") pool is rows 1, 2, 3 only** (21 pads); the top row and the last row are never activated. `shuffleArray` is a standard in-place Fisher–Yates shuffle.

### Activating pads — `setActivePads(clickablePads, card, activePadCount, items)`
Take the **first `activePadCount`** pads of the shuffled pool (`activePadCount` === number of items for that month). For each such `pad` at index `i`:
- `pad.classList.add("active")`.
- Paint it a **random** color: `pad.style.backgroundColor = activeColors[Math.floor(Math.random()*activeColors.length)]`.
- Attach a **click** handler (below). The item shown is `items[i]` — the i-th active pad maps to the i-th data item.

### The reveal (click on an active pad) — the star tween
On click:
1. **Raise this pad above its siblings** so its growth covers the card: `clickablePads.forEach(p => p.style.zIndex = "0"); pad.style.zIndex = "1";`.
2. Populate `card.querySelector(".card-content").innerHTML` with the Back button + three `.card-item` blocks (img / copy / copy.link) from `items[i]` (see HTML above).
3. **Flood-fill:** scale the clicked pad up 20×:
   ```js
   gsap.to(pad, {
     scale: 20,
     duration: 0.3,
     onComplete: () => { /* step 4 + 5 */ }
   });
   ```
   The pad is a 50×50 rounded square; at `scale:20` it becomes a 1000×1000 block of its color that overflows and paints the entire 460×400 card (clipped by the card's `overflow:hidden`). No ease specified → GSAP default `power1.out`.
4. **Fade the panel in** (inside the scale's `onComplete`): `gsap.to(cardContent, { opacity: 1, pointerEvents: "all", duration: 0.075 })` — the `.card-content` (z-index 2) becomes visible and interactive almost instantly over the flooded color.
5. **Fly the three items in with an elastic stagger** (also in the same `onComplete`, started together with step 4):
   ```js
   gsap.fromTo(
     cardContent.querySelectorAll(".card-item"),
     { y: 100, rotation: () => gsap.utils.random(-30, 30), opacity: 0 },
     { y: 0, rotation: 0, opacity: 1, duration: 2, ease: "elastic.out", stagger: 0.1 }
   );
   ```
   Each of the 3 `.card-item` blocks starts **100px below**, **transparent**, and **rotated a random amount between −30° and +30°** (the `rotation` FROM value is a **function** so every item gets its own random tilt), then springs to `y:0, rotation:0, opacity:1`. `duration:2`, `ease:"elastic.out"` (GSAP's default `elastic.out(1, 0.3)` — a pronounced overshoot-and-settle bounce), `stagger:0.1` so item 2 starts 0.1s after item 1 and item 3 0.1s after item 2. The result: image, then title card, then link row each drop in and wobble to rest.

### The reversal (click "Back")
A click handler on `card.querySelector("button")`:
```js
gsap.to(cardContent, {
  opacity: 0, pointerEvents: "none", duration: 0.2,
  onComplete: () => {
    gsap.to(pad, {
      scale: 1, duration: 0.3,
      onComplete: () => {
        pad.style.zIndex = "0";
        cardContent.style.opacity = "0";
        cardContent.style.pointerEvents = "none";
        gsap.set(cardContent.querySelectorAll(".card-item"), { clearProps: "all" });
      }
    });
  }
});
```
Sequence: the panel **fades out** (`duration:0.2`), then the pad **shrinks back** from 20× to `scale:1` (`duration:0.3`, default ease), then state is reset — pad z-index back to 0, panel forced transparent & click-through, and `clearProps:"all"` on the `.card-item`s wipes the inline transforms so a re-open animates cleanly from scratch. After this the same (or another) active pad can be clicked again.

### Timing summary (one open→close cycle)
open pad scale→20 (0.3s) → [panel fade-in 0.075s ∥ 3 items elastic fly-in, 2s each, 0.1s stagger] … Back → panel fade-out 0.2s → pad scale→1 (0.3s) → reset.

## Assets / images
**8 distinct editorial / product detail photos**, each dropped into a fixed 200px-tall rounded well with `object-fit:cover` (source aspect ratios vary — landscape, portrait and square all work). They are moody, single-subject studio/product shots on dark or neutral backgrounds (e.g. a glossy 3D app icon on grey, a frosted cosmetic bottle catching a shaft of light, a kraft-paper product box on wood, a white wireframe figure on black, a spotlit pump bottle, a metallic bottle in a gift tin, numbered capsule containers on black, a phone screen glowing a single word in the dark). Any set of 8 clean, high-contrast product/editorial images reads correctly. There are **10 item slots** across the four months (4+1+3+2), so two images are reused (the two used by the first card also serve the last card). No logos or real brand marks.

## Behavior notes
- **Per-load randomness:** the last row's pad count (2–4), which pads become active (shuffled pool), and each active pad's color (from the 4-color set) are re-randomized every page load — no two loads look identical. Active-pad **count** per card is deterministic (== that month's item count).
- **Interaction is click-only** — there is no scroll, hover, or load animation; the grid sits still until an active pad is clicked. Non-active pads have `pointer-events:none` and never respond.
- The page itself is a simple vertical stack of cards (`height:150%` body) that scrolls normally; each card animates independently and self-contained inside its own `overflow:hidden` box.
- Only one panel is meaningfully open per card at a time; opening a different active pad re-fills the same `.card-content`. No reduced-motion branch is required.
- Desktop and mobile both work; below 900px cards and pads shrink per the media query.
