Skip to content

Fix phone layout, contrast, focus and touch targets (T10: VS-05, AX-02, AX-04, AX-07, VS-01, VS-03, VS-09, AX-09) - #214

Merged
Programcode merged 4 commits into
mainfrom
fix/t10-layout-a11y
Oct 23, 2026
Merged

Programcode merged 4 commits into
mainfrom
fix/t10-layout-a11y

Conversation

@Programcode

@Programcode Programcode commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Batch T10 from the 22 Oct 2026 QA report: phone layout, contrast, focus and touch targets. Most of the fixes live in shared primitives (switch, checkbox, input, tabs, button sm), so they apply on every page, not only the ones the report measured. No migrations, no Edge Function changes, nothing to deploy beyond the frontend.

Issues

VS-05 (Medium), /app/debate scrolls sideways on phones. Reproduced on main: with a long motion and attached sources, the page was 640–680 px wider than a 360/375 px screen in EN, ES and HU (0 at 768 px). The cause was the side and source grids. With no explicit column below md, the implicit track grew to the min-content width of the truncated titles. They now use grid-cols-1 (minmax(0, 1fr)). The cards, the PageHeader h1 (so a long unbroken word wraps) and the item row get min-w-0. Badges and the Load button can't be squeezed, and card headers wrap. Now scrollWidth equals the viewport at 360/375/768 px in all three languages, including with a 70-character unbroken motion. The new e2e test fails on main (Load button's right edge at 991 px) and passes here.

AX-02 (Low), first Tab stop on / had no visible ring. On main the first stop was the logo, and it already had a ring from the global :focus-visible rule (d64d94c). There was no skip link at all. A new SkipLink is now the first Tab stop on the landing page, the app shell, the public page shell and the free tools pages. It is hidden until focused, then shows as a 44 px chip with a 2 px ring outline, primary on background, about 5:1 in light and more in dark. It moves focus to <main id="main-content"> without adding a hash to the URL, and makes main focusable only for that jump. New common.skipToContent string in en/es/hu.

AX-04 (Low), landing pills fail contrast. Confirmed on main: axe reports 7 nodes at 1.74:1, the What's New and Pricing pills (text-muted-foreground on bg-secondary/60, where --secondary is dark slate in light mode). Changing --secondary itself would also change every variant="secondary" button, so the pills use the --muted token pair instead: 4.90:1 light, 5.70:1 dark. The use-case subtitles (text-primary/80, 3.35:1, hidden from axe by the scroll reveal) now use text-primary: 4.69 light, 6.73 dark.

VS-01 (Nit), logo ring and 768 px nav. The ring wasn't clipped on main, but it was square and sat on the wordmark's last letter. The link now has rounded-lg pr-1. At 768 px the four section links and the CTA no longer fit beside the wordmark, so the landing nav collapses into the menu below lg (1024 px) instead of md.

