Skip to content

fix(mobile): make the site usable on a phone (audit pass 1) - #247

Open
Jose-Gael-Cruz-Lopez wants to merge 3 commits into
mainfrom
fix/mobile-touch-targets
Open

fix(mobile): make the site usable on a phone (audit pass 1)#247
Jose-Gael-Cruz-Lopez wants to merge 3 commits into
mainfrom
fix/mobile-touch-targets

Conversation

@Jose-Gael-Cruz-Lopez

Copy link
Copy Markdown
Collaborator

Mobile pass driven by an audited list rather than a blanket sweep. 72 candidate findings → 26 confirmed, 46 refuted after every one was handed to a second reviewer told to refute it.

The premise changed, and so did my conclusion

I first measured page scrollWidth at 390px on all six routes and got 0px horizontal overflow everywhere. That number is correct but the conclusion I drew from it was wrong: .shell and the modal panel both set overflow: hidden, so oversized content is cut off rather than pushed into a scrollbar. Nothing overflowed the page because everything that overflowed was already being clipped.

So "apply auto-layout everywhere" would have churned a lot of working code — the layout genuinely reflows. The real failures are clipped content, tap ergonomics, and iOS zoom.

What's fixed

Clipping (severity: breaks) — all three verified against the 84 real listings, not argued from principle:

