# Build: Sliding Lightbox Gallery

## Goal
An editorial **click-triggered image lightbox**: a 4×4 grid of 16 numbered thumbnails on the left, a tall portrait **preview box** bottom-right. Clicking any thumbnail **slides a full-size copy of that image into the preview box from the right edge**, while the image that was already there **scales up to 1.5× and slides off to the left**, and the **whole page background tweens (via a CSS transition) to a color hand-matched to that thumbnail**. Old previews are never removed — they pile up off to the left, clipped by the box's `overflow:hidden`. The star effect is the pure `gsap.to` cross-slide (incoming from the right, outgoing zooming out to the left) over a 1s duration, paired with the 1s background-color fade.

## Tech
Vanilla HTML/CSS/JS with ES module imports (Vite/npm project). Use **`gsap` (npm) only** — no plugins, no framework, no ScrollTrigger, no SplitText, no Lenis, no Three.js. A single import:
```js
import gsap from "gsap";
```
The animation is driven entirely by two `gsap.to()` calls inside a click handler; the background color change is a plain CSS `transition` (not GSAP).

## Layout / HTML
Class names are load-bearing (the JS queries `.item .img img` and `.preview-container`). Structure:

```html
<nav>
  <div class="col">
    <div class="copy"><p>(Shadows)</p><p>Lorem ipsum dolor sit, amet consectetur adipisicing elit.</p></div>
    <div class="copy"><p>(Light Exploration)</p><p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quam nobis, possimus id excepturi.</p></div>
  </div>
  <div class="col">
    <div class="copy"><p>(Clay)</p><p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quam nobis, possimus id.</p></div>
    <div class="copy"><p>(Concrete Forms)</p><p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quam nobis, possimus id excepturi exercitationem nesciunt.</p></div>
    <div class="copy"><p>(Mixed Materials)</p><p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quam nobis, possimus id excepturi exercitationem nesciunt facere non dolorem deleniti aliquid!</p></div>
    <div class="copy"><p>(Closeups)</p><p>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quam nobis, possimus!</p></div>
  </div>
  <div class="col">
    <p class="logo">1-01</p>
  </div>
</nav>

<div class="container">
  <div class="gallery">
    <div class="row">
      <div class="item"><div class="index"><p>1</p></div><div class="img"><img src="/c/lightbox/1.jpeg" alt="" /></div></div>
      <div class="item"><div class="index"><p>2</p></div><div class="img"><img src="/c/lightbox/2.jpeg" alt="" /></div></div>
      <div class="item"><div class="index"><p>3</p></div><div class="img"><img src="/c/lightbox/3.jpeg" alt="" /></div></div>
      <div class="item"><div class="index"><p>4</p></div><div class="img"><img src="/c/lightbox/4.jpeg" alt="" /></div></div>
    </div>
    <!-- row 2: items 5–8, row 3: items 9–12, row 4: items 13–16 — same pattern -->
  </div>
  <div class="preview">
    <div class="preview-container">
      <img src="/c/lightbox/1.jpeg" alt="" />   <!-- initial preview = image 1 -->
    </div>
  </div>
</div>

<script type="module" src="./script.js"></script>
```

- **16 items** total, laid out as **4 rows × 4 items**. Each `.item` = an `.index` block holding its number `<p>1</p>`…`<p>16</p>` (stacked above) and an `.img` wrapper holding one `<img>` whose filename is `/c/lightbox/<n>.jpeg` (the number must be recoverable from the src — the JS regex-matches `(\d+)\.jpeg`).
- The **`.preview-container` starts with one `<img>` already inside it** — image `1.jpeg` — so the box is not empty on load.
- Nav copy labels — `(Shadows)`, `(Light Exploration)`, `(Clay)`, `(Concrete Forms)`, `(Mixed Materials)`, `(Closeups)` — and the logo `1-01` are neutral demo text; keep them verbatim, no real brand names.

## Styling
Font: **"Editorial New"** (a high-contrast editorial serif) applied to all `<p>` and to `.container`. If unavailable, fall back to a serif (`Georgia`, serif). `p { font-size:14px; }`.

Global reset & page:
```css
* { margin:0; padding:0; box-sizing:border-box; }
html, body {
  width:100%; height:100%;
  background:#b2b4aa;              /* muted sage-grey base */
  transition: background-color 1s; /* CRITICAL: the JS sets body bg inline; this 1s transition IS the color fade */
}
img { width:100%; height:100%; object-fit:cover; }   /* every img cover-fills its box */
```

