Skip to content

Fix Safari chrome tint with simulator-verified mechanisms - #67

Merged
amyrlam merged 5 commits into
mainfrom
claude/github-issue-60-viewport-fit
Aug 24, 2026
Merged

Fix Safari chrome tint with simulator-verified mechanisms#67
amyrlam merged 5 commits into
mainfrom
claude/github-issue-60-viewport-fit

Conversation

@amyrlam

@amyrlam amyrlam commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Fourth pass at #60 (latest report) — this time debugged against real iOS Safari in the simulator (Xcode + iOS 26.5 runtime, iPhone 17 Pro) instead of by inference, with pixel-sampled before/after evidence.

What the simulator revealed

Your screen recording showed the truth: the bottom toolbar was following the sky (PR #66 worked), but the top status-bar strip stayed stuck, with a hard line. Driving the real site through scripted day/night switches in the simulator and sampling pixels:

  1. The status-bar strip tints from the body's computed background-color — not theme-color, not the page's rendered pixels. And when that color changes only because a CSS custom property (--sky-bottom) transitions, Safari never re-samples: the strip keeps the sky from page load. Sampled proof: strip stayed rgb(112,140,170) (day) through every switch before the fix; after, it tracks rgb(112,140,170)rgb(29,42,97) exactly.
  2. Safari applies theme-color once, around pageshow, and can miss meta swaps made while the page is still loading — which is when sky.ts does its first renders. That's the toolbar stuck on the static midnight #141a3d ("stuck as dark blue night mode"). It also explains the randomness: back/forward-cache restores skip the load path entirely.

The fixes

  • render() writes the ground color as an inline background-color on body — a real style mutation Safari notices and re-samples. Invisible on the page itself (the opaque gradient paints over it); it only feeds the chrome.
  • start() re-renders on pageshow (immediately + 1.2s later, straddling Safari's variable apply point), covering both the mid-load miss and bfcache restores. Meta replacement is now unconditional so an equal-color repaint still re-asserts the value.
  • viewport-fit=cover: the sky paints edge-to-edge under the notch and home indicator (safe-area side padding added for landscape; the controls, footer, and lightbox already pad by the insets). Verified the about page's back-link clears the notch.
  • New tester hook: ?sky=day|night|auto pins the mode for one load (not persisted) — this is what drove the simulator runs, and it makes phone testing much easier for you too.

Verified

  • iOS 26.5 simulator, fresh Safari session: status strip tracks every auto/day/night switch with no hard line; night chrome fully coherent (screenshots sampled at 4 points per frame).
  • All 19 Playwright tests pass; check:contrast holds AAA; Prettier/ESLint/stylelint clean.

Please give the Vercel preview a spin on your phone (both light and dark system appearance) before merging — the simulator runs iOS 26.5 and your Safari may differ: ?sky=night / ?sky=day on the preview URL makes the states easy to force.

🤖 Generated with Claude Code

Fourth pass at #60, this time debugged against real iOS Safari in the
simulator instead of by inference. Three findings, three mechanisms:

- Safari's status-bar strip tints from the body's computed
  background-color, and a change driven only by a CSS custom property
  transition never triggers its re-sample — that's the "stuck grey with
  a hard line" footer. render() now writes the ground color as an
  inline style, a real mutation Safari notices; the color is invisible
  on the page (the opaque gradient paints over it).
- Safari applies theme-color once around pageshow and can miss the meta
  swaps made while the page is still loading, leaving the toolbar on
  the static midnight fallback. start() now re-renders on pageshow
  (twice, straddling Safari's variable apply point), which also covers
  back/forward-cache restores. Meta replacement is unconditional so an
  equal-color repaint still re-asserts the value.
- viewport-fit=cover: the sky paints edge-to-edge under the notch and
  home indicator, with safe-area side padding for landscape; controls
  already pad by the insets.

Also adds ?sky=day|night|auto to pin the mode for one load — the
device-testing override used to drive these simulator runs, not
persisted so shared links don't overwrite a visitor's choice.

Verified on the iOS 26.5 simulator (iPhone 17 Pro): the status strip
now tracks auto/day/night switches exactly (rgb(112,140,170) day ↔
rgb(29,42,97) night, sampled), where it previously kept its load-time
sample through every switch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
youthphase Ready Ready Preview Aug 24, 2026 3:06am

Amy could still force the grey footer by clicking the sky button: iOS
Safari applies theme-color once per navigation, so an in-page mode
switch left the toolbar on the tint it latched at load. Simulator A/B
across candidate levers — setAttribute, node replacement,
re-insertion, history.replaceState, history.pushState, hash
assignment — found exactly one that makes Safari re-run the apply: a
same-document navigation. location.replace to a #sky-<color> fragment
performs one without adding a history entry, so Back stays clean.

render() now issues that nudge when the chrome color actually changes:
Apple touch devices only, never on first paint (load applies
natively), never at demo frame rate, and never clobbering a fragment
the site didn't put there. Simulator-verified across repeated
day/night switches: both the status strip and the toolbar now track
every change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@amyrlam

amyrlam commented Aug 18, 2026

Copy link
Copy Markdown
Owner Author

Pushed a follow-up commit: the grey-footer-by-clicking-the-sky-button case is the toolbar's once-per-navigation theme-color apply. Simulator A/B found the one lever that re-runs it — a same-document navigation — so render() now does a history-free location.replace to a #sky-<color> fragment when the chrome color changes (Apple touch devices only). Both bars now track every switch in the simulator. Fresh preview will build from this commit.

Two layers, per the half of #60 each can see:

- tests/sky-chrome.spec.ts runs in real WebKit with iPhone emulation
  (new webkit-iphone Playwright project; CI now installs webkit) and
  pins every signal the page emits into Safari's chrome: both
  media-gated theme-color metas rewritten on a mode switch, the inline
  body background-color Safari samples for its status strip, the
  history-free #sky- navigation nudge, fragment preservation, and the
  ?sky= pin.
- scripts/check-sky-chrome.mjs drives real simulator Safari through
  in-page day/night switches via a new dev-only ?skyseq= harness in
  sky.ts and pixel-samples the actual rendered chrome. It currently
  FAILS on its in-page-switch legs — deliberately kept red: it
  reproduces the exact remaining bug (switch modes after load, chrome
  keeps the old sky) deterministically, where it disproved three more
  candidate mechanisms tonight (deferred, history-pushing, and doubled
  hash navigations).

The Apple-touch gate for the nudge also accepts 'ontouchstart' — real
WebKit under iPhone emulation reports maxTouchPoints 0, and iPads with
desktop-mode UA carry the event handler without the touch-point count.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@amyrlam

amyrlam commented Aug 18, 2026

Copy link
Copy Markdown
Owner Author

Status update, honest version: the in-page switch bug is not fixed yet, but it is now fully reproducible and fenced with tests.

  • tests/sky-chrome.spec.ts (real WebKit, iPhone emulation, runs in CI): pins everything the page emits into Safari's chrome — meta rewrites, the inline body background Safari samples, the hash nudge, fragment safety, ?sky= pinning. All green.
  • scripts/check-sky-chrome.mjs (run locally with a booted simulator + dev server): drives real Safari through in-page day/night switches and pixel-samples the rendered chrome. Deliberately red right now — it reproduces Amy's exact repro deterministically (page switches, chrome keeps the old sky), and tonight it disproved three more candidate fixes (deferred, history-pushing, and doubled hash navigations) before they could ship.

What still holds: load-state chrome is correct (pageshow re-assert), and the status strip tracks the first switch after load. What's left: current simulator Safari appears to honor at most one dynamic chrome update per real navigation, and no re-apply lever found so far gets past that. Next session: iterate new mechanisms directly against the red harness.

PR #67's Lighthouse run failed a11y (0.94) on both blog pages — not
because of that PR: the run happened at 03:16 UTC, San Francisco
sunset, and Lighthouse audits the live sky. At dusk the computed
ink/card pairing bottoms out at ~6.6:1, and the blog date and
description lines carried opacity-70/80, multiplying the effective
contrast down to ~4.15:1 — under the 4.5:1 floor for small text. The
same failure exists on main; daytime CI runs just never see it.

Drop the opacity de-emphasis (size and tracking already carry the
hierarchy) and add the ink-on-card pairing to check:contrast with a
4.5:1 floor across every sun altitude (worst case 6.56:1), so this
class of regression fails the build deterministically instead of only
when CI happens to run at sunset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…trast

Fix sunset contrast on blog card text and gate it in check:contrast
@amyrlam
amyrlam merged commit 0ff0c4f into main Aug 24, 2026
8 checks passed
@amyrlam
amyrlam deleted the claude/github-issue-60-viewport-fit branch August 24, 2026 03:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant