fix(mobile): make the site usable on a phone (audit pass 1) - #247
Open
Jose-Gael-Cruz-Lopez wants to merge 3 commits into
Open
fix(mobile): make the site usable on a phone (audit pass 1)#247Jose-Gael-Cruz-Lopez wants to merge 3 commits into
Jose-Gael-Cruz-Lopez wants to merge 3 commits into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Deploying with
|
| 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
scrollWidthat 390px on all six routes and got 0px horizontal overflow everywhere. That number is correct but the conclusion I drew from it was wrong:.shelland the modal panel both setoverflow: 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:
sections.tsxstats stripprizeDisplayrenders 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 belowsm.detail-modaltitle(Pre-Registrationneeds 377px against a 240px box.break-wordsfixes it.detail-modalprize row$44,000+ in prizesby 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.tsxexports no viewport withmaximum-scaleto suppress it, so tapping search zoomed the page and left you there. Both aretext-basebelowsm.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-0untilgroup-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
eslint0 ·tsc --noEmit0 · 151 tests · production build exit 0.Checked against the production build, not the dev server — its CSS chunk was stale and showed
min-h-11present buth-11/w-11missing, which would have meant the remove-button fix silently did nothing. In the real build all three emit,--spacing: .25remputsmin-h-11at 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,/resourcesstage rail) and ~13polishtouch targets.Two need a product decision rather than a mechanical fix:
passport.tsxscales 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./deckalone. Raising the floor changes a deliberate aesthetic.Unverified: I could not resize the browser here (the tool reports success but
innerWidthnever 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