Skip to content

feat(site): mobile and accessibility pass - #67

Open
c-1k wants to merge 3 commits into
masterfrom
ship/mobile-a11y
Open

feat(site): mobile and accessibility pass#67
c-1k wants to merge 3 commits into
masterfrom
ship/mobile-a11y

Conversation

@c-1k

@c-1k c-1k commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

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: zero focus-visible occurrences site-wide, zero dvh, zero viewport-fit, and overflow-x-hidden on all three code panels. Every keyboard stop on the site was invisible.

Closes #49

What changed

ViewportViewport export with viewport-fit=cover, themeColor, colorScheme. Docs opts back out to auto: it has no edge-to-edge art direction, and fumadocs' navbar is position: fixed with no safe-area handling, so it would sit under the cutout with nothing able to pad it.

Focus — every <a> and <button> in site/app/** carries .focus-ring. Emerald on #0a0a1a at ~9:1, with a 2px offset so the ring stays readable on the bg-ut primary 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.5 to bottom-1.5 as 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 past md so the lock cannot strand on a viewport with no way to release it. aria-controls is emitted only while the menu exists.

Hero100dvh instead of 100vh, with the top padding moved to dvh in step.

Code panels — horizontal scroll instead of clipping. At 375px the longest lines were unreachable, not merely off-screen.

Before/afteraria-pressed and prefers-reduced-motion via useReducedMotion.

Tailwind v4's preflight already emits -webkit-text-size-adjust and -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.matchMedia in render (replaced with useReducedMotion, which the component already imported), border-radius in 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-0 prescribed universally, and a resize path that stranded the scroll lock. Rejected its headline finding that themeColor may not belong on Viewport — verified against the installed Next types, where Metadata.themeColor carries @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-0 landed 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 dangerouslySetInnerHTML is 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 body de-full-bleeds the hero photo, because a section's absolute inset-0 decoration resolves against the padding box and stays edge-to-edge, whereas padding body shrinks 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 no height (width styles the vertical bar only, so Windows/Linux would get the ~15px default inside a 6px design), a dangling aria-controls IDREF, pt-[18vh] beside min-h-[100dvh], and three comments that stated things which were not true.

safe-x and safe-bottom are registered with @utility rather than written as bare classes — a bare class is unlayered, and unlayered beats @layer utilities regardless of specificity, so either would have silently overridden any px-* on the same element.

Test plan

  • npm run lint → 0 · npm run typecheck → 0
  • npx next build from clean → 28/28 static pages, zero warnings
  • npm test → 2422 passed. The two failures are cli/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 under packages/
  • Emitted meta verified per route: /viewport-fit=cover, /docsviewport-fit=auto, theme-color preserved on both
  • Compiled CSS verified: .safe-x/.safe-bottom land in @layer utilities, ::-webkit-scrollbar carries width:6px;height:6px, no body padding
  • Focus-ring coverage scripted over every <a>/<button> opening tag in site/app/**/*.tsx — zero uncovered

Not done: a real-device pass. site/ is linted by CI but never typechecked or built by it, so the next build above is the compensating gate — but body-level overflow: hidden is the least reliable scroll-lock on iOS Safari specifically, and that is the one claim here I could not verify from this environment.

c-1k and others added 3 commits July 28, 2026 06:52
`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>
@c-1k

c-1k commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Codex Gate — review, triage and deliberation

codex review --base master raised two P2 findings. Both are dismissed, and Codex withdrew both on deliberation.

The findings

[P2-A] Preserve a legacy listener path for matchMedianav.tsx:76-81
On Safari/iOS versions that still expose MediaQueryList.addListener/removeListener instead of addEventListener, this effect throws as soon as the nav mounts.

[P2-B] Keep a vh fallback when switching the hero to dvhhero.tsx:59-62
Browsers that do not support dynamic viewport units ignore min-height: 100dvh entirely, which collapses the hero back to its content height.

Why both are unreachable

Both rest on browsers older than this codebase can render at all. The effective floor, measured rather than assumed:

Feature Ships in Where
MediaQueryList.addEventListener Safari 14.0 the API P2-A wants a fallback for
dvh units Safari 15.4 the unit P2-B wants a fallback for
:focus-visible Safari 15.4 introduced by this PR
overscroll-behavior Safari 16.0 introduced by this PR
@property, color-mix() Safari 16.4 already in the emitted stylesheet — Tailwind v4's own baseline

The last row is decisive: @property and color-mix() are present in the built CSS today. Any engine that can parse this stylesheet is Safari 16.4+, which has had MQL.addEventListener for two major versions and dvh for one. Neither fallback is reachable.

The toolchain proved P2-B independently

I applied the suggested fix before dismissing it — paired declarations, min-height: 100vh; min-height: 100dvh. Lightning CSS deleted the fallback from the output, emitting min-height:100dvh;padding-top:18dvh. Tailwind v4's own optimizer classified it as dead code for the shipped targets. Reverted, with the reasoning recorded at the call site in f54c70e so it is not re-added.

Deliberation — codex exec

Codex searched MDN, caniuse, the WebKit 15.4 release notes and the Tailwind compatibility docs, then conceded both:

I'd withdraw both findings. […] On that floor, an addListener fallback is dead code. […] there is no practical browser in the supported matrix that can parse the emitted Tailwind v4 stylesheet yet fail on dvh. The Lightning CSS behavior also cuts against my finding: it preserves manual fallbacks only when targets need them, and otherwise removes them. If your emitted CSS drops 100vh, that is direct evidence the toolchain considers the fallback unreachable.

Deliberations used: 2 of 2.

Summary

# 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.

@c-1k
c-1k marked this pull request as ready for review July 28, 2026 10:58
@c-1k

c-1k commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Ship Pipeline Complete

Branch: ship/mobile-a11y · Stages: 0-15 ✓

Audit trail

  • Plan review (GPT-5.4) — 12 findings: 9 accepted, 3 rejected. Rejected its headline claim that themeColor may not belong on Viewport; verified against the installed Next types, where Metadata.themeColor carries @deprecated Use the new viewport configuration instead.
  • Implementation — 5 tasks, each gated by a fresh adversarial reviewer (11 agents). One reviewer caught a regression the pass itself introduced; one implementer correctly refused two CSS declarations the plan prescribed, having proved Tailwind's preflight already emits them.
  • Security review — no findings. Verified no dependency added, the single dangerouslySetInnerHTML is pre-existing and untouched, and all five scroll-lock exit paths reach a matching effect cleanup.
  • Code review — 9 findings, 9 fixed.
  • Codex gate — 2 findings, 2 dismissed, both conceded on deliberation. 2 of 2 deliberations used.
  • CI — lint ✓ typecheck ✓ test ✓ tb-integration ✓ dco ✓

Verified in built output

Check Result
/ viewport viewport-fit=cover
/docs viewport viewport-fit=auto
.focus-ring:focus-visible outline:2px solid var(--color-ut);outline-offset:2px
::-webkit-scrollbar width:6px;height:6px
body horizontal padding absent (0) — the regression that de-full-bled the hero
Hero background absolute inset-0 intact inside the safe-x section
focus-ring on rendered page 38 elements
aria-pressed 2 — one true, one false
aria-controls while closed 0 — no dangling IDREF

Not done

No real-device pass. site/ is linted by CI but never typechecked or built by it, so the clean next build is the compensating gate. The one claim I could not verify from this environment: body-level overflow: hidden is the least reliable scroll-lock on iOS Safari specifically, which is the platform its comment names. Worth one physical check before trusting it.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

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.

Mobile/a11y pass against the redesigned site (salvage from #36)

1 participant