{
    "generated": "2026-09-02T14:40:02+03:00",
    "source_site": "https://gsap.pinul.com",
    "page": {
        "id": 491,
        "slug": "studio-monet",
        "title": "Studio Monet &#8211; spiral hero (test)",
        "url": "https://gsap.pinul.com/studio-monet/"
    },
    "requires": {
        "bricks": "2.3.12",
        "wp": "7.1",
        "header_scripts": "<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js\\\"></script>\r\n\r\n\r\n<script src=\\\"https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js\\\" id=\\\"gsap-js-js\\\"></script>\r\n<script src=\\\"https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js?\\\" id=\\\"gsap-st-js\\\"></script>\r\n<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/SplitText.min.js\\\" integrity=\\\"sha512-wOeEC+9qERAzhliwBFPDb6t8TiFFxdxG8vhK/Ygs7TuC44bpg8pg/X2/U/u+0X4fK05wb9id1EIipnF02+CFQw==\\\" crossorigin=\\\"anonymous\\\" referrerpolicy=\\\"no-referrer\\\"></script>\r\n\r\n\r\n<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/ScrollSmoother.min.js\\\" ></script>\r\n<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/Observer.min.js\\\" integrity=\\\"sha512-GdLRoSd7fNE36D+SKii0MOd6MG1kljA09It5wir9Rv6lYYWWYjXQTCtvzvJppbEG0eAGdoavuIxDsDN8pzzD2A==\\\" crossorigin=\\\"anonymous\\\" referrerpolicy=\\\"no-referrer\\\"></script>\r\n<script src=\\\"https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/MotionPathPlugin.min.js\\\" integrity=\\\"sha512-vuNgw6lF+p97XmL9lFpbQZWBXxIFnwUVt7J/uknE4NJru5GJjjhIAauXdDtNdezqOJFnit+M3AeaM+gZ93hlmg==\\\" crossorigin=\\\"anonymous\\\" referrerpolicy=\\\"no-referrer\\\"></script>\r\n\r\n",
        "note": "Theme style sets a 10px rem base on this site; the ported CSS is already in px."
    },
    "page_settings": {
        "bodyClasses": "monet",
        "disableLazyLoad": true
    },
    "elements": [
        {
            "id": "monsec",
            "name": "section",
            "parent": 0,
            "children": [
                "moncode"
            ],
            "settings": {
                "tag": "section",
                "_cssClasses": "mon",
                "_padding": {
                    "top": "0",
                    "right": "0",
                    "bottom": "0",
                    "left": "0"
                },
                "_width": "100%"
            }
        },
        {
            "id": "moncode",
            "name": "code",
            "parent": "monsec",
            "children": [],
            "settings": {
                "code": "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;700&family=Newsreader:opsz,wght@6..72,300;6..72,400&display=swap\">\n\n<!-- One tall section drives the whole experience (intro + project scroll).\n     Its inner .stage is pinned for the entire duration and scrubbed. -->\n<section class=\"experience\" data-experience>\n  <div class=\"stage\" data-stage>\n    <!-- Converging intro images (injected by JS); become project 1 -->\n    <div class=\"images\" data-images></div>\n\n    <!-- Per-project panels for the wipe transitions (injected by JS) -->\n    <div class=\"panels\" data-panels></div>\n\n    <!-- Wordmark: starts centred, rises to top centre -->\n    <div class=\"logo\" data-logo>Studio Monet</div>\n\n    <!-- Tagline under the wordmark; stays put, covered by the images -->\n    <div class=\"logo-sub\" data-logo-sub>Art Direction</div>\n\n    <!-- MENU pill below the logo (fades in during phase 3) -->\n    <button class=\"menu-pill\" data-menu type=\"button\">MENU</button>\n\n    <!-- Project title carousel: left + right columns (injected by JS) -->\n    <div class=\"title\" data-title></div>\n  </div>\n</section>\n",
                "signature": "b4251f31e9eaaf494efe5d1e76c5918c",
                "language": "html",
                "executeCode": true,
                "noRoot": true,
                "cssCode": "/* ============================================================\n   STUDIO MONET — scoped port of the standalone Vite build.\n   Every rule is scoped under .mon (the Bricks section) and every rem from\n   the source was converted to px: this site's theme style sets a 10px rem\n   base, so rem would render the whole type scale at 62.5%.\n   ============================================================ */\n\nhtml.lenis,\nhtml.lenis body { height: auto; }\n\nhtml.lenis { scroll-behavior: auto !important; }\n\n.lenis.lenis-smooth { scroll-behavior: auto !important; }\n.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }\n.lenis.lenis-stopped { overflow: hidden; }\n.lenis.lenis-smooth iframe { pointer-events: none; }\n\nbody.monet { overflow-x: hidden; background: #ffffff; }\nbody.monet #brx-header,\nbody.monet #brx-footer { display: none !important; }\n\n.mon {\n  /* Colors */\n  --color-bg: #ffffff;          /* default background            */\n  --color-panel: #f5f1ec;       /* panel / off-white             */\n  --color-ink: #000000;         /* text on light background      */\n  --color-on-image: #ffffff;    /* text on top of an image       */\n  --color-line: #c2bcb3;        /* light warm grey — hairline borders */\n\n  /* Typography */\n  --font-wordmark: 'Newsreader', Georgia, serif;\n  --font-title: 'Montserrat', Helvetica, sans-serif;\n  --font-label: 'Newsreader', Georgia, serif;\n\n  /* Type scale (px — see the note at the top of this file) */\n  --size-wordmark: clamp(22.4px, 2.2vw, 32px);\n  --size-title: clamp(16px, 1.5vw, 24.8px);\n  --size-label: clamp(11.52px, 0.95vw, 15.2px);\n  --size-menu: clamp(8px, 0.62vw, 9.6px);\n\n  /* Spacing */\n  --edge-pad: clamp(20px, 4vw, 64px);\n  --page-margin: clamp(28px, 6vw, 104px); /* shared outer margin, project page */\n  --title-tracking: 0.22em;\n  --menu-tracking: 0.32em;\n\n  display: block;\n  padding: 0;\n  margin: 0;\n  background: var(--color-bg);\n  color: var(--color-ink);\n  -webkit-font-smoothing: antialiased;\n  text-rendering: optimizeLegibility;\n}\n\n.mon *, .mon *::before, .mon *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n/* ============================================================\n   EXPERIENCE — tall scroll track; .stage is pinned & scrubbed\n   ============================================================ */\n.mon .experience {\n  position: relative;\n  width: 100%;\n  /* Height is set from JS at handover so the scrub distance stays in sync. */\n}\n\n.mon .stage {\n  position: relative;\n  width: 100%;\n  height: 100vh;\n  overflow: hidden;\n  background: var(--color-bg);\n}\n\n/* ------------------------------------------------------------\n   Converging intro images (become project 1)\n   ------------------------------------------------------------ */\n/* z-index makes .images its own stacking context, so the stacked proj-img\n   z-indexes stay internal and the panels sit cleanly above the whole group. */\n.mon .images { position: absolute; inset: 0; z-index: 2; }\n\n.mon .proj-img,\n.mon .panel-img {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  /* Natural landscape ratio of the source photos (1184 x 864). */\n  width: clamp(300px, 54vw, 920px);\n  aspect-ratio: 1184 / 864;\n  height: auto;\n  object-fit: cover;\n  user-select: none;\n  -webkit-user-drag: none;\n}\n\n.mon .proj-img { will-change: transform; }\n.mon .panel-img { transform: translate(-50%, -50%); }\n\n/* ------------------------------------------------------------\n   Project panels — full-stage colour blocks carrying the next\n   image, revealed bottom→top (the wipe).\n   ------------------------------------------------------------ */\n.mon .panels { position: absolute; inset: 0; z-index: 3; }\n\n.mon .panel {\n  position: absolute;\n  inset: 0;\n  /* --reveal goes 100% (hidden) -> 0% (fully shown). */\n  clip-path: inset(var(--reveal, 100%) 0 0 0);\n  will-change: clip-path;\n}\n\n/* ------------------------------------------------------------\n   Wordmark\n   ------------------------------------------------------------ */\n.mon .logo {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  font-family: var(--font-wordmark);\n  font-weight: 400;\n  font-size: var(--size-wordmark);\n  letter-spacing: 0.01em;\n  color: var(--color-ink);\n  white-space: nowrap;\n  will-change: transform;\n  z-index: 30;\n}\n\n/* Tagline under the wordmark. Sits at the centre BELOW the images (z:1) so the\n   converging images cover it. GSAP only animates its opacity. */\n.mon .logo-sub {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  transform: translate(-50%, calc(var(--size-wordmark) + 6px));\n  font-family: var(--font-title);\n  font-weight: 500;\n  font-size: clamp(9.6px, 0.78vw, 11.84px);\n  letter-spacing: 0.34em;\n  text-indent: 0.34em; /* optical centring with tracking */\n  text-transform: uppercase;\n  color: var(--color-ink);\n  white-space: nowrap;\n  z-index: 1;\n}\n\n/* ------------------------------------------------------------\n   MENU pill (below logo, top center)\n   ------------------------------------------------------------ */\n.mon .menu-pill {\n  position: absolute;\n  left: 50%;\n  /* `top` is set from JS (layoutTopCluster). */\n  top: 0;\n  transform: translateX(-50%);\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  padding: 0.6em 1.6em;\n  background: transparent;\n  border: 1px solid var(--color-line);\n  border-radius: 999px;\n  font-family: var(--font-title);\n  font-weight: 700;\n  font-size: var(--size-menu);\n  letter-spacing: var(--menu-tracking);\n  text-indent: var(--menu-tracking);\n  color: var(--color-ink);\n  cursor: pointer;\n  opacity: 0;            /* revealed in phase 3 */\n  z-index: 30;\n}\n\n/* ------------------------------------------------------------\n   Project title — left & right word groups\n   ------------------------------------------------------------ */\n.mon .title { position: absolute; inset: 0; z-index: 20; pointer-events: none; }\n\n/* Two stacked layers occupy the same space and animate identically:\n   - .title-base  : black words (+ sub-labels), the default colour.\n   - .title-white : white copies of the words, clipped to the image rect.\n   As a word slides off the image its colour transitions white->black exactly\n   along the image edge — positional, and only once. */\n.mon .title-base,\n.mon .title-white { position: absolute; inset: 0; }\n.mon .title-white { z-index: 1; }   /* clip-path set from JS */\n.mon .title-base { z-index: 0; }\n\n/* Each side is a COLUMN: a zero-size anchor at the viewport centre. GSAP moves\n   it horizontally (the intro split) and vertically (the project carousel). */\n.mon .title-col {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  will-change: transform;\n}\n\n/* One cell per project, stacked one slot (≈ viewport height) apart. */\n.mon .title-cell {\n  position: absolute;\n  left: 0;\n  top: 0;\n  text-align: center;\n  transform: translate(-50%, -50%) translateY(calc(var(--i) * var(--mon-slot, 100vh)));\n}\n\n.mon .title-word {\n  font-family: var(--font-title);\n  font-weight: 700;\n  font-size: var(--size-title);\n  line-height: 0.95;\n  letter-spacing: var(--title-tracking);\n  text-transform: uppercase;\n  white-space: nowrap;\n  color: var(--color-ink);          /* base layer = black */\n}\n\n.mon .title-white .title-word { color: var(--color-on-image); }\n\n.mon .title-sub {\n  position: absolute;\n  top: 100%;\n  left: 50%;\n  transform: translateX(-50%);\n  margin-top: 0.7em;\n  white-space: nowrap;\n  font-family: var(--font-label);\n  font-weight: 400;\n  font-size: var(--size-label);\n  letter-spacing: 0.08em;\n  color: var(--color-ink);\n  opacity: 0;            /* fades in only after the title settles */\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .mon .menu-pill, .mon .title-sub { opacity: 1 !important; }\n}\n\n/* ============================================================\n   PROJECT SCROLL — hover affordance on the centred image\n   ============================================================ */\n.mon .slot-hotspot {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  transform: translate(-50%, -50%);\n  z-index: 15;\n  pointer-events: none;   /* enabled via .active only on the scroll */\n  cursor: pointer;\n}\n.mon .slot-hotspot.active { pointer-events: auto; }\n\n.mon .slot-hotspot::before {            /* slight darken on hover */\n  content: '';\n  position: absolute;\n  inset: 0;\n  background: #000;\n  opacity: 0;\n  transition: opacity 0.35s ease;\n}\n.mon .slot-hotspot.active:hover::before { opacity: 0.18; }\n\n.mon .slot-hotspot .plus {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  width: 44px;\n  height: 44px;\n  transform: translate(-50%, -50%);\n  opacity: 0;\n  transition: opacity 0.35s ease;\n}\n.mon .slot-hotspot.active:hover .plus { opacity: 1; }\n.mon .slot-hotspot .plus::before,\n.mon .slot-hotspot .plus::after {\n  content: '';\n  position: absolute;\n  background: var(--color-on-image);\n}\n.mon .slot-hotspot .plus::before { left: 50%; top: 0; width: 1.5px; height: 100%; transform: translateX(-50%); }\n.mon .slot-hotspot .plus::after  { top: 50%; left: 0; height: 1.5px; width: 100%; transform: translateY(-50%); }\n\n/* Image that expands from the slot to full screen (open transition). */\n.mon .expander {\n  position: fixed;\n  object-fit: cover;\n  z-index: 100;\n  will-change: top, left, width, height;\n}\n\n/* ============================================================\n   PROJECT PAGE\n   ============================================================ */\n.mon .project-page { position: relative; background: var(--color-bg); }\n.mon .project-page[hidden] { display: none; }\n\n/* Fixed header: white wordmark + frosted MENU pill */\n.mon .pp-header {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  z-index: 120;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  padding-top: clamp(20px, 2.6vw, 30px);\n  opacity: 0;\n  visibility: hidden;\n  pointer-events: none;\n}\n.mon .pp-header.show { opacity: 1; visibility: visible; pointer-events: auto; }\n\n.mon .pp-logo {\n  font-family: var(--font-wordmark);\n  font-weight: 400;\n  font-size: var(--size-wordmark);\n  color: var(--color-on-image);\n  letter-spacing: 0.01em;\n  cursor: pointer;\n}\n.mon .pp-menu {\n  margin-top: 12px;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  padding: 0.62em 1.5em;\n  border: 1px solid rgba(255, 255, 255, 0.55);\n  border-radius: 999px;\n  background: rgba(255, 255, 255, 0.12);\n  -webkit-backdrop-filter: blur(10px);\n  backdrop-filter: blur(10px);\n  font-family: var(--font-title);\n  font-weight: 700;\n  font-size: var(--size-menu);\n  letter-spacing: var(--menu-tracking);\n  text-indent: var(--menu-tracking);\n  color: var(--color-on-image);\n  cursor: pointer;\n}\n\n/* Hero — full-bleed image with the title centred over it */\n.mon .pp-hero { position: relative; width: 100%; height: 100vh; overflow: hidden; }\n.mon .pp-hero-img { width: 100%; height: 100%; object-fit: cover; display: block; }\n.mon .pp-hero-title {\n  position: absolute;\n  inset: 0;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 0 var(--page-margin);\n  text-align: center;\n  font-family: var(--font-title);\n  font-weight: 700;\n  font-size: clamp(32px, 6vw, 88px);\n  letter-spacing: 0.28em;\n  text-indent: 0.28em;\n  text-transform: uppercase;\n  color: var(--color-on-image);\n  opacity: 0;            /* fades in only after the expand finishes */\n  pointer-events: none;\n}\n\n/* Shared section rhythm + outer margins */\n.mon .pp-section { padding: clamp(64px, 12vh, 168px) var(--page-margin); }\n.mon .pp-brief, .mon .pp-concept { background: var(--color-panel); }\n.mon .pp-ticker, .mon .pp-nav { background: var(--color-bg); }\n\n.mon .pp-eyebrow {\n  font-family: var(--font-title);\n  font-weight: 700;\n  text-transform: uppercase;\n  letter-spacing: 0.28em;\n  font-size: clamp(9.6px, 0.8vw, 12.48px);\n  color: var(--color-ink);\n  margin: 0 0 1.4em;\n}\n.mon .pp-body {\n  font-family: var(--font-label);\n  font-weight: 400;\n  font-size: clamp(13.12px, 0.95vw, 15.68px);\n  line-height: 1.75;\n  color: var(--color-ink);\n  max-width: 34ch;\n}\n\n/* Brief: text left, image right, tops aligned */\n.mon .pp-brief-grid {\n  display: grid;\n  grid-template-columns: 1fr 1.6fr;\n  gap: clamp(32px, 6vw, 104px);\n  align-items: start;\n}\n.mon .pp-brief-img {\n  width: 100%;      /* same container width */\n  height: auto;     /* full image, natural aspect ratio (no crop) */\n  display: block;\n}\n\n/* Ticker: seamless infinite marquee, full-bleed */\n.mon .pp-ticker { padding-block: clamp(40px, 8vh, 96px); overflow: hidden; }\n.mon .pp-ticker-track {\n  display: flex;\n  width: max-content;\n  animation: mon-ticker var(--ticker-dur, 48s) linear infinite;\n}\n.mon .pp-ticker-track img {\n  height: clamp(230px, 34vh, 420px);\n  width: auto;\n  margin-right: var(--ticker-gap, 18px); /* trailing gap included => seamless -50% */\n  object-fit: cover;\n  display: block;\n}\n@keyframes mon-ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }\n\n/* Concept: eyebrow + body left-aligned with the large image below */\n.mon .pp-concept-img {\n  width: 100%;\n  height: auto;\n  aspect-ratio: 16 / 9;\n  object-fit: cover;\n  display: block;\n  margin-top: clamp(32px, 5vh, 72px);\n}\n\n/* Project navigation */\n.mon .pp-nav {\n  display: flex;\n  justify-content: space-between;\n  padding-block: clamp(48px, 8vh, 104px);\n}\n.mon .pp-nav-item { cursor: pointer; text-decoration: none; }\n.mon .pp-nav-item.next { text-align: right; }\n.mon .pp-nav-label {\n  font-family: var(--font-title);\n  font-weight: 700;\n  text-transform: uppercase;\n  letter-spacing: 0.26em;\n  font-size: clamp(12.8px, 1.4vw, 18.4px);\n  color: var(--color-ink);\n  display: block;\n}\n.mon .pp-nav-name {\n  font-family: var(--font-label);\n  font-size: clamp(12.48px, 0.9vw, 14.72px);\n  color: var(--color-ink);\n  margin-top: 0.6em;\n  display: block;\n}\n\n@media (max-width: 700px) {\n  .mon .pp-brief-grid { grid-template-columns: 1fr; gap: 32px; }\n}\n",
                "javascriptCode": "/* ============================================================\n   STUDIO MONET — scroll-driven portfolio hero.\n   Port of the standalone Vite build (src/main.js + data.js) as one classic\n   script: Bricks injects javascriptCode as a plain <script>, so the three ES\n   modules are merged here and Lenis is pulled in with a dynamic import().\n   GSAP + ScrollTrigger + MotionPathPlugin already load site-wide.\n\n   Where to tune things:\n     CONFIG   — timing, curve intensity, spiral direction, scale range, eases\n     PROJECTS — the six projects (image + left/right words + date + discipline)\n   ============================================================ */\n(function () {\n  var root = document.querySelector('.mon');\n  if (!root || !window.gsap || !window.ScrollTrigger || !window.MotionPathPlugin) return;\n\n  gsap.registerPlugin(ScrollTrigger, MotionPathPlugin);\n\n  var BASE = 'https://gsap.pinul.com/wp-content/uploads/2026/09/';\n  var LENIS_URL = 'https://cdn.jsdelivr.net/npm/lenis@1.1.13/+esm';\n\n  import(LENIS_URL)\n    .then(function (m) { start(m.default || m.Lenis); })\n    .catch(function (err) { console.error('[monet] lenis failed to load:', err); });\n\n  function start(Lenis) {\n    /* ============================================================\n       PROJECT DATA — single ordered source for all six projects.\n       ============================================================ */\n    var img = function (n) { return BASE + 'monet-' + n + '.jpg'; };\n\n    // Every visual, for picking random images on the project page.\n    var ALL_IMAGES = [];\n    for (var k = 1; k <= 19; k++) ALL_IMAGES.push(img(('0' + k).slice(-2)));\n\n    function randomImage(exclude) {\n      exclude = exclude || [];\n      var pool = ALL_IMAGES.filter(function (u) { return exclude.indexOf(u) === -1; });\n      var from = pool.length ? pool : ALL_IMAGES;\n      return from[Math.floor(Math.random() * from.length)];\n    }\n\n    var PROJECTS = [\n      { id: 'the-field',   name: 'The Field',     image: img('03'), left: 'PROJECT', right: 'THE FIELD',  date: '02/02/2026', discipline: 'Art Direction' },\n      { id: 'in-motion',   name: 'In Motion',     image: img('14'), left: 'STUDY',   right: 'IN MOTION',  date: '11/04/2026', discipline: 'Photography' },\n      { id: 'baseline',    name: 'Baseline',      image: img('02'), left: 'ABOVE',   right: 'BASELINE',   date: '19/05/2026', discipline: 'Editorial' },\n      { id: 'off-court',   name: 'Off Court',     image: img('06'), left: 'OFF',     right: 'COURT',      date: '07/07/2026', discipline: 'Brand Identity' },\n      { id: 'long-game',   name: 'The Long Game', image: img('12'), left: 'THE',     right: 'LONG GAME',  date: '23/08/2026', discipline: 'Creative Direction' },\n      { id: 'match-point', name: 'Match Point',   image: img('19'), left: 'MATCH',   right: 'POINT',      date: '30/09/2026', discipline: 'Campaign' }\n    ];\n\n    /* ============================================================\n       CONFIG — every timing / curve / scale knob lives here.\n       ============================================================ */\n    var CONFIG = {\n      // Scroll track height per timeline unit (vh).\n      vhPerUnit: 180,\n\n      // Phase durations in abstract timeline units (ratios are what matter).\n      phase1: 3.2,   // images converge + logo rises (slow, graceful spiral)\n      phase2: 0.6,   // title fades in (white) over the image\n      phase3: 1.2,   // title splits to the margins (colour follows image edge)\n      phase4: 0.5,   // sub-labels fade in once the title has settled\n\n      // --- Intro autoplay ---\n      introUnitSeconds: 1.25, // seconds per timeline unit while the intro autoplays\n      offBuffer: 24,          // px past the edge so images start fully off-screen\n\n      // --- Project scroll ---\n      projStep: 1.0,        // timeline units per project transition\n      panelColors: ['#FFFFFF', '#F5F1EC'], // alternates per project\n      snapDelay: 140,       // ms of scroll-stillness before snapping\n      snapDuration: 0.7,    // s — soft settle onto a project\n\n      // Curve / spiral\n      curveIntensity: 0.35,   // how far the arc bows out (× travel distance)\n      spiralDir: 1,           // +1 or -1 — rotational direction of every arc\n\n      // Scale of images during convergence (small edge thumbs -> large center)\n      scaleStart: 0.36,\n      scaleEnd: 1.0,\n\n      // Edge start positions as fractions of half-viewport (x, y).\n      edges: [\n        { x: -0.42, y: -1.00 }, // top-left\n        { x:  0.42, y: -1.00 }, // top-right\n        { x: -1.00, y:  0.00 }, // mid-left\n        { x:  1.00, y:  0.00 }, // mid-right\n        { x: -0.42, y:  1.00 }, // bottom-left\n        { x:  0.42, y:  1.00 }  // bottom-right\n      ],\n\n      titleStartOffset: 0.12, // word's resting x in phase 2 (× viewport width)\n      logoMenuGap: 12,        // px gap between the wordmark and the MENU pill\n\n      ease: { converge: 'power2.inOut', logo: 'power2.inOut', split: 'power3.inOut' }\n    };\n\n    /* ============================================================\n       BUILD DOM from the PROJECTS data\n       ============================================================ */\n    var stage = root.querySelector('[data-stage]');\n    var imagesEl = root.querySelector('[data-images]');\n    var panelsEl = root.querySelector('[data-panels]');\n    var logoEl = root.querySelector('[data-logo]');\n    var logoSubEl = root.querySelector('[data-logo-sub]');\n    var menuEl = root.querySelector('[data-menu]');\n    var titleEl = root.querySelector('[data-title]');\n    var experienceEl = root.querySelector('[data-experience]');\n\n    var N = PROJECTS.length;\n    function panelColor(i) { return CONFIG.panelColors[i % CONFIG.panelColors.length]; }\n\n    // Intro images: project[0] ends up on top of the stack (= project 1).\n    var imgEls = PROJECTS.map(function (p, i) {\n      var el = document.createElement('img');\n      el.className = 'proj-img';\n      el.src = p.image;\n      el.alt = p.left + ' ' + p.right;\n      el.style.zIndex = String(N - i); // index 0 highest\n      imagesEl.appendChild(el);\n      return el;\n    });\n\n    // Panels for the project transitions. Project 1 needs none — it is shown by\n    // the intro images on the white page.\n    var panelEls = PROJECTS.slice(1).map(function (p, i) {\n      var idx = i + 1;\n      var panel = document.createElement('div');\n      panel.className = 'panel';\n      panel.style.background = panelColor(idx);\n      panel.style.zIndex = String(idx);\n      var pimg = document.createElement('img');\n      pimg.className = 'panel-img';\n      pimg.src = p.image;\n      pimg.alt = p.left + ' ' + p.right;\n      panel.appendChild(pimg);\n      panelsEl.appendChild(panel);\n      return panel;\n    });\n\n    // Title carousel. Two stacked layers (base = black words + sub-labels,\n    // white = clipped white copies for the colour flip). Each layer has a left\n    // and a right COLUMN; each column stacks one cell per project.\n    function cells(side) {\n      return PROJECTS.map(function (p, i) {\n        return '<div class=\"title-cell\" style=\"--i:' + i + '\">' +\n               '<div class=\"title-word\">' + (side === 'left' ? p.left : p.right) + '</div>' +\n               '</div>';\n      }).join('');\n    }\n    function baseCells(side) {\n      return PROJECTS.map(function (p, i) {\n        return '<div class=\"title-cell\" style=\"--i:' + i + '\">' +\n               '<div class=\"title-word\">' + (side === 'left' ? p.left : p.right) + '</div>' +\n               '<div class=\"title-sub\">' + (side === 'left' ? p.date : p.discipline) + '</div>' +\n               '</div>';\n      }).join('');\n    }\n\n    titleEl.innerHTML =\n      '<div class=\"title-base\" data-title-base>' +\n        '<div class=\"title-col title-left\">' + baseCells('left') + '</div>' +\n        '<div class=\"title-col title-right\">' + baseCells('right') + '</div>' +\n      '</div>' +\n      '<div class=\"title-white\" data-title-white aria-hidden=\"true\">' +\n        '<div class=\"title-col title-left\">' + cells('left') + '</div>' +\n        '<div class=\"title-col title-right\">' + cells('right') + '</div>' +\n      '</div>';\n\n    var titleWhiteEl = titleEl.querySelector('[data-title-white]');\n    var leftCols = [].slice.call(titleEl.querySelectorAll('.title-col.title-left'));\n    var rightCols = [].slice.call(titleEl.querySelectorAll('.title-col.title-right'));\n    var words = [].slice.call(titleEl.querySelectorAll('.title-word'));\n    var subs = [].slice.call(titleEl.querySelectorAll('.title-sub'));\n\n    /* ============================================================\n       PROJECT PAGE — DOM\n       ============================================================ */\n    // Hover/click hotspot over the centred image (sized in computeLayout).\n    var hotspotEl = document.createElement('div');\n    hotspotEl.className = 'slot-hotspot';\n    hotspotEl.innerHTML = '<span class=\"plus\"></span>';\n    stage.appendChild(hotspotEl);\n\n    // Fixed header for the project page (white wordmark + frosted MENU).\n    var ppHeader = document.createElement('header');\n    ppHeader.className = 'pp-header';\n    ppHeader.innerHTML =\n      '<div class=\"pp-logo\">Studio Monet</div><button class=\"pp-menu\" type=\"button\">MENU</button>';\n    root.appendChild(ppHeader);\n    var ppLogo = ppHeader.querySelector('.pp-logo');\n\n    var LOREM =\n      'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pulvinar ' +\n      'sagittis nibh. Praesent auctor gravida dolor porttitor porttitor.';\n\n    var projectEl = document.createElement('section');\n    projectEl.className = 'project-page';\n    projectEl.hidden = true;\n    projectEl.innerHTML =\n      '<div class=\"pp-hero\">' +\n        '<img class=\"pp-hero-img\" data-pp-hero alt=\"\" />' +\n        '<div class=\"pp-hero-title\" data-pp-title></div>' +\n      '</div>' +\n      '<section class=\"pp-section pp-brief\">' +\n        '<div class=\"pp-brief-grid\">' +\n          '<div class=\"pp-brief-text\">' +\n            '<p class=\"pp-eyebrow\">The Brief</p>' +\n            '<p class=\"pp-body\">' + LOREM + '</p>' +\n          '</div>' +\n          '<img class=\"pp-brief-img\" data-pp-brief alt=\"\" />' +\n        '</div>' +\n      '</section>' +\n      '<section class=\"pp-ticker\"><div class=\"pp-ticker-track\" data-pp-ticker></div></section>' +\n      '<section class=\"pp-section pp-concept\">' +\n        '<p class=\"pp-eyebrow\">The Concept</p>' +\n        '<p class=\"pp-body\">' + LOREM + '</p>' +\n        '<img class=\"pp-concept-img\" data-pp-concept alt=\"\" />' +\n      '</section>' +\n      '<section class=\"pp-section pp-nav\">' +\n        '<a class=\"pp-nav-item prev\" data-pp-prev>' +\n          '<span class=\"pp-nav-label\">Previous Project</span>' +\n          '<span class=\"pp-nav-name\"></span>' +\n        '</a>' +\n        '<a class=\"pp-nav-item next\" data-pp-next>' +\n          '<span class=\"pp-nav-label\">Next Project</span>' +\n          '<span class=\"pp-nav-name\"></span>' +\n        '</a>' +\n      '</section>';\n    root.appendChild(projectEl);\n\n    var ppHeroImg = projectEl.querySelector('[data-pp-hero]');\n    var ppHeroTitle = projectEl.querySelector('[data-pp-title]');\n    var ppBriefImg = projectEl.querySelector('[data-pp-brief]');\n    var ppConceptImg = projectEl.querySelector('[data-pp-concept]');\n    var ppTicker = projectEl.querySelector('[data-pp-ticker]');\n    var ppPrev = projectEl.querySelector('[data-pp-prev]');\n    var ppNext = projectEl.querySelector('[data-pp-next]');\n\n    var onProjectPage = false;\n    var currentProject = 0;\n\n    /* ============================================================\n       SMOOTH SCROLL (Lenis) wired into GSAP's ticker + ScrollTrigger\n       ============================================================ */\n    var lenis = new Lenis({ lerp: 0.1, smoothWheel: true, wheelMultiplier: 1 });\n    lenis.on('scroll', ScrollTrigger.update);\n    gsap.ticker.add(function (time) { lenis.raf(time * 1000); });\n    gsap.ticker.lagSmoothing(0);\n\n    /* ============================================================\n       GEOMETRY HELPERS\n       ============================================================ */\n    function vw() { return window.innerWidth; }\n    function vh() { return window.innerHeight; }\n\n    // Image layout size (offsetWidth/Height ignore the transform scale).\n    function imgSize() {\n      return {\n        w: imgEls[0].offsetWidth || vw() * 0.54,\n        h: imgEls[0].offsetHeight || vw() * 0.54 * (864 / 1184)\n      };\n    }\n\n    // Curved path (in GSAP x/y transform space, 0,0 = centred) from an edge\n    // start position to the centre. A perpendicular-offset control point makes\n    // the path bow out; the same offset sign for all images => spiral.\n    function arcPath(edge) {\n      var s = imgSize();\n      var sImgW = s.w * CONFIG.scaleStart;\n      var sImgH = s.h * CONFIG.scaleStart;\n      var sx = edge.x * (vw() / 2);\n      var sy = edge.y * (vh() / 2);\n      // Push only along the axis the image exits from (its edge component ≈ ±1).\n      if (Math.abs(edge.x) >= 0.9) sx += Math.sign(edge.x) * (sImgW / 2 + CONFIG.offBuffer);\n      if (Math.abs(edge.y) >= 0.9) sy += Math.sign(edge.y) * (sImgH / 2 + CONFIG.offBuffer);\n\n      var dx = -sx, dy = -sy;\n      var len = Math.hypot(dx, dy) || 1;\n      var px = (-dy / len) * CONFIG.spiralDir;\n      var py = (dx / len) * CONFIG.spiralDir;\n\n      var bow = len * CONFIG.curveIntensity;\n      return [\n        { x: sx, y: sy },\n        { x: sx + dx * 0.5 + px * bow, y: sy + dy * 0.5 + py * bow },\n        { x: 0, y: 0 }\n      ];\n    }\n\n    // Content-box size — excludes the scrollbar, the reference the centred\n    // image (left/top: 50%) and the title actually lay out against.\n    function cw() { return document.documentElement.clientWidth; }\n    function ch() { return document.documentElement.clientHeight; }\n\n    // Final resting x for a split title group: centred in the margin between\n    // the image edge and the viewport edge.\n    function marginCenterX() { return (imgSize().w / 2 + cw() / 2) / 2; }\n\n    // Clip the white overlay to the image rectangle, so the white words only\n    // show where they overlap the image.\n    function clipWhiteToImage() {\n      var s = imgSize();\n      var x = (cw() - s.w) / 2;\n      var y = (ch() - s.h) / 2;\n      titleWhiteEl.style.clipPath = 'inset(' + y + 'px ' + x + 'px ' + y + 'px ' + x + 'px)';\n    }\n\n    // Position the top cluster (wordmark + MENU pill) so it sits vertically\n    // centred between the top of the window and the top of the image.\n    function layoutTopCluster() {\n      var gap = CONFIG.logoMenuGap;\n      var logoH = logoEl.getBoundingClientRect().height || 28;\n      var menuH = menuEl.getBoundingClientRect().height || 28;\n      var imageTop = (ch() - imgSize().h) / 2;\n      var clusterH = logoH + gap + menuH;\n      var clusterTop = Math.max(0, (imageTop - clusterH) / 2);\n      menuEl.style.top = (clusterTop + logoH + gap) + 'px';\n      return -(ch() / 2) + clusterTop + logoH / 2;\n    }\n\n    /* ============================================================\n       LAYOUT + STATES\n       The intro AUTOPLAYS on load (scroll locked); when it finishes the user\n       takes over and scrolls through the projects (scrubbed + soft snap).\n       ============================================================ */\n    var st;                       // project ScrollTrigger\n    var projectTL;                // project scrubbed timeline\n    var projectProgresses = [];   // 0..1 progress of each project's rest\n    var introDone = false;\n    var layout = {};\n\n    var prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n    function computeLayout() {\n      // The tall scroll height is applied in buildProjectScroll (at handover),\n      // so the document stays one viewport tall during the intro — nothing to\n      // scroll, while the scrollbar stays reserved in frame.\n      var slot = ch();\n      document.documentElement.style.setProperty('--mon-slot', slot + 'px');\n      layout = {\n        slot: slot,\n        startOffset: CONFIG.titleStartOffset * vw(),\n        marginX: marginCenterX(),\n        logoTargetY: layoutTopCluster()\n      };\n      clipWhiteToImage();\n\n      var s = imgSize();\n      hotspotEl.style.width = s.w + 'px';\n      hotspotEl.style.height = s.h + 'px';\n    }\n\n    // Intro START: images fully off-screen, wordmark centred, tagline shown.\n    function setInitialState() {\n      imgEls.forEach(function (el, i) {\n        var p = arcPath(CONFIG.edges[i % CONFIG.edges.length]);\n        gsap.set(el, { xPercent: -50, yPercent: -50, x: p[0].x, y: p[0].y, scale: CONFIG.scaleStart });\n      });\n      gsap.set(logoEl, { xPercent: -50, yPercent: -50, x: 0, y: 0 });\n      gsap.set(logoSubEl, { autoAlpha: 1 });\n      gsap.set(leftCols, { x: -layout.startOffset, y: 0 });\n      gsap.set(rightCols, { x: layout.startOffset, y: 0 });\n      gsap.set(words, { autoAlpha: 0 });\n      gsap.set(subs, { autoAlpha: 0 });\n      gsap.set(menuEl, { autoAlpha: 0 });\n      panelEls.forEach(function (p) { p.style.setProperty('--reveal', '100%'); });\n    }\n\n    // Intro END (project 1): images centred, wordmark at top, title split.\n    function applyIntroEndState() {\n      imgEls.forEach(function (el) { gsap.set(el, { x: 0, y: 0, scale: CONFIG.scaleEnd }); });\n      gsap.set(logoEl, { y: layout.logoTargetY });\n      gsap.set(logoSubEl, { autoAlpha: 0 });\n      gsap.set(leftCols, { x: -layout.marginX, y: 0 });\n      gsap.set(rightCols, { x: layout.marginX, y: 0 });\n      gsap.set(words, { autoAlpha: 1 });\n      gsap.set(subs, { autoAlpha: 1 });\n      gsap.set(menuEl, { autoAlpha: 1 });\n      panelEls.forEach(function (p) { p.style.setProperty('--reveal', '100%'); });\n    }\n\n    /* ---- INTRO (autoplay): spiral in -> text fades -> split -> sub-labels ---- */\n    function buildIntro() {\n      var phase1 = CONFIG.phase1, phase2 = CONFIG.phase2,\n          phase3 = CONFIG.phase3, phase4 = CONFIG.phase4;\n      var tl = gsap.timeline({\n        defaults: { ease: 'none' },\n        onComplete: function () { introDone = true; unlockScroll(); startProjects(); }\n      });\n      tl.timeScale(1 / CONFIG.introUnitSeconds); // 1 unit = introUnitSeconds\n      window.__monetIntro = tl;\n\n      // PHASE 1 — converge along arcs + scale up; logo rises; tagline covered.\n      imgEls.forEach(function (el, i) {\n        tl.to(el, {\n          motionPath: { path: arcPath(CONFIG.edges[i % CONFIG.edges.length]), curviness: 1 },\n          scale: CONFIG.scaleEnd, ease: CONFIG.ease.converge, duration: phase1\n        }, 0);\n      });\n      tl.to(logoEl, { y: layout.logoTargetY, ease: CONFIG.ease.logo, duration: phase1 }, 0);\n      // Tagline fades out early — gone before the images reach the centre.\n      tl.to(logoSubEl, { autoAlpha: 0, duration: phase1 * 0.3, ease: 'power1.out' }, phase1 * 0.2);\n\n      // PHASE 2 — title words fade in (white over the image).\n      tl.to(words, { autoAlpha: 1, duration: phase2, ease: 'power1.out' }, phase1);\n\n      // PHASE 3 — split to the margins (colour follows the image edge).\n      var p3 = phase1 + phase2;\n      tl.to(leftCols,  { x: -layout.marginX, ease: CONFIG.ease.split, duration: phase3 }, p3);\n      tl.to(rightCols, { x:  layout.marginX, ease: CONFIG.ease.split, duration: phase3 }, p3);\n      tl.to(menuEl, { autoAlpha: 1, duration: phase3 * 0.5, ease: 'power1.out' }, p3 + phase3 * 0.5);\n\n      // PHASE 4 — sub-labels fade in once the title has settled.\n      tl.to(subs, { autoAlpha: 1, duration: phase4, ease: 'power1.out' }, p3 + phase3);\n    }\n\n    /* ---- PROJECT SCROLL (scrubbed): carousel + panel wipes ---- */\n    function buildProjectScroll() {\n      var projStep = CONFIG.projStep;\n      var projDur = (N - 1) * projStep;\n      var slot = layout.slot;\n\n      // Now make the page scrollable (the scrollbar was already reserved).\n      experienceEl.style.height = (projDur * CONFIG.vhPerUnit) + 'vh';\n\n      projectTL = gsap.timeline({\n        defaults: { ease: 'none' },\n        scrollTrigger: {\n          trigger: experienceEl, start: 'top top', end: 'bottom bottom',\n          scrub: true, pin: stage, pinSpacing: false, invalidateOnRefresh: true\n        }\n      });\n      st = projectTL.scrollTrigger;\n      window.__monetST = st;\n\n      // 1+2. Left & right text carousels: one continuous vertical push.\n      projectTL.to(leftCols.concat(rightCols),\n        { y: -(N - 1) * slot, ease: 'none', duration: projDur }, 0);\n\n      // 3. Image wipes: reveal each panel bottom→top during its transition.\n      panelEls.forEach(function (panel, i) {\n        var proxy = { v: 100 };\n        projectTL.to(proxy, {\n          v: 0, ease: 'none', duration: projStep,\n          onUpdate: function () { panel.style.setProperty('--reveal', proxy.v + '%'); }\n        }, i * projStep);\n      });\n\n      // Snap targets: evenly spaced, one per project.\n      projectProgresses = PROJECTS.map(function (_, k) { return k / (N - 1); });\n      ScrollTrigger.refresh();\n    }\n\n    // Hand over to the user at project 1 (scroll 0).\n    function startProjects() {\n      window.scrollTo(0, 0);\n      lenis.scrollTo(0, { immediate: true });\n      buildProjectScroll();\n      hotspotEl.classList.add('active');\n    }\n\n    /* ============================================================\n       BOOT — lock scroll, autoplay the intro, then hand over.\n       ============================================================ */\n    function lockScroll() { lenis.stop(); }\n    function unlockScroll() { lenis.start(); }\n\n    // Reserve the scrollbar from the first paint so the handover doesn't shift\n    // the layout when the page becomes scrollable.\n    document.documentElement.style.overflowY = 'scroll';\n    if ('scrollRestoration' in history) history.scrollRestoration = 'manual';\n    window.scrollTo(0, 0);\n    lenis.scrollTo(0, { immediate: true });\n    computeLayout();\n    setInitialState();\n    lockScroll();\n    if (prefersReduced) {\n      applyIntroEndState();\n      introDone = true;\n      unlockScroll();\n      startProjects();\n    } else {\n      buildIntro();\n    }\n\n    /* ============================================================\n       SOFT SNAP — settles onto the nearest project once scrolling stops,\n       with momentum (fly through several, rest softly).\n       ============================================================ */\n    var snapTimer;\n    function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }\n\n    function trySnap() {\n      if (!st || !introDone || onProjectPage) return;\n      var range = st.end - st.start;\n      if (range <= 0) return;\n      var p = (lenis.scroll - st.start) / range;\n\n      var nearest = projectProgresses[0];\n      projectProgresses.forEach(function (pp) {\n        if (Math.abs(pp - p) < Math.abs(nearest - p)) nearest = pp;\n      });\n      var target = st.start + nearest * range;\n      if (Math.abs(target - lenis.scroll) < 1.5) return; // already aligned\n\n      lenis.scrollTo(target, { duration: CONFIG.snapDuration, easing: easeOutCubic });\n    }\n\n    // Lenis emits a scroll event every frame, so this debounce waits for\n    // everything to settle; a user scrolling mid-snap simply re-debounces.\n    lenis.on('scroll', function () {\n      clearTimeout(snapTimer);\n      snapTimer = setTimeout(trySnap, CONFIG.snapDelay);\n      // The project header is white — only legible over the hero image.\n      if (onProjectPage) {\n        ppHeader.classList.toggle('show', lenis.scroll < window.innerHeight * 0.6);\n      }\n    });\n\n    /* ============================================================\n       RESPONSIVE REBUILD — only on real width changes (ignore the mobile\n       address-bar height jitter that fires on scroll).\n       ============================================================ */\n    var lastW = window.innerWidth;\n    var resizeTimer;\n    var needsExperienceRebuild = false;\n    window.addEventListener('resize', function () {\n      if (window.innerWidth === lastW) return;\n      lastW = window.innerWidth;\n      clearTimeout(resizeTimer);\n      resizeTimer = setTimeout(function () {\n        // On the project page the experience is hidden — defer its rebuild\n        // until we return (pinning a display:none element misbehaves).\n        if (onProjectPage) { needsExperienceRebuild = true; return; }\n        rebuildExperience();\n      }, 200);\n    });\n\n    function rebuildExperience() {\n      if (st) st.kill();\n      gsap.globalTimeline.clear();\n      computeLayout();\n      if (introDone) {\n        applyIntroEndState();\n        buildProjectScroll();\n      } else {\n        setInitialState();\n        buildIntro();\n      }\n    }\n\n    /* ============================================================\n       PROJECT PAGE — open / navigate / close\n       ============================================================ */\n    function heroTitleFade() {\n      return gsap.fromTo(ppHeroTitle, { autoAlpha: 0 },\n        { autoAlpha: 1, duration: 0.8, ease: 'power2.out', delay: 0.15 });\n    }\n\n    // Index of the project currently centred in the scroll.\n    function currentScrollIndex() {\n      if (!st) return 0;\n      var range = st.end - st.start;\n      var p = range > 0 ? (lenis.scroll - st.start) / range : 0;\n      return Math.max(0, Math.min(N - 1, Math.round(p * (N - 1))));\n    }\n\n    function fillProject(index) {\n      var p = PROJECTS[index];\n      ppHeroImg.src = p.image;\n      ppHeroTitle.textContent = p.name;\n\n      // Random images (no repeats within the page where possible).\n      var used = [];\n      function pick() { var u = randomImage(used); used.push(u); return u; }\n      ppBriefImg.src = pick();\n      ppConceptImg.src = pick();\n\n      // Ticker: a strip of random images, duplicated for a seamless -50% loop.\n      var strip = [];\n      for (var i = 0; i < 7; i++) strip.push(pick());\n      ppTicker.innerHTML = strip.concat(strip)\n        .map(function (u) { return '<img src=\"' + u + '\" alt=\"\" />'; }).join('');\n\n      var prev = (index - 1 + N) % N;\n      var next = (index + 1) % N;\n      ppPrev.querySelector('.pp-nav-name').textContent = PROJECTS[prev].name;\n      ppNext.querySelector('.pp-nav-name').textContent = PROJECTS[next].name;\n      ppPrev.onclick = function () { goToProject(prev); };\n      ppNext.onclick = function () { goToProject(next); };\n    }\n\n    // Open from the scroll: expand the centred image to full screen.\n    function openProject(index) {\n      if (onProjectPage) return;\n      onProjectPage = true;\n      currentProject = index;\n      hotspotEl.classList.remove('active');\n\n      var r = hotspotEl.getBoundingClientRect();\n      var exp = document.createElement('img');\n      exp.className = 'expander';\n      exp.src = PROJECTS[index].image;\n      exp.style.cssText =\n        'top:' + r.top + 'px;left:' + r.left + 'px;width:' + r.width + 'px;height:' + r.height + 'px;';\n      root.appendChild(exp);\n\n      // Header cross-fade: black (experience) out, white/frosted (project) in.\n      gsap.to([logoEl, menuEl], { autoAlpha: 0, duration: 0.3 });\n      ppHeader.classList.add('show');\n\n      fillProject(index);\n\n      gsap.to(exp, {\n        top: 0, left: 0, width: window.innerWidth, height: window.innerHeight,\n        duration: 0.7, ease: 'power2.inOut',\n        onComplete: function () {\n          if (st) st.disable();\n          experienceEl.style.display = 'none';\n          projectEl.hidden = false;\n          window.scrollTo(0, 0);\n          lenis.scrollTo(0, { immediate: true });\n          lenis.resize();\n          exp.remove();\n          heroTitleFade();        // title only after the expand finishes\n        }\n      });\n    }\n\n    // Navigate between project pages: swap content, fade the title.\n    function goToProject(index) {\n      currentProject = index;\n      fillProject(index);\n      window.scrollTo(0, 0);\n      lenis.scrollTo(0, { immediate: true });\n      gsap.set(ppHeroTitle, { autoAlpha: 0 });\n      heroTitleFade();\n    }\n\n    // Return to the project scroll, landing back on the project we came from.\n    function closeProject() {\n      if (!onProjectPage) return;\n      onProjectPage = false;\n      projectEl.hidden = true;\n      ppHeader.classList.remove('show');\n      experienceEl.style.display = '';\n      gsap.set(ppHeroTitle, { autoAlpha: 0 });\n\n      if (needsExperienceRebuild) { needsExperienceRebuild = false; rebuildExperience(); }\n      if (st) st.enable();\n      ScrollTrigger.refresh();\n\n      var target = st ? st.start + (currentProject / (N - 1)) * (st.end - st.start) : 0;\n      window.scrollTo(0, target);\n      lenis.scrollTo(target, { immediate: true });\n      gsap.to([logoEl, menuEl], { autoAlpha: 1, duration: 0.3 });\n      hotspotEl.classList.add('active');\n    }\n\n    hotspotEl.addEventListener('click', function () { openProject(currentScrollIndex()); });\n    ppLogo.addEventListener('click', closeProject);\n  }\n})();\n"
            }
        }
    ],
    "media": [
        {
            "attachment_id": 472,
            "file": "monet-01.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-01.jpg",
            "mime": "image/jpeg",
            "bytes": 197343
        },
        {
            "attachment_id": 473,
            "file": "monet-02.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-02.jpg",
            "mime": "image/jpeg",
            "bytes": 154609
        },
        {
            "attachment_id": 474,
            "file": "monet-03.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-03.jpg",
            "mime": "image/jpeg",
            "bytes": 207607
        },
        {
            "attachment_id": 475,
            "file": "monet-04.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-04.jpg",
            "mime": "image/jpeg",
            "bytes": 236713
        },
        {
            "attachment_id": 476,
            "file": "monet-05.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-05.jpg",
            "mime": "image/jpeg",
            "bytes": 206073
        },
        {
            "attachment_id": 477,
            "file": "monet-06.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-06.jpg",
            "mime": "image/jpeg",
            "bytes": 245869
        },
        {
            "attachment_id": 478,
            "file": "monet-07.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-07.jpg",
            "mime": "image/jpeg",
            "bytes": 120388
        },
        {
            "attachment_id": 479,
            "file": "monet-08.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-08.jpg",
            "mime": "image/jpeg",
            "bytes": 149988
        },
        {
            "attachment_id": 480,
            "file": "monet-09.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-09.jpg",
            "mime": "image/jpeg",
            "bytes": 141215
        },
        {
            "attachment_id": 481,
            "file": "monet-10.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-10.jpg",
            "mime": "image/jpeg",
            "bytes": 149266
        },
        {
            "attachment_id": 482,
            "file": "monet-11.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-11.jpg",
            "mime": "image/jpeg",
            "bytes": 166370
        },
        {
            "attachment_id": 483,
            "file": "monet-12.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-12.jpg",
            "mime": "image/jpeg",
            "bytes": 155579
        },
        {
            "attachment_id": 484,
            "file": "monet-13.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-13.jpg",
            "mime": "image/jpeg",
            "bytes": 195515
        },
        {
            "attachment_id": 485,
            "file": "monet-14.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-14.jpg",
            "mime": "image/jpeg",
            "bytes": 144289
        },
        {
            "attachment_id": 486,
            "file": "monet-15.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-15.jpg",
            "mime": "image/jpeg",
            "bytes": 142892
        },
        {
            "attachment_id": 487,
            "file": "monet-16.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-16.jpg",
            "mime": "image/jpeg",
            "bytes": 197879
        },
        {
            "attachment_id": 488,
            "file": "monet-17.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-17.jpg",
            "mime": "image/jpeg",
            "bytes": 193353
        },
        {
            "attachment_id": 489,
            "file": "monet-18.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-18.jpg",
            "mime": "image/jpeg",
            "bytes": 147172
        },
        {
            "attachment_id": 490,
            "file": "monet-19.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/monet-19.jpg",
            "mime": "image/jpeg",
            "bytes": 104610
        },
        {
            "attachment_id": 492,
            "file": "thumb-studio-monet.jpg",
            "url": "https://gsap.pinul.com/wp-content/uploads/2026/09/thumb-studio-monet.jpg",
            "mime": "image/jpeg",
            "bytes": 96081
        }
    ],
    "media_note": "The pages build these URLs in JS, so they do not appear literally in the element tree. Re-upload every file below to the target site and point the BASE / src values at the new uploads path."
}