patx/afterdarklabs

Replace scroll animations with CSS

Commit a0fc069 · patx · 2026-09-07T15:53:52-04:00

Changeset
a0fc0690d8f0a3c6c5917af2164cfdabf555ff45
Parents
0a21862a35f97485f8d8cbd9facbb6f0f8546742

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/docs/index.html b/docs/index.html
index 07d766d..179afdc 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -240,18 +240,6 @@
             transform-origin: left center;
         }
 
-        .nav-brand-ready .nav-brand {
-            visibility: hidden;
-            opacity: 1;
-            pointer-events: none;
-            transform: none;
-        }
-
-        .nav-brand-ready .nav-brand.is-visible {
-            visibility: visible;
-            pointer-events: auto;
-        }
-
         .nav-cta {
             display: inline-flex;
             flex-shrink: 0;
@@ -290,19 +278,7 @@
 
         .nav-brand-letter {
             display: inline-block;
-            opacity: var(--nav-letter-opacity, 1);
-            filter: blur(var(--nav-letter-blur, 0));
-            transform:
-                translate3d(var(--nav-letter-x, 0), var(--nav-letter-y, 0), 0)
-                rotate(var(--nav-letter-rotate, 0))
-                skewX(var(--nav-letter-skew, 0))
-                scale(var(--nav-letter-scale-x, 1), var(--nav-letter-scale-y, 1));
             transform-origin: center;
-            will-change: opacity, filter, transform;
-        }
-
-        .nav-brand-ready .nav-brand-letter {
-            opacity: var(--nav-letter-opacity, 0);
         }
 
         @media (min-width: 1024px) {
@@ -364,11 +340,7 @@
 
         .hero-letter {
             display: inline-block;
-            opacity: var(--hero-letter-opacity, 1);
-            filter: blur(var(--hero-letter-blur, 0));
-            transform: scale(var(--hero-letter-scale, 1));
             transform-origin: center;
-            will-change: opacity, filter, transform;
         }
 
         .hero-word {
@@ -513,65 +485,6 @@
             padding-bottom: 56px;
         }
 
-        .work-intro.is-scroll-ready .work-heading-word {
-            opacity: 0;
-            filter: blur(0.18em);
-            transform: translate3d(0, 0.42em, 0) rotate(1.5deg);
-            transform-origin: left bottom;
-            will-change: opacity, filter, transform;
-        }
-
-        .work-intro.is-scroll-ready.is-motion-ready .work-heading-word {
-            transition:
-                opacity 220ms ease,
-                filter 360ms ease,
-                transform 420ms cubic-bezier(0.16, 1, 0.3, 1);
-        }
-
-        .work-intro.is-scroll-ready .work-heading-word.is-revealed {
-            opacity: 1;
-            filter: none;
-            transform: none;
-        }
-
-        @media (prefers-reduced-motion: reduce) {
-            .nav-brand-letter {
-                filter: none;
-                transform: none;
-                will-change: auto;
-            }
-
-            .hero-letter {
-                opacity: 1;
-                filter: none;
-                transform: none;
-                will-change: auto;
-            }
-
-            .hero-scroll-hint::after {
-                animation: none;
-            }
-
-            .work-intro {
-                height: auto;
-            }
-
-            .work-intro-stage {
-                position: relative;
-                top: auto;
-                height: auto;
-                transform: none;
-                will-change: auto;
-            }
-
-            .work-intro.is-scroll-ready .work-heading-word {
-                opacity: 1;
-                filter: none;
-                transform: none;
-                transition: none;
-            }
-        }
-
         .work-stage {
             box-sizing: border-box;
             padding: clamp(0rem, 0.5vw, 0.5rem) clamp(1rem, 2vw, 2rem);
@@ -647,20 +560,18 @@
             backdrop-filter: blur(0.35rem);
         }
 
-        #work-scroller > article:focus-within > .portfolio-details,
-        #work-scroller > article.is-description-visible > .portfolio-details {
+        #work-scroller > article:focus-within > .portfolio-details {
             opacity: 1;
             clip-path: inset(0);
             pointer-events: auto;
             transform: none;
         }
 
-        #work-scroller > article:focus-within > .portfolio-media > img,
-        #work-scroller > article.is-description-visible > .portfolio-media > img {
+        #work-scroller > article:focus-within > .portfolio-media > img {
             transform: scale(1.025);
         }
 
-        #work-scroller > article:focus-within {
+        #work-scroller > article:focus-visible {
             outline: 2px solid #fff;
             outline-offset: -2px;
         }
@@ -739,22 +650,6 @@
             outline-offset: 0.08em;
         }
 
-        .portfolio-scroll-ready #work-scroller > article {
-            opacity: var(--card-opacity, 0);
-            filter: blur(var(--card-blur, 0.4rem));
-            transform:
-                translate3d(var(--card-x, 0), var(--card-y, 5rem), 0)
-                rotate(var(--card-rotate, 0deg))
-                skewY(var(--card-skew, 0deg))
-                scale(var(--card-scale-x, 0.94), var(--card-scale-y, 1.04));
-            transform-origin: center bottom;
-            will-change: auto;
-        }
-
-        .portfolio-scroll-ready #work-scroller > article.is-card-animating {
-            will-change: opacity, filter, transform;
-        }
-
         @media (min-width: 768px) {
             #work-scroller {
                 grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -788,27 +683,284 @@
                 cursor: pointer;
             }
 
