/* PROJECT 1804 · motion layer v16 (2026-11-06): page effects can be torn down and rebuilt, and while the soundtrack is on, links inside the site swap the page in place instead of reloading it, so the music never stops (Safari and Firefox block sound on every fresh page until the visitor clicks again). Loaded at the end of the site footer, after Lenis 1.3, GSAP 3 + ScrollTrigger and SplitType; the soundtrack script after it exposes window.P18Sound. Every effect sits behind a flag in F: set it to 0, upload the file again and point the footer at the new URL. Page transitions on full loads, the no-flash rules and the preloader live in the head code, not here. */ (function () { var F = { lenis: 1, // smooth scroll, one clock with GSAP header: 1, // page scroll locked under the open phone menu (the header itself stays as Webflow made it: fixed, inverting) hidehdr: 0, // off (Vova, 2026-11-04: the menu stays put). On: the header slides away on the way down, back on the way up cursor: 1, // "View case" label over case cards (mouse only) names: 1, // giant case names fill with ink as their card scrolls past reveal: 1, // case gallery images open from the bottom parallax: 0, // full-width case photos drift inside their frame (desktop). Off: the drift crops 6% of each side, wordmarks lost letters next: 1, // "Next project" before the footer of each case; a ring by the cursor fills as you scroll on and opens it footgap: 1, // one distance between the last block and the footer on every page hoverpic: 1, // Agency, About the bureau: the photo follows the cursor over the paragraph lines: 1, // one line-by-line heading per page (pages without case cards) sound: 1, // equaliser bars next to SOUND: ON while the track plays wordmark: 1, // footer wordmark rises out of its slot hover: 1, // one underline hover for text links grain: 1, // static paper grain on the first screen video: 1, // heavy GIFs play as MP4 softnav: 1, // with the soundtrack on, pages swap in place and the music carries on logo: 1 // Agency: the header wordmark (a Lottie Webflow shrinks on scroll) ends at the size of the logo on other pages }; var d = document, html = d.documentElement, W = window; if (html.classList.contains('w-editor')) return; var RM = matchMedia('(prefers-reduced-motion: reduce)').matches; var FINE = matchMedia('(hover: hover) and (pointer: fine)').matches; var EASE = 'cubic-bezier(.22,1,.36,1)'; var gsap = W.gsap, ST = W.ScrollTrigger; if (gsap && ST) gsap.registerPlugin(ST); var $$ = function (s, root) { return [].slice.call((root || d).querySelectorAll(s)); }; var pathOf = function () { return location.pathname.replace(/\/$/, '') || '/'; }; /* Smooth scroll: one instance for the whole visit (it survives in-place page swaps). lerp .09 answers the wheel at once and settles in ~0.4 s; the old build eased every wheel tick over 1.5 s. */ var lenis = null; if (F.lenis && W.Lenis && pathOf() !== '/') { lenis = new W.Lenis({ lerp: 0.09, anchors: false, stopInertiaOnNavigate: true }); W.p18lenis = lenis; // In-page links to a target that asks for room above it (scroll-margin-top, or the page's scroll-padding-top) land // where the browser itself would put them; Webflow's section scroll, which runs otherwise, ignores both and parks // the heading under the pinned menu (/services/production). Links without such room keep Webflow's behaviour. d.addEventListener('click', function (e) { if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; var a = e.target.closest && e.target.closest('a[href^="#"]'); if (!a || a.matches('.w-tab-link, [data-w-tab], .play, .pause')) return; var id = a.getAttribute('href').slice(1), t = id && d.getElementById(decodeURIComponent(id)); // The footer's back-to-top arrow points at an element some pages lack (its href is then a bare "#"). if (!id && a.matches('.arrow-top')) { e.preventDefault(); e.stopPropagation(); lenis.scrollTo(0, { immediate: RM }); return; } if (!t) return; var room = function () { return (parseFloat(getComputedStyle(t).scrollMarginTop) || 0) + (parseFloat(getComputedStyle(html).scrollPaddingTop) || 0); }; if (room() <= 0) return; e.preventDefault(); e.stopPropagation(); // Lenis subtracts scroll-margin-top and scroll-padding-top itself. Lazy photos above the target can grow the page during the // glide (phones: by thousands of px on /services/production), so it aims again until the target stays where it should. var aim = function (n) { lenis.scrollTo(t, { immediate: RM, onComplete: function () { setTimeout(function () { if (n < 6 && Math.abs(t.getBoundingClientRect().top - room()) > 2) aim(n + 1); }, 250); } }); }; aim(0); history.replaceState(history.state, '', '#' + id); // the address shows the section, as before, without a new history entry if (e.detail === 0) { if (!t.hasAttribute('tabindex')) t.setAttribute('tabindex', '-1'); t.focus({ preventScroll: true }); } // keyboard: focus follows }, true); if (gsap && ST) { lenis.on('scroll', ST.update); gsap.ticker.add(function (t) { lenis.raf(t * 1000); }); gsap.ticker.lagSmoothing(0); } else { requestAnimationFrame(function raf(t) { lenis.raf(t); requestAnimationFrame(raf); }); } } /* Back or Forward to a page the browser loads again (not from the back/forward cache). WebKit puts the scroll back at the first layout, short of the spot while images are still arriving; ScrollTrigger 3.11's refresh at DOMContentLoaded sends the page to the top to measure it and does not put it back; WebKit's second try when loading completes sometimes never comes. So the spot is kept per page and put back after load, unless the visitor has scrolled since. */ var yKey = 'p18-y' + location.pathname, nav = performance.getEntriesByType && performance.getEntriesByType('navigation')[0]; W.addEventListener('pagehide', function () { try { sessionStorage.setItem(yKey, Math.round(W.scrollY)); } catch (e) {} }); if (nav && nav.type === 'back_forward') { var moved = false, mark = function () { moved = true; }; ['wheel', 'touchstart', 'keydown'].forEach(function (t) { W.addEventListener(t, mark, { once: true, passive: true }); }); W.addEventListener('load', function () { var y; try { y = +sessionStorage.getItem(yKey); } catch (e) {} // ponytail: one spot per path, so two history entries of the same page share it; key it by entry if that ever matters. setTimeout(function () { if (y && !moved && Math.abs(W.scrollY - y) > 2) { if (lenis) lenis.scrollTo(y, { immediate: true, force: true }); W.scrollTo(0, y); } }); }); } /* Everything one page build adds (listeners, observers, nodes, scroll handlers) is recorded here, so destroy() can undo it. */ var undo = []; function on(t, type, fn, opt) { t.addEventListener(type, fn, opt); undo.push(function () { t.removeEventListener(type, fn, opt); }); } function add(node, parent) { (parent || d.body).appendChild(node); undo.push(function () { node.remove(); }); return node; } function onScroll(fn) { if (lenis) undo.push(lenis.on('scroll', function (e) { fn(e.scroll); })); else on(W, 'scroll', function () { fn(W.scrollY); }, { passive: true }); } function navigate(href) { return Nav.go ? Nav.go(href) : (location.href = href); } /* Cases in the /projects order: slug, name, place · year, key visual (the first image of the case, never cropped) and its size. */ var CASES = [ ['lavarice', 'LAVARICE', 'Los Angeles · 2024', '678ad39d509f8dbb54a2e351_Slide%2064%20(2).avif', 2560, 1440], ['glam', 'Glam Monte Carlo', 'Monte Carlo · 2024', '67880ac6749bcd91a3e8556b_Slide%2064.avif', 2560, 1664], ['saint-pierre', 'Saint Pierre', 'Saint Petersburg · 2022', '66ccfcae20b7997fb450d97f_Mask%20group%20(4).avif', 1380, 780], ['pose', 'POSE', 'London · 2024', '66deb7d3aed1107eb0a1a983_Mask%20group%20-%202024-09-09T135434.376.webp', 1920, 870], ['eyes-platform', 'EYES PLATFORM', 'Worldwide · 2024', '678acf9ce506d561441fa4ec_1%202345683.webp', 2560, 1108], ['darlen', 'Darlen Studio', 'Milan · 2025', '678ac6c09d6de37b976e13c9_Slide%2064%20(1).avif', 2560, 1348], ['project-1804', 'PROJECT 1804', 'Paris · 2023', '66e965364e839a988265b368_Mask%20group%20-%202024-09-17T161524.317-min.avif', 3840, 1740], ['ton', 'TON', 'Saint Petersburg · 2024', '66deb37e9d29f9c2bc0b5232_Mask%20group%20-%202024-09-09T133515.150.webp', 1962, 884], ['maison25', 'MAISON25', 'Saint Petersburg · 2021', '66e966fc1147077b76453230_Mask%20group%20-%202024-09-17T162400.721-min.avif', 3840, 1740], ['talli-beauty', 'Talli Beauty', 'Astana · 2023', '66e966abc5f8422ecad45fd8_Mask%20group%20-%202024-09-17T162239.982-min.avif', 2560, 1600], ['moyen', 'Moyen', 'Saint Petersburg · 2023', '66dcf5653361c83ddd770420_Mask%20group%20(52).avif', 1920, 870] ]; /* Styles do not depend on the page: written once. */ var css = '.lenis.lenis-stopped{overflow:clip}.lenis [data-lenis-prevent]{overscroll-behavior:contain}.lenis.lenis-smooth iframe{pointer-events:none}'; if (F.header) css += 'html.p18-menu-open,html.p18-menu-open body{overflow:hidden;overscroll-behavior:none}'; if (F.cursor) css += '.p18-card-img{cursor:none}' + '.p18-cursor{position:fixed;left:0;top:0;z-index:99990;pointer-events:none}' + '.p18-cursor-in{position:absolute;left:0;top:0;display:flex;align-items:center;gap:10px;padding:12px 16px;border-radius:5px;background:#1f1c1c;color:#f6f6f6;font:500 11px/1 Ppneuemontrealmono,ui-monospace,monospace;letter-spacing:-.02em;text-transform:uppercase;white-space:nowrap;transform:translate(-50%,-50%) scale(.5);opacity:0;transition:opacity .25s,transform .45s ' + EASE + '}' + '.p18-cursor.is-on .p18-cursor-in{opacity:1;transform:translate(-50%,-50%)}.p18-cursor i{font-style:normal}'; if (F.names) css += '.p18-ink{background:linear-gradient(90deg,#1f1c1c var(--p18ink,0%),rgba(31,28,28,.1) var(--p18ink,0%));-webkit-background-clip:text;background-clip:text;color:transparent!important}'; if (F.next) css += // Next project: a plain block at the end of the case, right before the footer. '.p18-next{padding:16vh 3vw 18vh}' + '.p18-next-a{display:grid;grid-template-columns:1fr 1fr;column-gap:3vw;align-items:end;color:#1f1c1c;text-decoration:none}' + '.p18-next-k{grid-column:1/-1;display:flex;justify-content:space-between;padding-bottom:14px;margin-bottom:5vh;border-bottom:1px solid #1f1c1c26;font:500 11px/1.2 Ppneuemontrealmono,ui-monospace,monospace;letter-spacing:-.02em;text-transform:uppercase;color:#1f1c1cb3}' + '.p18-next-t{grid-column:1;display:flex;flex-direction:column;gap:18px}' + '.p18-next-n{font:600 clamp(40px,6vw,96px)/.92 Ppmori,sans-serif;letter-spacing:-.03em;text-transform:uppercase}' + '.p18-next-n i{display:inline-block;font-style:normal;transition:translate .6s ' + EASE + '}' + '.p18-next-m{font:500 11px/1.2 Ppneuemontrealmono,ui-monospace,monospace;letter-spacing:-.02em;text-transform:uppercase;color:#1f1c1cb3}' + '.p18-next-f{grid-column:2;display:block;overflow:hidden;background:#e9e7e7}' + '.p18-next-f img{display:block;width:100%;height:100%;object-fit:cover;transition:scale 1.2s ' + EASE + '}' + '@media (hover:hover) and (pointer:fine){.p18-next-a:hover .p18-next-f img{scale:1.03}.p18-next-a:hover .p18-next-n i{translate:12px 0}}' + '.p18-next-a:focus-visible{outline:1px solid #1f1c1c;outline-offset:8px}' + '@media (max-width:767px){.p18-next{padding:10vh 16px 16vh}.p18-next-a{grid-template-columns:1fr;row-gap:28px}.p18-next-f{grid-column:1}}' + // Pull: a ring that follows the cursor (phones: bottom centre) and fills while the visitor keeps scrolling at the end. '.p18-pull{position:fixed;left:0;top:0;z-index:99990;pointer-events:none;opacity:0;transition:opacity .35s}' + '.p18-pull.is-on{opacity:1}' + '.p18-pull-in{position:absolute;left:0;top:0;display:flex;align-items:center;gap:16px;translate:-44px -44px}' + '.p18-pull svg{width:88px;height:88px;flex:none;rotate:-90deg;overflow:visible}' + '.p18-pull circle{fill:none;stroke-width:1.6}' + '.p18-pull .h{stroke:#f6f6f6;stroke-width:10}' + '.p18-pull .t{stroke:#1f1c1c33}.p18-pull .f{stroke:#1f1c1c;stroke-linecap:round;stroke-dasharray:289;stroke-dashoffset:calc(289 * (1 - var(--p,0)))}' + // The centre fills with ink as the ring fills: a small ink dot with a bobbing arrow grows into a full disc, and the arrow // turns from down (keep scrolling) to right (the next case is coming). '.p18-pull-d{position:absolute;left:44px;top:44px;width:68px;height:68px;margin:-34px 0 0 -34px;border-radius:50%;background:#1f1c1c;scale:calc(.32 + var(--p,0) * .68)}' + '.p18-pull-a,.p18-pull-g{position:absolute;left:44px;top:44px;translate:-50% -50%;font:500 15px/1 Ppneuemontrealmono,ui-monospace,monospace;color:#f6f6f6}' + '.p18-pull-a{opacity:calc(1 - var(--p,0) * 2.5);animation:p18bob 1.6s ease-in-out infinite}' + '.p18-pull-g{font-size:18px;opacity:calc((var(--p,0) - .45) * 4);translate:calc(-50% + (var(--p,0) - .7) * 10px) -50%}' + '@keyframes p18bob{50%{translate:-50% -30%}}' + '@media (prefers-reduced-motion:reduce){.p18-pull-a{animation:none}}' + '.p18-pull-l{display:flex;flex-direction:column;gap:4px;padding:9px 12px;border-radius:4px;background:#f6f6f6;font:500 12px/1.2 Ppneuemontrealmono,ui-monospace,monospace;letter-spacing:-.02em;text-transform:uppercase;color:#1f1c1c;white-space:nowrap}' + '.p18-pull-l i{font-style:normal;color:#1f1c1cb3}' + // Full ring: a sheet of paper slides up over the page (its top edge a hairline), the next case's name rises letter by // letter in the site's own faces, and a thin bar fills to 100% like the preloader's; then the case loads. '.p18-reveal{position:fixed;inset:0;z-index:99995;pointer-events:none;display:grid;place-content:center;justify-items:center;gap:26px;padding:0 4vw;background:#f6f6f6;color:#1f1c1c;text-align:center;transform:translateY(101%)}' + 'html.p18-handoff .navbar-logo-div{z-index:99999!important}' + '.p18-reveal:before{content:"";position:absolute;left:0;right:0;top:0;height:1px;background:#1f1c1c;opacity:var(--edge,1)}' + '.p18-reveal-n{display:flex;flex-wrap:wrap;justify-content:center;column-gap:.24em;margin:0;font:600 clamp(48px,9vw,150px)/.9 Ppmori,"Ppmori new",sans-serif;letter-spacing:-.035em;text-transform:uppercase}' + '.p18-reveal-w{display:inline-flex;overflow:hidden;padding:0 .02em .07em}' + '.p18-reveal-w span{display:inline-block;transform:translateY(112%)}' + '.p18-reveal i{font:500 11px/1.2 Ppneuemontrealmono,ui-monospace,monospace;font-style:normal;letter-spacing:-.02em;text-transform:uppercase;color:#1f1c1cb3;opacity:0}' + '.p18-reveal-b{position:relative;width:min(440px,64vw);height:1px;background:#1f1c1c26}' + '.p18-reveal-b:after{content:"";position:absolute;inset:0;background:#1f1c1c;transform-origin:0 50%;transform:scaleX(var(--b,0))}' + '.p18-reveal-m{display:flex;justify-content:space-between;width:min(440px,64vw);margin-top:-14px}' + '.p18-reveal-c{font-variant-numeric:tabular-nums}'; if (F.hidehdr) css += '.navbar,.navbar-logo-div,.div-logo-lottie.logo{transition:translate .55s cubic-bezier(.22,1,.36,1)}' + 'html.p18-hide:not(.p18-menu-open) .navbar,html.p18-hide:not(.p18-menu-open) .navbar-logo-div,html.p18-hide:not(.p18-menu-open) .div-logo-lottie.logo{translate:0 -110%}'; if (F.sound) css += // Out of the flow: the word ON and its neighbours never move. Phones show SOUND: ON • OFF in one tight row, so no bars there. '.play .nav-link-child>p{position:relative}' + '.p18-eq{position:absolute;left:100%;bottom:.12em;display:flex;align-items:flex-end;gap:1.5px;height:.72em;margin-left:5px;opacity:0;transition:opacity .3s}' + '@media (max-width:767px){.p18-eq{display:none}}' + 'html.p18-playing .p18-eq{opacity:1}' + '.p18-eq i{width:1.5px;height:100%;background:currentColor;transform-origin:bottom;transform:scaleY(.35)}' + 'html.p18-playing .p18-eq i{animation:p18eq 1s ease-in-out infinite}' + '.p18-eq i:nth-child(2){animation-duration:.8s!important;animation-delay:-.4s!important}.p18-eq i:nth-child(3){animation-duration:1.2s!important;animation-delay:-.7s!important}' + '@keyframes p18eq{0%,100%{transform:scaleY(.35)}50%{transform:scaleY(1)}}' + '@media (prefers-reduced-motion:reduce){html.p18-playing .p18-eq i{animation:none;transform:scaleY(.7)}}'; if (F.hover) css += '@media (hover:hover) and (pointer:fine){' + '.nav-link .nav-link-child>p{background:linear-gradient(currentColor,currentColor) no-repeat 100% 100%/0 1px;transition:background-size .5s ' + EASE + '}' + '.nav-link:hover .nav-link-child>p,.nav-link:focus-visible .nav-link-child>p{background-position:0 100%;background-size:100% 1px}}'; if (F.grain) css += '.p18-grain{position:absolute;left:0;top:0;width:100%;height:100vh;z-index:9997;pointer-events:none;opacity:.07;' + '-webkit-mask-image:linear-gradient(#000 55%,transparent);mask-image:linear-gradient(#000 55%,transparent);' + // Sparse dark specks (only the top third of the noise shows): tooth on the paper, its average tone moves by about 1 %. "background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .12 0 0 0 0 .11 0 0 0 0 .11 6 0 0 0 -3.3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\")}"; var sheet = d.createElement('style'); sheet.id = 'p18-motion'; sheet.textContent = css; d.head.appendChild(sheet); /* The soundtrack element lives for the whole visit, so its bars are wired once. */ var audio = d.getElementById('myAudio'); if (F.sound && audio) { var u = function () { html.classList.toggle('p18-playing', !audio.paused); }; audio.addEventListener('play', u); audio.addEventListener('pause', u); u(); } /* One page build. */ function init() { var path = pathOf(), isCase = /^\/projects\/[^/]+$/.test(path); var WIDE = matchMedia('(min-width: 992px)').matches; try { sessionStorage.setItem('p18-seen', '1'); } catch (e) {} html.classList.remove('p18-handoff'); // a page swapped in place after the hand-off starts with the usual header order if (path === '/') return; // the preloader page only forwards to /agency /* Phone menu open: the page underneath stays where it was (it used to scroll behind the sheet). .menu-main is the desktop nav row too; on phones it is the menu sheet, 0 px tall while closed. */ if (F.header) { var menu = d.querySelector('.menu-main'); if (menu && 'ResizeObserver' in W) { var locked = false; var ro = new ResizeObserver(function () { var open = W.innerWidth < 992 && menu.getBoundingClientRect().height > 40; if (open === locked) return; locked = open; html.classList.toggle('p18-menu-open', open); if (lenis) open ? lenis.stop() : lenis.start(); }); ro.observe(menu); undo.push(function () { ro.disconnect(); }); } } /* The header (Webflow's: fixed, inverting over the page) slides away while the visitor reads down and comes back on the first move up, so the menu never sits on top of the page's own text for long. Near the top it always shows. */ if (F.hidehdr) { var hy = W.scrollY, hacc = 0; var hs = function () { var y = W.scrollY, dy = y - hy; hy = y; if (y < 140) { hacc = 0; html.classList.remove('p18-hide'); return; } hacc = dy * hacc > 0 ? hacc + dy : dy; // distance travelled in the current direction if (hacc > 24) html.classList.add('p18-hide'); else if (hacc < -12) html.classList.remove('p18-hide'); }; on(W, 'scroll', hs, { passive: true }); // Keyboard: a focused menu link brings the header back. on(d, 'focusin', function (e) { if (e.target.closest && e.target.closest('.navbar, .navbar-logo-div')) html.classList.remove('p18-hide'); }); undo.push(function () { html.classList.remove('p18-hide'); }); } /* Contextual cursor over case cards: the photos become one big link to the case. Mouse and trackpad only. */ if (F.cursor && FINE && gsap) { var cards = $$('.single-card').filter(function (c) { return c.querySelector('a[href*="/projects/"]'); }); if (cards.length) { var cur = add(d.createElement('div')); cur.className = 'p18-cursor'; cur.setAttribute('aria-hidden', 'true'); cur.innerHTML = 'View case→'; var xTo = gsap.quickTo(cur, 'x', { duration: 0.5, ease: 'power3' }), yTo = gsap.quickTo(cur, 'y', { duration: 0.5, ease: 'power3' }); on(W, 'pointermove', function (e) { xTo(e.clientX); yTo(e.clientY); }, { passive: true }); cards.forEach(function (c) { var href = c.querySelector('a[href*="/projects/"]').getAttribute('href'); $$('img', c).forEach(function (img) { img.classList.add('p18-card-img'); img.addEventListener('pointerenter', function () { cur.classList.add('is-on'); }); img.addEventListener('pointerleave', function () { cur.classList.remove('is-on'); }); img.addEventListener('click', function () { cur.classList.remove('is-on'); navigate(href); }); }); }); on(d, 'pointerleave', function () { cur.classList.remove('is-on'); }); } } /* Giant case names: ink runs left to right while their card passes through the middle of the screen. Desktop shows one sticky name per card (.text-big._N), phones show the name inside each card. This is the text moment on these pages. */ var hasNames = !!d.querySelector('.single-card'); if (F.names && gsap && ST && hasNames && !RM) { // Sticky names (.text-big._N) are not numbered in card order, so they find their card by the name each card carries. var byName = {}, key = function (e) { return e.textContent.trim().toLowerCase(); }; $$('.wrap-cards .single-card').forEach(function (c) { var t = c.querySelector('.text-big'); if (t) byName[key(t)] = c; }); $$('.text-big').forEach(function (el) { var card = el.closest('.single-card') || byName[key(el)]; if (!card) Object.keys(byName).forEach(function (k) { if (!card && (k.indexOf(key(el)) === 0 || key(el).indexOf(k) === 0)) card = byName[k]; }); if (!card) return; el.classList.add('p18-ink'); var wrap = !el.closest('.single-card') && el.closest('.text-wrap-sticky'); if (!wrap) { // Name inside its card (phones): in while the card arrives, out while it leaves. gsap.fromTo(el, { '--p18ink': '0%' }, { '--p18ink': '100%', ease: 'none', scrollTrigger: { trigger: card, start: 'top 85%', end: 'top 30%', scrub: 0.4 } }); gsap.fromTo(el, { '--p18ink': '100%' }, { '--p18ink': '0%', ease: 'none', immediateRender: false, scrollTrigger: { trigger: card, start: 'bottom 80%', end: 'bottom 40%', scrub: 0.4 } }); return; } // Sticky names all wait on one line near the bottom of the screen. A name fades while its card's lower edge crosses // that line, and the next one starts only after, so a dark name never sits under the next card's captions. var line = function () { return Math.round(el.getBoundingClientRect().top - wrap.getBoundingClientRect().top + (parseFloat(getComputedStyle(wrap).top) || 0)); }; gsap.fromTo(el, { '--p18ink': '0%' }, { '--p18ink': '100%', ease: 'none', scrollTrigger: { trigger: card, start: function () { return 'top ' + line() + 'px'; }, end: 'top 30%', scrub: 0.4, invalidateOnRefresh: true } }); gsap.fromTo(el, { '--p18ink': '100%' }, { '--p18ink': '0%', ease: 'none', immediateRender: false, scrollTrigger: { trigger: card, start: function () { return 'bottom ' + (line() + el.offsetHeight) + 'px'; }, end: function () { return 'bottom ' + line() + 'px'; }, scrub: 0.4, invalidateOnRefresh: true } }); }); } /* One line-by-line heading per page, on pages without case cards: the first big heading below the first screen. Everything else that used to split into lines now just stands still (the site head no longer hides it). */ if (F.lines && W.SplitType && gsap && ST && !hasNames && !RM) { var moment = $$('h1[js-line-animation], h2[js-line-animation]').filter(function (e) { return e.offsetWidth && e.getBoundingClientRect().top + W.scrollY > W.innerHeight * 0.9; })[0]; if (moment) { var text = moment.innerHTML, played = false, tl; var split = function () { new W.SplitType(moment, { types: 'lines', tagName: 'span' }); $$('.line', moment).forEach(function (l) { l.style.overflow = 'hidden'; l.innerHTML = '' + l.innerHTML + ''; }); if (played) return; tl = gsap.fromTo($$('.line-inner', moment), { yPercent: 105 }, { yPercent: 0, duration: 1.1, ease: 'expo.out', stagger: 0.09, scrollTrigger: { trigger: moment, start: 'top 82%', once: true, onEnter: function () { played = true; } } }); }; split(); var ww = W.innerWidth; on(W, 'resize', function () { if (ww === W.innerWidth) return; ww = W.innerWidth; if (tl) { tl.scrollTrigger && tl.scrollTrigger.kill(); tl.kill(); } moment.innerHTML = text; split(); }); } } /* Case galleries. Images below the first screen open from the bottom and settle from a slight zoom. Full-width photos on desktop also drift inside a still frame: the photo moves, its clip keeps the original box. */ if (isCase && gsap && ST && !RM) { var skip = '.navbar, .navbar-logo-div, .container.footer, .p18-next'; var imgs = $$('img').filter(function (i) { return !i.closest(skip) && i.getBoundingClientRect().width >= 160 && i.getBoundingClientRect().top > W.innerHeight; }); imgs.forEach(function (img) { if (F.parallax && WIDE && img.classList.contains('full-image')) return frame(img); if (!F.reveal) return; gsap.set(img, { clipPath: 'inset(100% 0% 0% 0%)', scale: 1.06 }); ST.create({ trigger: img, start: 'top 90%', once: true, onEnter: function () { gsap.to(img, { clipPath: 'inset(0% 0% 0% 0%)', scale: 1, duration: 1.3, ease: 'expo.out', clearProps: 'clipPath,scale' }); } }); }); } /* Next project, the last block of the case before the footer: the case after this one in the /projects order with its key visual. Once the block reaches the middle of the screen a ring appears next to the cursor (phones: bottom centre), "Scroll down / to see the next project". Scrolling on down to the end of the page fills 70% of it; the rest takes a few more wheel turns, swipes or arrow keys at the very end (a pause drains that part). Full ring: the next cover opens from the ring to the whole screen and the next case loads. A click on the block opens it at once. Scroll position alone never completes the ring, so landing on the end (Back, restored scroll) is safe. The push only counts when it starts at the end (a new swipe, a fresh wheel turn after a pause, a key press, not momentum carried from scrolling down). The ring never sits on the header's text; it may sit on the footer. */ var footer = d.querySelector('.container.footer'); if (F.next && isCase && footer) { var at = -1; CASES.forEach(function (c, i) { if (path === '/projects/' + c[0]) at = i; }); if (at > -1) { var nx = CASES[(at + 1) % CASES.length], href = '/projects/' + nx[0]; var src = 'https://cdn.prod.website-files.com/66befd61cfe1b91f20ffdb64/' + nx[3]; var sec = d.createElement('section'); sec.className = 'p18-next'; sec.setAttribute('aria-label', 'Next project'); sec.innerHTML = '' + '' + ''; var fsec = footer.parentElement; add(sec, fsec.parentNode); fsec.parentNode.insertBefore(sec, fsec); if (gsap) { var pull = add(d.createElement('div')), rev = add(d.createElement('div')); pull.className = 'p18-pull'; pull.setAttribute('aria-hidden', 'true'); pull.innerHTML = '