#set page(margin: 2.2cm) #set text(font: "New Computer Modern", size: 10.5pt) #set par(justify: true) #show heading: set block(above: 1.4em, below: 0.8em) #align(center)[ #text(17pt)[*Pardes: A Text Environment*] #text(11pt)[design for the rewrite — 2026-07-05] ] = What it is Pardes is a text environment in the acme tradition: columns of panes, each pane a tag line plus a body; the body is a live terminal, a file, an image, or a PDF. The mouse carries meaning — left selects, middle executes, right looks. Everything on screen is text and all text is equally alive, whether the shell printed it or the user typed it. It runs in four places: the terminal (libvaxis), a native SDL3 window (the steamdeck), the browser (a freestanding wasm core driven by vanilla JavaScript and rendered as HTML/CSS), and a native macOS app (an AppKit and CoreText shell over a static `libpardes.a`). The rewrite exists because the prototype grew three parallel implementations of one program. The rewrite has exactly one program and four thin shells. = The shape Pardes is a *library*, in the way ghostty-vt is a library: you feed it bytes and events, and you read state out of it. Every platform owns its own event loop and its own renderer; the core owns everything the user would recognize as pardes. #table( columns: (auto, 1fr), stroke: 0.4pt, [*core*], [layout, panes, modes, selection, click semantics, themes, text of the UI. Pure state machine: `update(event)` mutates, `render(arena)` returns the surface. No rendering, no event loop, and no IO that has an effect for it — but "no syscalls" was never true and is less true now: `look` reads a file a Look opened, walks a directory for Find and reads every candidate for Grep, `fonts` walks the font directories, and MuPDF opens a `.pdf`. Those are the ones that are cheaper done in place than round-tripped through an effect and back; everything with a lifetime — a pty, a window, the clipboard — is still asked for.], [*shell*], [one MODULE per platform (tty is one file; gui adds the CRT and gamepad files, a C font loader and eight GLSL shaders; web and macOS each add a host language). Owns the event loop, translates native input into core events, renders the core's surface, and performs the core's requested effects (spawn a shell, write a pty, open a link).], ) Native shells share `shell_bin`'s OSC 133 startup snippets, but not their files. Each host owns a private `mkstemp` pair for its lifetime, writes and closes both before the first fork, passes those unpredictable paths directly in child argv, and unlinks them at teardown. Concurrent tty, SDL and macOS launches therefore cannot truncate, source, or replace one another's startup files. This also collapses the prototype's *two* applications into one: the browser build today is a separate 800-line read-only replay viewer. In the rewrite, loading a dump of another instance is a first-class feature of the one application, the way acme handles dumps: `pardes -l state.zon` on every platform reconstructs the panes—terminals by replaying their raw VT streams into fresh emulators, files, images and PDFs from their bytes. Editable tag tails are stored separately from their dynamic live prefixes, and image records retain PETSCII, palette, and ASCII renderer choices. A PDF rides the dump's `image` kind, carrying its path, its bytes and the page it was on. The web shell embeds a dump and leaves `spawn` unanswered. The reader validates weights, scroll ranges, pane references, and bounded tag tails before constructing anything; invalid base64 fails instead of silently becoming empty content. A PDF record whose path cannot be opened—or a build without MuPDF—falls back to an ordinary file pane with those exact embedded bytes and its original editable tail. Same core, no viewer fork. The boundary is two data types, both plain values: *Event* (in): `key` (which carries typed `text`), `mouse` (press/release/motion/ drag; button left, middle, right and the four wheel directions; cell position; and the `ctrl` flag, because Ctrl-left-click is goto-definition), `pinch`, `touch_scroll`, `resize` (cols, rows, and — in a MuPDF build — the cell's pixel size), `paste`, `pdf_scroll`, `output` (bytes a pty produced, tagged with the pane id), `eof`, `command` (a builtin line arriving from another process over the nested socket), `tick`, and the answers to an ask: `lsp_resp`, `pipe_resp`, `file_changed`. Touch policy belongs to the shell: web maps a one-finger tap to right-button LOOK and a drag past its tap slop to natural scrolling; native SDL keeps its two-finger gestures. A joystick cursor is a motion plus buttons. The shells do this translation and nothing else with input: one event vocabulary, four dialects translated at the door. *Surface* (out): `cols`, `rows`, a grid of cells — grapheme, fg, bg, attrs — a cursor, pixel attachments, and a bounded list of plain panel-transition tracks. This is the *canonical interface*: it is literally what the tty shell hands to vaxis, cell by cell. SDL rasterizes the same grid through a glyph atlas; the browser reads a packed copy and patches native DOM cells. If it cannot be expressed in the surface, it does not exist in pardes. (Pixel images ride along as a list of PLACEMENTS rather than one per pane — a PDF pane contributes every page its viewport intersects, and pages can be arbitrarily short. The SDL shells blit RGBA, the tty shell falls back to the petscii matcher, kitty graphics when available. Transition tracks identify their pane by slot and serial and carry only phase, effect, frame, and from/to cell boxes. Canonical layout is committed immediately; tracks are finite presentation data.) *Effect* (out, queued): `spawn{pane, cwd}`, `write{pane, bytes}`, `resize_pty{pane, cols, rows}`, `open_link`, `save_file{pane}`, `save_text{pane, serial, path}`, `write_dump`, `set_clipboard`, `read_clipboard`, `lsp`, `pipe`, `watch`, `quit`. The core never performs IO for any of these; it asks — and four of the asks have an answer coming back: `read_clipboard` returns an ordinary `paste` event (or nothing at all when the shell cannot read the clipboard — most terminals refuse the OSC 52 read), `lsp` returns `lsp_resp`, `pipe` returns `pipe_resp`, and `watch` returns `file_changed` whenever the shell notices a text file or PDF moved under it. The tty and SDL hosts currently implement that path with Linux inotify; the native macOS host watches both each file and its parent directory with debounced DispatchSources, then restats the exact path under a pane-generation guard. The file catches in-place writes while the parent follows rename-over saves. Text snapshots are filtered by their content hash; PDFs use bounded inode/size/time identity and commit it only when equal stats bracket a successful transactional MuPDF reopen. A mismatched transaction gets one bounded self-retry. This catches rename-over saves without reading a large PDF merely to notice it changed or spinning on a malformed one. Web has no filesystem watcher. A PDF response retains its reading position and pane settings. This is what makes the browser build honest instead of a fork: a wasm shell simply answers `spawn` differently (or not at all) — the core does not know. Platform divergence inside the core is a comptime tag, used the way the stdlib uses `os.tag`: ```zig pub const Platform = enum { tty, gui, web, macos }; // in look.zig: if (platform == .web and target.kind == .url) return .{ .open_link = target.text }; ``` There is one such file by design: `look.zig` holds path/`:line` resolution, URL detection, and the per-platform outcomes. The core's one `pointerOperand` primitive owns click-word expansion and is shared verbatim by right-click and the delayed hover preview; that policy stays beside input because it also observes live pane selections and wrapped grid coordinates. Pane implementations are similarly flat and direct: `file_pane.zig`, `term_pane.zig`, `image_pane.zig`, `output_pane.zig`, and `pdf_pane.zig` own their kind-specific storage and operations. `pardes.zig` keeps the layout, input dispatch, cross-pane invariants, and the small calls joining those modules. There is no pane vtable or callback layer; the kind is already plain data, so a direct switch/call is the shortest boundary. The large end-to-end PDF cases live in `pdf_pane_integration_test.zig`, keeping pane-specific fixtures and raster assertions out of that core file as well. = Data structures The whole state is one struct, fixed-size where it can be. Panes themselves are heap-allocated on demand and their contents (file bytes, the yank register, PDF rasters, tree-sitter state) grow with what you open; everything else is sized at init. User-settable runtime choices are one plain `runtime_config.State`: booleans, theme index, owned bounded shell/font strings, requested/effective font facts, one panel-transition enum, and scene-effect booleans. A compile-time `settings` array generates each setting builtin and the rows of the single `Config` query. It has no callbacks and no parallel query registry to drift from it. Horizontal layout weights are fixed-point integers and geometry rounds cumulative boundaries. Splitting a column replaces only its weight `W` by `A+B=W` at the same position. Therefore every boundary outside the source column is bit-identical before and after the split, including at awkward non-dyadic screen widths; only the source and new column can receive movement tracks. Vertical splits apply the corresponding rule to the source pane's weight. = Presentation animation `panel_animation.zig` is backend-neutral data and math: five transitions, their easing, exact endpoint progress, stable per-cell noise, and a POD track. The core detects opening/moving rectangles when it commits layout and publishes only active tracks. It retains the last successfully presented canonical grid and a typed old/new cell diff; unused grapheme bytes do not manufacture a change. A separate dense closing-track list is presentation-only state for a pane whose functional lifetime has already ended. Pointer input inverts the presented slide/zoom/vertical rectangle back to the canonical grid, lets unchanged dissolve/ASCII cells through immediately, and rejects closing pixels, so pixels and gestures cannot disagree during a transition. Pardes core first composes each ASCII-qualified diff by incrementing or decrementing its printable byte. Short walks move once per frame; longer walks use integer ease-out skips and finish within twelve movements. Style-only and non-ASCII changes pass through, and every backend receives the same presentation cells. TTY copies that `Surface` grid into a compositor scratch grid, clears slide/zoom destinations, then paints moving, opening, and closing panels in order. Cleared geometry uses the theme page color when it is explicit and the host terminal default only for transparent themes, so a light theme cannot flash a dark gap. Slide/zoom change the copied rectangle. Dissolve changes only diff cells from their old value to their new value; vertical raises only an opening or frozen closing pane inside its own clip. SDL supplies old/new glyph data, diff flags, final/presented boxes, and effect parameters to the glyph and native-image shaders. Pixel attachments bypass ASCII because they have no character byte. macOS passes the same records across its plain C ABI and composites old/new panel images in Metal/Core Image. Scene `Crt`, `Ripple`, and `Glitch` bits share one full-window pass in each native GUI. DOM web is a separate platform, not a shader GUI: retaining selectable HTML/CSS is more important than duplicating the renderer in canvas, so it exposes neither effect family. `EffectCode ` writes the actual backend math, host submission, and shader/grid source segments embedded by the build into an ordinary output pane. This makes the implementation inspectable after installation and makes sharing explicit: several builtins can quite honestly print the same shader with different uniform bits. ```zig Pardes ncol + col_weight[6], col_terms[6][16], col_n[6] // columns as flat arrays panes: [16]?*Pane // slot array; id = index active: usize drag: Drag // none | select | move | border_v | border_h | tag settings: runtime_config.State // theme, shell/font, display/effect choices rects: [16]Rect // where each pane landed, this frame panel_tracks: [16]?Track // live panes, serial-guarded closing_panel_tracks: [16]Track // dense visual tombstones, no pane owner presented_cells + panel_cell_diffs // acknowledged baseline + semantic diff effects: [4096]Effect + head/len // a fixed ring Pane tag: TagLine // live prefix (cwd/path) + editable tail vt, stream: ghostty-vt Terminal and its stream — on EVERY pane, not just terminals, which is what lets the same keys and the same parity suite drive a file and a shell file: ?file_pane.State / image: ?image_pane.State / pdf: ?pdf_pane.State // payload presence is the kind; none = terminal vweight: f32 mode: enum { normal, insert, tty } // helix-modal; tty = raw to the pty // `v` adds a fourth thing to DISPLAY, "select", // which is a bool on top of normal, not a mode cursor: absolute body position // rides the scrollback, not the screen sel: [3]Sel + msel/vsel + sels[63] // per-button block sels, the line and // char modal ones, and up to 64 cursors ovl: ?term_pane.EditBuffer // ONE typed run, anchored to an absolute row undo: two stacks, not one — term_pane.Snapshot history for an edit buffer, // and file_pane.State history for file content file_pane.State = path + bytes + line index + Syn (tree-sitter highlight bytes) image_pane.State = decoded RGBA + petscii grid cache pdf_pane.State = MuPDF document + continuous layout + search/selection/outline + bounded per-page raster relay + frame placement decisions ``` Layout is arithmetic, not objects: columns are weights over the width, panes are weights over the column. `splitBelow` shrinks only the source pane; `absorbVWeight` gives a dying pane's weight to one sibling; an emptied column hands its width to a neighbor. Minimal motion is the invariant: an operation on one pane may not move panes it does not touch. = Style TigerStyle, plus house rules proven in the prototype: imperative and flat; one big `update` dispatch, not handler objects; no one-line helpers — inline the four-line scan; assert invariants at entry (`assert(vsum > 0)`); static allocation at init, arenas per frame. The rewrite used source size as a pressure toward direct code, and that remains useful when a refactor deletes duplicate policy or state. A checked-in line-count inventory does not: it goes stale whenever a pane kind, backend, or generated asset moves. Measure the current tree when making that comparison; keep this document about ownership and invariants that should survive the next edit. Two things that number is not. It is not one program's worth of growth — the macOS and web shells and the PDF and language work are four products sharing a core. And it is not licence: the rule that survives is the local one, that a change should leave the file it touches no longer than it found it. = Testing: the old program is the oracle Before the rewrite compiled, the prototype got a harness (`snapshot.zig`, the `zig build snap` step): it forks either binary in a pty, feeds it an *event script* (one line per input: keys, SGR mouse, resizes, sync points), and captures the rendered grid — text, cursor, and per-cell style runs — through its own ghostty terminal. Goldens are generated from the old binary (`zig build snap -- --update`); the new binary must reproduce them byte for byte (`pardes-snap pardes/zig-out/bin/pardes`). Eighteen scripts covered the checklist in Appendix A at the rewrite; ninety-five cover it and everything since. All pass. Determinism pins: fixed workdir paths (they appear in tags), a controlled `$HOME` with `PS1='$ '`, `LC_ALL=C`, and a grid-stability sync primitive instead of timing guesses. Two of those pins were doing damage rather than work. A capture used to restate the whole screen, so 82% of golden lines were a copy of the line above, every golden carried the topbar and a tag row, and adding one builtin word to a tagline rewrote 78 of them — 3,758 lines of diff for a change no test was about. A capture is now a *delta* against the previous capture of the same kind in the same script: the first is the whole screen, the rest are only the rows that changed. The corpus went from 16,700 lines to 5,722 and the same one-word edit now moves 337. A capture whose only change is the cursor is the empty delta its script always meant. And a click used to name a screen column, which is a coordinate into that same chrome. When `Newtty`, `Joincol` and `Changelog` were added, seven scripts began clicking the word next door — `tutor.snap` clicked `Grep`, `exec.snap` clicked `Newtty` where it meant `Del`, `tagbottomimage.snap` clicked blank space 176 columns from the `Del` whose effect it asserted — and `--update` blessed the result, leaving 256 golden lines green while asserting the opposite of their own first line. A click may now name the word (`press middle @Del 2`, `@Del#2` for the second pane on a row, `@Save-2` for a column beside one), so the word is either there to be clicked or the script fails. Regeneration was the other half of that failure: `--update` captured once, serially, and wrote whatever it saw, which is how six wrong clicks became goldens. It now captures in parallel at the widest probe settings the harness has and then runs the ordinary verify pass over what it wrote, so a capture that does not reproduce is reported instead of committed (77s to 41s, and the retry machinery that serial update never had). Three deliberate deviations surfaced by the oracle, kept after review: the greeting `ls` waits for the exact OSC 133 B input mark after the real resize (the prototype raced bash's startup and won only by allocator luck); shells without prompt integration omit that cosmetic greeting rather than guessing. Commands which create a fresh shell are owned by its terminal pane until the host reports the actual prompt capability, then wait for the same mark when it exists. Dump files compare with base64 pty history elided (it encodes prompt-redraw micro-timing, not state — the cleaned text fields are the contract); and typed insert runs don't survive a dump replay (they are an overlay, not pty bytes — the prototype's replay viewer had the same semantics). Shell correctness is no longer out of scope, and that is the biggest change to this section since the rewrite. `web-snap` / `web-e2e` drive headless Chromium over CDP with real DOM pointer and touch events and diff `test/web-snapshots/`; `image-harness` and `pdf-harness` snapshot native PIXEL output through kitty graphics and SDL; `macos-e2e` is an offscreen AppKit snapshot suite over its own seven scripts. What remains untested by a snapshot is the last hop — that vaxis diffs correctly onto a real terminal — plus each shell's inline unit tests (the FreeType atlas raster, the trackpad and rotation maths, the gamepad replay). = Build Every `zig build` builds ONE platform: `platform` is an option defaulting to `.tty`, so a bare `zig build` gives `pardes` (vaxis) and the other three are separate invocations — `-Dplatform=gui` for `pardes-gui` (SDL3), `-Dplatform=web -Dtarget=wasm32-freestanding -Ddump=` for `pardes.wasm` plus a vanilla DOM shell (any other spelling of that one fails on purpose rather than building something silently wrong), and `-Dplatform=macos` plus the `macos-app` step for `pardes.app` wrapped around a static `libpardes.a` on a Darwin host. Native dependencies are ghostty, vaxis, uucode (shared config), zstbi, SDL (a pinned fork, lazy), FreeType, MuPDF (`-Dmupdf`, on by default everywhere but the web, lazy), ZLS, mvzr (the regex engine behind `s`/`S`), and zig-tree-sitter + 29 grammars for 28 languages (markdown takes two: block and inline) — all pinned through `zig fetch` and wired in `build.zig`. Generated during the build: `highlights.scm` → an options module; the vendored helix/zed theme sources → the generated half of the theme ring; working-tree `.zig` sources → the web shell's read-only archive; eight GLSL shaders → SPIR-V. That last one is the only build input wanting a tool a stock machine lacks (`glslc`), so it is also the only one whose output is committed: `zig build shaders` refreshes each paired `shaders/prebuilt/*.spv` binary and `.glsl` source snapshot together. `-Dprebuilt-shaders` embeds that exact pair rather than shelling out, so `EffectCode` cannot describe different shader text from the binary on screen; this is also what lets a gui build need nothing but a C toolchain. The tutor and the embedded font are plain `@embedFile`s, not codegen. Debug builds are incremental for the seconds-loop; release builds are the product. = What is deliberately absent No render abstraction over the shells (the surface *is* the abstraction). No plugin system. No async runtime in the core — the shells may thread, the core is single-threaded by construction. No 9P yet — but the library boundary is exactly where acme put the file server, and a fifth shell could serve `Surface` and `Event` over 9P without touching the core. Half of that is already built and unnamed: `src/nested.zig` listens on a unix socket at `$XDG_RUNTIME_DIR/pardes-.sock` (`~/.local/state/pardes` without one), accepting exactly one verb — `Look `, and nothing else, which is the security property — and that is how a `pardes ` run inside a pardes hands the file to the outer session instead of stacking a second full-screen UI inside one of its panes. "No config files" held until the startup file (`docs/config.md`) arrived, and that is the narrowest thing the phrase could still cover: a list of builtin COMMANDS run before the first frame — no schema, no new vocabulary, and no key remapping. The keymap is `src/config.zig`, compiled in, where a wrong binding is a compile error rather than a silent no-op. = Appendix A: feature parity checklist From the prototype survey; every line is covered by at least one of the event scripts in `test/snapshots/` — the original eighteen (boot, tty, edit, scroll, modal, look-file, look-dir, exec, tag, theme, tutor, windowops, dump, load, ttyonly, syntax, fileedit, images), and sixty-nine more added since for everything below that the prototype never had. *Layout.* Columns by weight (≤6), panes by vweight (16 panes in total, not per column); global topbar; per-pane gutter (move box + scrollbar) and tag row; `splitBelow` shrinks only the source (cursor row kept visible); `Newcol` takes width only from the source column and cannot resize any unrelated column; dying pane's weight absorbed by one sibling; emptied column hands width to a neighbor; border-drag resize on a pane's own trailing edge (v and h), hover shows `╎`/`╌` glyph-only hints; move-drag via the gutter box with preview; Alt-n new shell below, Alt-c move pane to new column; Ctrl-w h/j/k/l directional focus; body-normal Esc hops to the pane you were in before this one, alternating between two (the same builtin as SPC j j); `--tty` single-pane mode. *Mouse.* Left: select (block, stays highlighted after drag, pins cursor, enters normal), click clears, tag-row click enters tag edit, scrollbar click scrolls up-to-row (right button: down), border/move drags. Middle: execute — no-drag expands to file-ish word (alnum `.-+/:@_~`); builtin or send to shell; does not focus. Right: look — peel `:NNN`, resolve against the clicked pane's dir (`/proc/pid/cwd`, or a file's dirname) and, if that fails, against every other live pane's, most-recently-focused first (the jump stack backwards, duplicate dirs skipped; absolute words try one dir and stop) so a relative name is openable from any window that can see it; dir → shell + `ls` in source column (dedup by cwd), file → file pane at line (dedup by path, first doc opens left column and may evict a lone pristine shell; later docs split below the existing doc — an output pane, `+Search`/`+Help`, is not a doc for either half of that rule: it never claims a column, it splits below whatever pane asked for it, and nothing splits from it), image ext → image pane, `.pdf` → PDF pane. Ctrl+left is goto-definition, the one chord borrowed from every editor with a language backend. Two spellings the word expansion has beyond a path: `` @`ls -la` `` is taken WHOLE and runs as a command rather than opening as a file, and `@p7:10:5` addresses a live pane by number for the things — terminals, output buffers — that have no path to name. Middle+left chord: kept left selection appended as trailing CLI argument. A stationary pointer gets a delayed, theme-derived highlight of the exact side-effect-free selection that Look would expand; it neither focuses nor installs that selection, and pointer leave/input/content invalidation cancels it. Wheel: scroll hovered pane, batched. *Modes.* normal: helix motions (`h j k l w b e W B E 0 $ ^`, `f F t T` and `Alt-.`, counts, `gg ge gh gs gl g| G`, `Ctrl-d/u/f/b`, `zt zz zb zj zk`), insert entries (`i a I A o O`), selections (`v` extend — displayed as a fourth mode name, "select" — `x`/`X`/`Alt-x`, `%`, `;`/`Alt-;`, `_`), MULTIPLE CURSORS up to 64 (`C`/`Alt-C` copy, `s`/`S` select and split by regex with a live preview, `Alt-s` split on newline, `,`/`Alt-,` keep and remove the primary, `)`/`(` rotate, `Alt--`/`Alt-_` merge), operators (`d c y p P R u U`, `Alt-d` delete-noyank, `J`, `>`/`<`, `~`/`` ` ``/`` Alt-` ``, `Ctrl-a`/`Ctrl-x`, `Ctrl-c` comment-toggle), textobjects and surrounds under `m` (`mm mi ma ms mr md`), the `]`/`[` pairs (`]p ]d ]D ]`), `/` with `n`/`N`, `|` to filter the selection through a command, `:` for the tag as a command line, and Enter=look Tab=execute at cursor. Since the helix motion model landed, a traversal motion SELECTS the range it crossed — which is why there is no verb+noun grammar and why `i` after `w` types at the selection's start. insert: click-and-type; terminals get splice runs (shift right, never overwrite; absolute-row anchored), files get real edits. tty: raw pty forwarding (Ctrl-key toggle, default Ctrl-b, `--tty-toggle`), mouse still usable, promptClickMove on entry, prompts visible (hidden in the other modes via OSC 133). `y` fills the yank register and `p` pastes it; neither touches the system clipboard, which is helix's five words — `SPC y/Y/p/P/R`, out via `set_clipboard` and back via `read_clipboard` — and nothing else, so a delete cannot clobber what the desktop was holding. A paste from an outer terminal arrives bracketed, as one `paste` event. *Tag.* Live prefix (mode indicator, cwd or path) + editable tail with the full modal editor. Save leads the tail of every pane holding text of its own: `Save New Newtty Del` for a file or an output buffer, `Save New Newtty Del Filter` for a terminal, and the plain `New Newtty Del` for an image or a PDF, with `*` after an unsaved file's name; an image tag reports `img petscii: palette: ascii: ` before the ordinary tail (its renderer toggles are builtins under `SPC t p/l/a`). Topbar: `New Newcol Find Grep Help Tutor Dump NextColor Debug Kill` — execute-only (left click inert); Colors and Crt left it for their leader paths. *Panes.* Terminal: ghostty-vt, 16 MiB scrollback, OSC 133 prompt semantics, OSC 7 cwd, DSR/DA/kitty-query replies (write_pty + device_attributes — the nushell/helix regression), a default-on pane-local Filter which uses ghostty-vt's theme-derived 256-colour generation and keys rendered cell foreground/background truecolour and OSC overrides through that palette without mutating emulator state, greeting `ls`, auto-follow output unless navigating. File: line-number gutter (fixed width), tree-sitter highlights (c/cpp/zig minimal tier; 26 grammars full tier; re-highlight on edit, visible-range first), Save, open-at-line, undo/redo. Image: zstbi decode, kitty graphics when available, petscii matcher fallback (C64/terminal palettes, ascii glyph set toggle). PDF (`-Dmupdf`, native default on): MuPDF rendering as one continuous page strip, real text search, mouse text selection, the document outline into `+PdfSections`, fit-width/fit-height and a themed duotone tint — the last three named in the pane's own live tag. Tutor: embedded text as file pane. *Language.* ZLS compiled in and called IN-PROCESS — no subprocess, no JSON-RPC, no daemon and nothing cached between queries — behind a one-function seam (`lsp.query`) so that swapping a backend touches nothing else. `.zig` only, and on demand only: helix's five `g` gotos, ten builtins under `SPC l`, `]d`/`[d`, `=`, and insert-mode Tab after a `.`. Answers become rows in `+Search`, `+Hover` or `+Lsp` — output buffers of the same kind `/`, Find and Help fill — or, for a rename, byte ranges the core applies in one undo. The web shell has no threads and therefore no backend at all. *Chrome.* One theme per `.zig` file, folded into the ring at comptime: ours first — helix (default; near-black page, chrome one grey-ramp step off it, colors lifted from the helix editor's own theme), dark, and the acme-light one named simply `acme` — then everything `tools/gen_themes.zig` exports at build time out of the vendored helix `.toml` and zed `.json` sources in `vendor/themes`, which is all 214 helix ships plus zed's 11, sorted by name. Names are unique by construction: helix's own acme is vendored as `acme_helix.toml` so it cannot collide with ours, and every zed theme takes a `_zed` suffix for the same reason. helix and dark leave a child's ANSI palette native; acme and the zed exports resolve it onto the page so shell output stays readable on a light one. A theme owns the page, the tag bar, the gutter, the move box and the SELECTION: `sel_bg`/`sel_fg` are one pair per theme, and the three per-button tints and the dimmed extra cursors are mixed off it, so what stays fixed is the distinction between buttons and not the colours. `NextColor` browses the ring one step at a time — at 228 it is no longer how you REACH one — `Theme ` jumps to one and `ThemeSel` (`SPC t t`) lists them all into an output buffer whose rows are those very commands — execute a row (Tab, middle click) and the theme goes on; n/N select such a row WHOLE, since a command line holds no place to pick out of it — the third grain of that motion, the other two being one stop per ROW in a results list (the location at its head, never the matched text after it) and every look-able word in free text. Native shells also accept `ThemeFile `: one complete ZON `Theme`, loaded at runtime and, where document watches are available, watched with the same parent-directory/rename-over semantics. `DumpThemes` materializes the compiled ring under `/themes/builtin/`, providing the schema and a copyable starting point without adding inheritance or a second theme vocabulary. Colors toggles all recolor passes; Debug stats overlay; Dump writes state ZON. *Web (replay viewer parity).* Embedded dump; focus, border/move drags, wheel, scrollbar, Colors/NextColor, and link-LOOK opening a new tab (new — the prototype has no web link handling). One-finger touch is deliberately complete by itself: tap = LOOK; drag past a small slop = natural scroll, with no LOOK on release. A build-generated read-only archive lets LOOK open the current contents of tracked or new/nonignored Pardes `.zig` files despite the web shell having no host filesystem. The published launcher dump is captured from a running Pardes TTY after `git ls-files --cached --others --exclude-standard -- '*.zig' | sort`, so its complete terminal listing is the same tracked-plus-new/nonignored set the user can open. The freestanding module carries no host libc, SDL, or WebGL; Tree-sitter's C runtime and the selected parsers link into the module through a tiny local ABI shim (Zig is the compact web default). A body gesture retains tap-LOOK/drag-scroll, but finger-down on a tagline or a one-cell-tolerant pane separator latches to a left-mouse gesture for its lifetime, keeping layout drags out of the scroll heuristic. Touch input translation and scroll/tap state live entirely in the JavaScript shell. The native SDL shell uses a FreeType light-hinted grayscale atlas over the SDL GPU API (SPIR-V). The browser exposes each cell as selectable, inspectable text and applies the surface styles with CSS. The browser `.snap` harness drives real Chromium touch input and reads both text and per-cell styles from the DOM renderer's packed surface. Joystick cursor for the steamdeck is *new* scope — the prototype ships no gamepad input.