--- a/source/template.html
+++ b/source/template.html
@@ -1570,6 +1570,9 @@
 {let taps=[];document.addEventListener('click',e=>{if(!e.target.closest('.logo'))return;e.preventDefault();const n=performance.now();taps=taps.filter(x=>n-x<800);taps.push(n);if(taps.length>=3)mkDbg();},true);}addEventListener('keydown',e=>{if(e.key==='d'||e.key==='D')mkDbg();});
 const $=id=>document.getElementById(id);
 const clamp=(x,a=0,b=1)=>Math.min(b,Math.max(a,x));
+/* a film that has painted a frame keeps it on screen while it seeks, but readyState drops to 1 for every seek;
+   gating on the live readyState swapped in the previous scene on each scroll step (the 'blinking'), so latch it */
+const hasFrame=v=>{if(v.readyState>=2)v._hf=1;else if(v.readyState===0)v._hf=0;return !!v._hf;};
 const lerp=(a,b,t)=>a+(b-a)*t;
 const ease=x=>x<.5?2*x*x:1-Math.pow(-2*x+2,2)/2;
 const sticky=document.querySelector('.sticky');
@@ -2160,14 +2163,14 @@
     const T=Math.min(TOTAL-.05,lin(M?BTW:BTWD,pr));
     const inB=T>=SPLIT, v=inB?vB:vA, tv=inB?T-SPLIT:T;
     /* the Connect film ends on the keys; part A starts on the same frame, part B starts on part A's last frame */
-    const ready=v.readyState>=2||(inB&&vA.readyState>=2);
+    const ready=hasFrame(v)||(inB&&hasFrame(vA));
     bkv.style.opacity=ready?1:0; if(ready)cnv.style.opacity=0;
     $('cnCopy').style.opacity=+$('cnCopy').style.opacity*(1-k(0,.03));
-    vA.style.opacity=(inB&&vB.readyState>=2)?0:1; vB.style.opacity=(inB&&vB.readyState>=2)?1:0;
+    vA.style.opacity=(inB&&hasFrame(vB))?0:1; vB.style.opacity=(inB&&hasFrame(vB))?1:0;
     placeFilm(vA,null,T,[[0,1]],BFT,bkCopy,BFTD); placeFilm(vB,null,T,[[0,1]],BFT,bkCopy,BFTD);
     if(inB){if(vB.readyState>=1){sB+=(tv-sB)*.3;if(Math.abs(vB.currentTime-sB)>1/40&&!vB.seeking)vB.currentTime=sB;}}
     else{if(vA.readyState>=1){sA+=(tv-sA)*.3;if(Math.abs(vA.currentTime-sA)>1/40&&!vA.seeking)vA.currentTime=sA;}}
-    if(inB&&vB.readyState<2&&vA.readyState>=1&&!vA.seeking&&vA.currentTime<SPLIT-.2)vA.currentTime=SPLIT-.06;   // part B still loading: hold part A's last frame (the same frame part B starts on)
+    if(inB&&!hasFrame(vB)&&vA.readyState>=1&&!vA.seeking&&vA.currentTime<SPLIT-.2)vA.currentTime=SPLIT-.06;   // part B still loading: hold part A's last frame (the same frame part B starts on)
     const TS=inB?(vB.readyState>=1?SPLIT+vB.currentTime:SPLIT-.05):(vA.readyState>=1?vA.currentTime:T);   // what the viewer actually sees (valid even mid-seek)
     let cur=0;for(let i=0;i<BSEG.length;i++)if(TS>=BSEG[i])cur=i;BK.cur=cur;BK.curp=clamp((TS-BSEG[cur])/(((BSEG[cur+1])||TOTAL)-BSEG[cur]));
     /* finale: the night sky keeps moving, the brand arrives */
@@ -2178,7 +2181,7 @@
 
 EV.layout();
 let PS=null,PT=0,HOLD=0;
-function lidReady(){try{const b=vt.buffered;return vt.readyState>=3||(vt.duration&&b.length&&b.end(b.length-1)>=vt.duration*.5);}catch(e){return false;}}
+function lidReady(){if(lidReady.ok)return true;try{const b=vt.buffered;return lidReady.ok=!!(vt.readyState>=3||(vt.duration&&b.length&&b.end(b.length-1)>=vt.duration*.5));}catch(e){return false;}}   // latched: a seek's readyState dip must not pull the scroll back to the closed lid
 function progress(){const r=pin.getBoundingClientRect(),raw=clamp(-r.top/(pin.offsetHeight-innerHeight)),now=performance.now(),dt=Math.min(64,now-(PT||now));PT=now;
   if(PS===null||Math.abs(raw-PS)>.3)PS=raw;                                   // big jumps (dock links) land directly
   else{const k=1-Math.pow(1-.13,dt/16.7);let step=(raw-PS)*k;
@@ -2208,7 +2211,7 @@
   cue.style.opacity=1-clamp(p/.03);
   mA.style.opacity=1-hc;mA.style.pointerEvents=hc>.5?'none':'auto';
   /* lid opens (scrubbed) */
-  const tReady=vt.readyState>=2;
+  const tReady=hasFrame(vt);
   sTrans.style.opacity=tReady?clamp((p-.06)/.03):0;
   sHero.style.opacity=tReady?1-clamp((p-.08)/.02):1;
   const tp=ease(clamp((p-.06)/.66));