-            #work-scroller > article.is-description-visible {
-                cursor: default;
+            #work-scroller > article > .portfolio-details {
+                -webkit-backdrop-filter: none;
+                backdrop-filter: none;
             }
+        }
 
-            .portfolio-scroll-ready #work-scroller > article {
-                filter: none;
+        @supports (animation-timeline: scroll()) {
+            .hero-letter,
+            .nav-brand-letter {
+                animation-duration: auto;
+                animation-fill-mode: both;
+                animation-timeline: scroll(root block);
+                animation-timing-function: linear;
+                animation-range-start: var(--hero-range-start);
+                animation-range-end: var(--hero-range-end);
             }
 
-            #work-scroller > article > .portfolio-details {
-                -webkit-backdrop-filter: none;
-                backdrop-filter: none;
+            .hero-letter {
+                animation-name: hero-letter-out;
+                will-change: opacity, filter, transform;
+            }
+
+            .nav-brand-letter {
+                animation-name: nav-letter-in;
+                will-change: opacity, filter, transform;
+            }
+
+            .hero-letter:nth-child(1),
+            .nav-brand-letter:nth-child(1) {
+                --hero-range-start: 4.5svh;
+                --hero-range-end: 31svh;
+            }
+
+            .hero-letter:nth-child(2),
+            .nav-brand-letter:nth-child(2) {
+                --hero-range-start: 25svh;
+                --hero-range-end: 51.5svh;
+            }
+
+            .hero-letter:nth-child(3),
+            .nav-brand-letter:nth-child(3) {
+                --hero-range-start: 45.5svh;
+                --hero-range-end: 72svh;
+            }
+
+            .nav-brand {
+                animation-name: nav-brand-enable;
+                animation-duration: auto;
+                animation-fill-mode: both;
+                animation-timeline: scroll(root block);
+                animation-timing-function: steps(1, end);
+                animation-range: 4.5svh calc(4.5svh + 1px);
+            }
+
+            .hero-scroll-hint {
+                animation-name: scroll-hint-out;
+                animation-duration: auto;
+                animation-fill-mode: both;
+                animation-timeline: scroll(root block);
+                animation-timing-function: linear;
+                animation-range: 45.5svh 72svh;
+            }
+
+            .work-intro:not(.work-intro--closing) {
+                view-timeline-name: --work-intro;
+                view-timeline-axis: block;
+            }
+
+            .work-intro:not(.work-intro--closing) .work-heading-word {
+                animation-name: heading-word-in;
+                animation-duration: auto;
+                animation-fill-mode: both;
+                animation-timeline: --work-intro;
+                animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
+                animation-range-start: var(--word-range-start);
+                animation-range-end: var(--word-range-end);
+                transform-origin: left bottom;
+            }
+
+            .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(1) { --word-range-start: cover 38%; --word-range-end: cover 44%; }
+            .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(2) { --word-range-start: cover 39.2%; --word-range-end: cover 45.2%; }
+            .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(3) { --word-range-start: cover 40.4%; --word-range-end: cover 46.4%; }
+            .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(4) { --word-range-start: cover 41.6%; --word-range-end: cover 47.6%; }
+            .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(5) { --word-range-start: cover 42.8%; --word-range-end: cover 48.8%; }
+            .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(6) { --word-range-start: cover 44%; --word-range-end: cover 50%; }
+            .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(7) { --word-range-start: cover 45.2%; --word-range-end: cover 51.2%; }
+            .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(8) { --word-range-start: cover 46.4%; --word-range-end: cover 52.4%; }
+            .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(9) { --word-range-start: cover 47.6%; --word-range-end: cover 53.6%; }
+            .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(10) { --word-range-start: cover 48.8%; --word-range-end: cover 54.8%; }
+            .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(11) { --word-range-start: cover 50%; --word-range-end: cover 56%; }
+            .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(12) { --word-range-start: cover 51.2%; --word-range-end: cover 57.2%; }
+            .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(13) { --word-range-start: cover 52.4%; --word-range-end: cover 58.4%; }
+            .work-intro:not(.work-intro--closing) .work-heading-word:nth-child(14) { --word-range-start: cover 53.6%; --word-range-end: cover 59.6%; }
+
+            #work-scroller > article {
+                --card-start-blur: 7px;
+                --card-mid-blur: 3.5px;
+                --card-end-blur: 1.25px;
+                animation-name: portfolio-card-in-left;
+                animation-duration: auto;
+                animation-fill-mode: both;
+                animation-timeline: view(block);
+                animation-timing-function: linear;
+                animation-range: cover 4% cover 45%;
+                transform-origin: center bottom;
+            }
+
+            #work-scroller > article:nth-child(even) {
+                animation-name: portfolio-card-in-right;
+            }
+
+            .work-intro--closing {
+                view-timeline-name: --closing-intro;
+                view-timeline-axis: block;
+            }
+
+            .work-intro--closing .work-heading-email,
+            .work-intro--closing .work-heading-email > .work-heading-word {
+                animation-name: heading-word-in;
+                animation-duration: auto;
+                animation-fill-mode: both;
+                animation-timeline: --closing-intro;
+                animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
+                animation-range-start: var(--closing-range-start);
+                animation-range-end: var(--closing-range-end);
+                transform-origin: left bottom;
+            }
+
+            .work-intro--closing .work-heading-email { --closing-range-start: entry 4%; --closing-range-end: entry 20%; }
+            .work-intro--closing .work-heading-email > .work-heading-word:nth-child(1) { --closing-range-start: entry 18%; --closing-range-end: entry 34%; }
+            .work-intro--closing .work-heading-email > .work-heading-word:nth-child(2) { --closing-range-start: entry 32%; --closing-range-end: entry 48%; }
+            .work-intro--closing .work-heading-email > .work-heading-word:nth-child(3) { --closing-range-start: entry 46%; --closing-range-end: entry 62%; }
+            .work-intro--closing .work-heading-email > .work-heading-word:nth-child(4) { --closing-range-start: entry 60%; --closing-range-end: entry 76%; }
+            .work-intro--closing .work-heading-email > .work-heading-word:nth-child(5) { --closing-range-start: entry 74%; --closing-range-end: entry 90%; }
+            .work-intro--closing .work-heading-email > .work-heading-word:nth-child(6) { --closing-range-start: entry 84%; --closing-range-end: entry 100%; }
+
+            @media (orientation: portrait) {
+                #work-scroller > article:last-child {
+                    animation-range-end: cover 30%;
+                }
+            }
+
+            @media (min-width: 768px) and (orientation: portrait) {
+                #work-scroller > article:nth-last-child(2) {
+                    animation-range-end: cover 30%;
+                }
+            }
+        }
+
+        @keyframes hero-letter-out {
+            0% { opacity: 1; filter: blur(0); transform: scale(1); }
+            35% { opacity: 0.72; filter: blur(0.005em); transform: scale(0.984); }
+            65% { opacity: 0.28; filter: blur(0.013em); transform: scale(0.961); }
+            100% { opacity: 0; filter: blur(0.018em); transform: scale(0.945); }
+        }
+
+        @keyframes nav-letter-in {
+            0% {
+                opacity: 0;
+                filter: blur(0.32rem);
+                transform: translate3d(-1.35rem, 0, 0) rotate(-12deg) skewX(-18deg) scale(0.38, 1.28);
+            }
+            35% {
+                opacity: 0.28;
+                filter: blur(0.22rem);
+                transform: translate3d(-0.82rem, 0.08rem, 0) rotate(-6deg) skewX(-10deg) scale(0.61, 1.15);
+            }
+            65% {
+                opacity: 0.72;
+                filter: blur(0.09rem);
+                transform: translate3d(-0.18rem, -0.04rem, 0) rotate(1.5deg) skewX(2deg) scale(0.9, 1.04);
+            }
+            82% {
+                opacity: 0.92;
+                filter: blur(0.03rem);
+                transform: translate3d(0.05rem, 0.02rem, 0) rotate(-0.8deg) skewX(-1deg) scale(1.02, 0.99);
+            }
+            100% { opacity: 1; filter: blur(0); transform: none; }
+        }
+
+        @keyframes nav-brand-enable {
+            from { visibility: hidden; pointer-events: none; }
+            to { visibility: visible; pointer-events: auto; }
+        }
+
+        @keyframes scroll-hint-out {
+            from { opacity: 1; visibility: visible; pointer-events: auto; }
+            to { opacity: 0; visibility: hidden; pointer-events: none; }
+        }
+
+        @keyframes heading-word-in {
+            from {
+                opacity: 0;
+                filter: blur(0.18em);
+                transform: translate3d(0, 0.42em, 0) rotate(1.5deg);
+            }
+            to { opacity: 1; filter: blur(0); transform: none; }
+        }
+
+        @keyframes portfolio-card-in-left {
+            0% {
+                opacity: 0;
+                filter: blur(var(--card-start-blur));
+                transform: translate3d(-6vw, 96px, 0) rotate(-5deg) skewY(0deg) scale(0.93, 1.065);
+            }
+            50% {
+                opacity: 0.82;
+                filter: blur(var(--card-mid-blur));
+                transform: translate3d(-2vw, 42px, 0) rotate(-1.5deg) skewY(-0.8deg) scale(0.975, 1.025);
+            }
+            72% {
+                opacity: 1;
+                filter: blur(var(--card-end-blur));
+                transform: translate3d(0.35vw, 8px, 0) rotate(0.7deg) skewY(0.25deg) scale(1.006, 0.996);
+            }
+            100% { opacity: 1; filter: blur(0); transform: none; }
+        }
+
+        @keyframes portfolio-card-in-right {
+            0% {
+                opacity: 0;
+                filter: blur(var(--card-start-blur));
+                transform: translate3d(6vw, 96px, 0) rotate(5deg) skewY(0deg) scale(0.93, 1.065);
+            }
+            50% {
+                opacity: 0.82;
+                filter: blur(var(--card-mid-blur));
+                transform: translate3d(2vw, 42px, 0) rotate(1.5deg) skewY(0.8deg) scale(0.975, 1.025);
+            }
+            72% {
+                opacity: 1;
+                filter: blur(var(--card-end-blur));
+                transform: translate3d(-0.35vw, 8px, 0) rotate(-0.7deg) skewY(-0.25deg) scale(1.006, 0.996);
+            }
+            100% { opacity: 1; filter: blur(0); transform: none; }
+        }
+
+        @media (hover: none), (pointer: coarse) {
+            #work-scroller > article {
+                --card-start-blur: 0px;
+                --card-mid-blur: 0px;
+                --card-end-blur: 0px;
             }
         }
 
         @media (prefers-reduced-motion: reduce) {
-            .portfolio-scroll-ready #work-scroller > article {
+            .nav-brand,
+            .nav-brand-letter,
+            .hero-letter,
+            .hero-scroll-hint,
+            .work-heading-word,
+            #work-scroller > article {
+                animation: none !important;
                 opacity: 1;
                 filter: none;
+                visibility: visible;
                 transform: none;
             }
 
