Every screen in the game is drawn inside one fixed 1920×1080 box that is then scaled to the device. Nothing in the client is authored in device pixels. Getting this wrong has produced three separate defects in a single day, so it is written down here rather than rediscovered a fourth time.
The stage carries zoom: fitScale, where
fitScale = min((innerWidth − safe-area left − right)/1920, (innerHeight − safe-area top − bottom)/1080), recomputed on resize; on a desktop the safe-area insets are zero and it is the plain minimum of the two ratios. The whole stage scales as one piece — there is no separate text-scale token (the --txs text scale tried in 0.92 came out under D’s T57 ruling of 2026-11-02, “no text scaling!”).
| Device | fitScale | Stage drawn at |
|---|---|---|
| Desktop 1600×900 | 0.833 | 1600×900 — fills exactly |
| Phone 690×674 | 0.359 | 690×388, letterboxed top and bottom |
THE RULE: an authored px is a STAGE px. What the player sees is
authored × fitScale. A 14px label is about 5 CSS px on a phone.
Never judge a size by reading the source — measure the drawn result at a phone viewport.
Because the stage is letterboxed on a phone, its top-left sits at roughly
y = 143 on a 674px-tall viewport, not at 0. HUD elements are positioned against the
stage, which is the corner a player reads as the game's edge. A probe that clicks
“the top-left of the screen” is clicking page background, not the game.
A position:fixed overlay is not scaled by the stage's
zoom, so any percentage sizing on it resolves against the real viewport while
everything inside it is still in stage units. This asymmetry is the cause of the defects below.
| Instance | What happened |
|---|---|
| Dismantle toast | Rendered at 1× against a 0.36× UI, 572px wide on a
690px viewport. Horizontal overflow made mobile Chrome expand the layout viewport, and
fitScale did not recompute — the stage kept its old size inside a bigger
viewport, which reads as a permanent resolution shift. Still open on mobile. The desktop half of the same defect — the overflow scrolling the document instead of expanding the layout viewport — is closed on dev 2026-08-10; see below. |
| Devour panel | max-height:88% resolved to 593px on a phone
against 792px on desktop, so the body scrolled on the phone only. Fixed 2026-08-05 by sizing the
contents down. See Devour. |
| Premium shop | A three-column grid capped each card near 360 stage px, forcing text that drew at 5 CSS px. Fixed 2026-08-05. See Premium Shop. |
The stage sits inside a wrapper that fills the window and centres it. That wrapper used to be
width:100vw; height:100vh — ordinary in-flow layout — which means
if the document scrolls sideways, the entire game slides with it. The corners go off
the edge and the player cannot reach them again. It reads as “the game zoomed in and cut the
left side off”, which is what it was reported as.
html, body { overflow:hidden } does NOT stop this.
It suppresses scrollbars and wheel/touch scrolling. It does not stop programmatic
scrolling, and it does not stop the browser scrolling the document itself to reveal a newly focused
element. One click that moves focus onto anything rendering past the right edge scrolls the page
— and leaves it there.
Measured 2026-08-10 on D's own save at a 1893×993 viewport, forcing
documentElement.scrollLeft = 67:
| At rest | Scrolled 67px | |
|---|---|---|
| Stage left edge | 64 | −3 |
| Stage centre | 946 | 879 |
| Stage right edge | 1829 | 1762 |
Fullscreen button (position:fixed) | 1845 | 1845 — unmoved |
That reproduced the reporter's screenshot to within 1–3 px, so the mechanism is not inferred.
THE DIAGNOSTIC, and it is the whole trick: a position:fixed
element does not move when the document scrolls, but it does grow when the page is zoomed.
The shell's fullscreen button (#__fs, top:10; right:10) is therefore a free
ruler on any screenshot. Since T55 (2026-11-02) the fullscreen toggle lives inside the MENU hud, right of the plate, and the floating #__fs hides whenever that hud is mounted — so the ruler is only there on the sign-in gate, the loader and the evolution screen. Elsewhere compare the stage’s own left edge against the viewport: a scrolled page moves it, a zoomed page resizes it. If it sits at the same pixel while the HUD has shifted, the page is
scrolled. If it has changed size, the page is zoomed. Chasing this
as a zoom problem costs a session — browser zoom does not clear a scroll offset, so
Ctrl+/− appears to do nothing and looks like a dead end. Only a reload
clears it, and no save or server state is ever involved.
position:fixed; inset:0. It is anchored to the viewport, so it
cannot drift no matter what the document does. This is the load-bearing half.play/index.html resets
scrollLeft/scrollTop to 0 on scroll (capturing) and on
resize, so no blank strip can appear beside the stage either.position:fixed overlay authored at 1920-canvas widths is a candidate.Verified by tools/probe_stagepin.py: 4/4 on the fix, 2/4 on
0.45, with two controls (stage centred at rest; the fixed button immobile) passing on
both trees, so the flip is real and not a probe that stopped measuring.
left:0; top:0 on its host — and is rectangular,
border-radius:0. It is 64 stage px tall; since T55 (2026-11-02) a 48px round fullscreen toggle sits to its right in the same row, and every window header keeps a band clear for both (title margin 258 stage px). It was previously inset
24,8 with border-radius:999px, a pill. (D, 2026-08-05.)left:0. Moving the host any further right narrows that margin.Small overlay glyphs are where the stage-unit trap bites hardest, because a corner badge is
authored small to begin with. Survey at fitScale 0.3594 (phone 690×674):
| Surface | Stage px | Drawn |
|---|---|---|
| Item tile gender glyph | 30px | 10.78 CSS px — raised from 16px / 5.75 on 2026-08-05 (D: “very hard to see”) |
| Creature card gender | 30px | 10.78 CSS px |
| Breeding pen gender | 34px | 12.22 CSS px |
| Hatch result sex | 26px | 9.34 CSS px |
| Equipment current gender | 17px | 6.11 CSS px — still small, not yet raised |
| Breeding picker gender | 19px | 6.83 CSS px — still small, not yet raised |
Rule of thumb from these: anything under about 22 stage px is illegible on a phone. The item tile is 135×135 stage px, so a 30px badge is roughly 22% of it — large enough to read without swamping the art.
elementFromPoint and getBoundingClientRect disagree
under CSS zoom, so an occlusion test built on the two together reports elements that are
plainly on screen as invisible. elementFromPoint also answers null outside
the viewport, and returns the element beneath anything marked
pointer-events:none. Prefer geometry plus computed style, and convert offsets back into
stage units by dividing by fitScale before comparing them to authored values.
Probes: tools/probe_menucorner.py, tools/probe_devourfit.py,
tools/probe_shoplist.py — each measures at a phone viewport, not only at desktop.
2026-11-03 — codex review (dfmc-client lane codexreview-a; D 2026-11-03 “review the codex, clean it up, update whatever needs updating”). WIKI ONLY. The fitScale formula now shows the safe-area insets the client subtracts (phones with a notch), and says there is no text-scale token after T57. The scroll-vs-zoom diagnostic relied on the floating fullscreen button, which T55 moved into the MENU hud; the note says where the ruler still exists and what to compare elsewhere. The MENU plate’s stated size read 143×56; the button is 64 stage px tall with the 48px toggle beside it. The glyph survey and the measured tables stay as the dated measurements they are.
2026-08-29 — a creature's sheet shows the moves it actually knows. LIVE on release since 2026-09-01 (0.65). A player noticed his level 93 creature described a move one way when equipped and another way on its own sheet — the sheet was still printing the first-stage name and text. The sheet is shared with the codex, and the codex deliberately shows every line at stage one because a codex entry owns no creature and so has no level. A creature opened from the bag was using that same “no level” view even though it was carrying its level and its experience the whole time. It now resolves the ladder at the creature's own level, and experience takes precedence over a stored level — which matters, because the report came in straight after a devour, and devour is what moves experience. Browsing the codex is unchanged.
position:fixed; inset:0 plus a shell scroll pin. New section above records the measurements and the position:fixed ruler that tells scroll from zoom. ON DEV — LIVE on release since 2026-09-01 (0.65)..position:fixed escape, the menu HUD spec, and how to measure any of it.