Top nav (absolutely positioned band):
```css
nav { position:absolute; top:0; width:100%; display:flex; padding:2em; gap:1em; }
.col { display:flex; gap:1em; }        /* copy blocks sit in a row inside each column */
.copy { flex:1; }
.col:nth-child(1){ flex:2; }
.col:nth-child(2){ flex:5; }
.col:nth-child(3){ flex:3; }
p.logo { width:100%; text-align:right; }
```

Main container — gallery (left, flex 2) beside preview area (right, flex 4):
```css
.container { position:relative; top:175px; width:100%; height:80vh; display:flex; font-family:"Editorial New"; }
.gallery { flex:2; display:flex; flex-direction:column; height:100%; }
.row { flex:1; width:100%; display:flex; }
.item { flex:1; padding:1em; display:flex; flex-direction:column; align-items:center; gap:1em; }
.img { width:80%; height:80px; cursor:pointer; }   /* small thumbnail box; cursor:pointer signals it's clickable */
.preview { width:100%; flex:4; height:100%; display:flex; justify-content:flex-end; align-items:flex-end; padding:2em; }
```

**Preview box — this is the stage for the effect:**
```css
.preview-container {
  position:relative;
  width:400px; height:600px;        /* 2:3 portrait */
  background:#e3e3e3;               /* light grey showing before/behind images */
  overflow:hidden;                  /* CRITICAL: clips outgoing/incoming images sliding past the edges */
}
.preview-container img { position:absolute; }   /* every preview img is absolutely positioned so left/right can be tweened */
```

Responsive `@media (max-width:900px)`:
```css
body { height:200vh; }
.col:nth-child(1), .col:nth-child(2) { display:none; }   /* hide the two copy columns, keep the logo */
.preview-container { width:100%; }
.container { flex-direction:column; gap:5em; }            /* gallery stacks above preview */
```

## GSAP effect (the important part — be exhaustive)

There is **no plugin and no timeline**. The entire effect is a click handler that fires **two simultaneous `gsap.to()` tweens** plus one inline style write. GSAP's global default ease applies to both tweens (no `ease` is passed), i.e. **`power1.out`**, duration **1s**, no delay, no stagger, no repeat.

### Setup
```js
import gsap from "gsap";

const images = document.querySelectorAll(".item .img img");   // all 16 thumbnails

// One color per image, index 0 → image 1 … index 15 → image 16.
// Each is the dominant/average tone of that photo; clicking image N paints the page this color.
const colorArray = [
  "#B69671", "#6D7783", "#B9A288", "#B5B8AC",
  "#A2B0B1", "#5F5F5F", "#7E98B1", "#857F71",
  "#A6A6A6", "#B5B4AB", "#503D31", "#B1A591",
  "#616161", "#C29651", "#5E6471", "#B0A89D",
];
```

### The click handler (attach to every thumbnail `<img>`)
```js
function handleImageClick(event) {
  const imgSrc    = event.currentTarget.src;              // e.g. ".../c/lightbox/7.jpeg"
  const imgNumber = imgSrc.match(/(\d+)\.jpeg/)[1];       // "7"

  // 1) Background color — inline style write; the CSS `transition: background-color 1s` animates it.
  document.body.style.backgroundColor = colorArray[parseInt(imgNumber) - 1];

  const newImgSrc        = `/c/lightbox/${imgNumber}.jpeg`;
  const previewContainer = document.querySelector(".preview-container");
  const currentLastImg   = previewContainer.querySelector("img:last-child"); // the image currently on top

  // 2) OUTGOING tween — zoom the current image up and drift it off to the left.
  if (currentLastImg) {
    gsap.to(currentLastImg, { duration: 1, scale: 1.5, left: "-50%" });
  }

  // 3) Build the INCOMING image, parked fully off-screen to the right.
  const newImg = document.createElement("img");
  newImg.src = newImgSrc;
  newImg.style.position = "absolute";
  newImg.style.right = "-100%";        // its right edge is one container-width past the right edge
  previewContainer.appendChild(newImg); // appended LAST → it becomes the new img:last-child

  // 4) INCOMING tween — slide it in to fill the box.
  gsap.to(newImg, { duration: 1, right: "0%" });
}

images.forEach((img) => img.addEventListener("click", handleImageClick));
```

