# Spotlight Stroke Draw — Pinned Scroll Section Where Thick Diagonal SVG Strokes Paint Over the Screen and Un-paint

## Goal
Build a three-section scroll page whose star effect is a **pinned "spotlight" section** where **13 thick, parallel, diagonal SVG strokes draw themselves on** — one after another in a scattered, staggered order — until they **completely blanket the viewport**, then, at the moment of full cover, the centered headline **swaps underneath** ("WAIT FOR IT" → "THERE IT IS") and **three cartoon sparkles pop**, after which the same strokes **un-draw in reverse**, wiping themselves off the far end to reveal the new message. The whole sequence is one GSAP timeline **scrubbed** to a pinned `ScrollTrigger` (4 viewport-heights of scroll), with Lenis smooth scrolling. The strokes use the classic `strokeDasharray`/`strokeDashoffset` line-drawing trick, and each stroke is **doubled** into a dark outline + yellow fill for a comic-ink look.

## Tech
Vanilla HTML/CSS/JS with ES module imports, in a fresh Vite project. Install and import from npm:
- **`gsap`** (3.x) plus the plugin **`ScrollTrigger`**.
- **`lenis`** — smooth scroll, wired into GSAP's ticker and `ScrollTrigger.update`.

```js
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Lenis from "lenis";

gsap.registerPlugin(ScrollTrigger);
```
Run everything inside `document.addEventListener("DOMContentLoaded", …)`. No other plugins, no framework. **No SplitText, no CustomEase, no lerp/rAF interpolation, no Three.js.**

## Layout / HTML
Three top-level `<section>`s: an **intro** hero, the **spotlight** (the pinned stage), and an **outro** hero. Inside the spotlight, in this DOM order: two overlapping message blocks, then the strokes SVG, then the sparkles SVG. **Paint order matters** — the strokes SVG sits *after* (on top of) both message blocks so the drawn strokes cover the text, and the sparkles SVG is last (on top of the strokes). Class names are load-bearing.

```html
<section class="intro">
  <h1>Blink and you'll miss it</h1>
</section>

<section class="spotlight">
  <div class="spotlight-content spotlight-content-in">
    <h2>Wait for it</h2>
    <p>
      The good part is closer, just one scroll away. Don't blink, or you'll
      miss the moment it turns.
    </p>
  </div>

  <div class="spotlight-content spotlight-content-out">
    <h2>There it is</h2>
    <p>
      That's the kind of payoff worth holding out for. Clean, sharp, and
      right when you needed it.
    </p>
  </div>

  <div class="strokes">
    <svg viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">
      <g fill="none" stroke="#FFF280" stroke-linecap="round">
        <path class="stroke" d="M -251 -42 C 156 -405 595 -695 1176 -648" stroke-width="150" />
        <path class="stroke" d="M -195 90 C 212 -273 651 -562 1232 -516" stroke-width="150" />
        <path class="stroke" d="M -138 223 C 269 -140 707 -430 1288 -383" stroke-width="150" />
        <path class="stroke" d="M -82 355 C 325 -8 764 -297 1345 -250" stroke-width="150" />
        <path class="stroke" d="M -26 488 C 381 125 820 -165 1401 -118" stroke-width="150" />
        <path class="stroke" d="M 30 620 C 438 257 876 -32 1457 15" stroke-width="150" />
        <path class="stroke" d="M 87 753 C 494 390 932 101 1513 147" stroke-width="150" />
        <path class="stroke" d="M 143 885 C 550 522 989 233 1570 280" stroke-width="150" />
        <path class="stroke" d="M 199 1018 C 606 655 1045 366 1626 412" stroke-width="150" />
        <path class="stroke" d="M 255 1150 C 663 788 1101 498 1682 545" stroke-width="150" />
        <path class="stroke" d="M 312 1283 C 719 920 1157 631 1738 677" stroke-width="150" />
        <path class="stroke" d="M 368 1416 C 775 1053 1214 763 1795 810" stroke-width="150" />
        <path class="stroke" d="M 424 1548 C 831 1185 1270 896 1851 942" stroke-width="150" />
      </g>
    </svg>
  </div>

  <svg class="sparkles" viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">
    <g fill="#fff" stroke="#141414" stroke-width="4" stroke-linejoin="round">
      <g transform="translate(360 230) scale(1.5)">
        <path class="sparkle" vector-effect="non-scaling-stroke" d="M 0 -55 C 8 -16 16 -8 55 0 C 16 8 8 16 0 55 C -8 16 -16 8 -55 0 C -16 -8 -8 -16 0 -55 Z" />
      </g>
      <g transform="translate(1180 520) scale(1)">
        <path class="sparkle" vector-effect="non-scaling-stroke" d="M 0 -55 C 8 -16 16 -8 55 0 C 16 8 8 16 0 55 C -8 16 -16 8 -55 0 C -16 -8 -8 -16 0 -55 Z" />
      </g>
      <g transform="translate(640 730) scale(0.65)">
        <path class="sparkle" vector-effect="non-scaling-stroke" d="M 0 -55 C 8 -16 16 -8 55 0 C 16 8 8 16 0 55 C -8 16 -16 8 -55 0 C -16 -8 -8 -16 0 -55 Z" />
      </g>
    </g>
  </svg>
</section>

<section class="outro">
  <h1>Told you it was worth it</h1>
</section>

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

Notes on structure:
- **Keep the 13 stroke `<path>` `d` strings and `stroke-width="150"` exactly as above.** They are 13 near-parallel, gently-curved cubic beziers marching top-to-bottom (each start-point `y` steps down ~132 units), each 150 units thick in the `1600×900` viewBox — since 150 > the 132 vertical spacing, the bands **overlap and fully tile the viewport** when all are drawn. `preserveAspectRatio="xMidYMid slice"` makes the SVG cover-fill the container. `stroke-linecap="round"` gives rounded ends.
- The stroke group is `fill="none" stroke="#FFF280"` (pale yellow) — the JS will clone each path into a dark outline layer at runtime (see effect).
- **Keep the 3 sparkle `<path>` `d` and their wrapping `<g transform>`s exactly** — three four-pointed "twinkle" shapes at `(360,230) scale(1.5)`, `(1180,520) scale(1)`, `(640,730) scale(0.65)`, white fill with a `#141414` 4px `non-scaling-stroke` outline. Each `.sparkle` starts at `transform: scale(0)` (hidden) via CSS.
- Copy is neutral demo text — use it verbatim, no brands.

