What to build: On the ~700px breakpoint the editor's side-by-side deck-panel + browser-panel become Deck|Browser tabs with a sticky top bar; the bar must include the undo/redo buttons (ticket 11) and the final filter controls (ticket 13). Tap a card row to read its text (bottom sheet or inline expand — replaces hover tooltip on touch); add/remove stays on explicit +/− buttons. Game pages untouched.
Blocked by: 11 — editor undo/redo (editor-undo/); 13 — editor filter (subtype-filter/).
Status: fixed
2026-08-02: Shipped. static/editor.html gets the viewport meta ticket 14 deliberately left off, and everything mobile reuses that ticket's single @media (max-width: 700px) block in style.css — still the only media query in the repo, and the editor's own <style> block adds none. Because that block loads after style.css and wins ties, every editor rule at the breakpoint is scoped through .editor-page on <body>, the same trick .cards-page uses; tests/test_static_pages.py now derives the class list from the page's style block and fails any breakpoint rule that touches one of them unscoped.
Layout. Desktop keeps its fixed 100vh frame with two independently scrolling panels. Below 700px .editor-wrap becomes an ordinary scrolling document (height: auto, overflow: visible, inner scrollers released) with .editor-topbar sticky at the top — inner scroll containers inside a 100vh frame fight the mobile URL bar. .editor-body goes to display: block and body[data-tab] picks which panel shows; the tab strip is display: none outside the breakpoint, so desktop is untouched.
What is in the bar. Header (back, deck name, undo/redo, Save) + tab strip + the browser filters. The filters are authored inside the browser panel and editor.js moves them into #bar-filters when the query matches, restoring the recorded desktop position when it stops; the format/ban-list selects make the opposite trip, down into the Deck tab under a "Format" label. Both are single appendChilds, so listeners and state survive. That trade is the whole point: the bar is a header plus a tab strip already, and every row it grows is a row of cards the phone stops showing. The deck stats stay in the bar on both tabs — one text line, and the card count and influence total are what you steer by while adding cards.
Tap to read. matchMedia('(hover: none), (max-width: 700px)') decides, per row build, between the hover tooltip and a tap. Where it matches, rows get a click handler that opens a bottom sheet (#card-sheet) with title, type/subtypes/faction, stats and the card's text — no image, so it costs no bandwidth and fits a phone. The handler bails on closest('button, input, select, label, a'), so +/− stay the only things that edit the deck. Crossing the query rebuilds both lists.
Touch sizing follows ticket 14: 44px minimum and 16px font (the part that actually stops iOS zooming on focus) on every control, including the +/− and add buttons; the browser rows drop their type and faction columns, which the sheet shows anyway.
Tests: 128 pytest (121 baseline + 7), 137 cargo, both green; cargo fmt and cargo clippy --workspace -- -D warnings clean. Not rendered — no browser in this environment, so verification was reading plus specificity arithmetic against the page-local style block. Worth a real-phone pass, and worth measuring the bar: on the Browser tab it is roughly 280px of a ~560px viewport, which is defensible while the ticket asks for pinned filters, but a collapsible filter row is the obvious next refinement.