Where Problem
sections.tsx stats strip 2-up gives each cell 116px at 320px while the display clamp floors at 3rem. prizeDisplay renders up to 7 chars ($12.3M + +) — cannot fit at 48px under any font. Bled past both edges, collided with the neighbouring number, got clipped. Now one per row below sm.
detail-modal title Locked at 28.8px on every phone width (4vw is only 12.8–15.6px there, so the clamp floor never engages). Syncopate Bold costs ~25.8px/capital, and (Pre-Registration needs 377px against a 240px box. break-words fixes it.
detail-modal prize row 190px content box at 320px; every prize string in the data overflows once the countdown sits beside it — $44,000+ in prizes by 40px, the longest entry by 127px. The clipped part was the countdown, i.e. the urgency signal.

iOS zoom-on-focus — both search inputs (globe 14px, deck 13px). Safari zooms the page whenever a focused input is under 16px, and layout.tsx exports no viewport with maximum-scale to suppress it, so tapping search zoomed the page and left you there. Both are text-base below sm.

Touch targets — map pins were 14×14px and are the only way to open a hackathon, with a near-miss landing on the canvas whose handler dismisses. Tracker's remove ✕ was opacity-0 until group-hover: with no hover on touch it wasn't small, it was invisible and unreachable. The stage ← / → pills were ~23.5px and are the only touch path to advance a card, since HTML5 drag-and-drop doesn't fire on touch.

Scoping

Every change is behind sm: or a coarse-pointer query. Desktop renders as before. The marker hit area is deliberately gated on (hover:none),(pointer:coarse) — applied unconditionally it would fire the hover popup from 15px of empty space.

Verification

eslint 0 · tsc --noEmit 0 · 151 tests · production build exit 0.

Checked against the production build, not the dev server — its CSS chunk was stale and showed min-h-11 present but h-11/w-11 missing, which would have meant the remove-button fix silently did nothing. In the real build all three emit, --spacing: .25rem puts min-h-11 at exactly 44px, and the media query survives minification.

Not done

Still open from the audit: 7 degrades (footer nav pitch, globe filter pills, deck heart, hover-only status on the deck, modal action row, /resources stage rail) and ~13 polish touch targets.

Two need a product decision rather than a mechanical fix:

  • passport.tsx scales to 0.39–0.49 at phone widths, putting all its type at 3.3–7.4px. Pillar 03 is unreadable on mobile, and the fix changes how it looks.
  • Text legibility generally — 84 elements at 9px and 85 at 10px on /deck alone. Raising the floor changes a deliberate aesthetic.

Unverified: I could not resize the browser here (the tool reports success but innerWidth never changes), so measurements come from loading each route in a 390px iframe. Nobody has held this on an actual phone yet.

🤖 Generated with Claude Code

Jose-Gael-Cruz-Lopez and others added 2 commits August 5, 2026 01:20
Audited every route at 390px by loading it in an iframe (the browser cannot be
resized here). Result: the site already fits — horizontal overflow is 0px on
/, /globe, /deck, /resources, /my and /hackathons, and the wide fixed widths in
the source all sit inside deliberately clipped containers. What actually fails
on a phone is tap ergonomics: 68 undersized targets on /globe, 82 on /deck.

Three of those are load-bearing, and all three fixes are scoped so desktop is
byte-for-byte unchanged.

globals.css — map pins were 14x14, a third of the 44px minimum in each axis.
Opening a hackathon is the only thing the globe does, and a near-miss lands on
the canvas, whose click handler dismisses rather than opens. A ::after grows the
hit area to 44x44 with no visual change. `.hq-marker` is already
position:absolute — mapbox adds `.mapboxgl-marker` to the element you hand
`new Marker({element})` (mapbox-gl-dev.js:108765) and that class carries
`position:absolute;top:0;left:0` — so the pseudo-element anchors to the dot.
Kept behind `(hover:none),(pointer:coarse)`: unconditionally it would fire the
hover popup from 15px of empty space on desktop. It makes single pins tappable,
not dense clusters, where the 44px discs overlap and DOM order wins.

tracker.tsx — the remove ✕ was `opacity-0` until `group-hover`. There is no
hover on touch, so it was not merely small, it was invisible and unreachable.
Below sm it is now visible and 44x44; from sm up it reverts exactly to the
hover-revealed button. The negative margin absorbs into the card padding, so the
bigger hit box costs no layout.

tracker.tsx — the stage ← / → pills were ~23.5px, and they are the only
touch-usable way to advance a card, because the card's HTML5 drag-and-drop does
not fire on touch. `inline-flex min-h-11 items-center` with `sm:min-h-0` keeps
the compact desktop pill. `inline-flex items-center` is load-bearing: a bare
min-height on an inline-block button top-aligns the 9px label in a 44px box.

Verified against the production build, not the dev server, whose CSS chunk was
stale: --spacing is .25rem so min-h-11 resolves to exactly 44px, and the media
query survives minification. eslint 0, tsc 0, 151 tests, build exit 0.

No tests added: these are CSS and class changes, and the repo has no component
test harness — only pure functions under lib/ are covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second pass from the audit. Corrects something I got wrong earlier: I reported
"zero horizontal overflow on every route" from measuring page scrollWidth, and
that number was right but the conclusion was not. `.shell` and the modal panel
both set `overflow: hidden`, so oversized content is CUT OFF rather than pushed
into a scrollbar. Nothing overflowed the page because everything that overflowed
was already being clipped.

Three of those, all verified against the 84 real listings rather than argued
from principle:

sections.tsx — the stats strip was 2-up on mobile, giving each cell 116px at
320px, while the display clamp floors at 3rem and never shrinks. prizeDisplay
renders up to 7 characters ("$12.3M" plus the "+" span), which cannot fit at
48px under any font. It bled past both edges, collided with the neighbouring
number, and was clipped. Now one per row below sm; sm and up unchanged.
Stacking keeps the giant-number look rather than dropping the clamp floor.

detail-modal.tsx — the title is locked at 28.8px on every phone width (4vw is
only 12.8-15.6px there, so the clamp floor never engages), and Syncopate Bold
costs ~25.8px per capital. Real titles carry unbreakable tokens:
"(Pre-Registration" needs 377px against a 240px box at 320px. `break-words`
fixes it and is desktop-neutral, since overflow-wrap only breaks a word that
cannot fit on a line alone.

detail-modal.tsx — the prize row put prize and countdown in a non-wrapping
justify-between row with a 190px content box at 320px. Every prize string in the
data overflows it: "$44,000+ in prizes" by 40px, the longest entry by 127px. The
countdown — the urgency signal — was the part clipped off. Stacked below sm,
with min-w-0 and break-words because "participants." alone measures 228.9px.

Both search inputs — 14px on the globe, 13px on the deck. iOS Safari zooms the
whole page when a focused input is under 16px, and layout.tsx exports no
viewport with maximum-scale to suppress it, so tapping search zoomed the page
and left the user zoomed. Both are text-base below sm and restore their exact
previous size from sm up. The globe field also goes w-56 -> w-60, because at
16px the placeholder needs ~210px and w-56 leaves 190px.

eslint 0, tsc 0, 151 tests, production build exit 0. Every change is scoped
below sm, so desktop renders as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
hackhq Ready Ready Preview Aug 5, 2026 3:15pm

Request Review

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 5, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
hackhq a0db1af Commit Preview URL

Branch Preview URL
Aug 05 2026, 03:16 PM

deck.tsx — a listing's state (OPEN / CLOSING SOON / OPENS SOON) reached mobile
users through `title` on a 10px dot and nothing else. `title` needs a pointer to
appear, so on a phone the state was carried by hue alone, which is also
WCAG 1.4.1. The columns that spell it out are all hidden below lg. It now reads
as a line in the text column, with the countdown that `cd` already computes for
the lg deadline column, so it costs nothing.

detail-modal.tsx — the action row never wrapped despite `flex-wrap`, because
`flex-1` gives the CTA a hypothetical main size of 0. The pair stayed side by
side and the link was squeezed to 139px at 375px while "VISIT WEBSITE ↗" needs
199px, so the primary CTA's label broke across two lines inside its own pill.
Trimming padding cannot rescue it — beside the save button the CTA can never
exceed 166.5px — so the pair stacks below sm.

Touch targets, all restored exactly at sm and up:
- deck save heart 36 -> 44px (sm:h-9 sm:w-9 keeps the desktop circle)
- globe filter pills and the VIRTUAL button ~29 -> 44px
- /resources stage rail pills 35 -> 44px; these are the only in-page navigation
  there on mobile and sat 4px apart
- footer nav links were 17px tall on a 27px pitch; they are now flush 44px
  blocks below sm, so the column is live rather than targets separated by dead
  space, and revert to inline anchors at sm

On the rail: `--stage-scroll-offset` is measured from the rail's real height by
a ResizeObserver rather than hardcoded, so the taller pills self-correct the
jump-to-section clearance. That is what the existing docstring designed for.

`inline-flex items-center` is load-bearing wherever min-h-11 is added — a bare
min-height on an inline anchor or inline-block button does not apply, and would
top-align the label in a 44px box.

eslint 0, tsc 0, 151 tests, production build exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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