VS-03 (Nit), white logo square in dark mode and a dim "Reasoning: Low". C3 (#199) shipped favicon sizes and the share image, but no transparent or dark logo. public/favicon-72.png is now the mark cut out of icon-512.png with a transparent surround (only white connected to the image edge is removed, so the inner white strokes stay). The filename is unchanged on purpose: public/sw.js (T11) precaches it and test:sw asserts the offline logo's src. Because of that, returning visitors keep the old image for up to 30 days (max-age=2592000) or until the service-worker cache version changes. The tiny "DebateLab" lettering inside the old tile is gone; the wordmark next to it already says it. The locked Reasoning select now keeps its text at opacity-100 in text-muted-foreground on bg-muted/50: 5.38:1 light, 6.22:1 dark. It still reads as locked.

VS-09 (Nit), language menu stayed open. The popover is now controlled and closes after a choice in either list. Detection and routing are untouched. New e2e test (fails on main: both radiogroups still open).

AX-07 (Low), touch targets. Most of these are invisible hit areas on pointer: coarse in the primitives, so the desktop look doesn't change.

  • Switch: ::after makes the 44×24 track a 44×44 target.
  • Checkbox: ::after makes it 24 px everywhere and 44 px on touch (was 36 px on touch, 16 px on desktop).
  • Input: min-h-11 on touch (was 40 px). Button already had min-h-11, so input and button rows stay aligned.
  • Tabs: triggers get min-h-11 on touch and the list grows to fit. The login Email/Demo code tabs go from 32 to 44 px without touching LoginPage.tsx.
  • Button size="sm": min-w-11 on touch. The flowchart's icon-only Load/Save/Auto layout/Export buttons were 42 px wide.
  • Small links: landing nav and footer links, the app footer's Terms/Privacy and the /expocit/sources reference links are now at least 24 px tall, and 44 px on touch.
  • Flowchart edges: React Flow sizes the invisible tap stroke in canvas units, so at the phone's fitted zoom (about 0.44) the default 20 became about 9 px on screen. The canvas now writes its zoom to --flow-zoom on every move, and CSS sets the stroke to calc(24px / zoom), or 44 px on touch. Edge labels get 24 px pills. The React Flow attribution stays, as a 24/44 px target.
  • Not fixed: flowchart nodes measure 41 px tall on a phone only because the whole canvas is zoomed out to fit. Any fixed minimum would be scaled down the same way.

AX-09 (contrast and target-size parts).

  • Active nav item (bg-primary/10 + text-primary): 4.45:1 → 4.78:1 with a 5% tint (AppLayout and CurrentDebateMenu). This is the "nav span" Lighthouse flagged on every app page.
  • Timer active speech chip: light text on bg-blue-500 was 3.45:1 in light and 3.92:1 in dark. Each speech colour is two shades darker with white text, so every chip is at least 4.8:1 and the relative variation between speeches stays.
  • Flowchart badges: already fixed by T9 (nodes moved to the -700 shades), so nothing to do here.
  • Heading order: axe and Lighthouse report no heading-order issues on any of the 12 pages. History/Library were not touched (T7).

Also fixed (found while measuring): Lighthouse flagged label-content-name-mismatch on /app/debate, because the "Load" buttons were named "Open {title}". The name is now "Load {title}" (visible word plus a screen-reader-only title, so each row stays distinct), and the now-unused debate.openItemAria strings are removed.

Measurements

Lighthouse accessibility (snapshot of the live page, 375 px, coarse pointer, seeded stub session for app pages). Main → this branch:

Page Light Dark Failing audits on main
/ 96 → 100 96 → 100 color-contrast:7
/expocit/sources 100 → 100 100 → 100 none
/login 100 → 100 100 → 100 none
/app 100 → 100 100 → 100 none
/app/debate 96 → 100 100 → 100 color-contrast:1, label-content-name-mismatch:2
/app/generate 96 → 100 100 → 100 color-contrast:1
/app/attack 96 → 100 100 → 100 color-contrast:1
/app/sparring 96 → 100 100 → 100 color-contrast:1
/app/crossfire 96 → 100 100 → 100 color-contrast:1
/app/flowchart 96 → 100 100 → 100 color-contrast:1
/app/timer 96 → 100 96 → 100 color-contrast:1
/app/settings 100 → 100 100 → 100 none

axe (color-contrast, target-size, heading-order, landmarks, names) and a coarse-pointer target census (< 44 px on touch, < 24 px on desktop), EN. Main → this branch:

Page 375 px light 375 px dark 1280 px light 1280 px dark
/ contrast 7, small 1 → contrast 0, small 0 contrast 7, small 1 → contrast 0, small 0 contrast 7, small 9 → contrast 0, small 0 contrast 7, small 9 → contrast 0, small 0
/expocit/sources contrast 0, small 6 → contrast 0, small 0 contrast 0, small 6 → contrast 0, small 0 contrast 0, small 10 → contrast 0, small 0 contrast 0, small 10 → contrast 0, small 0
/login contrast 0, small 0 → contrast 0, small 0 contrast 0, small 0 → contrast 0, small 0 contrast 1, small 2 → contrast 0, small 0 contrast 0, small 2 → contrast 0, small 0
/app contrast 0, small 0 → contrast 0, small 0 contrast 0, small 0 → contrast 0, small 0 contrast 1, small 2 → contrast 0, small 0 contrast 0, small 2 → contrast 0, small 0
/app/debate overflow 681px, contrast 1, small 0 → contrast 0, small 0 overflow 681px, contrast 0, small 0 → contrast 0, small 0 contrast 1, small 2 → contrast 0, small 0 contrast 0, small 2 → contrast 0, small 0
/app/generate contrast 1, small 2 → contrast 0, small 0 contrast 0, small 0 → contrast 0, small 0 contrast 1, small 2 → contrast 0, small 0 contrast 0, small 2 → contrast 0, small 0
/app/attack contrast 2, small 0 → contrast 0, small 0 contrast 0, small 0 → contrast 0, small 0 contrast 1, small 2 → contrast 0, small 0 contrast 0, small 2 → contrast 0, small 0
/app/sparring contrast 1, small 1 → contrast 0, small 0 contrast 0, small 1 → contrast 0, small 0 contrast 1, small 2 → contrast 0, small 0 contrast 0, small 2 → contrast 0, small 0
/app/crossfire contrast 2, small 0 → contrast 0, small 0 contrast 0, small 2 → contrast 0, small 0 contrast 1, small 3 → contrast 0, small 0 contrast 0, small 3 → contrast 0, small 0
/app/flowchart contrast 1, small 5 +8 rf → contrast 0, small 0 +8 rf contrast 0, small 5 +8 rf → contrast 0, small 0 +8 rf contrast 1, small 3 +3 rf → contrast 0, small 0 +3 rf contrast 0, small 3 +3 rf → contrast 0, small 0 +3 rf
/app/timer contrast 1, small 0 → contrast 0, small 0 contrast 1, small 0 → contrast 0, small 0 contrast 2, small 2 → contrast 0, small 0 contrast 1, small 2 → contrast 0, small 0
/app/settings contrast 0, small 0 → contrast 0, small 0 contrast 0, small 0 → contrast 0, small 0 contrast 0, small 2 → contrast 0, small 0 contrast 0, small 2 → contrast 0, small 0

In this census the /login rows ran with the signed-in stub, so they redirected to /app. The logged-out login page is covered by the Lighthouse rows above. Its Email/Demo code tabs measure 32 → 44 px and the email field 40 → 44 px on touch, with desktop unchanged at 32/40 px.

"rf" counts are React Flow edges and nodes, whose SVG boxes can't show the stroke-based hit area. That area was measured directly: 44 px on a phone and 24 px on desktop, after zoom.

Screenshots: 192 full-page shots (12 pages × 360/375/768/1280 px × light/dark × EN/HU), all with scrollWidth equal to the viewport. Selected before/after pairs are on the pr-assets/t10-layout-a11y branch (not for merge):

Verification

GitHub Actions minutes are used up, so CI will not run on this PR. Everything below is a local pass, not a CI pass.

Full npm run verify at the PR head a93855b4 (rebased on 45afb251, #213): exit 0.

  • docs:check, lint (eslint + knip + repo rules), typecheck: clean
  • npm test: 80 files, 581/581 passed
  • test:collaboration: 5 files, 30/30 passed
  • test:edge: 457 passed, 0 failed
  • build: prerender and sitemap ok; check-site: 48 HTML files, sitemap and llms.txt resolve, no problems
  • test:sw: {"upgradeActivated":true,"firstVisitedRouteOffline":true,"unvisitedRouteShowsRecovery":true,"intentPrefetchedRouteOffline":true,"offlineLogo":true,"apiCacheEntries":0}
  • e2e smoke (desktop-chromium, @smoke): 47/47 passed

One earlier verify at this same head failed once in test:sw: the offline reload of /app didn't show its heading within 15 s. It then passed 3/3 on its own and again in this full run. The VPS was carrying two extra dev servers of mine at the time.

Beyond verify:

  • New tests (e2e/quality-backlog.spec.ts): the 360 px scrollWidth check in EN/ES/HU and the language menu closing on select. Run on desktop-chromium and mobile-chromium with --repeat-each=3 --retries=0: 24/24 passed. Both fail on main's code: the Load button's right edge lands at 991 px, and both language radiogroups stay open.
  • quality-backlog, home-navigation and public-pages on both projects: 64/64 passed.
  • Full npm run test:e2e (desktop + mobile, 284 tests): every failure was either the language test before its wait fix (fixed above) or one that also fails on untouched origin/main when I ran the same specs there. Those are navigation-performance.spec.ts:344 "subscription reads stay cached across sibling tool routes" (an extra subscriptions read, both projects), search-quality.spec.ts:7 "authenticated Factoids shows the consulted-source quality panel" (both projects), and home-navigation.spec.ts:90 onboarding delay (flaky on both). These are not T10's; they look like fallout from the billing (T4) and Factoids changes.
  • Not run: WebKit, the agentic suite (agentic/*.e2e.ts, which this branch's nav breakpoint and new first Tab stop could touch), the verify-debatelab stack (no backend change, and the Supabase lock is shared), staging, and a real phone.
  • verify-debatelab recipes: none needed changes. case-generation picks Magyar and then presses Escape. With the menu now closing on select, that Escape lands on the trigger and does nothing.

Review

One fresh-context round, as a T3 child on claudeAgent_cpa with claude-opus-5-5, reading git diff origin/main...HEAD. The fixes below came after that round. The batch rule asks for a new round only after Medium or higher findings, so I didn't run a second one.

Round 1: no Medium or higher findings. Lows and Nits, and what I did with each:

  • Low: tabIndex={-1} on <main> made a click on plain text focus main. Fixed: the skip link adds tabindex only for its jump and removes it on blur. Checked live: Tab → Enter focuses main, the next Tab goes into the content, and a click leaves focus on body.
  • Low: on touch, the checkbox's 44 px area reached about 7 px into the row above in the debate context list. Fixed: those rows trim the upward reach to 8 px; the row's label already makes the whole row a 44 px target.
  • Low: onMove was a new function each render, so React Flow re-registered its zoom listener. Fixed with useCallback.
  • Low: picking an AI output language also closes the menu. Kept on purpose: VS-09 asks to close on select, and one rule for both lists is simpler.
  • Low: /tools/* had no skip link. Fixed in ToolPageLayout.
  • Nit: hovering the active Timer chip shows hover:bg-primary/90. Left: it predates this branch and contrast stays fine.
  • Nits: the VS-05 test built a RegExp from the motion text, and the language test keyed the menu off the project name. Fixed: the name is now a plain string, and the test opens the menu whenever it is shown and waits for the header first. A full-suite run caught the first version of that change racing the navbar on mobile.

Left open / needs a real device

  • Real iOS/Android touch: the ::after hit areas and the flowchart edge stroke were measured in emulated Chromium (coarse pointer, touch). Tapping a switch's label-side edge and a flowchart edge on a real phone is worth one manual check.
  • WebKit was not run (release-only smoke).
  • The logo cache caveat above: returning visitors see the new transparent logo only after their 30-day cache or the next service-worker cache bump (T11).
  • Migrations: none. Edge Functions to deploy: none.

🤖 Generated with Claude Code

Programcode and others added 4 commits October 23, 2026 04:56
VS-05: the Current Debate grids use minmax(0, 1fr) below md and the motion
heading can shrink, so long titles truncate instead of widening the page.
AX-02: a skip link is the first Tab stop on the landing, app and public shells.
AX-04/AX-09: pills, the active nav tint, Timer speech chips and the use-case
subtitles reach 4.5:1 in light and dark.
AX-07: switch, checkbox, input, tabs and small buttons reach 44px on coarse
pointers and 24px elsewhere; flowchart edges get a zoom-aware hit stroke and
the React Flow credit a real target.
VS-01/VS-03/VS-09: the landing nav collapses below lg, the logo has a
transparent surround, the locked Reasoning field stays readable, and the
language menu closes on a choice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lighthouse flagged label-content-name-mismatch: the buttons read "Load" but were
named "Open {title}". The title now follows the visible word in a screen-reader-only
span, and the unused openItemAria strings are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The skip link makes main focusable just for its jump, so a click inside main
  still leaves focus on body.
- Debate context rows keep the checkbox's touch area inside their own row.
- The free tools pages get the skip link too.
- Flowchart passes React Flow a stable onMove.
- The new e2e tests match names as strings and open the menu whenever it shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
isVisible() does not wait, so on a phone it could run before the navbar
rendered and skip opening the menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 23, 2026 05:06

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 45afb25 and a93855b.

⛔ Files ignored due to path filters: 1
  • public/favicon-72.png is excluded by !**/*.png
📒 Files selected for processing: 29
  • e2e/quality-backlog.spec.ts
  • src/components/LanguageSwitcher.tsx
  • src/components/ModelSelector.tsx
  • src/components/features/debate/CurrentDebateMenu.tsx
  • src/components/features/debate/DebateContextControl.tsx
  • src/components/free-tools/ToolPageLayout.tsx
  • src/components/landing/Footer.tsx
  • src/components/landing/Navbar.tsx
  • src/components/landing/Pricing.tsx
  • src/components/landing/UseCases.tsx
  • src/components/landing/WhatsNew.tsx
  • src/components/layout/AppLayout.tsx
  • src/components/layout/PageHeader.tsx
  • src/components/layout/PublicPageShell.tsx
  • src/components/layout/SkipLink.tsx
  • src/components/ui/button.tsx
  • src/components/ui/checkbox.tsx
  • src/components/ui/input.tsx
  • src/components/ui/switch.tsx
  • src/components/ui/tabs.tsx
  • src/index.css
  • src/locales/en.json
  • src/locales/es.json
  • src/locales/hu.json
  • src/pages/CurrentDebatePage.tsx
  • src/pages/FlowchartPage.tsx
  • src/pages/LandingPage.tsx
  • src/pages/SourcesPage.tsx
  • src/pages/TimerPage.tsx

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

This change adds skip links to page layouts, adjusts touch targets and responsive content behavior, updates language-menu selection, and changes several interface styles. End-to-end tests cover long titles at 360px and language selection.

Changes: Accessibility and interface quality

Layer / File(s) Summary
Skip-link support across page shells
src/components/layout/SkipLink.tsx, src/components/layout/AppLayout.tsx, src/components/layout/PublicPageShell.tsx, src/components/free-tools/ToolPageLayout.tsx, src/pages/LandingPage.tsx, src/locales/*.json
Page layouts render a translated skip link that targets the main landmark. Locale files add the skip-link label and remove debate.openItemAria.
Touch targets and flowchart interactions
src/components/ui/*, src/components/features/debate/DebateContextControl.tsx, src/components/landing/Footer.tsx, src/components/layout/AppLayout.tsx, src/pages/SourcesPage.tsx, src/pages/FlowchartPage.tsx, src/index.css
Controls and links gain touch-target sizing for coarse pointers. Flowchart edge interaction widths track zoom, and edge labels receive padding.
Responsive navigation and long-content layout
src/components/landing/Navbar.tsx, src/components/layout/PageHeader.tsx, src/pages/CurrentDebatePage.tsx, e2e/quality-backlog.spec.ts
The landing navigation switches to the lg breakpoint. Debate-page cards and headers gain shrinking and wrapping styles. An end-to-end test checks long titles at 360px.
Language-menu selection
src/components/LanguageSwitcher.tsx, e2e/quality-backlog.spec.ts
Selecting either language setting closes the popover. An end-to-end test checks that selecting Magyar closes the menu and sets the document language to hu.
Interface color and state styling
src/components/ModelSelector.tsx, src/components/features/debate/CurrentDebateMenu.tsx, src/components/layout/AppLayout.tsx, src/components/landing/{Pricing,UseCases,WhatsNew}.tsx, src/pages/TimerPage.tsx
Selected navigation and debate states, badges, timer speech colors, and the disabled reasoning-effort trigger receive styling changes.

Estimated code review effort: 3 (Moderate) | ~25 minutes

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@Programcode
Programcode merged commit 87603cd into main Oct 23, 2026
3 of 9 checks passed
Programcode added a commit that referenced this pull request Oct 23, 2026
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.

2 participants