# DOM web backend The web backend keeps the existing Pardes core and replaces the old Emscripten/SDL/WebGL shell. Zig produces a `wasm32-freestanding` module with no imports. Vanilla JavaScript owns `requestAnimationFrame`, browser input, window sizing, and effect/IO dispatch; HTML and CSS render selectable, accessible text. There is no canvas. The default web build links Tree-sitter's C runtime and the Zig grammar inside that same import-free module. A small freestanding compatibility layer supplies the C ABI locally, so source panes retain the core's Tree-sitter highlighting without Emscripten, WASI, or browser libc imports. Other grammar tiers remain selectable with `-Dtree-sitter=minimal` or `-Dtree-sitter=full`. `src/web.zig` is the narrow WASM ABI. Events enter through exported functions, and `pardes_frame` writes the canonical `Surface` into a packed 20-byte cell array. `src/web/app.mjs` reads that array in one linear pass and patches stable DOM nodes. Core effects become browser operations (links, downloads, clipboard) or `pardes-io` custom events for a process-capable embedding host. Cell attribute bit 7 carries the core's tagline font role. The DOM keeps every cell at the body grid's fixed width and height, but scales and centres the tagline glyph using the build-time `gui_tagline_font_percent`; the percentage is exported by the module rather than duplicated in JavaScript. Animation time is also host-independent: `requestAnimationFrame` time is accumulated into 60 Hz core ticks, so 120/144 Hz displays do not accelerate frame-count transitions and a returning background tab has bounded catch-up work. The `web` platform is deliberately not one of the shader-capable native GUI shells. It exposes no `PanelSlide`/`PanelZoom`/`PanelDissolve`/`PanelAscii`/ `PanelVertical` or `Crt`/`Ripple`/`Glitch` builtins: applying those faithfully would require a second canvas renderer and give up the DOM renderer's selectable/accessibility contract. Theme fades and the delayed, side-effect-free Look hover remain grid animations and continue to use the fixed 60 Hz ticks above. Build a replay from a dump: ```sh zig build web \ -Dplatform=web \ -Dtarget=wasm32-freestanding \ -Dtree-sitter=zig \ -Ddump=test/web-snapshots/source-list.dump.zon ``` The output is in `zig-out/web`. Serve that directory over HTTP; browsers do not allow a useful WASM module load from `file:` URLs. The browser shell has no argv: every command-line flag `src/main.zig` parses — `--tty`, `--tty-toggle`, `-n`, `-l`, `--nested`, `-h`, and the positional file-or-directory — is native-only, because the wasm module roots at `src/web.zig` and never links `main.zig`. State comes from the embedded dump instead. Web LOOK's source archive follows Git's working-tree view: tracked `.zig` files plus new, nonignored ones, sorted by path. The checked-in browser replay is a real native TTY dump of that same selection; its top and bottom launcher snapshots jointly cover the complete list before opening `build.zig`. What is genuinely absent is narrower than "no IO". The module has no threads and no host filesystem, so there is no startup config directory, init file, ThemeFile, or DumpThemes; LOOK resolves against the build-generated source archive rather than disk, and no language BACKEND is compiled in (`zls_backend` is off for wasm — which also means `lsp.supports` is empty, so the core never even raises a language query there; `web.zig`'s prong for it is waiting for a host that links one). Replay terminals retain the same core shape but use Zig's failing IO value; this keeps the module freestanding without instantiating POSIX threaded IO that the browser can never call. The EFFECTS themselves are all still emitted, each as a numbered code across the ABI: `spawn` 1, `write` 2, `resize_pty` 3, `open_link` 4, `save_file` 5, `write_dump` 6, `set_clipboard` 7, `lsp` 8, `watch` 9, `quit` 10, `new_file` 11, `pipe` 12, `read_clipboard` 13. Inbound, `pardes_output`, `pardes_set_cwd`, `pardes_eof`, `pardes_paste` and `pardes_lsp_response` exist to answer them. A plain page ignores most of those and gets a replay viewer; a process-capable host (the `pardes-io` events above) answers them and gets terminals. The core does not know the difference — that is the point of the seam. Two dependency-free test layers cover the boundary: ```sh # Real WASM in Node plus a small DOM double. zig build web-harness \ -Dplatform=web \ -Dtarget=wasm32-freestanding \ -Dtree-sitter=zig \ -Ddump=test/web-snapshots/source-list.dump.zon # Real headless Chrome, DOM rendering, and browser touch input. zig build web-e2e ``` Set `PARDES_CHROME` when Chrome/Chromium is not on `PATH` or in a normal Playwright/Puppeteer cache.