From 35edc123be45bc40bcfe212aae3420a13eb1a2e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Jul 2026 00:10:50 +0000 Subject: [PATCH] Improve landing-page accessibility and fix sticky nav Focused UX/accessibility pass on the marketing site (src/): - Fix broken sticky nav: .site-shell used `overflow-x: hidden`, which forced `overflow-y` to compute to `auto`, making the shell a scroll container and causing the `position: sticky` nav to scroll away. Switched to `overflow-x: clip`, which contains horizontal overflow without creating a scroll container. Verified sticky on desktop + mobile with no horizontal-scroll regression. - Add keyboard focus visibility: no `:focus-visible` styles existed anywhere (one input even had `outline: none`), so keyboard/AT users had no focus indicator. Added a theme-aware focus ring for links, buttons, and inputs. - Add a "Skip to content" link that stays off-screen until focused, so keyboard users can bypass the nav on the first Tab. - Offset anchor-scroll targets with `scroll-padding-top` so section headings no longer land underneath the sticky nav, plus reduced-motion- aware `scroll-behavior: smooth` for in-page navigation. - Announce waitlist success/error via an `aria-live` status region and mark the submit button `aria-busy` while loading. - Add brand-tinted `::selection` styling in both themes. New styles live in src/a11y-polish.css (imported after mobile-polish). Build, lint, and the 25 web unit tests all pass; changes verified in a headless browser. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01PE4HFdLKjK4er6BBAgbgrb --- src/App.css | 5 +- src/App.jsx | 23 +++++---- src/a11y-polish.css | 105 ++++++++++++++++++++++++++++++++++++++++++ src/main.jsx | 1 + src/mobile-polish.css | 2 +- 5 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 src/a11y-polish.css diff --git a/src/App.css b/src/App.css index b64d3ce..2653567 100644 --- a/src/App.css +++ b/src/App.css @@ -60,7 +60,10 @@ h1, h2, h3, h4, p { .site-shell { width: 100%; - overflow-x: hidden; + /* clip (not hidden) contains horizontal overflow without turning the + shell into a scroll container — which would force overflow-y to `auto` + and break the sticky nav's position. */ + overflow-x: clip; min-height: 100vh; background: radial-gradient(ellipse at 50% -10%, rgba(138,180,248,0.20) 0%, transparent 52%), diff --git a/src/App.jsx b/src/App.jsx index 53d2c4b..22a2d48 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -339,7 +339,7 @@ function Waitlist() { - @@ -361,15 +361,17 @@ function Waitlist() {

Requires iPhone · iOS 17 or later

- {status === 'success' && ( -

You're in. We'll email your TestFlight invite when a spot opens.

- )} - {status === 'error' && ( -

- Something went wrong. Try{' '} - m.rafiq2006@icloud.com. -

- )} +
+ {status === 'success' && ( +

You're in. We'll email your TestFlight invite when a spot opens.

+ )} + {status === 'error' && ( +

+ Something went wrong. Try{' '} + m.rafiq2006@icloud.com. +

+ )} +
); @@ -385,6 +387,7 @@ function SiteShell() {
+ Skip to content
diff --git a/src/a11y-polish.css b/src/a11y-polish.css new file mode 100644 index 0000000..3c44688 --- /dev/null +++ b/src/a11y-polish.css @@ -0,0 +1,105 @@ +/* ===================================================== + LUMEN - Accessibility & interaction polish + Keyboard focus visibility, skip link, anchor-scroll + offset under the sticky nav, and reduced-motion-aware + smooth scrolling. Layered on top of the existing chrome + so the visual design is untouched for mouse users. + ===================================================== */ + +:root { + --focus-ring: rgba(138, 180, 248, 0.95); +} + +html[data-theme='light'] { + --focus-ring: rgba(79, 111, 208, 0.95); +} + +/* ===== KEYBOARD FOCUS VISIBILITY ===== + Only shows for keyboard/AT users (:focus-visible), so mouse + clicks stay ring-free. Outline follows each element's own + border-radius in modern browsers, so rounded controls stay round. */ + +a:focus-visible, +button:focus-visible, +input:focus-visible, +select:focus-visible, +textarea:focus-visible, +[tabindex]:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: 3px; +} + +/* The waitlist input clears its outline explicitly (class specificity), + so restore a visible focus ring for keyboard users at matching weight. */ +.waitlist-form input:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: 2px; +} + +/* ===== SKIP TO CONTENT ===== + Off-screen until focused, then slides into the top-left so a + keyboard user can jump past the nav on the first Tab press. */ + +.skip-link { + position: fixed; + top: 12px; + left: 12px; + z-index: 100; + padding: 10px 16px; + border-radius: 10px; + background: var(--cta-bg, #8ab4f8); + color: var(--cta-text, #0a0c12); + font-size: 13px; + font-weight: 600; + letter-spacing: 0.01em; + box-shadow: 0 12px 30px -12px rgba(138, 180, 248, 0.7); + transform: translateY(-160%); + transition: transform 0.18s ease; +} + +.skip-link:focus { + transform: translateY(0); + outline: 2px solid var(--focus-ring); + outline-offset: 3px; +} + +@media (prefers-reduced-motion: reduce) { + .skip-link { + transition: none; + } +} + +/* ===== ANCHOR-SCROLL OFFSET ===== + The nav is sticky, so bare #anchor jumps land section headings + underneath it. Reserve the nav's height at the top of every + scroll so targets settle just below it. */ + +html { + scroll-padding-top: 88px; +} + +@media (max-width: 600px) { + html { + scroll-padding-top: 72px; + } +} + +/* ===== SMOOTH IN-PAGE SCROLLING ===== */ + +@media (prefers-reduced-motion: no-preference) { + html { + scroll-behavior: smooth; + } +} + +/* ===== BRAND-TINTED TEXT SELECTION ===== */ + +::selection { + background: rgba(138, 180, 248, 0.28); + color: var(--text-heading, #f7f9fc); +} + +html[data-theme='light'] ::selection { + background: rgba(79, 111, 208, 0.22); + color: var(--text-heading, #0f1322); +} diff --git a/src/main.jsx b/src/main.jsx index 6cc76cf..a7fcada 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -4,6 +4,7 @@ import { createRoot } from 'react-dom/client'; import { App } from './App.jsx'; import './lumen-overrides.css'; import './mobile-polish.css'; +import './a11y-polish.css'; const root = document.getElementById('root'); diff --git a/src/mobile-polish.css b/src/mobile-polish.css index ec1d6e7..cf3ddc6 100644 --- a/src/mobile-polish.css +++ b/src/mobile-polish.css @@ -255,7 +255,7 @@ @media (max-width: 600px) { .site-shell { - overflow-x: hidden; + overflow-x: clip; } .nav {