+            .nav-brand,
+            .hero-scroll-hint {
+                pointer-events: auto;
+            }
+
+            .hero-scroll-hint::after {
+                animation: none;
+            }
+
+            .work-intro {
+                height: auto;
+            }
+
+            .work-intro-stage {
+                position: relative;
+                top: auto;
+                height: auto;
+            }
+
             #work-scroller > article > .portfolio-media > img,
             #work-scroller > article > .portfolio-details {
                 transition: none;
@@ -970,7 +1122,7 @@
             <div class="work-stage">
                 <div class="work-stage-content">
                 <div id="work-scroller">
-                    <article>
+                    <article tabindex="0">
                         <div class="portfolio-media">
                             <img src="https://afterdarklabs.io/gardenfresh.webp?v=2" width="1200" height="702" loading="lazy" decoding="async" alt="Garden Fresh Wholesale ecommerce web app project by After Dark Labs">
                         </div>
@@ -980,7 +1132,7 @@
                             </p>
                         </div>
                     </article>
-                    <article>
+                    <article tabindex="0">
                         <div class="portfolio-media">
                             <img src="https://afterdarklabs.io/miadruck.webp?v=3" width="1200" height="681" loading="lazy" decoding="async" alt="Mia Druck real estate website project by After Dark Labs">
                         </div>
