The web port carried a third copy of the grain shader, so it would have kept
printing the old static, unseeded field after the phone stopped. It now imports
GRAIN_SKSL and grainCell from src/utils/grainShader.ts, exactly like the phone's
export, so there is one shader and one size rule for the whole pipeline. The
floor is one output pixel here, which is the whole story on this path: a
CanvasKit surface is rasterised in the pixels it is given, so no dp-to-device
correction is needed the way the phone preview needs one.
Typechecked only — no harness in the tree runs this file; the deployed app
(docker/frontend) has its own engine and already prints the new grain.