iPhone · 390 × 844 · Reader item
On disk — what the pageState store actually holds
The other half of the fix

Stop the page rebuilding in the first place

Everything above is the belt. Saving drafts means that when the page is destroyed, the words come back. The braces is not destroying the page at all. Right now, the moment the phone finds signal, Reader runs route() unconditionally — it re-renders the whole item, which throws away the iframe your composer lives in, whether or not anything about that page has actually changed. Nine times in ten nothing has changed: you were reading the same item before the tunnel and after it. The fix is to let the reconnect quietly sync in the background and only re-render if the item you are looking at genuinely came back different, or if you are on the contents page where new items should appear. If you are mid-item, the page stays exactly where it is: same scroll position, same composer, same half-typed sentence, no flicker.

Both together is the right answer — drafts alone still lose your scroll position and your cursor, and a non-destructive reconnect alone still loses everything if the app is killed in your pocket or iOS reclaims the tab.

Ship the non-destructive reconnect alongside draft persistence — make route() conditional on the item having actually changed?
Scope note — three producers, not one
  1. The shell's own To Reader meta-feedback panel.
  2. render-report.mjs — both the foot composer and every inline per-question reply box.
  3. The /review-site skill's HTML template.

A fix of exactly this shape shipped to two of the three last time and silently missed the third for a week. Whichever option is chosen, it lands in one shared snippet all three include, not three hand-copies.

Mockup only · not wired to the repo · 31Jul26f · tokens copied verbatim from shell/public/styles.css
Type into the composer, then hit the button. That is the incident, reproduced.