How the game fits itself onto a screen. The whole UI is authored on one fixed 1920×1080 canvas and scaled to the device; the shell page around it owns loading, fullscreen, scroll pinning, platform quirks, and pushing every connected client onto a new build. These behaviours live in the hand-maintained shell, not in the generated screens.
fitScale = min(viewportWidth/1920, viewportHeight/1080) and applies it to the stage as CSS zoom. Roughly 0.92 on a desktop monitor, ~0.36 on a phone held landscape., . % + x × -; #519, 2026-08-23), so an overflowing stat value can shrink below word size. The original size is cached and restored when the text fits again.vw / vh unit, window media query or window-size branch lays out anything inside the stage; the stage is the containing block of its position:fixed descendants (contain: layout), so a fixed card is placed in stage px against the 1920×1080 box and clipped to it. The one residue is the browser’s own pixel snapping under zoom: a 1px border never draws thinner than one device pixel, so at a small scale a long list of bordered cards can drift a few stage px.fitScale of that on screen. Sizes chosen for readability must be chosen in stage units with the phone scale in mind.--txs), 0.9 / scale below 0.75×, built 2026-10-30 as rulings card T57’s rec) was removed on D’s answer to the card, 2026-11-02: “no text scaling!” Every font-size is its authored stage px again, including the five 9 px captions the token had floored to 10; small text on a small screen is a stage-units decision per element (the bullet above), never a global multiplier. Gate probe_t57_textscale pins it.What was actually wrong. The warped iPhone UI was not text autosizing. WebKit applies its built-in 9 px minimum logical font size to the zoomed size: under the stage’s zoom: 0.32 every font authored below 9/0.32 = 28 px renders at 28 stage px, and the browser reports that inflated size back as the computed size. Two consequences: every small label, socket cell and caption on an iPhone drew 2–3× too big for its box, and the text fitter re-read the inflated size as its base on every render and wrote it back — a multiplier per render, which is how the class card’s stat digits reached ~15× in D’s screenshot. Chrome has no such minimum, which is why Android was never affected. Reproduced in a real WebKit engine (Playwright WebKit 26 emulating an iPhone), not reasoned from source; gate tools/probe_iosstage.py 14/14, red 5/14 before.
width = max(1920, 1080 × screenW/screenH)) so the 1920×1080 stage sits at zoom 1 and Safari’s own viewport scale shrinks the page to the screen — the same path every desktop-width site takes on an iPhone. Fonts lay out at their authored size, metrics are exact, touch coordinates are native, and the app’s fitScale comes out at 1. Recomputed on every visual-viewport change (toolbar collapse, rotation, keyboard).tools/probe_iostap.py 9/9 on the exact surfaces named (portal, floor creature, battle unit, boon tile).2026-09-07: this was the wrong diagnosis for the warp — see the section above — but the switch is harmless and stays; the tag it introduced is what the stage fit keys on.
iOS Safari inflates font sizes on its own (“text autosizing”), and it boosts explicitly-set pixel sizes too. That breaks the text fitter's contract — the size the fitter writes is not the size that renders — so the fitter measures boosted text, shrinks it, Safari boosts the result, and the cycle repeats on every render. The visible symptom is a warped UI: text bursting out of boxes that are themselves correct. Desktop browsers never autosize, and Chrome on Android stands its boost down on pages with a device-width viewport meta (this page has one), so the fault is iOS-only.
text-size-adjust: 100%) so the fitter's number is the rendered number.overflow:hidden alone does not stop programmatic or focus-driven scrolling.Status: the three-tap route is LIVE on release since 2026-09-02 (0.67). Two ways in, one rule for every pointer:
Why two: a phone cannot hold. Touch browsers deliver the compatibility mouse-down and mouse-up together after the finger lifts, so the 450 ms timer is cancelled the instant it starts and a phone had no way into a unit’s sheet mid-fight. The rule is not device-sniffed; a desktop triple-click reaches the sheet too.
One layer per press, innermost first, and it stops there: a confirmation before its window, a creature sheet before the bag under it, a picker before the equipment screen, a sub-window before its parent, then the main window, then the menu, then the letter. Every close goes through the window’s own close routine, so nothing a click would clean up is skipped. In a fight ESC only backs out of a target pick — it never leaves combat. It does nothing while an upgrade attempt is in flight or settling, while a trade is in flight, while a hatch answer is in flight, on the skull card (OK only), during character creation, or during a floor transition. With a text field focused (the bug report, the name claim) the first ESC only leaves the field; the next one closes the window. The Settings hotkey capture keeps its own ESC. Gate tools/probe_esc.py 13/13 with real key presses, red 0/13 before.
English is the main language. Every string the covered screens show is a row of ONE table, codex/strings_en.csv (Key, Text, Context) — the English key set. Each other language is codex/strings_<code>.csv with the same keys; codex/locales.csv (Code, Name, Native, File) lists what Settings offers. An empty or absent text reads English, and an unknown key reads its own name — never a blank. Nothing is machine-translated: a language goes live by filling its file.
pt-PT device lands on pt-BR), else English. The pick is the settings prefs key lang.language, so it follows the account to every device.locales.csv and strings_en.csv load once at boot beside the other codex tables, and a language file loads the first time it is picked.tools/l10n_gate.py runs in every gate run. It fails when a locale file carries a key English does not, when a screen reads a key with no English row, when the baked English, a Settings label or a tx() string differs from the CSV, or when a NEW hard-coded text node appears in the markup. Change wording in the CSV (and its source line), then python tools/l10n_gate.py --bake.codex/strings_pt-BR.csv, every one of the 381 keys translated (2026-11-02, rulings card T63 item 5), the English beside each in Context. The locale file is read at runtime from the pinned codex, nothing is baked into the client; a key the English table gains later reads English until its pt-BR row is filled. Register: a dark-fantasy game UI in Brazilian Portuguese — short, imperative, uppercase where the English label is uppercase, the leading glyphs and the {f} {n} {t} {c} {e} placeholders kept verbatim. Proper nouns stay English: Herumon Tower, Wraithgold, Scrap, Ink, Dust, Essence, Ichor, Steam, Taskbar Mode, Autoplay / Auto PvP, Apex, Menagerie, DLC, Warrior and every class and creature name, build, tier, rating, ranking, replay, reset. Decided once: Abyss → Abismo, Alpha → Alfa, soul → alma, Soul Bag → Bolsa de Almas, socket → encaixe, scroll → pergaminho, gem → gema, rune → runa, grade → grau, floor → andar, boss → chefe, boss key → chave de chefe, streak → sequência, spoils → espólios, dismantle → desmontar, salvage → reciclar, devour → devorar, fodder → oferenda, pens → viveiros, quarry → presa, Lv → Nv, HP → PV, codex → Códice, badges → insígnias, ultimate → supremo, buff / debuff → bônus / penalidade. Which language comes next is D’s pick; adding one is a locales.csv row plus its file.T). Keyboard only: there is no controller button for it and none is planned (rulings card T64, D 2026-11-02 — “taskbar mode is desktop only”). The same tile or key closes it. A click on the strip brings the game window back. Neither the tile nor the key exists in the web build.D, 2026-11-03: “creature transfer to dungeon - build it! design a thematic UX for it in game and add it to the menu unlocks on beating level 100”. His rules (2026-11-02): “Creatures transfered to a different game retain mutant status, grade. But they start at lvl 1 on the new game. They have a World traveler mark on their sheet to show which other games it has visited”; rulings card T48: copy, free, once per creature; the Dimensional Traveler badge at level 50. The spec is dfmc-client docs/creature-transfer-2026-11.md; the doors are on system-networking.html.
codex/unlocks.csv row transfer at floor 100. The DIMENSIONAL RIFT menu tile shows the padlock OPENS AT FLOOR 100 until the ratchet reaches 100, exactly like Breeding’s, and the sheet action is hidden until then.tools/make_rift_plate.py, the taskbar plate’s own pipeline) wears the live floor-portal VFX in the 120 → 121 purple; it opens the rift window: the portal, the rules, how to send, and the WORLD TRAVELERS list (the creatures sent, from GET /api/transfer/sent)./api/transfer/dungeon and the Steam shell writes the signed record to the shared folder. A sent creature shows SENT and the mark World traveler — Tower · Dungeon; the Dungeon’s sheet shows the same mark.TRANSFER_OPEN (“0” parks every door again), Steam shell DFMC_TRANSFER_OPEN (“0” hides the bridge), Dungeon dial transfer_open 1. The live Worker’s signing key was put 2026-11-03 (kid yju6MS6S); the Dungeon accepts the dev and the live keys.codex/badges.csv, awarded when a sent creature’s Dungeon level read-back reaches 50.2026-11-03 — Steam achievements go live in the game: the icons and the Trophy Wall (board S2, D 2026-11-03: “I don't see anything about ... steam achievements ... Build those too.”; dfmc-client lane achievements). Dev build now; the partner-site rows, the icon upload and the publish are D's steps (dfmc-client steam/INTEGRATIONS.md § 9). The list stands as codex/achievements.csv (nine rows; the server decides, GET /api/steam/achievements). New: every row has its two 256×256 icons — achieved in colour, unachieved the same composition in greyscale — composed by tools/make_achievement_icons.py from art the game ships or the codex holds (the iron frame, the plates’ gold numerals, the Void ring, creature cut-outs, eggs): a floor plate for the FLOOR rows, 120 on the Void ring, a fan of creature cards for CODEX_300, a cracked egg for MUTANT_HATCH, a creature in the ring for FIRST_VOID, the ring crowned with six lines for VOID_COMPLETIONIST (steam/achievements/, review sheet _sheet.png). And the Trophy Wall, a menu window on the web build and in the Steam shell alike: one tile per row with its icon (locked = greyscale), title, description and EARNED / NOT YET EARNED, the earned count in the title line; a hidden row (Hidden=1: FLOOR_120, MUTANT_HATCH, VOID_COMPLETIONIST) reads ??? until it is earned. The marks are the server’s own verdict through the same door the shell reads; an earned date or a progress bar appears only once that record carries them (today it answers the earned list alone). Titles and descriptions are string-table rows (ach_<ID>_name / _desc, en + pt-BR), so a rename is a csv edit. Gate: dfmc-client tools/probe_trophywall.py.
2026-11-03 — The download page (board S6, D 2026-11-03: “give me a download page on my website at the end of the run with the latest version of the game build”; dfmc-client lane downloadpage). https://playdfmc.com/download (and /dev/download) is play/download.html, served by the Workers Assets html handling the way /privacy is privacy.html; the shell’s page look (Georgia, the gate’s gold button, the privacy page’s palette). It reads https://dl.playdfmc.com/latest.json at load and shows that build: version, build date, size, SHA-256, minimum OS, one DOWNLOAD FOR WINDOWS button to https://dl.playdfmc.com/<file>, a “Coming to Steam - app 3542320” line with no store link until the Steam page is public, install notes (unzip, run HerumonTower.exe, SmartScreen “More info > Run anyway” until the executable is signed, internet required, sign-in by Steam / Google / Discord), links back to the game, the codex and the privacy page. Two inline string sets (en, pt-BR) picked by navigator.language the way the sign-in gate picks its locale - the page is static and loads before any codex CSV. dl.playdfmc.com is the R2 bucket dfmc-builds; tools/push_build.py puts steam/dist/<zip> (immutable, cached a year) and latest.json (max-age=300) there after hashing the zip against latest.json, then verifies by HEAD (size) and GET (version, sha); the bucket’s CORS rule (GET/HEAD, any origin) is set once by its --cors. 0.93 is up: 248 MiB, sha256 d38ad2dc…. No Download link on the sign-in gate: the gate has no footer link row (privacy and terms are Settings actions in the app). Gate tools/probe_download.py (0/2 → 16/16, both locales).
2026-11-03 — The Steam client is a download (board N2, D 2026-11-03: “Where is the client for steam?”; rulings card T22 steps 2-3 of steam/ASSETS.md; dfmc-client lane steamclient). Dev build now; the depot upload to app 3542320 is D's step (dfmc-client steam/README.md “Depot upload”). Until now the shell was a window around https://playdfmc.com/ that read every CSV and every piece of art from the CDNs; nothing was packaged. Now steam/build.ps1 builds the client: tools/steam_bundle.py copies the client's named set from this repo at DATA_PIN (the thirteen art families, every codex/*.csv and .json, the boss art — 1878 files, 137 MB at 14ebc480; it fails on any name the client reads that is missing at the pin, so the bundle IS the pin) into steam/dfmc/; the package is the Electron runtime renamed HerumonTower.exe with the shell under resources/app, the bundle under resources/dfmc and steam_appid.txt = 3542320 beside the exe; the folder is zipped as HerumonTower-win-x64-<version>.zip and latest.json (version, file, size, sha256, built, min_os) is written beside it for the download page a later lane puts on playdfmc.com. At run time the shell serves the bundle on a loopback port and the game reads everything from it — zero requests to jsDelivr or GitHub — while the page itself and every /api/* call stay on playdfmc.com (the save is the server's; internet required). In the client, API_BASE next to DFMC_BASE: the API origin a shell that one day serves play/ itself hands over through DFMC_SHELL.apiBase. index.html computes the same value (__HT_API_BASE) and its window.fetch wrapper prefixes every relative /api/ request with it, so the 49 fetch sites in app.js keep their literal spelling and only the two sign-in redirects (location.assign, a navigation the wrapper never sees) read API_BASE; on the web both are empty and every URL is byte-for-byte what it was. The worker half of that route (a CORS door for the session cookie) is not built. The app id is read the way Steam provides it: STEAM_APP_ID, then SteamAppId (set by the Steam client on launch), then steam_appid.txt. Gate tools/probe_steambundle.py (3/5 → 5/5).
2026-11-03 — The creature transfer is BUILT: the Dimensional Rift (D 2026-11-03: “creature transfer to dungeon - build it! design a thematic UX for it in game and add it to the menu unlocks on beating level 100”; dfmc-client lane transfer; spec dfmc-client docs/creature-transfer-2026-11.md § 0). Dev build now. The parked switches are on by default on both Workers, in the Steam shell and in the Dungeon (TRANSFER_OPEN / DFMC_TRANSFER_OPEN “0” and the dial transfer_open 0 are the one way back); the live Worker got its TRANSFER_SIGN_KEY (kid yju6MS6S) and the Dungeon pins it beside the dev key. New: the unlocks.csv row transfer at floor 100 and the DIMENSIONAL RIFT menu tile (locked like Breeding below it; the live floor-portal VFX in purple over its plate) opening the rift window — the portal, D’s rules, how to send, the WORLD TRAVELERS list; the sheet action is SEND THROUGH THE RIFT on both builds (web: “The rift opens in the Steam client”, no door asked; Steam: CONFIRM SEND with the rules, then the signed record to the shared folder); the sent line is the World traveler mark “World traveler — Tower · Dungeon” (the games visited, not a level; the read-back feeds the badge door only). The Dungeon binds the arrival at level 1, xp 0, keeps the Tower level for the mark, stamps mutant on the bound mon (the codex mutant recolour in roster, sheet and run; a Mutant line on the sheet and the roster) and reads the same mark. DIMENSIONAL_TRAVELER is back in badges.csv (card T48: level 50). Nine new string keys in both languages. Gates: tools/probe_transferunlock.py 9/9, probe_t48_transfer_ui.py 12/12 (re-aimed), probe_t48_transfer_srv.py 12/12; Dungeon tools/tests/transfer_import.mjs 40/40.
2026-11-02 — The creature transfer is parked for a future expansion (D 2026-11-02, rulings answer: “hold off on the creatures for a future expansion”; dfmc-client lane xferhold; spec dfmc-client docs/creature-transfer-2026-11.md § 0). Dev build now; 0.92 ships with it parked. Nothing of the transfer is visible or reachable in 0.92, on either worker or either shell: the Worker answers 404 transfer_parked on every /api/transfer/* door, pubkey included, before the key check and the session gate, unless its var TRANSFER_OPEN is 1 — neither live worker carries it (the dev worker’s TRANSFER_SIGN_KEY stays put); the Steam shell exposes DFMC_SHELL.transfer and registers its dfmc:transfer:* ipc only under DFMC_TRANSFER_OPEN=1, so the game (SHELL_XFER) draws no SEND TO HERUMON DUNGEON, no traveler line and asks no transfer door; the Dungeon’s dial transfer_open ships 0 (its folder is never read or written, no import, no badge). The DIMENSIONAL_TRAVELER row left codex/badges.csv — an unobtainable badge must not show — and the six string keys stay in both languages. The code stays in all three places as the expansion’s base. D’s rules for the expansion, same answer, verbatim: “hold off on the creatures for a future expansion. Creatures transfered to a different game retain mutant status, grade. But they start at lvl 1 on the new game. They have a World traveler mark on their sheet to show which other games it has visited” — against what is built: level 1 on arrival (today the Dungeon maps the Tower level 1:1), mutant and grade retained (mutant is mon.tower.mutant metadata only today), a World traveler mark on the sheet listing the games visited (replaces the “Dimensional traveler levels” line). The badge at 50 is not in his new words — an open point for the expansion. Gates: tools/probe_t48_transfer_srv.py 9/12 → 12/12 (the three parked checks), tools/probe_t48_transfer_ui.py 12/12 under the open switch (W1, no bridge: no SEND, no line, no call, is what 0.92 shows), the Dungeon’s tools/tests/transfer_import.mjs 37/39 → 39/39.
2026-11-02 — The battle rings are back (bug run 2026-11-02 handoff step 2.3, lane iostap; dfmc-client play/app.js). Dev build now; release with the next ritual. Since 2026-10-23 (B17, the floating combat text) no battle ring painted at all — not the actor’s own ring, not the red target reticle on the enemies — in every engine, not only on iPhone: a // comment appended to the single-line props literal of the unit view swallowed the ringEl prop after it, so the markup slot for the ring received nothing. The comment is a block comment again and the ring renders as before: the masked rune circle in the attack red on each legal target, the actor ring on the unit choosing. The two WebKit probes that reported the ring and the floor-creature tap as iPhone-only bugs were measuring stale markup (the creature moved to a transform in 0.89, the MENU plate is art since #843, the class card’s stat boxes were rebuilt by #918) and are re-aimed at the current elements; nothing else on the iOS stage fit changed. Gates: tools/probe_iostap.py 8/10 → 10/10 and tools/probe_iosstage.py 9/14 → 14/14 in WebKit, Chromium control green in both.
2026-11-02 — The transfer bridge (rulings card T48, D “Copy, free, once per creature; a Dimensional Traveler badge at level 50 in the other game”; dfmc-client lane transfer-ui; spec dfmc-client docs/creature-transfer-2026-11.md). Dev build now; release with the next ritual and the next Steam shell build. The Steam shell exposes DFMC_SHELL.transfer beside strip: write(uid, recordJson) puts the signed record the server answered as tower/<uid>.json in the shared folder (.tmp then rename, never deleted), readLevels() reads the Dungeon’s dungeon/levels.json back ({} when absent). The folder is the one the Dungeon fixed: <appData>/dfmc-shared/ (Electron app.getPath('appData'): %APPDATA%\dfmc-shared\, ~/Library/Application Support/dfmc-shared/, ~/.config/dfmc-shared/), outside either install directory. These two doors are the page’s only filesystem access: the page names no path, nothing else is readable or writable, and nothing is ever read back as a creature (“Tower → offline is a copy, can’t come back”). The web build has no bridge, so the creature sheet shows no SEND there — the same rule Taskbar Mode follows. Shapes: dfmc-client steam/README.md “Creature transfer and the transfer bridge”. Gate: tools/probe_t48_transfer_ui.py 1/12 → 12/12.
2026-11-02 — Português (Brasil) is translated (rulings card T63 item 5, D “you can do most of this yourself”; dfmc-client lane ptbr). Codex data only — live the moment the pinned codex moves; no client change. codex/strings_pt-BR.csv carries a Text for all 381 keys of strings_en.csv (the 346-row empty scaffold also lacked the 35 keys added since C22: the Taskbar Mode settings rows and the challenge-by-code strings), in the English file’s key order, Context copied, same CSV dialect. Fragments the markup concatenates (Tap the … you want, Socketing X will DESTROY the currently slotted Y, the boss-drops sentence, the Auto PvP defense note) are translated so the assembled sentence reads in Portuguese word order. The terms and register are in “Language & the string table” above. Checked in a pt-BR browser at 1920×1080: Settings, the battle HUD and the equipment window show no clipped label. Gates: tools/l10n_gate.py PASS (381/381 translated, 0 missing); tools/probe_c22_l10n.py scenario B re-aimed by this ruling from “the empty scaffold reads English” to “the real file reads Portuguese”, red-first 14/15 on the filled file → 15/15.
2026-11-02 — Taskbar Mode has no pad button (rulings card T64, D “Keyboard only”, note “taskbar mode is desktop only”; dfmc-client lane padrevert. Dev build now; release with the next ritual and the next Steam shell build — D’s Steam Input upload takes the vdf without the hold.) The 2026-10-30 rec (View / Back held one second pressed T) is removed by D’s answer: the Steam Input config loses the hold and View / Back is a plain ESC again, the Deck glyph on the Taskbar Mode row is gone, and the row’s description no longer names a controller. Taskbar Mode opens and closes from the TASKBAR MODE tile and the T key, Steam shell only, exactly as before; no other pad binding changes. Gates: tools/probe_t22_shell.py re-aimed to pin the absence 12/14 → 14/14, tools/probe_steam_deck.py 8/9 → 9/9.
2026-11-02 — no text scaling: the phone / Steam Deck text-scale token is removed (rulings card T57, D: “no text scaling!”; dfmc-client lane txsrevert. Dev build now; release with the next ritual.) The 2026-10-30 entry below built T57’s rec while the card was open: a --txs token on the stage root, published by the resize handler, with the HUD, battle, inventory, equipment, codex, portal-label and AUTOPLAY fonts authored through it so a 10 px label grew to 9 css px on a Deck and the phone took a 1.5× cap. D answered the card “no text scaling!”, so it is gone: the resize handler publishes fitScale alone, the stage root carries no token, the equipment window no longer re-declares one, and every font-size is its authored stage px — the markup is byte-equal to the tree before the token (the five 9 / 9.5 px captions it floored to 10 are back at 9 / 9.5), the text fitter writes plain px and the battle plate fits its name into the full 300 px again. The stage scales as one piece: 1920×1080 at fitScale, so a 10 px label is 6.7 css px on a Deck and 3.6 on a landscape phone, as before 2026-10-30. Settings › Text Size keeps saying the whole screen scales with the window, which is again exactly true. Gate probe_t57_textscale is re-aimed to pin no scaling (no --txs anywhere, rendered px = authored px × stage scale at 1920×1080, 1280×800 and 844×390).
2026-11-02 — a fullscreen toggle in the HUD, right of the MENU plate (rulings card T55, D 2026-11-02: “need a button to leave fullscreen. needs to be a toggle, can't overlap anything else”; dfmc-client lane fsbutton. Dev build now; release with the next ritual.) Lane fs-toggle (2026-10-23, below) hid the floating fullscreen button in fullscreen because no corner or edge of a 16:9 stage is free of game UI, which left Esc and the Settings row as the only ways out. The toggle now lives inside the HUD: a 48px round button right of the MENU plate, in the band every window header already keeps clear for the MENU hud; that band grows by the slot (titles 204 → 258, the equipment ACCOUNT box 180 → 234, the arena and shop banners 190 → 244) so nothing can sit on it. It reads the screen's state (outward arrows to enter, inward to leave), drives the same door as the Settings row and Esc, stores nothing, and is absent where the browser has no Fullscreen API. Measured on the floor, the open menu, every window and a battle at 1920×1080, 1600×900, 1366×768 and 844×390, windowed and in fullscreen: nothing intersects it. The floating corner button remains only on the gate, the loader and the evolution screen, with its 2026-10-23 rules. Gate tools/probe_t55_fsbutton.py (28 checks).
2026-10-30 — phone and Steam Deck text scale (rulings card T57, open — the rec is built pending D’s answer; dfmc-client lane t57text. Dev build now; release with the next ritual.) The stage is a fixed 1920×1080 box zoomed to the screen, so a 10 px label drew at 6.7 css px on a Steam Deck (1280×800, 0.667× — under Valve’s 9 px Deck Verified floor) and 3.6 css px on a landscape phone (844×390, 0.36×). One text-scale token now rides with the stage fit: --txs on the stage root, published by the resize handler beside fitScale (the on-screen scale is the stage zoom × the viewport scale, so the iOS page-scale shell counts too). At or above 0.75× it is 1 and nothing changes (1920×1080 is untouched); below it, 0.9 / scale so a 10 px label lands at 9 css px (Deck 1.35×, a 1366×768 laptop 1.27×), capped at 1.5× on phones (1.75 was tried and the equipment window overflowed). The HUD chrome and battle UI, the floor’s portal labels and streak counter, the AUTOPLAY row, the inventory, equipment (with the shared item details), and the codex are authored through the token; the five 9 px captions in those regions are floored to 10 px. The text fitter keeps the token when it shrinks a value to fit. Not yet through the token (measured on the Deck): atlas 8.0 css px, settings 8.7, classes 5.7; the battle plate name (a 300 px plate holding name, level and type medals) is still sized down to ~8.1 on the Deck for long names, and on a phone the equipment window keeps 1× (its class-soul first-run caption has no free side once any text in it grows at 844×390, so the window is reported as unable to absorb the token there; the Deck gets the full 1.35×). D’s answer on T57 corrects the threshold, the cap or the approach.
2026-10-30 — Taskbar mode from a pad: hold View / Back one second (rulings card T64, open; the rec built pending D’s answer; dfmc-client lane t64pad. Dev build now; release with the next ritual, the next Steam shell build and D’s upload of the Steam Input config.) Taskbar Mode had only the T key; the shipped Steam Input config gave every other default hotkey a pad button and none pressed T, so a Steam Deck or controller could not open it. View / Back now presses T when held for one second (a short press still closes the top layer, as before); on a Deck the Taskbar Mode row in Settings › Controls reads “View (hold 1 s)”, and the row’s description names the hold on every build. Nothing in the game changes; D’s answer on the card corrects this if it picks another option. Gates: tools/probe_t22_shell.py 11/12 → 14/14.
2026-10-23 — no 404 on load for the profile avatar (dfmc-client lane probe-triage-5. Dev build now; release with the next ritual.) The profile card’s avatar <img src="{{ profAvatar }}"> was the one template-bound image left without loading="lazy", so every boot fetched the literal /{{ profAvatar }} URL (a 404) before the binding resolved. It now loads lazily like the scroll theatre’s images (39d4d05b). No visible change. Gates: tools/probe_tplsrc.py 1/2 → 2/2, tools/probe_review_laneab.py rule 6 red → 16/16.
2026-10-23 — Steam Deck: the on-screen keyboard, Deck buttons in Settings › Controls, and a re-sync after sleep (dfmc-client lane steam-tail. Dev build now; release with the next ritual and the next Steam shell build.) On a Steam Deck, tapping a text field (the character name, the feedback form) now opens Steam’s floating keyboard next to the field; before, the player had to press STEAM+X. Settings › Controls shows the Deck button that does each action (X inventory, Y equipment, L1 lock, R1 autoplay) instead of the keyboard key; a key the player rebound shows that key, because the pad still presses the default. Off a Deck and on the web nothing changes. When the Deck wakes from sleep, the game checks its connection once, as after a dropped connection: it saves what was pending, and a session that ended meanwhile goes back to the sign-in screen. It does not reload the save. Text size at the Deck’s 1280×800 was measured: the smallest labels on the main screen and in every window draw at about 7 px, below Valve’s 9 px, and that is filed with the phone text-size work (T57). Gate: tools/probe_steam_deck.py 2/9 → 9/9.
2026-10-23 — Taskbar mode in the Steam build (D 2026-10-22, BACKLOG § 3 Steam; dfmc-client lane taskbar. Dev build now; release with the next ritual and the next Steam shell build.) The Steam build can now shrink the game to a strip above the taskbar: the hero and the two creatures walk on a band of the floor’s art, meet the three enemies at the right edge, fight there with HP bars, floating damage and each move’s own effects, and walk into the portal on a floor up. A TASKBAR MODE menu tile and a Controls key (default T) open and close it; a click on the strip brings the game back. Nothing in the game changes: it keeps running in its own window, and the strip only draws it, at most 30 frames a second and not at all while the game window is open. The web build shows neither the tile nor the key. Gate tools/probe_taskbar.py 5/36 → 36/36.
2026-10-23 — phones: home-screen install, the rotate prompt from the first paint, the home bar (T36 D1 / D2 / D5, PREP_PERFORMANCE § 8.3; manager's call, reversible, on D's rulings form as a card; dfmc-client lane t36-fix. Dev build now; release with the next ritual.) “Add to Home Screen” on an iPhone opened an ordinary Safari tab whose toolbar took about an eighth of a landscape screen. The page now carries the iOS home-screen tags and, on a phone or tablet only, a web manifest (full screen, standalone as the fallback, landscape; Android locks it), so the installed game opens with no browser bars; a desktop browser and the Steam build see no manifest and offer no install. A phone held upright now sees the “turn your phone sideways” prompt from the first paint, over the loading screen (English until the game’s language loads), not only once the game has mounted. On an iPhone in landscape the notch and home-bar padding is now worked out in the right unit whichever way Safari reports it under the game’s page scale, so the bottom HUD and AUTOPLAY clear the home indicator. Gate tools/probe_t36_shell.py 5/16 -> 16/16. The keyboard and the rotation zoom (D3, D4) wait on D’s device steps.
2026-10-23 — sound on older iPhones: every clip also ships as AAC (B22, PREP_PERFORMANCE § 5; dfmc-client lane b22-t36. Dev build now; release with the next ritual.) Every sound and music bed shipped as Ogg Vorbis only, which Safari and iOS before 18.4 cannot play, so an older iPhone ran the game silent. Each of the 322 clips now has an AAC (.m4a) copy made from the same file by tools/make_audio_fallback.py, and a browser that cannot play Ogg but can play AAC loads that copy. Every other browser loads exactly what it did. Gate tools/probe_b22_audio.py 2/5 → 5/5.
2026-10-23 — the string table and the Language picker (C22, D Tower rulings T47-5; dfmc-client lane c22-l10n. Dev build now; release with the next ritual and data pin.) The client had no string table: every word was hard-coded English and Settings › Language was a static “More languages are coming”. Now the UI reads codex/strings_en.csv (346 keys: the markup screens and the Settings schema), a file per language with the same keys, and locales.csv; the Language row is a real control (Auto (device) + each language), stored in the account’s prefs. English and an empty Portuguese (Brazil) scaffold ship; an empty text reads English. Gate tools/l10n_gate.py in gates.sh (no new hard-coded markup text, every locale key in English, the client in sync with the CSV); probe tools/probe_c22_l10n.py 0/13 → 15/15. See “Language & the string table” above.
2026-10-23 — in fullscreen the floating fullscreen button hides; Settings › Display › Fullscreen enters and leaves (dfmc-client lane fs-toggle, after the screen-fixes-2 scan; a card on D's rulings form. Dev build now; release with the next ritual.) The bottom-right dock above still sat on game UI: at a 16:9 fullscreen (1920×1080, 1600×900) the stage fills the screen and no corner or edge of it is free. So in fullscreen, and on any viewport with under 48px of letterbox at both the top and the right, the button hides. Settings › Display gains a Fullscreen ON/OFF row — the way in and out on every device, the only one on a phone — and Esc still leaves. Letterboxed screens keep the button top-right, where it covers nothing. Gate tools/probe_fs_toggle.py (seven sizes, every window and a battle: zero collisions; the row enters and leaves) 6/22 → 22/22; tools/probe_b9_fsclose.py re-aimed to ‘hidden’, 14/15 → 15/15.
2026-10-23 — the fullscreen button no longer covers the window close × on a 16:9 screen (dfmc-client lane b9-info, screen review finding 1. Dev build now; release with the next ritual.) The shell's fullscreen toggle sits in the top-right letterbox around the 16:9 stage. On a 16:9 viewport — fullscreen on a 1920×1080 or 1600×900 screen — there is no letterbox, and the toggle sat on every window's close × and took its click. With less than 48px of letterbox at both the top and the right it now docks bottom-right; letterboxed screens (a browser window, a phone in landscape) keep it top-right. Gate tools/probe_b9_fsclose.py 3/15 → 15/15.
2026-10-23 — info buttons: every window has an (i) that says how it works (dfmc-client lane b9-info, ruling §9.12, INDEX §3 Opening B9. Dev build now; release with the next ritual.) A small (i) sits left of the close button on the floor map, the items bag, the soul bag, equipment, classes, breeding, codex, PvP arena, rankings, settings, the premium shop and the creature sheet, and under the STREAK box on the floor and in a fight. It opens one panel: a title, two or three sentences and a READ MORE link to the owning codex page. The words are codex/info.csv (Key = the window, Title, Text, Page) — nothing is written in the client; a window whose Key has no row shows no (i). The text never repeats an FTUE arrow line (unlocks.csv / opening.csv Line): the arrow says what to click once, the panel says what the screen is for. The screen-by-screen review behind it is dfmc-client docs/screen-review-2026-10.md (§9.11). Gate tools/probe_b9_info.py (14 windows at 1920×1080 and 844×390, plus the CSV checks) 2/30 → 30/30.
2026-10-23 — the fight HUD no longer covers the HP numbers on floors 1–5; the CLASSES window fits the screen (dfmc-client lane ui-polish-2, D FTUE playthrough 2026-10-23. Dev build now; release with the next ritual.) HUD: until autoplay opens (floor 6) the centred AUTOPLAY row carried the padlock chip “OPENS AT FLOOR 6” inline; that widened the row to ~490px of the 1920 stage, into the band of the bottom-row unit plates, so the chip covered the left plate’s HP number and the cog and version label sat on the right plate’s bar — at every screen size, since the stage scales. The chip now stacks above the row; the row itself is unchanged. Gate tools/probe_hudhp.py (floors 1/3/5 at 1920×1080, 1600×900, 844×390) 0/9 → 9/9. CLASSES: since the 250px class art the six cards stood in two rows of three, ~1150px in a window capped at 92% of the stage, and the grid scrolled inside the window. One row of six in a 1840px window, art still 250px; the stage/level line wraps the stage name under it. Gate tools/probe_clsfit.py 3/6 → 6/6 (1600×900, 844×390, 1920×1080).
2026-10-23 — the equipment window’s warm pass fetches the hero’s class art (dfmc-client 7907a8ad, lane ui-polish-1; img-load follow-up. Dev build now; release with the next ritual.) After the img-load work the hero’s class art was the equipment window’s one cold image: the warm pass read the party portraits from the units, whose hero slot can still carry the boot placeholder art when the pass runs. It now also asks the producer the window paints the hero with (CLS-<line>-<stage>). tools/probe_img_load.py DIAG: equipment pending [CLS-MAGE-3] → [] on the cold-warm, idle and hover opens.
2026-10-23 — no 404s on load for the scroll theatre’s images (dfmc-client 39d4d05b, lane ui-polish-1. Dev build now; release with the next ritual.) Every boot fetched the scroll theatre’s ring and seal images by their unfilled template names and got two 404s: the template is hidden before it is filled, which stops background images, but a plain image still loads while hidden. Those four images load lazily like every other bound image in the shell — never fetched in the hidden template, loaded at once when the theatre opens on screen. Gate tools/probe_tplsrc.py 0/2 → 2/2.
2026-10-23 — window art loads before and as the window opens (img-load (D 2026-10-22, BACKLOG §3 NEW “game loading, images slow after opening a UI”; dfmc-client lane img-load). Dev build now; release with the next ritual.) Measured per window, cold / warm / after idle, with the real art behind a CDN-shaped delay (tools/probe_img_load.py). Dev art ref: the dev build pinned its art to dfmc main HEAD, which moved 56 times in 14 days with no art change; every move renamed every creature, tile, class and floor-plate URL, so the browser cache missed on all of it and jsDelivr rebuilt a cold package, and each boot fetched the boot art twice. The ref is now the newest commit that touched codex/images, codex/thumbs or floors, remembered for 30 minutes so a boot starts on it. Release keeps DATA_PIN. Codex: card art is a lazy, async-decoded image, so opening the codex fetches the cards near the view (25 requests) instead of the whole catalogue (261-273). Warm before open: opening the menu warms the bag, equipment and pen art at once; hovering a plate warms that window (the codex its first screen, the floor map its zone); the warm now includes each window’s chrome (bag tier frames, move-row and status icons, socketed item art, codex medals and type icons), which every window still fetched on open before. Re-render: one commit per open (two for the shop and rankings), so no storm to cut. Not built: a service worker — jsDelivr @SHA art is already immutable for a year in the browser cache, and the misses were renamed URLs, not evictions.
2026-10-22 — the ADVENTURE LOG plate leaves the main menu (ADVLOG-OUT, client half (D, Tower rulings form card T31-2; dfmc-client lane advlog-client). LIVE on release since 2026-10-23 (0.89).) D: “Remove the icon from the menu.” The menu goes from CODEX straight to PVP ARENA; the log window it opened is retired (Adventure Log), so ESC has one layer fewer to close. A bug report’s RECENT ACTIVITY section, which read that log, is now NOTICES: the letter and the systems the climb has opened.
2026-10-18 — landscape on every handheld: the rotate veil covers tablets too, and names the phone’s rotation lock (Tower rulings form card T2). LIVE on release since 2026-10-23 (0.89). D: rotation should be locked; orientation always landscape. The veil asking for landscape matched phones only (a touch screen at most 600 px wide), so an iPad or an Android tablet held upright still drew the 16:9 stage letterboxed to a strip about two fifths of the screen tall — a portrait layout. Now every handheld held upright gets the veil: a coarse pointer with no hover, the same test the landscape lock uses, so touch-screen laptops, mouse-driven machines and a tall desktop window never see it. Its copy gains a second line, “If it does not turn, switch off your phone’s rotation lock”: an iPhone with Control Centre’s rotation lock on never turns, and “turn your phone sideways” alone was a dead end there. Nothing else in the client lays out by orientation (this is the one orientation rule; the stage reads the window only to scale evenly), and the lock on the first touch / on fullscreen is unchanged. A browser still cannot lock an iPhone: the veil is what iOS gets. Measured on eight viewports (phone and tablet portrait on Android and iOS, phone and tablet landscape, desktop 16:9 and a tall window) across the floor, the PvP window and the equipment window: 34/34.
2026-10-01 — one picture at every window shape, and the landscape lock on the first touch (Bug Run 0.89, Rulings board card portrait). LIVE on release since 2026-10-23 (0.89). D: lock it to horizontal always; where it cannot be locked, the game is always the same aspect ratio and does not resize its elements. Measured at eleven window shapes (desktop 16:9, 4:3, short, ultrawide and tall, tablet both ways, phone landscape on Android and iPhone, phone portrait): the stage was already one 16:9 box scaled evenly with bars, but three things inside it read the window. The PvP arena’s size unit followed the window height, so on a 600–768 px tall window its cards, crest and fonts were laid out up to a third smaller than at 1080; it is now the 1080 size everywhere. The monster picker’s minimum height was 84% of the window, so a tall window drew it taller than the stage and cut its top off; it is now 84% of the stage. The equipment item card, the soul hover card and the crystal hover were pinned to the window rather than the stage, so on a wide, tall or 4:3 window they sat away from their spot, out over the bars; the stage now holds them. Orientation: a handheld now asks for the landscape lock on its first touch (an installed Android app gets it with no button pressed), and fullscreen entered any way locks. iPhone Safari cannot lock; a phone held upright keeps the turn-sideways veil rather than a sideways-drawn game.
2026-10-01 — a hidden tab keeps playing and stops drawing (Bug Run 0.89, Rulings board card hiddentab). LIVE on release since 2026-10-23 (0.89). D: the game has to run in the background, it does not have to render there. Before, a hidden autoplaying tab re-drew the whole interface on every battle beat (about 2.3 full redraws a second) and ran the frame loop at ~24 frames a second off the background timer, for a screen nobody saw. Now, while the tab is hidden: state changes are applied at once, exactly as before, and the logic that follows each change (scene auto-advance, the sweeps, the victory stager, every callback) still runs in order, but nothing is redrawn; the floor creatures stop roaming and the units stop bobbing (pure visuals), while the autoplay engage step keeps its 2600 ms clock. Battles never waited on an animation - every step is a timer - so fights resolve on the same clock hidden as visible. On return one redraw shows the current state and the frame loop resumes. Measured on the seed save, 120 s of autoplay hidden vs visible: the same battle beats (271 vs 269) and the same wait between fights; redraws 2.26/s → 0, animation frames 24/s → 0, script time ~31 → ~3 ms per second. Network calls and save cadence are not touched by this change. Probe: tools/probe_bgrender.py.
2026-10-01 — no page zoom on a touch screen, and a phone held upright is asked to turn sideways (Bug Run 0.89 board Q9, PREP_PERFORMANCE § 4 steps 2 and 4, built blind). LIVE on release since 2026-10-23 (0.89). A two-finger pinch (and on an iPhone, a quick double tap) zoomed the whole page over a stage the game already scales itself: a zoom the game could not read, keep sharp or undo, and a full redraw of the scene (the zoom flicker). The full-screen wrapper around the stage now carries touch-action: pan-x pan-y - pinch and double-tap zoom are off, one-finger panning is kept, so every scrolling panel (inventory, codex, classes and the rest) still scrolls with a finger. iOS also cancels WebKit's own gesturestart / gesturechange. The old switches (user-scalable=no, maximum-scale) are not used: Safari has ignored them since iOS 10. A phone in portrait (a touch screen at most 600 px wide) now shows a veil asking for landscape instead of a stage a fifth of the screen tall: no web page can lock the orientation on an iPhone, and on Android the fullscreen button - which already locks landscape and stays tappable above the veil - is the one place a browser may. Tablets, desktop windows of any shape and mouse-driven machines never see it; a Capacitor build locks landscape natively. Verified in Chromium with emulated touch (tools/probe_iphone_touch.py, 11/23 red to 23/23). Still for D's phone: WebKit's own handling of the gesture and of the veil's media query. The trade, D's call (PREP_ZOOM_FLICKER § 6): a player can no longer pinch to read a small label; an in-game UI scale setting is the suggested replacement and is not built.
2026-10-01 — two hidden jobs stop: the 10-second save no longer redraws the screen, and the battle stage stops bobbing where nothing shows it (Bug Run 0.89 board Q13, the leftovers of the performance review). LIVE on release since 2026-10-23 (0.89). The local save heartbeat (every 10 s) ended by committing a new “saved at” time to the client’s state, which re-rendered the whole interface every 10 s for a readout no screen displays. The time is still recorded (the guard that stops two windows writing the same save, and the bug report’s save stamp, both read it) but no longer redraws anything: 1 full redraw per 10 s on an idle floor → 0. The battle stage’s six units stay mounted under the floor and the boss room, and the frame loop kept writing their hover bob there every frame. Measured first: moving them 40 px under either scene changes 0 screen pixels, so the bob now pauses on those two screens and resumes, in step, when the stage is the screen again. On the floor the hidden units’ style writes go from ~9 a second to 0; in the boss room, where they were the only thing asking for frames, the frame loop drops from 60 frames a second to idle (~2). Victory, defeat and advancement show the dimmed stage behind them and keep the bob. Nothing the player sees changes. Gate: tools/probe_perf_8.py, 8/12 before, 12/12 after.
2026-10-01 — the phone safe area: the stage keeps clear of the notch and the home bar (performance review chunk 7, PREP_PERFORMANCE § 4 step 1). LIVE on release since 2026-10-23 (0.89). The page did not ask for the full screen on a notched phone (no viewport-fit=cover) and nothing read the insets the browser reports for the notch and the home indicator, so the 1920x1080 stage was fitted to the whole window: Safari letterboxed the sides itself in landscape, but the home indicator always sat over the stage's bottom edge, and any edge-to-edge build (the Capacitor wrapper, Android with a display cutout) would have run under the notch. The shell now asks for the full screen, publishes the four insets as CSS variables (--sai-t/r/b/l), and the stage wrapper pads by them; the stage is fitted and centred in the safe box that remains, and the iOS stage fit picks Safari's page scale from the safe size, so the stage lands inside the padding at zoom 1 as before. The data-warnings toast offsets by the same insets. On a screen with no insets - every desktop, every Android browser in normal use - the numbers are unchanged to the pixel. Verified in Chromium with emulated iPhone 14 Pro insets at portrait 390x844 and landscape 844x390 (tools/probe_perf_7.py, 9/22 red to 22/22): the stage bbox inside the safe area on both, nothing clipped, the desktop control identical. Still for D's phone: how Safari expresses the insets under a page scale (the fit settles under either convention, but the padding on screen is exact only if env() scales with the page), and the board items of PREP § 4 steps 2-4 - pinch-zoom (touch-action), the transform:scale re-test, orientation.
2026-10-01 — the ticker diet: the frame loop runs only while something moves (performance review chunk 4, PREP_PERFORMANCE § 2). LIVE on release since 2026-10-23 (0.89). The client ran one requestAnimationFrame loop from first mount to unload, on every screen, and every frame called all eight of its callees: the two that animate (the unit bob, the floor roam) and six idle checks (the pens tick, the PvP poll, the save heartbeat, the boss-room auto-leave, the forge and egg sweeps, the hourly adventure-log round) that gate themselves on clocks of 400 ms and up and did nothing 59 frames in 60. The idle checks now ride a 100 ms interval, which keeps each one's own cadence exact, and the frame loop runs only while a frame is owed: the roam on an uncovered floor, or a bobbing unit on a scene that shows it. With an overlay over the floor (the bag, the PvP lobby, the pens, the menus) nothing on the scene moves — the roam was already frozen there, and no unit pixel moves behind the backdrop — so the loop stops after a quarter second and wakes on the commit that uncovers the floor. The hidden-tab behaviour is unchanged: timers are worker-backed, so the checks still run in the background, at 10 Hz instead of the 30 Hz frame stand-in. Nothing the player sees changes, and no cadence does: the pens still produce, the save still lands every 10 s, PvP still polls at the server's interval, the boss room still auto-enters on its 400 ms check. Gate tools/probe_perf_4.py: idle-check calls a second 60 to 10 on every screen; frames a second with the bag or the PvP lobby open 60 to 0, with the floor or a fight on screen 60 (they animate); the bag and lobby screens are pixel-identical.
2026-10-01 — floor creatures move by transform, not left/top (performance review chunk 1, PREP_PERFORMANCE § 3.1). LIVE on release since 2026-10-23 (0.89). The roaming creatures on a floor were placed by left/top percentages rewritten on every frame, by the frame painter and by the render alike, so the roam cost the browser a layout every frame (60 a second on the floor, 58 at a phone viewport). Both writers now place a creature with one transform (a 3D translate of stage pixels off the fixed 1920×1080 box), written only when it changed, so a moving creature is a compositor move with no layout and no repaint of its shadowed art. Nothing the player sees changes: the roam, the facing flip, the hover plate and the pre-combat zoom are as before, and the frozen scene is pixel-identical. Gate tools/probe_perf_1.py: layouts a second 60 to 0, frozen-scene diff 0 px.
2026-10-01 — no literal template requests at boot (review lane AB, dev 2a4e144). LIVE on release since 2026-10-23 (0.89). Every boot asked the server for 11 files named after unresolved bindings such as {{ ultPlate }} and got 404s. The template sits in the hidden x-dc host for a moment; hidden boxes fetch no backgrounds, but an eager <img src> still loads. The 14 bound <img> tags now carry loading="lazy", which a hidden image never fetches; on screen they load at once. Gate tools/probe_laneab_boot.py: 11 literal requests to 0.
2026-09-30 — the scene door, the fit frame and the autoplay timer (client review F305, F307, F308, lane P). ON DEV. ?scene= no longer runs the dev scene switcher on a release build (the switcher itself was already hidden there). Unmount now cancels the pending stage fit with cancelAnimationFrame (it was a frame id cleared as a timeout). Autoplay’s engage timer is disarmed whenever the floor is left, so coming back from a manual fight waits its 2.6 s again instead of engaging on the first tick. Gate tools/probe_review_lanep.mjs.
2026-09-30 — atlas keys and the battle unit hold (client review F342 / F343 / F345). ON DEV. The atlas boss-key tooltip asked for tier_NN.png and the frames ship as .webp, so the frame never loaded; floors 106-120 also asked for tier 11 and 12, which do not exist. It now reads tier_01..tier_10.webp, the Void Apex keys at T10 as items.csv has them, with the matching grade colour. An open den (ENTER DEN) now shows the pointer, as its gold button already implied. Holding a battle unit opens its sheet; the release lands on the sheet, and that used to leave a flag set that swallowed the next click on a unit after the sheet closed. Every new press now clears it.
2026-09-30 — review drift fixes: F318, F319. WIKI ONLY. Horizontal fitter: the floor is 11 px for words and 6 px for numbers since #519 (2026-08-23); the page gave 11.5/11 for all text. Vertical fitter: the loop tests the scale before stepping, so the real floor is 70% of authored size, not 72%. DESCRIPTIVE ONLY.
2026-09-30 — a Settings confirmation is its own ESC layer (client review F310). ON DEV. Reset Progress, Delete Account, Restore Defaults, Clear Wild and the sign-out notes open a dialog inside Settings. It was not an ESC layer and neither ESC, the window hotkeys nor the menu cleared it: ESC closed the whole Settings window, and the dialog came back on the next open. The dialog is now the layer above Settings - the first ESC closes it and leaves Settings open, as the one-layer-per-press rule says - and every other way out of Settings (its ESC close, a menu pick, E / I) clears it.
2026-09-28 (b) — a NEW filter in both bags (#1016). ON DEV. A player asked for “a filter to the inventory and soul bag for NEW”. The NEW badge (0.87, see Settings) marked fresh arrivals, but neither bag could show only them. The FILTERS window of each bag now ends with a NEW group: one New Only chip, counted like the others, that keeps only the records still wearing the badge (the same unseen uid set, bag.fresh). The soul bag and the item bag toggle it separately; it counts toward the FILTERS pill and CLEAR ALL clears it. Hovering a tile still clears its badge, but while New Only is on the tile stays in the list, so the grid never pulls a tile out from under the pointer; it leaves the list the next time the chip is toggled. Session state, like the other chips: nothing new is saved.
2026-09-28 — every overlay is one size at every resolution, and ignores-defense wears its own glyph (Q11, Q12, D). SHIPPED IN 0.87. Q11 finishes #985 (“all UI must be one size at every resolution”). The shell is one 1920×1080 stage under a single zoom; four overlays were still authored after it, in the unzoomed full-window wrapper — the devour theatre, the breeding toast, the dismantle results panel and the breeding message — so their sizes were screen pixels and they grew against the game as the window shrank (the breeding message was 28% of the game’s width at 1920×1080, 62% at 1067×487 and all of it on a phone). They now live inside the stage and scale with everything else. Four caps inside the stage were written in viewport units, which read the real window instead of the stage: the dismantle list, the mass-hatch list, the floor-101 letter and the skull-defeat popup. Each is now the stage size it meant at 1920×1080 (605px and 560px tall; the letter 1805px wide by 950px tall; the popup 1766px wide), so at 1067×487 the dismantle list is no longer a quarter of the game tall. Measured at 1920×1080, 1423×687, 1067×487 and 390×668: every one of the eight is the same fraction of the game at all four. Q12: the ignores-defense icon on the move selector and its hover window was a composite (#987: the raise-defense shield with an X drawn over it); it is now the approved fx_pierce glyph from move_icons.csv (“Defense-ignoring / true damage”), on the same debuff parchment as every other effect icon. The damage-over-time composite is unchanged.
2026-09-24 — the cues that were authored and never wired, and the bed that stopped restarting (D). ON DEV (0.86). Fourteen of the thirty-four event cues had a row in afx_events.csv and no call site at all. They have one now. A pick makes a sound: ui.select rings at the eight handlers that are a pick — the battle actor, the move, the ultimate, the target, an inventory card, a breeding parent, a forge recipe and the equipment / soul / socket picker’s card — each one after its own refusal guards, so a move on cooldown, an illegal target, a one-per-line clash or a level block stays silent, exactly as the highlight ring already refuses them. This build has no button component to hang one central pick on, so eight sites is the honest count. The one central ui.click fires off a capturing listener on the document, which runs before every one of those handlers, so a pick would have made two sounds; the click now waits a microtask and stands down when a select answers for the same press. Damage: a hit landing on one of your own creatures plays a quiet layer underneath the move’s own composition, timed to that composition’s impact so it sits inside the cast rather than in front of it — the move is never doubled. A generic hit sound exists only as a fallback for a move with no composition at all, which in this build is none of them: every authored stage and every boss’s kit resolves to one. A buff or debuff landing, and a bleed / poison / burn biting at the round tick, each have their own cue; the tick is judged on health lost and rings once however many creatures are bleeding. A gear drop is now told apart from an item drop. The breeding pens, the shell-breaking and the forge: an egg broken down for Ichor, a pen switched on, a pen switched off (a refused toggle stays silent) and a piece fed into another — which in the Tower means socketing a stone into worn gear or seating a creature’s soul, the only action that consumes one held piece into another; the upgrade window spends currency and eats nothing, so it has no feed sound. THERE IS NO HUB IN THE TOWER, so the four cues the mystery dungeon authored for its town are mapped onto the Tower’s own systems and renamed: devour at the Devour door, after the outcome is banked, so a devour that consumed nothing is silent; revive at the full restore every battle opens with, which is the one moment a fallen creature comes back, and only when one actually fell; and bank at the dismantle sweep, the one place you watch a pile of currency land in the wallet. The dungeon’s "enter the hub" cue is dropped — there is no screen to enter. "Mute in Background" was an authored Settings row that did nothing; a hidden tab now silences everything and a visible one restores whatever you asked for, so a muted session stays muted. THE BED IS THE ZONE, AND ONLY A ZONE CHANGE MOVES IT [R D 2026-09-24]: "every non-battle screen: those should keep playing the bgm from whichever zone the player is in, and not change the BGM. Only going to a different zone changes it. It shouldn’t restart between floors nor at the beginning of battle either, should just keep seamlessly looping until the player enters a different zone." This corrects the entry below, which said a hub window on top owns the bed and a boss floor takes the boss bed. It does not, and neither does anything else. That build resolved four states — a title bed before the first click, the hub bed whenever any window was open, the boss bed on a boss floor, the zone bed only inside an ordinary fight — so opening the menu, closing it, starting a fight, winning it and stepping up a floor each faded a different piece of music in: four restarts per floor without leaving the screen. Every one of those branches is gone. The bed is the zone bed of the floor you are on, folded two floors at a time across the Void Apex, and nothing about a screen, a phase or an open window can change it. The hub, boss and title beds are no longer asked for and their rows leave bgm.csv. Gate tools/probe_afxbank.mjs 59/80 before, 80/80 after.
2026-09-24 — the Tower has sound (D). ON DEV (0.85). This client had no audio at all: move_vfx.csv’s AFX_Archetype / AFX_Layer / AFX_Pitch columns had been read into the client’s move table since 2026-09-10 and nothing ever played them, and there was no AudioContext anywhere in the build. There is one player now, AFX_BANK, a sibling of the VFX bank’s block and built the same way — it depends on nothing else in the client, so the codex’s review page can lift it verbatim and hear exactly what the game hears. It reads three authored tables: afx_bank.csv (the 606-clip CC0 bank, shared verbatim with the mystery dungeon), afx_events.csv (one row per event cue) and bgm.csv (ten zone beds, one per zone, and no others). A move’s sound is a composition in move_vfx.csv’s AFX_S1/AFX_S2/AFX_S3, one per stage, in the grammar of AFX_SPEC.md; an ultimate carries AFX_S1 alone. THE STANDING AUDIO RULES ARE ENFORCED BY THE PARSER, NOT BY TASTE: a clip with a fifth of its energy above 3 kHz is refused unless it is pitched at least three semitones down, a clip that peaks above 0 dBFS is refused unless its gain is cut to 0.7, and the gains of layers landing inside one 60 ms window may not sum past 1.6 — what reaches the ear is the sum, not the loudest member. “Never use annoying high pitched sounds, deeper is better and more comfortable” [R D 2026-09-14] is a measurement here, and the bank carries the measurements. A move with no composition, or a file that does not load, plays NOTHING — there is no synthesised stand-in and never was. Low VFX, Battery Saver and Reduce Motion thin the picture and do NOT silence the sound: audio has its own mix. Twenty event cues are wired, each from one place: the button press, an overlay opening and closing, a refused action, a toast, the start of a fight, the win, the loss, a unit falling, a level-up, the advancement ceremony, a hatch, an egg laid, a craft that landed and one that did not, a drop, a purchase, the boss arriving and falling, and the ascent. THE BED BELONGS TO THE ZONE, NOT TO THE SCREEN [R D 2026-09-24]: every non-battle screen keeps playing the bed of whichever zone the player is in and does not change it; only crossing into a different zone changes it; it does not restart between floors, and it does not restart at the start of a battle. There is no hub bed, no boss bed and no title bed — the three ingested that morning were removed, with their files. The first bed still waits for the player’s first click, because no browser will start audio before one. VOID APEX REUSES THE ZONE BEDS TWO FLOORS AT A TIME [R D 2026-09-24]: 101-102 play the first zone, 119-120 the tenth. The zone ordering is read from floors.csv, so it is authored in one place and restated nowhere. Beds crossfade — 1200 ms in, 900 ms out — and a screen change that does not change the bed never restarts it. Gate tools/probe_afxbank.mjs, beside the VFX bank’s own: it lifts the player out of the shipped client, runs it against a Web Audio stub, and checks a 9-item legal and 27-item illegal corpus, the whole contract, the fold rule, and every authored composition and referenced file against the codex. 0/2 before, 46/46 after.
2026-09-17 — long numbers read in K above 9999 (#940, corrected by #941, D). ON DEV (0.84). #940: “for long damage numbers over 999, use K and round them to the nearest thousand. Same for stats.” #941, the correction that ships: “only use K when they go over 9999, not 999.” The root cause was an ABSENCE — this client had no long-number formatter at all. Around ninety call sites each stringify their own number, and the two D was looking at did it bare, a raw integer into the binding. There is one owned helper now, with three callers: the move card’s average-damage figure and the two stat grids (character and creature). A value of 10000 or more renders as K rounded to the nearest thousand — 10,499 reads 10K, 10,500 reads 11K — and anything below renders as it did. Four sites were looked at and DELIBERATELY left bare, each for its own reason: the battle log’s own damage line (generated by the shared engine the server also resolves Auto PvP with), the battle plate’s live hp/maxHp (a resource counter — folding it would freeze the readout at 12K/12K while HP ticks), the opening line-up sentence and its exportBattle() twin (prose diagnostic, shared with the text dump), and per-slot item stat contributions (an item’s stat, not a creature’s). Gate tools/probe_940.py 1/7 → 7/7, driven through the real producers and read back off the painted equipment screen because the helper is not reachable from the page.
2026-09-17 — a fourth loading tip: the Screen Shake toggle (#953, D; it retracts #951). ON DEV. #951 asked for “an option in the settings to turn those off or dial them down — a sectioned slider from 150% to 0%”; #953, three minutes later, withdrew it: “disregard the previous report about screen shake, the toggle already exists in the settings. add a loading screen tip.” No setting changes and no slider is built — Settings → Display → Screen Shake has always been a toggle, and the gap was that nobody knew. The pool gains “You can turn off screen shake in the display settings.” as a plain line with no icon (the tip’s icon slot takes a move-icon name and none of them means “settings”), drawn like the other three: one tip per loading screen, picked at random, stable for the life of that veil. Gate tools/probe_948.py checks that the new line draws on a real veil and that the other three are still in the pool, so a swap cannot pass as an add. Shipped alongside #948, which stops a VFX shake moving an open window — that one is on codex/VFX_SPEC.md.
2026-09-16 — a cold release pin can no longer halt the boot on a csv the CDN refuses (#913 / #914, two players eight seconds apart). ON DEV (0.81). “Could not load the game data (floors.csv). The game cannot start without it.” — filed automatically at 13:47:04 and 13:47:12 on 2026-09-14, by different players, both on 0.80. Not their connection: 0.80 had moved DATA_PIN to a cold sha two minutes earlier (13:45:15), and jsDelivr refuses cold files on this 3.2 GB repository — 403 “Package size exceeded the configured limit of 50 MB”, and 404 for a file that exists at the sha. The 2026-09-14 (15) incident measured the same refusal on creature art and reverted the pin at 14:20; these two reports are the csv face of that same window (bosses.csv 403, dismantle.csv 404, floors.csv 403, all present at the sha, all 200 there today). The client does not retry a 4xx by design — a file missing at an older pin must fail fast — and floors.csv is fatal, so the boot halted. Now, when the CDN refuses, the loader asks raw.githubusercontent.com once at the same immutable sha before declaring the file lost; the CDN stays the first origin, raw is the second chance for the window between a pin move and the CDN warming it. Second half: the halt’s automatic report fired before the deferred warning naming the file and its HTTP status had landed, so #913’s log has no floors.csv line at all; it now waits a tick and carries it. Gate tools/probe_csvfallback.py 2/4 → 4/4, red-first. The durable fix is server-side — the worker already carries the pinned csv baked into its bundle, and serving them to the client would end the boot’s dependence on a third party being warm; that is D’s call, not made here.
2026-09-14 (c) — the incense and bait counters clear the MENU plate (#843, D). LIVE on release since 2026-09-14 (0.80). The MENU plate’s art fills its file edge to edge and is drawn 64px tall from top:1, so it PAINTS to y=65, and the boon column began at top:62. Three pixels — but the menu HUD is z-index 110 against the column’s 105, so the plate both covered the incense tile’s top band AND took the click in it: pressing the top of the counter opened the menu. The column starts at 78 now, 13px of daylight. The lesson is the gate’s, not the fix’s — tools/probe_boonclear.py asserts on elementFromPoint, because a pixel-only check calls this cosmetic.
2026-09-14 (b) — a currency that ticks no longer moves its neighbours (#845 / #893 / #849, D). LIVE on release since 2026-09-14 (0.80). Every currency readout in the game now draws its number in a FIXED slot with the font as the thing that gives: 72px at 17px in the bag, 58px at 14px in the equipment header, 110px at 26px in the breeding pens. The widths are measured, not chosen — the widest digit in this font is 11px at 17px and a separator is 4px, so the widest six-digit string is 70px. Past six digits the font scales by 7/length, which is a proof of fit rather than a guess. One producer hands out the string AND its size, so a new surface cannot take the number without the rule that sizes it.
2026-09-14 — the loading tip’s egg icon was missing at the release pin, not stale (#897, D). LIVE on release since 2026-09-14 (0.80). The tip asked the codex for fx_egg_grade.webp; that file landed in this repo on 2026-09-11 and the release DATA_PIN was from 2026-09-10, so the CDN answered 404 on release while dev — which reads main — was perfect. No client change: the pin advanced with this release and the icon is there. The durable half is tools/check_pinned_art.py, now wired into release_gate: it fails on art missing everywhere and names PIN DEBT (present on main, absent at the pin) so a release cannot ship a hole again.
2026-09-07 (c) — ESC closes the window on top, one layer at a time, and backs out of a target pick in a fight (board 9.13). LIVE on release since 2026-09-08 (0.71).
2026-09-07 (b) — iOS touch feedback off for the game (board 10.1): tap highlight, long-press callout and selection highlight no longer paint over what you tap; text fields stay selectable. LIVE on release since 2026-09-08 (0.71). Gate tools/probe_iostap.py 9/9 in WebKit-as-iPhone, red 5/9 before.
2026-09-07 — iOS stage fit: the warped iPhone UI (D’s equipment-window screenshot) was WebKit’s 9 px minimum font size under the stage zoom, not text autosizing; the shell now widens the viewport so the stage sits at zoom 1 on iOS, and the text fitter can no longer compound a browser multiplier. LIVE on release since 2026-09-08 (0.71). Gate tools/probe_iosstage.py 14/14 in a real WebKit engine emulating an iPhone.
2026-09-02 (e) — the main menu’s buttons are generated plates (#688). LIVE on release since 2026-09-02 (0.67). Fourteen buttons, one master frame: a blank gold-filigree plate on crimson enamel was generated once and every button derived from it, so they are the same size and frame by construction; each carries an icon for what it is left of the label (a tower for GAME, map and compass, chest, soul pouch, helm and blades, egg, anvil, tome, quill and journal, crossed blades on a laurel, gears, merchant’s scale, a pinned beetle, a heraldic scroll) with a faint themed engraving behind the text. The plate is the button’s whole face; the rim glow hugs the plate’s silhouette on hover and marks the active item. Masters and prompts in the wiki repo’s parking; the game ships 850×344 derivatives. Gate tools/probe_menuplates.py 0/5 → 5/5.
2026-09-02 (d) — menu buttons highlight their rim only on hover. LIVE on release since 2026-09-02 (0.67). D’s ruling with the button-art order: the hover used to flood the tile’s fill; now only the rim lights — the border goes gold with a thin ring and a soft glow hugging the edge, the fill and the text stay as they are. The same rim effect will carry the generated button plates when they land. Gate tools/probe_menurim.py 1/3 → 3/3.
2026-09-02 (c) — the main menu slims and the report window’s buttons follow the report. LIVE on release since 2026-09-02 (0.67). Player reports #681 and #680. AUTOPLAY OPTIONS left the main menu — the window keeps its own opener where it belongs, the gear beside the autoplay toggle on the AUTO panel. And the bug-report window’s CANCEL is now wider (a fixed 28% of the row, taken from SEND REPORT), red, and centred. Gate tools/probe_reportui.py 2/5 before, 5/5 after.
2026-09-02 (b) — one filter surface on the item bag: the inline chip rows retire into the FILTERS window. LIVE on release since 2026-09-02 (0.67). D’s report (screenshot from release): the FILTERS button had been added to the inventory while the old filter rows stayed. History: when the filters window shipped, the soul bag’s rows collapsed into it and the item bag deliberately kept its chip rows; the button then went both-bags, so the item side showed both surfaces at once. Ruling: one surface. The KIND and GRADE chips now live only in the filters window on both bags — same chips, same counts, one CLEAR ALL — and the old rows left the markup together with the props that fed them. Gate tools/probe_invfilters.py red-first (the first collector falsely passed by excluding every button behind the inventory window’s own shadow — rewritten to match the filters panel’s unique shadow), then 6/6: button present, no inline chips, picking a kind in the window narrows the grid to the chip’s own count, CLEAR ALL restores, soul bag unchanged. The soul-filter gate’s #538-era control (“item rows inline, button soul-only”) encoded the old ruling and had silently drifted; rewritten to the new one, 11/11.
2026-09-02 — three taps open a battle unit’s sheet (backlog 9.9, a player ask). LIVE on release since 2026-09-02 (0.67). Documented in the section above. Gate tools/probe_tripletap.py 5/8 before, 8/8 after, driven with a mouse at 1920×1080 and with real taps in a touch-emulating browser context.
2026-08-28 — the inventory gets the soul bag's condensed filter window (#597). On release. The item side already filtered by kind and grade — what it lacked was somewhere to put the chips, because the FILTERS button and the window it opens were both gated to the soul bag. Both bags now share one button, one window, one count and one CLEAR ALL, so a button and its window cannot drift apart. Items get KIND and GRADE; TYPE and ABILITY stay soul-only, since items have neither.