@@ -990,7 +1142,7 @@
                             </p>
                         </div>
                     </article>
-                    <article>
+                    <article tabindex="0">
                         <div class="portfolio-media">
                             <img src="https://afterdarklabs.io/coralvibe.webp?v=1" width="1200" height="678" loading="lazy" decoding="async" alt="Shopify store for Coral Vibe Florida project by After Dark Labs">
                         </div>
@@ -1000,7 +1152,7 @@
                             </p>
                         </div>
                     </article>
-                    <article>
+                    <article tabindex="0">
                         <div class="portfolio-media">
                             <img src="https://afterdarklabs.io/frontlineboats.webp?v=1" width="1200" height="700" loading="lazy" decoding="async" alt="Frontline Boat Services marine services website project by After Dark Labs">
                         </div>
@@ -1010,7 +1162,7 @@
                             </p>
                         </div>
                     </article>
-                    <article>
+                    <article tabindex="0">
                         <div class="portfolio-media">
                             <img src="https://afterdarklabs.io/voldservices.webp" width="1200" height="702" loading="lazy" decoding="async" alt="Vold Services exterior cleaning website with a scroll-animated hero, designed by After Dark Labs">
                         </div>
@@ -1020,7 +1172,7 @@
                             </p>
                         </div>
                     </article>
-                    <article>
+                    <article tabindex="0">
                         <div class="portfolio-media">
                             <img src="https://afterdarklabs.io/vegy.webp?v=1" width="1200" height="702" loading="lazy" decoding="async" alt="Vegy wholesale ordering platform project by After Dark Labs">
                         </div>
@@ -1030,7 +1182,7 @@
                             </p>
                         </div>
                     </article>
-                    <article>
+                    <article tabindex="0">
                         <div class="portfolio-media">
                             <img src="https://afterdarklabs.io/prima.webp?v=1" width="1200" height="702" loading="lazy" decoding="async" alt="Prima Produce business website project by After Dark Labs">
                         </div>
@@ -1040,7 +1192,7 @@
                             </p>
                         </div>
                     </article>
-                    <article>
+                    <article tabindex="0">
                         <div class="portfolio-media">
                             <img src="https://afterdarklabs.io/invoiceotter.webp?v=1" width="1200" height="673" loading="lazy" decoding="async" alt="Invoice Otter SaaS project by After Dark Labs">
                         </div>
@@ -1078,487 +1230,5 @@
 
     </main>
 
