feat(site): mobile and accessibility pass - #67
Conversation
`viewport-fit=cover` is what makes `env(safe-area-inset-*)` resolve to real values on iOS; without it they are 0px and every safe-area rule below silently does nothing. `themeColor` and `colorScheme` go on the `Viewport` export, not `metadata` — both are `@deprecated` there, pointing at exactly this export. Docs opts back out to `viewportFit: "auto"`. It has no edge-to-edge art direction to gain from `cover`, and fumadocs' navbar is `position: fixed` with no safe-area handling of its own, so it would sit under the cutout in landscape with nothing able to pad it. The field is set explicitly rather than omitted: a nested viewport export merges into the parent field by field, so leaving it out inherits `cover` instead of clearing it — visible only in the emitted meta tag. `safe-x` is registered with `@utility`, not written as a bare class. A bare class is unlayered, and unlayered beats `@layer utilities` regardless of specificity, so it would have silently overridden any `px-*` on the same element — a trap for the next edit rather than a composable utility. An earlier revision of this put the horizontal inset on `body`. That is wrong: a section's full-bleed decoration is `absolute inset-0`, which resolves against the padding box and still reaches both screen edges, whereas padding `body` shrinks the box the section is laid out in and shows the page background as bars beside the hero image — on exactly the devices the inset exists for. `::-webkit-scrollbar` gains a `height`. `width` styles the vertical bar only, so the newly horizontally-scrollable code panels would otherwise get the ~15px platform default on Windows and Linux. macOS overlay scrollbars hide it, so it does not reproduce locally. Tailwind v4's preflight already emits `-webkit-text-size-adjust: 100%` and `-webkit-tap-highlight-color: transparent` on `html`, so neither is restated here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Cam <cam@camwhiteus.com>
The site had zero `focus-visible` occurrences: every link and button was an invisible focus stop for a keyboard or switch-control user. Every interactive element in `site/app/**` now carries `.focus-ring` — emerald on the dark background at ~9:1, with a 2px offset so the ring stays readable even on the `bg-ut` primary buttons, where a flush ring would be emerald on emerald. Touch targets below 44px are brought up in the same edit, since they land on the same class strings: the nav hamburger, the nav links, the before/after toggles. The nav active-dot moves from `-bottom-1.5` to `bottom-1.5` as a consequence — the link box is now 44px tall around a 20px text line, so an offset measured outside the box lands 12px below the label instead of under it. Nav menu: the body is locked while the dropdown is open, restoring the previous inline value rather than clearing it, so a lock owned by something else outlives this one. Escape closes and returns focus to the hamburger, otherwise a keyboard user is left focused on a removed node and tabbing restarts from the top of the document. Crossing up to the `md` breakpoint closes it too — the dropdown is `md:hidden`, so it would otherwise hide while leaving the scroll lock held on a viewport with no visible way to release it. `aria-controls` is emitted only while the menu exists; as a constant it would be an IDREF resolving to nothing, which validators flag and assistive tech ignores. Hero is `100dvh`, not `100vh`: `vh` measures the viewport without the retracting iOS toolbar, so the hero overflowed by the toolbar's height and the page jerked as it hid. Top padding moves to `dvh` in step — mixing the two sizes the box against one viewport and its padding against another. Code panels scroll horizontally instead of clipping. The rounded shells keep `overflow-hidden`, so the scroll container is the `<pre>` itself; at 375px the longest lines were previously unreachable rather than merely off-screen. Before/after toggle gains `aria-pressed` and honours `prefers-reduced-motion` via `useReducedMotion`. Note the hook snapshots once at mount and returns null on the server in motion@12 — harmless here, and recorded at the call site so nobody assumes it already listens for changes. Sections take the horizontal safe-area inset via `safe-x` in place of `px-6`; it resolves to the same 1.5rem wherever there is no cutout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Cam <cam@camwhiteus.com>
The Codex gate asked for a `vh` fallback beside `100dvh`. It is unreachable: the emitted stylesheet already uses `@property` and `color-mix()`, Tailwind v4's own Safari 16.4+ floor, while `dvh` is Safari 15.4+. No engine can parse this CSS yet fail on `dvh`. Tried it anyway to be sure — paired `min-height: 100vh; min-height: 100dvh` declarations. Lightning CSS deleted the fallback from the output, classifying it as dead for the shipped targets. Recorded at the call site so the next reader does not re-add it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Cam <cam@camwhiteus.com>
Codex Gate — review, triage and deliberation
The findings
Why both are unreachableBoth rest on browsers older than this codebase can render at all. The effective floor, measured rather than assumed:
The last row is decisive: The toolchain proved P2-B independentlyI applied the suggested fix before dismissing it — paired declarations, Deliberation —
|
| # | Finding | Severity | Classification | Action |
|---|---|---|---|---|
| A | Legacy matchMedia listener path |
P2 | Dismissed — unreachable below the PR's browser floor | Codex conceded |
| B | vh fallback for dvh |
P2 | Dismissed — unreachable; optimizer strips it | Rationale recorded in f54c70e |
No code change was required by either. The one change this gate produced is the comment explaining why, which is the part that would otherwise be re-litigated.
Ship Pipeline CompleteBranch: Audit trail
Verified in built output
Not doneNo real-device pass. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f54c70e4dc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| }`} | ||
| > | ||
| <div className="flex items-center justify-between px-6 py-4"> | ||
| <div className="flex items-center justify-between safe-x py-4"> |
There was a problem hiding this comment.
Keep the fixed nav below the top safe area
On the marketing route on notched iPhones, the new viewportFit: "cover" makes the layout extend behind the status bar/Dynamic Island, but this fixed top-0 navigation row applies only horizontal safe-area padding. Its logo and menu controls can therefore overlap the unsafe top inset and become obscured or difficult to tap; add env(safe-area-inset-top) padding or an equivalent offset, and account for that added height in the dropdown's max-h calculation.
Useful? React with 👍 / 👎.
Summary
Mobile and accessibility pass against the redesigned site — the work issue #49 was opened to redo after PR #36 went stale against the redesign.
Baseline at
0dae0e7, verified rather than assumed: zerofocus-visibleoccurrences site-wide, zerodvh, zeroviewport-fit, andoverflow-x-hiddenon all three code panels. Every keyboard stop on the site was invisible.Closes #49
What changed
Viewport —
Viewportexport withviewport-fit=cover,themeColor,colorScheme. Docs opts back out toauto: it has no edge-to-edge art direction, and fumadocs' navbar isposition: fixedwith no safe-area handling, so it would sit under the cutout with nothing able to pad it.Focus — every
<a>and<button>insite/app/**carries.focus-ring. Emerald on#0a0a1aat ~9:1, with a 2px offset so the ring stays readable on thebg-utprimary buttons where a flush ring would be emerald-on-emerald.Touch targets — hamburger, nav links and the before/after toggles brought to 44px. The nav active-dot moved from
-bottom-1.5tobottom-1.5as a consequence: the link box is now 44px around a 20px text line, so an outside offset landed 12px below the label.Nav menu — body scroll-lock (restoring the previous inline value, not clearing it),
overscroll-behavior: contain, Escape-to-close with focus returned to the hamburger, and close-on-resize pastmdso the lock cannot strand on a viewport with no way to release it.aria-controlsis emitted only while the menu exists.Hero —
100dvhinstead of100vh, with the top padding moved todvhin step.Code panels — horizontal scroll instead of clipping. At 375px the longest lines were unreachable, not merely off-screen.
Before/after —
aria-pressedandprefers-reduced-motionviauseReducedMotion.Tailwind v4's preflight already emits
-webkit-text-size-adjustand-webkit-tap-highlight-color, so point 3 of the issue was partly satisfied by the framework and is not restated here.Review audit trail
Plan review (GPT-5.4) — 9 accepted, 3 rejected. Accepted:
window.matchMediain render (replaced withuseReducedMotion, which the component already imported),border-radiusin the focus rule (it would change the element's real shape rather than round the outline), unused safe-area utilities, brittle greps inflated by their own comments,min-w-0prescribed universally, and a resize path that stranded the scroll lock. Rejected its headline finding thatthemeColormay not belong onViewport— verified against the installed Next types, whereMetadata.themeColorcarries@deprecated Use the new viewport configuration instead; following it would have adopted the deprecated field.Implementation — five tasks, each gated by a fresh adversarial reviewer that re-ran the failure scenario rather than trusting the report. Two findings worth recording: the implementer correctly skipped two CSS declarations the plan prescribed, having proved Tailwind's preflight already emits them; and
min-w-0landed only on the two<pre>elements with real flex parents, correctly omitted from the block-level one.Security review — no findings. Verified no dependency added, the one
dangerouslySetInnerHTMLis pre-existing and untouched, and all five scroll-lock exit paths reach a matching effect cleanup.Code review — 9 findings, all fixed. The significant one: putting the horizontal inset on
bodyde-full-bleeds the hero photo, because a section'sabsolute inset-0decoration resolves against the padding box and stays edge-to-edge, whereas paddingbodyshrinks the box the section is laid out in — black bars beside the hero image in landscape, on exactly the devices the inset was added for. Moved to the sections. Also fixed: horizontal scrollbars had noheight(widthstyles the vertical bar only, so Windows/Linux would get the ~15px default inside a 6px design), a danglingaria-controlsIDREF,pt-[18vh]besidemin-h-[100dvh], and three comments that stated things which were not true.safe-xandsafe-bottomare registered with@utilityrather than written as bare classes — a bare class is unlayered, and unlayered beats@layer utilitiesregardless of specificity, so either would have silently overridden anypx-*on the same element.Test plan
npm run lint→ 0 ·npm run typecheck→ 0npx next buildfrom clean → 28/28 static pages, zero warningsnpm test→ 2422 passed. The two failures arecli/secret.test.ts(issue Flaky test: cli/secret.test.ts shared-state/timing bug (test isolation) #52, the documented load-sensitive scrypt flake); it passes 13/13 in isolation and this diff touches nothing underpackages//→viewport-fit=cover,/docs→viewport-fit=auto,theme-colorpreserved on both.safe-x/.safe-bottomland in@layer utilities,::-webkit-scrollbarcarrieswidth:6px;height:6px, nobodypadding<a>/<button>opening tag insite/app/**/*.tsx— zero uncoveredNot done: a real-device pass.
site/is linted by CI but never typechecked or built by it, so thenext buildabove is the compensating gate — but body-leveloverflow: hiddenis the least reliable scroll-lock on iOS Safari specifically, and that is the one claim here I could not verify from this environment.