### Exact motion, value-by-value
- **Incoming image:** created fresh each click, `position:absolute`, inherits `width:100% height:100% object-fit:cover` from the global `img` rule. Starts at **`right:-100%`** (completely off the right side of the 400px box) and tweens to **`right:0%`** over **1s** → it slides leftward into the box until it exactly fills it. Because it is appended last, it lands on top of everything.
- **Outgoing image:** whatever was previously `img:last-child`. It tweens **`scale` 1 → 1.5** (transform-origin center) **and `left` (0) → -50%** over the same **1s**, i.e. it enlarges to 1.5× while sliding half a container-width to the left, exiting under the `overflow:hidden` clip on the left side. The two tweens run concurrently, so you read it as a cross-slide: new photo pushes in from the right as the old one blooms and leaves left.
- Note the deliberate **asymmetry**: the incoming image animates the `right` offset, the outgoing animates the `left` offset (and adds `scale`). Preserve that — don't unify them.
- **Ease/duration/delay/stagger:** ease = GSAP default `power1.out`; duration = `1`; no delay, no stagger, no repeat, no yoyo, no timeline. Both are independent one-shot `gsap.to` tweens.
- **No cleanup:** outgoing images are never removed from the DOM. After several clicks the container holds a growing stack of `<img>`s, each frozen where its tween ended (scaled 1.5, left -50%), overlapping and mostly hidden by `overflow:hidden`. Only the newest is fully visible.
- **Background fade is NOT GSAP:** it is the CSS `transition: background-color 1s` on `html, body` reacting to the inline `backgroundColor` write — so it fades over 1s in parallel with the slide.
- **First click** is a special but automatic case: the initial `1.jpeg` sitting in the container is the `currentLastImg`, so it gets the outgoing zoom-out treatment while the clicked image slides in.

## Assets / images
**16 gallery images.** Each appears twice: as a small **cover-cropped thumbnail** (~80px tall box) in the grid, and, when clicked, as a **full 2:3 portrait** filling the 400×600 preview box (also `object-fit:cover`, so any source aspect crops cleanly, but frame them portrait-friendly). The set reads as one **calm, muted, editorial mix** — natural/neutral tones on plain or dark backdrops, spanning warm tans, slate greys, sage, sky-blue and deep browns to match the per-image background colors above. Describe by role, no brands or logos:

1. Warm close-up of a bare shoulder and upper back, soft skin tones on a plain backdrop. *(bg #B69671)*
2. Stacked bundles of rolled grey material on a wooden pallet, industrial. *(bg #6D7783)*
3. Close-up of feet in strappy lace-up leather sandals on a wood floor. *(bg #B9A288)*
4. White daisy-like flowers against dark green foliage. *(bg #B5B8AC)*
5. Cluster of small snail shells scattered on a muted teal surface. *(bg #A2B0B1)*
6. Black-and-white studio portrait, hands clasped on the head, dark background. *(bg #5F5F5F)*
7. A dark glass bottle silhouetted against a clear blue sky (product shot). *(bg #7E98B1)*
8. A weathered white window frame on a building exterior in soft light. *(bg #857F71)*
9. Black-and-white portrait, head tilted back, eyes closed, sleek dark hair. *(bg #A6A6A6)*
10. A bar of soap on a glossy reflective surface (product still-life). *(bg #B5B4AB)*
11. Close-up of a brown horse's eye and bridle against blue sky. *(bg #503D31)*
12. A folded, pleated paper fan-like lamp/sculpture in warm tones. *(bg #B1A591)*
13. A balanced stack of grey stones with a white cube, sculptural, on black. *(bg #616161)*
14. A golden-green pear carved open, with a blue cast shadow (still life). *(bg #C29651)*
15. A figure in a flowing white dress, barefoot, holding a wooden frame (minimal editorial). *(bg #5E6471)*
16. A small pleated parasol-style lamp casting a long shadow in moody light. *(bg #B0A89D)*

If you have fewer than 16, repeat in order — the effect is identical regardless of content; the color-per-index mapping is what matters for the background fade.

## Behavior notes
- **Click is the only trigger** — no hover, no scroll, no autoplay. Every thumbnail `<img>` gets the same listener.
- The **number in each filename is load-bearing**: the JS extracts it with `/(\d+)\.jpeg/` to both pick the background color (`colorArray[n-1]`) and build the preview src (`/c/lightbox/<n>.jpeg`). Keep numeric `<n>.jpeg` filenames.
- Desktop-oriented; below 900px the two copy columns hide, the layout stacks vertically, and the preview grows to full width (page becomes 200vh tall).
- No reduced-motion branch in the original. Both tweens use GSAP's default `power1.out` ease at 1s.
