THE STAGE

Other Systems › The Stage

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.

fitScale

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!”).

DevicefitScaleStage drawn at
Desktop 1600×9000.8331600×900 — fills exactly
Phone 690×6740.359690×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.

The stage corner is not the screen corner

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.

position:fixed escapes the zoom

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.

InstanceWhat happened
Dismantle toastRendered 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 panelmax-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 shopA 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.

A scrolled document drags the whole stage with it

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 restScrolled 67px
Stage left edge64−3
Stage centre946879
Stage right edge18291762
Fullscreen button (position:fixed)18451845 — 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.

What it is now

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.

The menu HUD

Glyph and badge sizes, as drawn

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):

SurfaceStage pxDrawn
Item tile gender glyph30px10.78 CSS px — raised from 16px / 5.75 on 2026-08-05 (D: “very hard to see”)
Creature card gender30px10.78 CSS px
Breeding pen gender34px12.22 CSS px
Hatch result sex26px9.34 CSS px
Equipment current gender17px6.11 CSS px — still small, not yet raised
Breeding picker gender19px6.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.

Measuring it

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.

Changelog

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.