# Mobile design baseline Mobile (≥55% of traffic) is the primary target for this app. Any new screen or component must be designed at the **360 px** viewport first and verified at three reference widths before merge. ## Reference viewports | Width | Device class | Purpose | | --- | --- | --- | | 360 px | Low-end Android (Samsung A-series, older Pixels) | Hard floor — if it doesn't fit here it isn't shipping | | 390 px | iPhone 14 / 15 / 16 (38% of mobile traffic) | Primary iOS target | | 430 px | iPhone 16 Pro Max, large Android | Upper mobile bound | ## Acceptance checks for any screen change Before raising a PR that touches user-visible UI, confirm at each reference width: 1. **No horizontal overflow.** `document.documentElement.scrollWidth === window.innerWidth`. The most reliable check: in DevTools console run ```js document.documentElement.scrollWidth - innerWidth ``` It must read `0`. Any positive number means something is bleeding past the right edge — usually a fixed-width element, an inline-block that didn't wrap, or a flex row missing `flex-wrap: wrap`. 2. **Tap targets ≥ 44 × 44 px** on every interactive element (iOS Human Interface Guidelines minimum). Probe with: ```js Array.from(document.querySelectorAll('a, button, [role=button], input, select')) .filter(el => el.offsetParent) .map(el => ({ t: el.innerText?.trim().slice(0,30), r: el.getBoundingClientRect() })) .filter(o => o.r.width < 44 || o.r.height < 44) ``` 3. **No text below 11 px** in any visible-by-default block. Decorative demo content (illustrations, mocked previews) should either scale up or be hidden under the `640 px` breakpoint — see `MOB-04` for the pattern used on the home page's "What you'll see" section. 4. **iOS Chrome bottom-bar parity.** The fixed `Navigation` bottom tab bar already compensates for the auto-hiding URL bar via the Visual Viewport API (`Navigation.tsx`). New fixed-bottom elements must either use the same offset (read `var(--mobile-bar-offset)`) or sit inside the existing tab-bar container. 5. **Safe-area insets** on any new sticky/fixed chrome: `padding-bottom: env(safe-area-inset-bottom)` for bottom-pinned UI, `padding-inline: env(safe-area-inset-left/right)` for header-class chrome that runs full bleed. 6. **`dvh`, not `vh`.** iOS Safari's collapsing toolbar makes raw `vh` units jump. Prefer `100dvh` (with a `100vh` fallback if you support older engines) for any height that needs to track the visible viewport. ## Component patterns - **Hide-on-mobile decoration:** wrap with `@media (max-width: 640px) { .x { display: none; } }` — examples in `HomeView.module.css` (`.hiwVisual`), `MetricTooltip.module.css` (`.wrapper`). - **Right-edge scroll-fade for horizontal scrollers:** `mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);` Drop the fade when scrolled to the end with a JS-toggled class — see `SchoolDetailView.tsx`'s `sectionNavAtEnd` state for the pattern. ## Automation (future) A Playwright regression test that asserts `docW === vw` at the three reference widths on `/`, `/rankings`, `/admissions`, `/compare`, and a representative `/school/:urn` page would catch overflow regressions immediately. Not added yet — Playwright isn't currently in the project dependency set, and the existing Jest setup doesn't compute layout. Worth adding if mobile overflow regressions recur.