1/** 2 * SLEEK â homepage column parallax desync (DECK2 p.16-19). 3 * 4 * The right column's inner wrapper ([data-parallax-target], now a child 5 * of .sleek-col--right rather than being the col itself) gets 6 * translateY at 1.6à the page scroll velocity, so it appears to read 7 * "ahead" of the left column. Pure rAF loop, no scroll listener (the 8 * scroll-event flake documented in progress.js applies here too). 9 * 10 * Hard guards: 11 * - prefers-reduced-motion â no transforms ever. 12 * - viewport ⤠768px â no transforms (mobile is single-column stack). 13 * - target element missing â no-op. 14 * 15 * Clip placement (2026-04-24 refactor): 16 * Previous implementation set `overflow: hidden` on the grid 17 * container. That clipped EVERYTHING inside the grid â which blocked 18 * left-column tiles from breaking out to 100vw (needed for the Print 19 * Block ribbon). 20 * 21 * New placement: the outer `.sleek-col--right` acts as the clipping 22 * box (height = HL, overflow hidden), while its inner child carries 23 * the transform. The grid no longer clips, so left-column tiles are 24 * free to extend via negative margin / viewport units. 25 * 26 * Bottom-end alignment: 27 * Right col outer box is height=HL; the inner translates inside it. 28 * Page natural scroll = max(HL, HR_wrap_dom) - V â HL - V (since the 29 * outer is HL and the inner overflow is clipped). Nike banner / 30 * footer sit below the grid cleanly as before. 31 * 32 * ResizeObserver re-measures HL when images load late so the cap 33 * stays in sync with the real left-column height. 34 */ 35(function () { 36 'use strict'; 37 38 // 1. Reduced motion â bail early. 39 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 40 return; 41 } 42 43 // 2. Mobile breakpoint â bail. Mobile is a single-column stack. 44 var MOBILE_BREAKPOINT = 768; 45 function isMobile() { 46 return window.innerWidth <= MOBILE_BREAKPOINT; 47 } 48 49 var grid; 50 var leftCol; 51 var rightCol; // outer clipping box 52 var target; // inner transformed wrapper 53 var resizeObs; 54 var EXTRA_VELOCITY = 0.6; // 1.0 native + 0.6 extra = 1.6à total (tuned 2026-04-23) 55 var LERP = 0.15; // 0..1; higher = snappier, lower = silkier 56 var current = 0; 57 58 function setClipHeight() { 59 if (!rightCol || !leftCol) return; 60 var h = leftCol.offsetHeight; 61 if (h > 0) { 62 rightCol.style.height = h + 'px'; 63 rightCol.style.overflow = 'hidden'; 64 } 65 } 66 67 function clearClipHeight() { 68 if (!rightCol) return; 69 rightCol.style.height = ''; 70 rightCol.style.overflow = ''; 71 } 72 73 function tick() { 74 if (!target) return; 75 76 var scrollY = window.pageYOffset || document.documentElement.scrollTop || 0; 77 78 // Desired translation: pull the inner wrapper upward as the 79 // page scrolls. 80 var desired = -scrollY * EXTRA_VELOCITY; 81 82 // Clamp so the inner wrapper doesn't slide off the bottom of 83 // its own content. If inner height is HR_inner, the lowest its 84 // top can go is -(HR_inner - clipBoxHeight). We use the clip 85 // box height (outer right col) because that is effectively our 86 // "viewport" for the inner. 87 var innerH = target.offsetHeight; 88 var clipH = rightCol ? rightCol.offsetHeight : window.innerHeight; 89 var minDesired = Math.min(0, -(innerH - clipH)); 90 if (desired < minDesired) desired = minDesired; 91 if (desired > 0) desired = 0; 92 93 // Lerp current toward desired for silky motion. 94 current += (desired - current) * LERP; 95 96 // Snap final pixel when very close to avoid sub-pixel jitter. 97 if (Math.abs(desired - current) < 0.05) current = desired; 98 99 target.style.transform = 'translate3d(0, ' + current.toFixed(2) + 'px, 0)'; 100 101 window.requestAnimationFrame(tick); 102 } 103 104 function reset() { 105 if (target) target.style.transform = ''; 106 clearClipHeight(); 107 } 108 109 function init() { 110 grid = document.querySelector('[data-parallax-grid]'); 111 leftCol = grid ? grid.querySelector('.sleek-col--left') : null; 112 rightCol = grid ? grid.querySelector('.sleek-col--right') : null; 113 target = grid ? grid.querySelector('[data-parallax-target]') : null; 114 115 if (!grid || !leftCol || !rightCol || !target) return; 116 if (isMobile()) return; 117 118 // Initial measurement. Defer one frame so flex layout settles. 119 window.requestAnimationFrame(function () { 120 setClipHeight(); 121 }); 122 123 // Late image loads change left col height â re-measure when they do. 124 if (typeof ResizeObserver !== 'undefined') { 125 resizeObs = new ResizeObserver(function () { 126 setClipHeight(); 127 }); 128 resizeObs.observe(leftCol); 129 } else { 130 // Fallback: measure on every img load in left col. 131 var imgs = leftCol.querySelectorAll('img'); 132 for (var i = 0; i < imgs.length; i++) { 133 if (!imgs[i].complete) { 134 imgs[i].addEventListener('load', setClipHeight, { once: true }); 135 } 136 } 137 } 138 139 current = 0; 140 window.requestAnimationFrame(tick); 141 } 142 143 // On viewport resize across the mobile breakpoint, restart or stop. 144 var lastIsMobile = isMobile(); 145 window.addEventListener('resize', function () { 146 var nowMobile = isMobile(); 147 if (nowMobile !== lastIsMobile) { 148 lastIsMobile = nowMobile; 149 if (nowMobile) { 150 if (resizeObs) { resizeObs.disconnect(); resizeObs = null; } 151 reset(); 152 target = null; 153 } else { 154 init(); 155 } 156 } else if (!nowMobile) {
157 // Width changed but still desktop â re-measure clip height. 158 setClipHeight(); 159 } 160 }, { passive: true }); 161 162 if (document.readyState === 'loading') { 163 document.addEventListener('DOMContentLoaded', init); 164 } else { 165 init(); 166 } 167})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.