@@ -209,6 +209,11 @@ async function stabilize(page, { scroll, settle, freeze }) {
209209 // which leaves whole sections blank. Bounded so infinite-scroll pages can't loop forever.
210210 if ( scroll ) {
211211 await page . evaluate ( async ( ) => {
212+ // Force instant scrolling before we move. With the site's `scroll-behavior: smooth`,
213+ // the return-to-top below would animate and `resolve()` fires without awaiting it,
214+ // leaving a residual scroll offset that misplaces sticky/fixed navbars and scroll-linked
215+ // transforms in the full-page shot. This override persists for the final re-assert too.
216+ document . documentElement . style . scrollBehavior = 'auto' ;
212217 await new Promise ( ( resolve ) => {
213218 const step = Math . max ( 200 , Math . floor ( window . innerHeight * 0.8 ) ) ;
214219 const cap = 200000 ; // safety cap in px for infinite-scroll pages
@@ -267,6 +272,19 @@ async function stabilize(page, { scroll, settle, freeze }) {
267272 } else {
268273 await page . waitForTimeout ( 500 ) ;
269274 }
275+
276+ // 6. Re-assert the top of the page as the very last thing before capture. The scroll pass
277+ // can leave a residual offset, and scroll-driven UI reacts to it — sticky/fixed navbars
278+ // (which a full-page screenshot then paints floating mid-image) and `useScroll`-linked
279+ // transforms. scroll-behavior was set to `auto` in step 1, so this lands instantly; then
280+ // let the scroll listeners re-render (e.g. a navbar flipping back from fixed to relative)
281+ // and any resulting reveal settle before the shot.
282+ await page . evaluate ( ( ) => window . scrollTo ( 0 , 0 ) ) ;
283+ if ( freeze ) {
284+ await waitForVisualStability ( page , { timeout : Math . min ( settle , 3000 ) } ) ;
285+ } else {
286+ await page . waitForTimeout ( 500 ) ;
287+ }
270288}
271289
272290// ---- capture ---------------------------------------------------------------
0 commit comments