← all tickets

14 — Mobile: responsive index + cards pages

What to build: Make the deck index and cards pages usable on mobile: add the viewport meta (currently absent everywhere) and one ~700px breakpoint in style.css (currently zero @media queries). Single-column reflow, ≥44px touch targets, wide tables scroll inside their own container. Game pages untouched — not even viewport meta (full mobile gameplay is parked in TODO under longterm).

Blocked by: 12 — browser filter (subtype-filter/).

Status: fixed

Comments

2026-08-02: Shipped. <meta name="viewport" content="width=device-width, initial-scale=1"> added to static/index.html and static/cards.html only, and one @media (max-width: 700px) block appended to static/style.css — still the file's only media query. static/game.html is untouched and deliberately has no viewport, so a phone renders the board at the 980px fallback width rather than at a squeezed device width; that is the parked-in-TODO behaviour, not an oversight.

At the breakpoint: .deck-summary, .starter-pairs, .starter-pair-btns, .deck-inputs and .quick-start-footer all flip to flex-direction: column with min-width: 0 on the children that carried a 260px floor, so nothing forces horizontal page scroll; .start-btn-big and the starter/toggle buttons go full width. Touch sizing is one grouped rule setting min-height: 44px on buttons, selects and text inputs, plus min-width: 44px on the saved-deck and a 44px flex row for the radio labels. That rule also sets font-size: 16px, which is what actually stops iOS Safari zooming the page on focus — the 13px/0.85rem controls would otherwise trigger it. Modals lose their min-width: 300px (it overflows a 320px viewport) and top-align with overflow-y: auto so a soft keyboard cannot strand the buttons.

The seven-column card table is now wrapped in <div class="table-scroll">, which gets overflow-x: auto plus min-width: 620px on the table only under the breakpoint, so it scrolls in its own box. The sticky thead th is reset to position: static there on purpose: .table-scroll becomes a scroll container, and a sticky header inside a container that only scrolls horizontally freezes mid-table instead of tracking the page.

Two things needed markup changes to be reachable from CSS at all. (1) The filter row was a flat sequence of <label>/<select> siblings, so a column reflow would have put every label on its own line; each pair is now wrapped in .filter-field (with for=/id= association, which also makes the labels tappable). (2) #filter-search carried style="margin-left:auto; width:180px" — an inline style outranks any media query, so on a phone it would have stayed a 180px right-floated box; those two declarations moved into the page's <style> block. tests/test_static_pages.py guards both, along with the viewport contract (present on / and /cards, absent on /game) and the single-breakpoint rule.

Not done, and not in scope: static/editor.html gets no viewport meta and no breakpoint here — ticket 15 owns the editor, and giving it a device-width viewport without the tab layout would shrink the side-by-side panels rather than help.

Tests: 121 pytest (114 baseline + 7 new), 137 cargo — both green, cargo unchanged. cargo fmt and cargo clippy --workspace -- -D warnings clean. No browser is installed in this environment (no chromium, no playwright), so the layout was not rendered visually; verification was reading plus specificity checks against the page-local <style> block in cards.html, which loads after style.css and therefore wins ties — every mobile rule for that page is scoped through .cards-page for exactly that reason. A visual pass on a real phone is still worth doing.