-    <script>
-        (function () {
-            const hero = document.getElementById('hero');
-            if (!hero) return;
-
-            const nav = document.querySelector('nav');
-            const mobileLayout = window.matchMedia('(max-width: 767px)');
-            let lockedWidth = 0;
-            let resizeFrame = 0;
-
-            function lockMobileHeroHeight() {
-                resizeFrame = 0;
-
-                if (!mobileLayout.matches) {
-                    hero.style.removeProperty('--mobile-hero-height');
-                    hero.style.removeProperty('--mobile-nav-height');
-                    lockedWidth = 0;
-                    return;
-                }
-
-                const navHeight = Math.round(nav?.getBoundingClientRect().height || 0);
-                if (navHeight) hero.style.setProperty('--mobile-nav-height', `${navHeight}px`);
-
-                const layoutWidth = Math.round(document.documentElement.clientWidth);
-                if (Math.abs(layoutWidth - lockedWidth) < 2) return;
-
-                const visibleHeight = Math.round(window.visualViewport?.height || window.innerHeight);
-                if (!visibleHeight) return;
-
-                lockedWidth = layoutWidth;
-                hero.style.setProperty('--mobile-hero-height', `${visibleHeight}px`);
-            }
-
-            function scheduleHeroHeightLock() {
-                if (!resizeFrame) resizeFrame = window.requestAnimationFrame(lockMobileHeroHeight);
-            }
-
-            window.addEventListener('resize', scheduleHeroHeightLock);
-            window.visualViewport?.addEventListener('resize', scheduleHeroHeightLock);
-            mobileLayout.addEventListener?.('change', scheduleHeroHeightLock);
-            lockMobileHeroHeight();
-        })();
-
-        (function () {
-            const hero = document.getElementById('hero');
-            const heading = hero?.querySelector('.hero-content h1');
-            const letters = Array.from(heading?.querySelectorAll('.hero-letter') || []);
-            const scrollHint = hero?.querySelector('.hero-scroll-hint');
-            const nav = document.querySelector('nav');
-            const brands = Array.from(document.querySelectorAll('.nav-brand'));
-            const navLetterGroups = brands.map(brand => Array.from(brand.querySelectorAll('.nav-brand-letter')));
-            if (!hero || !heading || !letters.length || !nav || !brands.length) return;
-
-            const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
-            let heroTop = 0;
-            let fadeDistance = 1;
-            let headingSize = 1;
-            let updateFrame = 0;
-            let resizeFrame = 0;
-
-            function clamp(value, minimum = 0, maximum = 1) {
-                return Math.min(maximum, Math.max(minimum, value));
-            }
-
-            function updateHeroLetters() {
-                updateFrame = 0;
-
-                if (reduceMotion.matches) {
-                    const revealBrand = hero.getBoundingClientRect().bottom <= nav.getBoundingClientRect().bottom + 1;
-                    letters.forEach(letter => {
-                        letter.style.removeProperty('--hero-letter-opacity');
-                        letter.style.removeProperty('--hero-letter-blur');
-                        letter.style.removeProperty('--hero-letter-scale');
-                    });
-                    navLetterGroups.forEach(group => group.forEach(letter => {
-                        letter.style.setProperty('--nav-letter-opacity', revealBrand ? '1' : '0');
-                        letter.style.removeProperty('--nav-letter-blur');
-                        letter.style.removeProperty('--nav-letter-x');
-                        letter.style.removeProperty('--nav-letter-y');
-                        letter.style.removeProperty('--nav-letter-rotate');
-                        letter.style.removeProperty('--nav-letter-skew');
-                        letter.style.removeProperty('--nav-letter-scale-x');
-                        letter.style.removeProperty('--nav-letter-scale-y');
-                    }));
-                    brands.forEach(brand => brand.classList.toggle('is-visible', revealBrand));
-                    if (scrollHint) {
-                        scrollHint.style.opacity = revealBrand ? '0' : '1';
-                        scrollHint.style.pointerEvents = revealBrand ? 'none' : 'auto';
-                        scrollHint.toggleAttribute('inert', revealBrand);
-                    }
-                    return;
-                }
-
-                const progress = clamp((window.scrollY - heroTop) / fadeDistance);
-                let visibleNavLetters = 0;
-
-                letters.forEach((letter, index) => {
-                    const start = 0.05 + index * 0.23;
-                    const localProgress = clamp((progress - start) / 0.3);
-                    const eased = localProgress * localProgress * (3 - 2 * localProgress);
-                    const remaining = 1 - eased;
-                    const wobble = Math.sin(localProgress * Math.PI * 3 + index * 0.65) * remaining;
-
-                    letter.style.setProperty('--hero-letter-opacity', (1 - eased).toFixed(4));
-                    letter.style.setProperty('--hero-letter-blur', `${(eased * headingSize * 0.018).toFixed(2)}px`);
-                    letter.style.setProperty('--hero-letter-scale', (1 - eased * 0.055).toFixed(4));
-
-                    if (scrollHint && index === letters.length - 1) {
-                        const hintVisible = remaining > 0.02;
-                        scrollHint.style.opacity = remaining.toFixed(4);
-                        scrollHint.style.pointerEvents = hintVisible ? 'auto' : 'none';
-                        scrollHint.toggleAttribute('inert', !hintVisible);
-                    }
-
-                    navLetterGroups.forEach(group => {
-                        const navLetter = group[index];
-                        if (!navLetter) return;
-
-                        navLetter.style.setProperty('--nav-letter-opacity', eased.toFixed(4));
-                        navLetter.style.setProperty('--nav-letter-blur', `${(remaining * 0.32).toFixed(3)}rem`);
-                        navLetter.style.setProperty('--nav-letter-x', `${(-1.35 * remaining + wobble * 0.42).toFixed(3)}rem`);
-                        navLetter.style.setProperty('--nav-letter-y', `${(wobble * 0.18).toFixed(3)}rem`);
-                        navLetter.style.setProperty('--nav-letter-rotate', `${(-12 * remaining + wobble * 7).toFixed(2)}deg`);
-                        navLetter.style.setProperty('--nav-letter-skew', `${(-18 * remaining + wobble * 9).toFixed(2)}deg`);
-                        navLetter.style.setProperty('--nav-letter-scale-x', `${(0.38 + eased * 0.62 + wobble * 0.08).toFixed(4)}`);
-                        navLetter.style.setProperty('--nav-letter-scale-y', `${(1.28 - eased * 0.28 - wobble * 0.06).toFixed(4)}`);
-                    });
-
-                    if (eased > 0.02) visibleNavLetters += 1;
-                });
-
-                brands.forEach(brand => brand.classList.toggle('is-visible', visibleNavLetters > 0));
-            }
-
-            function measureHeroLetters() {
-                resizeFrame = 0;
-                heroTop = hero.getBoundingClientRect().top + window.scrollY;
-                fadeDistance = Math.max(1, hero.offsetHeight - (nav?.getBoundingClientRect().height || 0));
-                headingSize = parseFloat(window.getComputedStyle(heading).fontSize) || 1;
-                updateHeroLetters();
-            }
-
-            function scheduleHeroLetterUpdate() {
-                if (!updateFrame) updateFrame = window.requestAnimationFrame(updateHeroLetters);
-            }
-
-            function scheduleHeroLetterMeasure() {
-                if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureHeroLetters);
-            }
-
-            window.addEventListener('scroll', scheduleHeroLetterUpdate, { passive: true });
-            window.addEventListener('resize', scheduleHeroLetterMeasure);
-            window.visualViewport?.addEventListener('resize', scheduleHeroLetterMeasure);
-            window.addEventListener('pageshow', event => {
-                if (event.persisted) scheduleHeroLetterMeasure();
-            });
-            reduceMotion.addEventListener('change', scheduleHeroLetterMeasure);
-            document.fonts?.ready.then(scheduleHeroLetterMeasure);
-            document.documentElement.classList.add('nav-brand-ready');
-            measureHeroLetters();
-        })();
-
-        (function () {
-            const intro = document.querySelector('.work-intro:not(.work-intro--closing)');
-            const heading = intro?.querySelector('.work-heading');
-            const firstCard = document.querySelector('#work-scroller > article');
-            const words = Array.from(intro?.querySelectorAll('.work-heading-word') || []);
-            if (!intro || !heading || !words.length) return;
-
-            const nav = document.querySelector('nav');
-            const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
-            let revealStart = 0;
-            let revealDistance = 1;
-            let revealedWords = -1;
-            let updateFrame = 0;
-            let resizeFrame = 0;
-
-            function clamp(value, minimum = 0, maximum = 1) {
-                return Math.min(maximum, Math.max(minimum, value));
-            }
-
-            function getDocumentTop(element) {
-                let top = 0;
-                let current = element;
-
-                while (current) {
-                    top += current.offsetTop;
-                    current = current.offsetParent;
-                }
-
-                return top;
-            }
-
-            function updateWorkIntro() {
-                updateFrame = 0;
-
-                if (reduceMotion.matches) {
-                    words.forEach(word => word.classList.add('is-revealed'));
-                    revealedWords = words.length;
-                    return;
-                }
-
-                const progress = window.scrollY <= 1
-                    ? 0
-                    : clamp((window.scrollY - revealStart) / revealDistance);
-                const nextRevealedWords = Math.ceil(progress * words.length);
-                if (nextRevealedWords === revealedWords) return;
-
-                revealedWords = nextRevealedWords;
-                words.forEach((word, index) => {
-                    word.classList.toggle('is-revealed', index < revealedWords);
-                });
-            }
-
-            function measureWorkIntro() {
-                resizeFrame = 0;
-                intro.classList.toggle('is-scroll-ready', !reduceMotion.matches);
-                intro.classList.remove('is-motion-ready');
-
-                const navHeight = nav?.getBoundingClientRect().height || 0;
-                const viewportHeight = window.visualViewport?.height || window.innerHeight;
-                const headingCenter = getDocumentTop(heading) + heading.offsetHeight / 2;
-                const visualCenter = navHeight + (viewportHeight - navHeight) * 0.58;
-                const cardRevealStart = firstCard
-                    ? getDocumentTop(firstCard) - viewportHeight * 0.94
-                    : headingCenter - visualCenter + viewportHeight * 0.32;
-                revealStart = headingCenter - visualCenter;
-                const revealEnd = Math.max(
-                    cardRevealStart + viewportHeight * 0.06,
-                    revealStart + viewportHeight * 0.24
-                );
-                revealDistance = Math.max(1, revealEnd - revealStart);
-                revealedWords = -1;
-                updateWorkIntro();
-
-                if (!reduceMotion.matches) {
-                    window.requestAnimationFrame(() => {
-                        window.requestAnimationFrame(() => intro.classList.add('is-motion-ready'));
-                    });
-                }
-            }
-
-            function scheduleWorkIntroUpdate() {
-                if (!updateFrame) updateFrame = window.requestAnimationFrame(updateWorkIntro);
-            }
-
-            function scheduleWorkIntroMeasure() {
-                if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureWorkIntro);
-            }
-
-            window.addEventListener('scroll', scheduleWorkIntroUpdate, { passive: true });
-            window.addEventListener('resize', scheduleWorkIntroMeasure);
-            window.addEventListener('pageshow', event => {
-                if (event.persisted) scheduleWorkIntroMeasure();
-            });
-            reduceMotion.addEventListener('change', scheduleWorkIntroMeasure);
-            document.fonts?.ready.then(scheduleWorkIntroMeasure);
-            measureWorkIntro();
-        })();
-
-        (function () {
-            const section = document.getElementById('work');
-            const cards = Array.from(section?.querySelectorAll('#work-scroller > article') || []);
-            if (!cards.length) return;
-
-            const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
-            const touchDisclosure = window.matchMedia('(hover: none), (pointer: coarse)');
-            let cardTops = [];
-            let cardStates = cards.map(() => -1);
-            let layoutWidth = 0;
-            let layoutHeight = 0;
-            let lastScrollY = -1;
-            let updateFrame = 0;
-            let resizeFrame = 0;
-
-            function clamp(value, minimum = 0, maximum = 1) {
-                return Math.min(maximum, Math.max(minimum, value));
-            }
-
-            function getDocumentTop(element) {
-                let top = 0;
-                let current = element;
-
-                while (current) {
-                    top += current.offsetTop;
-                    current = current.offsetParent;
-                }
-
-                return top;
-            }
-
-            function updateCards() {
-                updateFrame = 0;
-                if (reduceMotion.matches) {
-                    cards.forEach(card => card.classList.remove('is-card-animating'));
-                    return;
-                }
-
-                const scrollY = Math.max(0, window.scrollY);
-                const viewportHeight = layoutHeight || document.documentElement.clientHeight || window.innerHeight;
-                if (scrollY === lastScrollY) return;
-
-                lastScrollY = scrollY;
-                const revealDistance = Math.max(240, viewportHeight * 0.58);
-                const travelX = Math.min(72, document.documentElement.clientWidth * 0.06);
-
-                cards.forEach((card, index) => {
-                    const progress = clamp((scrollY + viewportHeight * 0.94 - cardTops[index]) / revealDistance);
-                    const state = progress <= 0 ? 0 : progress >= 1 ? 1 : 2;
-                    if (cardStates[index] !== state) {
-                        card.classList.toggle('is-card-animating', state === 2);
-                    }
-                    if (state !== 2 && cardStates[index] === state) return;
-
-                    cardStates[index] = state;
-                    const eased = 1 - Math.pow(1 - progress, 3);
-                    const remaining = 1 - eased;
-                    const direction = index % 2 === 0 ? -1 : 1;
-                    const wave = Math.sin(progress * Math.PI * 3) * remaining;
-                    const x = direction * (remaining * travelX + wave * 20);
-                    const y = remaining * 96 - Math.sin(progress * Math.PI) * 10;
-                    const rotation = direction * (remaining * 5 + wave * 2.4);
-                    const skew = direction * wave * 1.6;
-                    const scaleX = 1 - remaining * 0.07 + Math.abs(wave) * 0.025;
-                    const scaleY = 1 + remaining * 0.065 - wave * 0.02;
-                    const opacity = clamp(progress * 1.65);
-                    const blur = remaining * 7;
-
-                    card.style.setProperty('--card-x', `${x.toFixed(2)}px`);
-                    card.style.setProperty('--card-y', `${y.toFixed(2)}px`);
-                    card.style.setProperty('--card-rotate', `${rotation.toFixed(2)}deg`);
-                    card.style.setProperty('--card-skew', `${skew.toFixed(2)}deg`);
-                    card.style.setProperty('--card-scale-x', scaleX.toFixed(4));
-                    card.style.setProperty('--card-scale-y', scaleY.toFixed(4));
-                    card.style.setProperty('--card-opacity', opacity.toFixed(4));
-                    card.style.setProperty('--card-blur', `${blur.toFixed(2)}px`);
-                });
-            }
-
-            function measureCards() {
-                resizeFrame = 0;
-                const nextLayoutWidth = document.documentElement.clientWidth;
-                const nextLayoutHeight = document.documentElement.clientHeight || window.innerHeight;
-                const widthChanged = Math.abs(nextLayoutWidth - layoutWidth) >= 2;
-                const desktopHeightChanged = !touchDisclosure.matches && Math.abs(nextLayoutHeight - layoutHeight) >= 2;
-                if (layoutWidth && !widthChanged && !desktopHeightChanged) {
-                    scheduleCardUpdate();
-                    return;
-                }
-
-                layoutWidth = nextLayoutWidth;
-                layoutHeight = nextLayoutHeight;
-                cardTops = cards.map(getDocumentTop);
-                cardStates = cards.map(() => -1);
-                lastScrollY = -1;
-                updateCards();
-            }
-
-            function scheduleCardUpdate() {
-                if (!updateFrame) updateFrame = window.requestAnimationFrame(updateCards);
-            }
-
-            function scheduleCardMeasure() {
-                if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureCards);
-            }
-
-            function scheduleCardRemeasure() {
-                layoutWidth = 0;
-                scheduleCardMeasure();
-            }
-
-            function closeDescriptions(exceptCard = null) {
-                cards.forEach(card => {
-                    if (card !== exceptCard) card.classList.remove('is-description-visible');
-                });
-            }
-
-            cards.forEach(card => {
-                card.addEventListener('click', event => {
-                    if (!touchDisclosure.matches || event.target.closest('.portfolio-live-link')) return;
-
-                    closeDescriptions(card);
-                    card.classList.add('is-description-visible');
-                });
-            });
-
-            document.addEventListener('pointerdown', event => {
-                if (!touchDisclosure.matches || event.target.closest('#work-scroller > article')) return;
-                closeDescriptions();
-                const activeCard = document.activeElement?.closest?.('#work-scroller > article');
-                if (activeCard) document.activeElement.blur();
-            }, { passive: true });
-
-            touchDisclosure.addEventListener('change', () => closeDescriptions());
-
-            section.classList.add('portfolio-scroll-ready');
-            window.addEventListener('scroll', scheduleCardUpdate, { passive: true });
-            window.addEventListener('resize', scheduleCardMeasure);
-            window.visualViewport?.addEventListener('resize', scheduleCardMeasure);
-            window.addEventListener('pageshow', event => {
-                if (event.persisted) scheduleCardRemeasure();
-            });
-            reduceMotion.addEventListener('change', scheduleCardRemeasure);
-            measureCards();
-        })();
-
-        (function () {
-            const closingStatement = document.querySelector('.work-intro--closing');
-            const words = Array.from(closingStatement?.querySelectorAll('.work-heading-word') || []);
-            if (!closingStatement || !words.length) return;
-
-            const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
-            let revealStart = 0;
-            let revealDistance = 1;
-            let revealedWords = -1;
-            let updateFrame = 0;
-            let resizeFrame = 0;
-
-            function clamp(value, minimum = 0, maximum = 1) {
-                return Math.min(maximum, Math.max(minimum, value));
-            }
-
-            function updateClosingIntro() {
-                updateFrame = 0;
-
-                if (reduceMotion.matches) {
-                    words.forEach(word => word.classList.add('is-revealed'));
-                    revealedWords = words.length;
-                    return;
-                }
-
-                const progress = clamp((window.scrollY - revealStart) / revealDistance);
-                const nextRevealedWords = Math.ceil(progress * words.length);
-                if (nextRevealedWords === revealedWords) return;
-
-                revealedWords = nextRevealedWords;
-                words.forEach((word, index) => {
-                    word.classList.toggle('is-revealed', index < revealedWords);
-                });
-            }
-
-            function measureClosingIntro() {
-                resizeFrame = 0;
-                closingStatement.classList.toggle('is-scroll-ready', !reduceMotion.matches);
-                closingStatement.classList.remove('is-motion-ready');
-
-                const statementTop = closingStatement.getBoundingClientRect().top + window.scrollY;
-                const viewportHeight = window.visualViewport?.height || window.innerHeight;
-                const statementHeight = closingStatement.offsetHeight;
-                const maximumScroll = Math.max(0, document.documentElement.scrollHeight - viewportHeight);
-                revealStart = Math.min(maximumScroll, statementTop - viewportHeight + statementHeight * 0.05);
-                revealDistance = Math.max(1, maximumScroll - revealStart);
-                revealedWords = -1;
-                updateClosingIntro();
-
-                if (!reduceMotion.matches) {
-                    window.requestAnimationFrame(() => {
-                        window.requestAnimationFrame(() => closingStatement.classList.add('is-motion-ready'));
-                    });
-                }
-            }
-
-            function scheduleClosingIntroUpdate() {
-                if (!updateFrame) updateFrame = window.requestAnimationFrame(updateClosingIntro);
-            }
-
-            function scheduleClosingIntroMeasure() {
-                if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureClosingIntro);
-            }
-
-            window.addEventListener('scroll', scheduleClosingIntroUpdate, { passive: true });
-            window.addEventListener('resize', scheduleClosingIntroMeasure);
-            window.visualViewport?.addEventListener('resize', scheduleClosingIntroMeasure);
-            window.addEventListener('pageshow', event => {
-                if (event.persisted) scheduleClosingIntroMeasure();
-            });
-            reduceMotion.addEventListener('change', scheduleClosingIntroMeasure);
-            document.fonts?.ready.then(scheduleClosingIntroMeasure);
-            measureClosingIntro();
-        })();
-
-    </script>
 </body>
 </html>