## Styling
Fonts (Google Fonts): **Barlow Condensed** (all weights 100–900, italics too; used for the big uppercase headings) and **DM Sans** (variable optical-size + weight; used for the spotlight paragraph).
```css
@import url("https://fonts.googleapis.com/css2?family=Barlow+Condensed:ital,wght@0,100..900;1,100..900&family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap");
```

Global reset: `* { margin:0; padding:0; box-sizing:border-box; }`. `body { font-family:"Barlow Condensed", sans-serif; }`.

Sections — each is a full-screen panel with the same pink→yellow gradient:
```css
section {
  position: relative;
  width: 100%;
  height: 100svh;
  background: linear-gradient(90deg, #ff668c 0%, #fff280 100%); /* hot-pink → pale-yellow */
  color: #141414;                                               /* near-black text */
  overflow: hidden;
}
```

Intro / outro heroes — one big centered uppercase headline:
```css
.intro, .outro {
  display: flex; align-items: center; justify-content: center;
  text-align: center; padding: 2rem;
}
.intro h1, .outro h1 {
  text-transform: uppercase;
  font-weight: 900;
  font-size: clamp(2.5rem, 7vw, 6rem);
  letter-spacing: -2%;
  line-height: 0.8;
  max-width: 15ch;
}
```

Spotlight messages — two absolutely-stacked, centered blocks filling the section:
```css
.spotlight-content {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  justify-content: center; align-items: center;
  text-align: center; padding: 0 8vw;
}
.spotlight-content h2 {
  text-transform: uppercase; font-weight: 900;
  font-size: clamp(2.2rem, 5.5vw, 4.5rem);
  letter-spacing: -2%; line-height: 0.9; max-width: 14ch;
}
.spotlight-content p {
  margin-top: 1.5rem;
  font-family: "DM Sans", sans-serif;
  font-size: clamp(1.1rem, 1.8vw, 1.6rem);
  font-weight: 400; line-height: 1.25; max-width: 25ch;
}
.spotlight-content-out { opacity: 0; } /* second message starts hidden */
```

