@@ -92,8 +92,81 @@ for (const marquee of document.querySelectorAll(".marquee")) {
9292 for ( const item of items ) track . appendChild ( item . cloneNode ( true ) ) ;
9393 }
9494 clone . innerHTML = track . innerHTML ;
95+
96+ // duration scales with track width so every row moves at the same speed
97+ const duration = `${ track . scrollWidth / 40 } s` ;
98+ track . style . animationDuration = duration ;
99+ clone . style . animationDuration = duration ;
95100}
96101
102+ // ---- liquid-glass nav: lens-style edge refraction.
103+ // Needs backdrop-filter: url(), so it's enabled only where that works (Chromium);
104+ // other browsers keep the plain frosted-blur look. ----
105+ ( ( ) => {
106+ const nav = document . querySelector ( ".pill-nav" ) ;
107+ const map = document . getElementById ( "lg-map" ) ;
108+ if ( ! nav || ! map ) return ;
109+
110+ const isWebkit =
111+ / S a f a r i / . test ( navigator . userAgent ) &&
112+ ! / C h r o m e / . test ( navigator . userAgent ) ;
113+ const isFirefox = / F i r e f o x / . test ( navigator . userAgent ) ;
114+ const probe = document . createElement ( "div" ) ;
115+ probe . style . backdropFilter = "url(#liquid-glass)" ;
116+ if ( isWebkit || isFirefox || probe . style . backdropFilter === "" ) return ;
117+
118+ // displacement map: R drives x, G drives y. 50% gray (0.5) means "no shift",
119+ // so both gradients hold 0.5 through the middle and ramp smoothly only near
120+ // the rim — a continuous profile (no steps), otherwise the backdrop doubles
121+ const updateMap = ( ) => {
122+ const { width, height } = nav . getBoundingClientRect ( ) ;
123+ if ( ! width || ! height ) return ;
124+ // rasterize at device resolution — at fractional/high DPR the map is
125+ // otherwise misaligned with the backdrop and parts of the pill stay sharp
126+ const dpr = window . devicePixelRatio || 1 ;
127+ // same physical rim width on every side, so all four edges bend equally
128+ const ramp = Math . min ( width , height ) * 0.35 ;
129+ const rx = ( ( ramp / width ) * 100 ) . toFixed ( 2 ) ;
130+ const ry = ( ( ramp / height ) * 100 ) . toFixed ( 2 ) ;
131+ const svg =
132+ `<svg width="${ width * dpr } " height="${ height * dpr } " viewBox="0 0 ${ width } ${ height } " xmlns="http://www.w3.org/2000/svg">` +
133+ `<defs>` +
134+ `<linearGradient id="lg-x" x1="0%" y1="0%" x2="100%" y2="0%">` +
135+ `<stop offset="0%" stop-color="#ff0000"/>` +
136+ `<stop offset="${ rx } %" stop-color="#800000"/>` +
137+ `<stop offset="${ 100 - rx } %" stop-color="#800000"/>` +
138+ `<stop offset="100%" stop-color="#000000"/>` +
139+ `</linearGradient>` +
140+ `<linearGradient id="lg-y" x1="0%" y1="0%" x2="0%" y2="100%">` +
141+ `<stop offset="0%" stop-color="#00ff00"/>` +
142+ `<stop offset="${ ry } %" stop-color="#008000"/>` +
143+ `<stop offset="${ 100 - ry } %" stop-color="#008000"/>` +
144+ `<stop offset="100%" stop-color="#000000"/>` +
145+ `</linearGradient>` +
146+ `</defs>` +
147+ `<rect width="${ width } " height="${ height } " fill="url(#lg-x)"/>` +
148+ `<rect width="${ width } " height="${ height } " fill="url(#lg-y)" style="mix-blend-mode:screen"/>` +
149+ `</svg>` ;
150+ // pin the map to the element box in user units — the filter region is
151+ // larger than the element, so percentage sizing would misalign it
152+ map . setAttribute ( "x" , "0" ) ;
153+ map . setAttribute ( "y" , "0" ) ;
154+ map . setAttribute ( "width" , `${ width } ` ) ;
155+ map . setAttribute ( "height" , `${ height } ` ) ;
156+ map . setAttribute (
157+ "href" ,
158+ `data:image/svg+xml,${ encodeURIComponent ( svg ) } ` ,
159+ ) ;
160+ } ;
161+
162+ // single displacement pass: subtle lens bend at the rim, no color fringing
163+ document . getElementById ( "lg-lens" ) ?. setAttribute ( "scale" , "-35" ) ;
164+
165+ updateMap ( ) ;
166+ new ResizeObserver ( updateMap ) . observe ( nav ) ;
167+ nav . classList . add ( "glass-active" ) ;
168+ } ) ( ) ;
169+
97170// ---- cursor spotlight on feature cards ----
98171if ( window . matchMedia ( "(hover: hover)" ) . matches ) {
99172 for ( const card of document . querySelectorAll ( ".feature-card" ) ) {
@@ -136,6 +209,8 @@ function countUp(el, target) {
136209 el . textContent = formatStat ( target ) ;
137210 return ;
138211 }
212+ // reserve the final width up front so the row doesn't shift while counting
213+ el . style . minWidth = `${ formatStat ( target ) . length } ch` ;
139214 const duration = 1200 ;
140215 const start = performance . now ( ) ;
141216 const tick = ( now ) => {
0 commit comments