Why this exists
The web client is laid out as document flow: every region's geometry is decided by whatever
content happens to be inside it. That single fact produces all of the following, and none of them
are cosmetic:
- At 200% zoom the layout viewport halves and text clips. Every zoom or resolution change breaks
something new, because nothing was designed against a scale.
- A hand card renders
C…, Dis…, L… — the mana cost shares the name's row and wins, so the
card's only identifier is destroyed.
- The battlefield scrolls. Measured on
gameview-board.json at 1440×900: your field is a 247px
box holding 633px of content, so your lands row is simply not on screen.
- An empty opponent board reserves 200px to say "No permanents", while the seat that has
permanents is starved.
- A tapped permanent rotates its text with its frame, so a land reads
Mou… sideways under a
rotated "Tapped" pill covering its own art.
- Each permanent's column is 233px wide for a 108px card, because the relationship sentence under
it sets the column width.
docs/client-design.md is the design that replaces the flow with a scene. This epic sequences the
work that builds it.
The one rule this epic exists to enforce
Zoom, resolution, and aspect ratio are the same problem. A browser at 200% zoom does not scale
the page — it halves the layout viewport. 1280×720 at 200% is a 640×360 viewport, which is
phone-landscape territory. Whatever makes a phone possible makes zoom safe, and either can be
tested by the other.
The bar
| Class |
Screens |
Commitment |
| Optimized |
Desktop landscape, ≥1280×720 at 100% |
A good experience |
| Possible |
Common phone sizes; any desktop at 200% zoom |
Playable, nothing clipped, everything reachable |
| Unsupported |
Short edge < 320px |
Say so plainly, in place of a broken board |
Sequence
Twelve children. Waves may run in parallel internally; a wave starts when the one before it lands.
| Wave |
Issue |
Depends on |
Why it sits here |
| 0 |
#653 — adopt the spec, delete the bootstrap constraints |
— |
Sets the bar everything else is graded against. Until it lands, the repo still says the current client is acceptable. |
| 0 |
#654 — three open design decisions |
— |
Maintainer decisions. No code. Blocks the frame, the hand, and the lobby. |
| 1 |
#655 — the scene model |
#653 |
One pure function: viewport in, band and region geometry out. Load-bearing for four later issues. |
| 1 |
#657 — the card, redrawn |
#655 |
The atom every surface draws, and the most visible defect. Runs alongside the scene model. |
| 2 |
#656 — the scale gate |
#655 |
Lands early on purpose: it fails on main, and every later PR has a target that goes green. |
| 2 |
#658 — tapped as a mark, not a rotation |
#657 |
Small, separable, and it removes a case the packer would otherwise have to model. |
| 3 |
#659 — the frame |
#654, #655, #656 |
The migration from document to scene. Decisions 1 and 2 land here. |
| 4 |
#660 — pack the battlefield |
#657, #659 |
Card size and overlap from the room and the count. Ends the board scrollbar. |
| 4 |
#662 — the stack |
#657, #659 |
Top item always named; the rail stops being clipped. |
| 5 |
#661 — the hand |
#657, #659, #660 |
Reuses the packer's fan. Never abbreviates a name. |
| 5 |
#663 — relationships that read at a glance |
#660, #662 |
Anchors move when packing changes, so it follows them. |
| 6 |
#664 — redesign the lobby |
#654, #655 |
Needs decision 3 first. Design section, then build. |
Critical path
#653 → #655 → #656 → #659 → #660 → #663
Start here
#653 and #654 in parallel. Neither needs any code, and the second is a conversation with the
maintainer rather than an implementation.
Guard rails that apply to every child issue
Copy these into your working context before starting any child:
- Zero game logic in the client. It renders
GameView and returns an action_id from
valid_actions[]. Nothing here computes legality, cost, effect, or targets. Card presentation
decisions (what to draw, at what size, what to drop) are presentation; the rules answer is always
the server's.
- The entire UI reconstructs from one
GameView plus a pending prompt. No layout state is
load-bearing across messages. A refresh mid-game produces the same screen.
- No child issue in this epic needs a protocol change. If you conclude you need one, stop and
say so on the issue rather than adding a field.
- Keep logic out of components. Anything with a rule in it goes in a pure module under
clients/web/src/ with a .test.ts beside it, testable with neither React nor a browser.
- Don't let a file grow past ~800 lines (
docs/coding-standards.md).
- Every change ends in a playable state. Do not leave the table half-migrated between PRs.
make verify before opening a PR. make e2e-views is the broad, non-blocking tier;
make e2e-smoke is the blocking gate.
- Branch off
main (feat/…, fix/…, docs/…), Conventional Commits, one issue per PR.
Definition of done for the epic
Relationship to #640
#640 was the previous umbrella and its remaining open criteria are absorbed here: the
"nothing relies on colour, position, or motion alone" audit becomes part of the visibility contract,
and the zone-change flight animation is deliberately not scheduled in this epic — animating a
card into a clipped region is not worth building. It should be re-filed once the scene lands.
Why this exists
The web client is laid out as document flow: every region's geometry is decided by whatever
content happens to be inside it. That single fact produces all of the following, and none of them
are cosmetic:
something new, because nothing was designed against a scale.
C…,Dis…,L…— the mana cost shares the name's row and wins, so thecard's only identifier is destroyed.
gameview-board.jsonat 1440×900: your field is a 247pxbox holding 633px of content, so your lands row is simply not on screen.
permanents is starved.
Mou…sideways under arotated "Tapped" pill covering its own art.
it sets the column width.
docs/client-design.mdis the design that replaces the flow with a scene. This epic sequences thework that builds it.
The one rule this epic exists to enforce
Zoom, resolution, and aspect ratio are the same problem. A browser at 200% zoom does not scale
the page — it halves the layout viewport. 1280×720 at 200% is a 640×360 viewport, which is
phone-landscape territory. Whatever makes a phone possible makes zoom safe, and either can be
tested by the other.
The bar
Sequence
Twelve children. Waves may run in parallel internally; a wave starts when the one before it lands.
main, and every later PR has a target that goes green.Critical path
#653 → #655 → #656 → #659 → #660 → #663
Start here
#653 and #654 in parallel. Neither needs any code, and the second is a conversation with the
maintainer rather than an implementation.
Guard rails that apply to every child issue
Copy these into your working context before starting any child:
GameViewand returns anaction_idfromvalid_actions[]. Nothing here computes legality, cost, effect, or targets. Card presentationdecisions (what to draw, at what size, what to drop) are presentation; the rules answer is always
the server's.
GameViewplus a pending prompt. No layout state isload-bearing across messages. A refresh mid-game produces the same screen.
say so on the issue rather than adding a field.
clients/web/src/with a.test.tsbeside it, testable with neither React nor a browser.docs/coding-standards.md).make verifybefore opening a PR.make e2e-viewsis the broad, non-blocking tier;make e2e-smokeis the blocking gate.main(feat/…,fix/…,docs/…), Conventional Commits, one issue per PR.Definition of done for the epic
docs/client-design.mdis marked binding, and bothAGENTS.mdfiles point at it instead ofcarrying their own layout constraints.
Relationship to #640
#640 was the previous umbrella and its remaining open criteria are absorbed here: the
"nothing relies on colour, position, or motion alone" audit becomes part of the visibility contract,
and the zone-change flight animation is deliberately not scheduled in this epic — animating a
card into a clipped region is not worth building. It should be re-filed once the scene lands.