Overlay layers:
```css
.strokes {
  position: absolute; inset: -10%;
  width: 120%; height: 120%;         /* over-sized so bands overshoot the edges */
  pointer-events: none;
}
.strokes svg { width: 100%; height: 100%; overflow: visible; }
.stroke { will-change: stroke-dashoffset; }

.sparkles {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  overflow: visible; pointer-events: none;
}
.sparkle { will-change: transform; transform: scale(0); }
```

Responsive: `@media (max-width:1000px) { .spotlight-content { padding: 0 6vw; } }`.

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

Module-level constants (use these exact values):
```js
const STROKE_STAGGER = 0.045;    // base per-step stagger (timeline seconds)
const STROKE_DRAW_TIME = 1.25;   // base per-stroke draw duration
const OUTLINE_WIDTH = 7;         // dark outline is 7 units wider than the yellow fill
const SPOTLIGHT_PIN_HEIGHT = 4;  // pin lasts 4 viewport-heights of scroll
const STROKE_DRAW_ORDER = [0, 12, 2, 10, 4, 8, 6, 1, 3, 5, 7, 9, 11]; // scattered reveal order
```

### 1. Lenis + GSAP ticker wiring
```js
const lenis = new Lenis();
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000)); // ticker seconds → Lenis ms
gsap.ticker.lagSmoothing(0);
```

### 2. Double each stroke into outline + fill (the comic-ink trick)
For every `.stroke` path, clone it into a **dark outline** painted *behind* a slightly-narrower **yellow fill**, then prime both for line-drawing. Build an array of `{ layers:[outline, fill], length }`:
```js
const strokes = Array.from(document.querySelectorAll(".stroke")).map((fill) => {
  const fillWidth = parseFloat(fill.getAttribute("stroke-width"));  // 150
  const outline = fill.cloneNode(true);
  outline.setAttribute("stroke", "#141414");                        // dark outline, still width 150
  fill.setAttribute("stroke-width", fillWidth - OUTLINE_WIDTH);     // yellow fill narrowed to 143
  fill.before(outline);                                             // outline inserted BEFORE → paints underneath

  const length = fill.getTotalLength();
  const layers = [outline, fill];
  layers.forEach((layer) => {
    layer.style.strokeDasharray = length;
    layer.style.strokeDashoffset = length;   // one dash the full length, pushed off the end → invisible
  });
  return { layers, length };
});
```
Result per stroke: a `#141414` outline (150 wide) under a `#FFF280` fill (143 wide) → a ~3.5px dark border rims each yellow band. Both layers animate together (they share the `layers` array). All strokes start fully hidden.

```js
const sparkles = gsap.utils.toArray(".sparkle");
const messageBefore = document.querySelector(".spotlight-content-in");
const messageAfter  = document.querySelector(".spotlight-content-out");
```

### 3. Per-stroke timing helpers
`order` is the position within `STROKE_DRAW_ORDER` (0…12), `strokeIndex` is which of the 13 paths it draws.
```js
const startTime    = (order) => order * STROKE_STAGGER;                       // 0, 0.045, 0.09, …
const timingWobble = (order) => (order % 2 === 0 ? 0 : STROKE_STAGGER * 0.6); // odd orders get +0.027 jitter
const drawDuration = (order) => STROKE_DRAW_TIME + (order % 3) * 0.12;        // 1.25 / 1.37 / 1.49 cycling
```

### 4. The scrubbed, pinned ScrollTrigger
One master timeline (no repeat), driven entirely by a pinned ScrollTrigger:
```js
const timeline = gsap.timeline();

ScrollTrigger.create({
  trigger: ".spotlight",
  start: "top top",
  end: () => `+=${window.innerHeight * SPOTLIGHT_PIN_HEIGHT}px`, // pins for 4 viewport-heights
  pin: true,
  pinSpacing: true,
  scrub: 1,            // 1s catch-up smoothing between scroll and timeline
  animation: timeline,
});
```

