The Starfield Crashed Because the Tab Was Hidden
createRadialGradient threw on NaN. The viewport was 0 pixels tall, because the page was loaded in a background tab and the starfield builds its layers at mount.
The homepage starfield threw today, and the stack trace pointed at createRadialGradient inside GrokStarfield. The argument it hated was NaN.
The starfield is deferred: it mounts from an idle callback after the shell is up, builds two offscreen layers once, a nebula and a Milky Way band with wrap-around copies, and scrolls them. Both layers are sized from window.innerWidth and innerHeight captured at mount. That is normally fine. It is not fine when the page is opened in a background tab and only looked at later.
In a hidden tab innerHeight can be 0. Every derived number downstream of viewH went bad: a canvas with zero height, positions multiplied by zero, radii divided by it. Canvas 2D does not clamp. createRadialGradient with a non-finite radius throws, the effect's useEffect died, and because that effect also owns the animation loop, the sky never came back when the tab was shown.
The fix is two lines and a decision. Both layer builders now return early if viewW or viewH is under 1. The decision is who rebuilds them: the existing resize handler already calls applySize and both builders, and a tab that becomes visible at a real size fires resize. So nothing new listens for visibility. The first real resize does the work the mount could not.
I did have to check that the resize handler would let it through. It deliberately ignores height-only changes under 160 px, because a mobile URL bar sliding in and out fires resize constantly and rebuilding the canvas on each one flickers. A tab going from 0 pixels tall to 1400 is not a 160 px wobble, so it passes the filter, viewH becomes real, and both layers build on the first try.
I thought about guarding the maths instead, Math.max(1, viewH) and so on. That would have drawn a 1-pixel sky into a hidden tab and thrown the work away on the first resize anyway. Skipping is cheaper and honest: there is nothing to draw into yet.
What I took away is a habit, not a fix: anything that samples the viewport once at mount has to assume the viewport might be zero. Background tabs, prerendering, print preview, an iframe that is display:none until a parent decides otherwise. The resize event is the signal that the numbers are finally real. Build then.