### 5. Draw-in phase (strokes paint over the screen)
Precompute each step's start-time and duration, and the moment everything is covered:
```js
const drawSteps = STROKE_DRAW_ORDER.map((strokeIndex, order) => ({
  strokeIndex,
  at: startTime(order) + timingWobble(order),
  duration: drawDuration(order),
}));
const coveredAt = Math.max(...drawSteps.map((s) => s.at + s.duration)); // ≈ 2.012

drawSteps.forEach(({ strokeIndex, at, duration }) => {
  timeline.to(
    strokes[strokeIndex].layers,
    { strokeDashoffset: 0, duration, ease: "power2.out" }, // length → 0: band draws itself on
    at,                                                    // absolute position on the timeline
  );
});
```
Each stroke's `strokeDashoffset` tweens `length → 0` on `power2.out`, so the yellow-with-dark-border band snakes into view. Because `at = order*0.045 (+0.027 on odd orders)` and the draw order is the scattered `[0,12,2,10,4,8,6,1,3,5,7,9,11]`, the bands appear in a shuffled, overlapping cascade rather than plain top-to-bottom — by `coveredAt ≈ 2.012s` the 13 overlapping bands fully tile the viewport, hiding the message underneath.

Concrete first few steps (for reference): order 0 → stroke 0 at `0.000` dur `1.25`; order 1 → stroke 12 at `0.072` dur `1.37`; order 2 → stroke 2 at `0.090` dur `1.49`; order 3 → stroke 10 at `0.162` dur `1.25`; … order 11 → stroke 9 at `0.522` dur `1.49` (this is the last-to-finish → sets `coveredAt`).

### 6. The message swap (instant, hidden under full cover)
At `coveredAt`, snap the visible message out and the second one in — invisible because the strokes fully blanket the screen at that instant:
```js
timeline.set(messageBefore, { opacity: 0 }, coveredAt);
timeline.set(messageAfter,  { opacity: 1 }, coveredAt);
```

### 7. Draw-out phase (strokes wipe off the far end, reverse order)
Reverse the draw order and continue each dashoffset **past zero into negative** — the drawn band slides off in its own drawing direction (it does **not** rewind), uncovering the new message. Positions are relative to `coveredAt`, reusing the same `startTime`/`timingWobble`/`drawDuration` cadence:
```js
[...STROKE_DRAW_ORDER].reverse().forEach((strokeIndex, order) => {
  const { layers, length } = strokes[strokeIndex];
  timeline.to(
    layers,
    { strokeDashoffset: -length, duration: drawDuration(order), ease: "power2.in" },
    coveredAt + startTime(order) + timingWobble(order),
  );
});
```
Note the ease flips to `power2.in` for the exit (vs `power2.out` on the entrance). Total timeline runs ~4s of "content time," stretched across the 4-viewport pinned scroll by the scrub.

### 8. Sparkle pops (three twinkles around the reveal moment)
Each sparkle scales up with a back-overshoot while rotating, then scales back to 0, staggered `0.25s` apart, centered just before `coveredAt`:
```js
sparkles.forEach((sparkle, index) => {
  const popAt = coveredAt - 0.4 + index * 0.25; // ≈ 1.612, 1.862, 2.112
  timeline
    .fromTo(
      sparkle,
      { scale: 0, rotate: -60, transformOrigin: "center" },
      { scale: 1, rotate: 60, duration: 0.5, ease: "back.out(2)" },
      popAt,
    )
    .to(
      sparkle,
      { scale: 0, rotate: 140, duration: 0.5, ease: "back.in(2)" },
      popAt + 0.6, // start shrinking 0.6s after popping in
    );
});
```
Pop-in: `scale 0→1`, `rotate -60°→60°`, `back.out(2)`. Pop-out: `scale 1→0`, `rotate 60°→140°`, `back.in(2)`. The three sparkles flash white right as the strokes finish covering and the headline turns over.

## Assets / images
**None.** The entire visual is CSS gradient + type + two inline SVGs (the 13 strokes and the 3 sparkles). Do not add image files.

## Behavior notes
- **Trigger:** scroll only, fully scrubbed inside a pinned section. Nothing autoplays; parking the scroll freezes the strokes mid-draw, and reversing scroll un-covers the screen and reverts the message swap.
- **Fresh load (scroll=0):** intro hero visible; the spotlight (once reached) shows "WAIT FOR IT" with all strokes hidden and all sparkles at `scale(0)`; the "THERE IT IS" block is `opacity:0`.
- **Layering is essential:** strokes SVG paints over both messages; sparkles SVG paints over the strokes; both overlays are `pointer-events:none`. `overflow:visible` on the SVGs plus the `120%` over-sized `.strokes` container let the thick bands overshoot the edges with no gaps.
- The `end` uses a function (`() => …`) so the pin length re-resolves on refresh/resize against the current `window.innerHeight`.
- No reduced-motion guard in the original.
