# Prototemplate: the full text Every docs page of https://www.prototemplate.com, the handbook's included, in reading order. # What Prototemplate is > Prototemplate is Kevin Liu's hub and working wiki for General Translation work: what it holds, who reads it and the order to read it in. URL: https://www.prototemplate.com/docs Prototemplate is Kevin Liu's hub and working wiki for his work at General Translation (GT), the localization platform for developers. It is a Next.js 16 site served at [www.prototemplate.com](https://www.prototemplate.com/), the canonical address, with prototemplate.vercel.app and prototemplate.kevinliu.studio as aliases. Two Vercel projects deploy the `main` branch of the public repository, [Kevin-Liu-01/Prototemplate](https://github.com/Kevin-Liu-01/Prototemplate): the General Translation team's project serves www, and Kevin's personal project serves the two aliases. ## The five tabs [#the-five-tabs] The docs sit in five tabs, and the menu at the top of the sidebar switches between them. The Guide card opens the handbook, and every other card opens its tab's first page. ## The order to read it in [#the-order-to-read-it-in] An agent reads three things before it works: 1. The [agent guide](https://www.prototemplate.com/docs/handbook/agents). 2. The [operating principles](https://www.prototemplate.com/docs/handbook/operating-principles). 3. The skill for its task at [/skills](https://www.prototemplate.com/skills). The skill's Review checklist is the bar for the work. A person new to the hub reads in this order: 1. [Get started](https://www.prototemplate.com/docs/get-started), to run the site. 2. The [handbook](https://www.prototemplate.com/docs/handbook), for how the work is done. 3. The tab for the work at hand: [Components](https://www.prototemplate.com/docs/components) to build a surface, [Craft](https://www.prototemplate.com/docs/craft) for the design numbers, [Engineering](https://www.prototemplate.com/docs/engineering) for the browser and build traps, and [Internals](https://www.prototemplate.com/docs/internals) to change the hub itself. ## What it holds [#what-it-holds] * **The design lab** at `/`: twenty-seven directions for the GT website, each a built page that runs live, with the anatomy wall and the capabilities ledger. * **The brand book** at `/brand` and the **brand deck** at `/deck`, 93 slides in their own viewer. * **The handbook** at [/docs/handbook](https://www.prototemplate.com/docs/handbook), a section of these docs: how Kevin runs GT work, and the canon: the agent guide, `BRAND.md`, `DESIGN.md`, `ARCHITECTURE.md`, the ship loop, the tools, the libraries, the graphics pipeline and the artifact pictures. * **The skills** at [/skills](https://www.prototemplate.com/skills): the curated agent skills, each installable in any project. * **The marks**, **the graphics** and **the films** at `/marks`, `/graphics` and `/motion`. * **These docs** at `/docs`, in the layout generaltranslation.com/docs runs, in five tabs: the Guide with the handbook, the live component library at [/docs/components](https://www.prototemplate.com/docs/components) with the blog globe figure under Drawings, Craft, Engineering and Internals. ## Who reads it [#who-reads-it] * **Kevin**, who reviews every change on localhost before it reaches `main`. * **Agents** working in this repository, and agents in other repositories that installed its skills or copied its handbook. * **The brand agency**, which reads the brand book, the deck and the design lab. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Get started > Clone the repository, install it, check the machine with the doctor, run the dev server, and work beside other sessions in a worktree. URL: https://www.prototemplate.com/docs/get-started ## Run the site [#run-the-site] ```bash git clone --filter=blob:none https://github.com/Kevin-Liu-01/Prototemplate.git cd Prototemplate pnpm install pnpm run doctor git config core.hooksPath .githooks pnpm dev ``` * `--filter=blob:none` makes a blobless clone. The history holds large reference captures, and a blobless clone downloads an old file's contents only when a command reads it. * `pnpm install` also writes `.source/`, the compiled docs collection these pages are built from (`fumadocs-mdx`, run as the install's postinstall step). * `pnpm run doctor` checks Node at the `.nvmrc` major, pnpm at `package.json`'s `packageManager`, the installed packages, the Chrome for Testing build the browser tools launch (`pnpm exec playwright-core install chromium`, or `CHROME_PATH`), and Python 3 with the packages in `requirements.txt`. ffmpeg and the HyperFrames CLI are optional; only the films need them. Each missing item prints the command that fixes it. Write `pnpm run doctor`: a bare `pnpm doctor` runs pnpm's own command. * The pre-push hook in `.githooks/` runs `pnpm lint:public` and stops a push on any finding: a key shape, a machine path or, with `PT_DENYLIST` set, a term from the private denylist. * `pnpm dev` serves the site at `http://localhost:3005`. ## Work beside other sessions [#work-beside-other-sessions] Several agent sessions work in one checkout on Kevin's machine. A lane of work runs in its own git worktree, on its own branch and its own port, so no two sessions compile in the same `.next` folder: ```bash git worktree add -b k/my-lane ../Prototemplate-wt-my-lane origin/main cd ../Prototemplate-wt-my-lane pnpm install --frozen-lockfile pnpm exec next dev --turbopack --port 3022 ``` * Stage explicit paths and commit with a pathspec. Never run `git add -A` or `git add .`: `motion/` belongs to the Videos session and stays untracked. * Never stop the shared server on 3005, and never build in the shared `.next`. * The [multi-session playbook](https://www.prototemplate.com/docs/handbook/multi-session-playbook) holds the rules for lanes, collisions and resuming. ## Rules for a shared machine [#rules-for-a-shared-machine] Several sessions on one machine share its processes and its shell, so a command that is safe for a single user can stop another session's work. 1. Kill a process by its PID only. macOS ships BSD `pkill`, which stops reading options at the first pattern, so every option after it becomes another pattern. On 2026-10-01 `pkill -f "" -P 1` signalled every process whose command line contained "1": about 140 Chrome processes, other sessions' dev servers, a test run and the session's own renders. Record `$!` for each process you start, check it with `ps -o pid,ppid,command -p `, and stop it with `kill `. 2. Quote and brace every variable in zsh, the macOS default shell. A variable named `path` is zsh's array view of `PATH`, so assigning it makes every later command "not found". In `"$B:apps/web"` zsh reads `:a` as a modifier on `$B`, so write `"${B}:apps/web"`. An unquoted variable does not split into words, a glob with no match aborts the whole command line, and a word that starts with `=` is a command lookup, so `echo ====` fails and stops a `&&` chain. 3. Stop a server by the process that listens on its port: `kill $(lsof -ti tcp: -sTCP:LISTEN)`. Without `-sTCP:LISTEN`, `lsof` also returns the browser connected to the port, and the command kills it. [Agents on one machine](https://www.prototemplate.com/docs/handbook/shared-machine) holds the full set, with memory, disk and long-running commands. [Dev servers and builds](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) covers the failures of `next dev` itself, such as the half-written `.next/dev/prerender-manifest.json` that turns every route into a 500 with "Unexpected non-whitespace character after JSON". ## The repository map [#the-repository-map] | Folder | What it holds | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `src/app` | One folder per route: the gallery, `/brand`, `/docs`, `/skills`, `/motion`, `/d/` and the rest | | `src/components` | `viewer` (the shell), `shared`, `shell`, `plate` (the sign-in port), `book` (the handbook's addresses and the Markdown renderer the skills pages use), `docs` (this site's parts, with `docs/demos`, the live plates) and `blog-globe` (the globe figures) | | `src/lib` | Registries, generated data, the engines, and `seo/`, the SEO system | | `scripts` | `lib`, `lint`, `check`, `build`, `skills` and `media`; [Scripts and generated files](https://www.prototemplate.com/docs/scripts) covers them | | `content` | `docs/` (these pages), `blog/` and `authors/` | | `deck` | The brand deck's slides and parts | | `graphics` | The illustration toolchain | | `skills` | The curated skills, one folder each | | `docs` | The handbook, the ship loop, the libraries index, the graphics pipeline and the tools index | | `public` | Captures, thumbnails, fonts, media and the built deck | `AGENTS.md` at the root is the agent's entry point, and `README.md`, `ARCHITECTURE.md`, `DESIGN.md` and `BRAND.md` hold the routes, the code map, the visual laws and the identity. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Handbook > What the handbook is, who reads it, the order to read it in, and how it relates to the skills. URL: https://www.prototemplate.com/docs/handbook This handbook records how Kevin Liu runs his work at General Translation (GT), a localization platform for developers. It is written for the agents and people who work with him: Claude Code and Codex sessions, teammates, and anyone who copies this repository into another project to work the same way. The [skills](https://www.prototemplate.com/skills) hold the procedures for each area of the work. The handbook holds what spans them: the standing principles, the bar for done, the rules for running many sessions and agents at once, pull requests and review, the product facts, the vocabulary and the dated rulings. The handbook is a section of the docs site, served at /docs/handbook on each of the site's three addresses ([www.prototemplate.com](http://www.prototemplate.com), the canonical one, and the aliases prototemplate.vercel.app and prototemplate.kevinliu.studio). The files live in `docs/handbook/` of the public repository, and each page of the section includes its file, so the site always shows the file as it stands. The section opens with the [agent guide](https://www.prototemplate.com/docs/handbook/agents) and the operating principles. Its Process folder holds seven of the twelve documents below, its Reference folder holds the product map, the glossary, the numbers and the traps and fixes, and its Canon folder holds eight files from the repository's root and `docs/`: [BRAND.md](https://www.prototemplate.com/docs/handbook/brand), [DESIGN.md](https://www.prototemplate.com/docs/handbook/design), the [libraries](https://www.prototemplate.com/docs/handbook/libraries) with the build log, the [graphics pipeline](https://www.prototemplate.com/docs/handbook/graphics), the [artifact pictures](https://www.prototemplate.com/docs/handbook/artifact-pictures), [ARCHITECTURE.md](https://www.prototemplate.com/docs/handbook/architecture), the [ship loop](https://www.prototemplate.com/docs/handbook/ship-loop) and the [tools](https://www.prototemplate.com/docs/handbook/tools). Each page also answers as Markdown at its address plus `.md`. The rest of the docs site explains how the hub works. ## Guide and principles [#guide-and-principles] ## Process [#process] ## Reference [#reference] ## Canon [#canon] The canon is the eight files at the repository's root and in `docs/` that hold the brand, the design system and the working procedures. ## The documents [#the-documents] | Document | What it holds | Read it | | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | [Operating principles](https://www.prototemplate.com/docs/handbook/operating-principles) | The eighteen standing rules behind Kevin's corrections, each with how to apply it, the skill that holds its procedure and the dated directive it comes from | Before the first task of any GT work | | [Quality bar](https://www.prototemplate.com/docs/handbook/quality-bar) | The measurable bar for each kind of artifact and for done, each row with the skill that owns it | Before showing Kevin work or shipping it | | [Multi-session playbook](https://www.prototemplate.com/docs/handbook/multi-session-playbook) | Lanes, forked sessions, one instruction sent to several sessions, shared checkouts, collisions, Claude and Codex on one main, relay notes and shared resources | When another session works on the same machine or repository | | [Decisions log](https://www.prototemplate.com/docs/handbook/decisions) | Kevin's dated rulings, what each replaced, his words and the lint, gate or skill that holds it | Before changing a standard, and when two sources disagree | | [Agents on one machine](https://www.prototemplate.com/docs/handbook/shared-machine) | Stopping processes by their ids, zsh traps, exit-status gates, memory and disk, long commands and shared checkouts, each with the incident that set it | Before running agents, builds or servers on a machine other sessions use | | [Agent workflows](https://www.prototemplate.com/docs/handbook/workflows) | Lane prompts that survive relayed messages, resuming a run, workflow shapes, refuters, kinds of check and paid model runs | Before launching subagents or a long autonomous run | | [Pull requests and CI](https://www.prototemplate.com/docs/handbook/pull-requests) | CI signals that mislead, the review bots, pull request size and crops, parity for a redesigned flow, lints and ratchets, and git on shared trees | Before opening, updating or reporting a pull request | | [Reviewing with Kevin](https://www.prototemplate.com/docs/handbook/review) | How his review words map to measures, one exemplar before a set, how a result reaches him, the ship states and the ask ledger | Before showing Kevin work and when reading his feedback | | [GT product and architecture map](https://www.prototemplate.com/docs/handbook/gt-product-map) | What GT sells and to whom, how the products fit together, the copy that must be exact, the CLI and agent entry points, and the repositories | Before writing about, designing for or changing a GT product | | [Glossary](https://www.prototemplate.com/docs/handbook/glossary) | The terms in Kevin's messages, the skills and the code, each with where it lives | When a word in a request or a skill is unfamiliar | | [Numbers](https://www.prototemplate.com/docs/handbook/numbers) | Every load-bearing number by area, each with its reason, its token or constant, the lint that holds it and the page that explains it | Before changing a size, a duration or a limit, and when a review names one | | [Traps and fixes](https://www.prototemplate.com/docs/handbook/traps) | One line per trap met in GT work: the symptom, the fix in a few words and the section that explains the cause | When something breaks, and before a round of work in an area | ## How the handbook relates to the skills [#how-the-handbook-relates-to-the-skills] * A skill holds the procedure. The handbook states the rule and names the skill. When the two disagree, the skill is right, and the change that finds the difference corrects the handbook. * `AGENTS.md` at the repository root is the entry point for an agent. It summarizes the principles, routes each kind of task to its skill, and points here for depth. * The skills name handbook files by their paths (`docs/handbook/quality-bar.md`). A project that installs the skills copies this folder and `AGENTS.md` beside them, as README.md's "Import this into another project" section describes. ## Changing the handbook [#changing-the-handbook] * A new ruling from Kevin goes into the decisions log as a dated row that names what it replaced, and the skill, lint or gate that holds it changes in the same commit. * A document keeps the writing rules of `gt-voice`: plain declarative sentences, no em dashes, no metaphors, and no contrast pairs. * The repository is public. No document carries keys, account ids, email addresses, personal details, customer names that generaltranslation.com does not show, unannounced plans or business figures (operating principle 18). * On the site, a new document needs its page in the folder group of `content/docs/(guide)/handbook/` it belongs to (`(process)`, `(reference)` or `(canon)`), which includes the file with `docs/handbook/.md`, its row in that folder's `meta.json`, its entry in `src/app/handbook/registry.ts` in the same order, its card on the section's index page and its headings in `HANDBOOK_HEADINGS` (`src/lib/search-index.ts`). A page that sets live figures between sections includes the file one heading at a time, and the build fails when such a page leaves a heading of the file out. The `prototemplate` skill (section 6) lists every step, and `pnpm lint:registries` reports what a change missed. ## Sources [#sources] * The first six documents were written on 2026-10-05 from Kevin's messages to Claude Code and Codex between 2026-07-20 and 2026-10-05, about 5,800 in all, and from the curated skills. The four Process documents on agents on one machine, agent workflows, pull requests and review were added on 2026-10-21 from the skills and the incident records behind them, and the numbers and the traps and fixes were added the same day as indexes of the docs pages and skills that hold each value and each failure. Each document's Sources section lists its own evidence. * Kevin, 2026-10-05: "this prototemplate is supposed to be my hub for general translation work, but also i should be able to use it anywhere and build on top of it so it also acts as a wiki / repository of how i do work at GT". --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Agent guide > The entry point for an agent: the read order, the principles in brief, the skill for each task, the house rules and using the hub elsewhere. URL: https://www.prototemplate.com/docs/handbook/agents General Translation (GT) builds localization tools for developers: open-source i18n libraries, a translation platform and the Locadex agent. Prototemplate is Kevin Liu's hub for his GT work and the wiki of how he does it. It holds the brand and design canon, the brand deck, the design lab, the curated skills and the handbook, and serves all of it at [www.prototemplate.com](http://www.prototemplate.com), the canonical address, and at the aliases prototemplate.vercel.app and prototemplate.kevinliu.studio. The General Translation team's Vercel project serves the first and Kevin's personal Vercel project serves the other two, and both projects deploy this repository's main branch. This file is the entry point for an agent working in this repository, or in another project that imported the hub. It is short on purpose: the skills hold the procedures and the handbook holds the depth. Kevin's own words, 2026-10-05: "this prototemplate is supposed to be my hub for general translation work, but also i should be able to use it anywhere and build on top of it so it also acts as a wiki / repository of how i do work at GT". ## Read in this order [#read-in-this-order] 1. This file. 2. [Operating principles](https://www.prototemplate.com/docs/handbook/operating-principles), before the first task. 3. The skill for the task, from the routing table below. Its Review checklist is the bar for the work. 4. [Quality bar](https://www.prototemplate.com/docs/handbook/quality-bar), before showing Kevin anything or shipping it. 5. [Decisions log](https://www.prototemplate.com/docs/handbook/decisions), before changing a standard or when two sources disagree. ## The principles in brief [#the-principles-in-brief] The [operating principles](https://www.prototemplate.com/docs/handbook/operating-principles) hold all eighteen with their sources. 1. Finish everything that was asked. "Continue" resumes every open item and every open lane. 2. Do every step an agent can do, and collect what only Kevin can do (a login, a payment, an approval) into one numbered list. 3. Change only what was asked, keep what he approved, and after a rejection restore the exact earlier state. 4. Fix the whole class of a defect, and add the lint or test that keeps it from coming back. 5. Measure the rendered result at the spot Kevin flagged. A page that loads is a proxy check and proves nothing about the look. 6. Show the result before he asks: before and after crops, a running URL, a gallery of takes. 7. Use real content and real assets, sourced and credited. Invent no quotes, numbers or logos. 8. Ship nothing unnecessary, and prefer the free and cheap design. 9. Keep secrets in protected files, and keep private facts off this public repository. 10. Codify what worked as a skill, a document or a lint that any agent can load. ## Which skill to load [#which-skill-to-load] The skills live in `skills//SKILL.md`. In this repository they are already linked into `.claude/skills` and `.agents/skills`, so Claude Code, Codex and other Agent Skills loaders find them by name. [skills/README.md](https://www.prototemplate.com/skills) lists them by area. The Tools column names the pnpm commands for the task, run from this checkout; [docs/TOOLS.md](https://www.prototemplate.com/docs/handbook/tools), served at /docs/handbook/tools, lists every command and the scripts the skills bundle. | Task | Skill | Tools | | ------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------ | | Writing any text: copy, docs, captions, PRs, posts, Slack | `gt-voice` | `pnpm lint:code` (em dashes, eyebrows and heading periods in code) | | Ending a turn, reporting state, the PR slate, decision lists | `gt-reporting` | `pnpm check:pages --sheets` (contact sheets) | | A page, doc, route or build of generaltranslation.com | `gt-website` (with gt-cloud's own `gt-landing`) | `pnpm check:pages --base --pages-module `, `pnpm lint:lines ` | | A landing, pricing, enterprise or careers page | `gt-landing-pages` | `pnpm lint:lines ` | | Anything in this repository | `prototemplate` | `pnpm lint:static`, `pnpm lint:registries`, `pnpm gen:all` | | Running or debugging a local server, letting Kevin try a build | `gt-local-dev` | `pnpm dev`, `pnpm run doctor` | | Lag, frame cost, Lighthouse, a heavy shader or canvas | `gt-performance` | `pnpm check:pages` (layout shift per cell) | | Design taste, polish, a surface Kevin calls bad or busy | `gt-aesthetic` | `pnpm lint:lines:shell`, `pnpm lint:radius`, `pnpm lint:heads` | | The brand, the one Inter, marks and third-party logos | `gt-brand` | `pnpm lint:type`, `pnpm build:marks` | | A slide, the brand deck or any GT presentation | `gt-deck` | `pnpm build:deck` | | Options, directions, variants, converging on a pick | `gt-explorations` | `pnpm capture:pages --direction ` | | A lint, a gate, a new rule to enforce | `gt-lints` | `pnpm lint:static`, `pnpm lint:all`, `pnpm lint:public` | | Proving a fix or feature is done | `gt-verify` | `pnpm check:pages`, `pnpm lint:lines ` | | Commits, PRs, review bots, stacks, landing | `gt-ship` | `pnpm build`, `pnpm build:updated --staged`, `pnpm lint:public` | | Workflows, subagents, briefs, handoffs, resuming after a stop | `gt-orchestration` | `pnpm skills:usage` | | Animation of any kind | `gt-motion` | `pnpm check:pages` (interactions) | | A blog or launch graphic | `gt-graphics` | none; the toolchain in `graphics/` (docs/GRAPHICS.md) | | Dithered fields and artifact pictures | `gt-dither` | `pnpm mood-tone`, `pnpm lint:pictures` | | A film, trailer, promo or demo GIF | `gt-films` | `pnpm build:motion` | | A diagram or an isometric drawing | `gt-diagrams` | none | | UI components and how product UI behaves | `gt-components` | `pnpm lint:code` | | A product surface (sign-in, onboarding, the dashboard) and reviewing its states | `gt-product-surfaces` | none | ## The handbook [#the-handbook] [docs/handbook/](https://www.prototemplate.com/docs/handbook), also served at /docs/handbook: * [Operating principles](https://www.prototemplate.com/docs/handbook/operating-principles): the standing rules and where each procedure lives. * [Quality bar](https://www.prototemplate.com/docs/handbook/quality-bar): the bar for each kind of artifact and for done. * [Multi-session playbook](https://www.prototemplate.com/docs/handbook/multi-session-playbook): lanes, forks, shared checkouts, collisions, Claude and Codex on one main. * [Decisions log](https://www.prototemplate.com/docs/handbook/decisions): Kevin's dated rulings and what each replaced. * [Agents on one machine](https://www.prototemplate.com/docs/handbook/shared-machine): stopping processes by id, zsh traps, exit-status gates, memory and disk, long commands. * [Agent workflows](https://www.prototemplate.com/docs/handbook/workflows): lane prompts, resuming a run, workflow shapes, refuters, kinds of check, paid model runs. * [Pull requests and CI](https://www.prototemplate.com/docs/handbook/pull-requests): misleading CI signals, review bots, size and crops, parity, git on shared trees. * [Reviewing with Kevin](https://www.prototemplate.com/docs/handbook/review): his review words as measures, one exemplar first, showing results, ship states, the ask ledger. * [GT product and architecture map](https://www.prototemplate.com/docs/handbook/gt-product-map): what GT sells, the copy that must be exact, the repositories. * [Glossary](https://www.prototemplate.com/docs/handbook/glossary): the working vocabulary and where each term lives. * [Numbers](https://www.prototemplate.com/docs/handbook/numbers): every load-bearing number by area, with its token, its lint and the page that explains it. * [Traps and fixes](https://www.prototemplate.com/docs/handbook/traps): one line per trap, the symptom first, then the fix and the section that explains it. On the site the handbook is one section of the docs site. It opens with this guide and the operating principles, then three folders: Process (the quality bar, the playbook, the decisions log, and the documents on one machine, workflows, pull requests and review), Reference (the product map, the glossary, the numbers and the traps and fixes) and Canon (BRAND.md, DESIGN.md, the libraries with the build log, the graphics pipeline, the artifact pictures, ARCHITECTURE.md, the ship loop and the tools). Each file has a page under `content/docs/(guide)/handbook/` that shows it through fumadocs-mdx's include tag, so the Markdown file stays the one source. A document added to the handbook or the canon gets its page there, and a page that includes its file one heading at a time (to set a live plate between two sections) lists every heading of the file, or the build fails. The rest of the docs site at /docs explains how the hub works, holds the live component library under /docs/components and records the directions for the blog globe figure. ## House rules [#house-rules] * **Writing.** Plain technical English: complete declarative sentences, no em dashes, no metaphors, no "X, not Y" pairs, no signposts and no eyebrows. Headings are plain nouns in sentence case with no trailing period (`gt-voice`). * **Type.** Inter is the only typeface, the self-hosted rsms InterVariable, at weight 500 or less, read through the type tokens. `pnpm lint:type` holds it (`gt-brand`). * **Lines.** Every line is drawn once by one owner, in one of three roles (DESIGN.md section 2). `pnpm lint:lines:shell` holds it. * **Corners.** Rounded controls, square shells: controls and cards at 6px, chips at 4px, and only the shells square (DESIGN.md section 2, Corners). `pnpm lint:radius` holds it. * **Pages.** Every page with a book head follows one structure (DESIGN.md section 4, The book page). `pnpm lint:heads` holds it. * **Explorations stay local.** New directions and redesign rounds stay uncommitted or on a branch until Kevin reviews them on localhost and says to land them (`gt-explorations`, `gt-ship` section 8). * **One session per lane.** Several sessions share this checkout. Work that belongs to another lane goes to that session by name, `motion/` belongs to the Videos session and stays untracked, and nobody runs `git add -A` or `git add .` (`docs/handbook/multi-session-playbook.md`, `prototemplate` section 8). * **Commits.** Stage explicit paths and commit with a pathspec. Prototemplate commits author as Kevin with his GitHub noreply address and keep the session's `Co-Authored-By:` trailer (`docs/SHIP-LOOP.md` section 6). gt-cloud commits carry the repository's configured General Translation work identity, which its Vercel deploys require; never set that identity globally (`gt-ship` section 2). * **Landing.** Kevin merges, or says to merge. In this repository nothing reaches main until he has reviewed it on localhost. * **Privacy.** The repository and the site are public. No keys, tokens, account ids, email addresses, personal details, unannounced plans or business figures go into a file, a capture or a commit (operating principle 18). ## Working in this repository [#working-in-this-repository] ```bash pnpm install pnpm run doctor # what this machine is missing to run the tools pnpm dev # http://localhost:3005 pnpm exec tsc --noEmit # 3 to 5 minutes pnpm lint:static # every lint and test that needs no server (the lane and CI gate) pnpm lint:all # lint:static, then the live audits against PT_BASE (default 3005) pnpm check:pages --preset quick --pages # eight devices in dark and 1440x900 in light on the touched pages ``` * Reuse the dev server on 3005 when it is running, and never build in the shared `.next`. The build gate runs in a scratch worktree with `&&` (`gt-ship` section 8). * The browser lints launch the Chrome for Testing build `playwright-core` installs (`pnpm exec playwright-core install chromium`); `CHROME_PATH` overrides it. * Generated files change only through their scripts: `pnpm build:skills` (`src/lib/skills.ts`, `skills/README.md`), `pnpm build:updated` (`src/lib/updated.ts`), `pnpm build:deck`, `pnpm build:marks`, `pnpm build:tools` (`docs/TOOLS.md`); `pnpm gen:all` runs every one of them. `pnpm build:motion` runs only when the Videos session hands a film over (`gt-films` section 11). * `README.md` maps the routes, `ARCHITECTURE.md` the code, `DESIGN.md` the visual laws and `BRAND.md` the identity. ## Using the hub in another project [#using-the-hub-in-another-project] README.md's "Import this into another project" section gives the commands. In short: 1. Copy `skills/` and `scripts/skills/install.mjs` to the project's root, then run `node scripts/skills/install.mjs --project . --dry-run` there and again without `--dry-run`. The skills land as relative links in `.claude/skills` and `.agents/skills`, the layout this repository uses. 2. Copy `AGENTS.md`, `BRAND.md`, `DESIGN.md` and `docs/handbook/` beside them, so every link between the skills, the canon and the handbook keeps working, and add a `CLAUDE.md` that imports `AGENTS.md`. 3. Edit the copied `AGENTS.md`: the project's name and purpose, its own commands and ports, its lanes, and its own gates. Keep the principles, the routing table and the house rules that still apply. 4. Read the current canon by its address on the site ([www.prototemplate.com/docs/handbook/design](http://www.prototemplate.com/docs/handbook/design), `/docs/handbook`, `/skills/`), and copy the folders again to take later changes. The docs site's last page, [www.prototemplate.com/docs/template](http://www.prototemplate.com/docs/template), explains how to lift the docs site itself into the project. ## Ending a turn [#ending-a-turn] A turn that changed something ends the way `gt-reporting` section 4 sets out: the answer to what Kevin asked in the first line, what shipped with its exact state, something he can see, a numbered list of what only he can do, and the risks, numbered so he can answer by number. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Operating principles > The eighteen standing rules behind Kevin’s corrections, each with how to apply it, the skill that holds it and its dated directive. URL: https://www.prototemplate.com/docs/handbook/operating-principles General Translation (GT) is a localization platform for developers. Kevin Liu does design, website and product work at GT, and he runs it through several Claude Code and Codex sessions at once. The eighteen rules below come from his corrections to that work between 2026-07-20 and 2026-10-08. Read them before the first task of any GT work with him. Each rule names the skill or document that holds its procedure. Where the two disagree, the skill is right and this page needs a fix. The [glossary](https://www.prototemplate.com/docs/handbook/glossary) defines working terms such as lane and feature flag. The [quality bar](https://www.prototemplate.com/docs/handbook/quality-bar) sets the bar for each kind of artifact, and the [product map](https://www.prototemplate.com/docs/handbook/gt-product-map) describes what GT sells and how its products fit together. ## 1. Keep going until everything is done [#1-keep-going-until-everything-is-done] Run every wave of a plan to completion without stopping between waves to ask for permission. "Continue", "continue all" and "keep going" resume every open lane and every unfinished item. When an agent dies on a usage limit, an outage or a closed app, check the working tree, then resume the agent with its brief intact (`gt-orchestration` section 6). Do the hard items as well and report each one. Spend the time on the fixes, and write tests to prove them. Kevin, 2026-09-09: "can we just do all of our updates? why are you holding back". He said the same on 2026-07-24 and 2026-09-12. ## 2. Do the work yourself and ask once for what only Kevin can do [#2-do-the-work-yourself-and-ask-once-for-what-only-kevin-can-do] Do every step an agent can do, including console setup through a CLI (`vercel`, `gh`, `gcloud`) or the browser page Kevin already has open. Collect what only he can do (a login, a payment, a credential consent, an approval, a Linear issue id) into one numbered list with the exact file, line or URL for each item. A blanket approval covers the plan it answers. The safety lines still hold. Before a step that cannot be undone, such as a merge over recent work, check that nothing is lost. Wait for his word on a force push, a production deploy to GT's Vercel team, new spend, or landing an exploration round on Prototemplate main (`gt-ship` section 8). Kevin, 2026-10-05: "anything approved is approved from me." Also 2026-07-20 and 2026-10-02. ## 3. Change only what was asked [#3-change-only-what-was-asked] Make the smallest literal change that answers the request. "Move it down" means translate it and keep its angle and easing. When a request could mean a small or a large change, make the small one or ask. When a fix spreads into neighbouring behaviour, assets or metadata, revert the extra changes exactly and apply the narrow change. An image Kevin pastes is the whole spec. Kevin, 2026-08-06: "why did so much change, i just wanted to alter the connectors and line coming up". On 2026-08-07 a landing update had also redone the OG image, the favicon and the metadata, and he said there was no need. ## 4. Keep approved work [#4-keep-approved-work] Before changing a surface, list what Kevin praised on it and carry every item forward, because a rewrite that drops an approved element is a regression. When he rejects a direction, restore the files to exactly their state before the rejected change and rebuild from there, since the rejected version is never the base of the next attempt. Commit early in each round on a local branch so git holds the restore point, and archive each published version before overwriting it. The dated verdicts are in `gt-aesthetic` section 2, and the restore and archive steps are in `gt-explorations` section 6 (After a rejection) and section 7 (Archive before overwriting). Kevin, 2026-08-07: "lets redo this to before i started asking for this change and rebuild". Also 2026-07-30 and 2026-10-03. ## 5. Fix the whole class [#5-fix-the-whole-class] When Kevin flags one instance (an icon, a footer, a flag chip, a double border), enumerate the class programmatically with grep, a registry, a lint or a measurable check, fix every member, verify each one and report the count. The sweep repeats the requested change on each instance of the same defect, and rule 3 still holds for everything around them. When he likes a treatment in one place, apply it to the sibling components. The cross-app sweep is in `gt-components` (Standardization), and the fixes by failure class are in `gt-lints` section 6. Kevin, 2026-09-28: "look for ANYWHERE ELSE icons nneed to be synced". Also 2026-08-03. ## 6. Find the root cause and guard it [#6-find-the-root-cause-and-guard-it] Diagnose before editing: read the computed values of the element Kevin selected, read the full log, and reproduce with the deployed environment. Fix the cause, then add the regression test or lint that keeps the class from returning (`gt-lints` section 4). Error handling keeps the underlying error in its message and never substitutes a guessed cause. Kevin, 2026-08-18: "right but why is the fetch failing". Also 2026-08-05. ## 7. Measure the rendered result [#7-measure-the-rendered-result] A fix is reported only with visual or measured proof of the rendered result at the place Kevin flagged: computed styles on the live element, 2x crops of corners and junctions, and every animation state. A code change does not prove the render, and neither does a green proxy check (the page loads, the sizes match) or a settings panel that shows the right value. When a tool reads badly, switch to another one, such as a Playwright capture, and finish the verification yourself. The methods are in `gt-verify`, and the [quality bar](https://www.prototemplate.com/docs/handbook/quality-bar) (Done) states when work counts as finished. Kevin, 2026-07-30: "no, the borders are still sstraight black, still padding, and borders wrong color". The fix before it had been reported as verified from a screenshot. ## 8. Show the result [#8-show-the-result] Results reach Kevin as something he can see or hear: before and after crops at the same viewport, the localhost URL with its server running, a page open in his browser, or a small HTML page for a set of images or audio takes. Send them before he asks. The formats are in `gt-reporting` section 1, the review servers and the steps for letting him try a build are in `gt-local-dev` sections 1 and 5, and the crop and PR rules are in `gt-aesthetic` (Showing the work) and `gt-ship` section 4. Kevin, 2026-10-05: "i cant listen to the voices, make it in html so i can go through and listen to em". ## 9. Real content and real assets [#9-real-content-and-real-assets] Copy and numbers come from production, the benchmark and data files, or GT's published pages. Never invent testimonials, quotes, metrics, prices or feature promises, and mark placeholder content as placeholder. Marks, logos, flags and scans are the real files, sourced and credited: third-party logos from thesvg.org (`gt-brand` section 5), pictures with their source, license and credit (`gt-dither` section 5), and the search for an asset continues until the real one is found (`gt-brand` section 8, Third-party material). Kevin, 2026-08-05: "like these arent real quotes", about a testimonial block an agent had written. ## 10. Ask only real decisions, with a recommendation [#10-ask-only-real-decisions-with-a-recommendation] Ask Kevin only what is his to decide: spend, naming, licensing, scope, or an instruction that reads two ways. Ask each as a multiple-choice question with the recommended option first, and batch several into one numbered list with short ids so he can answer inline (`gt-reporting` section 6). When a new state resembles an existing one, reuse the existing behaviour and say so instead of asking. A gate that blocks a requested change is a question for him, and bypassing it silently is a defect. Kevin, 2026-09-25: "no need to ask me anythiing just do this all for me please". ## 11. Correct from the first frame [#11-correct-from-the-first-frame] Treat a gap at startup, a flash on reload and lag on the first interaction as defects, even when the page settles a moment later. Check the first frame and the first interaction after a cold load and after a reload, and reserve space so nothing shifts (`gt-verify`, `gt-performance`). Kevin, 2026-08-08: "if i reload i briefly see 3 layers stacked on top of each other". Also 2026-08-04, on an animation that had to start "without a startup lag". ## 12. Nothing unnecessary ships [#12-nothing-unnecessary-ships] A release carries no development code. Keep galleries, stand-ins, test-only hooks, dev routes, temporary tests, recorded fixtures and harnesses off the release branch (`gt-ship` section 3). Add no dependency that existing code can replace, keep comments to one or two lines, and keep an unready dashboard feature behind a feature flag. Kevin, 2026-10-01: "we cant ship anythign unnnecessary". On 2026-08-28 he questioned three packages added for one feature, and since 2026-09-03 a dashboard page that is not ready ships behind a feature flag. The same rule applies inside a change. Use the platform feature that removes code before adding code, such as a CSS grid row from 0fr to 1fr for an auto height in place of a measured one. When a review asks to test machinery that need not exist, remove the machinery. Kevin, 2026-10-06, on a caption card that measured its own height: "Beautiful but don't complicate. Always simplify." ## 13. Prefer free and cheap [#13-prefer-free-and-cheap] Challenge any design that adds paid infrastructure, and lay out the free alternative with numbers first. Use the cheapest model that performs well enough, put spend caps on paid model runs (`gt-orchestration` section 7), and turn off git-triggered preview deploys where they only cost money: the landing builds main and staging only (`gt-website` section 8). Check which account a cost lands on before acting. Kevin, 2026-09-15: "make a new pr to not allow preview deployments from git in landing, this is running up a lot of costs". ## 14. Secrets stay in protected files [#14-secrets-stay-in-protected-files] Keys and tokens live in a protected config file or a gitignored env file. They never appear in chat, URLs, prompts, commits, docs or PR bodies, and briefs and docs name them by variable name only. When Kevin pastes a key into chat, move it into the env or config file, never echo it or commit it, and tell him it now sits in a transcript so he can rotate it. Never ask him to paste a key into chat. Kevin's brief, 2026-08-03: "never print a key value". ## 15. "Fully" means complete [#15-fully-means-complete] When Kevin asks to build out, enhance or update something fully, cover every section and capability with real diagrams, visuals and code, and state why the design works. A placeholder or a partial pass does not count. Kevin, 2026-08-05: "show actual diagrams and visuialzations and code in these. enhance fully". Also 2026-08-04. On a page that already exists, "enhance" keeps the page and adds to it: the copy, the sections and the pieces Kevin likes stay. Kevin, 2026-10-08, after a rebuild of the careers page dropped its hero and its copy: "keep the original write up". ## 16. Codify what worked [#16-codify-what-worked] After something works once, turn it into a skill, a doc, a lint or saved parameters: the prompts that worked and how they were structured, a lint for each standard, and generation parameters saved next to each asset. Every tool and skill belongs to a workflow with a routing doc. A new skill follows the contract in `prototemplate` section 10, and a new lint follows `gt-lints` section 4. Kevin, 2026-10-05: "create docs and skills for audio creation and generation, what prompts worked and how u structured it". Also 2026-07-24 and 2026-08-06. ## 17. Vendor-agnostic agent tooling [#17-vendor-agnostic-agent-tooling] Agent tools, skills and scripts work with Codex, Claude Code and local models alike. Skills use the Agent Skills frontmatter fields only, so every loader accepts them (`prototemplate` section 10), and no script hard-wires one provider. Kevin, 2026-09-10: "be vendor agnostic, dont just ennforce claude code". ## 18. Privacy on public surfaces [#18-privacy-on-public-surfaces] Prototemplate is public at [www.prototemplate.com](http://www.prototemplate.com) and github.com/Kevin-Liu-01/Prototemplate. Nothing in it carries keys, account ids, email addresses, personal details, customer names that generaltranslation.com does not show, unannounced plans, or business numbers such as funding, revenue and headcount (`gt-brand` section 8, `gt-voice` "Facts and public surfaces"). Colleagues appear by role. `pnpm lint:public` scans every file for key shapes, machine paths and the terms on a private denylist, and the pre-push hook in `.githooks/` runs it. The rule dates from the brand questionnaire round (2026-08-06 and 2026-08-11) and from 2026-10-05, when Kevin made Prototemplate his public hub for GT work. ## Where the procedures live [#where-the-procedures-live] | Rules | Skill or document | | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | | 1, 13 | [`gt-orchestration`](https://www.prototemplate.com/skills/gt-orchestration): resuming agents, briefs, fan-out, spend caps | | 1, 2 | [Multi-session playbook](https://www.prototemplate.com/docs/handbook/multi-session-playbook): lanes, broadcast messages, relays between Claude and Codex | | 2, 4, 12 | [`gt-ship`](https://www.prototemplate.com/skills/gt-ship): PR shape, release hygiene, Prototemplate landing and deploys | | 3, 4 | [`gt-aesthetic`](https://www.prototemplate.com/skills/gt-aesthetic): the reference, the dated verdicts, reading Kevin's asks | | 4 | [`gt-explorations`](https://www.prototemplate.com/skills/gt-explorations): exploration rounds, archive and restore | | 5, 6, 16 | [`gt-lints`](https://www.prototemplate.com/skills/gt-lints) and [`gt-components`](https://www.prototemplate.com/skills/gt-components): lints, sweeps, fixes by class | | 6, 7, 11 | [`gt-verify`](https://www.prototemplate.com/skills/gt-verify): measurement, the viewport matrix, live checks | | 7, 15 | [Quality bar](https://www.prototemplate.com/docs/handbook/quality-bar): the bar for each kind of artifact and for done | | 8, 10 | [`gt-reporting`](https://www.prototemplate.com/skills/gt-reporting): crops, galleries, status lines, decision lists | | 8 | [`gt-local-dev`](https://www.prototemplate.com/skills/gt-local-dev): review servers, letting Kevin try a build | | 9 | [`gt-brand`](https://www.prototemplate.com/skills/gt-brand) (Third-party material) and [`gt-dither`](https://www.prototemplate.com/skills/gt-dither): real assets, provenance, credits, licences | | 11, 13 | [`gt-performance`](https://www.prototemplate.com/skills/gt-performance): frame cost, first interaction, Lighthouse | | 13 | [`gt-website`](https://www.prototemplate.com/skills/gt-website): landing deploys | | 16, 17 | [`prototemplate`](https://www.prototemplate.com/skills/prototemplate): the skill contract and the hub | | 18 | [`gt-brand`](https://www.prototemplate.com/skills/gt-brand), [`gt-voice`](https://www.prototemplate.com/skills/gt-voice) and [BRAND.md](https://www.prototemplate.com/docs/handbook/brand): public surfaces | Kevin's wiki skill `agent-iteration-loop` holds the general loop of implementing, verifying, reviewing and shipping, and these rules apply inside it. ## Sources [#sources] * Kevin's messages to Claude Code and Codex from 2026-07-20 to 2026-10-05, about 5,800 in all. Quotes keep his spelling. Rules 1, 4, 5 and 8 recur most: about 90 messages ask to continue or resume, 137 to revert or restore an earlier state, 177 for a change "everywhere" or "for all", and 86 to be shown the result. The counts include copies in forked sessions. * Prototemplate: BRAND.md section 9; `skills/gt-aesthetic` sections 2 and 5; `skills/gt-ship` sections 3, 4 and 8; `skills/gt-lints` sections 1, 4 and 6; `skills/gt-components` (Standardization); `skills/gt-brand` sections 5 and 8; `skills/gt-dither` section 5; `skills/gt-voice` (Hard rules, Facts and public surfaces); `skills/gt-website` section 8; `skills/gt-local-dev` sections 1 and 5; `skills/prototemplate` sections 8 and 10; `docs/handbook/quality-bar.md` and `docs/handbook/glossary.md`. * Kevin's private gt-cloud memory notes, which hold earlier statements of rules 2, 3, 4 and 12 and the writing rules this page follows, and two notes read on 2026-10-10 for the additions to rules 12 and 15. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Quality bar > The measurable bar for each kind of artifact, from pages and motion to copy, pull requests and films, and for done, each row with its owner. URL: https://www.prototemplate.com/docs/handbook/quality-bar Kevin Liu reviews General Translation (GT) design and site work. Work is ready to show him or to ship when it clears the bars below for its kind of artifact. Each bar names the skill that owns it, and that skill holds the procedure for meeting the bar. When this page and a skill disagree, the skill wins, and the change that finds the difference corrects this page. A bar that no skill holds yet lives on this page, and its Owner column says so. ## The overall bar [#the-overall-bar] * GT's own finished work is the reference. The brand deck governs product surfaces, and the shipped site, generaltranslation.com, governs landing and marketing pages ([gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), section 1). The deck is built from `deck/` and served at `/deck`, and the site is built from `apps/landing` on gt-cloud origin/main. From 2026-08-06 the Dossier (`/d/singularity-dossier`) governed the site (Kevin, 2026-08-06: "dossier is indeed our completed version and treat it as such"). On 2026-10-07 Kevin ruled "fix the dossier references", and the Dossier stays in the gallery as the direction the site grew from. Before the deck and the Dossier existed, the July rounds measured against generaltranslation.com and resend.com (Kevin, 2026-07-30). * Clearing every row on this page is the minimum. The work must also hold up beside its reference at the same geometry, and the convergence loop below checks that. * When tweaks will not reach the bar, restart from the reference. A version Kevin rejected is never the base of the next attempt ([gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), section 2). On 2026-08-11 he had the diagrams of the new site pages restarted outright. * Every page gets a second pass against its reference before it is shown ([gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), verdict of 2026-08-17). * A bar is met on the rendered result. A gate is a typecheck or a lint. A structural check confirms that a page loads with the right hierarchy and dimensions, and reports also call it a proxy check. A measured check reads computed values or pixels on the render, and a visual check is a person or a fresh critic comparing a crop with its reference. An item that passed only gates and structural checks is reported as unverified ([gt-verify](https://www.prototemplate.com/skills/gt-verify), The definition of done). Every report says which kind of check passed ([gt-orchestration](https://www.prototemplate.com/skills/gt-orchestration), section 4; [gt-reporting](https://www.prototemplate.com/skills/gt-reporting), section 1). ## Reading the tables [#reading-the-tables] Owner is the skill or document that holds the bar, with the section to read. Read is the day the owner was read for this page. Where the owner is this page, Read gives the day Kevin set the bar. Paths are relative to the Prototemplate root, and the [glossary](https://www.prototemplate.com/docs/handbook/glossary) defines terms such as plate, rail and the Dossier. ## Pages [#pages] The rows apply to product surfaces, the site, Prototemplate's own pages and docs pages. | Bar | Owner | Read | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------- | | The page matches its reference measure for measure, and every value the reference lacks has a stated reason. | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), section 1 | 2026-10-05 | | Content comes from production or the prototype spec, and nothing is invented. | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), review checklist | 2026-10-05 | | A display head holds two lines at most at every width. | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), section 4 | 2026-10-05 | | A lead reads in two or three lines at 60 to 70 characters, and prose stays under 75. `node skills/gt-aesthetic/scripts/measure-type.mjs --strict` passes, or each flag has a reason. | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), sections 1 and 4 | 2026-10-05 | | Nothing overflows its column or the viewport, and no text is clipped. | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), review checklist; [gt-lints](https://www.prototemplate.com/skills/gt-lints), check:pages | 2026-10-05 | | Nothing shifts after first paint, and client-mounted rows reserve their height. | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), review checklist; [gt-lints](https://www.prototemplate.com/skills/gt-lints), check:pages | 2026-10-05 | | Tap targets on a phone are 44px. `check:pages` fails a target under 40px and notes one of 40 to 43px. | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), section 3 (Rhythm); [gt-lints](https://www.prototemplate.com/skills/gt-lints), check:pages | 2026-10-05 | | Text a reader needs clears 4.5:1 in both themes. | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), section 3 (Hierarchy and contrast) | 2026-10-05 | | Every line is drawn once by one owner, rules run edge to edge, and junctions pass at 2x crops. In Prototemplate's chrome each line takes the `hair`, `hair-soft` or `edge` role and `pnpm lint:lines:shell` is clean. Any other page passes `pnpm lint:lines ` in both themes. | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), section 3; [DESIGN.md](https://www.prototemplate.com/docs/handbook/design), section 2; [gt-lints](https://www.prototemplate.com/skills/gt-lints), lint:lines | 2026-10-05 | | The surface carries one dithered material, at the landing hero's strength or under it. | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), section 3 (Material); [gt-dither](https://www.prototemplate.com/skills/gt-dither) | 2026-10-05 | | The page uses one accent, GT blue, and semantic color sits only on icons. | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), section 3 | 2026-10-05 | | Each width shows one composition. Intermediate widths and zoomed windows fall to the mobile composition, with no hybrid of desktop logic and mobile layout. | [gt-verify](https://www.prototemplate.com/skills/gt-verify), section 4 | 2026-10-05 | | The longest translation of every string fits its reserved lines, and the layout holds in CJK, RTL and Indic text as well as Latin. | [gt-verify](https://www.prototemplate.com/skills/gt-verify), section 5; [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), section 4 | 2026-10-05 | | Both themes are shot at 1440 and 390 (plate pages also at 1527 by 814), and `pnpm check:pages --preset quick --pages ` shows zero defects on every touched page. | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), section 5 (Local review); [gt-lints](https://www.prototemplate.com/skills/gt-lints), section 2 | 2026-10-05 | | A Prototemplate page with a book head follows the book page standard (nothing drawn above the title, the panel with Updated first, one band ruled on both edges), and its corners follow the radius law. `pnpm lint:heads` and `pnpm lint:radius` pass, live modes included. | [prototemplate](https://www.prototemplate.com/skills/prototemplate), sections 3 and 5; [DESIGN.md](https://www.prototemplate.com/docs/handbook/design), sections 2 and 4 | 2026-10-06 | | The lints pass: `pnpm lint:all` in Prototemplate and `pnpm lint` in gt-cloud. | [gt-lints](https://www.prototemplate.com/skills/gt-lints), review checklist | 2026-10-05 | A landing or marketing page also clears the [gt-landing-pages](https://www.prototemplate.com/skills/gt-landing-pages) checklist (one rail, hatch spacers, heads, the shared `Cta`), checked at 390, 768, 1024, 1440 and 1920 in both themes. ## Motion [#motion] | Bar | Owner | Read | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ---------- | | Dither loops draw at their 30 fps budget by design. Every other animation meets the 60 fps bar in the Performance table. | [gt-motion](https://www.prototemplate.com/skills/gt-motion), section 2 (Durations) | 2026-10-05 | | A language morph lasts longer than 0.5 s, so the glyphs visibly regroup. It finishes inside the time its locale holds the screen, such as one stop of the hero's locale belt. Kevin, 2026-08-05: "the morphings are still way too fast theyre like \<0.5 sec". Later that day he called a slower morph "a lil too long". | This page | 2026-08-05 | | A morph dissolves through glyphs at every width and never collapses into a fade. | [gt-motion](https://www.prototemplate.com/skills/gt-motion), section 5 | 2026-10-05 | | A scroll story runs on one scrubbed dial. Each beat owns a scroll range with dwell, and scrolling back plays the story backward. | [gt-motion](https://www.prototemplate.com/skills/gt-motion), section 6 (Scroll stories); [gt-landing-pages](https://www.prototemplate.com/skills/gt-landing-pages), Scroll stories | 2026-10-05 | | Product pages have no entrance animation. | [gt-motion](https://www.prototemplate.com/skills/gt-motion), section 1 | 2026-10-05 | | Reduced motion renders a designed still, and the resting markup reads without JavaScript. | [gt-motion](https://www.prototemplate.com/skills/gt-motion), section 1 | 2026-10-05 | | Scrolling is native (`gt-ui/no-smooth-scroll`). | [gt-motion](https://www.prototemplate.com/skills/gt-motion), section 1 | 2026-10-05 | | Motion is verified with real pixels from an external browser, in both themes, under reduced motion, at 1440 and 390. | [gt-motion](https://www.prototemplate.com/skills/gt-motion), Verifying motion | 2026-10-05 | ## Graphics and diagrams [#graphics-and-diagrams] | Bar | Owner | Read | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------- | | Every crop comes from a registered capture of the real product. An illustrative mock in a set is described as a mock. | [gt-graphics](https://www.prototemplate.com/skills/gt-graphics), What a visual is | 2026-10-05 | | Text in a blog visual measures 26px or more after the fit, and `pnpm graphics:audit` passes. | [gt-graphics](https://www.prototemplate.com/skills/gt-graphics), Sizing for the blog column | 2026-10-05 | | Renders are supersampled (shot 7680 wide and halved to 3840). Every visual with lines or small type is read at 1:1 as well as on the contact sheet. | [gt-graphics](https://www.prototemplate.com/skills/gt-graphics), What a visual is and Procedure | 2026-10-05 | | A diagram shows a relationship the text alone does not, drawn from the real artifact as one connected composition. | [gt-diagrams](https://www.prototemplate.com/skills/gt-diagrams), section 1 | 2026-10-05 | | Numbers printed in a diagram match what the eye sees. Kevin, 2026-08-05: "yeah it just doesn tlook like +63%". | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), review checklist | 2026-10-05 | | Junctions are read at 2x crops in both themes, because the line auditor skips SVG. | [gt-diagrams](https://www.prototemplate.com/skills/gt-diagrams), section 9 | 2026-10-05 | | An OpenGraph card has a composition of its own, apart from the header cover, set in the house type with the house lines and centered on its canvas. It is rendered and looked at after every change, and its size and themes follow [gt-graphics](https://www.prototemplate.com/skills/gt-graphics) (Sizing for the blog column). Kevin, 2026-09-17: "the tweet/blog header cover is the angled one and the opengraph is the exploded components". | This page | 2026-09-17 | ## Copy [#copy] | Bar | Owner | Read | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ---------- | | The hard rules hold: no em dashes, metaphors, contrast pairs, fragment rhythm, exclamation marks, rhetorical questions, signposts, eyebrows, marketing vocabulary or emojis. | [gt-voice](https://www.prototemplate.com/skills/gt-voice), Hard rules | 2026-10-05 | | Every line passes the copy test: it states a number or a mechanism, and a line a competitor could use unchanged is cut. | [gt-voice](https://www.prototemplate.com/skills/gt-voice), Facts and public surfaces | 2026-10-05 | | The humanizer pass scores 35 of 50 or higher across directness, rhythm, trust, authenticity and density. | [gt-voice](https://www.prototemplate.com/skills/gt-voice), The humanizer pass | 2026-10-05 | | Heads are plain nouns in sentence case with no trailing period, and Title Case is kept for buttons. | [gt-voice](https://www.prototemplate.com/skills/gt-voice), Headings and casing | 2026-10-05 | | Every sentence has a subject and a verb, the subject is short, and no aside interrupts a clause. | [gt-voice](https://www.prototemplate.com/skills/gt-voice), Sentence shape | 2026-10-05 | | Public writing stands alone for a developer who has never heard of Kevin or GT. It defines the product once, and each section opens with the point a stranger can reuse. | [gt-voice](https://www.prototemplate.com/skills/gt-voice), Writing for strangers | 2026-10-05 | | Nothing sensitive appears on a public surface: funding, revenue, headcount, customer counts or unannounced launches. | [gt-voice](https://www.prototemplate.com/skills/gt-voice), Facts and public surfaces | 2026-10-05 | ## Pull requests [#pull-requests] | Bar | Owner | Read | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------- | | On a gt-cloud PR, Greptile reads 5/5 on the head commit. Kevin, 2026-09-13, after a merge at 4/5: "bruh u merged without addressingthis". | [gt-ship](https://www.prototemplate.com/skills/gt-ship), section 5 | 2026-10-05 | | Every bot thread has a reply naming the fixing commit and is then resolved. | [gt-ship](https://www.prototemplate.com/skills/gt-ship), section 5 | 2026-10-05 | | The three required checks on gt-cloud main are present and green, and `node skills/gt-ship/scripts/pr-bots.mjs ` exits 0. | [gt-ship](https://www.prototemplate.com/skills/gt-ship), section 5 | 2026-10-05 | | `node skills/gt-ship/scripts/pr-size.mjs` was read and every large group is justified in the body. The diff holds no recorded fixtures, harnesses, review galleries or planning Markdown. | [gt-ship](https://www.prototemplate.com/skills/gt-ship), section 3 | 2026-10-05 | | The PR carries one concern under a conventional title, and a `feat` PR links a Linear issue. | [gt-ship](https://www.prototemplate.com/skills/gt-ship), section 3 | 2026-10-05 | | The body is a two-sentence summary and What to look at, with before and after crops refreshed after every visible change. It is written in Kevin's own register (lowercase, first person), and the crops carry most of the explanation (Kevin, 2026-10-05). | [gt-ship](https://www.prototemplate.com/skills/gt-ship), section 4; [gt-voice](https://www.prototemplate.com/skills/gt-voice), Two registers | 2026-10-05 | | Kevin merges the PR or says to merge it. | [gt-ship](https://www.prototemplate.com/skills/gt-ship), section 5 | 2026-10-05 | ## Convergence loops [#convergence-loops] A convergence loop iterates on quality until a bar holds: a page against its reference, a set of graphics against its best accepted item, a film cut against its brief. Kevin, 2026-07-30: "remember the bar is generaltranslation.com and resend.com. literally rereview and continuously score yourself until it look sright". | Bar | Owner | Read | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | ---------- | | The brief names the reference and the bar before the first round. For a set, the bar is the best item Kevin has accepted. | [gt-orchestration](https://www.prototemplate.com/skills/gt-orchestration), section 4 | 2026-10-05 | | The work and the reference are captured at the same geometry (viewport, theme, camera, crop) and placed side by side. | [gt-orchestration](https://www.prototemplate.com/skills/gt-orchestration), section 4 | 2026-10-05 | | A fresh critic that did not build the work scores each view blind against the reference. It is told to be harsh, and it names what the reference does that the work does not. | [gt-orchestration](https://www.prototemplate.com/skills/gt-orchestration), section 4 | 2026-10-05 | | Each view has a numeric bar wherever it can be scored, stated in the brief. The bar ends the loop; a cap on rounds is a budget stop, and the report lists the gaps still open. | [gt-orchestration](https://www.prototemplate.com/skills/gt-orchestration), section 4 | 2026-10-05 | | One strong view never averages away a weak one, and the builder never scores its own work. | [gt-orchestration](https://www.prototemplate.com/skills/gt-orchestration), section 4 | 2026-10-05 | | Refiners fix only the gaps the critic named. | [gt-orchestration](https://www.prototemplate.com/skills/gt-orchestration), section 4 | 2026-10-05 | | Kevin's note outranks a critic's pass. An item he called wrong stays open until he says otherwise. | [gt-orchestration](https://www.prototemplate.com/skills/gt-orchestration), section 4 | 2026-10-05 | | When scores stop rising, the method changes: measure the reference and rebuild, or change the idea. | [gt-orchestration](https://www.prototemplate.com/skills/gt-orchestration), section 4; [gt-films](https://www.prototemplate.com/skills/gt-films), section 3 | 2026-10-05 | ## Performance [#performance] | Bar | Owner | Read | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------- | | Performance work leaves the look unchanged. An audit whose fix would change the design (contrast, tap target size, a dropped effect, a lower resolution) goes to Kevin with its measured gain. Kevin, 2026-08-05: "fix the lighthouse without changing anything aesthetically". | [gt-performance](https://www.prototemplate.com/skills/gt-performance), section 1 | 2026-10-05 | | Animation and interaction run at 60 fps at full resolution. Loops start complete, and the first interaction never stalls. | [gt-performance](https://www.prototemplate.com/skills/gt-performance), section 1 | 2026-10-05 | | Frame time is measured before and after the change (`node skills/gt-performance/scripts/frame-probe.mjs `) and reported in a table. | [gt-performance](https://www.prototemplate.com/skills/gt-performance), section 2 | 2026-10-05 | | Every timing carries the machine's load average, and before and after runs go back to back under similar load, because parallel agents distort the numbers. | [gt-performance](https://www.prototemplate.com/skills/gt-performance), section 2 | 2026-10-05 | | An engine with quality tiers picks its tier from measured frame cost. | [gt-performance](https://www.prototemplate.com/skills/gt-performance), section 3; [gt-motion](https://www.prototemplate.com/skills/gt-motion), section 6 | 2026-10-05 | | Lighthouse is read from a production build in a clean profile, or from production after the merge, and the PR carries the real report. | [gt-performance](https://www.prototemplate.com/skills/gt-performance), section 6 | 2026-10-05 | | A budget lives in the probe that measures it and fails like a lint, and a red budget blocks the change. | [gt-performance](https://www.prototemplate.com/skills/gt-performance), section 8 | 2026-10-05 | ## Films [#films] | Bar | Owner | Read | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ---------- | | Kevin reads the scripts before the build, which waits for his answer. He picks a new narrator from auditions. | [gt-films](https://www.prototemplate.com/skills/gt-films), sections 3 and 7 | 2026-10-05 | | A critic watches each draft frame by frame (every 0.5 s, and every 0.25 s around each cut and major move) and reports every defect with its time and a fix. | [gt-films](https://www.prototemplate.com/skills/gt-films), section 9 | 2026-10-05 | | `node skills/gt-films/scripts/measure-render.mjs` passes on the render (-17 to -15 LUFS, true peak under -1 dBTP, 1920 by 1080 at 60 fps), and `hyperframes check` ends with 0 errors. | [gt-films](https://www.prototemplate.com/skills/gt-films), review checklist | 2026-10-05 | | A non-English take has a native listener's sign-off. | [gt-films](https://www.prototemplate.com/skills/gt-films), section 7 | 2026-10-05 | ## Charts [#charts] Kevin set these bars for benchmark charts on 2026-07-30 and 2026-09-30. The wiki's `dataviz` skill holds the general method for form, color and marks. | Bar | Owner | Read | | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ---------- | | The win is obvious at a glance. | This page | 2026-07-30 | | Compared series are stacked or overlaid so the margin reads exactly, and the bars in one set share one style. | This page | 2026-07-30 | | The method is stated beside the chart: what was measured, how, and on what. | This page | 2026-07-30 | | A metric that shows cost savings is prominent. | This page | 2026-07-30 | | Benchmarks are run fresh for the chart that ships. | This page | 2026-09-30 | | The chart leads with the most impressive true metric. Kevin, 2026-09-30: "why just tokens? show our speed diagram too thats more impressive". | This page | 2026-09-30 | | Marks match their values, and every number agrees with each other place it appears. | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), review checklist | 2026-10-05 | ## Repositories [#repositories] Kevin set these bars for a repository's own documents between 2026-08-02 and 2026-09-30. | Bar | Owner | Read | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------- | | The README holds the shipped product's instructions and stats in a concise form, with the OpenGraph image and a demo GIF at the top and screenshots of what it does. Kevin, 2026-09-30: "youre treating the readme as like a log of work instead of a concise summary". | This page | 2026-09-30 | | The repository's description and topics are set. | This page | 2026-08-04 | | A `LICENSE` file states the terms. Prototemplate's license reserves all rights to General Translation, Inc., and third-party fonts, icons and adapted skills keep their own licenses. | [prototemplate](https://www.prototemplate.com/skills/prototemplate), section 10; [LICENSE](https://github.com/Kevin-Liu-01/Prototemplate/blob/main/LICENSE) | 2026-10-05 | | `AGENTS.md` explains the repository's layout and philosophy, `SKILL.md` explains how to use the tool and everything it does, and both match the code. | This page | 2026-08-02 | | Prose that has drifted from the code is corrected in the change that finds it. | [prototemplate](https://www.prototemplate.com/skills/prototemplate), section 1 | 2026-10-05 | ## Done [#done] Work is ready to show when its artifact's table holds on localhost or in a file Kevin can open, and the report carries before and after crops of each changed region in both themes ([gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic), Showing the work). It is ready to ship when the pull request table holds as well. It is done when the rows below hold. | Bar | Owner | Read | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------- | | A visual fix is measured on the render at the spot Kevin flagged (computed values, 2x or 4x crops, pixel probes), in every state of a moving element. | [gt-verify](https://www.prototemplate.com/skills/gt-verify), section 1 | 2026-10-05 | | The real gesture and the real flow work in the running app, for every category of item the change covers. Any shortcut is named and then replaced by the real gesture. | [gt-verify](https://www.prototemplate.com/skills/gt-verify), section 2 | 2026-10-05 | | The stress matrix, viewports, themes, browsers and edge counts that apply were walked. | [gt-verify](https://www.prototemplate.com/skills/gt-verify), sections 3 to 5 | 2026-10-05 | | Items that were right before the change are still right. | [gt-verify](https://www.prototemplate.com/skills/gt-verify), section 6 | 2026-10-05 | | Every member of the defect's class was found, fixed and checked. | [gt-verify](https://www.prototemplate.com/skills/gt-verify), section 8 | 2026-10-05 | | Shipped work is live: every lane's commits are on main, production's commit contains them, the live page carries that deployment's stamp, and the OG card is read live. For Prototemplate, the `dpl` stamp on www equals the team deployment. | [gt-verify](https://www.prototemplate.com/skills/gt-verify), section 7; [gt-ship](https://www.prototemplate.com/skills/gt-ship), section 8 | 2026-10-05 | | The report says how each item was verified and names what was not, with the reason. | [gt-verify](https://www.prototemplate.com/skills/gt-verify), section 12 | 2026-10-05 | ## Sources [#sources] * Skills read on 2026-10-05: `skills/gt-aesthetic/SKILL.md` and `references/verdicts.md`, `skills/gt-verify/SKILL.md`, `skills/gt-orchestration/SKILL.md`, `skills/gt-performance/SKILL.md`, `skills/gt-landing-pages/SKILL.md`, `skills/gt-lints/SKILL.md`, `skills/gt-motion/SKILL.md`, `skills/gt-graphics/SKILL.md`, `skills/gt-diagrams/SKILL.md`, `skills/gt-voice/SKILL.md`, `skills/gt-ship/SKILL.md`, `skills/gt-films/SKILL.md`, `skills/gt-dither/SKILL.md`, `skills/gt-reporting/SKILL.md`, `skills/prototemplate/SKILL.md`. * Canon: `DESIGN.md` section 2, `deck/DECK-GRAMMAR.md`, `docs/SHIP-LOOP.md` section 2, `scripts/check/pagecheck/README.md`, `LICENSE`. * Kevin's rulings, checked against his messages to Claude Code and Codex: 2026-07-30 (the reference sites, side-by-side scoring, chart margins, method and cost metric), 2026-08-01 and 2026-08-02 (the OpenGraph card's type and lines), 2026-08-02 (`AGENTS.md` and `SKILL.md`), 2026-08-04 (two-line heads, repository topics), 2026-08-05 (morph length, numbers in diagrams, Lighthouse with no visual change), 2026-08-06 (the Dossier), 2026-08-11 (the diagram restart), 2026-08-17 (the second pass), 2026-09-13 (Greptile 5/5), 2026-09-17 (the OpenGraph card's own composition), 2026-09-30 (chart metric, README), 2026-10-05 (the page heads, PR bodies that let the crops explain), 2026-10-07 (the shipped site as the site's reference). --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Multi-session playbook > How many agent sessions share one machine: lanes, forked sessions, shared checkouts, collisions, Claude and Codex on one main, and resumption. URL: https://www.prototemplate.com/docs/handbook/multi-session-playbook Kevin Liu runs many agent sessions on one machine at the same time for his General Translation (GT) work: named Claude Code sessions, Codex sessions, and forks of both. Codex is OpenAI's coding agent, and Kevin runs it beside Claude Code. Inside Claude Code, the ListAgents tool lists the other Claude sessions on the machine by name, and the SendMessage tool delivers a message to one of them. This page holds the practices that let every session add to the others' work and undo none of it. The single rules live in skills, and this page points to them. [`gt-ship`](https://www.prototemplate.com/skills/gt-ship) sections 1, 2 and 8 hold lanes, worktrees, staging and the shared Prototemplate checkout. [`gt-orchestration`](https://www.prototemplate.com/skills/gt-orchestration) holds a session's own subagents, resumption (section 6) and handoffs at a hard stop (section 8). The [`prototemplate`](https://www.prototemplate.com/skills/prototemplate) skill section 8 holds the lane map of the shared Prototemplate tree. The [glossary](https://www.prototemplate.com/docs/handbook/glossary) defines lane, session, forked session, handoff and relay note. When a skill and this page disagree, the skill wins. ## 1. Lanes [#1-lanes] One session owns one lane. ListAgents prints the live list of sessions, and the table lists the GT lanes as of 2026-10-05. | Session | Owns | Where it works | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | Prototemplate | The site at prototemplate.com, its viewer shell, the deck build, the skills and the docs | The shared checkout (`$PROTOTEMPLATE`), with the dev server at `http://localhost:3005` | | Videos | The films, their kit and `MOTION.md` | Prototemplate `motion/`, which stays untracked. Its Prototemplate commits carry only film and poster files in `public/media`. | | Glyph Map | A page of generaltranslation.com that has not shipped | Its own gt-cloud worktree and dev port | | New Onboarding and Dashboard | The dashboard sign-in and onboarding PRs, and in Prototemplate the mood slides in `deck/`, `src/components/plate`, `src/app/craft` and `public/brand/mood` | Its own gt-cloud worktrees. It pushes Prototemplate main from its own worktree. | | Lottie | The blog's Lottie translation figure | Its own gt-cloud worktree and dev port | | docs redesign | The docs site on generaltranslation.com | Its own gt-cloud worktrees | Kevin's personal projects run in sessions and repositories of their own. They share the machine with the GT lanes (section 10) and nothing else. * Work that belongs to another lane goes to that session. Find it with ListAgents and send the request with SendMessage, with Kevin's words for the request and the paths involved. Address the session by its name, because the bracketed id after the name changes when the session restarts. * Codex sessions do not appear in ListAgents. Reach them through a relay note that Kevin pastes (section 7). * Two lanes edit `deck/`. The session about to change a shared folder messages the other first (section 8). * When Kevin narrows a session to one thing, the session drops everything else, including its own open items. While it waits, it starts no PR sweep and no work from another lane. Kevin, 2026-10-03: "that videos one should be making them, and this one is just for prototemplate work". * A session that gives up work to another lane sends the work's real state: the files, the commits, what passed and what is open. On 2026-10-03 the Prototemplate session handed two films to Videos with a message that understated how far they had come, and Videos learned the rest from the files. The receiving session reads the files and the git history before it acts on a handover message. ## 2. Forked sessions [#2-forked-sessions] Kevin forks sessions often. Ten gt-cloud sessions active between August and October descend from one session started on 2026-07-30. Videos was forked from New Onboarding and Dashboard on 2026-10-01, Glyph Map from Videos on 2026-10-02, and the Prototemplate session from New Onboarding and Dashboard on 2026-10-03. A fork starts with its parent's whole message history, and every inherited message reads as if it were addressed to the fork. * A fork acts on the newest instruction addressed to it and on its own lane. Inherited messages are history. Their asks belong to the parent, which is usually still running and still owns them. * In a fork, "continue", "continue everything" and "keep going" cover the fork's own lane. Two forks got this wrong. The Glyph Map fork restarted its parent's film build before starting the map, and Kevin stopped it. The Prototemplate fork took "continue everything" as its parent's list and pushed review fixes to the parent's open PRs, one of them from the parent's scratch worktree. Kevin narrowed it twice that day, first to its own request and then to Prototemplate work alone, and the films it had started went to Videos. Kevin, 2026-10-02: "wait another agent is workign on the motion and videos. just focus on the nnew page". * Before acting, a fork learns what the parent already did and who holds the parent's lanes now. It reads `git status --short`, `git log --oneline -15` on its branch and on `origin/main`, `git worktree list`, the lane's notes (memory notes, `HANDOFF.md`, the round's spec) and ListAgents. * A fork takes its own branch, worktree and dev port before it changes any file. Kevin's rule for any conversation, 2026-10-02: "the work in this convo should be on its own worktree branched off from main". * In a Claude Code transcript (`~/.claude/projects//.jsonl`), an inherited message keeps the parent's `sessionId`. One message of 2026-07-31 sits in eleven gt-cloud transcripts with a single originating `sessionId`. A count of Kevin's messages across transcripts dedupes on `sessionId` and timestamp. ## 3. One instruction to several sessions [#3-one-instruction-to-several-sessions] Kevin sometimes sends one instruction to several sessions within seconds. On 2026-08-06 "commit and push everything to prototemplate, our branches, and 4213" reached four gt-cloud sessions within 25 seconds. Two of them went after the same three targets. One of the two had its Prototemplate build collide with a concurrent build. The other later saw a worktree directory vanish between two commands, because a parallel session was porting into it. * Each session does its own part: its own paths, its own branch, its own PR. * The session whose lane owns a shared step does it, once. Shared steps include a push of Prototemplate main, a sync of files into another repository, and a commit on a PR branch that two sessions use. The other sessions commit their own paths and check whether the shared step already happened (`git fetch`, then `git log --oneline HEAD..origin/` or `gh pr view --json headRefOid`). They report the sha they found. When ownership is unclear, they ask the other session by SendMessage before pushing. * A diverged branch is never force-pushed to make an instruction come true. On 2026-08-06 the branch of PR #4213 carried eight commits of the landing port that a force push would have erased. The session reported the divergence and offered to port the missing work onto that branch as new commits, which Kevin then asked for. * When Kevin asks which session did something, answer from git and GitHub: the PR, the branch and commit, and the exact source text or file the work used. When Kevin asked "did we work on docs redesign in this convo?" on 2026-09-01, each session listed the PRs and files its conversation had touched, and the sessions that had not done the redesign said so. * "Try again" after an API error or a spend limit re-runs the step that failed, starting from a fresh read of the state. On 2026-08-06 a session cut off by the spend limit re-read every target after "Try again" and reported each one clean. ## 4. Shared checkouts [#4-shared-checkouts] `gt-ship` sections 1, 2 and 8 and `prototemplate` section 8 hold the rules for a shared checkout. They give `motion/` to Videos, require explicit paths and a pathspec commit, check the branch before every commit, keep the primary gt-cloud checkout on its old branch, and leave the Prototemplate dev server on 3005 and its `.next` alone. * Re-read a shared file immediately before editing it, because another session may have saved it a minute ago. * Keep scratch output in the session's scratchpad. Every session started in the main gt-cloud checkout reads its `.claude` folder, and on 2026-10-02 a preview written there had to be moved out. ## 5. Collisions [#5-collisions] * Report another session's in-flight changes and leave them where they are. A lint or type failure in files the session does not own goes into its report and does not hold back its own clean diff (`docs/SHIP-LOOP.md` section 0). Kevin, 2026-07-31: "dont worry about the uncommited work. only worry about localhost3006/ page". * Keep another session's uncommitted file out of your commit, even when your own tweak sits inside that file. Say in the report that the tweak will land with that session's commit. * Rerun a failure once before diagnosing it when another session may be mid-save. Such failures include a 500 from hot reload, a page the line auditor refuses, and a type error in a file the other session is editing. On 2026-08-05 `tsc` failed on a parallel edit that used `Code2` before importing it. By the time the session looked, the other session had added the import, and the rerun passed. * When the build stays broken on another session's change, make the smallest change that restores the build and tell the owner. On 2026-07-31 one session pushed a full sync to Prototemplate main while a second session was mid-push. The second session rebuilt on top of that push and landed one fix: `pnpm-workspace.yaml` held a placeholder line that made `pnpm install` fail. * When another session's commit regresses your work, measure the regression and send the owner the exact fix. On 2026-09-29 a second session's commit on PR #5021 stopped a gallery state from playing when the console paged to it. The measurement and the fix went to that session, and it fixed the regression on the same PR. * Fix another session's regression in the smallest unit that regressed (a token, a rule, a layer), and keep the rest of its file. On 2026-08-05 a parallel session had grayed a belt in `v0-pages.css` at higher specificity, and the fix changed that one rule to blue. On 2026-09-29 Kevin asked for the earlier glyphs back, and the fix restored only the rain layer from the earlier head and kept the other session's gallery console. * Ask the owner before building a file another repository or session owns. `gt-ship` section 1 records the case of the CLI login callback page (2026-09-30). * When two sessions find they are doing the same work, the one further along messages the other with its PRs, and the other stops. On 2026-09-30 the session that had opened the docs drift PRs told a second session, started from a task chip for the same fix, to stand down. * When two sessions must change one feature, agree on a split by module and file. On 2026-09-29, in the sign-in gallery work, one session took the console requests in its own worktree, and the other kept the caption card's copy in a module of its own. When the first session shipped caption plates of its own, the second stopped its caption build before it wrote any files. ## 6. Claude and Codex together [#6-claude-and-codex-together] Kevin, 2026-07-30: "let codex do its own thing, but branch off again". * Each tool works on its own branch, in its own worktree, on its own dev port. A Codex branch named `codex/` is renamed `k/` before it becomes a gt-cloud PR (2026-08-27). * Answer "are you on X's branch?" from git: `git -C branch --show-current` and `git -C log --oneline -1` for each worktree involved. The answer on 2026-07-30 named both worktrees, both branches, both ports and the commit both branches started from. * Every landing starts from a fresh `origin/main` and adds to the other agent's work. Kevin ruled on 2026-09-23 for a main that both agents push. Each agent keeps absorbing the other's pushes by rebase, every push to main has run the core test suite, and the two agents share main without separate deploy windows ("WE MERGE"). * Before publishing to a shared main: 1. Record the starting base commit when the worktree is created. 2. Fetch and compare `git diff --stat origin/main` with your own change. For every file both agents touched, keep the incoming behaviour and re-apply yours on top. A merge without conflicts can still undo a feature through a whole-file replacement. 3. Read `git diff origin/main HEAD` in full, removals included, as the change about to be published. 4. Run the gates on the integrated commit. A green run on the branch before the merge says nothing about the merge. 5. Push without force. If the push is rejected, fetch, integrate and run the gates again. In GT's repositories, gt-cloud main takes only squash merges through PRs with three required checks (`gt-ship` section 5), and Prototemplate's gates and deploy checks are in `gt-ship` section 8. * Before a deploy that follows a stretch of both agents pushing, audit the last week of main for work one agent undid. Kevin, 2026-09-22: "MAKE SURE ALL CHANGES WITHIN LAST WEEK ARE PROPERLY SYNCED". * In a fresh worktree from `origin/main`, list the week: `git log origin/main --since='7 days ago' --format='%h %ad %s'`, `git worktree list` and `git branch -r`. * Check every branch that looks unmerged for patch equivalence with `git cherry -v origin/main ` and `git range-diff`. A squash or a rebase changes commit ids and keeps the work. * Read each merge commit's conflict resolutions with `git show --remerge-diff `. * For merged PRs, measure how many of their added lines survive on main and trace every removal to a stated follow-up. On 2026-09-08 Kevin thought the routing PR #4359 had been reverted. The gt-cloud audit found it open and unmerged with every commit intact, and it merged the next day. * Leave the other agent's unfinished checkout as it is and list it in the report. * When the two agents build competing versions of one thing, keep both under distinct names and ids, and Kevin names the canonical one (2026-09-18). ## 7. Relays [#7-relays] A relay note is written by one agent for another, usually Claude for Codex or Codex for Claude, and Kevin pastes it as is. It holds: * The branch, the tip commit and the worktree path, and whether the receiver's own worktree was touched. * Each commit with one line on what it did. * The receipts: each check with its command, exit code and log path. * The open list. * Locks still held and processes the writer stopped. On 2026-09-26 an idle session had held a shared capture lock for 81 minutes. * Deploys queued and what each one carries. * Rebase advice: what lands on main next, and which side's duplicate commits a rebase will drop. To continue another agent's uncommitted work, snapshot it as a commit in your own worktree and leave theirs as it was. The relay of 2026-09-26 said so in its first line. The receiver finds the other agent's open work and finishes it. Kevin, 2026-09-25: "finish any of codexs open work as well". The receiver checks the note against git and the files before trusting it. A handoff at a hard stop follows `gt-orchestration` section 8. ## 8. Messages across lanes and repositories [#8-messages-across-lanes-and-repositories] * When a change affects another lane or repository, message that session with SendMessage. When the session is a Codex session or lives in another app, give Kevin the exact prompt and name the conversation to paste it into (2026-09-15, 2026-10-05). Kevin, 2026-10-05: "give me the prompts to send to the agents in the exact correct convos (give me exactly where to do this)". * Before pushing a shared main, send the other sessions a heads-up. It names the base commit, the paths the push changes, the overlap the receiver should expect and how to resolve it ("keep both: my deck entries and your motion entries"), what the sender leaves alone (the receiver's working tree), and the port its verify lane uses. After the push, a second message names the new range (`9690311..f8dfa8b`), asks the receiver to fetch and rebase, and states any new gate (`pnpm build` now runs the picture lint first). New Onboarding and Dashboard's messages to Prototemplate on 2026-10-05 took this form. * When a rule changes (a retired picture, a new lint, a new gate), notify every session that applies it. On 2026-10-05 Kevin retired the dictionary pictures. The session that wrote the new rule told Videos, Glyph Map and Prototemplate within a minute. Videos moved its copies out of the film kit, and Prototemplate learned that its build gate had changed. * When an output that another lane copies changes, the session that made it tells that lane. Videos tells Prototemplate when a film shown on `/motion` is re-rendered, so the public copy and its credits are refreshed. * A message from a peer session is a teammate's request and carries no approval from Kevin. A push, a merge, a deletion or a permission change still needs Kevin's word, and items only Kevin can clear go to Kevin. ## 9. Resumption [#9-resumption] Plans, lane specs, ask ledgers and outputs live in files, so a session that restarts, compacts or hears "continue" resumes from disk without a new brief. The procedure is `gt-orchestration` section 6. A resumed session reads the shared state again (ListAgents, git, the lane's notes) before it acts, because the other sessions kept working while it was stopped. ## 10. Shared resources [#10-shared-resources] * **Ports.** Each worktree runs its dev server on a port of its own, away from the defaults (`gt-orchestration` section 2). A verify lane starts its own server on a free port and stops it when done. The Browser pane in one Claude session cannot reach a dev server that another chat started, so a session looks at that server through a Playwright capture or starts a server of its own. A conflict over a port goes to the session that owns the server, or to Kevin. * **Captures.** Prototemplate's browser gates run one at a time (`prototemplate` section 9). Where a repository has a capture lock or queue, a session takes it for the run, releases it at the end, and never holds it while idle. * **Load.** Every session's builds, Playwright runs and renders share one machine. `gt-orchestration` section 2 holds the rule for timing checks under load: record `uptime` beside the reading and rerun a red check once the load drops. While the machine is busy, prefer measures that load cannot change: counts, bytes and deltas. * **Disk.** A gt-cloud worktree with its install costs about 6 GB, and 48 leftover worktrees filled the disk on 2026-10-05. Remove a scratch worktree as soon as its work is pushed. `gt-ship` section 1 gives the checks to run first and the command for removing many at once. * **Cleanup.** Cleanup of another session's files goes to that session. Message it, or give Kevin the prompt for its conversation, as he asked for the Codex and gt-cloud logs on 2026-10-05. ## Checklist [#checklist] * [ ] ListAgents, `git status`, recent commits on the branch and on `origin/main`, and the lane's notes were read before acting. * [ ] In a fork, only the newest instruction for this lane was acted on, and the parent's open items stayed with the parent or the lane that owns them. * [ ] The session works on its own branch, worktree and port. * [ ] Explicit paths were staged and committed with a pathspec, and the branch was checked before the commit (`gt-ship` section 2). * [ ] A shared push was done once, by its owner, and the other sessions reported the sha they found. * [ ] Other sessions' in-flight files stayed out of the commit and were named in the report. * [ ] A heads-up went out before pushing a shared main, and the session fetched and rebased after another session's push. * [ ] A changed rule reached every session that applies it. * [ ] A relay note carries the branch, tip, worktree, commits, receipts with exit codes, open list, locks, queued deploys and rebase advice. * [ ] Servers this session started are stopped, locks are released, `uptime` sits beside timings, and scratch worktrees are removed. ## Sources [#sources] * Prototemplate: `skills/gt-ship/SKILL.md` sections 1, 2, 5 and 8; `skills/prototemplate/SKILL.md` sections 8 and 9; `skills/gt-orchestration/SKILL.md` sections 2, 6 and 8; `docs/SHIP-LOOP.md` section 0; `docs/handbook/glossary.md` (working terms). * Kevin's private gt-cloud memory notes on lanes, ship gates, worktree disk use, the lost-work audit and four lanes' handoffs. * Kevin's Claude Code and Codex session transcripts for the gt-cloud project, which stay private: the fork lineage read from each transcript's inherited `sessionId` and title records; the 2026-08-06 instruction and the four sessions' responses; the 2026-10-03 handover to Videos; the 2026-09-29 and 2026-09-30 messages between the sign-in sessions; the 2026-10-05 messages from New Onboarding and Dashboard and Videos; and the desktop app's notice that another chat's dev server is out of the Browser pane's reach (2026-09-29). The Codex sessions of September 2026 hold the shared-main audit of 2026-09-22 and the relay of 2026-09-26. * Kevin's messages to Claude Code and Codex from 2026-07-30 to 2026-10-05, for the rulings quoted above. Quotes keep his spelling. * Dates are in UTC, as the transcripts record them. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Decisions log > Kevin’s dated rulings on process, product, design and the repositories, what each replaced, his words and the gate that holds it. URL: https://www.prototemplate.com/docs/handbook/decisions In his Claude Code and Codex sessions, Kevin Liu rules on how General Translation (GT) work is done, across process, product, design and the repositories. This log lists the rulings that set or replaced a standing practice from 2026-07-31 to 2026-10-20. Each row gives the date, the ruling, the practice it replaced, his words where the ruling needs a source, and the lint, gate or skill that holds it. Read it before you change a GT standard, and whenever an older note, transcript or skill gives conflicting guidance. ## Using the log [#using-the-log] * The later ruling holds. Guidance with an earlier date in a memory note, a transcript or a skill gives way to a later row here, and the change that finds the conflict corrects the stale text. When a skill changed after a row and the two disagree, the skill holds and this log needs a fix. * Design verdicts are recorded in full in [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md) in `gt-aesthetic`, with Kevin's words and what was wrong. Their rows here are one line and link there. * The skills named in the last column hold the procedures. A row states the ruling and where it is enforced. * Each path names its repository: gt-cloud, Prototemplate, or `content:` for generaltranslation/content, which holds the docs and blog source. * A row quoting a transcript carries the day the transcript stamped on Kevin's message, which is UTC. A design verdict row carries its date in verdicts.md, and a row with no message behind it carries the date its skill or memory note gives. Those two use Kevin's local date in California, so they can sit one day before the UTC stamp of the same evening. * Agents do not change a standard on their own. Bring Kevin the current row with a recommendation. When he rules, add a dated row that names what it replaced, and update the lint, gate or skill that holds it in the same change. ## July and August 2026 [#july-and-august-2026] | Date | Ruling | Replaced | Kevin's words | Held by | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 2026-07-31 | Scrolling is native on every GT surface: no smooth scrolling, scroll hijacking or inertia library. A scroll story scrubs its timeline on the page's own scroll. | Lenis smooth scrolling, which Kevin asked for on 2026-07-22, 07-28 and 07-30 for the scroll stories and the presenter | "remove smooth scrolling from everything, i hate how it keeps going after i stop scrolling" | `no-smooth-scroll` in gt-cloud (landing, `packages/ui`) and Prototemplate `pnpm lint:code`, with `/present` exempt in Prototemplate's `.oxlintrc.json`; [gt-motion](https://www.prototemplate.com/skills/gt-motion) sections 1 and 6 | | 2026-08-01 | Monospace is for numbers and code only. Headings, paragraphs, labels, pills and social cards are set in Inter. | Mono labels, pills and OG text in the July explorations | "mono should only be used for numbers." | `mono-is-not-voice`; `inter-only` allows Geist Mono for code only; [gt-brand](https://www.prototemplate.com/skills/gt-brand) section 4 | | 2026-08-04 | Mock layouts and plan images are literal specs, and a display head holds two lines at most. Locadex appears as an isometric drawing that carries its mark, and a gif of it is refused. | Sections that kept the copy and drifted from the Figma layouts; three-line heads; a Figma note proposing a gif of Locadex | [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md), 2026-08-04 | `no-gif-mark`; Prototemplate's `skills/gt-aesthetic/scripts/measure-type.mjs` flags a display head over two lines | | 2026-08-06 | The Dossier (`/d/singularity-dossier`) is the completed reference for the site. Product surfaces follow the brand deck since 2026-09-25, for the brand the 2026-10-06 row below holds, and for the site the 2026-10-07 row below holds. | The July bar of generaltranslation.com and resend.com | [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md), 2026-08-06 | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic) section 1; [BRAND.md](https://www.prototemplate.com/docs/handbook/brand) section 8 | | 2026-08-07 | Every crawler and agent may read the public site, and no rule blocks an AI crawler or a public page. Only the staging build disallows crawling. | None; a new standing rule | "make it so that all robots and agents can crawl us" | gt-cloud's `apps/landing/src/app/robots.ts`; [gt-website](https://www.prototemplate.com/skills/gt-website) section 7 and its review checklist | | 2026-08-07 | A heading carries no trailing period. Body sentences keep theirs. The deck followed on 2026-09-08. | Landing section headings that ended in a period | "remove the periods from headers to standardize. our hero header has no periods"; on the deck: "i dont thinkn we need periods everywhere" | `no-heading-period` (gt-cloud #5007, Prototemplate `pnpm lint:code`); [gt-voice](https://www.prototemplate.com/skills/gt-voice), Headings and casing | | 2026-08-08 | The staging build translates Spanish only. Production keeps the full locale roster. | Staging translating Japanese and Spanish | "only es for staging." | gt-cloud's `apps/landing/staging.gt.config.json` (`["es"]`) beside `gt.config.json`, run by `scripts/deploy-landing.sh --staging`; [gt-website](https://www.prototemplate.com/skills/gt-website) sections 3 and 8 | | 2026-08-11 | For the gt-cloud redesign stack (landing, sign-in and dashboard), the production landing page is canonical. That page is gt-cloud #4213, built from the Dossier. Prototemplate and the toolchain lend isolated mechanics, assets and example copy, and the toolchain enterprise and pricing prototypes are the spec for those two pages. Product surfaces moved to the brand deck on 2026-09-25. | Copying the old toolchain page shell into new pages | "The production GT landing page is canonical." (a handoff brief he sent to the redesign sessions) | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic) section 1; [gt-landing-pages](https://www.prototemplate.com/skills/gt-landing-pages), The reference | | 2026-08-11 | GT surfaces avoid mono as the brand voice, smooth scrolling, robot or sparkle AI icons, flag soup, AI gradients and glass, eyebrows, and em dashes in rendered text. | The July explorations' eyebrows, gradients, glass and mono labels | "3 lines of text stacked to say the same thing" (on eyebrows). [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md), 2026-08-11 | `no-eyebrow`, `no-em-dash`, `mono-is-not-voice`, `no-smooth-scroll`, `no-raw-locale-flags`, `icon-tiers`; AI icons, gradients and glass by review ([gt-brand](https://www.prototemplate.com/skills/gt-brand) section 7) | | 2026-08-11 | No invented content. Copy, quotes, metrics, prices and product vignettes come from production, the benchmarks or GT's published pages. | Placeholder testimonials and invented enterprise vignettes | Kevin, 2026-08-05: "like these arent real quotes". [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md), 2026-08-11 (round two) | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic) review checklist; [operating-principles.md](https://www.prototemplate.com/docs/handbook/operating-principles) rule 9 | | 2026-08-12 | Once Kevin picks a variant, the alternatives leave the code and stay in git history. | The variants program's switchers and dock, about 16,000 lines | "get rid of the variation system" | [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md), 2026-08-12; [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic) section 2 | | 2026-08-14 | A landing button is the shared `Cta` component with a Title Case label, and its `tracked` value is one of the existing PostHog location slugs. A new slug needs Kevin's approval. | Buttons built by hand on each landing page | Decided in the landing button round (gt-cloud #4348) | `gt-ui/shared-cta` and `gt-ui/cta-title-case`; [gt-landing-pages](https://www.prototemplate.com/skills/gt-landing-pages), [gt-components](https://www.prototemplate.com/skills/gt-components) | | 2026-08-14 | Flags render only through the shared `LocaleFlag` SVGs, on every surface including the dashboard. | Emoji flags | "NO EMOJIS!!! USE OUR FLAGF BLIB RARY!!!" | `no-raw-locale-flags` on gt-cloud's dashboard, landing and `packages/ui`; `packages/ui/src/components/ui/LocaleFlag.tsx`; [gt-components](https://www.prototemplate.com/skills/gt-components) | | 2026-08-15 | PR titles and bodies are lowercase, plain technical English, with no em dashes and no mention of Claude. Kevin repeated it on 2026-09-08. | Generated PR prose | "write up the prs with no reference to claude code and no emdashes in lower case"; 2026-09-08: "make the pr language in my standard technical english lower case" | [gt-voice](https://www.prototemplate.com/skills/gt-voice), Code comments, commits and PR descriptions; [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 4 and `references/pr-body.md` | | 2026-08-17 | Every page gets a second pass against its reference before it is shown. | Pages shipped on their first pass (contact, sign-in, legal) | [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md), 2026-08-17 | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic) section 2; [quality-bar.md](https://www.prototemplate.com/docs/handbook/quality-bar) | | 2026-08-17 | A docs or blog URL never dead-ends. A near miss redirects to its page, a folder goes to its index, and an unmatched path lists the closest pages. | The plain 404 | "u should probably never hit a 404 on docs and blogs" | `resolveDocsPath` in gt-cloud's `apps/landing/src/lib/slug-suggest.ts`, and `apps/landing/src/lib/blog-near-miss.ts` (gt-cloud #4359, merged 2026-09-09); [gt-website](https://www.prototemplate.com/skills/gt-website) `references/docs.md` | | 2026-08-26 | A docs page description opens with an action verb such as "Use" or "Install". Descriptions that open with "Learn how" or "Learn what" are rewritten. | Get Started captions that began "Learn how" | "learn is a terrible verb to start with, we like Use and install" | `content:DOCS-SKILL.md` (the rule for descriptions) | | 2026-08-27 | Stacked PRs use GitHub's native stack, linked with `gh stack link`. Kevin reaffirmed it on 2026-10-05. | A `Stacked on #N` line in the body as the only link between PRs | "use the ACTUAL github stack feature"; 2026-10-05: "i meant use the actual pr stack feature" | [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 6 (`github/gh-stack`) | | 2026-08-27 | gt-cloud commits carry Kevin's General Translation work identity. | Commits under his personal email, which gt-cloud's Vercel projects refuse to deploy | "gt-cloud deployments on vercel wiwll not accept my home email authored commits" | gt-cloud's repository `user.email`, which covers every worktree; [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 2 | ## September 2026 [#september-2026] | Date | Ruling | Replaced | Kevin's words | Held by | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 2026-09-02 | Integration docs go under Integrations. A top-level docs section is reserved for a framework, plus the CLI and the Platform, and Integrations is the default when in doubt. | rrweb as a top-level docs section | "default to putting in the integrations section when in doubt" | `content:DOCS-SKILL.md`, "When something gets its own top-level section"; the top-level list is pinned in content CI by `content:scripts/validate-docs-structure.ts` (content #501) | | 2026-09-03 | An unfinished dashboard feature ships behind a feature flag or does not ship. | Unfinished dashboard pages released without a gate | A team decision Kevin relayed with the fix request | The dashboard's feature flags, managed in the admin app (gt-cloud's `gt-dashboard` skill); [glossary.md](https://www.prototemplate.com/docs/handbook/glossary) | | 2026-09-08 | Review requests go through GitHub reviewer requests, and Kevin chooses the reviewers. | Asking for reviews in a Slack channel | "keep this in mind for all our prs nnow, i connected everything in linear properly" | [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 5, Reviewers | | 2026-09-08 | Writing is plain technical English: no em dashes, metaphors, contrast pairs, fragment rhythm or signposts. | Agent prose with metaphors and parallel constructions | "do it in stragiht standard technical english without any weird metaphoors" | [gt-voice](https://www.prototemplate.com/skills/gt-voice); `no-em-dash`; the humanizer skill in Kevin's wiki | | 2026-09-09 | The brand review works from first principles. Only the company's legal name and the `gt-*` library names customers install are fixed. | None; the brief for the brand review | "just a few things that cant change (the legal name of the company, general translation, annd the nname of libraries our customers use" | No gate; the fixed names are defined in [BRAND.md](https://www.prototemplate.com/docs/handbook/brand) section 1 and [gt-brand](https://www.prototemplate.com/skills/gt-brand) section 1 | | 2026-09-10 | Pricing shows whole-dollar amounts of $1 or more without cents. Cent prices and amounts under a dollar keep two decimals. | Two decimals on every price | "turn $2.00 and $1.00 and anything like this to $2 and $1" | gt-cloud's `packages/ui/src/components/pricing/dollar-format.ts` and `apps/landing/__tests__/usage-dollar-format.test.ts` (gt-cloud #4785) | | 2026-09-10 | Public writing is written for a reader who knows nothing about Kevin, the team or the repository. It defines the product once, opens each section with the point that reader can reuse, and keeps PR numbers and internal names out of the prose. | Drafts that narrated process and cited internal names | "be so much more straightforward and clear to a user who has no idea who I am" | [gt-voice](https://www.prototemplate.com/skills/gt-voice); [quality-bar.md](https://www.prototemplate.com/docs/handbook/quality-bar), Copy | | 2026-09-10 | The docs sidebar on a 404 page is the stock flat tree. | A client-side pathname override that kept the section tree and flashed the flat tree before hydration | "i cann see it flash. should we just have what it is before if its what fuma nama does?" | gt-cloud #4773 (the revert); [gt-website](https://www.prototemplate.com/skills/gt-website) section 4 | | 2026-09-14 | Exploration rounds stay on a branch or uncommitted until Kevin reviews them on localhost and says to land them. The directions in a round differ in silhouette, material and action. | A ten-direction round pushed to Prototemplate main before he saw it | [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md), 2026-09-14 | [prototemplate](https://www.prototemplate.com/skills/prototemplate) section 9; [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 8; [gt-explorations](https://www.prototemplate.com/skills/gt-explorations) | | 2026-09-15 | Git pushes trigger no landing preview deploys. Main and staging deploy, and a preview is deployed by hand from the CLI. | A preview deploy on every push to a PR branch | "this is running up a lot of costs" | gt-cloud's `apps/landing/vercel.json` (`git.deploymentEnabled`), gt-cloud #4828; [gt-website](https://www.prototemplate.com/skills/gt-website) section 8 | | 2026-09-15 | PostHog loading and analytics initialization stay as they are in any PR that is not about analytics. | A PR that deferred PostHog initialization in `packages/ui/src/components/analytics/PostHogProvider.tsx` | "dont touch posthog. evaluate your chnages for stuff that is high risk like posthog" | [gt-website](https://www.prototemplate.com/skills/gt-website) section 4; for onboarding, the parity gate in [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 7 | | 2026-09-18 | Inter is the only typeface. [gt-brand](https://www.prototemplate.com/skills/gt-brand) section 4 names the few exceptions, such as the Prototemplate nameplate and the presenter. | Switzer as the display face beside Inter (the deck dropped it on 2026-09-08) | [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md), 2026-09-18 | `inter-only`; gt-cloud #4887 deleted the Switzer files; [gt-brand](https://www.prototemplate.com/skills/gt-brand) section 4 | | 2026-09-24 | Icons come in two tiers: Heroicons solid for marks that carry meaning, Lucide outline for controls. Brand marks stay as drawn. | Lucide outline glyphs everywhere | Kevin, 2026-09-28: "look for ANYWHERE ELSE icons nneed to be synced" | `icon-tiers` (the rule in gt-cloud #4909, the lint in #5007); [gt-components](https://www.prototemplate.com/skills/gt-components) `references/icons.md` | | 2026-09-24 | The identity stays in the neutral, scalable territory: a grid, one type system, one accent blue, and parts that hold at 16px and in a terminal. | A more opinionated creative territory proposed in the brand review | "need to be scalable in future. not incredibly opinionated. feel like switzerland" | [BRAND.md](https://www.prototemplate.com/docs/handbook/brand); [gt-brand](https://www.prototemplate.com/skills/gt-brand) sections 2 and 3; no gate | | 2026-09-25 | Prototemplate reserves all rights to General Translation, Inc. The repository is public to read, and third-party fonts, icons and adapted skills keep their own licenses. | The MIT license | "make prototemplate not mit licensed anymore." | [LICENSE](https://github.com/Kevin-Liu-01/Prototemplate/blob/main/LICENSE); README, License | | 2026-09-25 | A coding agent runs the Setup for Agents prompt end to end on a fresh app before it ships, and again after any change to the prompt, the CLI or the quickstarts. | Shipping a generated prompt untested | "TRY IT OUT SEE IF IT WORKS AND YOU CAN GET TO CREATING A TRANSLATION". He asked again on 2026-09-30 and 2026-10-01. | [gt-website](https://www.prototemplate.com/skills/gt-website) section 6 and `references/pages.md` | | 2026-09-25 | PR screenshots go into the body from the first push and are refreshed after every visible change. | Screenshots added at the end of review | "add the screenshots of this to the pr the whole time" | [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 4 | | 2026-09-28 | Each column has one rail on each side, drawn once by the `.tc-rail` wrapper. | Doubled outer rails 10px outside each column | "make sure theres nno rules that leead to two side rails" | `single-rail` (gt-cloud #5007, Prototemplate `pnpm lint:code`); `retired-rail-vocabulary` in Prototemplate's `scripts/lint/practices.mjs`; [DESIGN.md](https://www.prototemplate.com/docs/handbook/design) section 3 | | 2026-09-28 | The theme control draws the circle glyphs ◐ and ◑. | Sun and moon icons | [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md), 2026-09-28 | The shared `ThemeToggle` in gt-cloud's `packages/ui/src/components/frame/ThemeToggle.tsx`; `no-theme-icons` on open gt-cloud #4977, which has not reached main | | 2026-09-28 | PR screenshots are tight before and after crops of the changed elements, with the same clip on main and on the branch. | Full-page screenshots in two-column tables | "make this pr page much much easier to read and show the images" | [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 4 and `references/pr-body.md` | | 2026-09-29 | No offer, credit or capability appears in the product before the backend delivers it. | An offer in the redesigned onboarding that the backend did not yet deliver | "we only show it if we actually do it" | The parity gate admits only stricter checks and recovery paths as additions ([gt-ship](https://www.prototemplate.com/skills/gt-ship) section 7); review | | 2026-09-30 | The hero's second button is Setup for Agents, which copies the tested agent prompt. | A Docs button | "change docs into setup for agents with a copy icon and make that into its own pr" | `HeroSecondButton.tsx` and `CopyPromptButton.tsx` in gt-cloud's `apps/landing/src/components/landing/sections/agent/`; the prompt's one source in `content:docs/en-US/overview/for-coding-agents.mdx`, served at `/agent-prompt.md` (gt-cloud #5049); [gt-website](https://www.prototemplate.com/skills/gt-website) section 6 | ## October 2026 [#october-2026] | Date | Ruling | Replaced | Kevin's words | Held by | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 2026-10-01 | GT's identity work takes dithered artifact pictures in black, white and blue as a reference. Kevin sent them to the brand agency with the race-type speed marks he chose on 2026-09-29, and he called the marks an experiment. The doubled-line monogram stays the current mark. | None; it builds on the 2026-09-24 territory | "artifacts, dithered, we like this aeshteitc of black white blue"; on the marks: "nothing but funnu experiementation" | [gt-dither](https://www.prototemplate.com/skills/gt-dither) (the pictures); `scripts/build/speed-marks.mjs` and `/marks` in Prototemplate; [gt-brand](https://www.prototemplate.com/skills/gt-brand) section 5 | | 2026-10-01 | A release PR carries no development code. Galleries, stand-ins, test-only hooks, dev routes, recorded fixtures and harnesses stay on their own branch or in the scratchpad. | A release branch that carried the dev states gallery and a 24k-line parity suite | "please cleanup / remove all dev / temporary 'testing' code" | [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 3, Verification instruments stay out, and `skills/gt-ship/scripts/pr-size.mjs` | | 2026-10-02 | An onboarding or auth redesign keeps every tracking call and backend side effect that production has. Parity is proven by recording production, replaying the release and diffing the two ledgers. | Judging parity from a reviewer's list of intended changes | "we need the new onboarding to keep all the tracking and backend stuff that the old onboarding did" | [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 7, The parity gate | | 2026-10-02 | A PR's line count is part of its review. Before a push the diff is grouped into product, tests, fixtures, scripts and lockfile, and every large group has a reason. | A release PR of about 39,500 lines, most of them a recorded parity suite | "why is 5063 39.5k lines? fix this." | `skills/gt-ship/scripts/pr-size.mjs`; [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 3 | | 2026-10-02 | A public repository with a third-party licensing problem stays public, and the third-party material is removed or replaced. Kevin ruled this in one of his own repositories, and it applies to Prototemplate. | Making a repository private to settle third-party terms | "never privatize the repo. simply remove third party models." | Review; each picture's source and license are confirmed when it is added ([gt-dither](https://www.prototemplate.com/skills/gt-dither) sections 5 and 7); [LICENSE](https://github.com/Kevin-Liu-01/Prototemplate/blob/main/LICENSE) lists Prototemplate's third-party material | | 2026-10-02 | A PR is closed only when its change is already on main. The closing comment names the merged PR that carries the change, and the branch stays. Every other PR stays open for Kevin to decide. | Closing stale and superseded PRs in bulk | "only delete prs that are already in the codebase" | [gt-reporting](https://www.prototemplate.com/skills/gt-reporting) section 5 (close candidates in the PR slate); the closing comment is the check | | 2026-10-03 | Each session owns one lane, and work for another lane goes to that session by name. | Sessions doing each other's work, such as a site session making films | "that videos one should be making them, and this one is just for prototemplate work" | [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 1; [prototemplate](https://www.prototemplate.com/skills/prototemplate) section 8; [multi-session-playbook.md](https://www.prototemplate.com/docs/handbook/multi-session-playbook) | | 2026-10-03 | Content about places is organized by language. It names and outlines no country or territory, and its facts come from CLDR. | A brief for a map of countries drawn in their languages | "avoid political sensitivity without making it look forced" | [gt-website](https://www.prototemplate.com/skills/gt-website) section 6; [gt-product-map.md](https://www.prototemplate.com/docs/handbook/gt-product-map) section 6, Geography | | 2026-10-05 | Prototemplate is the hub and wiki of how GT work is done, with a curated skill set in `skills//` that holds only the skills in real use. | A generated dump of 261 wiki skills on `/skills` | "this prototemplate is tupposed to be my hub for general trnalsation work"; on `/skills`: "we only need to show and store the ones weve actually been using" | [prototemplate](https://www.prototemplate.com/skills/prototemplate) section 10; `/skills` renders only the set | | 2026-10-05 | Every element resolves through the type tokens to one face, the self-hosted rsms InterVariable 4.1. | Faces set differently from element to element, and Google's build of Inter | "esepcially encorce the CORRECT RASMUS INTER". [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md), 2026-10-05 | `inter-only` in gt-cloud; `pnpm lint:type` in Prototemplate (`scripts/lint/type.mjs`, from the 2026-10-05 type round); [gt-brand](https://www.prototemplate.com/skills/gt-brand) section 4 | | 2026-10-05 | Artifact pictures carry no readable English prose. | The dictionary, Johnson, OED and Oxford pictures | [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md), 2026-10-05 | `pnpm lint:pictures` and [docs/ARTIFACT-PICTURES.md](https://www.prototemplate.com/docs/handbook/artifact-pictures) in Prototemplate; `check:artifact-pictures` on open gt-cloud #5133; [gt-dither](https://www.prototemplate.com/skills/gt-dither) | | 2026-10-05 | A book head's title is the page's plain name (Brand, Documentation, Handbook), and the head's right side is a panel: the last update, then the page's own facts, each with an icon. | Titles that were phrases ("The brand", "Prototemplate docs") beside a plain meta table | "dont make it "the brand" "prototemplate docs" it should be Brand and Documentation and make the right side of those headers better, show last update, use icons" | `src/lib/page-names.ts`; `pnpm lint:heads` (S1, S5, H5) and `pnpm lint:updated`; [prototemplate](https://www.prototemplate.com/skills/prototemplate) section 3 | | 2026-10-05 | Rounded controls, square shells: buttons, fields and segmented controls round at 6px, chips and key caps at 4px, cards and popovers at 6px, and only the UI shells are square. | Square boxes everywhere outside a short list of exceptions | "i want to enforce and keep this amount of border rounding instead of forcing Boxes. boxes only for ui shells" | `pnpm lint:radius` (`scripts/lint/radius.mjs`); [DESIGN.md](https://www.prototemplate.com/docs/handbook/design) section 2, Corners; [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic) section 3 | | 2026-10-06 | Every page with a book head renders one structure: nothing is drawn between the sheet's top border and the title, one hatch band carries a rule on both edges, and the mast, the panel and the section dividers are the same on every page. | Heads that differed from page to page, a guide line through the title, and a band without its top rule | "see how the brand is touching the up line and the spacer is missing a border above it? standardize our presentation more" | `pnpm lint:heads` and `pnpm lint:heads:live`; [DESIGN.md](https://www.prototemplate.com/docs/handbook/design) section 4, The book page; [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md), 2026-10-06 | | 2026-10-06 | The brand's head shows a reading time like the other pages and names no reference application. The brand has moved past the Dossier, and BRAND.md section 8 ("The completed reference") waits for Kevin's decision. | The Dossier as the brand's completed reference (the 2026-08-06 row) | "why is the dossier the reference for Brand? its evolved so much more since then", then "just give it a reading time like the other pages." | `/brand`'s panel; [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md), 2026-10-06; no gate | | 2026-10-06 | Simplify. The platform feature that removes code comes before new code, and machinery a review asks to test is removed when it need not exist. | A caption card that measured its own height with an observer and component state | "Beautiful but don't complicate. Always simplify." | [operating-principles.md](https://www.prototemplate.com/docs/handbook/operating-principles) rule 12; [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic) | | 2026-10-07 | The shipped site is the reference for landing and marketing pages: generaltranslation.com, built from `apps/landing` on gt-cloud origin/main. The Dossier stays in the gallery as the direction the site grew from. BRAND.md section 8 ("Where it ships") names where the identity ships: the site, the product surfaces that follow the brand deck, the films on `/motion` and the marks. | The Dossier as the completed reference for the site (the 2026-08-06 row), and the decision on BRAND.md section 8 that the 2026-10-06 row left open | "fix the dossier references." | [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic) section 1; [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md), 2026-10-07; [BRAND.md](https://www.prototemplate.com/docs/handbook/brand) section 8; no gate | | 2026-10-07 | A skill's title and a document's h1 name the thing with no leading "The": Brand and the correct Inter, Brand deck, Landing page grammar and GT website for the skills, and Graphics pipeline, Libraries and Ship loop for the documents. | Titles that opened with "The" ("The brand deck", "The Ship Loop") | "fix the The titles." | `scripts/build/skills.mjs` fails a skill title that opens with "The" and a skill whose h1 differs from its `metadata.title`; review holds the documents' h1s; the 2026-10-05 row on head titles | | 2026-10-07 | Prototemplate is served at three addresses: [www.prototemplate.com](http://www.prototemplate.com) from the team's Vercel project, and prototemplate.vercel.app and prototemplate.kevinliu.studio from Kevin's personal project. On 2026-10-07 prototemplate.kevinliu.studio served the same deployment as prototemplate.vercel.app. | [www.prototemplate.com](http://www.prototemplate.com) as the one live domain, with prototemplate.vercel.app as the personal project's alias | "prototemplate.com and vercel.app are both ok, but prototemplate.kevinliu.studio shoul dbe a thing too." | [README.md](https://github.com/Kevin-Liu-01/Prototemplate/blob/main/README.md), What is here; [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 8 | | 2026-10-07 | The film cuts in review are approved for the site. jihe-yuanben is published under CC BY-SA 4.0, and its credits file says so. Kevin signed off the last open items, the non-English takes that waited for a native listener, and expects those films to be updated. | Cuts held for Kevin's approval, the share-alike license of jihe-yuanben left to his decision, and non-English takes held for a native listener | "film cuts approved. jihe is yes. and the last ones are signed off, theyre going to be updated anyways" | [gt-films](https://www.prototemplate.com/skills/gt-films) sections 7, 8 and 11; `public/motion/published.json` | | 2026-10-08 | On a page that already exists, "enhance" keeps the page and adds to it. Copy beside a team photo names nobody in it and states no headcount. | A careers rebuild that dropped the hero, the customer band and the original copy | "keep the original write up" | [operating-principles.md](https://www.prototemplate.com/docs/handbook/operating-principles) rule 15; [quality-bar.md](https://www.prototemplate.com/docs/handbook/quality-bar), Copy; [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic) | | 2026-10-10 | Prototemplate commits author as Kevin with his GitHub noreply address and keep the session's `Co-Authored-By:` trailer. | Commits under his personal or his work address | Answered in the review of the system v2 plan | [docs/SHIP-LOOP.md](https://www.prototemplate.com/docs/handbook/ship-loop) section 6; [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 2 | | 2026-10-10 | [www.prototemplate.com](http://www.prototemplate.com) is the canonical address. prototemplate.vercel.app and prototemplate.kevinliu.studio stay as aliases. | prototemplate.vercel.app as the address in the site metadata and `llms.txt` | The recommended default, accepted in the same review | `SITE_URL` and `public/llms.txt`; [README.md](https://github.com/Kevin-Liu-01/Prototemplate/blob/main/README.md), What is here | | 2026-10-10 | A customer or partner is named in this repository only where generaltranslation.com shows it. | A list in BRAND.md section 9 that named a customer the site no longer showed | The recommended default, accepted in the same review | [BRAND.md](https://www.prototemplate.com/docs/handbook/brand) section 9; `pnpm lint:public` and its private denylist | | 2026-10-10 | gt-cloud's internal material stays out of the public skills: its dev-environment internals and the file maps of its private packages live in gt-cloud's own skills. PR numbers and branch names may appear. The copies of the gt-ui lint plugin and the plate stay at the copy taken, with no re-sync until gt-cloud's product and infrastructure owners agree. | Skill sections that described gt-cloud's dev infrastructure and package file maps | The recommended default, accepted in the same review | `pnpm lint:public`; `scripts/lint/copies.json`; [prototemplate](https://www.prototemplate.com/skills/prototemplate) | | 2026-10-10 | The curated skills reach gt-cloud sessions as project links made by `pnpm skills:install --project` on a gt-cloud checkout and listed in that checkout's `.git/info/exclude`, so nothing gt-cloud tracks changes. | Skills that no gt-cloud session loaded | Answered in the same review | [README.md](https://github.com/Kevin-Liu-01/Prototemplate/blob/main/README.md), Skills | | 2026-10-10 | The film sources in `motion/` are tracked through an allowlist: the kit code and one entry per published film. Unreleased films stay untracked, and Kevin reads the first staged diff. | `motion/` untracked as a whole | Answered in the same review | The motion block in `.gitignore`; [gt-films](https://www.prototemplate.com/skills/gt-films) | | 2026-10-10 | Finished round specs move to `docs/archive/`, with machine paths replaced by placeholders. | Finished and abandoned specs kept beside the live sources | The recommended default, accepted in the same review | [docs/archive/README.md](https://github.com/Kevin-Liu-01/Prototemplate/blob/main/docs/archive/README.md); `pnpm lint:public` | | 2026-10-14 | `/docs` is a docs site of ten pages on how Prototemplate works, built on fumadocs with the notebook layout generaltranslation.com/docs runs, outside the viewer shell, and written to serve as a template for another project. The canon documents (AGENTS.md, BRAND.md, DESIGN.md, ARCHITECTURE.md, the ship loop, the tools, the libraries with the build log, the graphics pipeline and the artifact pictures) joined the handbook at `/handbook/`, and their old `/docs/` addresses redirect there. | `/docs` as a book of the repository documents on the viewer shell, with the build log under the readme | "i want prototemplate to have its own proper seo system we can also make a very small like 10 page version of the docs site that just explains how it works and totally serves as a template" | `content/docs`, `src/app/docs`, `src/app/handbook/registry.ts`, the redirects in `next.config.ts`; `pnpm lint:registries`, `pnpm lint:seo` | | 2026-10-20 | The handbook and the canon documents are a section of the docs site at `/docs/handbook/`. Each page includes its repository file, so `docs/handbook/` and the canon files stay the one source, and `/handbook` and `/handbook/` redirect to the new addresses. The libraries page carries the live build log, and pages set live figures beside the sections they illustrate. | The handbook as a book on the viewer shell at `/handbook/` (the 2026-10-14 row) | "put the handbook into prototennokate docs, with a ton more super cool componnents in there now" | `content/docs/handbook/`; the redirects in `next.config.ts`; `pnpm lint:registries`, `pnpm lint:seo` | ## Superseded July practices [#superseded-july-practices] Notes, commits and transcripts from July 2026 still describe these practices, and no row above names them. The Replaced column of the tables above covers the other July practices: Lenis, headings with periods, Switzer and the July bar. | July practice | Replaced by | Date | Held by | | ------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Entrance animation, such as a fade-in on the landing hero | Kevin, 2026-08-06: "dont have a fade in animation in hero". Product pages have no entrance animation since 2026-09-28. The landing's sections keep one quiet reveal as they scroll into view, and other motion shows a real state change. | 2026-08-06; product pages 2026-09-28 | [gt-motion](https://www.prototemplate.com/skills/gt-motion) section 1 and section 6, The landing's quiet reveal | | Liquid glass and glassmorphism. Kevin, 2026-07-30: "MAKE THIS HAVE A BACKGROUND WINDOW DISTORTION GLASSMORPHISM" | The avoid list refuses the AI gradient and glass look. | 2026-08-11 | Review ([gt-brand](https://www.prototemplate.com/skills/gt-brand) section 7) | | DirectionDock, the control dock on every direction page. Kevin, 2026-07-30: "remove this thing on each page. this is a /present specific feature" | One pill took over the presenter's controls and DirectionDock was retired (Prototemplate 502dc0a). It returned the same night as a top-left back button on the standalone direction pages, hidden under `?chrome=0` (08b33c6). On 2026-09-08 `DirectionCorner` (`src/components/viewer/DirectionCorner.tsx`) replaced it on the viewer shell, and DirectionDock was deleted (c064945). | 2026-07-31; 2026-09-08 | [gt-components](https://www.prototemplate.com/skills/gt-components), Prototemplate's viewer shell; [prototemplate](https://www.prototemplate.com/skills/prototemplate) section 1 | | gt-cloud's `apps/redesign` on port 3006 as the place where canon was edited | Prototemplate main is the only copy, and canon is edited there. `apps/redesign` stopped being a copy, nothing is copied in from another tree, and Prototemplate's dev server runs on 3005. | 2026-09-08 | [ARCHITECTURE.md](https://www.prototemplate.com/docs/handbook/architecture), The mirror; [docs/SHIP-LOOP.md](https://www.prototemplate.com/docs/handbook/ship-loop) section 7; [prototemplate](https://www.prototemplate.com/skills/prototemplate) section 1 | ## Recorded elsewhere [#recorded-elsewhere] * Version pins, ports, open PR states and other temporary technical states describe the code on a date and change without a ruling. They live in [gt-website](https://www.prototemplate.com/skills/gt-website) section 8 and the Sources lines of the skills. The landing's Next.js pin is one of them. * The full wording of each design verdict, with what was wrong and what Kevin approved, is in [verdicts.md](https://www.prototemplate.com/skills/gt-aesthetic/references/verdicts.md). The table in [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic) section 2 summarizes it. * The Sources lines of `gt-voice`, `gt-website`, `gt-ship` and `gt-lints` list the directives each skill was written from. This log puts every ruling in one dated list with the practice it replaced. ## Sources [#sources] * Kevin's messages to Claude Code and Codex from 2026-07-20 to 2026-10-05, about 5,800 in all; his rulings on the page heads, the corners and the brand's reference on 2026-10-05 and 2026-10-06, and on the Dossier references, the titles, the addresses and the films on 2026-10-07; and the session transcripts behind them for context. Quotes keep his spelling. Three quotes come from messages the corpus cut short or queued with an image, and were read in the transcripts: the 2026-08-11 eyebrow line, the 2026-09-25 screenshot line and the 2026-09-30 hero line. * Prototemplate, read 2026-10-05: `skills/gt-aesthetic/references/verdicts.md` and `SKILL.md` sections 1 and 2; `skills/gt-ship` sections 1 to 8, `references/pr-body.md` and `scripts/pr-size.mjs`; `skills/gt-lints` and `references/gt-ui-rules.md`; `skills/gt-website` sections 3 to 8 and `references/docs.md` and `references/pages.md`; `skills/gt-voice`, Headings and casing; `skills/gt-brand` sections 1 to 5 and 7; `skills/gt-components`; `skills/gt-motion` sections 1 and 6; `skills/gt-dither` sections 5 and 7; `skills/gt-landing-pages`; `skills/gt-reporting` section 5; `skills/prototemplate` sections 1 and 8 to 10; `BRAND.md`; `DESIGN.md` section 3; `ARCHITECTURE.md`, The mirror; `LICENSE`; `README.md`; `docs/ARTIFACT-PICTURES.md`; `package.json`; `.oxlintrc.json`; `scripts/lint/practices.mjs`; `scripts/lint/type.mjs` in the working tree; commits 502dc0a and 08b33c6 (2026-07-31 Pacific), 229d413 and c064945 (2026-09-08). * gt-cloud at origin/main e17fce499 (2026-10-05): `.oxlintrc.json` and `tooling/oxlint-plugins/gt-ui.ts`, `apps/landing/src/app/robots.ts`, `apps/landing/vercel.json`, `apps/landing/gt.config.json`, `apps/landing/staging.gt.config.json`, `scripts/deploy-landing.sh` and `packages/ui/src/components/pricing/dollar-format.ts`; merged PRs #4213, #4359, #4773, #4785, #4828, #4887, #4909, #5007 and #5049; open PRs #4977 and #5133. * generaltranslation/content at main, read 2026-10-05: `DOCS-SKILL.md` ("When something gets its own top-level section" and the rule for descriptions); PR #501. * Kevin's wiki: the humanizer skill. * Kevin's Claude Code memory notes for gt-cloud, which stay private (25 notes; 5 added on 2026-10-10 from an allowlist of standing rulings, each row written fresh). * The review of the system v2 plan on 2026-10-10: Kevin's answers and the recommended defaults he accepted, for the 2026-10-10 rows. * Kevin's request of 2026-10-14 for the SEO system and the docs site, and his answers to the plan's open questions that day, for the 2026-10-14 row. * Kevin's request of 2026-10-20 to move the handbook into the docs site with live components, for the 2026-10-20 row. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Agents on one machine > Agent sessions on one workstation: stopping processes by id, zsh traps, exit-status gates, memory and disk, long commands and shared checkouts. URL: https://www.prototemplate.com/docs/handbook/shared-machine Kevin Liu runs many coding agent sessions on one Mac for his General Translation (GT) work: Claude Code sessions, Codex sessions, the subagents they launch, and his own apps beside them. The sessions share the machine's processes, its shell, its memory and its disk. Each trap below stopped other sessions' work at least once between August and October 2026, and each section gives the rule, the incident that set it, the fix and the skill that holds the procedure. The [multi-session playbook](https://www.prototemplate.com/docs/handbook/multi-session-playbook) holds the rules for lanes, branches and messages between sessions. ## Stopping a process [#stopping-a-process] Stop a process by the process id recorded when it started. macOS ships the BSD `pkill`, which stops reading options at its first pattern, so every argument after the pattern becomes another pattern and matches far more than the command meant. | Date | Command | What it matched | What it stopped | | ---------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | 2026-10-01 | `pkill -f "" -P 1` | Every command line that contained "1" | About 140 Chrome processes, other sessions' dev servers, a test run and the session's own renders | | 2026-10-13 | `pkill -f "next build --turbopack" -U $(id -u)` | Every command line that contained "501", the user id, which is part of the path of the agents' shared temp folder | Another session's deploy gate after hours of running, and several background waiters | Record `$!` for every process you start, check it before you stop it, and stop it with `kill`. A dev server is found by the port it listens on. The `-sTCP:LISTEN` filter matters: without it, the kill also reaches a browser connected to that port. ```sh pnpm dev --port 3011 > dev.log 2>&1 & pid=$! # record the id at launch ps -o pid,ppid,command -p "$pid" # the command is still yours lsof -p "$pid" -d cwd # and it runs in your folder kill "$pid" kill $(lsof -ti tcp:3011 -sTCP:LISTEN) # a server, by the port it listens on ``` When matching by name cannot be avoided, put every option before the pattern and run `pgrep -fl` with the same arguments first, so the list of matches is read before anything is signalled. Every agent brief carries the rule, because a subagent knows only its brief. The procedure is in [harness-traps.md](https://www.prototemplate.com/skills/gt-orchestration/references/harness-traps.md) of `gt-orchestration` (Processes on a shared machine), and the port kill in [gt-local-dev](https://www.prototemplate.com/skills/gt-local-dev) section 2. ## Shell traps in zsh [#shell-traps-in-zsh] Claude Code runs its shell commands in the user's shell, which is zsh on a Mac. Nine zsh behaviours differ from bash, and each one either fails silently or stops the rest of a command line. | Trap | What happens | Fix | | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | A variable named `path` | zsh ties the `path` array to `PATH`, so `path=apps/x` replaces the search path and every later command reports "command not found" | Name it `p`, `file` or `target` | | `"$B:apps/landing"` | zsh reads `:a` as the absolute-path modifier and rewrites the string | Brace the name: `"${B}:apps/landing"` | | `timeout 90 pnpm lint \|\| echo failed` | macOS has no GNU `timeout`, and the `\|\| echo` hides the "command not found" | `perl -e 'alarm 90; exec @ARGV' pnpm lint` | | `for u in $urls` | An unquoted variable does not split on spaces, so the loop runs once over the whole string | An array, an explicit list, or `${=urls}` | | Several heredocs on one `&&` chain | zsh feeds the bodies in operator order, and a Python body once became a pushed commit message | Write each message or script to a file first, then `git commit -F ` | | `grep --include=*.tsx` | zsh expands the glob itself and fails with "no matches found" | Quote it: `--include='*.tsx'` | | A glob with no match | The whole command line aborts with "no matches found", so `grep x a/*.md b/*.md` greps nothing when `b/` has no Markdown | `find ... -print0 \| xargs -0 grep`, or quote the pattern for a tool that globs itself | | `echo ====` | A word that starts with `=` is a command lookup, so the line fails with "=== not found" and the rest of the chain stops | Quote it: `echo '----'` | | `$TAIL[m]` in an ffmpeg filter graph | zsh reads `[m]` as an array subscript | `${TAIL}[m]` | The heredoc trap is the costly one, because its result reaches the remote before anyone reads it. A commit message written to a file with `cat > msg.txt <<'EOF'`, as a command of its own, cannot pick up another heredoc's body. [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 2 holds the commit form, [gt-local-dev](https://www.prototemplate.com/skills/gt-local-dev) section 1 the quoting rules for port loops, and [sound.md](https://www.prototemplate.com/skills/gt-films/references/sound.md) of `gt-films` the ffmpeg case. ## Exit-status gates [#exit-status-gates] A command that must stop on a failure chains to the next step with `&&` on the real exit status. A `;` ignores the status, and a pipe replaces it with the status of the last command in the pipe, because zsh runs without `pipefail` unless it is set. | Date | Command | What happened | | ---------- | -------------------------------------------------------- | ----------------------------------------------------------------- | | 2026-08-07 | `pnpm build > log; git add -A && git commit && git push` | A broken build reached main: the `;` ignored the build's exit 1 | | 2026-08-11 | `pnpm build 2>&1 \| tail -2 && git ...` | A failing build was pushed: the pipeline returned `tail`'s exit 0 | | 2026-10-14 | `headroom can-start ... \| tail -1 && node check.mjs` | A browser started while the memory check said to wait | Capture output to a file and gate on the command itself. When a report needs the number, capture the exit code on its own line. ```sh pnpm build > build.log 2>&1 && git push origin HEAD:main || tail -40 build.log pnpm build > build.log 2>&1; echo "build exit $?" # the real code, for a report set -o pipefail # only where a pipe cannot be avoided ``` The rule is held by the [ship loop](https://www.prototemplate.com/docs/handbook/ship-loop) section 7, [gt-lints](https://www.prototemplate.com/skills/gt-lints) section 5 and the [Gates](https://www.prototemplate.com/docs/gates) page of the docs, which runs the build gate this way. ## Memory and disk [#memory-and-disk] Gate each heavy job on its own memory estimate, and check free disk before a large batch. A runaway process on a shared workstation can take the whole machine faster than any alert reports it, and a crash restarts every session at once. | What was measured | Value | | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | The workstation | A 128GB Mac that ran out of application memory twice on 2026-10-12 | | One runaway node process | 457GB | | All node processes at the crashes | 243 to 273 processes holding 91 to 555GB | | The agent desktop app | 232GB with about 25 sessions open | | `git grep -E` over 130 ORed ids without `-I`, 18 threads, in a repository with binary assets | Grew from 42.6 to 166GB in about 55 seconds | | A local model trial on the GPU | Grew from 0 to 87GB in about 66 seconds | | A gt-cloud worktree with its install | 4.3GB in `du`; removing one frees about 1.15GB, because pnpm clones files from its store | | Leftover scratch worktrees on 2026-10-05 | 48 worktrees in 259GB of scratch space, with 27GB of the disk left free | | The disk on 2026-10-14 | 100% full | Headroom, Kevin's open-source menu bar app, reads the machine's memory, and its `can-start` command exits 0 when a job of the stated size fits and 1 when it should wait. A literal search runs with `-F` and a pattern file, and `git grep` gets `-I` to skip binaries and a thread count. A torch process gets a watermark ratio, so it fails with an out-of-memory error inside itself before it starves the machine. A scratch worktree is removed as soon as its branch is pushed. ```sh headroom can-start --gb 6 --label build && pnpm build # 0 starts the job, 1 holds it df -h /System/Volumes/Data # free disk, before a large batch rg -F -l -f patterns.txt # many literal ids at once git grep -I -F -f patterns.txt --threads 4 PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.35 python trial.py # caps the GPU allocator for this process xargs -P 6 -n 1 git worktree remove --force --force < pushed-worktrees.txt ``` A worktree goes on the removal list only after `git -C status --short` prints nothing and `git -C branch -r --contains HEAD` names a remote branch. One removal job hit the two-hour background limit after removing 35 worktrees one at a time, and the parallel form above removed the rest in 12 minutes. A crash ends in a reboot, and a reboot empties the system temp folder that holds the sessions' scratchpads (helper scripts, ship records, scratch worktrees) and stops every process. The checkouts, files under `~/.config` and the agents' run journals survive, so a spec or a helper that must outlive the day goes into a repository or a durable folder. [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 1 holds the worktree checks, and [harness-traps.md](https://www.prototemplate.com/skills/gt-orchestration/references/harness-traps.md) of `gt-orchestration` holds what survives a reboot. ## Long commands [#long-commands] An agent's background shell task is stopped at its timeout, which is at most two hours in Claude Code. A job that may run or wait past about 90 minutes starts detached from the session, writes its own process id, and is watched by a poll that ends on a milestone or on the job's exit. ```sh (nohup sh -c 'echo $$ > job.pid; exec ./long-job.sh' >> job.log 2>&1 < /dev/null &) # parent PID reads 1 pid=$(cat job.pid) until grep -q 'DONE' job.log || ! kill -0 "$pid" 2>/dev/null; do sleep 20; done # run as a background task, re-armed every two hours ``` On 2026-10-08 lane agents hung for two to five hours on shell calls that never started, which fits a confirmation prompt that a subagent cannot answer. Lane prompts now hold these rules: * No `rm` of any kind in a lane's shell call. Deletions go to the lead or to a helper that refuses paths outside the scratchpad, and a symlink is replaced with `ln -sfn`. * File edits and deletions go in separate calls. * A git network call (`push`, `fetch`, `ls-remote`) runs alone under an alarm: `perl -e 'alarm 90; exec @ARGV' git push origin HEAD`. * Every background worker is redirected (`> worker.log 2>&1 &`), and every long wait runs in the background and is polled. * A long runner is resumable: it skips the passes it finished and reruns only its failures. [harness-traps.md](https://www.prototemplate.com/skills/gt-orchestration/references/harness-traps.md) of `gt-orchestration` holds these rules under Long commands and Bash calls inside lanes. ## One checkout and several sessions [#one-checkout-and-several-sessions] In a checkout that several sessions share, stage and commit only explicit paths. The index, the working tree and the branch are shared, so any command that takes "everything" takes the other sessions' work too. The [multi-session playbook](https://www.prototemplate.com/docs/handbook/multi-session-playbook) sections 4 and 5 hold the rules, and these are the incidents behind them. | Date | What happened | The rule it set | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | | 2026-08-07 | A bare `git commit` after `git add ` published another session's staged deletions inside a landing commit | Commit with a pathspec | | 2026-08-07 | A desktop git client's revert, run mid-round, left conflict markers in four CSS files | Sweep for markers before every push or copy | | 2026-08-07 | A co-editing session that commits with `git add -A` folded another session's uncommitted files into its own commit, and "nothing to commit" meant they were already in | Verify by content, and commit your own files promptly | | 2026-08-26 | A copy into the tree with `rsync --delete`, run without a fetch, erased a 20-commit campaign another session had pushed | Fetch and read `HEAD..origin/main` before any copy or push | | 2026-10-01 | `git add -A` swept 229 files (92MB) of another lane's folder onto main | Stage explicit paths | ```sh git status --short # read paths you did not touch git add src/app/page.tsx docs/handbook/review.md git commit -F msg.txt -- src/app/page.tsx docs/handbook/review.md git grep -n '^<<<<<<< ' -- src docs deck skills scripts # any hit stops the push ``` Rerun a failure once before diagnosing it when another session may be mid-save: a 500 from hot reload or a type error in a file another session is editing often clears on the second run. [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 2 holds the staging rules, and the [ship loop](https://www.prototemplate.com/docs/handbook/ship-loop) sections 0 and 7 hold the landing order. ## Sources [#sources] * Prototemplate, read on 2026-10-21: `skills/gt-orchestration/SKILL.md` sections 2, 6 and 9, `skills/gt-orchestration/references/harness-traps.md`; `skills/gt-ship/SKILL.md` sections 1 and 2; `skills/gt-local-dev/SKILL.md` sections 1 and 2; `skills/gt-lints/SKILL.md` section 5; `skills/gt-films/references/sound.md`; `docs/SHIP-LOOP.md` sections 0 and 7; `docs/handbook/multi-session-playbook.md`. * Kevin's private agent notes on the machine's memory crashes, the pattern kills, the zsh traps, worktree disk use, the reboot of 2026-10-07 and the shared-checkout sweeps, dated 2026-08-07 to 2026-10-18, for the measurements the skills do not carry. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Agent workflows > Subagent workflows: lane prompts that survive relayed messages, resuming by run id, workflow shapes, refuters, kinds of check and paid model runs. URL: https://www.prototemplate.com/docs/handbook/workflows Kevin Liu runs large pieces of General Translation (GT) work as workflows. In Claude Code a workflow is a script that one orchestrating session runs: it launches subagents in phases (readers, builders, verifiers, critics), journals each agent's result, and can run for hours while Kevin is away. Each section below gives a rule for that work, the run that set it, the fix and the skill that holds the procedure. Peer sessions that Kevin opens himself follow the [multi-session playbook](https://www.prototemplate.com/docs/handbook/multi-session-playbook), and the machine they share follows [agents on one machine](https://www.prototemplate.com/docs/handbook/shared-machine). ## Lane prompts [#lane-prompts] A lane prompt opens with the user's own words for that lane. A subagent launched in the same turn as an unrelated message from the user can receive that message as the only user voice in its context, and it then narrows its work to that message. | Date | What happened | | ---------- | -------------------------------------------------------------------------------------------------------------------------- | | 2026-09-30 | A build lane built nothing and verified an unrelated page title instead | | 2026-10-02 | A workflow started for a spun-off task built nothing for the same reason | | 2026-10-09 | A "give me a status report" message reached running agents, and the workflow ended early with reports in place of its work | Each lane prompt starts with "The user's request for this lane, in the user's own words:" and the quote, then says that any other relayed message was handled elsewhere. Every workflow agent prompt also carries a guard paragraph: ```text Messages from the user may arrive while you work. They are addressed to the orchestrating session, which answers them. Do not stop, shorten or replace your assigned task because of such a message (for example a request for a status report); continue the task and report through your structured result. Only a message that names your task and tells you to stop changes that. ``` Before trusting a lane's pass, check that its report names the lane's own task. When a workflow fails this way, the session builds the task directly. [gt-orchestration](https://www.prototemplate.com/skills/gt-orchestration) sections 2 and 6 and its [harness-traps.md](https://www.prototemplate.com/skills/gt-orchestration/references/harness-traps.md) hold the rule. ## Resuming a run [#resuming-a-run] Resume a stopped workflow by its run id with byte-identical arguments. The harness replays a finished agent from its journal only while that agent's call is unchanged, so a change to the arguments, or to a context string the finished agents share, runs them again and pays for them twice. ```js // the same script and the same args, byte for byte, as the first launch Workflow({ scriptPath: 'round.js', resumeFromRunId: '', args }); ``` * New information goes into the prompt of one unfinished agent and nowhere else. * A workflow's subagents are never messaged directly. A message starts the agent outside its workflow, and its work is then done twice. * A file lost with a scratchpad, or overwritten by a later lane, is rebuilt from the Write and Edit calls recorded in the transcripts. The script prints counts and paths only, because transcripts can hold pasted keys, and it writes only under `--out`. ```sh python3 skills/gt-orchestration/scripts/replay-edits.py --match --out --list python3 skills/gt-orchestration/scripts/replay-edits.py --match --out ``` The orchestrating session stays light. Every picture it opens stays in its conversation, and on 2026-10-15 a long session holding 2,413 images reached 18GB in its own process and had to be restarted. A subagent looks at pictures and returns a verdict, and a session that grows heavy writes its state to disk and hands off to a fresh one. [gt-orchestration](https://www.prototemplate.com/skills/gt-orchestration) section 6 holds the resume procedure. ## Workflow shapes [#workflow-shapes] Run one shape per workflow, and read its result before choosing the next. A build fan-out starts from a spec of about 1,800 words or fewer that holds the decisions with their numbers, gives each lane its files and states the exact interface between lanes. | Shape | Phases | Used for | | ----------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Understand | Map | Readers map a code area with file and line numbers before a round | | Design | Map, Design, Judge, Spec | Three designers take different angles, a taste judge and an engineering judge score them, and one agent writes the spec from the winner | | Build | Build, Verify, Fix, Review | Lanes build from the spec and are verified against its acceptance criteria, with a cap on fix rounds | | Review | Find, Verify, Synthesize | A pull request or a feature before merge, one lens per finder | | Prove | Review, Mutate or Refute, Critic | A risky change: refuters, plus mutants that show the tests catch a revert | | Sweep | Sweep, Audit or Verify | One mechanical change over many files, one agent per file group, then an adversarial re-read | | Parity | Record, Synthesize, Fix, Verify | A redesign that must keep every tracking call and side effect of production | | Probe | Probe, Verify | A hunt across the real inputs of a running server, one hunter per input class | | Investigate | Investigate, Verify, Synthesize | A performance or behaviour question, one read-only agent per dimension with two skeptics each | | Research | Research, Verify, Critique, Gap research, Synthesize | Facts a build will rest on, each note checked against primary sources | | Package | Package, Check | Finished work cut into stacked branches from main, each gated in a clean checkout | | Gauntlet | Build consumers, Matrix, Hostile inputs, Verify | A library change run on real consumer apps, the same request matrix on the change and on main, diffed | A build lane reports in about 700 words or fewer: each file with one line, deviations from the spec with reasons, and the commands it ran with their results. A sweep or review lane returns a `checked` list (what it inspected and found clean) and an `uncertain` list, and leaves any value it cannot classify, because a missed change costs one more pass while a wrong one breaks the work. [workflow-shapes.md](https://www.prototemplate.com/skills/gt-orchestration/references/workflow-shapes.md) holds each shape with its schemas. ## Refuting findings [#refuting-findings] Every critical and major finding goes to three refuters before anyone acts on it. Each refuter has its own lens and defaults to refuted when it cannot substantiate the claim, and a finding stands when at least two of the three fail to refute it. | Lens | The question it answers | | ------------ | --------------------------------------------------------------------------------------------- | | Correctness | Is the claim true of the code on the pinned head commit? | | Reproduction | Can the problem be observed in the code path, in the browser or by measurement? | | Necessity | Would the fix change behaviour, bytes or a reader's understanding, and would it break a lint? | * Refuted findings go to an appendix of the verdict and are never raised again. * A probe carries an INTENDED list: one line per designed behaviour with an example input and its result, and every known issue marked "do not report again". Without it, hunters report the design as defects and the skeptics spend their runs refuting noise. * A verifier pins the tree it read (the commit, or a hash of the uncommitted diff), and checks a regression claim against main before it blames the round. * Packaging gates each branch in a clean checkout of that branch. Between 2026-10-03 and 2026-10-05 this caught CI failures that the working tree's gates had passed: the repository-wide formatter, and a generator that ran only with packages the first branch did not install. [gt-orchestration](https://www.prototemplate.com/skills/gt-orchestration) section 5 and [workflow-shapes.md](https://www.prototemplate.com/skills/gt-orchestration/references/workflow-shapes.md) (Review, Probe, Package) hold the rules. ## Kinds of check [#kinds-of-check] Report gates, structural checks, measured checks and visual checks on separate lines. On 2026-07-30 a structural audit read 91 of 91 while 0 of 72 items passed a visual bar of 90 in every view, and a combined report would have read as a pass. | Kind | What it proves | | ---------- | ------------------------------------------------------------- | | Gate | The typecheck or the lint passes | | Structural | The page loads with the right hierarchy and dimensions | | Measured | Computed values or pixels on the render match the numbers | | Visual | A person or a fresh critic compared a crop with its reference | Quality work converges through a separate critic scoring against a named reference, at bars of 8.5 of 10 per dimension and 90 of 100 overall and in every view. These rules close the ways to game the score: * The critic is fresh, did not build the work, is told to be harsh, writes its mismatch notes before it scores and keeps the lower score when unsure. * One strong view never averages away a weak one, and the builder never scores its own work. * Work runs from the worst item upward over the whole set, because a sample proves nothing about the items it skipped. * When a move fails twice, the idea changes: measure the reference and rebuild from the measurements. * The cheapest phase the user most wants runs first. On 2026-07-29 a run of about 45 agents put its blind judge first, because the run before it had died on usage credits before its judge ran. * The judge starts after the builders land, because a verdict on work still being rewritten goes stale within the hour. [gt-orchestration](https://www.prototemplate.com/skills/gt-orchestration) sections 4 and 5 and [convergence.md](https://www.prototemplate.com/skills/gt-orchestration/references/convergence.md) hold the loop, and the [quality bar](https://www.prototemplate.com/docs/handbook/quality-bar) holds its rows. ## Paid model runs [#paid-model-runs] A run that pays per model call starts with a proposal and runs nothing until Kevin approves it. The proposal names the grid, a per-run cap and a total cap on estimated spend, a fixed trial count per cell and a no-retry rule, and it says that the cap can stop the study early. | Part of the proposal | The comparison approved on 2026-10-05 | | -------------------- | -------------------------------------- | | Grid | Four models by six tasks by four modes | | Fresh runs | 96 | | Total cap | $25 of estimated spend | | Retries | None | 1. Freeze the plan: the worker, its helpers and the grid file stay unedited after approval. 2. Run a pilot of a few cells and verify every record, costs and reservations included. 3. Launch the approved remainder once, under one worker that holds the run's lock file, so nothing starts a second collector. 4. At the end, run the verifier, which writes the verified summary and the accounting export. A stop is diagnosed read-only, and its continuation is a new, separately labelled campaign that keeps every earlier record and cost reservation inside the original allowance. A timeout or a dropped connection is a defect in the harness, fixed before the continuation runs. A campaign of single attempts per cell names no winner. A heartbeat that checks a long run on a schedule stays read-only, never retries a cell or relaxes a limit, and speaks only on completion, on failure or when Kevin must decide. [gt-orchestration](https://www.prototemplate.com/skills/gt-orchestration) section 7 and [campaigns.md](https://www.prototemplate.com/skills/gt-orchestration/references/campaigns.md) hold the proposal, the stop question and the heartbeat prompt. ## Sources [#sources] * Prototemplate, read on 2026-10-21: `skills/gt-orchestration/SKILL.md` sections 1 to 7; `skills/gt-orchestration/references/harness-traps.md`, `workflow-shapes.md`, `convergence.md` and `campaigns.md`; `skills/gt-orchestration/scripts/replay-edits.py`; `docs/handbook/quality-bar.md` (Convergence loops). * Kevin's private agent notes on the relayed-message trap, workflow resumption after the reboot of 2026-10-07 and the session restarted at 18GB, dated 2026-09-30 to 2026-10-15. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Pull requests and CI > Pull requests on gt-cloud: CI signals that mislead, review bots, size and shape, parity for redesigned flows, ratcheted lints and git on shared trees. URL: https://www.prototemplate.com/docs/handbook/pull-requests General Translation (GT) keeps its product and its website in gt-cloud, a private monorepo on GitHub. gt-cloud squash-merges pull requests, requires three checks on main (`Validate PR title and Linear issue`, `check-planning-files` and `run-tests`), and is reviewed by bots and by people. Kevin Liu's product and site work lands there as small pull requests, each from its own worktree. Each section below gives a rule for that work, the case that set it, the fix and the skill that holds the procedure. The facts are gt-cloud's as of October 2026, and the methods apply to any GitHub repository with CI and review bots. ## CI signals that mislead [#ci-signals-that-mislead] Read a pull request's checks only after its merge conflicts are resolved, and list the branch's runs before blaming the latest push. GitHub runs no `pull_request` workflows while a pull request conflicts with its base, and each push cancels the run in flight, so the checks on the head can describe another commit or none. | Signal | Cause | Fix | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | Checks are stale, cancelled or missing | The branch conflicts with its base (`mergeStateStatus: DIRTY`), so no `pull_request` workflow ran | Merge or rebase first, then read the checks | | A red e2e on the latest push | The first run to finish reports every commit since the last green one; on 2026-09-29 a red e2e came from a commit five pushes earlier | List the runs and find the first red one | | The build dies in "Running TypeScript" | The dashboard's type check outgrew Node's default 2GB heap | Raise `--max-old-space-size`; the build has run with 4096 since 2026-09-29 | | A Playwright strict-mode violation | `getByText(..., { exact: true })` across the page also matches the label inside a closed `
` | Scope text lookups inside a `getByTestId` locator | | `mergeStateStatus: BLOCKED` with every check green | The code-owner review is missing | Request the code-owner review | ```sh gh pr view --json mergeStateStatus,headRefOid gh run list --branch --workflow "CI - Run Tests" # a trail of cancelled runs hides the first red ``` A required aggregate check fails when any job under it fails. `run-tests` aggregates lint, build, the unit test shards, validate, integration, e2e and db, so a red e2e blocks the merge although e2e is not a required check itself. [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 5 and [review-loop.md](https://www.prototemplate.com/skills/gt-ship/references/review-loop.md) hold the check list and what trips each job. ## Review bots [#review-bots] Before acting on a bot's finding, compare the commit it reviewed with the pull request's head. Two review bots on gt-cloud write their summaries into the pull request body between HTML comment markers, and they rerun on different triggers. | Bot | Reruns | Re-trigger | Reviewed commit | | ------------- | ------------- | ------------------------------------------------------------------------------------------ | ------------------------------------- | | Greptile | On open only | Comment `@greptile review again` (`@greptile review` the first time); about 4 to 5 minutes | `Last reviewed commit` in its summary | | Cursor Bugbot | On every push | Comment `bugbot run` | Named in its summary | Bots review a new pull request within about a minute. On 2026-09-24 a force push a minute after opening refreshed Bugbot and left Greptile's summary describing a diff that no longer existed. An edit to the body right after a force push can also restore Bugbot's stale summary. The bar on gt-cloud is Greptile at 5/5 on the head commit with every bot thread answered and resolved. * Rewrite only the part of the body above the first bot marker, and keep every bot block byte for byte. * Classify each finding before acting on it: mine and real (fix, push, reply with the sha, resolve), stale (reply with the commit that removed the code, resolve), not mine (say so and leave it to its owner), or an owner's decision (ask). * A risk label from an approval bot is a label and needs no reply. * Reply on each thread with the fixing commit, then resolve it. A thread resolved without a reply still reads as unanswered to a human reviewer. ```sh gh api graphql \ -f query='mutation($id:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$id,body:$body}){comment{url}}}' \ -F id=PRRT_xxx -f body='Fixed in abc1234: the query is bounded to 100 rows.' gh api graphql \ -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' \ -F id=PRRT_xxx node skills/gt-ship/scripts/pr-bots.mjs # exits 0 at 5/5 on the head, no open thread, no conflict, checks green ``` [gt-ship](https://www.prototemplate.com/skills/gt-ship) sections 4 and 5 and [review-loop.md](https://www.prototemplate.com/skills/gt-ship/references/review-loop.md) hold the loop, and the [quality bar](https://www.prototemplate.com/docs/handbook/quality-bar) holds its rows for pull requests. ## Size and shape [#size-and-shape] A reviewer reads a pull request's line count first. Group the diff by kind before pushing, and justify each large group in the body or cut it. On 2026-10-02 Kevin sent back a release pull request for a redesigned onboarding flow because of its size. | Part of the release pull request | Size | | ---------------------------------------------- | ------------------------------- | | The whole pull request | 39.5k lines | | A record-and-replay parity suite | 24k lines | | Pretty-printed recorded JSON inside that suite | 18k lines | | Comments | 20% of the added product lines | | Tests | 1.2 test lines per product line | * Verification instruments (recorded fixtures, record-and-replay suites, harnesses, capture scripts, review galleries) live on their own branch or in the scratchpad. * Each behaviour is tested once, with targeted assertions added to the existing test file. * A comment says why in one or two lines. * A pull request carries one concern, and an unrelated repair gets its own. * Planning Markdown stays out: `check-planning-files` fails on an added Markdown file that reads as a plan. ```sh node skills/gt-ship/scripts/pr-size.mjs # origin/main...HEAD, grouped by kind node skills/gt-ship/scripts/pr-size.mjs --base origin/ # in a stack node scripts/check-plan-files.mjs --base origin/main # in gt-cloud, before pushing ``` Screenshots go in with the first push and are refreshed after every visible change, as before and after crops. Full-page captures in a two-column table render as strips that nobody can read. | Crop rule | Value | | --------- | ----------------------------------------------------------------------------------------------------- | | Clip | The changed elements' bounding boxes plus 24px of padding | | Width | 420 to 900 CSS px | | Height | 620 CSS px at most | | Pairing | The same clip on main and on the branch | | Layout | `\| Before (main) \| After (this branch) \|` for crops up to 640px wide; wider crops stack full width | | Themes | Both, where the change has a theme | [gt-ship](https://www.prototemplate.com/skills/gt-ship) sections 3 and 4 and [pr-body.md](https://www.prototemplate.com/skills/gt-ship/references/pr-body.md) hold the size audit and the body's shape. ## Parity for a redesigned flow [#parity-for-a-redesigned-flow] A redesign of a sign-in or onboarding flow keeps every tracking call and backend side effect that production has, and the proof is a diff of recorded side effects. A reviewer's list of intended changes proves nothing: one audit called an organization audit event preserved while a later fix had already dropped a field from the event's scope. 1. Record production at the module boundaries for each scenario, in a detached worktree of main. 2. Replay the same scenarios on the release and record the same boundaries. 3. Diff the two ledgers, and run the diff again after every fix. ```sh git fetch origin git worktree add --detach ../parity-main origin/main # production's code, for the recording ``` Additions are allowed only for stricter authorization checks and new recovery paths, and the body lists each one. When parity would mean reporting a step the redesign removed, ask the owner of the flow and fake no event. The record-and-replay suite, the 24k lines above, stays on its own branch. [gt-ship](https://www.prototemplate.com/skills/gt-ship) section 7 holds the gate, and the [decisions log](https://www.prototemplate.com/docs/handbook/decisions) holds the ruling of 2026-10-02. ## Lints and ratchets [#lints-and-ratchets] A rule that matters gets a lint that fails on drift, and the lint's design follows from how many sessions edit the code at once. * A ratchet counts violations per file. Parallel sessions move lines all the time, and a per-file count stays the same when a line moves. * A baseline above the real count lets a new violation into that file without a failure, so the baseline is lowered after every cleanup. * A clean exit under `--report` proves nothing, because `--report` drops the failing exit codes. * A gate never passes when it could not judge: it exits 2 on an HTTP error, a missing file or a state that did not open. * Every exemption carries its reason in config. Every Prototemplate lint prints `file:line rule message` and exits 0, 1 or 2, and the [Gates](https://www.prototemplate.com/docs/gates) page of the docs lists them. [gt-lints](https://www.prototemplate.com/skills/gt-lints) sections 1, 4 and 5 and [prototemplate-gates.md](https://www.prototemplate.com/skills/gt-lints/references/prototemplate-gates.md) hold the contract. ## Git on shared trees [#git-on-shared-trees] Before reporting a push, prove it from the remote. When several sessions share a worktree, its branch, its files and its tracking refs can change between two commands. | Trap | What happened | Fix | | ------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | A fast-forward across a `.gitignore` change | It overwrote ignored files at newly tracked paths with no warning (2026-10-10) | Copy the ignored originals aside before the fast-forward | | A rebuild of history | 32 shell scripts were committed at mode 100644, and lanes that ran them directly failed (2026-10-10) | `git update-index --chmod=+x `, then commit | | A lease against a stale tracking ref | A force push erased a collaborator's five commits (2026-08-26) | Fetch first, then lease against the sha you expect | | GitHub's update-branch button | It creates a merge commit on the remote and turns auto-merge off; a force push turns it off too | `git pull --rebase` before the next push, then `gh pr merge --auto` again | | Two sessions in one worktree | One switched the branch, and two commits meant for a pull request never reached it (2026-09-28) | Check the branch before each commit, and compare the pull request's head with `HEAD` before reporting | | A listing cut with `head -8` | It hid an existing test file, and a write meant for a new file replaced seven tests | Prove a file is new with `git ls-tree` and `test -f`, and never cut an existence check | ```sh git fetch origin && git log --oneline HEAD..origin/ # a collaborator's commits git push --force-with-lease=: origin HEAD: git branch --show-current # before every commit gh pr view --json headRefName,headRefOid && git rev-parse HEAD # before saying "pushed" git ls-tree origin/main -- ; test -f && echo exists # before writing a "new" file ``` [gt-ship](https://www.prototemplate.com/skills/gt-ship) sections 1 and 2 hold the commit and push rules, and the [multi-session playbook](https://www.prototemplate.com/docs/handbook/multi-session-playbook) holds the rules for branches that two agents push. ## Sources [#sources] * Prototemplate, read on 2026-10-21: `skills/gt-ship/SKILL.md` sections 1 to 7, `skills/gt-ship/references/review-loop.md` and `pr-body.md`, `skills/gt-ship/scripts/pr-size.mjs` and `pr-bots.mjs`; `skills/gt-lints/SKILL.md` sections 1, 4 and 5 and `references/prototemplate-gates.md`; `docs/handbook/quality-bar.md` (Pull requests); `docs/handbook/decisions.md` (2026-10-02). * gt-cloud's CI workflows, rulesets and review bot behaviour, as `review-loop.md` records them from origin/main on 2026-10-05. * Kevin's private agent notes on the review bots, pull request size, crops in pull request bodies, the parity gate and the history rebuild of 2026-10-10, dated 2026-08-26 to 2026-10-10. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Reviewing with Kevin > How Kevin’s review words map to measures, one exemplar before a set, how a result reaches him, the five ship states and the ask ledger. URL: https://www.prototemplate.com/docs/handbook/review Kevin Liu does design, website and product work at General Translation (GT), and he reviews the work his agent sessions do: on localhost, in pull requests and in the desktop app where the sessions run. His review words have fixed readings, his rounds start from one exemplar, and every result reaches him as something to see with its state named. Each section below gives the rule, the review that set it and the skill that holds the procedure. The [quality bar](https://www.prototemplate.com/docs/handbook/quality-bar) holds the bar the work is measured against, and the [operating principles](https://www.prototemplate.com/docs/handbook/operating-principles) hold the standing rules behind his corrections. ## Words read as measures [#words-read-as-measures] Each word Kevin uses in a review has one reading, and each reading was set by a correction. Apply the reading on the render and report the measured values. | Word | Reading | The case that set it | | --------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | "Equivalent", "equal" | Both measures take the larger value | 2026-09-30: two gaps evened at 10px in an 814px-tall window were rejected; the accepted pair reads 16 and 16 there and 26 and 26 at 900px | | "Smaller", "larger" | The next rung of the existing ladder | 2026-09-28: 13px card facts went to the 12px rung | | "Fix this", on one instance | The whole class: find every member, fix it, report the count | [Operating principle 5](https://www.prototemplate.com/docs/handbook/operating-principles) | | "Remove this" | The smallest reading, then a check of the neighbours | Standing rule | | "Add it to the left of X" | X alone; the rest of X's column stays | 2026-08-13 | | "Standardize" | One structure on every page of the kind, with a lint or the page check to catch a page that drifts | 2026-10-06, the book page | | "Enhance", on an existing page | Keep the page, its sections and its copy word for word, and add to it | 2026-10-08: a rebuild that dropped a page's hero, logo band and copy was rejected | | "Redesign" | Visibly new; a change that keeps the old shape comes back as not done | 2026-09-30 | | "Keep its aesthetic" | The geometry and the material stay, and only the named thing changes | Standing rule | | "Make it look better", with a reference | Measure the reference and match it | Standing rule; without a reference, the brand deck for a product surface and the shipped site for a landing page | Kevin, 2026-08-13: "incorrect. i only want you to add it to left of ...". Once a rule matters to him, Kevin asks for a lint, so a new rule ships with its lint or names the lint that should hold it. [process.md](https://www.prototemplate.com/skills/gt-aesthetic/references/process.md) of `gt-aesthetic` (Reading Kevin's asks) holds the readings. ## One exemplar first [#one-exemplar-first] Build one section, one page or one state, get Kevin's sign-off on its grammar, then replicate it across the set. A full set built on a reading he had not confirmed costs the whole round. A version he rejected is never the base of the next attempt, which restarts from the reference. | Date | What happened | The rule it set | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | 2026-08-04 | Seven landing sections fanned out on an unconfirmed reading of the mock images, and one element survived review: an isometric drawing | One exemplar first; mock images and prototype pages are literal specs, and their notes are change orders | | 2026-08-12 | After Kevin picked one variant per page, a variants system of about 16,000 lines left the code, and the sign-in route was checked byte-identical to the committed page | The alternatives leave the code after a pick and stay in git history | | 2026-09-04 | Nine of ten shader frames sent for a pick looked the same | Check that a set differs before sending it | | 2026-09-14 | A round rebuilt ten directions as forks of one earlier direction, and seven of the ten heroes had the same layout, headline and hero visual | Directions differ in silhouette, material and action | Kevin, 2026-09-04: "they all looks the same bro 9 / 10 are the same". The set check flags a pair of pictures under 2% mean difference as the same picture, and a pair whose tone grids correlate at 0.9 or above as the same composition. A clean report means only that no two options are near copies, so the silhouette, material and action table is still read by eye. ```sh node skills/gt-explorations/scripts/distinct-set.mjs public/shots/light/.jpg public/shots/light/.jpg node skills/gt-explorations/scripts/distinct-set.mjs --strict ``` * A direction lives in its own route with its CSS scoped under its own root class, so it never restyles another direction. Keyframe names are global, so a direction prefixes its own with its slug. * Explorations stay on a branch or uncommitted until Kevin has reviewed them on localhost, because Prototemplate's main branch deploys the public site. [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic) section 5 holds the loop, and [gt-explorations](https://www.prototemplate.com/skills/gt-explorations) section 2, [rounds.md](https://www.prototemplate.com/skills/gt-explorations/references/rounds.md) and [charter.md](https://www.prototemplate.com/skills/gt-explorations/references/charter.md) hold the rules for a round. ## Showing the result [#showing-the-result] Every visual or audible result reaches Kevin as something he can see or hear, in the turn it is made and before he asks for it. | What changed | What Kevin gets | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | | An area of a page or a component | Before and after crops of every changed area at the same viewport and clip, 420 to 900 CSS px wide, in both themes | | A set of marks, covers or directions | One contact sheet with every item numbered, plus the files | | A page, a flow or an app | The URL of a running server, with its port, branch and commit | | Audio takes or voices | Each option numbered on screen, as a video or on a small listening page | | Scripts, voiceover lines, deck text | The text, for review before the final render | Images go into the reply as attached files, so he can forward them. A claim about size or spacing carries the value measured in the browser, such as "both buttons read 9.5px from label to glyph". Crops are shot with an external browser harness, because the desktop app's browser pane pauses `requestAnimationFrame` and canvases come out blank there. The desktop app's file card plays video files and does not play audio-only files, so on 2026-10-16 a pronunciation sample sent as `.mp3` could not be heard. A listening sample goes out as `.mp4`: one 1280 by 720 card per option with its number large, drawn with Pillow where the installed ffmpeg has no `drawtext` filter, cut to the audio and joined with ffmpeg's concat demuxer. The question then names the numbers on screen. ```sh ffmpeg -loop 1 -i card-1.png -i take-1.wav -c:v libx264 -tune stillimage -pix_fmt yuv420p -c:a aac -shortest option-1.mp4 printf "file '%s'\n" option-*.mp4 > options.txt ffmpeg -f concat -safe 0 -i options.txt -c copy options.mp4 ``` [gt-reporting](https://www.prototemplate.com/skills/gt-reporting) section 1 holds the table, and [process.md](https://www.prototemplate.com/skills/gt-aesthetic/references/process.md) of `gt-aesthetic` holds the local review recipe. ## Ship states [#ship-states] Each item in a report ends in one state, and each state carries its proof. | State | Words | Proof in the line | | ---------- | --------------------------------------------------------------------- | --------------------------------------------------- | | Local only | "local only, uncommitted" or "committed locally at ``, unpushed" | The sha | | Pushed | "pushed to `` at ``" | The branch and the sha | | PR open | "PR open, checks green, needs one approval" | The full link and its checks | | Merged | "merged to main as ``" or "merged into the PR branch ``" | The merge sha and the branch it went into | | Live | "deployed and verified live at ``, serving ``" | The URL and how the build was matched to the commit | Kevin, 2026-10-03: "when u say "merge" u mean into ur pr right". A merged change is live only once a production build of its commit has succeeded and serves the page. While production builds fail, the site keeps serving the last good deployment, so the newest production build is read before a report says "live". A clean local type check does not prove the hosted build either. On 2026-10-16 a push that compiled cleanly with `tsc -b` failed its hosted build three times: TanStack Start's import protection refuses a server-only import that stays in a page's module graph, and a helper in a module the page imported called `setResponseStatus` from a default parameter. In a module a page imports, every request-scoped server call stays inside the server function's handler or behind an awaited import. [gt-reporting](https://www.prototemplate.com/skills/gt-reporting) section 2 holds the states, and [gt-verify](https://www.prototemplate.com/skills/gt-verify) section 7 holds the checks that prove a deploy. ## The ask ledger [#the-ask-ledger] Kevin sends long messages with several asks and adds more while work runs, so every ask is numbered in the order received and tracked until it is reported. ```text N. [done | in progress | blocked: reason] the ask in Kevin's terms. Where: file, route or URL. Proof: commit, PR or URL. Verified: how. ``` * A report on multi-ask work closes with the full ledger, done items included. When he re-sends a compiled list, it is matched line by line and every item is reported. * "Did we do this" gets a yes or a no first, then the item's ledger line. * A turn opens with the answer to what he asked, about the exact pull request, page or file he named, then what shipped, something to see, and a numbered list of what only he can do (an approval, a login, a payment, a credential, a choice). * A short summary holds what shipped and what only he can do, in under fifteen lines. * Workflow internals stay out: lane and agent names, bot rounds, lint runs, rebases and follow-up pull requests opened along the way. When a run will outlast a compaction of the session, the ledger lives in a file named in each report, so a bare "continue" resumes from it. [gt-reporting](https://www.prototemplate.com/skills/gt-reporting) sections 3 and 4 hold the ledger and the order of a turn's close. ## Sources [#sources] * Prototemplate, read on 2026-10-21: `skills/gt-aesthetic/SKILL.md` section 5 and `references/process.md`; `skills/gt-explorations/SKILL.md` section 2, `references/rounds.md` and `references/charter.md`, `scripts/distinct-set.mjs`; `skills/gt-reporting/SKILL.md` sections 1 to 4; `skills/gt-orchestration/SKILL.md` section 1; `docs/handbook/decisions.md` (2026-08-12); `docs/handbook/operating-principles.md`. * Kevin's messages to Claude Code and Codex for the quoted rulings. Quotes keep his spelling. * Kevin's private agent notes on the sign-in gap review, the first landing build, the variants program, sending audio samples and a hosted build failure, dated 2026-08-04 to 2026-10-16. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # GT product and architecture map > What General Translation sells and to whom, how the products fit together, the copy that must be exact, and where each part lives. URL: https://www.prototemplate.com/docs/handbook/gt-product-map This page is for agents and people who write about General Translation (GT), design for it or change its products. It states what GT sells and to whom, how the products fit together, the facts copy must get right, and where each part lives in the repositories. Each fact carries the date it was read or ruled. Everything on the page is safe to publish: it names no customer, states no business figures and leaves internal data-model details to gt-cloud's own documents. Detail stays with the documents that own it: * [BRAND.md](https://www.prototemplate.com/docs/handbook/brand) and [gt-brand](https://www.prototemplate.com/skills/gt-brand) hold the name, the idea, the positioning line and the marks. * [gt-website](https://www.prototemplate.com/skills/gt-website) holds the landing app map, docs routing, the blog and deploys. * [gt-landing-pages](https://www.prototemplate.com/skills/gt-landing-pages) holds the page grammar for the home, pricing, enterprise and careers pages. * [gt-voice](https://www.prototemplate.com/skills/gt-voice) holds the writing rules, and [gt-components](https://www.prototemplate.com/skills/gt-components) holds the translation component rules. * gt-cloud's `AGENTS.md` (which `CLAUDE.md` links to) and its `.agents/skills` hold the monorepo file map and the engineering rules, such as `audit-events` and `server-security`. `$GT_CLOUD` is a gt-cloud checkout at origin/main. gt-cloud facts below were read at e17fce499 on 2026-10-05. ## 1. Positioning [#1-positioning] The home page hero says GT "builds full-stack localization for apps, docs, and websites" (read 2026-10-05). GT builds every layer of that stack itself: 1. open-source i18n libraries and SDKs for every major framework; 2. context-aware translation APIs; 3. the platform for versioning, editing, review and delivery; 4. Locadex, the agent that internationalizes a codebase and keeps it localized. Kevin, 2026-07-30: "the whole point is that were a full stack localization, end to end". * The positioning is the Vercel model applied to localization. GT pairs open-source developer tools with the hosted infrastructure that runs them best (BRAND.md section 2). This is the one comparison GT copy makes (gt-voice, hard rule 2). * The thesis is "Every product in every language" (BRAND.md section 2). Copy quotes it as written. * Pricing is usage-based. Legacy translation management systems (TMS) charge per seat and cover part of the stack. Because GT owns the whole stack, it can own the whole customer experience (BRAND.md section 9). * Copy states the mission only as the live careers page states it. ## 2. Audience and the sale [#2-audience-and-the-sale] * The audience has two ends, which Kevin calls a barbell (2026-07-30). Developers and small teams start self-serve at one end. Enterprises buy through sales at the other. A page carries enterprise presence and developer comfort together. * BRAND.md section 9 names the buyer: technical and product executives at growth-stage startups, whose engineering and growth teams use the product. The open source carries a community register and the platform an enterprise register (BRAND.md section 2). * Many buyers cannot yet tell localization, translation and internationalization (i18n) apart, so pages show what localization does: translated copy, changed code, converted currency, reformatted dates and numbers, and layout rearranged for a script. The terms themselves stay on the page for evaluators who check for them. The docs define them on [Key concepts](https://generaltranslation.com/docs/overview/key-concepts). * Heroes and demos show the product working. Kevin, 2026-08-11, on the enterprise page: "far less telling, more showing, so same content but less text and more better diagrams". ## 3. Products [#3-products] | Product | What it does | Where it lives | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | The `gt` libraries | `gt-next`, `gt-react`, `gt-tanstack-start` and `gt-react-native` (each 11.4.9 on npm, 2026-10-05), `gt-vue` and `gt-node` render translations and format locale-sensitive values in the app. `gt-next` also detects the locale and routes by it. | `generaltranslation/gt`, `packages/` | | The Python SDKs | `gt-flask` and `gt-fastapi` on PyPI, built on the `generaltranslation` core package. BRAND.md's naming table writes the family as `gt-python`, which is the repository name; no package of that name exists (PyPI, 2026-10-05). | `generaltranslation/gt-python` | | The `gt` CLI | Sets up a project, signs in, translates files and calls the API (`gt` 2.25.1 on npm, 2026-10-05). Section 7 has the commands. | `generaltranslation/gt`, `packages/cli` | | The platform and dashboard | Projects, Context Groups, the translation editor, review and approval, version branching, annotations and audit logs at dash.generaltranslation.com. Review works over the web workspace, the API and the CLI. | `$GT_CLOUD/apps/dashboard`, `apps/api` | | The translation API | Context-aware translation of text, files and components, published as `/openapi.yaml`. | `$GT_CLOUD/apps/api` | | The translation CDN | Serves pre-generated translations from a global, low-latency CDN, and pushes over-the-air updates without a redeploy of the app. | `$GT_CLOUD/apps/edge` (Cloudflare Workers) | | Runtime translation | Translates user-generated content on demand with the project's context. In Next.js it is `` and `tx()` from `gt-next/server`, built on `GTRuntime` in the core library. | `generaltranslation/gt`, `packages/core` and `packages/next` | | Locadex | A hosted agent. It connects to GitHub and to content sources (Google Drive, Figma, a CMS), scans the repository, internationalizes the code, generates translations and opens pull requests. Its automations keep the app localized after the first pass. | `$GT_CLOUD/apps/locadex`, `packages/locadex-core` | | Integrations | Mintlify, Sanity (`gt-sanity`), Storyblok, Google Drive and rrweb (`gt-rrweb`), the GitHub Action, and the MCP server (`@generaltranslation/mcp`). | `generaltranslation/gt` (Sanity, rrweb, MCP), `$GT_CLOUD/packages/integrations` (hosted connectors such as Google Drive and Storyblok), `generaltranslation/translate` (the Action) | The React libraries share one API. `` wraps a block of static JSX, and ``, ``, `` and `` mark its dynamic parts. `gt()` translates a string, `msg()` marks strings in a data array, and `` and `tx()` translate content at runtime in server code. The usage rules, including `const gt = useGT();` and `await getGT()` in async server code, are in [gt-components](https://www.prototemplate.com/skills/gt-components) and gt-cloud's `AGENTS.md`. This plate runs `EdgeGlobe`, the drawing of the translation CDN that the home page of generaltranslation.com uses. A request travels to the closest point of presence, where the translation already sits, and the payload returns along the same route. ## 4. Context [#4-context] Context reaches a translation through three layers: the Organization, then the Project, then the Component (Kevin's outline of 2026-07-30; the dashboard guide [Defining context for translations](https://generaltranslation.com/docs/platform/dashboard/guides/defining-context-for-translations), read 2026-10-05). Each layer is inherited by the one below it. | Layer | What sets context there | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | Organization | Context Groups live at the Organization level. A Context Group pairs a Glossary with Custom Prompts. | | Project | A project applies one or more Context Groups in priority order. The top group wins where two groups overlap, and newly generated terms are saved to it. | | Component | `` sets context for one block. `gt()` and `getGT()` take a `$context` option. `$context` on `` is a deprecated alias. | * The Glossary holds terminology: product names, brand names, feature names, technical terms and phrases that stay untranslated. Its standing example is that Locadex is never translated in any locale. * Custom Prompts hold style: tone, audience, formality, conventions and formatting. A prompt applies to every locale or to one. Kevin's July outline called this half Directives, and the importer still accepts exports that use that label. The dashboard, the API and the pricing page call it Custom Prompts (2026-10-05), and copy uses that name. * Kevin's component example, from the 2026-07-30 outline: `Click the toast to dismiss`. * An edit to a Context Group applies to new translations. The Apply button on a project's Context page updates existing translations with chosen Glossary terms. * The Context Management API (`org:context:read` and `org:context:write`) and its MCP tools automate groups, terms and prompts. ## 5. Enterprise and plans [#5-enterprise-and-plans] Plan facts come only from the public pricing page, [https://generaltranslation.com/pricing](https://generaltranslation.com/pricing), quoted as it read on 2026-10-10. Plan rules the page does not state, such as what a new account receives, are internal and stay in gt-cloud's `gt-dashboard` skill. * The lead: "Start for free with usage-based billing. Unlimited projects and users on every plan." * Starter: "$0", "For individuals and small teams". It lists unlimited projects, users and languages, the Translation Editor, the GitHub Integration, the Locadex AI Agent and usage-based pricing, with a Get Started button. * Enterprise: marked Recommended, "For large teams with complex localization needs". It lists unlimited projects, users and languages; "Dedicated FDE hours to build any workflow for your use case."; "Custom integrations, webhooks, and tailored automation."; and "SSO, RBAC with custom permissions, SOC 2 and ISO 27001 certificates", with a Contact Us button. * The comparison table: a Platform Fee of $0 on Starter and Custom on Enterprise, and Usage Rates that link to [https://generaltranslation.com/pricing/usage](https://generaltranslation.com/pricing/usage) on Starter and read Custom on Enterprise. Its rows name the core products (Locadex AI Agent, Custom Workflows, Translation CLI, Context Platform, Translation CDN, Version Branching), the platform (languages, projects and users unlimited on both plans, Context Groups, Keyword Glossary, Custom Prompts, Translation Editor, Custom Roles, Webhooks, SOC 2 Type II and ISO 27001 certification, SSO over SAML and OIDC) and support (GitHub, email, Discord, Slack and phone). The table marks which plan has each row with icons, so copy that says which plan includes a row reads the live page first. The comparison table follows the cards directly (Kevin, 2026-08-15), and the page's diagram sits below the plans. The money format on pricing pages is gt-landing-pages' whole-dollar rule. Kevin set the four enterprise pillars on 2026-08-15, and the enterprise page carries them (read 2026-10-05). His list named the third pillar Forward-deployed engineers, and the live page heads it Forward-Deployed Support, the name copy uses. | Pillar | Copy | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Security and governance | SSO, SOC 2, ISO 27001, audit logs, and custom roles. The pricing table adds SSO over SAML and OIDC, SOC 2 Type II and RBAC with custom permissions. | | Enterprise platform | Share translation context, glossaries, and custom prompts across every project and content source in your company. | | Forward-deployed support | Dedicated FDE hours with localization engineers to set up your system and bring localization to production. | | Custom workflows | Reliable, scalable translation workflows across any file format or framework. Custom integrations, webhooks, and tailored automation. | * The enterprise page's section order is governance and SSO, translation review, shared context across teams, forward-deployed support, then the contact form (Kevin, 2026-08-16). A change to the page's sections follows that order. * Features grouped under one heading share one purpose (Kevin, 2026-08-15). * The enterprise hero has one button. Kevin cut its second button, Talk to an Engineer, on 2026-08-15. ## 6. Copy that must be exact [#6-copy-that-must-be-exact] | Topic | What copy says | Source | | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | Layout per language | "Dynamic layout for each language". GT rearranges components for a language, such as right-to-left order for Arabic. The components themselves stay unchanged. | Kevin, 2026-08-06: "technically we dont alter components, but can rearrange them dynamically like for arabic" | | Review surfaces | "Review and approve with your team, over web, API, or CLI." Copy names all three surfaces, because enterprises judge vendors on agent readiness. | Kevin, 2026-08-06: "from one surface implies that we're forcing you to use our dashboard" | | Formatting | "Numbers, currencies, dates, plurals, and more", with no "edge cases handled" lead. | Kevin, 2026-08-06 | | Delivery | "Served using a global, low-latency translation CDN." and "Push over-the-air updates without redeploying your app." Translations can also ship with the code, so the home page's delivery band reads "Served from local files or the edge" (read 2026-10-05). Copy that describes delivery in general names both paths. | Kevin, 2026-08-15, for the enterprise page's delivery panel | | Enterprise voice | "Every company's localization needs are different. General Translation adapts to your existing stack, workflows, and review process." The customer uses the platform with its own team or alongside GT's forward-deployed engineers. A line that implies GT's team is better than the customer's is cut, and gt-voice holds the register. | Kevin, 2026-08-15 | | Localized heroes | Each locale's hero line is idiomatic and carries the exact meaning in that language and culture. A locale route such as `/ja` opens with its hero sentence and diagram in that locale. | Kevin, 2026-08-08 | | Geography | Content about places is organized by language. It names no country or territory, draws no borders, takes its facts from CLDR, and treats disputed areas through one policy. | Kevin, 2026-10-03: "avoid political sensitivity without making it look forced" | | Infrastructure diagrams | Edge nodes carry real cloud region names such as `us-east-1`, shortened where a label would clip. | Kevin, 2026-08-06 | | Names | Product tokens keep their exact form (`gt`, `gt-next`, `gt login`), Locadex is never translated, and the naming table is BRAND.md section 1. | BRAND.md, gt-voice | | Public facts | A customer or partner is named only where generaltranslation.com shows it, and BRAND.md section 9 follows the site. No surface states funding, revenue, headcount or the timing of an unannounced launch. | gt-brand section 8; the decisions log, 2026-10-10 | ## 7. CLI and agent entry points [#7-cli-and-agent-entry-points] The CLI facts below come from the start prompt served at `/agent-prompt.md` (read 2026-10-05) and from the CLI source on `generaltranslation/gt` origin/main. A coding agent ran the prompt end to end on a fresh Next.js app on 2026-09-25 with gt 2.22.3 and again on 2026-09-30 with gt 2.23.1. The published CLI was 2.25.1 on 2026-10-05. * `gt login` signs the CLI in with the person's GT account. It opens GT's sign-in page in the browser and shows a confirmation page when sign-in completes. `gt login --no-browser` prints a sign-in URL and a code to use on any device, and an SSH session takes that path by default. `gt whoami` reports the signed-in account. * Public writing explains how to sign in and use the product. It leaves out how the CLI obtains, stores or refreshes its tokens, which is the public authentication boundary in the `AGENTS.md` of `generaltranslation/content`. * `gt translate --dry-run` shows the scope, and `gt translate` translates it. With translations stored in the repository, the run writes `public/_gt/.json`, `gt-lock.json` and a `_versionId` in `gt.config.json`. * `gt init` and `gt configure` ask any unanswered question through an interactive prompt and wait with no timeout. An agent passes `--no-interactive` (or `--json`, which implies it) and a flag for every question. A missing answer then exits 1, lists the flags still needed and changes no file. Kevin asked on 2026-10-01 that agents always run the CLI this way. * The CLI has no `--version` flag. `npm view gt version` gives the published version, and the `translate` banner prints the installed one. `gt auth` is not a command in current releases. * A development key belongs in `.env.development.local`, which `next dev` loads and `next build` does not. A production `next build` refuses to run with a development key present. The docs serve agents directly (gt-cloud #4499, merged 2026-09-10). gt-website's `references/docs.md` holds the routes behind them. * `/llms.txt` is the curated entry point, `/llms-index.txt` lists every page, and `/openapi.yaml` is the API specification. `/llms-full.txt`, scoped `/docs//llms.txt` files, `/AGENTS.md` and `/sitemap.md` sit beside them. * Appending `.mdx` or `.md` to a docs page URL returns the page's raw Markdown twin. * Every docs page's Copy page menu carries the llms.txt entries (Kevin, 2026-09-11). * The hero's second button is Setup for Agents (gt-cloud #5049, merged 2026-10-01). It copies the start prompt, whose one source is `docs/en-US/overview/for-coding-agents.mdx` in the content repository. The same text is served raw at `/agent-prompt.md`. After any change to the prompt, the CLI or the quickstarts, the prompt is run end to end by a coding agent on a fresh app. Kevin, 2026-09-30: "Agent experience (AX) is a huge deal for us". The start prompt sets these onboarding rules for an agent (read 2026-10-05): 1. Inspect the repository before asking anything: framework, router, app directory, existing i18n library, GT configuration, source language and build scripts. 2. In a monorepo, choose the app or content directory and run setup there. Setup never runs at the workspace root. 3. Ask for missing decisions one at a time: the source language, the target languages, then the first page or workflow. When the project itself is unclear, ask one question ("What would you like to translate?") with concrete choices. 4. Preserve existing locale choices and reuse an existing i18n setup. The source language is never assumed to be English. 5. Sign in with `gt login`. The agent never asks for a pasted API key, a password or OAuth tokens, and it writes new keys to ignored files without printing them. 6. Carry the work through setup, translation and verification, then hand off what changed, the project and locales, what was verified and what the person still has to do. Auto-merge and paid plan changes wait for the person's direction. ## 8. Repositories [#8-repositories] | Repository | Visibility | What it holds | | ------------------------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `generaltranslation/gt-cloud` | private | The turbo and pnpm monorepo: the dashboard, landing, API, workers, AI proxy, edge, Locadex and admin apps, and the shared packages (`packages/ui`, `db`, `node`, `settings` and others). | | `generaltranslation/content` | public | Docs, blog posts and devlogs, mounted at `apps/landing/content` as a submodule. Every landing deploy checks out the tip of content main (`$GT_CLOUD/scripts/deploy-landing.sh`), and a merge to content main triggers a deploy. | | `generaltranslation/legal` | public | The legal pages, a pinned shallow submodule at `apps/landing/legal`. | | `generaltranslation/gt` | public | The CLI, the JavaScript libraries, the core library, the compiler, the MCP server and the Sanity and rrweb plugins. | | `generaltranslation/gt-python` | public | The Python SDKs. | | `generaltranslation/translate` | public | The GitHub Action. | | `Kevin-Liu-01/Prototemplate` | public | The design hub and public reference behind prototemplate.com: BRAND.md, DESIGN.md, the deck, the skills and this handbook. | File-level maps stay with their owners: gt-cloud's `AGENTS.md` and `.agents/skills` (`gt-landing`, `gt-dashboard`, `gt-api`, `gt-admin`), [gt-website](https://www.prototemplate.com/skills/gt-website) for the landing and [gt-brand](https://www.prototemplate.com/skills/gt-brand) for the identity files. ## 9. Site behaviour [#9-site-behaviour] [gt-website](https://www.prototemplate.com/skills/gt-website) owns the site's routing. Its `references/docs.md` holds the unsupported-locale redirects, the docs near-miss ladder and suggestion page (gt-cloud #4359), the markdown twins and the step order in `src/proxy.ts`. The rules below are not in that reference. Each was read on gt-cloud's origin/main and checked on production on 2026-10-05. * Staging builds translate one locale. `apps/landing/staging.gt.config.json` lists `es` only, and `gt.config.json` keeps the production roster (`es`, `fr`, `zh`, `ja`, `it`, `ru` and `en-GB` beside `en-US`). Kevin, 2026-08-08: "only es for staging". * Missing localized content falls back to en-US. A legal page renders the en-US copy in place, an untranslated docs page serves the default language, and a blog slug absent in a locale redirects to the en-US post. Kevin, 2026-08-17: "fix pages that are breaking from like en-GB". * The docs suggestion page answers HTTP 200 with `noindex, nofollow` by design (gt-cloud #4359). A real 404 needs the not-found boundary, and under the force-static root layout that boundary never sees the requested slug. The blog checks frontmatter aliases and close matches before it renders its own suggestion page. This plate sets the production roster of the landing app in `LocaleTag` pills: en-US as the source and seven target locales. The staging build translates `es` alone. ## 10. Public surfaces [#10-public-surfaces] Every address below answered HTTP 200 on 2026-10-10. [BRAND.md](https://www.prototemplate.com/docs/handbook/brand) section 8 says how the identity applies to each kind of surface. | Surface | Address | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Home page | [https://generaltranslation.com](https://generaltranslation.com) | | Pricing and usage rates | [https://generaltranslation.com/pricing](https://generaltranslation.com/pricing), [https://generaltranslation.com/pricing/usage](https://generaltranslation.com/pricing/usage) | | Enterprise and its contact form | [https://generaltranslation.com/enterprise](https://generaltranslation.com/enterprise), [https://generaltranslation.com/enterprise/contact](https://generaltranslation.com/enterprise/contact) | | Careers and contact | [https://generaltranslation.com/careers](https://generaltranslation.com/careers), [https://generaltranslation.com/contact](https://generaltranslation.com/contact) | | Documentation | [https://generaltranslation.com/docs](https://generaltranslation.com/docs), with [https://generaltranslation.com/llms.txt](https://generaltranslation.com/llms.txt) and [https://generaltranslation.com/agent-prompt.md](https://generaltranslation.com/agent-prompt.md) for agents (section 7) | | Knowledge base | [https://generaltranslation.com/kb](https://generaltranslation.com/kb) | | Blog and changelog | [https://generaltranslation.com/blog](https://generaltranslation.com/blog) | | Supported locales | [https://generaltranslation.com/supported-locales](https://generaltranslation.com/supported-locales) | | Localization report card | [https://generaltranslation.com/report-card](https://generaltranslation.com/report-card) | | Legal pages | [https://generaltranslation.com/legal](https://generaltranslation.com/legal) | | Dashboard sign-in | [https://dash.generaltranslation.com](https://dash.generaltranslation.com) | | Social | [https://x.com/generaltxn](https://x.com/generaltxn), [https://www.linkedin.com/company/generaltranslation/](https://www.linkedin.com/company/generaltranslation/) | | Open source | [https://github.com/generaltranslation](https://github.com/generaltranslation) | | Design hub (this repository) | [https://www.prototemplate.com](https://www.prototemplate.com/), with /brand, /docs (this handbook is /docs/handbook), /skills, /deck, /motion, /graphics, /marks, /blog and /d/production | | Companion tooling | [https://glyphfield.com](https://glyphfield.com), the shader and animation studio behind several graphics and films | ## Sources [#sources] * Prototemplate: [BRAND.md](https://www.prototemplate.com/docs/handbook/brand) sections 1, 2 and 9; [gt-brand](https://www.prototemplate.com/skills/gt-brand) sections 1, 2 and 8; [gt-website](https://www.prototemplate.com/skills/gt-website) with `references/docs.md` and `references/pages.md`; [gt-voice](https://www.prototemplate.com/skills/gt-voice); [gt-landing-pages](https://www.prototemplate.com/skills/gt-landing-pages); [gt-components](https://www.prototemplate.com/skills/gt-components). * gt-cloud at origin/main e17fce499 (2026-10-05): `AGENTS.md`, `.gitmodules`, `apps/landing/gt.config.json`, `apps/landing/staging.gt.config.json`, `apps/landing/src/app/[locale]/docs/[variant]/[...slug]/page.tsx`, `apps/landing/src/app/[locale]/blog/[slug]/page.tsx`, `apps/landing/src/lib/legal.ts`, `apps/landing/src/components/landing/sections/global/Global.tsx`, `apps/landing/src/components/pages/enterprise/services-landing/GovernedExplorer.tsx`, `scripts/deploy-landing.sh`, `.agents/skills/gt-landing/SKILL.md`; pull requests #4359, #4499 and #5049. * `generaltranslation/gt` at origin/main a16ae03c6 (2026-10-05): `packages/`, `packages/cli/src/cli/base.ts`, `packages/cli/src/auth/__tests__/oauth.test.ts`, `packages/cli/src/cli/__tests__/initOnboarding.test.ts`, `packages/core/src/runtime.ts`, `packages/next/src/server.ts`, `packages/next/src/index.types.ts`. * `generaltranslation/content` at origin/main (2026-10-05): `AGENTS.md`, Public authentication boundary. * generaltranslation.com, read 2026-10-10: `/pricing` (section 5) and the addresses in section 10, each fetched with curl. Read 2026-10-05: the home page, `/en-US/pricing`, `/en-US/enterprise`, `/en-US/careers`, `/ja`, `/llms.txt`, `/llms-index.txt`, `/agent-prompt.md`, `/openapi.yaml` and the docs guide Defining context for translations. * npm and PyPI package pages, and the public repository list of the `generaltranslation` GitHub organization, read 2026-10-05. * Kevin's rulings: the full stack, the barbell, the buyers who cannot yet name localization, and the context layers (2026-07-30); layout, review surfaces, formatting and region names (2026-08-06); locale heroes and staging (2026-08-08); showing over telling (2026-08-11); the enterprise pillars and CDN copy (2026-08-15); the enterprise order and the pricing cards (2026-08-15 and 2026-08-16); en-GB routing (2026-08-17); the copy page menu (2026-09-11); the agent button and AX (2026-09-30); the interactive CLI (2026-10-01); geography by language (2026-10-03). * The list of brand surfaces sent to the brand agency on 2026-09-04, from which section 10 is rebuilt and checked against the live addresses. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Glossary > The terms in Kevin’s messages, the skills and the code, each defined in a sentence or two with where it lives. URL: https://www.prototemplate.com/docs/handbook/glossary This glossary defines the terms that appear in Kevin Liu's messages, in the General Translation (GT) skills and in the code of gt-cloud and Prototemplate. Each entry defines its term in one to three sentences and names the file, skill or document that owns it. The owner holds the full rule, so where the two disagree the owner is right and this page needs a fix. The vocabulary covers 2026-07-20 to 2026-10-05, and the retired terms in the last section point to [decisions.md](https://www.prototemplate.com/docs/handbook/decisions). Paths without a prefix are in a Prototemplate checkout. A path marked `gt-cloud:` is in gt-cloud, `gt:` marks one in the open-source gt repository and `content:` one in the content repository. ## Company and products [#company-and-products] | Term | Meaning | Where it lives | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | General Translation (GT) | General Translation, Inc. builds the full stack for localization: open-source i18n libraries, context-aware translation APIs, a platform for review and delivery, and the Locadex agent. GT is the short form and the mark. | [BRAND.md](https://www.prototemplate.com/docs/handbook/brand) sections 1 and 2; [gt-product-map.md](https://www.prototemplate.com/docs/handbook/gt-product-map) section 1 | | gt (the libraries) | GT publishes its open-source i18n libraries from one public monorepo. On npm the package named `gt` is the CLI, and `generaltranslation` is the core library the others build on. | `gt:packages/`; BRAND.md section 1 | | gt-next and its siblings | These framework packages render translations and format locale-sensitive values in an app: `gt-next`, `gt-react`, `gt-tanstack-start`, `gt-react-native`, `gt-vue` and `gt-node`, with `gt-flask` and `gt-fastapi` for Python. gt-cloud's own apps translate their UI with `gt-next`. | `gt:packages/next` and its neighbours; the Python SDKs in `generaltranslation/gt-python`; gt-product-map.md section 3 | | `` | The `` component marks a block of JSX for translation. Wrap the largest static block once, never nest it, pass dynamic parts through `Var`, `Num`, `Currency`, `DateTime`, `Plural` and `Branch`, and guide one block with its `context` prop. | [`gt-components`](https://www.prototemplate.com/skills/gt-components) `references/react.md`; gt-cloud `CLAUDE.md` | | `gt()` | The `gt()` function translates a string. It covers the user-facing props that `` does not reach, such as a placeholder, a title or an `aria-label`. | gt-components `references/react.md` | | `msg()` | The `msg()` function marks the strings of a data array where the array is declared. The component that renders the array reads them through `useMessages()` or `getMessages()`. | gt-components `references/react.md` | | `useGT` | The client hook returns the `gt` function itself, so the line is always `const gt = useGT();` and never `const { gt } = useGT()`. | gt-cloud `CLAUDE.md` | | `getGT` | Async server components and `generateMetadata` get the function with `const gt = await getGT();`, imported from `gt-next/server`. | gt-cloud `CLAUDE.md` | | The gt CLI | The CLI signs in with `gt login` through GT's sign-in page in the browser, and `gt translate` translates a project after a `--dry-run`. Because `gt init` and `gt configure` wait on interactive prompts with no timeout, an agent passes `--no-interactive` and a flag for every question. | `gt:packages/cli`; gt-product-map.md section 7 | | Locadex | Locadex is GT's hosted AI agent. It internationalizes a codebase, generates translations, opens pull requests and keeps the app localized afterwards. Its name is never translated, and drawings seat its real mark as an alpha mask (the gt-ui rule `no-gif-mark` refuses a gif). | `gt-cloud:apps/locadex`, `public/brand/locadex-mark.svg`; [`gt-brand`](https://www.prototemplate.com/skills/gt-brand); gt-product-map.md section 3 | | Context Group | A Context Group is a reusable collection of translation context kept at the organization level: a Glossary and the style prompts. A project applies groups in priority order, and context then narrows from the organization to the project to the component (``). | `gt-cloud:apps/dashboard`, route `org/[orgId]/context/[groupId]`; gt-product-map.md section 4 | | Glossary | The Glossary is the terminology half of a Context Group. It lists product and brand names, feature names, technical terms and phrases that stay untranslated, and its standing example is that Locadex is never translated. | the group's `glossary` route; gt-product-map.md section 4 | | Directives | Directives was Kevin's July name for the style half of a Context Group: tone, audience, formality, conventions and formatting. The dashboard, the API and the pricing page call it Custom Prompts, copy uses that name, and the code keeps `directives` (`DirectivesEditor`). | the group's `custom-prompts` route; gt-product-map.md section 4 | | The dashboard | The dashboard is GT's product app at dash.generaltranslation.com. It holds projects, Context Groups, the translation editor, review, billing, and the sign-in and onboarding pages on the plate frame, and its product surfaces are judged against the brand deck. | `gt-cloud:apps/dashboard`; gt-cloud `.agents/skills/gt-dashboard`; [`gt-aesthetic`](https://www.prototemplate.com/skills/gt-aesthetic) section 1 | | The translation CDN | The translation CDN is an edge service on Cloudflare Workers that serves translations. Through it a team pushes translation updates over the air without redeploying its app. | `gt-cloud:apps/edge`; gt-product-map.md section 3 | | Setup for Agents | Setup for Agents is the second button in the landing hero (gt-cloud #5049). It copies the start prompt that takes a coding agent from an empty project to a first translation, and every change to the prompt, the CLI or the quickstarts is proven by handing the prompt unchanged to a fresh agent on a fresh app. | `content:docs/en-US/overview/for-coding-agents.mdx`, served at `/agent-prompt.md`; [`gt-website`](https://www.prototemplate.com/skills/gt-website) section 6 | | The report card | The report card is a public page that grades how localized any site is on six checks: hreflang, language declaration, routing, metadata, content and charset. Prototemplate's copy is the `/report-card` page of the shipped site. | generaltranslation.com/report-card (`gt-cloud:apps/landing/src/app/[locale]/(home)/report-card`, `apps/landing/src/lib/report-card/`); `/d/production/report-card`, `src/components/try/`, `src/lib/try/` | ## Repositories and places [#repositories-and-places] | Term | Meaning | Where it lives | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | gt-cloud | The gt-cloud repository is GT's private product monorepo on turbo and pnpm. It holds the dashboard, the landing site with the docs and blog, the API, workers, the AI proxy, the edge CDN, Locadex, the admin app and the shared packages, and it squash-merges small PRs from `k/` branches once three required checks pass and the review bots are answered. | github.com/generaltranslation/gt-cloud; gt-cloud `AGENTS.md`; [`gt-ship`](https://www.prototemplate.com/skills/gt-ship) section 5 | | The gt repository | The public monorepo `generaltranslation/gt` holds the CLI, the JavaScript libraries, the core library, the compiler and the MCP server. CLI changes such as the login callback page land there. | github.com/generaltranslation/gt; gt-product-map.md section 8 | | The content repo | The public `generaltranslation/content` repository holds the docs, blog posts and authors, and gt-cloud mounts it as the `apps/landing/content` submodule. Every landing deploy checks out the tip of content main, so merging a content PR publishes it, and when a post needs new components, images or redirects the gt-cloud side merges first. | gt-website sections 4 and 5; gt-product-map.md section 8 | | Prototemplate | Prototemplate is Kevin's public hub and wiki for GT work. Its Next.js site, served at [www.prototemplate.com](http://www.prototemplate.com), prototemplate.vercel.app and prototemplate.kevinliu.studio, holds the brand book, the design canon, the deck, the design lab, the marks, the films, the graphics, the skills and this handbook. Its `main` holds the primary copy of the canon, all rights are reserved to General Translation, and its dev server runs on localhost:3005. | github.com/Kevin-Liu-01/Prototemplate; [`prototemplate`](https://www.prototemplate.com/skills/prototemplate) section 1 | | The deck | The deck is the GT brand deck: 93 slides built from `deck/` into one self-contained viewer that `/deck` frames. It is the reference for every product surface, and the Prototemplate viewer shell was modeled on its viewer (2026-09-08). | `deck/`, `deck/DECK-GRAMMAR.md`, `pnpm build:deck`; [`gt-deck`](https://www.prototemplate.com/skills/gt-deck) | | The Dossier | The Dossier (`singularity-dossier`) is the direction the shipped GT site grew from, and it stays in the gallery. Kevin called it complete on 2026-08-06 ("dossier is indeed our completed version and treat it as such"), and it was the reference for every marketing page until 2026-10-07, when he ruled "fix the dossier references". Since then the shipped generaltranslation.com has been the site's reference ([decisions.md](https://www.prototemplate.com/docs/handbook/decisions)). | `/d/singularity-dossier` and its `/enterprise`; BRAND.md section 8; gt-aesthetic section 1 | | Toolchain | Toolchain is the direction that served as the single source of truth for the forks from July. It set one ruled column, bento rows, isometric line diagrams and the `tc-*` vocabulary that the gt-cloud landing engine still uses, and its enterprise and pricing pages are literal specs, copy included. | `src/app/d/toolchain/`, `/d/toolchain`; [ARCHITECTURE.md](https://www.prototemplate.com/docs/handbook/architecture) "The SSOT rule" | | A direction | A direction is one exploration of the GT site's design, served at `/d/` and registered in `DIRECTIONS`, where four entries are full sites (the Dossier, Orbit, Signal and production). In July and August Kevin named directions by their label numbers, and the labels were renumbered more than once (Toolchain was 10 then and is 01 now). | `src/lib/directions.ts`; `/directions/`; retired ones at `/archive/`; [`gt-explorations`](https://www.prototemplate.com/skills/gt-explorations) section 3 | | A fork | A fork is a direction built on Toolchain's sections and diagrams, imported directly and re-skinned through its own root class (`.lensgate-root`, `.terminusboard-root`). Fork work never edits Toolchain's files, and a forked session is a different thing, defined under Working terms. | ARCHITECTURE.md "The SSOT rule" | | The presenter | The presenter at `/present` is a full-screen tool built on 2026-07-30 to review the directions. It runs an intro, the case for the redesign, a craft gallery, a live viewer of the first 16 prototypes and a scoreboard. | `src/app/present/` | | /d/production | This route rebuilds the shipped generaltranslation.com page for page in Prototemplate, with the dashboard's sign-in, onboarding, consent, device and CLI pages and their draggable state console (`?state=` opens a state). Review galleries that Kevin asks for live here, outside gt-cloud PRs. | `src/app/d/production/`, `src/components/plate/`; gt-ship section 3 | | /dev/states | The dashboard's `/dev/states` route is a development-only gallery of every sign-in and onboarding state. It stays on its own gt-cloud branch and never ships in a PR (2026-10-01). | gt-cloud's own `gt-dashboard` skill | | The review server | The review server is the running dev server Kevin reviews a branch on. The landing's `pnpm dev` serves localhost:3001 and Prototemplate serves localhost:3005, each extra worktree takes a port of its own, and the session keeps the server up and says which branch and commit it serves. | gt-local-dev section 1; gt-website section 2; prototemplate section 7 | | motion/ | The `motion/` folder in Kevin's Prototemplate checkout is untracked, and the Videos session builds the films there with HyperFrames from the brief `motion/MOTION.md`. Other sessions read it and never stage, edit or delete it, and the published copies live in `public/media/` and on `/motion`. | [`gt-films`](https://www.prototemplate.com/skills/gt-films) section 1 | | The scratchpad | The scratchpad is the temporary folder Claude Code gives each session for specs, context files, harnesses and recorded fixtures. Nothing in it reaches a PR, and its files can vanish at the desktop app's date change, so durable work goes into a repository. | gt-ship section 3; gt-local-dev section 6 | ## Design vocabulary [#design-vocabulary] | Term | Meaning | Where it lives | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Rail | A rail is the single hairline on each side of the page column, drawn once by the column wrapper (`.tc-rail` with `border-inline: 1px solid var(--tc-hair)`). Since 2026-09-28 a column has one rail on each side, and the `single-rail` lint holds it. | [DESIGN.md](https://www.prototemplate.com/docs/handbook/design) section 3; [`gt-landing-pages`](https://www.prototemplate.com/skills/gt-landing-pages) "Rail and bands" | | Band | A band is a horizontal section of a landing page. A band inside the column (`.tc-sec`) draws only its bottom rule, and a full-bleed band (`.tc-band.tcb`, `.v0-dep`) spans the viewport and draws its own rail pair once, at the column edges. | gt-landing-pages "Rail and bands" | | Hatch spacer | A hatch spacer is a strip 28 to 40px tall with a hairline on each edge over a 45 degree hatch. It separates home bands (`.v0-hatch`) and rows inside a section (`.tc-hatch`), and it owns both of its edges, so the section before it drops its bottom rule. | gt-landing-pages "Hatch spacers"; DESIGN.md section 2 "Sanctioned devices" gives the shell's version, the book page's band (`.pt-book-band`), which also draws its own rule on both edges (2026-10-06) | | Seam | A seam is the 1px line where two rows, cells or sections meet, formed by the row's hairline ground showing through a 1px gap. DESIGN.md section 10 also calls the slide-to-reveal handle the seam (`RevealSeam`, `--seam-cut`). | DESIGN.md sections 2 and 10 | | The row owns every seam | This ownership rule for grids says that the row draws every structural line between its cells, cells draw no borders, and a framed cell shows the row's ground through a 1px padding reveal. Where a row meets a line that already exists, the cell sits flush and drops that side's reveal. | DESIGN.md section 2 "Ownership"; gt-landing-pages "The row owns every seam" | | BentoRow and BentoCell | These two grid primitives implement that rule. The row is a grid with `gap-px` over the `--tc-hair` ground (props `cols`, `eqHeads` and `headH`), and the cell has no border props (`framed` adds the reveal). | `gt-cloud:apps/landing/src/components/landing/shell/Bento.tsx`, `src/components/shell/Bento.tsx`; gt-components | | Registration cross | A registration cross is a small 1px plus seated exactly where two real hairlines cross. Kevin calls them corner crosses and DESIGN.md calls them border crosses, each landing section seam paints one at both rail corners, and a cross floating in app UI is ornament (2026-09-25). | DESIGN.md section 2 "Sanctioned devices"; gt-landing-pages "Registration crosses"; gt-aesthetic section 3 | | The doubled line | The doubled line is GT's connector. One SVG path is stroked twice, a full ink stroke under a narrower stroke in the surface color, which leaves two parallel hairlines at a constant gap. It is the one sanctioned double line, also called the thread, and the GT mark draws every stroke in this grammar. | DESIGN.md section 5; `src/components/shared/diagrams/DoubledLine.tsx`; [`gt-diagrams`](https://www.prototemplate.com/skills/gt-diagrams) | | The plate | The plate is the dashboard's auth frame for the sign-in, consent, device, CLI and onboarding pages, with the form column on the left up to `--plate-edge` and the dithered field on the right. Elsewhere a plate is the one second surface under diagrams (`--pt-plate`), a layer of an isometric stack, or one of the speed marks. | `gt-cloud:apps/dashboard/src/components/frame/PlateFrame.tsx`, `src/components/brand/FieldStack.tsx` and `src/app/brand-tokens.css` there; the port in `src/components/plate/` | | Hairline roles | The three line weights each have one job. The `hair` weight (ink at 18% in light) draws large surfaces and the lines that divide a page, `hair-soft` (ink at 9%) draws list rows, and `edge` (ink at 62%) frames a picture or a capture, so a large surface drawn in the edge weight reads as a boxed image. | DESIGN.md section 2 "Line law for chrome"; `src/components/viewer/tokens.css` | | The book page | Every Prototemplate page with a book head renders one structure: the page's plain name as the title with nothing drawn above it, a lead of one to three lines, the panel (Updated, then the page's facts), one rule, an optional note and contents, one hatch band, then sections opened by a `Section n` divider. A page differs from another only in its words and its facts. | DESIGN.md section 4, The book page; `BookHead` in `src/components/viewer/BookView.tsx`; `pnpm lint:heads` | | Rounded controls, square shells | Prototemplate's corner law: the shells that hold the interface (the frame, the sidebar, the toolbar bar, the sheet, rules and bands) are square, and everything a reader presses, types into or picks up is rounded: controls at 6px, a part inside one at 5px, chips at 4px, cards and popovers at 6px. | DESIGN.md section 2, Corners; the radius tokens in `src/components/viewer/tokens.css`; `pnpm lint:radius` | | Ink | Ink is `#070707`, the darkest of the four absolute colors, used for content text on paper and as the ground of the dark theme. Raised ink (`#101010`) is the one dark artifact surface for code, config and diffs. | DESIGN.md section 1; `src/app/globals.css` | | Ink-2 | Ink-2 is the second text step, for secondary text and leads (`--pt-ink-2`, `#3a3d44` in light). On a light surface, text a reader needs is set in ink-2 wherever titanium would fail contrast. | gt-aesthetic section 3; `src/components/viewer/tokens.css` | | Titanium | Titanium is `#8a8f98`, the third and last text step, for captions, counters and keys. It measures 3.25:1 on white, so the dashboard plate darkens it to `#6e737c` for its 13px captions. | DESIGN.md section 1; gt-aesthetic section 3 | | Paper | Paper is `#ffffff`, the light ground. Dark mode is a token remap in which paper collapses onto ink, so each theme has one surface. | DESIGN.md section 1 | | GT blue | GT blue is the one accent, `#2f5ce0`, lifted to `#86a8ff` on dark bands. It marks actions, hover, highlights and icons while text and underlines stay ink, and a drawing gives it to one element. | DESIGN.md section 1; BRAND.md section 5; gt-aesthetic section 3 | | The mobile type ladder | The mobile type ladder is the set of `--tcm-*` tokens that set type and spacing under 720px, declared in the engine's last 720px block and read as `var(--tcm-X, )`. The mobile cut in `v0-pages.css` outranks the engine on the `sgdh-root` pages, so a change to the ladder is made in both places. | DESIGN.md section 12 | | The svh and dvh law | On a mobile stage, whatever must reach the true screen bottom sizes from `100dvh`, and every height that takes part in layout uses `100svh`. A layout height in dvh grows the document each time the URL bar shows or hides. | DESIGN.md section 13 | | Read lines | The read lines are the two scroll anchors of a scroll story. A beat locks in when its copy block's center crosses the read line at 55% of the viewport, and its copy lights as it rises through the highlight line at 80%. | DESIGN.md section 14; `src/app/d/_v0/sections/FullStack.tsx` | | The moving type law | The moving type law governs a word that morphs between languages. The word is one shaped text node with `lang` and `dir`, its width is measured from a hidden probe and is the only layout property that animates, and the host drives one clock per page. | DESIGN.md section 8; `src/components/shared/EverySentence.tsx`; [`gt-motion`](https://www.prototemplate.com/skills/gt-motion) section 5 | | Box-air | Box-air is the mobile standard for boxed cells: block padding of 36 and 38px (`--tcm-box-pt`, `--tcm-box-pb`), 28px (`--tcm-box-gap`) between a box's head and its artifact, and no copy within 20px of a hairline. | DESIGN.md section 12 "The box-air standard" | ## Material and motion [#material-and-motion] | Term | Meaning | Where it lives | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Bayer dither | Bayer dither is ordered dither through a 4x4 or 8x8 Bayer threshold matrix, and it is the brand's only texture. A tone ramp prints as whole square cells in one ink, and alpha veils, smooth gradients and error diffusion are refused. | DESIGN.md section 7; `src/lib/dither.ts`; [`gt-dither`](https://www.prototemplate.com/skills/gt-dither) section 1 | | The studio field | The studio field is the GPU engine for the animated Bayer materials: `createStudioField` with ten `BAYER_PRESETS` sharing one WebGL context. Preset 02, bayer-8x8 (`bayer8`), is the default, the landing hero's field and the reference for material on product surfaces. | `src/lib/studio-field.ts`, `gt-cloud:apps/landing/src/lib/studio-field.ts`; gt-dither sections 2 and 3 | | Glyph field | The glyph field is the canvas engine (`createGlyphField`) that rains glyphs from eight writing systems and condenses them into words, conserving matter between morphs. Its glyphs stay solid anti-aliased type, because dither on moving glyphs flickers (2026-08-06, 2026-09-28). | `src/lib/glyph-field.ts`, `gt-cloud:packages/ui/src/lib/glyph-field.ts`; BRAND.md section 7; gt-dither section 3 | | Glyph rain | Glyph Rain is a direction, and the landing component of the same name mounts the glyph field rising (`GlyphRain.tsx`, `drift: 'rise'`). Kevin uses the name for the effect wherever it appears, such as the careers page and the pricing close. | `src/app/d/glyph-rain/`, `gt-cloud:apps/landing/src/components/landing/shared/GlyphRain.tsx`; gt-landing-pages | | Ink field | The ink field is the closing band's material (`createInkField`). The glyph field's eight-script glyphs rise off the ink in the band's margins, stay clear of the content box measured from the live DOM, and answer the pointer. | `src/app/d/glyph-rain/sections/band/inkField.ts`, `gt-cloud:apps/landing/src/components/landing/shared/InkField.tsx`; gt-components `references/instruments.md` | | Event horizon | Event Horizon is the direction where component walls dive into a black-hole gate, and the name also covers its WebGL lensing engine. The landing's careers and 404 heroes stand on that field, and as page-scale material in the app it was judged too heavy and deleted (2026-09-25). | `src/app/d/event-horizon/`, `src/lib/horizon-field.ts`, `gt-cloud:apps/landing/src/lib/horizon-field.ts` | | The terminus board | Terminus Board is the direction with a split-flap headline over a flip-wave grid of languages, styled on an airport departures board. The landing's blog header is a split-flap board built from it, and its face settle is the one overshoot easing the landing allows. | `src/app/d/terminus-board/`; `gt-cloud:apps/landing/src/components/blog/BlogHeroBoard.tsx` and `flap.ts`; gt-motion section 2 | | Gem smoke | Gem smoke is Paper Shaders' Gem Smoke material (Apache-2.0) as Glyphfield renders it, in blue, fire and ink palettes. It is the films' palette, it fills two deck section openers and the designed blog covers, and it can wrap a mark such as the bar monogram as a glass shape. | `motion/kit/gemsmoke.js`; gt-films sections 5 and 6; gt-deck `references/full-picture-slides.md` | | Glyphfield | Glyphfield is Kevin's open-source shader and animation studio at glyphfield.com/studio, where new GT material starts before it ships as engine code or an export. Agents drive it through `window.glyphfield.studio`, and its name refers to the studio while "glyph field" names the engine above. | github.com/Kevin-Liu-01/Glyphfield; gt-dither section 4; gt-cloud `.agents/skills/glyphfield` | | Artifact pictures | Artifact pictures are photographs and scans of objects, artworks and places, screened through the house 8x8 dither at 1 CSS px cells. They appear on the dashboard's sign-in and onboarding field, the plate port, the deck's mood slides and the transition demo in the build log on /docs/handbook/libraries, and each one shows writing, language or the earth, carries no readable English prose and credits its source. | [docs/ARTIFACT-PICTURES.md](https://www.prototemplate.com/docs/handbook/artifact-pictures); gt-dither section 5 | | The Blue Marble standard | The Blue Marble standard is the tone and screen standard every artifact picture is cut to, taken from the Blue Marble picture of the earth on the onboarding field. A scene lands on mean tone 0.361 and standard deviation 0.342, the cutter is `pnpm mood-tone`, and the picture lint pins the standard file by sha256. | `scripts/media/mood-tone/standard.json`; docs/ARTIFACT-PICTURES.md, "The rules" items 3 and 4 | | Speed marks | The speed marks are the seven race-type GT marks Kevin chose on 2026-09-29 to show beside the current doubled-line mark: the bar monogram, its lockup, the plate, double cut, the livery stack, the dithered monogram and the ASCII monogram. They are generated from geometry and font outlines and never redrawn by hand. | `scripts/build/speed-marks.mjs` (`pnpm build:marks`), `public/marks/`, `/marks`, deck slides 17 to 23; gt-brand `references/marks.md` | | The bar monogram | The bar monogram is the speed mark that builds G and T from rectangles on a 120-unit cap, slanted forward 12 degrees, with one cut at mid cap height and three speed bars leading into the G. It holds at 16px and has lockup, dithered and ASCII versions. | `public/marks/bar-monogram.svg`; gt-brand `references/marks.md` | This plate mounts the studio field. Each numbered chip remounts it with one of the ten `BAYER_PRESETS`, and chip 02 is bayer-8x8, the field of the landing hero. This plate runs the glyph field. Glyphs from eight writing systems fall and condense into the word for language in one script after another, and the glyphs of each word become the next word. This plate runs the ink field in its interactive mode. Glyphs rise off the ink, the glyphs near the pointer shiver, and a click bursts the nearest one. This plate runs the event horizon field: a photon ring around a dark core, with ruled lines bending into it. ## Gates and tools [#gates-and-tools] | Term | Meaning | Where it lives | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | The line auditor | The line auditor (`scripts/lint/lines.mjs`) reads computed CSS in Chrome and fails on doubled lines, two owners on one seam, self-stacks, invisible seams and chrome borders outside the three hairline roles. The shell audit `pnpm lint:lines:shell` walks the shell routes against the dev server on 3005, `pnpm lint:lines ` audits any page, and SVG strokes still need 2x crops checked by eye. | DESIGN.md section 2; [`gt-lints`](https://www.prototemplate.com/skills/gt-lints) section 2, `references/line-auditor.md` | | The page check | The page check (`pnpm check:pages`) loads every page on a table of phones, tablets and desktops in both themes, reading phones and tablets as touch devices. It reports overflow, clipping, console errors, tap targets under 40px, layout shift and failed interactions in `.pagecheck/REPORT.md`, and it runs on the touched pages each round and on the whole site before a release. | `scripts/check/pagecheck/` and its README; gt-lints section 2 | | The practices ratchet | The practices ratchet (`pnpm lint:practices`) counts nine code checks per file against a baseline, among them bare `useEffect`, `any`, `!important`, raw hex in TypeScript and the retired rail vocabulary. It fails when a file's count rises, so recorded debt is paid down in its own changes. | `scripts/lint/practices.mjs`, `scripts/lint/practices.baseline.json`; gt-lints section 2 | | The gt-ui oxlint plugin | The gt-ui plugin holds gt-cloud's design and copy laws as oxlint rules, among them `single-rail`, `inter-only`, `icon-tiers`, `no-em-dash`, `no-eyebrow`, `no-smooth-scroll`, `mono-is-not-voice` and `shared-cta`. Prototemplate runs a whole-file copy through `pnpm lint:code` and recopies it whenever gt-cloud's version changes. | `gt-cloud:tooling/oxlint-plugins/gt-ui.ts`, `scripts/lint/oxlint-plugins/gt-ui.ts`; gt-lints `references/gt-ui-rules.md` | | The picture lint | The picture lint (`pnpm lint:pictures`) holds every artifact picture to the Blue Marble standard by hash, size, tone and credit. It runs before `next build`, so a picture defect stops the build, and `pnpm test:pictures` breaks one thing per test to prove the lint catches it. | `scripts/lint/pictures.mjs`; gt-lints section 2; gt-dither section 8 | | The type lint | The type lint (`scripts/lint/type.mjs`, started 2026-10-05) holds Prototemplate's type to the rsms InterVariable through the type tokens. It allows no other family, no weight above 500 and no positive tracking, and it keeps mono to code and to named entries such as numbers, paths and hex values. | gt-lints "The type lint"; gt-brand `references/type.md` | | `lint:all` | Prototemplate's `pnpm lint:all` chains the static lints, the browser audits against the dev server on 3005 and the lint tests with `&&`. Run the browser gates one at a time, and read `package.json` for the exact chain. | `package.json`; prototemplate section 9 | | The parity gate and ledgers | The parity gate is the release gate for any onboarding or auth redesign: the release keeps every tracking call and backend side effect that production had. The proof records production's ledger of side effects per scenario at the module boundaries, replays the scenarios on the release and diffs the two ledgers after every fix (2026-10-02). | gt-ship section 7 | | Greptile 5/5 | Greptile writes a confidence score into the PR body, and the target is 5/5 on the head commit with every Greptile thread answered and resolved; a PR does not merge at 4/5 (2026-09-13). It does not rerun on every push, so comment `@greptile review again` and wait about five minutes. | gt-ship section 5, `references/review-loop.md`, `scripts/pr-bots.mjs` | | Bugbot | Bugbot is Cursor's review bot. It reruns on every push, `bugbot run` re-triggers it, and each finding is answered with the fixing commit or a reason before its thread is resolved. | gt-ship section 5 | | The Cursor Approval Agent | The Cursor Approval Agent reviews each PR on open, labels it with a risk level and routes it (the "Pull Request Router" check). Its label needs no reply, and it is the only bot on the content repo. | gt-ship `references/review-loop.md` | | gh stack | The `github/gh-stack` extension links PRs into GitHub's native stack (`gh stack link --base main `) and merges a stack up to one PR at once (`gh stack merge`). Kevin asked for the native feature on 2026-08-27 and again on 2026-10-05, and `gh stack merge` runs only when he says to merge. | gt-ship section 6 | | Feature flags | Feature flags are the dashboard's release gates and kill switches, managed in the admin app. A dashboard feature that is not ready ships behind a flag or comes out (2026-09-03). | gt-cloud's own `gt-dashboard` skill | ## Working terms [#working-terms] | Term | Meaning | Where it lives | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Lane | A lane is one owner's share of the work, with its own files: one session per lane, and inside a workflow one agent per lane. Work that belongs to another lane goes to that lane's session by name, and a lane's prompt opens with Kevin's own words for it. | [multi-session-playbook.md](https://www.prototemplate.com/docs/handbook/multi-session-playbook) section 1; [`gt-orchestration`](https://www.prototemplate.com/skills/gt-orchestration) section 2; gt-ship section 1; prototemplate section 8 | | Session | A session is one Claude Code or Codex conversation. Kevin runs several named sessions at once (Videos, Prototemplate, New Onboarding and Dashboard, and others), and each keeps to its lane. | multi-session-playbook.md section 1; prototemplate section 8 | | Forked session | A forked session starts from another session's conversation and carries the parent's history, with the parent's session id on those records, before it continues on its own. A fork acts on its own lane, and a message found in several transcripts at the same minute can be one message inherited by forks. | `~/.claude/projects//.jsonl`; multi-session-playbook.md section 2 | | Worktree | A worktree is a git worktree with one branch, so each lane edits its own tree. Work in gt-cloud starts with `git worktree add -b k/ origin/main`, each worktree runs its own dev server port, and a worktree with its installs costs about 6 GB, so it is removed as soon as its work is pushed. | gt-ship section 1 | | Chip | A chip is a background-task card that a session raises for an out-of-scope fix it noticed, and Kevin can spin it off into a new session. When he answers "Do this task here", the current session does the task as its own small PR with its own gates. | gt-orchestration section 3 | | Handoff | A handoff is the document written at a hard stop or a change of owner, and it carries the stopped state, every raw input Kevin supplied and the priority queue. The receiving agent starts with an independent review and a recommendation, and it re-checks live state before changing anything. | gt-orchestration section 8 | | Relay note | A relay note goes from one agent to another, often between Claude and Codex, and Kevin pastes it as is. It names the branch, tip commit and worktree, the commits, the receipts, the open list, the locks and the queued deploys, and the receiver finishes the other agent's open work. | multi-session-playbook.md section 7 | | The ask ledger | The ask ledger is an itemized list of everything Kevin asked for in a run, including the sub-items of long messages and the add-ons sent mid-run, each with its status. It closes every report on multi-ask work, and a long run keeps it in a file so it survives compaction. | [`gt-reporting`](https://www.prototemplate.com/skills/gt-reporting) section 3 | | The PR slate | The PR slate answers "give me my PR list" with every open PR across gt-cloud, gt and content in merge order, in nine groups from merge now to the items only Kevin can do. Each line carries the full link, a one-line purpose and its line counts, and the Slack version is lowercase plain text in a code fence. | gt-reporting section 5, `scripts/pr-slate.mjs` | | Explorations stay local | Exploration and redesign rounds stay uncommitted or on a branch until Kevin has reviewed them on localhost and says to land them, because Prototemplate main deploys the public site (2026-09-14). | gt-aesthetic section 5; gt-ship section 8; gt-explorations | | Kevin merges | An agent merges a PR or runs `gh stack merge` only when Kevin says so in that session. | gt-ship sections 5 and 6 | | The agent guide | `AGENTS.md` at the root of Prototemplate is the entry point for an agent: the read order, the principles in brief, which skill to load for a task, the house rules, and how to import the hub into another project. `CLAUDE.md` imports it for Claude Code. | `AGENTS.md`; `/docs/handbook/agents` | ## Kevin's shorthand [#kevins-shorthand] | Term | Meaning | Where it lives | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | "image 1" | "Image 1" names the first image attached to the message, and "image 2" the second, in attach order. Each numbered note maps to a region of its image, and the image is the spec. | gt-reporting section 7; gt-aesthetic section 5 "Rules of the loop" | | "equivalent" or "equal" | Both measures take the larger value (2026-09-30). | gt-aesthetic "Reading Kevin's asks", `references/verdicts.md` | | "keep its aesthetic" | The geometry and the material stay, and only the named thing changes. | gt-aesthetic "Reading Kevin's asks" | | "smaller" or "larger" | The value moves to the next rung of the existing ladder, as when the 13px card facts went to the 12px rung (2026-09-28). | gt-aesthetic "Reading Kevin's asks" | | "fully" | The work covers every section, capability and detail with real diagrams, visuals and code. A placeholder or a partial pass does not count. | [operating-principles.md](https://www.prototemplate.com/docs/handbook/operating-principles) section 15 | | "continue all" or "keep going" | Resume every open lane and every unfinished item, the hard ones included, without pausing between waves. In a forked session the words cover the fork's own lane, and after a restart or a compaction the work resumes from the plans kept on disk. | operating-principles.md section 1; gt-orchestration section 6; multi-session-playbook.md section 2 | | "no new spawns" | Agents already running finish, their work is committed and pushed, and no new agent starts (2026-08-06). | gt-orchestration section 1 | | "show me" | Put the result where Kevin can see or hear it: before and after crops, a running localhost URL, or a small HTML gallery or listening page for a set. | gt-reporting section 1; operating-principles.md section 8 | | "second pass" | A second pass reviews a finished page against its reference before it is shown, and pages that skipped one are redone (2026-08-17). | gt-aesthetic section 2 | | "try again" | After an API error or a spend limit, re-run the step that failed, starting from a fresh read of the state. | multi-session-playbook.md section 3 | | "commit and push" | Commit and push at once to every target he names, staging explicit paths. | gt-ship sections 2 and 8 | ## Retired terms [#retired-terms] These terms still appear in older messages, commits and skills. The ruling that retired each one, its date and its reason are recorded in [decisions.md](https://www.prototemplate.com/docs/handbook/decisions). | Term | What it was | What replaced it | | -------------------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Switzer | Switzer was a second typeface used beside Inter in the July rounds. | Retired 2026-09-18: Inter is the only face, gt-cloud #4887 removed the files, and `inter-only` holds it ([decisions.md](https://www.prototemplate.com/docs/handbook/decisions)). | | Lenis | Lenis was the smooth-scroll library behind the July scroll stories. | Retired from the site 2026-07-31: scrolling is native and `no-smooth-scroll` holds it. The presenter's last Lenis code was removed on 2026-10-08. Its paging still animates the jumps between slides, under a named exemption in `.oxlintrc.json` ([decisions.md](https://www.prototemplate.com/docs/handbook/decisions)). | | Double rails | The double rails were a second pair of lines 10px outside each rail, also called the outer pair or doubled rails. | Retired 2026-09-28 in gt-cloud #5007: one rail per side, held by `single-rail` and the practices ratchet's rail checks ([decisions.md](https://www.prototemplate.com/docs/handbook/decisions)). | | DirectionDock | DirectionDock was the control pill on the direction pages in July. | Retired 2026-07-31 and deleted 2026-09-08. `DirectionCorner` (`src/components/viewer/DirectionCorner.tsx`) took its place and hides under `?chrome=0` ([decisions.md](https://www.prototemplate.com/docs/handbook/decisions)). | | apps/redesign on port 3006 | The July explorations app ran from `apps/redesign` in a gt-cloud worktree on the branch `redesign/diagram-standard`. | Retired as the primary copy on 2026-09-08: Prototemplate main holds the canon and serves on 3005, and prose that names 3006 is stale ([decisions.md](https://www.prototemplate.com/docs/handbook/decisions)). | | The dictionary pictures | These artifact pictures showed pages of readable English: the dictionary, Johnson, OED and Oxford pictures. | Retired 2026-10-05: artifact pictures carry no readable English prose, and the picture lint rejects the retired names ([decisions.md](https://www.prototemplate.com/docs/handbook/decisions)). | ## Sources [#sources] * Prototemplate: BRAND.md sections 1, 2, 5, 7 and 8; DESIGN.md sections 1 to 5, 7, 8 and 10 to 14; ARCHITECTURE.md; docs/ARTIFACT-PICTURES.md; `package.json`; `.oxlintrc.json`; `src/lib/directions.ts`, `src/lib/surfaces.ts` and `src/components/viewer/tokens.css`; `scripts/lint/practices.mjs`; `motion/kit/gemsmoke.js` (header); all read 2026-10-05. * Skills: `prototemplate`, `gt-aesthetic`, `gt-ship`, `gt-lints`, `gt-dither`, `gt-landing-pages`, `gt-website`, `gt-components`, `gt-brand`, `gt-deck`, `gt-films`, `gt-motion`, `gt-diagrams`, `gt-orchestration`, `gt-reporting`, `gt-local-dev` and `gt-explorations`. The handbook pages [gt-product-map.md](https://www.prototemplate.com/docs/handbook/gt-product-map), [operating-principles.md](https://www.prototemplate.com/docs/handbook/operating-principles), [multi-session-playbook.md](https://www.prototemplate.com/docs/handbook/multi-session-playbook) and [decisions.md](https://www.prototemplate.com/docs/handbook/decisions) hold the procedures behind the working terms and the rulings behind the retired ones. * gt-cloud at origin/main e17fce499 (2026-10-05): `apps/landing/package.json` (`dev`), `apps/dashboard` (`components/frame/PlateFrame.tsx`, `components/brand/`, the context routes), `apps/landing/src/lib/` (`studio-field.ts`, `horizon-field.ts`, `report-card/`) and `apps/landing/src/components/blog/`. * The gt repository: `packages/*/package.json` for the package names. * Kevin's messages to Claude Code and Codex from 2026-07-20 to 2026-10-05 and the session transcripts behind them, for the shorthand and the working terms. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Numbers > Every load-bearing number in one place, grouped by area, each with its reason, its token or constant, the lint that holds it and the page that explains it. URL: https://www.prototemplate.com/docs/handbook/numbers This page collects the numbers that General Translation (GT) design and engineering work in Prototemplate depends on, grouped by area. Each row gives the value, the reason for it, the token or constant that carries it, the lint or gate that holds it, and the page that explains it. The explanation lives on the linked page and the value lives in its source file, so a change starts in those two places and this row follows. [Traps and fixes](https://www.prototemplate.com/docs/handbook/traps) indexes the failures behind many of these numbers in the same way. `tokens.css` is `src/components/viewer/tokens.css`, and every other path is relative to the root of the Prototemplate repository. A value in brackets applies under 900px wide and on a phone on its side. In the Held by column a `pnpm` command names the lint or gate, with its rule where the lint numbers its rules, and Review means that no lint holds the value, so a person or a critic checks it on the render. Every value was read from its source on 2026-10-21. ## Type [#type] The site sets one face, the self-hosted rsms InterVariable 4.1, with an optical-size axis from 14 to 32 and a weight axis from 100 to 900. | Value | Reason | Token or constant | Held by | Explained in | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | | 44px at line height 1.04 and -0.025em (32px) | The page title sits in Inter's Display design, which starts at 32px. | `--pt-d1`, `--pt-d1-lh`, `--pt-d1-track` in `tokens.css` | `pnpm lint:type` T7; `pnpm lint:type:live` L3 | [Design system: book type](https://www.prototemplate.com/docs/handbook/design#book-type) | | 32px at 1.05 and -0.025em (26px at 1.15) | A section title is the second step of the display ladder. | `--pt-d2`, `--pt-d2-lh`, `--pt-d2-track` in `tokens.css` | `pnpm lint:type` T7; `pnpm lint:type:live` L3 | [Design system: book type](https://www.prototemplate.com/docs/handbook/design#book-type) | | 24px at 1.25 and -0.02em (21px) | An h2 in prose and a record's title track less at a smaller size. | `--pt-d3`, `--pt-d3-lh`, `--pt-d3-track` in `tokens.css` | `pnpm lint:type` T7; `pnpm lint:type:live` L3 | [Type: tracking by role](https://www.prototemplate.com/docs/craft/type#tracking-by-role) | | 18px at 1.35 | An h3 and a row's title take this step at weight 500 and -0.01em. | `--pt-t-title` in `tokens.css` | `pnpm lint:type` R1 ratchets literal sizes | [Design system: book type](https://www.prototemplate.com/docs/handbook/design#book-type) | | 17px at 1.55 | A head's lead takes this step, and one lead line, 26.35px, is also the panel's row pitch. | `--pt-t-lead`, `--pt-t-lead-lh` in `tokens.css` | `pnpm lint:heads:live` H5 | [Pages: the book page's numbers](https://www.prototemplate.com/docs/pages#the-book-pages-numbers) | | 16px at 1.6 | Prose, notes and section leads take the body step. | `--pt-t-body` in `tokens.css` | `pnpm lint:type` R1 | [Design system: book type](https://www.prototemplate.com/docs/handbook/design#book-type) | | 14px at 1.55, 13px at 1.5 and 12.5px | Table cells and contents links take the small step, captions and table heads the meta step, and group labels in chrome the group step. | `--pt-t-small`, `--pt-t-meta`, `--pt-t-group` in `tokens.css` | `pnpm lint:type` R1 | [Design system: book type](https://www.prototemplate.com/docs/handbook/design#book-type) | | -0.01em and -0.005em | Labels at 13 to 18px and group labels at 12 to 12.5px are set at weight 500, which tightens with size. | `--pt-track-label`, `--pt-track-group` in `tokens.css` | `pnpm lint:type` R2 ratchets literal tracking | [Type: tracking by role](https://www.prototemplate.com/docs/craft/type#tracking-by-role) | | 0 at weight 400, and nothing positive | Running text is never tracked, and positive tracking on Inter is a defect at every size. | The comment under the text ladder in `tokens.css` | `pnpm lint:type` T8; `pnpm lint:type:live` L3 | [Type: tracking by role](https://www.prototemplate.com/docs/craft/type#tracking-by-role) | | 500 at most | The brand stops at 500, and the browser's `bolder` computes `b` and `strong` to 700. | `--pt-w-display`, `--pt-w-strong` and the `:is(b, strong)` base rule in `tokens.css` | `pnpm lint:type` T6; `pnpm lint:type:live` L5 | [Type: weight and stack traps](https://www.prototemplate.com/docs/craft/type#weight-and-stack-traps) | | opsz equal to the font size, clamped to 14 to 32 | With `font-optical-sizing: auto` the browser follows the size, so the Display design starts at 32px and the axis tightens spacing by itself. | No token; `font-variation-settings` is never written | `pnpm lint:type` T5; `pnpm lint:type:live` L4 | [Type: optical size](https://www.prototemplate.com/docs/craft/type#optical-size) | | 398.55px against 380.81px | A 44px line tracked -0.025em measures the first without `calt` named and the second with it, because Chrome drops contextual alternates under letter-spacing. | `--pt-ff-text` and `--pt-ff-display` in `tokens.css` | `pnpm lint:type` T4; `pnpm lint:type:live` L2 and L7 | [Type: contextual alternates under letter-spacing](https://www.prototemplate.com/docs/craft/type#contextual-alternates-under-letter-spacing) | | 32em for body text and 30em for a lead | Inter prose averages about 0.45em a character, so both hold about 66 characters a line. | `--pt-measure`, `--pt-measure-lead` in `tokens.css` | `skills/gt-aesthetic/scripts/measure-type.mjs --strict` exits 1 past 75 characters; `pnpm lint:type:live` L8 | [Type: measure](https://www.prototemplate.com/docs/craft/type#measure) | | 83 to 86 characters in `62ch` at 17px | The `ch` unit is the width of a zero, about 0.63em in Inter, so a measure in `ch` holds far more text than its number suggests. | None; measures are set in em | `measure-type.mjs --strict` | [Type: measure](https://www.prototemplate.com/docs/craft/type#measure) | | 1 to 3 lead lines at 1440 (6 at 390), 200 characters at most | A head's lead reads in two or three lines. | `--pt-measure-lead` in `tokens.css`; `H4` in `scripts/lint/heads.mjs` | `pnpm lint:heads:live` H4 | [Pages: the book page](https://www.prototemplate.com/docs/pages#the-book-page) | | 352,240 and 112,748 bytes | The full roman file is the subsets' source, and its latin subset is the only Inter file every route preloads. | `public/fonts/InterVariable.woff2`; `ptInter` in `src/lib/fonts.ts` | `pnpm lint:type` T3 | [Type: subsets and glyph names](https://www.prototemplate.com/docs/craft/type#subsets-and-glyph-names) | | 107.89% | next/font's fallback face is `local("Arial")` at this `size-adjust`, and Arial draws Hebrew and Arabic before any face listed after Inter. | next/font's `ptInter Fallback` | Review | [Type: scripts and the Arial fallback](https://www.prototemplate.com/docs/craft/type#scripts-and-the-arial-fallback) | | 0.8638em and 0.728em | The baseline sits 0.8638em below the top of a line-height-1 box, and the cap height is 1490 of 2048 units, so display type is seated by its ink. | None; Inter's own metrics | Review | [Type: seating display type](https://www.prototemplate.com/docs/craft/type#seating-display-type) | ## Corners [#corners] Controls are rounded and shells are square. Kevin's toolbar of 2026-10-05 set the values. | Value | Reason | Token or constant | Held by | Explained in | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------- | | 0 | The viewer frame, the sidebar, the toolbar bar, the reading column, the book head, the dividers and full-width rows hold the interface, so they are square. | `--pt-radius-shell` in `tokens.css` | `pnpm lint:radius`; `pnpm lint:radius:live` | [Design system: corners](https://www.prototemplate.com/docs/handbook/design#corners-rounded-controls-square-shells) | | 6px | Buttons, fields, segmented groups and the sidebar's pills are things a reader presses or types into. | `--pt-radius-control` in `tokens.css` | `pnpm lint:radius`; `pnpm lint:radius:live` | [Design system: corners](https://www.prototemplate.com/docs/handbook/design#corners-rounded-controls-square-shells) | | 5px | A part flush inside a control's 1px border stays concentric with it. | `--pt-radius-inner` in `tokens.css` | `pnpm lint:radius` | [Design system: corners](https://www.prototemplate.com/docs/handbook/design#corners-rounded-controls-square-shells) | | 4px | Key caps, chips, tags, inline code and a control set 2 to 7px inside a field take the chip corner. | `--pt-radius-chip` in `tokens.css` | `pnpm lint:radius`; `pnpm lint:radius:live` | [Design system: corners](https://www.prototemplate.com/docs/handbook/design#corners-rounded-controls-square-shells) | | 6px | Cards, tiles, thumbnails, figures and popovers are objects a reader looks at. | `--pt-radius-card` in `tokens.css` | `pnpm lint:radius`; `pnpm lint:radius:live` | [Design system: corners](https://www.prototemplate.com/docs/handbook/design#corners-rounded-controls-square-shells) | | 50% | Dots and avatars on a square box take a full round. | `--pt-radius-round` in `tokens.css` | `pnpm lint:radius:live` | [Docs site traps: the full round corner](https://www.prototemplate.com/docs/engineering/docs-traps#the-full-round-corner) | | The role's radius minus 1 to 3px | A part 1 to 3px inside a rounded box takes the concentric radius. | `calc(var(--pt-radius-) - npx)`, `TOKEN_FORM` in `scripts/lint/radius.mjs` | `pnpm lint:radius` | [Design system: corners](https://www.prototemplate.com/docs/handbook/design#corners-rounded-controls-square-shells) | | 8px | Present's solid button is the one named exception, a chrome request of Kevin's. | `EXCEPTIONS` in `scripts/lint/radius.mjs` | `pnpm lint:radius` K3 and L3 | [Design system: chrome exceptions](https://www.prototemplate.com/docs/handbook/design#15-chrome-exceptions-kevin-asked-for) | | 33,554,400px | Chrome computes Tailwind 4's `rounded-full`, `calc(infinity * 1px)`, to this value, so the radius lint reports it as an illegal corner. | None; a square box takes `--pt-radius-round` | `pnpm lint:radius:live` | [Docs site traps: the full round corner](https://www.prototemplate.com/docs/engineering/docs-traps#the-full-round-corner) | ## Lines [#lines] Every line is drawn once by one owner. In the shell's chrome every rule is 1px in one of three roles, so a line's weight says what it divides. | Value | Reason | Token or constant | Held by | Explained in | | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | Ink at 0.18 in light, 242 at 0.22 in dark | The structural role draws large surfaces and the lines that divide the shell. | `--pt-hair` in `tokens.css` | `pnpm lint:lines:shell` | [Design system: line law for chrome](https://www.prototemplate.com/docs/handbook/design#line-law-for-chrome) | | Ink at 0.09 in light, 242 at 0.1 in dark | The row role draws list rows, panel rows and the sidebar's rail. | `--pt-hair-soft` in `tokens.css` | `pnpm lint:lines:shell` | [Design system: line law for chrome](https://www.prototemplate.com/docs/handbook/design#line-law-for-chrome) | | Ink at 0.62 in light, 242 at 0.55 in dark | The frame role is the darkest line in chrome, because it holds a picture in place against the paper. | `--pt-edge` in `tokens.css` | `pnpm lint:lines:shell` | [Design system: line law for chrome](https://www.prototemplate.com/docs/handbook/design#line-law-for-chrome) | | 59 and 99 in dark, 210 and 174 in light | One and two stacked strokes of `--pt-hair` composite to these greys, so a seam's grey counts its strokes. | `--pt-hair` in `tokens.css` | `skills/gt-verify/scripts/probe.mjs --scan` | [Lines and seams: reading a seam by its grey](https://www.prototemplate.com/docs/craft/lines#reading-a-seam-by-its-grey) | | 4 device pixels at 2x | Two touching 1px rules of an opaque hairline keep one grey and show as a run twice as wide. | `--tc-hair` on the GT landing | `probe.mjs --dsf 2` | [Lines and seams: reading a seam by its grey](https://www.prototemplate.com/docs/craft/lines#reading-a-seam-by-its-grey) | | 1 to 2.5px wide and over 24px long | The line auditor counts a border side, an outline, a border-like shadow, a thin fill or an exposed-ground strip in this range as a line. | Thresholds in `scripts/lint/lines.mjs` | `pnpm lint:lines`; `pnpm lint:lines:shell` | [Lines and seams: the line auditor's thresholds](https://www.prototemplate.com/docs/craft/lines#the-line-auditors-thresholds) | | 1 to 4px apart, overlapping 75% of the shorter line and 80px | Two parallel lines from different owners this close read as one doubled line. | `scripts/lint/lines.mjs` | `pnpm lint:lines` | [Lines and seams: the line auditor's thresholds](https://www.prototemplate.com/docs/craft/lines#the-line-auditors-thresholds) | | Under 1px apart | Two owners draw one seam and their alphas stack, which the auditor reports as a junction. | `scripts/lint/lines.mjs` | `pnpm lint:lines` | [Gates: what the line auditor counts](https://www.prototemplate.com/docs/gates#what-the-line-auditor-counts) | | Within 45 summed RGB steps of the ground | A strip this close to the page's ground is an invisible seam. | `scripts/lint/lines.mjs` | `pnpm lint:lines` | [Gates: what the line auditor counts](https://www.prototemplate.com/docs/gates#what-the-line-auditor-counts) | | Fill alpha 0.12 to 0.95 on an edge of 120px or longer | A self-stack fails only at this size, so chips and small controls are reported without failing. | `scripts/lint/lines.mjs` | `pnpm lint:lines` | [Lines and seams: self-stacks and stacked grounds](https://www.prototemplate.com/docs/craft/lines#self-stacks-and-stacked-grounds) | | Opacity 0.05 or less | The auditor skips such elements, with SVG and canvas subtrees, so those are read by eye at 2x crops. | `scripts/lint/lines.mjs` | Review of 2x crops | [Lines and seams: the line auditor's thresholds](https://www.prototemplate.com/docs/craft/lines#the-line-auditors-thresholds) | | 100vmax | A book's rules paint as a border image outset this far past both ends, so they run across the stage while the element keeps the column's layout. | `--pt-bleed-top`, `--pt-bleed-bottom`, `--pt-bleed-block` in `tokens.css` | `pnpm lint:heads:live` H6; the practices lint's `border-image-none` | [Lines and seams: rules that run the full width](https://www.prototemplate.com/docs/craft/lines#rules-that-run-the-full-width) | | 1104px | The GT landing's original pages ran `max-w-[1120px] px-2`, so their side lines were 1104px apart, and a 1100px rail doubled every hairline by 2px a side. | `--tc-rail` in gt-cloud's landing engine | The practices lint's retired-rail checks | [Lines and seams: rails drawn as borders](https://www.prototemplate.com/docs/craft/lines#rails-drawn-as-borders) | | 9px arms hanging 5px past the rail, at 0.38 in light and 0.34 in dark | A registration cross marks where a section seam meets the rail, and the page root takes `overflow-x: clip` for its arms. | `--cross-ink` in gt-cloud's landing engine | Review | [Lines and seams: registration crosses and horizontal clipping](https://www.prototemplate.com/docs/craft/lines#registration-crosses-and-horizontal-clipping) | ## The book page [#the-book-page] Every page with a book head renders one structure, and every space in it is a token. | Value | Reason | Token or constant | Held by | Explained in | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ----------------------------------------------- | -------------------------------------------------------------------------------- | | 44px (24px) | The title sits this far under the stage's top, nothing is drawn in that space, and no line crosses a title. | `--pt-title-clear` in `tokens.css` | `pnpm lint:heads` S6; `pnpm lint:heads:live` H3 | [Pages: the book page's numbers](https://www.prototemplate.com/docs/pages#the-book-pages-numbers) | | 14px | This space separates the title's box from the lead's box. | `--pt-head-gap` in `tokens.css` | `pnpm lint:heads` S6 | [Pages: the book page's numbers](https://www.prototemplate.com/docs/pages#the-book-pages-numbers) | | 56px | This gap separates the lead's column from the panel. | `--pt-head-col-gap` in `tokens.css` | `pnpm lint:heads` S6 | [Pages: the book page's numbers](https://www.prototemplate.com/docs/pages#the-book-pages-numbers) | | 304px | The panel's column fits its longest value, a date with its hint such as "Sep 10, 2026 4 weeks ago", which measures 168px. | `--pt-head-panel` in `tokens.css` | `pnpm lint:heads` S6; `pnpm lint:heads:live` H5 | [Badges and heads: the head's numbers](https://www.prototemplate.com/docs/components/heads#the-heads-numbers) | | 26.35px | One lead line sets the panel's row pitch, so every fact sits on a lead baseline. | `--pt-row` in `tokens.css` | `pnpm lint:heads:live` H5 | [Pages: the book page's numbers](https://www.prototemplate.com/docs/pages#the-book-pages-numbers) | | 26px | The mast's rule sits this far under the taller of the lead and the panel. | `--pt-head-rule-pad` in `tokens.css` | `pnpm lint:heads:live` H6 | [Pages: the book page's numbers](https://www.prototemplate.com/docs/pages#the-book-pages-numbers) | | 42px | The hatch band measures this with both of its rules, and its 1px diagonal repeats every 7px. | `--pt-band-h`, `--pt-hatch` in `tokens.css` | `pnpm lint:heads` S3; `pnpm lint:heads:live` H7 | [Design system: sanctioned devices](https://www.prototemplate.com/docs/handbook/design#sanctioned-devices) | | 48px (40px) | A rule sits this far under the block above it. | `--pt-sec-over` in `tokens.css` | `pnpm lint:heads:live` H7 | [Pages: the book page's numbers](https://www.prototemplate.com/docs/pages#the-book-pages-numbers) | | 30px | An h2's box sits this far under its rule. | `--pt-sec-pad` in `tokens.css` | `pnpm lint:heads:live` H3 | [Pages: the book page's numbers](https://www.prototemplate.com/docs/pages#the-book-pages-numbers) | | 32px (24px) and 56px (16px) | Blocks in a book column sit this far apart, and the column takes this side padding. | `--pt-book-gap`, `--pt-col-pad-x` in `tokens.css` | `pnpm lint:heads` S6 | [Pages: the book page's numbers](https://www.prototemplate.com/docs/pages#the-book-pages-numbers) | | 193.16px | At 1440 a one-line title's mast rule sits this far below the title's top on every page; the panel's four rows set it. | `H9_GAP` in `scripts/lint/heads.mjs` | `pnpm lint:heads:live` H9 | [Pages: the book page's numbers](https://www.prototemplate.com/docs/pages#the-book-pages-numbers) | | 35.5px at 1440 and 79px at 390 | Every section's h2 sits the same distance under its rule. | `scripts/lint/heads.mjs` | `pnpm lint:heads:live` H8 | [Pages: the book page's numbers](https://www.prototemplate.com/docs/pages#the-book-pages-numbers) | | 52px, and 208px or 256px | The toolbar is 52px tall, and the sidebar is 208px wide in outline density and 256px with thumbnails. | `--pt-bar-h`, `--pt-sb-w` in `tokens.css`; `ViewerShell.css` | Review | [Pages: the viewer shell](https://www.prototemplate.com/docs/pages#the-viewer-shell) | ## Motion [#motion] Bounce, elastic and back overshoot are not used, and every number here has a reduced-motion still. | Value | Reason | Token or constant | Held by | Explained in | | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------- | | 0.5s | Film arrivals and scene changes land on this beat grid. | The film kit's beat grid; `skills/gt-motion/SKILL.md` | Review | [SVG lines in motion: timing numbers](https://www.prototemplate.com/docs/craft/svg-motion#timing-numbers) | | (words / 3) + 1s | A sentence holds this long after it has fully arrived, so it can be read. | `skills/gt-motion/SKILL.md` | Review | [SVG lines in motion: timing numbers](https://www.prototemplate.com/docs/craft/svg-motion#timing-numbers) | | 40 to 90ms | Staggers run in reading direction at this spacing. | `skills/gt-motion/SKILL.md` | Review | [SVG lines in motion: timing numbers](https://www.prototemplate.com/docs/craft/svg-motion#timing-numbers) | | 0.62s with a 0.055s stagger, from 16px down | The GT landing reveals its content with this tween. | gt-cloud's `reveal.ts` | Review | [SVG lines in motion: timing numbers](https://www.prototemplate.com/docs/craft/svg-motion#timing-numbers) | | 180ms `ease-out` from 4px down | Rows on the dashboard's sign-in and onboarding plates reveal with this tween. | gt-cloud's `brand-tokens.css` | Review | [SVG lines in motion: timing numbers](https://www.prototemplate.com/docs/craft/svg-motion#timing-numbers) | | 120 to 220ms, 0 under reduced motion | The shell's chrome transitions run in this range, from a button's colour to the sidebar column. | `--pt-dur-fast` to `--pt-dur-sb` in `tokens.css` | Review | [SVG lines in motion: timing numbers](https://www.prototemplate.com/docs/craft/svg-motion#timing-numbers) | | 350ms and 150ms | A dithered resolve and a dithered step take these times, halved from 700 and 300ms on Kevin's request of 2026-09-29. | `RESOLVE_MS`, `STEP_MS` in `src/app/craft/TransitionDemo.tsx` | Review | [Dither in practice: transitions on one grid](https://www.prototemplate.com/docs/craft/dither#transitions-on-one-grid) | | 240ms `ease-out` | A field's first paint is a keyframe fade, because a CSS transition never runs on the first frame. | gt-cloud's `brand-field-picture-in` keyframe | Review | [SVG lines in motion: transitions at first paint](https://www.prototemplate.com/docs/craft/svg-motion#transitions-at-first-paint) | | 0.25s or more | An assembly moves one group at a time and rests this long between groups. | `skills/gt-motion/SKILL.md` | Review | [SVG lines in motion: timing numbers](https://www.prototemplate.com/docs/craft/svg-motion#timing-numbers) | | 0.7s `power2.inOut`, requests debounced 0.25s, none under 720px | The moving type law allows one width tween per cycle, and phones found it slow, so the em pins to the column there. | `skills/gt-motion/SKILL.md` | Review | [SVG lines in motion: moving type](https://www.prototemplate.com/docs/craft/svg-motion#moving-type) | | Scale 1.00 to 1.04 over a scene | A camera drift is allowed on a picture or a field, and type holds still. | `skills/gt-motion/SKILL.md` | Review | [SVG lines in motion: timing numbers](https://www.prototemplate.com/docs/craft/svg-motion#timing-numbers) | | 30 fps | The CPU dither loop runs at this rate by default, which keeps the main thread free. | `src/lib/dither.ts` | Review | [Dither and the studio field: what a field costs](https://www.prototemplate.com/docs/components/dither#what-a-field-costs) | ## SVG lines [#svg-lines] | Value | Reason | Token or constant | Held by | Explained in | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `'100 200'` parked at +101 or -101 | The gap keeps every dash edge clear of both ends, and the sign picks the end a draw-on grows from: + from the first point, - from the last. | The draw-on tweens; `skills/gt-motion/references/traps.md` | Review of 2x crops | [SVG lines in motion: the sign of the parked offset](https://www.prototemplate.com/docs/craft/svg-motion#the-sign-of-the-parked-offset) | | `pathLength` 1000 | GSAP integer-rounds pixel CSS properties, so a looping dash on 1000 units moves by sub-pixel steps. | `autoRound: false` on every dash tween | Review | [SVG lines in motion: rounding, cached paths and empty paths](https://www.prototemplate.com/docs/craft/svg-motion#rounding-cached-paths-and-empty-paths) | | About 100ms early | A capture synced to a GSAP state polls a CSS variable and fires early, so its own latency lands it inside the state. | The capture harness | Review | [SVG lines in motion: GSAP from outside the page](https://www.prototemplate.com/docs/craft/svg-motion#gsap-from-outside-the-page) | | 22 units for 20.1px | Labels in an 800-unit viewBox that renders at 0.914 are set after the scale. | `skills/gt-diagrams/SKILL.md` | `skills/gt-diagrams/scripts/figure-check.mjs` | [SVG lines in motion: drawing under a stretched viewBox](https://www.prototemplate.com/docs/craft/svg-motion#drawing-under-a-stretched-viewbox) | | `y1="70.5"` | A 1px line at 1:1 sits on a half-pixel coordinate to fill one row of device pixels. | `skills/gt-diagrams/SKILL.md` | Review of 2x crops | [SVG lines in motion: drawing under a stretched viewBox](https://www.prototemplate.com/docs/craft/svg-motion#drawing-under-a-stretched-viewbox) | | `-8 214 1213 771`, aspect about 1.573 | The GT monogram symbol carries this viewBox, so an svg that copies it draws the mark 214 units too high; the referencing svg takes only a width and height, such as 132 by 84. | `#gt-mark` in `deck/parts/head.html`; `src/lib/marks.ts` | Review | [SVG lines in motion: symbols and masks](https://www.prototemplate.com/docs/craft/svg-motion#symbols-and-masks) | ## Isometric drawings [#isometric-drawings] | Value | Reason | Token or constant | Held by | Explained in | | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------- | | 30 degrees, tan 30 (0.577) and the square root of 1.5 | Flat art reaches the iso plane by a 45 degree turn, y scaled by 0.577 and the whole scaled by the square root of 1.5. A 2:1 squash scales y by 0.5 and lies flatter than the face under it. | `project()` and `plane()` in `src/app/d/toolchain/diagrams/iso.ts` | Review | [Isometric drawings: numbers and traps](https://www.prototemplate.com/docs/components/iso#numbers-and-traps) | | 2.4, and 1.2 on diff slats | One corner radius serves the family; at 2.4 a thin slat reads as a capsule. | `ISO_RADIUS` in `iso.ts` | Review | [Isometric drawings: numbers and traps](https://www.prototemplate.com/docs/components/iso#numbers-and-traps) | | About 4% of the footprint thick, about 40% air between plates | Shared proportions make separate drawings read as one set. | `skills/gt-diagrams/references/isometric.md` | The gt-diagrams review checklist | [Isometric drawings: numbers and traps](https://www.prototemplate.com/docs/components/iso#numbers-and-traps) | | Rim 0.12 + 0.08 d, top edge 0.24 + 0.24 d, front edge 0.08 | Depth comes from each plate's stroke alpha, where d runs from 0 at the bottom plate to 1 at the top. | `skills/gt-diagrams/references/isometric.md` | Review | [Isometric drawings: numbers and traps](https://www.prototemplate.com/docs/components/iso#numbers-and-traps) | | 240 by 180 | The shared viewBox fixes the relative scale of the family. | `ISO_VIEW_W`, `ISO_VIEW_H` in `src/app/d/toolchain/diagrams/IsoFrame.tsx` | Review | [Isometric drawings: the kit](https://www.prototemplate.com/docs/components/iso#the-kit) | ## Dither [#dither] | Value | Reason | Token or constant | Held by | Explained in | | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------- | | 8 by 8 Bayer, 65 levels | A constant tone k/64 lights exactly k cells of every tile, and a cell is lit when its tone exceeds (m + 0.5) / 64. | `BAYER_8` in `src/lib/dither.ts` | Review | [Dither in practice: ordered dither](https://www.prototemplate.com/docs/craft/dither#ordered-dither) | | 1, 2, 3, 4 and 6 CSS px cells | The cell grows with the use: photographs, the sign-in globe, full-bleed fields and every film, an animated burst, then small stills. From 8px a screen reads as pixel art. | `scale` on the CPU loop; `skills/gt-dither/SKILL.md` | Review | [Dither in practice: cells are screen pixels](https://www.prototemplate.com/docs/craft/dither#cells-are-screen-pixels) | | `min(devicePixelRatio, 2)` | The GPU studio field computes cells from device pixels, and a dpr of 1 upscaled blurs them together. | `dpr` in `src/lib/studio-field.ts` | Review | [Dither in practice: cells are screen pixels](https://www.prototemplate.com/docs/craft/dither#cells-are-screen-pixels) | | Opacity 0.55 in screen blend, mask 0.72 at the edges to 0.12 at the centre | The landing hero's material is the strongest a product surface may carry. | Studio preset 02, `bayer-8x8`; `skills/gt-dither/SKILL.md` | Review | [Dither in practice: material strength](https://www.prototemplate.com/docs/craft/dither#material-strength) | | Mean tone 0.361, standard deviation 0.342 | Every dithered photograph is cut to the Blue Marble's own statistics, accepted from 0.30 to 0.42 and 0.28 to 0.44. | `scripts/media/mood-tone/standard.json` | `node scripts/lint/pictures.mjs` | [Dither in practice: the picture standard](https://www.prototemplate.com/docs/craft/dither#the-picture-standard) | | Gamma 1.2, 0.5% autocontrast, no blur | These settings are fixed for every picture, since a blur of sigma 3 erased the Rosetta Stone's inscriptions. | `scripts/media/mood-tone/standard.json` | `node scripts/lint/pictures.mjs` | [Artifact pictures: the tone grid](https://www.prototemplate.com/docs/handbook/artifact-pictures#3-the-tone-grid-is-the-blue-marbles) | | JPEG quality 82, 75 over 573,440 bytes | The byte cap keeps every grid under 560KB. | `scripts/media/mood-tone/standard.json` | `node scripts/lint/pictures.mjs` | [Dither in practice: the picture standard](https://www.prototemplate.com/docs/craft/dither#the-picture-standard) | | 144k cells at `scale` 3: 0.8 to 15.1ms a frame | The CPU loop's cost is linear in cells, and halving the cell size quadruples it, so a full-bleed field never animates at `scale` 1 (1.3 million calls). | The field functions in `src/lib/dither.ts` | Review | [Dither and the studio field: what a field costs](https://www.prototemplate.com/docs/components/dither#what-a-field-costs) | | A disc 1.7 times the stack's height, its limb five sevenths along the ramp | The onboarding earth sits under the thick part of the field's ramp, where the first placement left it under the thin start. | `src/components/plate/brand/moodPictures.ts` | Review | [Dither in practice: placing a picture on a field](https://www.prototemplate.com/docs/craft/dither#placing-a-picture-on-a-field) | ## Interface [#interface] | Value | Reason | Token or constant | Held by | Explained in | | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | 44px target, under 40px a defect | A phone tap target aims at 44px, and the page check fails one under 40px and notes 40 to 43px. | `TAP_DEFECT_UNDER`, `TAP_NOTE_UNDER` in `scripts/check/pagecheck/probes.mjs` | `pnpm check:pages` | [Interface details: tap targets that keep the look](https://www.prototemplate.com/docs/craft/interface-details#tap-targets-that-keep-the-look) | | 0.001 per layout-shift entry | An entry within 500ms of an input stays out of the CLS score and is still a jump a reader sees. | `SHIFT_FLOOR` in `scripts/check/pagecheck/cls.mjs` | `pnpm check:pages --cls-trace` | [Interface details: layout shifts](https://www.prototemplate.com/docs/craft/interface-details#layout-shifts) | | CLS over 0.1 fails, over 0.05 is a note | The first is the Web Vitals limit, and the second is a margin under it. | `CLS_DEFECT`, `CLS_NOTE` in `scripts/check/pagecheck/probes.mjs` | `pnpm check:pages` | [Proving a visual change: the stress matrix](https://www.prototemplate.com/docs/engineering/verification#the-stress-matrix) | | 4.5:1 for text under 24px | Titanium `#8a8f98` on white is 3.25, so light captions take `#6e737c` at 4.77 or ink-2. | `--pt-titanium`, `--pt-ink-2` in `tokens.css` | Review; `measure-type.mjs` | [Interface details: contrast](https://www.prototemplate.com/docs/craft/interface-details#contrast) | | About 1.6s | A copy control holds its check this long, announced to screen readers, and its label never changes. | `InstallField` in `src/components/viewer/InstallField.tsx` | Review | [Interface details: the copy control](https://www.prototemplate.com/docs/craft/interface-details#the-copy-control) | | 350ms and 4.5s | A carousel drops a command within 350ms of the last one, and autoplays every 4.5s until hover, focus or reduced motion stops it. | The blog carousel in gt-cloud; `skills/gt-website/references/blog.md` | Review | [Interface details: carousels and galleries](https://www.prototemplate.com/docs/craft/interface-details#carousels-and-galleries) | | 16px or larger | Inputs below the `md` breakpoint stay at this size so iOS does not zoom on focus. | `skills/gt-components/SKILL.md` | Review | [Interface details: third-party widgets and the site's theme](https://www.prototemplate.com/docs/craft/interface-details#third-party-widgets-and-the-sites-theme) | ## The deck [#the-deck] | Value | Reason | Token or constant | Held by | Explained in | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------ | -------------------------------------------------------- | --------------------------------------------------------------------------------- | | 1600 by 900 | Every slide is drawn on one sheet that scales to the window. | `deck/DECK-GRAMMAR.md` | `node shoot-slide.mjs` prints any element past the sheet | [The brand deck: the sheet and the grammar](https://www.prototemplate.com/docs/deck#the-sheet-and-the-grammar) | | 56px | The viewer draws the rails and rules this far in from each edge, and a slide never redraws them. | `deck/DECK-GRAMMAR.md` | Review | [The brand deck: the sheet and the grammar](https://www.prototemplate.com/docs/deck#the-sheet-and-the-grammar) | | h1 88px, h2 44px, `.big` 72px, `p` 22px, `.lead` 26px | The deck sets its type at these sizes on the sheet. | The shared styles in `deck/parts/` | Review | [The brand deck: the sheet and the grammar](https://www.prototemplate.com/docs/deck#the-sheet-and-the-grammar) | | 15px for text, 18px for SVG labels | Text under 15px on the sheet is a defect, and an SVG label is never under 18px. | `.cap` and `.sm` in `deck/DECK-GRAMMAR.md` | Review | [Writing: slides and cards](https://www.prototemplate.com/docs/craft/writing#slides-and-cards) | | 256px or 208px | The deck viewer's slide list takes these widths with thumbnails and in outline density. | `--sb-w` in `deck/parts/head.html` | Review | [The brand deck: the viewer](https://www.prototemplate.com/docs/deck#the-viewer) | ## Graphics [#graphics] | Value | Reason | Token or constant | Held by | Explained in | | --------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------ | --------------------- | ------------------------------------------------------------------------------- | | 3 to 5x | Product captures are taken at this scale, lossless, so crops stay sharp after zoom. | `SHOTS` in `graphics/build/gen-lib.js`; `graphics/shots/hi/` | Review | [Graphics pipeline: the files](https://www.prototemplate.com/docs/handbook/graphics#the-files) | | 26px after zoom-to-fit | No text on the 1600px stage is smaller than a label chip once the composition is fitted. | `MIN_TEXT` in `graphics/build/gen-lib.js` | `pnpm graphics:audit` | [Graphics pipeline: sizing](https://www.prototemplate.com/docs/handbook/graphics#sizing) | | About 700 CSS px, 0.44x | A blog column shows the 1600px stage at this scale, so every label is sized for it. | The 1600px `stage` in `graphics/build/gen-lib.js` | `pnpm graphics:audit` | [Graphics pipeline: sizing](https://www.prototemplate.com/docs/handbook/graphics#sizing) | | 3840 wide webp at quality 95; OG 2400 by 1260 | The exports serve a dense `deviceSizes` ladder through next/image. | `graphics/build/export-blog.py` | Review | [Graphics pipeline: sizing](https://www.prototemplate.com/docs/handbook/graphics#sizing) | | 712px | The blog globe figure is laid out at this review column before it scales. | `src/components/blog-globe/blog-globe.css` | Review | [The blog globe figure: measurements](https://www.prototemplate.com/docs/components/blog-globe#measurements) | ## Films [#films] | Value | Reason | Token or constant | Held by | Explained in | | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------- | | -16 LUFS within 0.5, true peak at or under -1 dBTP | A film meets these loudness targets, and the renderer lowers a track whose true peak passes -1 dBTP to -1.5 dBTP. | `skills/gt-films/references/sound.md` | `ffmpeg -af ebur128=peak=true` | [Films: the mix and its traps](https://www.prototemplate.com/docs/craft/films#the-mix-and-its-traps) | | About -26 LUFS under speech, -20 LUFS alone | The music bed's duck is about 6dB in loudness terms. | `skills/gt-films/references/sound.md` | Review | [Films: the mix and its traps](https://www.prototemplate.com/docs/craft/films#the-mix-and-its-traps) | | 0.1s of silence; a -9dB shelf at 100Hz | A clip at full level from its first sample bumps, and sub-bass downbeats made the AAC encoder overshoot by up to 2.6dB. | `skills/gt-films/references/sound.md` | Review | [Films: the mix and its traps](https://www.prototemplate.com/docs/craft/films#the-mix-and-its-traps) | | 2.60 words a second | The narrator speaks at this rate, scripts are planned on it, and a take is never sped up. | `skills/gt-films/references/sound.md` | Review | [Films: speech synthesis](https://www.prototemplate.com/docs/craft/films#speech-synthesis) | | 640 by 360 | A film plays at this size on a phone, so every thin line is checked there. | `skills/gt-films/SKILL.md` | The film critic | [Films: thin lines over a dithered ground](https://www.prototemplate.com/docs/craft/films#thin-lines-over-a-dithered-ground) | | 3px cells, 8 by 8 Bayer | Every GT film uses one screen, set on 2026-10-08. | `skills/gt-films/SKILL.md` | The film critic | [Dither in practice: cells are screen pixels](https://www.prototemplate.com/docs/craft/dither#cells-are-screen-pixels) | | 5 GiB free, 8 GiB for several workers | A render with several workers writes every frame before it encodes. | `skills/gt-films/references/kit.md` | Review | [Films: frames, probes and disks](https://www.prototemplate.com/docs/craft/films#frames-probes-and-disks) | | The stakes in 6 to 8s; about two picture events a second | A film states its stakes early and moves one thing at a time. | `skills/gt-films/SKILL.md` | The film critic | [Films: pacing and scripts](https://www.prototemplate.com/docs/craft/films#pacing-and-scripts) | ## Skills [#skills] | Value | Reason | Token or constant | Held by | Explained in | | --------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------- | ------------------------------------------------- | | 1024 characters | A skill's description says what it covers and when to use it within the length agents load. | `MAX_DESCRIPTION` in `scripts/build/skills.mjs` | `pnpm lint:skills` | [Skills: the contract](https://www.prototemplate.com/docs/skills#the-contract) | | 24,000 bytes | A SKILL.md body stays under this budget, and detail moves to `references/` without deleting a rule. | `BUDGET` in `scripts/build/skills.mjs`; exemptions with reasons and expiry dates in `scripts/build/skills.budget.json` | `pnpm lint:skills` | [Skills: the contract](https://www.prototemplate.com/docs/skills#the-contract) | | 30 days | A skill with no loads thirty days after distribution is cut unless Kevin keeps it by name. | `DAYS` in `scripts/skills/usage.mjs` | `pnpm skills:usage` | [Skills: kept by use](https://www.prototemplate.com/docs/skills#kept-by-use) | ## Docs and SEO [#docs-and-seo] | Value | Reason | Token or constant | Held by | Explained in | | -------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | 50 to 160 characters | A description of this length shows whole in a search snippet. | `DESCRIPTION_MIN`, `DESCRIPTION_MAX` in `scripts/lint/seo.mjs` | `pnpm lint:seo`; `pnpm lint:registries` | [The SEO system: what one call writes](https://www.prototemplate.com/docs/seo#what-one-call-writes) | | 60 characters | A title drops its section, then the site name, when it passes this length. | `TITLE_MAX` in `src/lib/seo/route.ts` | `pnpm lint:seo` | [The SEO system: what one call writes](https://www.prototemplate.com/docs/seo#what-one-call-writes) | | 1200 by 630 | The social card is cached for a year, because it is a pure function of its query. | `src/app/og/route.tsx`; `OG_VERSION` | `pnpm lint:seo:live` | [The SEO system: the social card](https://www.prototemplate.com/docs/seo#the-social-card) | | Two segments under `/docs` | The Markdown twin rewrites cover docs URLs this deep, so a deeper folder needs a third rule. | `TWIN_REWRITES` in `next.config.ts` | `pnpm lint:seo:live` | [The SEO system: why the twins are prerendered](https://www.prototemplate.com/docs/seo#why-the-twins-are-prerendered) | | `tokenizeTimeLimit: 0` | Shiki's default 500ms limit leaves part of a line plain on a loaded build machine, so two builds differ. | `source.config.ts` | Review of a production build | [Docs site traps: code highlighting across builds](https://www.prototemplate.com/docs/engineering/docs-traps#code-highlighting-across-builds) | | `next` 16.2.12 | Next.js 16.3 holds memory for every prerendered page, and a 7,117-page build died at page 1,779. | `package.json` | Review | [Docs site traps: prerender memory on Next.js 16.3](https://www.prototemplate.com/docs/engineering/docs-traps#prerender-memory-on-nextjs-163) | ## Engines [#engines] | Value | Reason | Token or constant | Held by | Explained in | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | About 16 live WebGL contexts | Past this budget Chrome's `getContext` returns null and a field draws black with no error. | One engine per module in `src/lib/studio-field.ts`, `horizon-field.ts` and `prismatic-field.ts` | Review; the frame probe's `gl` column | [Canvas and WebGL engines: the context budget](https://www.prototemplate.com/docs/engineering/engines#the-context-budget) | | 1 context per engine module, 0 created by a probe | The context survives every `destroy()`, and a probe that calls `getContext` exhausts the budget it counts. | The same modules | Review | [The component library: the numbers behind it](https://www.prototemplate.com/docs/components#the-numbers-behind-it) | | 180px | A lazy plate mounts its engine when it first comes this close to the viewport. | `rootMargin: '180px'` in `src/components/docs/demos/hooks.ts` | Review | [The component library: the numbers behind it](https://www.prototemplate.com/docs/components#the-numbers-behind-it) | | 2.5s warmup, windows of 120 frames, gaps over 900ms skipped | The glyph rain judges its quality tier from measured frame time, because a tier chosen by window width gave weak hardware the heaviest one. | `GOV_WARMUP_MS`, `GOV_WINDOW`, `GOV_GAP_MS` in `src/lib/glyph-field.ts` | The frame probe's `--cpu 12` | [Canvas and WebGL engines: quality from measured frame time](https://www.prototemplate.com/docs/engineering/engines#quality-from-measured-frame-time) | | 35% of frames over 22ms | The trip counts slow frames, because a median missed throttled hardware that ran over budget on 34% of its frames. | `GOV_TRIP`, `GOV_BUDGET_MS` in `src/lib/glyph-field.ts` | The frame probe's `--cpu 12` | [Canvas and WebGL engines: quality from measured frame time](https://www.prototemplate.com/docs/engineering/engines#quality-from-measured-frame-time) | | Pixel-ratio cap 2 to 1.25, then 560 of 1,280 glyphs | The governor's two tiers only step down, so it never oscillates. | `DPR_CAP`, `DPR_CAP_NARROW`, `NARROW_POOL`, `POOL` in `src/lib/glyph-field.ts` | Review | [Canvas and WebGL engines: quality from measured frame time](https://www.prototemplate.com/docs/engineering/engines#quality-from-measured-frame-time) | | About 1,000ms of every second | An engine that resized its one GL canvas to each field reallocated it twice a frame, so the canvas now only grows. | The blit in `src/lib/prismatic-field.ts` | Review | [Canvas and WebGL engines: a shared canvas that only grows](https://www.prototemplate.com/docs/engineering/engines#a-shared-canvas-that-only-grows) | | 12 live pages, 1.3 GB and about 950ms of every second | A gallery that froze each preview still paid this, so previews are stills until hover. | `src/app/present/viewer/LazyFrame.tsx`; the rAF gate in `src/app/layout.tsx` | Review | [Canvas and WebGL engines: galleries of live previews](https://www.prototemplate.com/docs/engineering/engines#galleries-of-live-previews) | | 6.5KB to 4.4KB | Comments inside a GLSL template literal ship to every visitor, so they move to a TypeScript comment. | The horizon shader in `src/lib/horizon-field.ts` | Pixel diff of a deterministic frame | [Canvas and WebGL engines: shader source weight](https://www.prototemplate.com/docs/engineering/engines#shader-source-weight) | | 5 fps against 56 fps | A seam drag ran at these rates over 27,800 SVG nodes and over one canvas, at 4x CPU throttle. | `--seam-cut` on the reveal seam | Review | [Lottie on canvas: before and after](https://www.prototemplate.com/docs/engineering/lottie#before-and-after) | ## Gates [#gates] | Value | Reason | Token or constant | Held by | Explained in | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------ | | 0, 1 and 2 | Every lint exits 0 on a pass, 1 on findings and 2 when it could not judge, so a missing server stops a chain as a finding does. | Each script under `scripts/lint/` | Every gate | [Gates: exit codes](https://www.prototemplate.com/docs/gates#exit-codes) | | 3 to 5 minutes | `pnpm exec tsc --noEmit` is a gate of its own, because `next build` never type-checks here. | `typescript.ignoreBuildErrors` in `next.config.ts` | The type check | [Gates: the type check](https://www.prototemplate.com/docs/gates#the-type-check) | | About 25 minutes | `pnpm lint:all` runs every static lint, then the browser audits one at a time. | `package.json`; `skills/prototemplate/SKILL.md` | `pnpm lint:all` | [Gates: the gates, in order](https://www.prototemplate.com/docs/gates#the-gates-in-order) | | One browser gate at a time | A parallel capture made `pnpm lint:lines:shell` fail on correct pages on 2026-10-01. | `siteRoutes()` in `scripts/lib/site-pages.mjs` | Every browser gate | [Gates: browser gates run one at a time](https://www.prototemplate.com/docs/gates#browser-gates-run-one-at-a-time) | | Per file | A ratchet counts findings per file, so a line another session moves never reads as a new finding. | `scripts/lint/*.baseline.json` | `--update-baseline` after a cleanup | [Gates: ratchets](https://www.prototemplate.com/docs/gates#ratchets) | | HTTP 400 or more exits 2 | A 404 page audits at about one line and would pass. | `scripts/lint/lines.mjs` | `pnpm lint:lines` | [Lines and seams: the line auditor's thresholds](https://www.prototemplate.com/docs/craft/lines#the-line-auditors-thresholds) | | Five rows read by regex | The route list finds the first skill, motion package, archive entry and exploration and the newest post in data files, so a generator's output shape is load-bearing. | `firstSlug()` in `scripts/lib/site-pages.mjs` | `pnpm lint:registries` check 3 | [Scripts: first slugs read by regex](https://www.prototemplate.com/docs/scripts#first-slugs-read-by-regex) | ## Dev servers and the machine [#dev-servers-and-the-machine] | Value | Reason | Token or constant | Held by | Explained in | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------- | | Port 3005 | The shared dev server runs here, and every other worktree runs its own server on its own port. | `pnpm dev`; `PT_BASE` in the browser gates | Review | [Get started: run the site](https://www.prototemplate.com/docs/get-started#run-the-site) | | 64 GB | A long-running shared server's `.next/dev` grew this large, so the restart deletes it. | `.next/dev` | Review | [Dev servers and builds: failure modes of next dev](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) | | 900s or more | A cold image request on a loaded dev server hangs this long and times out a page check. | The image optimizer of `next dev` | Review | [Dev servers and builds: failure modes of next dev](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) | | 2,880 minutes | gt-cloud's pnpm refuses any version published in the last two days, so `pnpm add` can fail on an unrelated dependency. | `minimumReleaseAge` in gt-cloud's `pnpm-workspace.yaml` | pnpm itself | [Dev servers and builds: a minimum release age for dependencies](https://www.prototemplate.com/docs/engineering/dev-servers#a-minimum-release-age-for-dependencies) | | Two hours, and about 90 minutes | Claude Code stops a background shell task at two hours, so a job that may run past 90 minutes starts detached and writes its own process id. | `skills/gt-orchestration/references/harness-traps.md` | Review | [Agents on one machine: long commands](https://www.prototemplate.com/docs/handbook/shared-machine#long-commands) | | 128GB | The workstation ran out of application memory twice on 2026-10-12, so each heavy job is gated on its own memory estimate. | `headroom can-start --gb ` | Review | [Agents on one machine: memory and disk](https://www.prototemplate.com/docs/handbook/shared-machine#memory-and-disk) | ## Pull requests and workflows [#pull-requests-and-workflows] | Value | Reason | Token or constant | Held by | Explained in | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------- | | 420 to 900 CSS px wide, 620px tall at most, 24px of padding | A before and after crop is readable in a pull request body, where full-page captures render as strips. | `skills/gt-ship/references/pr-body.md` | Review | [Pull requests and CI: size and shape](https://www.prototemplate.com/docs/handbook/pull-requests#size-and-shape) | | 5/5 on the head commit | A gt-cloud pull request waits for this Greptile score, with every bot thread answered and resolved. | `skills/gt-ship/references/review-loop.md` | Review | [Pull requests and CI: review bots](https://www.prototemplate.com/docs/handbook/pull-requests#review-bots) | | 4096MB | The dashboard's type check outgrew Node's default 2GB heap in CI. | `--max-old-space-size` in gt-cloud's build | CI | [Pull requests and CI: CI signals that mislead](https://www.prototemplate.com/docs/handbook/pull-requests#ci-signals-that-mislead) | | About 1,800 words in, about 700 words out | A build fan-out starts from a spec of this size, and a build lane reports in this many words. | `skills/gt-orchestration/references/briefs.md` | Review | [Agent workflows: workflow shapes](https://www.prototemplate.com/docs/handbook/workflows#workflow-shapes) | | Three refuters, two of three to stand | A critical or major finding stands only when at least two refuters fail to refute it. | `skills/gt-orchestration/SKILL.md` | Review | [Agent workflows: refuting findings](https://www.prototemplate.com/docs/handbook/workflows#refuting-findings) | | 8.5 of 10 per dimension, 90 of 100 overall | A fresh critic scores quality work against a named reference, in every view. | `skills/gt-orchestration/references/convergence.md` | The critic | [Agent workflows: kinds of check](https://www.prototemplate.com/docs/handbook/workflows#kinds-of-check) | | 35 of 50 | A draft whose humanizer score on five axes totals less goes back for another revision. | `skills/gt-voice/SKILL.md` | Review | [Writing: the humanizer pass](https://www.prototemplate.com/docs/craft/writing#the-humanizer-pass) | ## Sources [#sources] * Prototemplate, read on 2026-10-21: `src/components/viewer/tokens.css`; `DESIGN.md` sections 2, 4 and 15; `deck/DECK-GRAMMAR.md`; `docs/GRAPHICS.md`; `scripts/lint/type.mjs`, `radius.mjs`, `lines.mjs`, `heads.mjs` and `seo.mjs`; `scripts/check/pagecheck/probes.mjs` and `cls.mjs`; `scripts/build/skills.mjs` and `scripts/skills/usage.mjs`; `src/lib/glyph-field.ts`, `studio-field.ts`, `prismatic-field.ts`, `dither.ts`, `fonts.ts` and `seo/route.ts`; `src/components/docs/demos/hooks.ts`; `src/app/craft/TransitionDemo.tsx`; `src/app/d/toolchain/diagrams/iso.ts` and `IsoFrame.tsx`; `graphics/build/gen-lib.js`; `scripts/media/mood-tone/standard.json`; `package.json`; the files in `public/fonts/`. * The curated skills, read on 2026-10-21: `gt-brand` (`references/type.md`), `gt-motion` (section 2 and `references/traps.md`), `gt-diagrams` (`references/isometric.md`), `gt-dither` (`references/engines.md`, `references/pictures.md`), `gt-films` (`references/sound.md`, `references/kit.md`), `gt-performance` (`references/webgl-lifecycles.md`), `gt-website`, `gt-ship`, `gt-orchestration`, `gt-voice`, `gt-aesthetic` and `prototemplate`. * The docs pages each row links to, which hold the measurements with their dates. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Traps and fixes > One line per trap met in GT work, grouped by area: the symptom a reader sees, the fix in a few words and the section that explains the cause. URL: https://www.prototemplate.com/docs/handbook/traps Each row below is a failure met in General Translation (GT) work on Prototemplate, generaltranslation.com and the films between July and October 2026. A row gives the symptom a reader sees first, then the fix in a few words, and links the section that explains the cause, the proof and the date. When something breaks, find its symptom here; before a round of work in an area, read that area's rows. [Numbers](https://www.prototemplate.com/docs/handbook/numbers) indexes the values the fixes depend on in the same way. The cause and the measurement live on the linked page, so a row changes when its section does. Paths are relative to the root of the Prototemplate repository. ## Dev servers [#dev-servers] | Symptom | Fix | Explained in | | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | A second `next dev` for the same checkout refuses to start | One server per checkout; another worktree runs its own server on its own port | [Dev servers and builds: failure modes of next dev](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) | | Every route answers 404 after a restart | Two servers shared one `.next`: stop both by PID, `rm -rf .next`, start one | [Dev servers and builds: failure modes of next dev](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) | | Every route answers 500 with "Unexpected non-whitespace character after JSON" | Stop the server by PID, delete `.next/dev`, start it again | [Dev servers and builds: failure modes of next dev](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) | | The server answers nothing after a large fast-forward | The same restart | [Dev servers and builds: failure modes of next dev](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) | | The disk fills while a shared server runs | Delete `.next/dev` at the restart | [Dev servers and builds: failure modes of next dev](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) | | A changed image still shows its old version | `rm -rf .next` and restart | [Dev servers and builds: failure modes of next dev](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) | | A cold image request hangs for 900s and times out a page check | Retry once the machine's load drops | [Dev servers and builds: failure modes of next dev](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) | | `next start` serves the old build after a rebuild reported success | Delete `.next` in the scratch tree before diagnosing | [Dev servers and builds: failure modes of next dev](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) | | Every docs page answers 404 after a merge, with nothing in the log | `pnpm install --frozen-lockfile` and a restart | [Dev servers and builds: failure modes of next dev](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) | | `Module not found` for a workspace package whose sources changed | Build the package, which resolves from a stale `dist/` | [Dev servers and builds: failure modes of next dev](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) | | A pending navigation lasts longer than in production | Judge navigation timing on a production build | [Dev servers and builds: failure modes of next dev](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) | | A route answers 200 and its body is the error overlay | Call a server ready only when a real page's body has no build error | [Dev servers and builds: failure modes of next dev](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) | | A hydration mismatch appears at random under `next dev` | Judge it on a production build before chasing it | [Localized routing: links, routers and strings in gt-next](https://www.prototemplate.com/docs/engineering/i18n-routing#links-routers-and-strings-in-gt-next) | | `pnpm add` fails on a dependency the change never touched | Add the one dependency by hand under the minimum release age | [Dev servers and builds: a minimum release age for dependencies](https://www.prototemplate.com/docs/engineering/dev-servers#a-minimum-release-age-for-dependencies) | | "Can't resolve 'gt-next/link'" after a relink | Restart the dev server that ran during it | [Dev servers and builds: a minimum release age for dependencies](https://www.prototemplate.com/docs/engineering/dev-servers#a-minimum-release-age-for-dependencies) | ## Gates and tooling [#gates-and-tooling] | Symptom | Fix | Explained in | | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | A failing build reached main through `pnpm build 2>&1 \| tail -2 && git push` | Capture output to a file and chain on the real exit status with `&&` | [Agents on one machine: exit-status gates](https://www.prototemplate.com/docs/handbook/shared-machine#exit-status-gates) | | `next build` passes with type errors | It never type-checks here; run `pnpm exec tsc --noEmit` as its own gate | [Gates: the type check](https://www.prototemplate.com/docs/gates#the-type-check) | | `tsc` reports errors in `.next/dev/types` that no change touched | Run the type check in the scratch worktree that runs the build | [Gates: the type check](https://www.prototemplate.com/docs/gates#the-type-check) | | `pnpm lint:lines:shell` fails on pages that are correct | Run browser gates one at a time, then rerun the failing one alone | [Gates: browser gates run one at a time](https://www.prototemplate.com/docs/gates#browser-gates-run-one-at-a-time) | | The line audit exits 2 on its first route | Load the route by hand; restart the server only when your session owns it | [Gates: before trusting a red browser gate](https://www.prototemplate.com/docs/gates#before-trusting-a-red-browser-gate) | | A lint exits clean under `--report` | Read the findings; `--report` drops exits 1 and 2 | [Gates: exit codes](https://www.prototemplate.com/docs/gates#exit-codes) | | A new violation passes its ratchet | Lower the baseline in the cleanup commit and read the JSON diff | [Gates: ratchets](https://www.prototemplate.com/docs/gates#ratchets) | | A missing page or a list of URLs audits clean | A status of 400 or more and a URL with whitespace exit 2; pass lists as arrays | [Lines and seams: the line auditor's thresholds](https://www.prototemplate.com/docs/craft/lines#the-line-auditors-thresholds) | | Every browser gate breaks at once after a generator change | Keep the single quotes and field names the route list's regexes read | [Scripts: first slugs read by regex](https://www.prototemplate.com/docs/scripts#first-slugs-read-by-regex) | | A lint at rest misses the search dialog and the scroll thumb | Open the dialog and hover the list before collecting | [Proving a visual change: states that mount on demand](https://www.prototemplate.com/docs/engineering/verification#states-that-mount-on-demand) | | `pnpm doctor` runs something other than the setup check | Write `pnpm run doctor`; the bare form is pnpm's own command | [Get started: run the site](https://www.prototemplate.com/docs/get-started#run-the-site) | | A skill install replaces a project's own skill with no report | The installer refuses foreign entries; check a new slug against every skills folder | [Skills: slug collisions](https://www.prototemplate.com/docs/skills#slug-collisions) | ## Builds and deploys [#builds-and-deploys] | Symptom | Fix | Explained in | | ------------------------------------------------------------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Docs HTML grows from 245KB to 731KB | Revert `experimental.inlineCss`, which embeds the entry CSS twice | [Docs site traps: inline CSS, built and reverted](https://www.prototemplate.com/docs/engineering/docs-traps#inline-css-built-and-reverted) | | The build worker is killed partway through prerendering | Pin `next` 16.2.12 | [Docs site traps: prerender memory on Next.js 16.3](https://www.prototemplate.com/docs/engineering/docs-traps#prerender-memory-on-nextjs-163) | | Every deploy fails with `TS6306` on a referenced package | Reference only the packages the app imports | [Dev servers and builds: filtered installs and project references](https://www.prototemplate.com/docs/engineering/dev-servers#filtered-installs-and-project-references) | | CI fails on content a submodule never checked out | Guard the read itself and degrade the feature | [Dev servers and builds: CI without the content submodule](https://www.prototemplate.com/docs/engineering/dev-servers#ci-without-the-content-submodule) | | A live count renders on localhost and is empty in production | A static minimum label and a cached same-origin route for the value | [Canvas and WebGL engines: static pages with live values](https://www.prototemplate.com/docs/engineering/engines#static-pages-with-live-values) | | A merged change does not show on the site | Read the `dpl_` stamp on the production host against the newest Ready build | [Proving a visual change: whether it is live](https://www.prototemplate.com/docs/engineering/verification#whether-it-is-live) | | Builds fail at random in the Google font loader | Self-host the faces | [Proving a visual change: whether it is live](https://www.prototemplate.com/docs/engineering/verification#whether-it-is-live) | | A push that compiles cleanly fails its hosted build | Keep request-scoped server calls inside the server function's handler | [Reviewing with Kevin: ship states](https://www.prototemplate.com/docs/handbook/review#ship-states) | ## SEO and agent files [#seo-and-agent-files] | Symptom | Fix | Explained in | | --------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | Every page names the home page as its canonical | Write each route's whole head with `seoRoute()`; the root layout sets no canonical | [The SEO system: the lint](https://www.prototemplate.com/docs/seo#the-lint) | | A route's partial `openGraph` drops the parent's fields | `seoRoute()` writes every field every time | [The SEO system: what one call writes](https://www.prototemplate.com/docs/seo#what-one-call-writes) | | A Markdown twin answers 200 locally and 500 in production | Prerender every twin with `dynamicParams = false` | [Docs site traps: Markdown twins in production](https://www.prototemplate.com/docs/engineering/docs-traps#markdown-twins-in-production) | | A `.md` URL never reaches the middleware | Put the twin rules in `next.config.ts` rewrites or the route | [The SEO system: why the twins are prerendered](https://www.prototemplate.com/docs/seo#why-the-twins-are-prerendered) | | Agent-readability checkers find no links in a closed menu | A machine-only `sr-only` anchor in the server-rendered body | [The SEO system: why the twins are prerendered](https://www.prototemplate.com/docs/seo#why-the-twins-are-prerendered) | | A disallowed URL is still indexed | Put the policy in `noindex` and leave robots.txt open | [The SEO system: the policy](https://www.prototemplate.com/docs/seo#the-policy) | ## Docs adapters [#docs-adapters] | Symptom | Fix | Explained in | | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | A click on a docs folder paints the layout with no article for about 280ms | Link to leaf pages; redirect folders in `next.config.ts` | [Docs site traps: links that point at leaf pages](https://www.prototemplate.com/docs/engineering/docs-traps#links-that-point-at-leaf-pages) | | The sidebar's rail and current-row tint pop in at hydration | Show only the row tint before `data-sb-ready` | [Docs site traps: marks that exist at first paint](https://www.prototemplate.com/docs/engineering/docs-traps#marks-that-exist-at-first-paint) | | The sidebar's rail runs past the remaining rows while a folder folds | Read whether a folder is open from its collapsible | [Docs site traps: the sidebar after fumadocs 16.16](https://www.prototemplate.com/docs/engineering/docs-traps#the-sidebar-after-fumadocs-1616) | | The pill and thumb stay on a row that is folding away | Treat an active link inside a closed collapsible as not current | [Docs site traps: the sidebar after fumadocs 16.16](https://www.prototemplate.com/docs/engineering/docs-traps#the-sidebar-after-fumadocs-1616) | | The sidebar list jumps when a folder reopens | Replace the viewport's `scroll()` and write `scrollTop` | [Docs site traps: the sidebar after fumadocs 16.16](https://www.prototemplate.com/docs/engineering/docs-traps#the-sidebar-after-fumadocs-1616) | | The sidebar list scrolls when a folder opens near its end | `overflow-anchor: none` on the viewport | [Docs site traps: the sidebar after fumadocs 16.16](https://www.prototemplate.com/docs/engineering/docs-traps#the-sidebar-after-fumadocs-1616) | | The sticky sidebar lifts at the end of the page | `margin-bottom: -1px` on its grid column | [Docs site traps: the sidebar after fumadocs 16.16](https://www.prototemplate.com/docs/engineering/docs-traps#the-sidebar-after-fumadocs-1616) | | Two builds of one page ship different code colours | `tokenizeTimeLimit: 0` wherever Shiki runs | [Docs site traps: code highlighting across builds](https://www.prototemplate.com/docs/engineering/docs-traps#code-highlighting-across-builds) | | An MDX component renders with its props undefined and no build error | Components that take string attributes and child elements | [Docs site traps: MDX that drops props](https://www.prototemplate.com/docs/engineering/docs-traps#mdx-that-drops-props) | | Every docs element computes `font-feature-settings: normal` | Type the docs root as a class in the token base rules | [Docs site traps: features that compute normal](https://www.prototemplate.com/docs/engineering/docs-traps#features-that-compute-normal) | | A token set in the docs sheet's `@theme` reaches other routes | Scope docs rules to docs-only ids or `:root:has(#nd-notebook-layout)` | [Docs site traps: features that compute normal](https://www.prototemplate.com/docs/engineering/docs-traps#features-that-compute-normal) | | The docs title renders at 28px, off the display ladder | Put the title, lead and prose on the ladder in `docs.css` | [Docs site traps: what fumadocs leaves for the lints](https://www.prototemplate.com/docs/engineering/docs-traps#what-fumadocs-leaves-for-the-lints) | | Docs headings render at 600 and 700 | One weight cap at 500 in `docs.css` | [Use these docs as a template: the adapters](https://www.prototemplate.com/docs/template#the-adapters) | | fumadocs' dark mode ignores `html[data-theme]` | `theme={{ enabled: false }}` and a `dark` variant on `:root[data-theme='dark']` | [Use these docs as a template: the adapters](https://www.prototemplate.com/docs/template#the-adapters) | | Tailwind's theme layer resets the font tokens for the whole document | Point `--font-sans` and `--font-mono` at the site's type tokens in `@theme` | [Use these docs as a template: the adapters](https://www.prototemplate.com/docs/template#the-adapters) | | A prose override restyles the table of contents | Scope it to `#nd-page .prose` | [Docs site traps: markup that hides from a lint](https://www.prototemplate.com/docs/engineering/docs-traps#markup-that-hides-from-a-lint) | | The search dialog and the phone drawer keep shadows and blur | Name the portaled dialog, its overlay and the drawer in the flat rule | [Interface details: shadows and blur from a component library](https://www.prototemplate.com/docs/craft/interface-details#shadows-and-blur-from-a-component-library) | | A script looks for `/docs/(internals)/gates` | Drop every `(name)` segment when a path becomes a route | [Use these docs as a template: adding a page](https://www.prototemplate.com/docs/template#adding-a-page) | | A plate appears in the Markdown twin as a bare tag | A sentence beside every plate says what it shows | [Use these docs as a template: adding a page](https://www.prototemplate.com/docs/template#adding-a-page) | ## CSS and Tailwind [#css-and-tailwind] | Symptom | Fix | Explained in | | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `font-semibold`, `tracking-*` and `mt-6` do nothing on headings | Metrics in the section's own stylesheet, gaps on the parent | [Docs site traps: unlayered rules and layered utilities](https://www.prototemplate.com/docs/engineering/docs-traps#unlayered-rules-and-layered-utilities) | | One ID in a list of root classes overrides component rules site-wide | Join an `:is()` list as a class | [Docs site traps: specificity of an is() list](https://www.prototemplate.com/docs/engineering/docs-traps#specificity-of-an-is-list) | | `text-sm` beats `.prose` on a table-of-contents row | Scope prose rules under `#nd-page`, which breaks the tie with the utilities layer | [Docs site traps: the typography plugin in the utilities layer](https://www.prototemplate.com/docs/engineering/docs-traps#the-typography-plugin-in-the-utilities-layer) | | A corner lint reports 33,554,400px | `rounded-full` is `calc(infinity * 1px)`; a square box takes `50%` | [Docs site traps: the full round corner](https://www.prototemplate.com/docs/engineering/docs-traps#the-full-round-corner) | | A token read only by inline styles is missing from the built CSS | Declare it in `@theme static` | [Docs site traps: Tailwind 4 build traps](https://www.prototemplate.com/docs/engineering/docs-traps#tailwind-4-build-traps) | | The CSS build breaks after an edit to a comment | Fix the bracketed utility in the comment, then `rm -rf .next` | [Docs site traps: Tailwind 4 build traps](https://www.prototemplate.com/docs/engineering/docs-traps#tailwind-4-build-traps) | | Classes added to a workspace package never compile | `touch` the app's `globals.css` | [Docs site traps: Tailwind 4 build traps](https://www.prototemplate.com/docs/engineering/docs-traps#tailwind-4-build-traps) | | `text-[var(--x)]` becomes a colour | `text-[length:var(--x)]` | [Docs site traps: Tailwind 4 build traps](https://www.prototemplate.com/docs/engineering/docs-traps#tailwind-4-build-traps) | | A rule removed with `border-image: none` still paints | `border-image-source: none` | [Lines and seams: rules that run the full width](https://www.prototemplate.com/docs/craft/lines#rules-that-run-the-full-width) | | A hover-scale reset in CSS does nothing | Reset the scale in the markup, because Lightning CSS folds `scale` into `transform` | [Lines and seams: rules that run the full width](https://www.prototemplate.com/docs/craft/lines#rules-that-run-the-full-width) | | Margin utilities do nothing on a ported page | Put margins in the port's compatibility sheet | [The design lab and the directions: the port recipe](https://www.prototemplate.com/docs/directions#the-port-recipe) | ## WebGL and canvas [#webgl-and-canvas] | Symptom | Fix | Explained in | | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | A field draws black after a dozen client-side navigations, with no error | One engine and one context per engine module for the session | [Canvas and WebGL engines: the context budget](https://www.prototemplate.com/docs/engineering/engines#the-context-budget) | | Fields go black while a probe counts contexts | Wrap `getContext` before the page's scripts run | [Canvas and WebGL engines: the context budget](https://www.prototemplate.com/docs/engineering/engines#the-context-budget) | | A mark leaks one WebGL context per mount | Delete its buffers and shaders, then `loseContext()`, on unmount | [The component library: the numbers behind it](https://www.prototemplate.com/docs/components#the-numbers-behind-it) | | Two fields of different sizes keep the main thread busy | Grow the shared canvas to the largest field and blit from its bottom-left corner | [Canvas and WebGL engines: a shared canvas that only grows](https://www.prototemplate.com/docs/engineering/engines#a-shared-canvas-that-only-grows) | | The glyph rain lags on phones, worst on WebKit | Snapshot the readback atlas into an `ImageBitmap` for the blits | [Canvas and WebGL engines: readback canvases and the blit path](https://www.prototemplate.com/docs/engineering/engines#readback-canvases-and-the-blit-path) | | A canvas goes soft after browser zoom | Watch the resolution media query and re-arm it on every change | [Canvas and WebGL engines: browser zoom](https://www.prototemplate.com/docs/engineering/engines#browser-zoom) | | A wide window on weak hardware gets the heaviest quality tier | Pick the tier from measured frame time | [Canvas and WebGL engines: quality from measured frame time](https://www.prototemplate.com/docs/engineering/engines#quality-from-measured-frame-time) | | Only a shader's fallback renders after an edit | No newline before `#version 300 es`; check that the shader draws | [Canvas and WebGL engines: shader source weight](https://www.prototemplate.com/docs/engineering/engines#shader-source-weight) | | A wall of frozen previews still lags | Stills on every card, the live page only under the pointer | [Canvas and WebGL engines: galleries of live previews](https://www.prototemplate.com/docs/engineering/engines#galleries-of-live-previews) | | A frozen preview keeps animating | Move the loop to `requestAnimationFrame`, the only thing the gate holds | [Canvas and WebGL engines: galleries of live previews](https://www.prototemplate.com/docs/engineering/engines#galleries-of-live-previews) | | A duplicate canvas stays in the DOM in development | Remove every node an effect creates, and check for exactly one canvas | [Canvas and WebGL engines: strict mode and effects](https://www.prototemplate.com/docs/engineering/engines#strict-mode-and-effects) | | Every listener is dead in development | Defer the mount effect's cleanup by one task, as `useMountEffect` does | [Pages and the viewer shell: effects under strict mode](https://www.prototemplate.com/docs/pages#effects-under-strict-mode) | | Dragging a seam over a large figure runs at 5 fps | Keep the subtree under a moving inherited variable small, such as one canvas | [Canvas and WebGL engines: inherited properties on a moving subtree](https://www.prototemplate.com/docs/engineering/engines#inherited-properties-on-a-moving-subtree) | | A canvas shows its stretched 300 by 150 store for a frame | Paint one frame before any `requestAnimationFrame` | [Dither and the studio field: engine copies and canvas traps](https://www.prototemplate.com/docs/components/dither#engine-copies-and-canvas-traps) | | A cloned canvas is blank | Redraw clones from the theme tokens, sized from `clientWidth` | [Dither and the studio field: engine copies and canvas traps](https://www.prototemplate.com/docs/components/dither#engine-copies-and-canvas-traps) | ## Dither [#dither] | Symptom | Fix | Explained in | | ------------------------------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | | A dithered transition flashes on its first frame | Anchor the tile: `phase.y = n % 8` for a canvas n cells lower | [Dither in practice: transitions on one grid](https://www.prototemplate.com/docs/craft/dither#transitions-on-one-grid) | | Quick steps flash back to a picture that has gone | Start each new mix from the frame on screen | [Dither in practice: transitions on one grid](https://www.prototemplate.com/docs/craft/dither#transitions-on-one-grid) | | A step through a new level becomes a cut | Decode all 65 mask levels before the first step | [Dither in practice: transitions on one grid](https://www.prototemplate.com/docs/craft/dither#transitions-on-one-grid) | | A picture's edge fades in grey cells | A hard cut at the plate's edge, then a dissolve by tone | [Dither in practice: transitions on one grid](https://www.prototemplate.com/docs/craft/dither#transitions-on-one-grid) | | A field arrives as a fade | Raise its tone from 0 | [Dither in practice: transitions on one grid](https://www.prototemplate.com/docs/craft/dither#transitions-on-one-grid) | | A dithered pattern on a moving sprite flickers | Put dither on fields and stills; a moving sprite carries solid ink | [Dither and the studio field: dither on a moving sprite](https://www.prototemplate.com/docs/components/dither#dither-on-a-moving-sprite) | | Dither dots alias into noise at 1x | Draw at 2x the stage height with `image-rendering: pixelated` | [Dither in practice: cells are screen pixels](https://www.prototemplate.com/docs/craft/dither#cells-are-screen-pixels) | | A dithered photograph loses its inscriptions | No blur; the cutter solves the black and white points | [Dither in practice: the picture standard](https://www.prototemplate.com/docs/craft/dither#the-picture-standard) | | The picture cutter refuses a source file | Its bytes changed upstream; the recorded sha256 pins the original | [Dither in practice: the picture standard](https://www.prototemplate.com/docs/craft/dither#the-picture-standard) | | The hero's field has no effect in the light theme | A filter, because a screen blend is inert in an isolated host | [Dither in practice: material strength](https://www.prototemplate.com/docs/craft/dither#material-strength) | | A placed picture reads as an empty window with a sliver | Anchor it to a share of the field's ramp | [Dither in practice: placing a picture on a field](https://www.prototemplate.com/docs/craft/dither#placing-a-picture-on-a-field) | ## Lines and seams [#lines-and-seams] | Symptom | Fix | Explained in | | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | A seam reads at the two-stroke grey | Check the neighbouring pixels: a glow raises them and a double leaves them at the ground | [Lines and seams: reading a seam by its grey](https://www.prototemplate.com/docs/craft/lines#reading-a-seam-by-its-grey) | | A bordered element's edge composites darker than every other hairline | `background-clip: padding-box` | [Lines and seams: self-stacks and stacked grounds](https://www.prototemplate.com/docs/craft/lines#self-stacks-and-stacked-grounds) | | A new rail doubles every hairline by 2px a side | Seat it on the existing pages' edges | [Lines and seams: rails drawn as borders](https://www.prototemplate.com/docs/craft/lines#rails-drawn-as-borders) | | Rules keep their author colour in forced colours | The hairline tokens become `CanvasText` | [Lines and seams: rules that run the full width](https://www.prototemplate.com/docs/craft/lines#rules-that-run-the-full-width) | | A grid of links draws stacked lines | Only the first link of each row draws the rule | [Lines and seams: rules that run the full width](https://www.prototemplate.com/docs/craft/lines#rules-that-run-the-full-width) | | Cards paint over the rule below them after a reveal | Set `will-change` only for the tween and clear what the tween set | [Lines and seams: stacking contexts over seams](https://www.prototemplate.com/docs/craft/lines#stacking-contexts-over-seams) | | Sticky figures stop sticking | `overflow-x: clip` on the root, which clips without making a scroll container | [Lines and seams: registration crosses and horizontal clipping](https://www.prototemplate.com/docs/craft/lines#registration-crosses-and-horizontal-clipping) | ## SVG lines [#svg-lines] | Symptom | Fix | Explained in | | ----------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | A dash meant as a fraction of a path draws as a short fleck | Drop `pathLength` under `non-scaling-stroke` and dash in user units | [SVG lines in motion: pathLength under a non-scaling stroke](https://www.prototemplate.com/docs/craft/svg-motion#pathlength-under-a-non-scaling-stroke) | | One travelling pulse shows up as several | Move a short `` segment by translate | [SVG lines in motion: a travelling dash that tiles](https://www.prototemplate.com/docs/craft/svg-motion#a-travelling-dash-that-tiles) | | An arc parked on a closed shape leaves part of it unpainted | Start the path where the arc starts | [SVG lines in motion: dashes on a closed path](https://www.prototemplate.com/docs/craft/svg-motion#dashes-on-a-closed-path) | | A draw-on runs backwards | Read the first point of `d`, then flip the parked sign | [SVG lines in motion: the sign of the parked offset](https://www.prototemplate.com/docs/craft/svg-motion#the-sign-of-the-parked-offset) | | A fleck stays at the end of a line that should be empty | `'100 200'` parked at 101 or -101 | [SVG lines in motion: a fleck at the end of the path](https://www.prototemplate.com/docs/craft/svg-motion#a-fleck-at-the-end-of-the-path) | | A dash freezes for about four frames, then jumps | `autoRound: false`, and `pathLength` 1000 on a looping dash | [SVG lines in motion: rounding, cached paths and empty paths](https://www.prototemplate.com/docs/craft/svg-motion#rounding-cached-paths-and-empty-paths) | | A rerun of an effect traces an empty path | Store the authored `d` once and restore it before measuring | [SVG lines in motion: rounding, cached paths and empty paths](https://www.prototemplate.com/docs/craft/svg-motion#rounding-cached-paths-and-empty-paths) | | Curves warp and text distorts in a stretched strip | Straight taps with one elbow, labels in HTML | [SVG lines in motion: drawing under a stretched viewBox](https://www.prototemplate.com/docs/craft/svg-motion#drawing-under-a-stretched-viewbox) | | The GT monogram renders 214 units too high | Reference the symbol from an svg with no viewBox | [SVG lines in motion: symbols and masks](https://www.prototemplate.com/docs/craft/svg-motion#symbols-and-masks) | | A mask lands away from its shape | Write the mask in the masked shape's coordinates | [SVG lines in motion: symbols and masks](https://www.prototemplate.com/docs/craft/svg-motion#symbols-and-masks) | | A second copy of a figure draws the first one's mask | A unique id prefix per figure | [SVG lines in motion: symbols and masks](https://www.prototemplate.com/docs/craft/svg-motion#symbols-and-masks) | | A mark seated on an iso face lies flatter than the face | `plane()` in place of `scale(1 0.5) rotate(45)` | [Isometric drawings: numbers and traps](https://www.prototemplate.com/docs/components/iso#numbers-and-traps) | ## GSAP [#gsap] | Symptom | Fix | Explained in | | --------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `window.gsap` is undefined in the page | Read state from the page, such as a computed CSS-variable dial | [SVG lines in motion: GSAP from outside the page](https://www.prototemplate.com/docs/craft/svg-motion#gsap-from-outside-the-page) | | Overriding `requestAnimationFrame` does not freeze an animation | Poll the dial and capture about 100ms early | [SVG lines in motion: GSAP from outside the page](https://www.prototemplate.com/docs/craft/svg-motion#gsap-from-outside-the-page) | | A harness that seeks a timeline gets stale frames | `tl.seek(t, false)` so `onUpdate` runs | [SVG lines in motion: GSAP from outside the page](https://www.prototemplate.com/docs/craft/svg-motion#gsap-from-outside-the-page) | | A shimmer's sweep lands about 180 units off its mark | Pre-rotated windows moved by a pure translate | [SVG lines in motion: GSAP from outside the page](https://www.prototemplate.com/docs/craft/svg-motion#gsap-from-outside-the-page) | | A CSS transition never runs on the first frame | A keyframe animation | [SVG lines in motion: transitions at first paint](https://www.prototemplate.com/docs/craft/svg-motion#transitions-at-first-paint) | | Glyphs jump across the screen after the fonts load | Measure again and never resample | [SVG lines in motion: moving type](https://www.prototemplate.com/docs/craft/svg-motion#moving-type) | ## Lottie on canvas [#lottie-on-canvas] | Symptom | Fix | Explained in | | ---------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | | A translated animation draws nothing after one text layer | An empty glyph entry for every drawable character, `\r` and `\u0003` | [Lottie on canvas: a missing glyph stops the frame](https://www.prototemplate.com/docs/engineering/lottie#a-missing-glyph-stops-the-frame) | | Content under a track matte stays clipped after the canvas grows | Resize every matte buffer after each resize and redraw | [Lottie on canvas: track-matte buffers are sized once](https://www.prototemplate.com/docs/engineering/lottie#track-matte-buffers-are-sized-once) | | `drawImage` throws from a canvas parked at 0 by 0 | Park a hidden player at `resize(1, 1)` | [Lottie on canvas: track-matte buffers are sized once](https://www.prototemplate.com/docs/engineering/lottie#track-matte-buffers-are-sized-once) | | A playing animation redraws 120 times a second | `setSubframe(false)`, and per-frame work only when the frame changes | [Lottie on canvas: subframes redraw at display rate](https://www.prototemplate.com/docs/engineering/lottie#subframes-redraw-at-display-rate) | | A hidden layer builds its canvas at the wrong size | A `visibility: hidden` class in place of the `hidden` attribute | [Lottie on canvas: hidden layers that keep their box](https://www.prototemplate.com/docs/engineering/lottie#hidden-layers-that-keep-their-box) | | Two players meant to match stay a frame apart | One leader; followers stay paused and take its raw frame | [Lottie on canvas: two players on one clock drift](https://www.prototemplate.com/docs/engineering/lottie#two-players-on-one-clock-drift) | | The animation jumps at every loop | `loop: false`, a copy of the last frame, then a dither back to the start | [Lottie on canvas: the loop is a cut](https://www.prototemplate.com/docs/engineering/lottie#the-loop-is-a-cut) | | Reduced motion shows an empty frame | A poster frame from the longest stretch with text on screen | [Lottie on canvas: a poster frame for reduced motion](https://www.prototemplate.com/docs/engineering/lottie#a-poster-frame-for-reduced-motion) | | A lower canvas resolution barely speeds up text | Cut the text drawn; the cost is replaying glyph paths in script | [Lottie on canvas: canvas text costs JavaScript](https://www.prototemplate.com/docs/engineering/lottie#canvas-text-costs-javascript) | ## Type [#type] | Symptom | Fix | Explained in | | ------------------------------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | A page renders desktop Inter 3 when its font file fails | Bind the face to an identifier no installed font carries | [Type: the binding](https://www.prototemplate.com/docs/craft/type#the-binding) | | A client-side navigation swaps one page's face for another's | A unique identifier for every `localFont` call | [Type: the binding](https://www.prototemplate.com/docs/craft/type#the-binding) | | A subset renders a shade off the full file | Keep glyph names when subsetting | [Type: subsets and glyph names](https://www.prototemplate.com/docs/craft/type#subsets-and-glyph-names) | | A tracked heading loses its `->` arrow and its centred colons | Name `calt` in the feature list | [Type: contextual alternates under letter-spacing](https://www.prototemplate.com/docs/craft/type#contextual-alternates-under-letter-spacing) | | The brand deck's headings lose their contextual alternates | The same `calt`, which waits for the deck's owner | [The brand deck: the sheet and the grammar](https://www.prototemplate.com/docs/deck#the-sheet-and-the-grammar) | | Optical sizing stops for a whole subtree | Never write `font-variation-settings` or `font-optical-sizing: none` | [Type: optical size](https://www.prototemplate.com/docs/craft/type#optical-size) | | A measure of `62ch` holds 84 characters a line | Set measures in em | [Type: measure](https://www.prototemplate.com/docs/craft/type#measure) | | Hebrew and Arabic render in Arial | A script stack with no Inter variable in front | [Type: scripts and the Arial fallback](https://www.prototemplate.com/docs/craft/type#scripts-and-the-arial-fallback) | | Numerals in a weight 500 block render at 700 | A base rule sets `b, strong` to 500 | [Type: weight and stack traps](https://www.prototemplate.com/docs/craft/type#weight-and-stack-traps) | | A second Inter appears only while the page loads | Stacks read `var(--font-inter)` and name no bare Inter | [Type: weight and stack traps](https://www.prototemplate.com/docs/craft/type#weight-and-stack-traps) | | A label draws a different `a` from the row beside it | Labels read the text token | [Type: weight and stack traps](https://www.prototemplate.com/docs/craft/type#weight-and-stack-traps) | | A heading sits 1.85px above its label from 1348px wide | A trim inside the media query where the rounding step occurs | [Type: seating display type](https://www.prototemplate.com/docs/craft/type#seating-display-type) | | A heading painted on a canvas comes out 12px short | A `FontFace` built with `featureSettings` | [Type: canvas text](https://www.prototemplate.com/docs/craft/type#canvas-text) | | WebKit canvas text changes its optical size | Copies of the font pinned to one opsz and `text-rendering: auto` | [Type: canvas text](https://www.prototemplate.com/docs/craft/type#canvas-text) | | A face meant for one page is preloaded on every page | `preload: false` and a family name only that route's CSS names | [Type: preloads that leak across routes](https://www.prototemplate.com/docs/craft/type#preloads-that-leak-across-routes) | | The theme button's glyph renders in a system face | Read the face with `CSS.getPlatformFontsForNode`, since the lint's range misses it | [Type: glyphs outside the subsets](https://www.prototemplate.com/docs/craft/type#glyphs-outside-the-subsets) | ## Interface [#interface] | Symptom | Fix | Explained in | | ------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | Two buttons with one CSS gap show different gaps beside their icons | Match the measured visible gaps | [Interface details: gaps beside a glyph](https://www.prototemplate.com/docs/craft/interface-details#gaps-beside-a-glyph) | | A row of buttons moves when one label reads Copied | Size the face from the idle label through a grid sizer | [Interface details: gaps beside a glyph](https://www.prototemplate.com/docs/craft/interface-details#gaps-beside-a-glyph) | | A copy button fails in an agent app's browser pane | Test it in Chrome; the pane blocks clipboard writes | [Interface details: the copy control](https://www.prototemplate.com/docs/craft/interface-details#the-copy-control) | | The foot of a page jumps when a control mounts at hydration | Reserve the control's height in the server markup | [Interface details: layout shifts](https://www.prototemplate.com/docs/craft/interface-details#layout-shifts) | | Controls move when the language changes | Anchor the language left and the theme toggle right | [Interface details: layout shifts](https://www.prototemplate.com/docs/craft/interface-details#layout-shifts) | | A sign-in widget draws a white card on a dark page | Pass the resolved theme and key the widget on it | [Interface details: third-party widgets and the site's theme](https://www.prototemplate.com/docs/craft/interface-details#third-party-widgets-and-the-sites-theme) | | Captions fail contrast in the light theme | `#6e737c` or ink-2 in place of titanium | [Interface details: contrast](https://www.prototemplate.com/docs/craft/interface-details#contrast) | | A double click moves a carousel two slides | Drop commands within 350ms of the last one | [Interface details: carousels and galleries](https://www.prototemplate.com/docs/craft/interface-details#carousels-and-galleries) | | An element that moves on hover flickers under the pointer | A still hit area with the moving drawing inside it | [Interface details: a still hit area for a moving target](https://www.prototemplate.com/docs/craft/interface-details#a-still-hit-area-for-a-moving-target) | ## Verification [#verification] | Symptom | Fix | Explained in | | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | Shader and dither canvases come back blank in an embedded browser pane | Take pixels with Playwright in Chrome for Testing | [Proving a visual change: the in-app browser pane](https://www.prototemplate.com/docs/engineering/verification#the-in-app-browser-pane) | | A capture comes out in the wrong theme | Seed the theme before load and assert it took | [Proving a visual change: the in-app browser pane](https://www.prototemplate.com/docs/engineering/verification#the-in-app-browser-pane) | | An edit trims padding where the gap came from a margin | Quote the box model before editing | [Proving a visual change: the spot before the edit](https://www.prototemplate.com/docs/engineering/verification#the-spot-before-the-edit) | | A scroll story locks in a different place scrolling up | Run the whole stress matrix | [Proving a visual change: the stress matrix](https://www.prototemplate.com/docs/engineering/verification#the-stress-matrix) | | A fix shown with a scripted selection proves nothing about a drag | Perform the real gesture and read the result back | [Proving a visual change: real gestures](https://www.prototemplate.com/docs/engineering/verification#real-gestures) | | A harness request leaves its origin | Build every URL as `new URL(ORIGIN + path)` | [Proving a visual change: real gestures](https://www.prototemplate.com/docs/engineering/verification#real-gestures) | | A layout breaks in Safari alone | Check one invariant in Chromium and WebKit | [Proving a visual change: WebKit](https://www.prototemplate.com/docs/engineering/verification#webkit) | | A route list read from a running dev server is truncated | Take the routes from the source or the sitemap | [Proving a visual change: parity across an upgrade](https://www.prototemplate.com/docs/engineering/verification#parity-across-an-upgrade) | | Timings read slow and a dev-server Lighthouse run scores 0.65 | Record the load average, and run a production build in a clean profile | [Performance without visual loss: measure honestly](https://www.prototemplate.com/docs/engineering/performance#measure-honestly) | | A lab score does not answer a complaint about a slow first paint | Read field data | [Performance without visual loss: field data against lab data](https://www.prototemplate.com/docs/engineering/performance#field-data-against-lab-data) | | A structural audit passes every item while the visual bar fails | Report each kind of check on its own line | [Agent workflows: kinds of check](https://www.prototemplate.com/docs/handbook/workflows#kinds-of-check) | ## Films [#films] | Symptom | Fix | Explained in | | ------------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | Two renders of one film differ | Compare every pair by `framemd5`; large animated type renders on the software path | [Films: compare every render pair](https://www.prototemplate.com/docs/craft/films#compare-every-render-pair) | | One render loses its dither field and reports success | Guard every cached image read with `img.complete && img.naturalWidth > 0` | [Films: images and the first timeline update](https://www.prototemplate.com/docs/craft/films#images-and-the-first-timeline-update) | | The render log reads "Fetched 11 font face(s)" | Set type through the kit's `var(--font)` | [Films: fonts in a composition](https://www.prototemplate.com/docs/craft/films#fonts-in-a-composition) | | A thin blue stroke turns slate grey in the encoded file | Start it on an even pixel | [Films: thin coloured strokes in H.264](https://www.prototemplate.com/docs/craft/films#thin-coloured-strokes-in-h264) | | 1px routes vanish at phone size | 2.5px routes over a keyline, checked at 640 by 360 | [Films: thin lines over a dithered ground](https://www.prototemplate.com/docs/craft/films#thin-lines-over-a-dithered-ground) | | A louder mix comes out quieter | Put peak control in the ffmpeg masters | [Films: the mix and its traps](https://www.prototemplate.com/docs/craft/films#the-mix-and-its-traps) | | A gain ramp clicks | `aeval` for any gain curve | [Films: the mix and its traps](https://www.prototemplate.com/docs/craft/films#the-mix-and-its-traps) | | The music bed pumps between lines | Hold the duck across gaps under about 1.2s | [Films: the mix and its traps](https://www.prototemplate.com/docs/craft/films#the-mix-and-its-traps) | | A voice model ignores its speed setting | Inline pace tags | [Films: speech synthesis](https://www.prototemplate.com/docs/craft/films#speech-synthesis) | | A still pulled with `-ss` comes out near black | Select the frame by index | [Films: frames, probes and disks](https://www.prototemplate.com/docs/craft/films#frames-probes-and-disks) | | A render that looks fine in its log is an error page | Flag frames whose grey standard deviation is under about 20 | [Films: frames, probes and disks](https://www.prototemplate.com/docs/craft/films#frames-probes-and-disks) | | `page.evaluate(() => tl.seek(t))` hangs | Return a number from `evaluate` | [Films: frames, probes and disks](https://www.prototemplate.com/docs/craft/films#frames-probes-and-disks) | ## Routing [#routing] | Symptom | Fix | Explained in | | ------------------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | A locale prefix outside the roster answers 500 | A locale guard ahead of every rewrite | [Localized routing: middleware order](https://www.prototemplate.com/docs/engineering/i18n-routing#middleware-order) | | Every old URL redirects to a 404 | Deploy a redirect only after the moved pages exist | [Localized routing: middleware order](https://www.prototemplate.com/docs/engineering/i18n-routing#middleware-order) | | Links to `/sitemap.xml` and other root files answer 404 | A plain `` for root files | [Localized routing: links, routers and strings in gt-next](https://www.prototemplate.com/docs/engineering/i18n-routing#links-routers-and-strings-in-gt-next) | | A programmatic navigation loses the locale | Push a path that already carries the locale | [Localized routing: links, routers and strings in gt-next](https://www.prototemplate.com/docs/engineering/i18n-routing#links-routers-and-strings-in-gt-next) | | Strings render in English in every locale | `const gt = await getGT();` on its own line | [Localized routing: links, routers and strings in gt-next](https://www.prototemplate.com/docs/engineering/i18n-routing#links-routers-and-strings-in-gt-next) | | The language menu breaks on default-locale pages | Map `usePathname()` back to the public path | [Localized routing: links, routers and strings in gt-next](https://www.prototemplate.com/docs/engineering/i18n-routing#links-routers-and-strings-in-gt-next) | | A CLI setup command waits forever under an agent | `--no-interactive` and a flag for every answer | [Localized routing: a start prompt that must run unchanged](https://www.prototemplate.com/docs/engineering/i18n-routing#a-start-prompt-that-must-run-unchanged) | ## The shared machine [#the-shared-machine] | Symptom | Fix | Explained in | | ---------------------------------------------------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | | A `pkill -f` stops other sessions' servers, browsers and renders | Stop a process by the PID recorded when it started | [Agents on one machine: stopping a process](https://www.prototemplate.com/docs/handbook/shared-machine#stopping-a-process) | | A kill by port closes a browser | `kill $(lsof -ti tcp: -sTCP:LISTEN)` | [Agents on one machine: stopping a process](https://www.prototemplate.com/docs/handbook/shared-machine#stopping-a-process) | | Every later command reports "command not found" | Never name a zsh variable `path` | [Agents on one machine: shell traps in zsh](https://www.prototemplate.com/docs/handbook/shared-machine#shell-traps-in-zsh) | | `"$B:apps/landing"` rewrites the string | Brace the name: `"${B}:apps/landing"` | [Agents on one machine: shell traps in zsh](https://www.prototemplate.com/docs/handbook/shared-machine#shell-traps-in-zsh) | | A loop over an unquoted variable runs once | An array, an explicit list or `${=urls}` | [Agents on one machine: shell traps in zsh](https://www.prototemplate.com/docs/handbook/shared-machine#shell-traps-in-zsh) | | A Python heredoc becomes a pushed commit message | Write each message to a file first, then `git commit -F` | [Agents on one machine: shell traps in zsh](https://www.prototemplate.com/docs/handbook/shared-machine#shell-traps-in-zsh) | | `echo ====` stops a `&&` chain | Quote it | [Agents on one machine: shell traps in zsh](https://www.prototemplate.com/docs/handbook/shared-machine#shell-traps-in-zsh) | | A grep over two folders greps nothing | A glob with no match aborts the line; use `find` or quote the pattern | [Agents on one machine: shell traps in zsh](https://www.prototemplate.com/docs/handbook/shared-machine#shell-traps-in-zsh) | | A browser starts while the memory check said to wait | Chain on the check's own exit status with `&&` and no pipe | [Agents on one machine: exit-status gates](https://www.prototemplate.com/docs/handbook/shared-machine#exit-status-gates) | | One `git grep` grows from 42.6 to 166GB | `-F` with a pattern file, `-I` and a thread count | [Agents on one machine: memory and disk](https://www.prototemplate.com/docs/handbook/shared-machine#memory-and-disk) | | A reboot empties the sessions' scratch folders | Keep specs and helpers that must outlive the day in a repository | [Agents on one machine: memory and disk](https://www.prototemplate.com/docs/handbook/shared-machine#memory-and-disk) | | Lane agents hang for hours on shell calls that never start | No `rm` in a lane, edits in separate calls, network calls under an alarm | [Agents on one machine: long commands](https://www.prototemplate.com/docs/handbook/shared-machine#long-commands) | | A commit publishes another session's staged deletions | Commit with a pathspec | [Agents on one machine: one checkout and several sessions](https://www.prototemplate.com/docs/handbook/shared-machine#one-checkout-and-several-sessions) | | A copy into the tree erases commits another session pushed | Fetch and read `HEAD..origin/main` before any copy or push | [Agents on one machine: one checkout and several sessions](https://www.prototemplate.com/docs/handbook/shared-machine#one-checkout-and-several-sessions) | ## Pull requests and workflows [#pull-requests-and-workflows] | Symptom | Fix | Explained in | | --------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | A pull request's checks are stale, cancelled or missing | Resolve the merge conflicts first | [Pull requests and CI: CI signals that mislead](https://www.prototemplate.com/docs/handbook/pull-requests#ci-signals-that-mislead) | | The latest push shows a red e2e it did not cause | List the runs and find the first red one | [Pull requests and CI: CI signals that mislead](https://www.prototemplate.com/docs/handbook/pull-requests#ci-signals-that-mislead) | | The build dies in "Running TypeScript" | Raise `--max-old-space-size` | [Pull requests and CI: CI signals that mislead](https://www.prototemplate.com/docs/handbook/pull-requests#ci-signals-that-mislead) | | A bot's summary describes a diff that no longer exists | Compare its reviewed commit with the head before acting | [Pull requests and CI: review bots](https://www.prototemplate.com/docs/handbook/pull-requests#review-bots) | | A force push erases a collaborator's commits | Fetch, then lease against the sha you expect | [Pull requests and CI: git on shared trees](https://www.prototemplate.com/docs/handbook/pull-requests#git-on-shared-trees) | | A fast-forward overwrites ignored files without a warning | Copy the ignored originals aside first | [Pull requests and CI: git on shared trees](https://www.prototemplate.com/docs/handbook/pull-requests#git-on-shared-trees) | | Commits meant for a pull request never reach it | Check the branch before each commit and compare the head with `HEAD` | [Pull requests and CI: git on shared trees](https://www.prototemplate.com/docs/handbook/pull-requests#git-on-shared-trees) | | A lane builds nothing and verifies something unrelated | Open every lane prompt with the user's own words for that lane | [Agent workflows: lane prompts](https://www.prototemplate.com/docs/handbook/workflows#lane-prompts) | | A resumed workflow pays for its finished agents again | Resume by run id with byte-identical arguments | [Agent workflows: resuming a run](https://www.prototemplate.com/docs/handbook/workflows#resuming-a-run) | ## Sources [#sources] * The pages each row links to, read on 2026-10-21: the Craft and Engineering tabs of /docs, the Components and Internals pages with trap sections, and the handbook's documents on one machine, workflows, pull requests and review. * The curated skills those pages cite: `gt-motion` (`references/traps.md`), `gt-performance` (`references/webgl-lifecycles.md`), `gt-website` (`references/docs.md`, `references/deploys.md`), `gt-local-dev`, `gt-verify`, `gt-films`, `gt-dither`, `gt-brand` (`references/type.md`), `gt-orchestration` and `gt-ship`. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Brand > The identity canon: the name, the idea, the character and voice, the mark, color, type, language as material, and where the identity ships. URL: https://www.prototemplate.com/docs/handbook/brand General Translation's identity, laid out for anyone who has to build with it, including our partners at basement studio. This document is the written canon. The living version is the [`/brand`](https://www.prototemplate.com/docs/handbook/brand) page, and the visual laws live in [`DESIGN.md`](https://www.prototemplate.com/docs/handbook/design). *** ## 1. The name [#1-the-name] **General Translation** was chosen deliberately, in this order: 1. **Ambition.** Like General Motors or General Electric, the name says we intend to be the trustworthy, technologically innovative number one in the category, an enterprise in the old sense. 2. **Generality.** A reference to "artificial general intelligence": general AI models outperform specific translation models because they understand context and can be directed. 3. **Distinction.** Every other localization company seemed to begin with an "L". ### The naming system [#the-naming-system] | name | what it is | | ------------------------------------------------------- | ------------------------------------------------------------------------------- | | General Translation, Inc. | the company | | GT | the short form, and the mark | | `gt` | the open-source code library; you run `gt translate` | | `gt-next`, `gt-react`, `gt-vue`, `gt-node`, `gt-python` | the framework-specific packages | | Locadex | the AI agent product | | generaltranslation.com | the domain (also held: gt.sh, generaltranslation.ai/.dev, locadex.com/.ai/.dev) | | glyphfield.com | the companion tooling site (shader library, animation studio) | ## 2. The idea [#2-the-idea] **Every product in every language.** Native-level speed and quality, from day one. That's the whole thesis, executed insanely hard. The positioning: **the Vercel of localization.** Two halves designed together: open-source developer tools (the Next.js of the analogy: the `gt` libraries) and closed-source infrastructure (the Vercel: context-aware translation APIs, versioning, editing, integrations, agents) that is the best-in-class way to use those tools. Because we build the entire stack, we can promise what point solutions can't: consistent, high-quality translation across a whole business, integrated in an afternoon. Two registers, one family: the open source should feel community-owned; the platform should feel enterprise-grade. The values, which the visual identity must carry: * **Engineering-first.** Built by people with deep technical roots, for the world's best engineering teams. * **Craft.** We care about the difference between drawn-once and drawn-twice lines. That is literal: see the line law in `DESIGN.md`. * **Infrastructure-grade.** Reliable, fast, secure. The brand should feel like something an enterprise stands on, not an app it tries. * **Cosmopolitan.** Urbane, sophisticated, connecting the world and its languages. Language is our material, not just our market. * **Hand-crafted.** The brand reads as made by people who care, not assembled from a template. ## 3. The character [#3-the-character] The brand carries itself like a **"fullstack director"**: it writes the script and it pushes the camera. Creative and technically innovative, never one without the other. On time, under budget, over-delivering, always working with the best people, with a keen sense of the market and for making things people love. ### Personality (working readings, from the completed system) [#personality-working-readings-from-the-completed-system] | | position | | | ------------ | -------------------------- | -------------------- | | Classic | ●──── toward → | **Modern** | | **Reserved** | ← toward ────● | Playful | | **Minimal** | ← firmly | Expressive | | **Clever** | ← leaning | Warm | | **Rational** | ← firmly | Quirky | | Understated | toward → | **Confident** | | **Serious** | ← leaning | Witty | | **Neutral** | ← leaning, a glint allowed | Slightly mischievous | ### Aesthetic (working readings) [#aesthetic-working-readings] | | position | | | -------------- | -------------------------------------------- | ----------- | | **Clean** | texture only as ordered dither | Textured | | Soft | → | **Sharp** | | **Geometric** | ← firmly | Organic | | **Light** | paper-first; dark mode is one ink surface | Dark | | **Muted** | one spectral accent per page | Vibrant | | **Flat** | depth from lines and material, never shadows | Dimensional | | **Monochrome** | four absolute colors + one accent | Colorful | | **Structured** | ← firmly | Playful | | Warm | → | **Cool** | | **Elegant** | ← | Fun | These are read from the shipped system for basement to confirm or push. ### Voice [#voice] Measured, declarative, precise, quietly confident. Every sentence is complete and makes one claim. A caption states the mechanism: "A framed cell has a 1px padding reveal that shows the ground underneath, so the cell draws no border." Copy uses no exclamation marks, no hedging and no marketing adjectives where a fact would do. Wit is allowed when it is precise. Technical terms are used precisely and sparingly, then explained plainly. The register sits closer to a well-written spec or a good engineering blog than to marketing copy, and the copy is about the product. * Say: "Translations are generated at build time and deploy with the app." * Not: "Supercharge your global growth with cutting-edge AI!" ## 4. The mark [#4-the-mark] The GT monogram (`/brand/gt-logo-light.svg`, `gt-logo-dark.svg` + transparent PNG variants): every stroke of the mark is two parallel lines, the doubled-line grammar at brand scale. Locadex carries its own mark (`/brand/locadex-mark.svg`). Rules: * **One ink.** The mark renders in ink on paper or paper on ink. Never a third color, never a gradient, never a shadow. * **The dark surface inverts the drawn mark's ink**, either as an alpha mask taking the surface's ink or as a clean invert. The mark is a drawing, not a picture. * In illustration systems (the iso family), marks render as **alpha masks** so the shape takes the surface's ink; the sanctioned flourish is the Bayer-dithered specular shimmer (`DitheredMark`), never a GIF, never a filter glow. * At text size, the wordmark sits inline with prose at the cap height of the line it lives in, as in the generaltranslation.com hero's "GT builds full-stack localization for apps, docs, and websites". * **The identity must survive compression:** a favicon, a CLI banner, a README, a syntax-highlighted code block. Developers meet the brand in a terminal as often as on a website. ## 5. Color [#5-color] Four absolute colors (ink `#070707`, raised ink `#101010`, titanium `#8a8f98`, paper `#ffffff`) plus **exactly one spectral accent per page** (the working accent: `#2f5ce0`; its dark-band lift `#86a8ff`). Structural color everywhere derives from the four as alpha steps; dark mode is a pure token remap. Full law: `DESIGN.md` §1. The accent is a controlled edge, never a wash; one bright white; depth from lines and material, not shadows. ## 6. Type [#6-type] * **Inter** is the one typeface: display, interface and text. Headlines, interface chrome, captions, long-form reading and documentation prose are all set in it. The build is the real rsms.me Inter (v4.1 variable, roman + italic, opsz axis), self-hosted rather than loaded from Google Fonts. * **Headings use weight 500 at most.** Display text never goes bolder than 500; long-form text is set at weight 400. * **Multilingual-first.** Headlines, UI, and marks must hold up in CJK, RTL, and Indic scripts as well as Latin. A wordmark or layout that only works in English contradicts the company. Inter covers Latin, Greek and Cyrillic; other scripts fall back to the system face for that script, and every layout is checked in them. Licensed type needs credible CJK and RTL coverage, or well-matched companion faces. * **Inter is the working typeface for the identity project.** It is not a final decision. Alternatives remain open if they satisfy the script-coverage constraint. * **Monospace is an instrument voice, not a brand voice.** It appears where code artifacts appear (tokens, terminals, file paths, small labels in technical diagrams and product UI) and nowhere else, and even those labels are avoided where possible. The direction explicitly avoids monospace as the brand voice in headlines, body, and marketing. * The prototemplate chrome (this site's own serif/grotesk pairing) is the lab's stationery, not the product brand. ## 7. Language as material [#7-language-as-material] The signature device: **glyphs, characters that make up greater wholes.** Writing systems are the raw material the brand keeps returning to: * The **sentence reassembler** (`EverySentence`): a headline dissolves into glyph dust and reassembles in the next language. Matter is conserved; the same swarm becomes the next sentence. * The **glyph field**: rain from eight writing systems condensing into the word "language," script after script. * **Locale pills** (`LocaleTag`): flag print + code, the one way a locale is named anywhere. The prints are SVG, never emoji; a flag is a functional data chip, never decoration. * The **1-bit Bayer language**: density as ordered dither, never alpha veils. This is the texture of the brand. * The **doubled line**: every connector one path stroked twice; the mark's own grammar running through every diagram. All of them run live in the build log on `/docs/handbook/libraries`, with their APIs. ## 8. Where it ships [#8-where-it-ships] The identity is applied on four kinds of surface today. * **The site.** Marketing pages ship on generaltranslation.com, built in gt-cloud's `apps/landing`. The production landing page (gt-cloud #4213) has been canonical for the site since 2026-08-11. * **Product surfaces.** The dashboard, onboarding, the sign-in plate and the Prototemplate shell have followed the brand deck (`/deck`) since 2026-09-25. * **Motion.** The films on `/motion` apply the identity to film: the blog trailers, the product films and the translation-history series. * **The marks.** The speed marks chosen on 2026-09-29 (`/marks`) sit beside the doubled-line monogram, which stays the current mark (section 4). The title badges in the `/brand` and `/marks` heads cycle through a selection of the marks on `/marks`. When in doubt about how the brand behaves, read the shipped surface the work belongs to. The site grew from the direction of the **Dossier** (`/d/singularity-dossier`). It stays in the gallery with the other directions as the working record of how we got there. From 2026-08-06 to 2026-10-07 this section named it the completed reference for the brand. ## 9. Context for partners [#9-context-for-partners] * **Industry:** AI developer tools; the full stack for localization, meaning i18n libraries, context-aware translation APIs, and the infrastructure for versioning, editing, and integrations. * **Audience:** technical and product executives at growth-stage startups; their engineering and growth teams are the users. * **Public names:** a customer or partner is named only where generaltranslation.com shows it. On 2026-10-10 the home page showed Cursor, Ramp, Mintlify, Profound, Partiful and ClickHouse, and a quote from Theo, CEO of T3 Chat. `/enterprise` showed Ramp with 11+ locales across its dashboard, landing page and iOS, Cursor with 14+ across web and docs, Profound with 47+ across platform and docs, Partiful with 2+ across web, iOS and Android, and Sierra with 7+ across web and GTM, with a quote from Lee Robinson, VP of Developer Experience at Cursor. `/enterprise/contact` had partner pages for YC, Mercury, Slash, speedrun, a16z and The Residency. Every other name stays off public surfaces, this repository included. Read the live site again before a new surface names anyone. * **Against:** legacy TMS (seat-based, partial-stack). GT is usage-based and owns the whole stack, so it can own the whole experience. * **Admired:** Vercel, Resend, Stripe, for reliable, developer-first infrastructure with engineering excellence and customer experience. * **Direction:** International Style discipline with Art Deco's future-embracing stance. Swiss grids, blueprints, boxes, no rounded corners. Water and ocean as the recurring theme, connecting the globe. Bespoke material textures (reference: materialarchiv.ch). * **References:** Josef Müller-Brockmann and the Swiss poster tradition; Otl Aicher's Munich 1972 pictograms; Vignelli's subway map; split-flap departure boards; undersea cable maps and nautical charts; the Rosetta Stone; the Chrysler Building; Powers of Ten; vintage National Geographic; Borges' Library of Babel; the Whole Earth Catalog; transit signage; NYRB Classics; Dieter Rams; vintage Olympics stamps. * **Avoid (final):** * monospace as the brand voice in headlines, body, or marketing; small mono labels inside technical diagrams and product UI remain instruments, avoided where possible. * smooth scrolling, scroll-hijacking, and inertia libraries; native scroll everywhere. * robot and sparkle iconography for AI. * the flag-soup cliche; flags are functional data chips only, printed as SVG, never emoji. * iridescent AI gradients and glassmorphism. * eyebrow text that has not earned its place; three stacked lines saying the same thing is noise, functional tags and labels are fine. * em dashes in rendered prose. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Design system > The design canon: the four-color system, the line law, rails and seams, the doubled line, iso, the 1-bit language, moving type and motion. URL: https://www.prototemplate.com/docs/handbook/design The canon for every page in Prototemplate: the laws the directions and the shipped site run on, distilled from the founder rounds. The build log on `/docs/handbook/libraries` is the living version of this document, with the diagrams drawn and the engines running. This file is the reference you read before touching a page. *** ## 1. The four-color system [#1-the-four-color-system] Four absolute colors, declared once in `src/app/globals.css`: | token | value | name | | -------------------- | --------- | ---------- | | `--color-ink` | `#070707` | ink | | `--color-ink-raised` | `#101010` | raised ink | | `--color-titanium` | `#8a8f98` | titanium | | `--color-paper` | `#ffffff` | paper | * Structural color everywhere derives from these plus alpha: every text step is ink or white at some alpha, every hairline is titanium at some alpha, every hatch is a thin ink or white veil. * **Each page adds exactly one spectral accent** (toolchain: `#2f5ce0`; its dark-band lift: `#86a8ff`). One bright white. An accent is a controlled edge, never a wash. * Pages never touch the raw values. Each root class (`.pt-root`, `.toolchain-root`, fork roots) publishes a **semantic layer** — surfaces, ink steps (`-2/-3/-4`), hairlines (`--*-hair`, `--*-hair-2`), hatch — and everything downstream reads those. * **Dark mode is a token remap, nothing else**: under `[data-theme='dark'] ` the paper family collapses onto ink, ink flips to white, and hairline alphas rise so a 1px seam survives between two ink surfaces. One surface family in the dark, the way light mode is one white. * The second surface: light diagrams sit on exactly one sanctioned second surface (`--tc-plate`), mirroring the ink / raised-ink pair dark mode runs on. `--tc-panel` (`#101010`) is the one dark artifact surface for code, config, and diffs. ## 2. The line law [#2-the-line-law] Structure comes from hairlines, and **every line is drawn exactly once**. The auditor (`scripts/lint/lines.mjs`) enforces it mechanically — see `docs/SHIP-LOOP.md`. Four defect classes: 1. **Doubled lines** — two parallel strokes from different owners within 4px, including coincident strokes that composite darker than either. 2. **Missing seams** — section and row junctions no rule closes. 3. **Self-stacks** — a translucent border over the element's own translucent background. Fix: `background-clip: padding-box`, everywhere. 4. **Invisible seams** — rules that exist geometrically but sit within a few RGB steps of the surface they cross. ### Ownership [#ownership] * Exactly one thing draws the page rails for any section — a rails wrapper, or the section's own full-bleed pair. Never both. The pair is the column's inner pair only: one hairline at each column edge, nothing outside it. * The row owns every structural line; cells never draw borders that parallel a row seam. A framed cell has a **1px padding reveal** that shows the ground underneath, so the cell draws no border. * **Flush at the rail**: where a row meets a line that already exists, the cell sits flush and that side's reveal is dropped. A reveal never runs beside a rail; a border never runs beside a seam. ### The named antipattern [#the-named-antipattern] The overlap: two owners each drawing a real line along the same edge, compositing into a band darker and thicker than any line the system allows. The auditor cannot see SVG strokes — reconstructed lines come from computed CSS — so figures must be checked by eye at 2× pixel crops of the junctions. ### Sanctioned devices [#sanctioned-devices] * **The diagonal-hatch band** marks the boundary between a book's front matter and its first section, once per book: 42px of `repeating-linear-gradient(-45deg, transparent 0 6px, var(--pt-hatch) 6px 7px)` between two hairlines it draws itself, `border-block: 1px solid var(--pt-hair)` (`.pt-book-band`, BookView\.css). Its rules and its hatch run across the whole stage, as every rule of a book does (see The reading column), and nothing else draws a line within 4px of either rule; the first section's divider draws no rule of its own. The gallery's article (`.pt-hatch`, prototemplate.css) is the one place a band draws only its bottom rule, because every section there draws its own bottom rule and owns the band's top seam. * **Border crosses**: where two hairlines must legitimately cross, a small plus seated exactly on the intersection (ink, 1px, non-scaling) declares the crossing deliberate — the one ornament a junction is allowed. * **The doubled line** is the one sanctioned double — one owner, one path, stroked twice (see §5). The auditor's allow list holds deliberate devices by name; everything else stays strict. * **The fixed sheet's ring** is the one sanctioned doubled line in chrome. It is the edge of a fixed-size artifact: a slide, the gallery's live 1440x900 exhibit, the compare rig's panes, and the live page on `/directions/` (`.dr-sheet`). The shell draws it as a 1px `--pt-hair` border on `.sheet`, a 1px paper gap from the padding of `.sheet-mat`, and a 1px `--pt-hair-soft` outline on the mat, with no shadow, on the `--pt-plate` letterbox of the fixed stage, as the deck's slide view draws it. A reading page has no ring and no plate. The active thumbnail frame and the active book page frame carry the same border plus offset outline. The auditor allows them under `sheet`, `thumb-frame` and `page-frame`. ### The reading column [#the-reading-column] A reading page sits straight on the stage's paper. That covers the book mode of /brand, /motion, /graphics, /skills and /marks, their record pages, `/directions/` and `/archive/`: every route that renders `Sheet variant='flow'`. Its column is the deck's book column (`deck/parts/head.html` `.book-in`): 1280px at most and centered in the stage, with `--pt-col-pad-x` (56px, 16px under 900px) on each side, `--pt-title-clear` over the title and 120px (80px under 900px) under the last block. The column draws no border, no ground and no mat. The toolbar's bottom rule is the only line above the title, and the sidebar's right edge is the only line beside the page. A fixed-size artifact inside a reading page keeps a frame, because the frame is its edge: the live page on a direction page keeps the fixed sheet's ring, and a capture takes the frame role with the card corner. Kevin asked for this on 2026-10-06, after the grey mat had made every page a framed picture of a page. Every shell rule written for 900px and under also matches a phone on its side, `(pointer: coarse) and (max-height: 500px)`, and `ViewerShell.tsx` `isNarrow()` reads the same query: a 932x430 phone gets the phone's composition and touch sizes (the list as the overlay, 44px book links) instead of the desktop list column of 28px rows. A tablet is taller and keeps the desktop composition. Every structural horizontal rule of a book runs across the whole stage, from the sidebar's edge to the stage's right edge: the mast's rule, the contents rows, the band and the dividers, the full-width list rows of /skills, /graphics, /motion, /marks and a direction page, an archive record's rows, and the rule over a blog post's footer. The element keeps its 1px border for layout and paints it as a border image outset past both ends (`--pt-bleed-top`, `--pt-bleed-bottom` and `--pt-bleed-block` in tokens.css); the outset never scrolls, and the scroll region clips it at the stage's edges. A rule that drops its border also writes `border-image-source: none`, because the image ignores border-width and the build empties the shorthand `border-image: none` (lint:practices holds this). Under forced colors the hairs take `CanvasText`. A contents row is one line: the first link of each row draws it and the others draw none. Rules inside the content keep their measure: the head panel's rows, tables, prose lists, the `---` rule, ledgers and file lists. Kevin asked for full-width rules on 2026-10-07. ### Line law for chrome [#line-law-for-chrome] Chrome is everything the viewer shell draws around content: the toolbar, the sidebar, the index panel, the search, the fixed sheet's ring, the grid, the book frame, the help card, the direction corner, and the standalone deck viewer's own chrome. Every rule in chrome is 1px, drawn once, in one of three roles, and this is part of the identity: a page reads as Prototemplate because its lines are single and their weights mean something. The three roles, and only three: | role | token | draws | | ---------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | structural | `--pt-hair` | large surfaces and the lines that divide the shell: the fixed sheet's ring, the search card, the index panel's left edge, the toolbar bottom, the sidebar right edge, the deck surface index's group headers (the sidebar's group headers draw no rule), the book head's rule, the hatch band's two rules, the section dividers, the segmented control, the field boxes at rest, the install field at rest | | row | `--pt-hair-soft` | list rows, search results, panel rows, the book head's panel rows, the help card's table rows, the fixed sheet's outer ring, the progress track, the sidebar's rail (`.pt-sb-rail`, outline density) and the run guides (thumbnail density) | | frame | `--pt-edge` | frames of images and tiles only: thumbnails, the book's page frames, the grid tiles, the 96x54 and 64x36 captures, the hover preview's frame, and the help card | The weights carry meaning. A frame at 0.62 alpha is the darkest line in chrome because it holds a picture in place: the eye must find the edge of a capture against the paper around it. A large surface (a fixed sheet, a card, a panel, a head) is not a picture; it is the paper itself, so its edge is the structural hairline, and its outer ring (where it has one) steps down again to the row weight. A fixed sheet drawn in the frame weight reads as a boxed image, which is the bug Kevin named in round six ("make the borders around these areas the proper border colors"). The help card is the one card that keeps the frame weight: it floats over a scrim, where the hairline would vanish. Nothing in chrome sets a border color from any other token or literal. `--pt-ink` appears on a border only as a state: a pressed button (`.is-on`), the active thumbnail or page frame (`.is-active`), the count while it is being edited, the solid call to action, the sidebar's current thumb on its rail, and a field while it holds focus. Book heads, table headings and the deck surface index's group headers draw `--pt-hair`, never ink; the sidebar's group headers draw no rule at all. Outlines are rings: the three roles, ink for focus and active rings, paper for a ring on an ink plate. Where two bordered components touch, exactly one draws the line: | junction | owner | the other side | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | sidebar and stage | the sidebar's right edge | the main region draws no left edge | | toolbar and stage | the toolbar's bottom edge | the stage, the hint row and the index panel draw no top edge | | index panel and stage | the panel's left edge | a fixed sheet's ring runs under the panel; the panel covers the progress track while open | | group header and its first row | no one: the header draws no rule, and the group's boundary is the 16px gap above its header | the first row draws no top rule | | a page row and its run | the group's rail (`.pt-sb-rail`, `--pt-hair-soft`), bending 45 degrees under the page row into the run; in thumbnail density the run's guide | the page row draws no bottom rule; run rows and labels draw no left border | | a run and the deep run in it | the same rail, one more bend; in thumbnail density each run its own guide, 16px or more apart | no row draws a left border | | the marked row and the rail | the rail; the 2px ink thumb is a state over it | the rail under the thumb is covered, not doubled; the row draws no bar | | a row and the pills | no one: the pills are grounds and draw no line | the row draws no box | | a page row and its fold | no one: the fold draws no border at rest, and its focus ring is a state | the row draws no right rule | | last row of a group and what follows | the last row's bottom edge | the next header carries no top rule | | a fixed sheet and its content | the ring (hair border, paper gap, hair-soft outline) | content draws no outer border | | a reading column and the stage | no one: the column draws no edge | the toolbar's bottom rule is the only line above the page and the sidebar's right edge the only line beside it; content draws no outer border | | book head and its contents | the mast's rule (`--pt-hair`), then the note when there is one | the note draws no rule; the contents grid draws no top rule; its rows draw their own bottom rules | | the title and what is above it | no one | nothing is drawn in the `--pt-title-clear` space between the toolbar's bottom rule (the stage's top) and the h1's box, and no line crosses a title | | book head and its panel | the panel's rows draw `--pt-hair-soft` under every row but the last | the panel draws no frame; side by side, the mast's rule closes the mast under the taller of lead and panel; on a narrow head the panel draws a row rule over its first row and the mast's rule is its last row's rule; the install field draws its own `--pt-hair` box and the row above it drops its rule | | front matter and the first section | the band, both rules | the contents grid draws no rule against it; the first section's divider draws no rule | | two sections | the lower section's divider rule | the upper section draws no bottom rule | | tile and its shot | the tile's frame | the shot draws no border | | segmented control and its options | the control's outer border | options draw no dividers; the active option is an ink pill 2px inside it with the chip corner, and the hover and the focus ring trace the same pill | | stacked corner buttons | the upper button's bottom edge | the lower button's top edge is transparent at rest | | sidebar head and toolbar | each owns its own side of the vertical seam | the two bottom rules meet at the sidebar's edge and never overlap | The auditor enforces this from computed CSS. `pnpm lint:lines:shell` walks `/`, `/brand`, `/compare`, `/archive/`, `/directions/`, `/skills`, `/skills/`, `/motion`, `/motion/`, `/graphics`, `/marks`, `/d/production` and `/deck` (the deck's own document) at 1440, 1280 and 390 in both themes against the dev server on port 3005, with the list toggled, the index panel open, the search open, and the grid and book modes on `/` and `/deck`. It fails on any doubled line (two owners within 4px), any junction (two owners coincident on one seam), and any border color in chrome outside the three roles. The sidebar's rail is a masked box the walk cannot see, so the auditor reads its vertical runs from its path (`data-rail-path`), holds its ink to the row role, and fails a state audited with the list open in outline density when the rail layer is not live. `pnpm lint:all` runs it. The allow list in `scripts/lint/lines.mjs` names every sanctioned multi-stroke device with its reason inline; for chrome those are the fixed sheet's ring (`sheet`: a hair border, a paper gap, a hair-soft outline, on /compare, in slide mode and around a direction's live page), the active frames (`thumb-frame`, `page-frame`, the sidebar's site tiles as `pt-tile`) and the hover preview's mat (`pt-preview`). Scroll regions belong to the same law: one thin scrollbar everywhere in chrome, a 4px gutter with no track rule and a 2px thumb in `--pt-thumb` that widens to 4px on hover, owned by `.pt-scroll` in `src/components/viewer/tokens.css` and mirrored by the deck's `.scroll` rule. No scroll region draws a rule beside its thumb. ### Corners: rounded controls, square shells [#corners-rounded-controls-square-shells] Kevin's toolbar is the reference (2026-10-05): the search pill and the segmented control round at 6px, the key chip at 4px. Everything a reader presses, types into, picks up or looks at as an object is rounded; the surfaces that hold the interface are square. `src/components/viewer/tokens.css` holds the corners as six tokens, and nothing in the shell or the pages writes a radius any other way. | class | token | value | members | | ------- | --------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | shell | `--pt-radius-shell` | 0 | the viewer frame, the sidebar column, the toolbar bar, the fixed sheet and its ring, the reading column, the index panel's column, the overlay layers and scrims, the progress track, scroll thumbs, the book head, the contents, the dividers and the bands; full-width list rows, which own their seams and never draw a box; an input inside its field | | control | `--pt-radius-control` | 6px | every button (`.pt-ib`), segmented groups, fields and field-shaped buttons (the search pill, the count, the filters, the palette's field, the install field), buttons on pages, the sidebar's pills, which are grounds the pointer is on | | inner | `--pt-radius-inner` | 5px | a part flush inside a control's 1px border: the install field's copy segment | | chip | `--pt-radius-chip` | 4px | key caps, chips, pills, tags, inline code, a segmented control's active and hover pill (2px inside its option), and a control set 2 to 7px inside a field | | card | `--pt-radius-card` | 6px | cards, tiles, thumbnails, figures, and popovers: the palette, the help card, the toast, the hover preview | | round | `--pt-radius-round` | 50% | dots and avatars on a square box | * A part n px inside a rounded box (n from 1 to 3) takes `calc(var(--pt-radius-) - npx)`. * A picture in a rounded frame is clipped by the frame or takes the concentric radius. * Named exceptions: Present keeps 8px (section 15); the marks page's app tile keeps the platform's 22% and its browser tab mock stays square, because they are specimens. The /d/ directions, the deck, the presenter, the craft article and the dashboard plate keep their own grammar. * `scripts/lint/radius.mjs` holds the law: statically in `pnpm lint:radius` and the build, and on the rendered pages in `pnpm lint:radius:live`. The corner notches on hero cards are not drawn — they are the ground showing through, so a corner can never disagree with the seam that meets it. ## 3. The rails [#3-the-rails] The page's spine is a ruled column with ONE rail on each side. * The column wrapper draws the pair once, with its own `border-inline`. * A band mounted inside the wrapper draws no side rails of its own: the wrapper already owns them. * A full-bleed band that sits outside the wrapper draws its own inner pair once, at the column edges, from its own `-in` column, in the band's rule ink. It never draws a pair beside the column. * Retired everywhere: the outer pair at ±10px that earlier directions drew from a wrapper pseudo (the "doubled rail"). A second stroke beside a rail is defect class 1 above, and a coincident second stroke stacks alphas and reads as a different color. The doubled LINE of section 5 is a different device: one path stroked twice, a connector, never a page rail. The sidebar's rail (section 16) is a different device: a row-weight line inside the list that marks the place, never a page rail. ## 4. Typography and voices [#4-typography-and-voices] * Three voices: the serif face for brand moments, the grotesk for labels (11px, `letter-spacing: 0.06em`), the mono for numbers and tokens only. * Hairline-boxed code blocks; figcaptions under figures; one type scale per page. Labels never shout; the active element is the only color. ### Book type [#book-type] The shell's type is one family, the rsms InterVariable (v4.1), cut into unicode-range subsets by `scripts/build/subset-inter.py` (`pnpm build:inter`): Google Fonts' latin, latin-ext, vietnamese, greek, greek-ext, cyrillic and cyrillic-ext, plus Latin Extended-A, the private use area and the remaining symbols, each with every feature and both axes. The roman's latin subset comes through next/font, bound as `ptInter` in `src/lib/fonts.ts` so its family name matches no installed Inter, and every route preloads it. The other subsets and the italic are plain `@font-face` rules in the same `ptInter` family in `src/app/inter-subsets.css`, each fetched only where the page draws a code point in its range. Any other `localFont` call sets `preload: false` (`pnpm lint:type`, T3). Every stylesheet reads the type tokens of `src/components/viewer/tokens.css` and declares no family, feature list or display size of its own. The /d/ directions keep their own type. * **Stacks.** `--pt-text` is `var(--font-inter), system-ui, sans-serif`; `--pt-display` reads it. `--pt-mono` is for code, numbers and tokens only. The per-language stacks (`--pt-text-hant`, `-hans`, `-ja`, `-he`) name system faces after Inter. The nameplate's faces are `--pt-face-serif` and `--pt-face-grot`. * **Features.** h1 and h2 (and a display sentence such as the brand plate's `.ptc-every`) take `--pt-ff-display`: the single-storey a (cv11) and the open digits (ss01). Everything else takes `--pt-ff-text`. Both name `'liga' 1, 'calt' 1`, because Chrome turns contextual forms off under any letter-spacing. The base rules on the shell's roots set them; a rule never writes a feature list. * **Weight.** 400 for text, 500 for labels, headings and `strong`. Nothing above 500. * **The display ladder.** Optical size follows the size, so 32px and up is the Display design. | step | role | size / line | tracking | 900px and under | | ---- | ----------------------------------------------- | ----------- | -------- | --------------- | | d1 | page title (the book head's h1) | 44 / 1.04 | -0.025em | 32 | | d2 | section title (a divider's h2, a brand chapter) | 32 / 1.05 | -0.025em | 26 / 1.15 | | d3 | h2 in prose, a record's title | 24 / 1.25 | -0.02em | 21 | * **The text ladder.** Title 18/1.35 at 500 and -0.01em (h3, a row's title); lead 17/1.55 in ink; body 16/1.6 in ink-2; small 14/1.55 (table cells, contents links, ledgers); meta 13/1.5 in titanium (meta tables, gutter notes, captions, th); group labels 12.5 at 500 and -0.005em. Weight 400 text is never tracked, and nothing on Inter is tracked positive. * **Measure.** Body text at `--pt-measure` (32em, about 66 characters); a head's lead at `--pt-measure-lead` (30em). Tables, figures, code and ledgers keep the full column. * **The book page.** Every page with a book head has one structure, rendered by `BookHead` (BookView\.tsx), and differs from another only in its words and its facts: 1. nothing is drawn in the `--pt-title-clear` space between the toolbar's bottom rule and the title (the reading column's top padding), and no line crosses a title; 2. the title, the page's plain name from `src/lib/page-names.ts` (Brand, Documentation, Motion) or a record's own title, at d1 across the mast; on /brand alone (Kevin, 2026-10-21), a badge after the name on the title's line (BookHead's `badge`: a 1px `--pt-hair` box at the card corner, its content at most the cap height tall and set on the baseline, hidden from the accessible name) cuts every 1.4s through the GT monogram and the register's monograms (`src/components/viewer/BadgeCycle.tsx`, steps(n) on one strip, so the count lives in the frames; under reduced motion the first frame stands), and `pnpm lint:heads` fails a badge on any other page; 3. the lead (one to three lines at `--pt-measure-lead`, at most 200 characters, plain declarative sentences) and the panel on one row under the title, both starting on the lead's first line; 4. the panel: Updated first (the day of the last commit that touched the page's own sources, from `src/lib/updated.ts`, linked to its commit, with a relative hint after hydration), then three facts, or one fact and the install field; each row a Heroicons 20 solid glyph in titanium, a label in ink-2 and a value in ink at 500, one lead line tall with its text centered between 2px of air above and below, and a `--pt-hair-soft` rule between rows, none over the first row or under the last; beside the lead the panel starts 2px under the lead's top, so each row's baseline lands on a lead baseline; 5. one `--pt-hair` rule `--pt-head-rule-pad` under the taller of lead and panel, across the stage; 6. the note at the body step, then the contents; 7. one hatch band ruled on both edges and running across the stage, `--pt-sec-over` under the block above it; 8. the sections, each a `section.pt-book-part` opened by a divider: a `--pt-hair` rule (the first section's rule is the band's), `--pt-sec-pad`, a gutter note of two lines in titanium (`Section n`, then one fact) on the h2's last baseline, the h2 at d2. Under 880px of head width the panel stacks under the lead, ruled the same way, and the mast's rule sits `--pt-head-rule-pad` under it. The gallery (`/`) is the site's front page, the nameplate hero and the redesign post in the article's own grammar, and has no book head; `/compare` is a tool and has none. `scripts/lint/heads.mjs` holds the structure, statically and on the rendered pages. * **The gutter.** Sections and rows keep the deck's 128px gutter and 28px gap. A gutter number takes the size, line height and baseline of the title beside it, in titanium (ink while active); under 900px it sits above the title at the meta step. * **The lint.** `scripts/lint/type.mjs` holds all of this: statically in `pnpm lint:type` and the build, and on the rendered pages with `pnpm lint:type:live`. Its allowlist names every exception with its reason: the nameplate, the gallery's grotesk labels, the mono numbers and tokens, the specimens, and other sessions' code. `scripts/lint/heads.mjs` holds the book page (`pnpm lint:heads`, `pnpm lint:heads:live`). ## 5. The doubled line (thread grammar) [#5-the-doubled-line-thread-grammar] The brand's connector: **one SVG path stroked twice** — a full-gauge ink stroke under a narrower surface-colored core (`stroke-width: gap`), carving two parallel hairline threads at a constant gap along any curve. Tokens: `--thread-gauge: 1.5px`, `--thread-gap: 3px`. * All three roles carry `vector-effect: non-scaling-stroke`. * The core's stroke must match the actual surface behind the diagram, or the carve reads as a painted stripe. * Layer order is load-bearing: threads first, pulses next, cores last. Draw a later sandwich over an earlier one and the junction re-carves into one clean pair — merges cost zero parallel-curve math. * The live pulse is a third copy of the same path in accent at full sandwich gauge, between threads and cores — carved into two accent hairlines, never a filled band. The traveling window is **real geometry** (a sub-polyline rewritten per tick), never `stroke-dasharray` — dash distances drift under anisotropic stretch. * **Two-tone** (one white thread, one gray): clip the white copy to a half-plane closed along the same center path — the split seam lies inside the carve. Componentized as `src/components/shared/diagrams/DoubledLine.tsx`. Offset clones collapse on curves; concentric restrokes only make symmetric rings. This plate draws `DoubledLine`. Two source paths merge into one trunk, each path is one stroke carved into two hairlines by the plate's ink, and the trunk carries a static accent window in its pulse slot. ## 6. The isometric family [#6-the-isometric-family] One 30° axonometric map for every iso drawing (`src/app/d/toolchain/diagrams/iso.ts`): `project(x, y, z) = [(x−y)·cos30, (x+y)·sin30 − z]`, camera at (+,+,+). * **Light from the upper left, always**: exactly three faces visible, shaded top 4% / left 9% / right 15% (currentColor mixes; accent faces keep the same order). Reordering is forbidden. * The extrusion recipe: opaque hull from `roundedPolygon(silhouette(...))` (the occluder), then right/left/top face fills, then hairlines — rim, top contour, and the interior front edge(s) the silhouette doesn't draw. * `IsoPrism` extrudes any convex plan polygon under the same visibility and tone law; `plane(z, ox, oy)` seats whole flat drawings into a surface with one matrix; `markPath()` lays rounded bars in a face; brand marks render as alpha masks so the shape takes the surface's ink. * One corner radius family-wide (`ISO_RADIUS = 2.4`); plate thickness ≈4% of footprint, air between stacked plates ≈40%; depth cues live in per-plate stroke alphas, never group opacity; each drawing spends its accent on exactly one element. * The mark shimmer is `DitheredMark` (§7's 1-bit language as a specular band): nested Bayer coverage tiers windowed by pre-rotated 60° clip paths, swept by pure horizontal translate — `rotate()` windows are GSAP-origin fragile. This plate draws with the isometric kit: a slab extruded from its rounded silhouette carries two boxes and one hexagonal prism, every face lit from the upper left. ## 7. The 1-bit language (Bayer dither) [#7-the-1-bit-language-bayer-dither] Density ramps render as ordered dither, never alpha veils. * The 4×4 matrix (`[0,8,2,10 / 12,4,14,6 / 3,11,1,9 / 15,7,13,5]`) and the 8×8 (an exact permutation of 0..63 — 65 tonally linear levels) are the house screens. * Coverage tiers **nest by construction** — every tier's lit cells are a subset of the next — so non-overlapping regions filled with different tiers compose exact ramps, and opaque glint ink means doubled cells never brighten. * Cells are square screen pixels: transforms that would foreshorten them (a plane seat) go **inside** the alpha mask; `crispEdges` keeps cells 1-bit at any zoom; CSS upscales with `image-rendering: pixelated`. * Engines: `src/lib/dither.ts` (CPU, any scalar field, 1 device px per cell) and `src/lib/studio-field.ts` (GPU, the authentic `BAYER_PRESETS` roster of ten variants, one shared GL context, switch by remount). * Artifact pictures (the dithered photographs and scans on the plate's field, the deck's mood slides and the transition demo on /docs/handbook/libraries) follow the Blue Marble standard in `docs/ARTIFACT-PICTURES.md`, held by `scripts/lint/pictures.mjs`. This plate runs `createDitherLoop()`: wavy streak bands pass through the 8x8 Bayer screen, and every dot is one cell thresholded against the matrix. ## 8. The moving type law [#8-the-moving-type-law] * The morphing unit is **one shaped text node** — never per-character spans, which break Arabic joining and Devanagari matras. `lang` + `dir` on the node; `unicode-bidi: isolate` on the container. * Width is measured from a hidden probe carrying the word's own lang/dir — never a sum of per-character boxes — cached, device-pixel snapped, and the container's width is the **only layout-affecting property that animates**, one continuous tween per cycle. Re-measure on resize and `fonts.ready`. * One clock per page: the component never runs a timer; the host drives it (the dossier's locale belt, a craft plate's interval). Engine: `src/components/shared/EverySentence.tsx`. * Locale pills render through one component (`LocaleTag`): flag first (fixed 15×11 SVG print), code in the host's mono on the baseline; the host supplies the box. This plate runs `EverySentence`: one shaped sentence breaks into glyphs and reassembles in the next language, and its width moves once per change, measured from a hidden probe. The chips set the number of re-spread beats from 1 to 5. ## 9. Motion discipline [#9-motion-discipline] * Every loop is created paused; ScrollTrigger (or an IntersectionObserver) plays/pauses it while the section is on screen; multi-phase choreography lives on ONE timeline so phases can never drift. * `prefers-reduced-motion` short-circuits setup entirely; the markup pose IS the still (static accent pair, parked seam cut, single dither frame). * Dash gotchas: normalize `pathLength` to 1000 (GSAP integer-rounds offsets); dashes clip at a subpath's end rather than wrapping closed loops; Chromium computes dash patterns in screen space under non-scaling-stroke and ignores `pathLength` there — rings that dash use raw user units with neither. * Cache a path's source `d` (`el.dataset.traceD`) before an animation blanks it per tick — a re-run would otherwise trace an emptied path. * Leader joints overshoot into opaque hulls so joints are gapless; leaning shapes (scan beams) are re-projected per phase (`beamAt(t)`), never a translated constant. * The sidebar's thumbs and pills move on transform only, by WAAPI, at `--pt-dur-sb` (a pick) and `--pt-dur-toast` (the pointer), eased by `--pt-ease`. Their geometry is read once per layout change (a ResizeObserver per group, a MutationObserver for rows and marks), never per frame and never on hover. Before the shell settles they are placed, not moved. Reduced motion places them at once. ## 10. The seam (slide-to-reveal) [#10-the-seam-slide-to-reveal] Both layers full-width and pinned; the handle writes one CSS var (`--seam-cut`) and the top layer clips to it — content never travels with the handle. State lives in the var: drags cause zero re-renders, GSAP tweens drive the same dial, the keyboard path re-reads the live computed value. The handle is the **dossier refit**: a 64px transparent hitbox, solid two-tone threads with the payload's ink filling the run, and a 16×40 rectangular grip tab — the light surface wears the dark chip, the dark surface the light chip. Component: `src/app/d/toolchain/sections/RevealSeam.tsx` (host CSS supplies the skin). This plate mounts `RevealSeam`. Drag the handle, or focus it and press the arrow keys, to reveal the served translation payload under the rendered string. ## 11. Engine lifecycle [#11-engine-lifecycle] Every canvas/GL engine follows one contract: mount lazily (an IntersectionObserver arms the plate), pause offscreen and on hidden tabs by its own observer, render exactly one still under reduced motion, and `destroy()` releases everything the instance owns (shared GL contexts persist for the session by design). Ink re-resolves on `data-theme` flips. Where an engine has quality tiers, quality follows **measured** frame cost, not just the window: glyph-field's frame-time governor ratchets a slow device down the phone cut's own ladder (lean device-pixel cap, then the lean pool), down only, with pool cuts applied at idle. ## 12. The mobile type ladder [#12-the-mobile-type-ladder] Under 720px, type and spacing are **one token ladder, not per-rule values** — `--tcm-*` slots declared on `.toolchain-root` in the engine's LATE 720px block (`src/app/d/toolchain/styles.css`), consumed by every floor below: | slot | value | | --------------------------------------- | ------------------ | | `--tcm-h2` / `-h2-lh` | 2.25rem / 1.18 | | `--tcm-h3` / `-h3-lh` | 1.375rem / 1.3 | | `--tcm-h4` / `-h4-lh` | 1.125rem / 1.35 | | `--tcm-lead` / `-lead-lh` | 17px / 1.55 | | `--tcm-body` / `-body-lh` | 16px / 1.6 | | `--tcm-small` / `-small-lh` | 14px / 1.55 | | `--tcm-kick` | 13px | | `--tcm-quote` / `-quote-lh` | 22px / 1.4 | | `--tcm-head-pt` / `-head-pb` | 72px / 36px | | `--tcm-gap-h2` / `--tcm-gap-h3` | 18px / 14px | | `--tcm-cell-pad` | 28px | | `--tcm-box-pt` / `-box-pb` / `-box-gap` | 36px / 38px / 28px | * **The px-fallback consumption law**: every consumer reads `var(--tcm-X, )` with the slot's canonical value as the fallback — a rule the ladder doesn't reach (a 721–900px cut, a fork that hasn't declared it) degrades to the same number, never to unstyled. New mobile rules never hardcode a size the ladder has a slot for. * **Unlayered engine beats utilities**: the engine's plain CSS is unlayered and Tailwind utilities live in a `@layer`, so engine floors outrank utility metrics (the box-gap rule beats a head's `max-lg:mb-6`). Heading and paragraph metrics therefore live in engine CSS, never in page utilities. * **The late block**: the ladder and its floors sit in the engine's LAST 720px block because several base rules they override appear after the earlier 720px block — same-specificity declarations only win by following them. New mobile floors go there, not in the early block. * **The sg cut outranks the engine**: `v0-pages.css`'s mobile cut (`.toolchain-root:is(.sgdh-root, …)`) carries higher specificity than any engine floor, so it must consume the same tokens — an engine-only raise silently fails to render on the singularity homes. Change one, change both. ### The box-air standard [#the-box-air-standard] Boxed cells — framed cards, bento plates, stacked dark-band cells — breathe alike on mobile: block padding `--tcm-box-pt`/`--tcm-box-pb` (36/38), and `--tcm-box-gap` (28px) between a box's head and its artifact. The gap is card-scoped (`.tc-card > .shell-cell-head`) on purpose: cells are flex columns, margins never collapse, so an unscoped head margin would stack onto the copy cells' own body margins. The seam floor rides alongside: copy never sits within 20px of a hairline (`--tc-card-pad: 20px`; the trust lead's 20px bottom pad). ## 13. The svh/dvh law [#13-the-svhdvh-law] A mobile stage measures the viewport twice, and the two units never trade jobs: * **Reach is dvh.** Whatever must touch the true screen bottom — the stage's rails, the text zone — sizes from `100dvh`, the real viewport with browser chrome collapse included. * **Layout is svh.** Any height that participates in layout rides `100svh`, the stable viewport: a dvh layout height grows the document on every chrome toggle and jitters the scroll under it. * The difference is a token — the stack stage's `--v0sm-bar-gap` (`dvh − svh`, 0 while the URL bar shows) — and only overhangs (the rails' lower reach) spend it. Anything SIZED from the stage derives from the stable var, or it breathes with the URL bar. ## 14. The two read lines [#14-the-two-read-lines] The stack story runs on two ladders of scroll anchors, measured per beat and re-anchored on every ScrollTrigger refresh (`src/app/d/_v0/sections/FullStack.tsx`): * **The read line (55%) is structural.** A beat LOCKS IN when its COPY BLOCK's center takes the 55% line — the same line the sticky figure's seat centers the tower on, and, for the finale, its pin-engage. The tower's build clock and the capstone scrub both key on it. Anchors are measured on the copy itself, from FLOW geometry (the sticky finale's own rect would read the stuck pose on a mid-dwell refresh) — never on the beat window's top, which runs nearly half a viewport ahead of the copy and fires every arrival early. * **The highlight line (80%) is the spotlight's alone.** A beat's copy lights as its center rises through the 80% line — just after entering the screen, while the arriving layer is still mid-build beside it. One scrubbed dial spans the whole read; a piecewise map (measured lock-ins in, story time out) HOLDS the clock while a row is being read and spends each gap hold → build → lock, so a layer stands locked before its own copy takes the line. ## 15. Chrome exceptions Kevin asked for [#15-chrome-exceptions-kevin-asked-for] The shell's chrome is Inter only, weight 500 or less, corners as section 2 sets them (square shells, 6px controls and cards, 4px chips), one hairline per rule, colors from `src/components/viewer/tokens.css` and borders in the three roles of section 2. Kevin's round six directives override those rules for exactly five elements. Each exception is listed here with its owner file so nothing else reaches for it, and so the next sweep does not "fix" it back. | element | exception | owner | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | Search pill (toolbar) | hover border `--pt-ink-2`, a fourth border color in chrome, reachable only under the pointer (the line auditor never drives hover). At rest the pill stays in the `hair` role, and while its palette is open (`aria-expanded="true"`, a state the auditor does drive) it draws `--pt-ink`, the active-state color every open field takes | `Toolbar.css`, `.pt-toolbar .pt-search-btn` | | Search key chip | `kbd.pt-search-kbd`: ground `--pt-hair-soft`, 11px weight 500 titanium; reads ⌘K, and `Ctrl K` on Windows and Linux (`Search.tsx` swaps the text after mount from the user agent) | `Toolbar.css`, `Search.tsx` | | Present | the one `.is-solid` button: `border-radius: 8px` (the one named corner exception, hatched in lint/radius.mjs), 14px sides, the label first and the 12px play glyph after it (`flex-direction: row-reverse`, so ToolButton keeps one markup), hover drops the ink ground for ink text in the ink frame, the old `.pt-nav-present` from 430e3c7 | `ToolButton.css`, `.pt-ib.is-solid`; `Toolbar.tsx` passes `solid` | | Prototemplate mark | the rainbow core: five chroma stops (`#4b3bff`, `#00b3ff`, `#27d17e`, `#ffc53b`, `#ff3b6b`, display-p3 where supported) declared as `--pt-mark-c1` to `--pt-mark-c5` on `.pt-mark`, the one color in chrome outside the site icons; hovering the head link (or any `a` or `.pt-mark-host` around the mark) fades the hatched paper fill in over the core, opacity only, over `--pt-dur-enter` (200ms, 0 under reduced motion). The markup is the old nav's span with four `i.pt-mark-line` and `i.pt-mark-fill` | `PtMark.tsx`, `PtMark.css` | | Sidebar nameplate | the name beside the mark on every Prototemplate route is `span.pt-brand-word` with `b.pt-face-serif` `proto` in Fraunces 600 and `b.pt-face-grot` `template` in Space Grotesk 500 at 14.5px, two faces outside Inter and a weight above 500; the fonts load from `src/lib/brand-fonts.ts` with their variable classes on the span, so `/skills` and `/brand` carry them too. The deck keeps its Inter title beside the GT mark | `Sidebar.tsx` (head), `Sidebar.css` (head rules) | Everything else in chrome keeps the rules above: every other toolbar button takes the control corner, the palette and the help card take the card corner, the index panel is a shell column and stays square, and no other element carries a chroma or a face outside Inter. ## 16. The sidebar's rows [#16-the-sidebars-rows] Three rules govern the list in column one (`src/components/viewer/Sidebar.tsx`), on every route that shows the site map. **Every row is a link to a page.** A site map row navigates to its route through the router; a row that leaves the site opens a new tab. A route's own item navigates too when its href names a page other than the one the reader is on: a direction row on `/` opens `/directions/`, an archive row `/archive/`, a skill row `/skills/`. The shell selects in place only an item with no address of its own (a slide, a brand section), an item that asks for it (`ShellItem.inPlace`, for a page whose own scroll writes the address), or the item of the page the reader is already on. No row scrolls the gallery. The grid's tiles and the filter's Enter follow the same rule. **A page's sections open under its row.** On the page's own routes, its sections open as a run under the page's row, in the row's group (`ShellSection.under`: `brand`, `atlas`, `marks`, `graphics`, `motion`, `skills`). A run is flush under the row. Each row of the run draws the route's own number in the icon's slot, in the deck's number style, and a name that wraps (a run row to two lines at most, a deep heading to three) and is never cut; a title that is a sentence takes a short name (`ShellItem.short`). When two or more of the page's sections hold more than one row, each gets a label with a chevron and a count inside the run; a section of one row is that row. The headings of the item being read hang under it as a deep run, at 12.5px titanium, ink while read. A chevron button at the right end of the page row folds the run for the visit; the run opens again on the page's next visit. A labelled group opens when the run has 40 rows or fewer, and past 40 only the group holding the active item opens; those folds persist under `gt-shell-groups`. A route section that stands for a site map group (Shipped, Sites, Explorations, Archive) keeps replacing that group. The sidebar is 208px in outline density and 256px with thumbnails, the same two columns as the deck's list. **The rail marks the place.** In outline density each top-level group carries one rail: a 1px `--pt-hair-soft` line drawn by `.pt-sb-rail` (`src/components/viewer/SidebarRails.ts`) and masked to an SVG path down the group's rows. The rail runs in a column 16px before each level's first glyph: x 18, under the group header's chevron, for the tree rows; x 34 for a run's rows and labels, a site's enterprise page and the live site's rows, whose icons and numbers start at 50; x 50 for the deep headings, which start at 66. Where the level changes, the path bends at 45 degrees across one 16px step. The marked row carries a 2px ink thumb on the rail and a `--pt-sb-pill-current` ground with the control corner. The row under the pointer, or under keyboard focus, carries a `--pt-thumb` thumb and a `--pt-plate` ground. The marked row is the deep heading being read when the deep run is shown, else the active item when its row is shown, else the current page's row, else the page row the reader is inside. A plain click moves both marks to the clicked row over `--pt-dur-sb` and turns it ink before the page arrives. When the click opens another route, the new page's list keeps the clicked row where it was and the marks travel from it to that page's mark once the shell settles. Thumbnail density has no rail: its rows lead with their captures, each run keeps its guide and the active row its 2px bar. **The current page is always marked.** The row whose path is the longest one covering the pathname is current, site map row or route item alike: on `/skills/` the skill's row, on `/directions/` the direction's, on `/d/production/enterprise` the Enterprise row and not Home. While no item is marked (a direction page, the gallery at its top) that row carries the rail's thumb and ink text; while an item is, it draws ink text (`.is-current`, `aria-current="page"`), as does the page row the reader is inside (Skills on `/skills/`). Exactly one row is `aria-current="page"`: the current row whose own path is the pathname; a page row the reader is inside is `"true"`. **The list is cheap to change.** Rows are memoized on data built once per route, so a fold renders only the rows it adds or removes, a pick renders the rows whose state changed, and a hover renders nothing in React. Rows are opaque in their first frame; nothing fades in on mount. A row that opens another route prefetches it after the pointer rests on it for 80ms or on keyboard focus, never on pointer down: the click's own navigation follows within milliseconds, and a prefetch there fetches the route twice and renders the arriving page twice. The index panel builds its rows on first open. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Libraries > The index of the componentized instruments, then the build log: every library running live with its plates and API snippets. URL: https://www.prototemplate.com/docs/handbook/libraries The index of the componentized instruments. The living version, with the bodies, live plates and API snippets, is "The libraries" in the build log that follows this index on `/docs/handbook/libraries` (`src/app/craft/libraries.ts`; `/craft` redirects there). Keep the two in step: when an engine gains an option, update its entry in `libraries.ts` in the same round. Once perfected these graduate to their own repos. | library | entry point | one line | | ------------------ | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | horizon-field | `src/lib/horizon-field.ts` | WebGL lensing black hole; handle: setParams / pause / resume / renderStatic / destroy. Draws nothing until given a geometry (center + radius). | | glyph-field | `src/lib/glyph-field.ts` | Canvas glyph rain, 1,280-glyph typed-array pool, Bayer-dithered far tier, matter-conserving morphs. Options: `drift: 'fall'\|'rise'`, `copy: 'auto'\|'left'\|'top'\|'none'`, `glyphScale`. A frame-time governor steps quality down (lean dpr, then the lean pool) when measured cadence proves the device can't keep up — down only, pool cuts at idle. | | prismatic-field | `src/components/shared/PrismaticField.tsx` | The chroma wash behind dark bands; presets `'1'`/`'2'`, `exposureScale` is the dimmer. | | dither | `src/lib/dither.ts` | CPU 1-bit renderer: any `fn(u,v,t)→0..1` through the 8×8 Bayer screen; field factories (radialBurst, globe, streakBands, gradientRamp, makeGlyphField) + combinators. | | studio-field | `src/lib/studio-field.ts` | GPU Bayer family — the codified `BAYER_PRESETS` roster (10 variants, default `BAYER_DEFAULT_ID` = 02 bayer-8x8). Switch by remount; one shared GL context. React wrapper: `src/components/shared/StudioField.tsx`. | | iso | `src/app/d/toolchain/diagrams/iso.ts` | The isometric kit: project/faces/silhouette for boxes, `IsoPrism` for convex plan polygons, `plane()` seats flat art, `markPath()` lays bars. | | DitheredMark | `src/app/d/toolchain/diagrams/DitheredMark.tsx` | A masked brand mark + Bayer specular shimmer; `shineTravel()` gives drivers their tween endpoints. Worn by the tower capstone. | | DoubledLine | `src/components/shared/diagrams/DoubledLine.tsx` | The two-thread stroke as a component; optional two-tone via half-plane clip; pulse slot between threads and core. | | EdgeGlobe | `src/app/d/toolchain/diagrams/EdgeGlobe.tsx` | The delivery globe — ink-only orthographic sphere, five PoPs, one accent route; pairs with a static Bayer atmosphere (dossier `GlobeAtmosphere`). | | LocaleTag | `src/app/d/toolchain/components/LocaleTag.tsx` | The one locale pill: flag print + mono code; hosts supply the box. | | RevealSeam | `src/app/d/toolchain/sections/RevealSeam.tsx` | The slide-to-reveal slider; writes `--seam-cut`; skins are host CSS (the dossier refit is the house handle). | | EverySentence | `src/components/shared/EverySentence.tsx` | The sentence-rewriting glyph reassembler; host-owned clock via `ref.setLocale(loc)` (calls landing before boot are buffered); `hops` sets the re-spread beats, 1–5, default 2 — the dossier hero runs 1. | | TranslateWindow | `src/app/d/_v0/TranslateWindow.tsx` | The dossier hero's windowed demo, extracted whole so any home can mount it — and the home of the **locale belt**: an infinite conveyor of LocaleTag chips whose crossing of the strip zone's centre is the page's one clock (render and payload retype from the same dial). Hover pauses it, any interaction holds it, a click slides the picked chip to centre; `onLocaleChange` vents the active locale to hosts (the dossier headline runs on it). | | V0FullStack | `src/app/d/_v0/sections/FullStack.tsx` | The scroll-scrubbed stack story: beats anchor on the copy block's centre taking the 55% read line, the spotlight fires early at the 80% line, and a piecewise clock holds at each lock-in (`DESIGN.md` §14). The figure is CSS sticky — JS never moves it; the mobile stage runs the svh/dvh law (§13). | | gallery-shoot | `scripts/check/gallery-shoot.mjs` | The anatomy wall's tile factory: section-anchored element shots of the flagship (desk/mob × light/dark), theme pre-set via `localStorage['gt-theme']`, `manifest.json` written beside the tiles; a missed selector is reported, never fatal. Not a mounted engine — a harness. | | mobile type ladder | `src/app/d/toolchain/styles.css` (the late 720px block) | The ≤720px `--tcm-*` token roster every mobile floor consumes as `var(--tcm-X, )`. Not an engine — a contract; full law in `DESIGN.md` §12. | Lifecycle contract for every mounted instrument above: lazy mount behind an IntersectionObserver, self-pause offscreen, one still under `prefers-reduced-motion`, `destroy()` tears down everything the instance owns, ink re-resolves on theme flips. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Graphics pipeline > How the blog illustrations are made with the toolchain in graphics/: captures, labelled crops, the sizing rules, clips, export and hand-off. URL: https://www.prototemplate.com/docs/handbook/graphics How the illustrations for the General Translation blog are made, from the brief to the carousel, with the toolchain that lives in `graphics/`. It produced the thirty-six visuals of "Designing docs for humans" and is the procedure every post after it runs. The `gt-graphics` skill (`skills/gt-graphics`, `/skills/gt-graphics`) carries the same rules into agent sessions; this document is the long form. ## What a visual is [#what-a-visual-is] One HTML page, 1600 by 900, rendered headlessly at 7680 wide and downsampled to 3840. The page holds real screenshots of the product cut into padded crops, labels and leader lines drawn once, and a glyphfield export as the ground. Nothing in it is invented: every rectangle is a measured region of a captured page, every label names something the reader can find in the product. The rules the set settled on, each learned from a review: * No text except labels. No titles, captions or descriptions inside an image; the post carries the prose. * One idea per slide. A carousel is a set of different points; a slide that restates its neighbour gets new content, not a new style. * Real crops, not mockups. The one illustrative mock in the set is labelled as a mock. * Red for what was removed, blue for the page and what replaced it. * No glass panels, no sparkle icons, no over-rounded boxes: the graphics obey the same hit list the post publishes. ## Sizing [#sizing] The article column is about 700 CSS px wide, so a 1600px stage is shown at 0.44x. Everything is sized for that, not for a full-screen viewer: | element | size on the stage | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | any text | 26px or more after zoom-to-fit; `audit.js` fails the run below that | | label chip | 28px Inter 600, 46px tall; tags and badges 30px, 54px and 50px tall | | numbered dot | 56px disc, 28px numeral | | panel header | 32px, icon 34px | | measurement label | 28px Geist Mono on a dark backing pill | | lines and rulers | 3px; ruler caps 18 by 3px | | crops | 1.2x or larger; a sparse composition is zoomed to fill 90% of the frame | | background | drawn at 2x the stage height with its own aspect and square pixels (`image-rendering: pixelated`), so a 1920 export shows as crisp 2px dots and a card is never stretched; dimmed to 36% under article visuals, full strength under covers | | export | 3840 wide webp, quality 95; covers 3840 webp in a dark and a light version; OG 2400 by 1260 PNG; clips as 1400 wide GIFs | | delivery | served through next/image at the column's device width (a dense `deviceSizes` ladder plus an accurate `sizes`), quality 95 | Resolution was never the blur. At 1x the dither's one-pixel dots landed on less than a device pixel and aliased into noise; at 2x with smooth scaling they read as a halftone. Small labels were the other half of it. The floor is measured after zoom-to-fit, not as declared. A composition wider than 90% of the frame is scaled down, and its 28px labels with it, so when the audit reports text under the floor the usual fix is to make the composition narrower (smaller crops, shorter labels, one label column) rather than to enlarge the type. `MIN_TEXT` in `gen-lib.js` holds the floor. ## The files [#the-files] | path | what it is | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `graphics/build/gen-lib.js` | the primitives and the asset maps: `SHOTS` (captures with their device pixel ratio), `BG` (backgrounds), `crop`, `hl`, `tag`, `dot`, `line`, `elbow`, `badge`, `panel`, and `stage`, which wraps a visual with the fonts, the ground and the centering script | | `graphics/build/gen-visuals.js` | the visuals, one `add()` each, the page rectangles (`N` new docs, `O` old docs), `ASSIGN` (which export each visual sits on) and `BG_WASH` | | `graphics/build/render.sh` | shoots every visual, or the ids given, through agent-browser at 7680 and downsamples with Pillow | | `graphics/build/export-blog.py` | writes the webp set and the GIF clips into `public/static/blogs` or `--dest` | | `graphics/build/composite-videos.sh` | lays a recording over a still, reading the crop rectangle from the rendered page | | `graphics/build/capture-sidebar.sh` | the stop-motion capture of the docs sidebar | | `graphics/build/rewriting-covers.py` | both theme covers of the earlier post: keeps the published cover's ground and framing (`graphics/shots/rewriting-cover-ground.png`) and lays the light and dark captures into its page frame at one scale, so a theme switch moves nothing | | `graphics/build/sheet.py` | contact sheets for review | | `graphics/build/manifest.json` | the generated index of visuals: id, area, name, background, why, size, animated; `/graphics` reads it | | `graphics/serve/server.js` | the static server on 127.0.0.1:8765; also accepts POSTs into `inbox/` for the DOM-capture trick | | `graphics/glyph/` | the glyphfield studio scripts and Kevin's project source | | `public/graphics/bg/` | the glyphfield exports, lossless webp identical to the PNG originals: `user/` the palette, `blue/` the blue duotone and light variants; `graphics/bg` links here so the toolchain and the site read one set | | `graphics/shots/hi/` | the product captures at 3 to 5x, lossless webp | | `graphics/fonts/` | Inter and Geist Mono variable | | `graphics/rec/` | the recordings the two clips composite from | Generated folders (`graphics/build/visuals/`, `graphics/out/`, `graphics/rec/stopmo*/`) are ignored by git; everything in them comes back from the scripts. ## Procedure [#procedure] 1. Install once: `pnpm install` at the root (heroicons for the label icons), `pip install pillow`, and `agent-browser` and `ffmpeg` on the PATH. 2. Serve: `pnpm graphics:serve` keeps `graphics/` on port 8765. 3. Capture sources into `graphics/shots/hi/` (see "Capturing") and register them in `SHOTS` with their density. 4. Measure the rectangles you will cut, in CSS px at 1440 by 900, into the page objects in `gen-visuals.js`. 5. Write each visual as one `add(id, area, name, bg, why, () => html)`. Assign it its own export in `ASSIGN`. 6. `pnpm graphics:gen` writes the HTML and the manifest; `pnpm graphics:audit [ids]` measures every text against the floor and lists overlapping boxes (fix the failures, judge the warnings); `pnpm graphics:render [ids]` shoots them; `python3 graphics/build/sheet.py [ids]` tiles them for review. 7. For a clip, record (see "Clips"), then `graphics/build/composite-videos.sh`. 8. `pnpm graphics:export` writes the webp set and GIFs into `public/static/blogs`; `--dest` points at a landing checkout. Bump the `?v=` stamp on every reference in the post. 9. Check the set on `/graphics` and the post on `/blog`. 10. Let the page serve each image at the width it is shown. The masters are 3840px wide and the column is about 700 CSS px, so a browser left to shrink them itself blurs them (measured: about an eighth of the edge contrast on a 2x screen, none for a variant at the device width). Every blog image goes through `next/image` with the column's `sizes` and the dense `images.deviceSizes` ladder in `next.config.ts`, at quality 95; clips are cut at 1400px, the column's 2x width. ## Capturing [#capturing] Sources come from a landing dev server built from `main`, captured with agent-browser at a device pixel ratio of 4 or 5 at 1440 by 900, the Next.js dev portal hidden, the pointer parked on plain text. Themes come from `agent-browser set media light|dark`; the docs follow the preference when no theme is stored. Menus open with their real control. The old docs lived on an SSO-protected Vercel preview; those were captured by serializing the DOM from a signed-in Chrome to the local server and screenshotting the saved page. Convert captures to lossless webp; the library reads webp sizes. ## Backgrounds [#backgrounds] The exports are Kevin's glyphfield project, rendered headlessly through `window.glyphfield.studio` (`graphics/glyph/gfexport.sh`). The generator draws one export per visual at exactly 2x the stage and dims it under article visuals; covers keep the full export. Blue duotone variants carry the covers. Every cover has a light-theme twin (`light: true` on `coverExploded` and `coverWiremap`): the same ground and scale, the docs captured in their light theme, the wireframe plate in paper and ink. The post lists the light cover under `imagesLight` and the blog shows the one for the reader's theme. ## Clips [#clips] The recorder keeps one or two frames per second at 4x, so interactions are captured as stop-motion: pause the Web Animations, step `currentTime` between screenshots, assemble with ffmpeg at variable frame durations. Screenshots re-fire trusted pointer events on the last clicked element, so the capture blocks real pointer events in the region and drives hovers synthetically. The composite reads the crop rectangle from the rendered visual, overlays the clip, and writes a 30fps MP4 master and the GIF the post embeds: 1600 wide, 256 colours, sierra dither, rectangle diffing. ## Handing off to a post [#handing-off-to-a-post] The post embeds the set with the `Carousel` and `HitList` MDX components of the landing blog (children with string attributes; the blog's MDX renderer strips expression props). Covers ship as 3840 webp and the OG image as a 2400 by 1260 PNG; the landing app serves webp covers at quality 95, because dithered artwork smears at the default 75. Posts live in the `generaltranslation/content` submodule, so a change is two pull requests. When the post needs anything from gt-cloud (a new MDX component, images under `public/static/blogs/`, redirects or navigation entries), the gt-cloud pull request merges first and the content pull request second; in the other order the post publishes with missing images or a failed build. The content repository's preview app needs a simple version of every component or its build fails. `gt-website` (`references/blog.md`, read against gt-cloud main on 2026-10-05) holds both rules. This repository keeps the authoritative copy of the three docs-redesign posts under `content/blog` and their graphics under `public/static/blogs`, read by `/blog` and `/graphics`. ## Where it went wrong, and the fix [#where-it-went-wrong-and-the-fix] * A captured Lucide glyph is a `` plus a path (copy, sidebar). The inliner used to strip `width`/`height` from every element, so the rect collapsed and only a corner path drew. It now resizes the root tag only. * With the static server down, `render.sh` shot Chrome's error page and the export shipped it. The render now skips a page that never centres. | symptom | cause | fix | | ----------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------- | | "pixelated" cover | 1x dither aliasing, plus the optimizer re-encoding at quality 75 | 2x smooth background; webp cover at quality 95 | | "blurry" diagrams | 12px labels at 0.44x | labels 26px, lines 3px, zoom-to-fit | | lines clipped | 1px rules and 1.5px caps under a device pixel | 3px everywhere, labels on backing pills | | hover pill jumped after every click in the clip | screenshots re-fired trusted pointer events | block real pointer events; synthetic hovers | | carousel prop `items` undefined | next-mdx-remote strips expression props | child elements with string attributes | | content preview build failed | unknown component | stub every component in `apps/content` | | PR policy failed | `feat` title without a Linear issue | `docs(blog)` title, or link the issue | --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Artifact pictures > The standard for the dithered photographs: the rules, the Blue Marble tone grid and screen, the cutter, adding a picture and its lint. URL: https://www.prototemplate.com/docs/handbook/artifact-pictures An artifact picture is a photograph or scan of an object, artwork or place, screened through the house dither. In this repository they appear in three places: | Surface | Pictures | | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | The plate port's field (`src/components/plate`) | earth, rosetta, calligraphy, tablet, gloss | | The brand deck's mood slides (`deck/`) | earth, rosetta, tablet, calligraphy, lighthouse, devanagari, gloss, cable, wave, compass | | The transition demo on /docs/handbook/libraries (`src/app/craft`) | earth and rosetta, the deck's grids | On the plate port, the onboarding steps show earth, rosetta, calligraphy and tablet, the OAuth consent states show rosetta, the device flow shows calligraphy, and the CLI wizard shows gloss (`gallery/devStates.ts`), as on the dashboard. Procedural fields (the sign-in globe, the ramp), marks, glyph fields, the deck's shader openers and blog covers are not artifact pictures, and these rules do not apply to them. The same standard covers gt-cloud's dashboard sign-in and onboarding field (`.agents/skills/artifact-pictures` there). `standard.json` and `mood_tone.py` are the same files in both repositories. ## Files [#files] | File | Role | | -------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `scripts/media/mood-tone/standard.json` | The standard: screen, tone, file sizes, windows, retired names | | `scripts/media/mood-tone/mood_tone.py` | The cutter (Python 3 with Pillow) | | `scripts/media/mood-tone/mood-tone.mjs` | The recipes for the deck and plate sets, and the wrapper | | `deck/shots/tone/mood-{name}.jpg` | The deck's tone grids | | `deck/shots/tone/manifest.json` | One entry per deck grid, written by the wrapper | | `public/brand/mood/mood-{name}.jpg` | The plate's tone grids, the same bytes as gt-cloud's | | `public/brand/mood/manifest.json` | One entry per plate grid, written by the wrapper | | `public/craft/mood-{earth,rosetta}.jpg` | Byte copies of the deck's earth and rosetta grids for the transition demo on /docs/handbook/libraries | | `src/app/craft/CraftArticle.tsx` | The text around the transition demo, with the demo's credit line | | `src/components/plate/brand/moodPictures.ts` | The plate registry: src, placement and caption per picture | | `deck/slides/NN-mood-{name}.html` | The deck's mood slides, one canvas per grid, with the credit | | `deck/parts/tail.html` | The deck engine that screens the grids | | `scripts/lint/pictures.mjs` | The lint | ## The rules [#the-rules] ### 1. No plain English prose [#1-no-plain-english-prose] A picture may not be a page of readable English: dictionary entries, book pages, documents, signs or captions. That text reads as copy and distracts from the form beside it. Writing is welcome when it is the artistic or design subject of the object: carved inscriptions (the Rosetta Stone), calligraphy, manuscripts in other scripts or old hands (Devanagari, the gloss), signs pressed in clay (the tablet), and the lettering engraved on a historical chart or instrument (the cable chart, the compass rose). Each recipe declares `writing: 'none' | 'artistic'`. The cutter's text-line detector counts rows of six or more letter-sized marks and records the count as `textLines`. A picture with any text lines must declare `'artistic'`. The detector cannot read meaning, so a reviewer checks every `'artistic'` declaration against this rule. Plain English prose never qualifies. ### 2. Sources [#2-sources] * Cut from the original scan or photograph. Never cut from a screened, two-tone or resized copy. * The license is public domain or Creative Commons, taken in this order: public domain and CC0 first (Wikimedia Commons public domain files and the open-access collections of the Met, the Smithsonian, the Cleveland Museum of Art, the Rijksmuseum and the Library of Congress), then CC BY, then CC BY-SA only when nothing else shows the object. A CC BY-SA print is a derived work under the same license, and its credit says so. Non-commercial (NC) and no-derivatives (ND) licenses are refused, so the British Museum's images, which carry a non-commercial license, are refused. * The subject is the original object. A cast, a replica or a modern reprint is refused when the original can be shown. A picture shows no flag, emblem or coat of arms, no map with borders, no caption that states sovereignty, and no private person. The license order and these subject rules come from the artifact pictures of the language map (gt-cloud branch `k/language-map`, 2026-10-03). * The credit is shown with the picture: on the deck slide's plate, in the picture's caption in `moodPictures.ts`, which the plate port's field shows, and in the credit line under the transition demo on /docs/handbook/libraries (`CraftArticle.tsx`). The demo's line carries the deck slide's credit for each picture the demo shows, without the label ("Image:", "Photograph:"). The lint checks the deck slides and the demo's line. * Sources are never committed. Each person keeps a sources directory and passes it to the wrapper. `scripts/media/mood-tone/README.md` lists each file's name, origin, pixel size and sha256, and the wrapper refuses a source whose sha256 differs from `SOURCES` in `mood-tone.mjs`. ### 3. The tone grid is the Blue Marble's [#3-the-tone-grid-is-the-blue-marbles] The Blue Marble on the onboarding field is the reference. Its numbers are in `standard.json` under `tone`. Fixed for every picture: * no blur; * gamma 1.2; * autocontrast that clips 0.5 percent at each tail, over the frame, or over the subject mask for a masked object; * an 8-bit gray JPEG at quality 82, or 75 when 82 is over the cap. Chosen per picture, in the recipe: * the crop; * the channel: `red` (the earth's) or `gray`; * the polarity: `invert` so the subject is lit on a black ground; * the kind: `scene` or `marks`. Solved per picture by the cutter, never set by hand: the black and white points. * A **scene** lands on the Blue Marble's mean tone 0.361 and standard deviation 0.342, over the region of the grid that is shown. * **Marks** (writing, engraving, lines on a plain ground) put the ground at black with an Otsu threshold and the 90th percentile of the marks at white. The solver returns levels 80 and 230 for the plate's earth, which is the earth's own recipe. Sizes: a cover is 1600 by 900, one grid cell per CSS px at a 1440 by 900 view. A disc is 2400 by 1350. The cap is 560 KB (573440 bytes). ### 4. The screen is the Blue Marble's [#4-the-screen-is-the-blue-marbles] `standard.json` under `screen`: * an ordered 8x8 Bayer matrix with threshold `(m + 0.5) / 64`; * cells of 1 CSS px, screened live at display size. Never ship a pre-screened bitmap that is then scaled; * tone floor 10: bytes at or under 10 read as 0; * the loop runs at gamma 1 and bias 0, because the whole curve is in the grid; * dark theme: white cells at opacity 0.62 over the #070707 ground. Light theme: #070707 cells at opacity 0.7 over white. Where each surface states these: | Surface | Cell | Tone floor | Bayer and threshold | Ink and opacity | | ----------------------------- | ---------------------------------------- | -------------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Plate port | `PICTURE_SCALE` in `FieldStack.tsx` | `TONE_FLOOR` in `lib/picture-field.ts` | `BAYER_8` in `lib/dither.ts` | `--tc-picture-ink`, `--field-picture-opacity` in `plate.css` | | Deck | `MOOD_CELL_PX` in `deck/parts/tail.html` | `MOOD_TONE_FLOOR` in `tail.html` | `bayer8` in `tail.html` | `--mood-ink`, `--mood-opacity` in `deck/parts/head.html` | | /docs/handbook/libraries demo | `CELL` in `TransitionDemo.tsx` | `TONE_FLOOR` in `TransitionDemo.tsx` | `BAYER_8` in `src/lib/dither.ts` | `--ptc-picture-ink`, `--ptc-picture-opacity` in `craft.css`, the dark values, because the plate is dark in both themes | The deck engine sizes each mood canvas's backing store to its box on screen (`getBoundingClientRect`, because the stage is CSS-scaled), so a cell is 1 CSS px on the sheet, in a thumbnail, in the book view and on the full-stage backdrop. It reads the grid at cover fit, centred, and draws again on slide activation, resize and a theme flip. ### 5. The windows [#5-the-windows] The cutter measures each grid over the region shown (the deck's full 1600 by 900 sheet; on the plate, the part right of the ramp at 1440 by 900; only the disc or the masked subject where there is one) and records the stats in the manifest. The lint holds them to `standard.json` under `windows`: | Kind | Mean | Std | At white (250 and up) | At the floor (10 and under) | | ----- | ------------ | ------------ | --------------------- | --------------------------- | | scene | 0.30 to 0.42 | 0.28 to 0.44 | at least 2% | at least 5% | | marks | at most 0.35 | any | at least 0.5% | at least 50% (the ground) | ## The cutter [#the-cutter] From the repository root: ```sh pnpm mood-tone [--set deck|plate] [--preview ] [--check] ``` The wrapper sends the recipes and the standard to `mood_tone.py`, which writes `mood-{name}.jpg` for each recipe (the deck set to `deck/shots/tone`, the plate set to `public/brand/mood`) and prints one manifest entry per picture. The wrapper writes those entries to `manifest.json` and prints each picture's levels and stats. With `--preview`, the cutter also saves the 1 px screen of each grid at the view size as `preview-{name}.png`. The cutter is deterministic: a run on the same sources writes the same bytes. A grid's `sha256` ties the file to its manifest entry, and the lint checks that. It does not show that the entry came from the recipe: an entry edited by hand next to an untouched grid passes the lint. Only `--check`, run with the sources, proves that. It cuts into a temporary folder and fails unless every grid and the manifest equal the committed files byte for byte. The lint cannot run it, because the sources are not committed. Never edit a grid or a manifest by hand. A recipe holds only: `name`, `source`, the size and cap from `standard.json`, `crop`, `channel`, `invert`, `kind`, `writing` and `placement`. It holds no tone numbers. The crop kinds are documented above `DECK` in `mood-tone.mjs`. ## Adding a picture [#adding-a-picture] 1. Choose the object against rule 1. Find the original scan or photograph and confirm its license (rule 2). 2. Put the source file in your sources directory, add its origin and sha256 to `SOURCES` in `mood-tone.mjs`, and add its row to the source table in `scripts/media/mood-tone/README.md`. 3. Add a recipe to `DECK` or `PLATE` in `mood-tone.mjs`. Spread `COVER` for a cover. Set `kind: 'marks'` for writing or engraving on a plain ground and `kind: 'scene'` for anything else. Declare `writing`. A deck recipe's placement is a cover at focus 0.5, 0.5. 4. Run the wrapper with `--preview` and look at `preview-{name}.png`. 5. If the subject is not where it should be, or the stats miss the window, change the crop, the channel, the polarity, the kind or the placement and run it again. If no crop puts a scene in its window, the picture does not meet the standard. Do not add tone settings to make it pass. 6. Show it. * On the deck: add `deck/slides/NN-mood-{name}.html` from an existing mood slide, with ``, the plate's title, sentence and credit, then update `SLIDE_COUNT` in `scripts/build/deck.mjs`, `SECTIONS` in `deck/parts/tail.html`, `DECK_SLIDES` in `src/lib/search-index.ts`, and the entry in `deck/shots/OPENERS.md`. Run `pnpm build:deck`. * On the plate port: add the name to `PictureName` and an entry to `MOOD_PICTURES` in `moodPictures.ts`: `src` is `/brand/mood/mood-{name}.jpg`, `placement` equals the recipe's (a disc also carries the manifest's `disc` values `cx`, `cy` and `r`), and the caption has a title, a note of at most 90 characters that says what the object is, and the credit. Make the same change in gt-cloud. 7. Run `node scripts/lint/pictures.mjs` (it also runs before `next build` and in `pnpm lint:all`). 8. Commit the grid, `manifest.json`, the recipe, the README row and the slide or registry entry. If the manifest shows `textLines` above 0, say so in the commit so the reviewer checks rule 1. To remove a picture, delete its recipe, its grid and its slide or registry entry, run the wrapper so the manifest drops it, and move its pages to another picture. ## The lint [#the-lint] `scripts/lint/pictures.mjs` runs before `next build` (`pnpm build`), in `pnpm lint:all`, and on its own with `pnpm lint:pictures`. Its tests are in `scripts/lint/pictures.test.mjs`. `pnpm lint:all` runs them after the lint, and `pnpm test:pictures` runs them alone. It fails when: * a grid file has no manifest entry, or an entry has no grid file; * a grid's sha256 or byte count differs from its entry; * a grid is not an 8-bit gray JPEG of its entry's size; * the plate registry's names, `src` or `placement` differ from its manifest; * a deck mood slide does not name a manifest picture on a canvas, a manifest picture has no slide, a slide shows a bitmap, a pre-screened `mood-*` file sits in `deck/shots`, or `public/brand-deck.html` names grid files under `public/deck-assets` other than the manifest's (run `pnpm build:deck`); * a deck mood slide's plate has no `
`, or the text around the transition demo on /docs/handbook/libraries does not carry the deck credit of each picture the demo shows; * the transition demo's grids are not byte copies of the deck's, or its disc differs from the deck earth's fitted disc; * an entry's blur, gamma, autocontrast cutoff or quality is off the standard; * a grid's size is off its placement's size, or its bytes are over the cap; * an entry's region stats are outside its kind's window; * `writing` is not `'none'` or `'artistic'`; * `textLines` is above 0 and `writing` is not `'artistic'`; * a retired name appears in a manifest, a grid file name, the registry, a `mood-{name}` token under `deck/`, `src/` or the built deck, or a string literal in the picture code; * the screen constants differ from the standard on any of the three surfaces, or in the built deck: the 1 CSS px cell, the loop at gamma 1 and bias 0, tone floor 10, the Bayer matrix and threshold, and the picture ink and opacity per theme. The lint checks the screen constants by declaration. It strips comments first (Python `#` comments and docstrings, HTML ``, block comments and `//` comments), so a value quoted in a comment does not count. Each constant must be declared once and never assigned again. Each ink and opacity token may be set only in its own rules, and every declaration there must carry the standard's value: `.brand-field-stack` (light) and `:root[data-theme='dark'] .brand-field-stack` (dark) in `plate.css`, `:root` (light) and `:root[data-theme="dark"]` (dark) in the deck's sources and the built deck, and `.ptc-plate.is-transition` (dark) in `craft.css`. The lint does not follow how the code uses a constant: a draw call that divides by 2, or a loop that skips the floor, passes. The backstop is to look. Run the wrapper with `--preview` and compare the page with `preview-{name}.png`, the 1 px screen of the grid. ## Changing the standard [#changing-the-standard] `standard.json` is the same file in gt-cloud and Prototemplate. A change to it is made in both repositories in the same round, and every grid in both is cut again with the new standard. ## Retired pictures [#retired-pictures] These names never return as picture names, files or registry keys. They are listed in `standard.json` under `writing.retired`, and the lint rejects them. | Name | What it was | Why it was retired | | ------------- | --------------------------------------------------- | ------------------------------------- | | `dictionary` | A page of the Oxford English Dictionary | Plain English prose distracts as copy | | `johnson` | A page of Johnson's Dictionary | Plain English prose distracts as copy | | `oed-volumes` | The Oxford English Dictionary volumes | The spines carry plain English titles | | `oxford` | Any page or volume of the Oxford English Dictionary | Its pages are plain English prose | | `oed` | The Oxford English Dictionary by its initials | Its pages are plain English prose | Retiring the first two deleted the deck's Design system and Documentation mood slides on 2026-10-05; the deck went from 95 to 93 slides. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Architecture > The code map: the direction registry, the toolchain as the single source and fork rescoping, the componentized instruments, the mirror. URL: https://www.prototemplate.com/docs/handbook/architecture How Prototemplate is put together, and the rules that keep its twenty-seven directions coherent. Read `DESIGN.md` for the visual laws; this file is the code map. ## The shape of the app [#the-shape-of-the-app] ``` src/ app/ page.tsx the index: the design lab (GalleryViewer), the anatomy wall and the capabilities ledger layout.tsx the root layout and the pre-boot theme script d// one route per direction (src/lib/directions.ts) d/toolchain/ THE SSOT: sections, diagrams, styles other forks import d/_v0/ shared v0 sections (TranslateWindow, StackTower, Locadex, FullStack, Deploy) used by the singularity-* homes and the shipped site d/production/ generaltranslation.com as it shipped, rebuilt page for page, with the plate pages directions/ /directions/: each direction as a book page, with its live frame (DirectionFrame) archive/[slug]/ the retired directions, kept as captures (src/lib/archive.ts) compare/ /compare: two directions in synced frames present/ the presenter deck (intro, prototypes, scoreboard) brand/ /brand: the brand book docs/ /docs: the docs site, fumadocs' notebook layout outside the viewer shell; layout.tsx, the one page route [[...slug]] over content/docs (MDX, read through src/lib/source.ts and source.config.ts) and docs.css, the adapters to the site's tokens handbook/ registry.ts, the handbook's registry: its own documents and the canon, which the docs pages under /docs/handbook show md/, llms.txt/, the agent files: the Markdown twins of every llms-full.txt/, docs page, the handbook's included (reached at AGENTS.md/ a page's address plus .md), /llms.txt, /llms-full.txt and /AGENTS.md, generated from the registries api/search/ the docs site's static search index craft/ the build log after docs/LIBRARIES.md on /docs/handbook/libraries: laws, auditors and live library plates (libraries.ts); /craft redirects there skills/ /skills, /skills/ and the raw skill files marks/ /marks: the mark explorations blog/ the docs-redesign posts (content/blog) rendered with the landing site's MDX components graphics/ the illustration catalogue, read from graphics/build/manifest.json motion/ /motion and /motion/, from src/lib/motion.ts api/try/fetch/ the one runtime function, behind the shipped /try prototemplate.css the pt grammar (index + craft chrome) globals.css the four color tokens components/ viewer/ the viewer shell every route except /deck, /present, /blog, /docs and /d runs on: ViewerShell, Sidebar, Toolbar, Sheet, BookView, GridView, IndexPanel, Search, PreviewLayer, tokens.css shared/ cross-page instruments: EverySentence, StudioField, PrismaticField, HeroFieldSwitcher, TcMobileNav, diagrams/ (DoubledLine and the older line-art set); FeatureBento, StorySection and LanguageWheel are unmounted references blog/ the blog's MDX components (PostAuthors, PostCover, Carousel, AuthorSpotlight and the figures) try/ the report card the shipped /try page mounts book/ book.tsx (reads a repository file on the server), model.ts (the handbook's addresses), markdown.tsx and links.ts (the renderer the skills and motion pages use, and where a repository path opens on the site) docs/ the docs site's parts: the sidebar footer, the theme button, the page actions, the sidebar motion copied from gt-cloud, pages.ts (the pages outside the handbook as the hub's registries list them) and demos/, the live plates the Components pages and the handbook mount, each loaded through demos/lazy.tsx blog-globe/ the blog globe figures on /docs/components/blog-globe, each a frame of its round's route under src/app/d/blog-globe shell/ Bento primitives (Rails / BentoRow / BentoCell) plate/ the dashboard's sign-in and onboarding system (frame, field, pages, fixtures) with its state console, served under /d/production/{signin, onboarding,consent,device,cli} lib/ the registries: surfaces.ts, search-index.ts, shell-data.ts, directions.ts (the direction registry), archive.ts, marks.ts, page-names.ts (each page's plain name); the engines: dither.ts, studio-field.ts, glyph-field.ts, horizon-field.ts, prismatic-field.ts; try/ (the report card's checks); and the generated skills.ts, motion.ts and updated.ts (build/updated.mjs; server modules only) with page-updated.ts (its type); the hooks: use-mount-effect.ts and use-layout-work.ts (dependency layout effects with useGSAP's timing, without GSAP) scripts/ pnpm names are the interface; every entry point takes --help lib/root.mjs the repository root every script resolves from lib/site-pages.mjs page discovery, the theme door, PT_BASE and the Chrome the browser scripts share lib/help.mjs --help: prints a script's opening comment build/deck.mjs deck/ to public/brand-deck.html and public/shots/deck (pnpm build:deck); next.config.ts rewrites /deck to the built file build/thumbs.mjs the 640x360 previews in public/shots/thumb, cut from the captures under public/shots build/skills.mjs skills/ to src/lib/skills.ts and skills/README.md, with the skill contract and lint (pnpm build:skills; --check is pnpm lint:skills) build/updated.mjs git to src/lib/updated.ts, the day each book page last changed for its head's Updated row (pnpm build:updated; --check is pnpm lint:updated, in the build), with its tests build/speed-marks.mjs the speed marks under public/marks build/subset-inter.py the Inter roman and italic to unicode-range subsets in public/fonts/inter, their faces in src/app/inter-subsets.css and the latin range in src/lib/fonts.ts (pnpm build:inter) build/motion.mjs motion/ to src/lib/motion.ts and public/motion, run only when the Videos session hands a film over check/capture-pages.mjs the 1440x900 page captures in public/shots/pages that build/thumbs.mjs cuts check/gallery-shoot.mjs the anatomy wall's tiles (see "The gallery pipeline") check/pagecheck/ the page check: every page at ten viewports in both themes, layout shifts, interactions, a report (pnpm check:pages; its README explains) lint/lines.mjs the line auditor (see docs/SHIP-LOOP.md) lint/practices.mjs the practices ratchet (+ baseline JSON) lint/type.mjs the type lint: one Inter through the tokens, static (lint:type, in the build) and --live (lint:type:live), with its ratchet baseline type.baseline.json and its tests lint/radius.mjs the radius lint: rounded controls, square shells, every corner through the six --pt-radius- tokens, static (lint:radius, in the build) and --live (lint:radius:live), with its tests lint/heads.mjs the book page lint: one head, one band, one divider grammar on every book, static (lint:heads, in the build) and --live (lint:heads:live), with its tests lint/pictures.mjs the artifact picture standard (docs/ARTIFACT-PICTURES.md), in the build lint/shell.mjs token discipline for the shell layer lint/registries.mjs registry drift: headings, route lists, first slugs, the skill contract (pnpm lint:registries) lint/oxlint-plugins/ the gt-ui rules pnpm lint:code runs media/mood-tone/ the artifact picture cutter and standard.json skills/install.mjs links or copies skills/ into a project's .claude/skills and .agents/skills (pnpm skills:install; its tests: install.test.mjs, pnpm test:skills) docs/ handbook/ how Kevin runs GT work, served at /docs/handbook: the operating principles, the quality bar, the multi-session playbook, the product map, the glossary and the decisions log, with README.md research/, reference/, reference-shots/, composites/ the exploration archive the skills cite; nothing here is served archive/ finished round specs kept as history, with a README naming each one; nothing here is served deck/ the deck source: parts/, slides/, fonts/, shots/ graphics/ the blog-illustration toolchain (docs/GRAPHICS.md) content/ the three docs-redesign posts and their authors public/ what the site serves: brand-deck.html, shots/ (captures, thumbnails, the gallery tiles), brand/, marks/, media/ and motion/ (films), static/ (blog images and avatars), fonts/, logos/ skills/ the curated skills, one folder per skill (SKILL.md, references/, scripts/); the canonical copy, published on /skills .claude/skills/, .agents/skills/ relative links to skills/, made by pnpm skills:install --project . motion/ the films, untracked, owned by the Videos session AGENTS.md the agent entry point (CLAUDE.md imports it); served at /docs/handbook/agents and raw at /AGENTS.md content/docs/ the docs site's MDX pages, one folder per tab, each with its meta.json: (guide)/ with handbook/ (a page per handbook and canon file), components/ (the live component library), craft/, engineering/ and (internals)/; a folder group (name) adds no URL segment ``` ## The direction registry [#the-direction-registry] `src/lib/directions.ts` is the single source of truth for what exists: twenty-seven directions, four of them full site pairs (`site: true`): **singularity-dossier**, **singularity-orbit**, **singularity-signal** and **production**, the shipped site. singularity-dossier is the completed concept; signal and orbit keep their own heroes and carry the previous-generation sections the dossier retired, as exploration showcases. The index and the sitemap map over `DIRECTIONS`, and the presenter walks its first 16 without Signal (`src/app/present/directions.ts`), so adding or removing a direction there updates all three. When a direction is deleted, also sweep `scripts/lint/practices.baseline.json` for its paths, record it in `src/lib/archive.ts`, and re-check stated counts (index funnel, layout description, craft intro). ## The SSOT rule [#the-ssot-rule] `src/app/d/toolchain/` is the single source of truth for the `tc-*` vocabulary. The fork homes (chroma-flow, dither-field, aurora-paper, glyph-rain, prism-light, lens-gate, paper-foundry, terminus-board, wide-rule, event-horizon, hourglass, singularity and the rest) import toolchain's sections and diagrams directly and re-skin by **root-class rescoping**: each fork carries a root class (`.lensgate-root`, `.terminusboard-root`, and so on) and copies only the CSS it must re-scope. * Never edit toolchain's files as part of fork work; change the SSOT only when the change is meant for every consumer. * The frozen local copies of toolchain sections that aurora-paper and chroma-flow carried were deleted on 2026-10-08, since nothing imported them. Fork copies that remain are imported (`glyph-rain/diagrams/lang/*` among them). Before adding a new copy, import the SSOT and rescope CSS instead. * The singularity-\* homes are toolchain-based (`toolchain-root sgXh-root`) and pull shared v0 sections from `src/app/d/_v0/`; their `/enterprise` pages are singularity-based. Never edit toolchain's TopNav for one site. ## Componentized instruments [#componentized-instruments] The signature pieces live as libraries, outside page code. Each has a live plate and an API snippet in the build log on `/docs/handbook/libraries` (`src/app/craft/libraries.ts`): | module | what it is | | ------------------------------------------------ | --------------------------------------------------------------- | | `src/lib/horizon-field.ts` | WebGL lensing black hole | | `src/lib/glyph-field.ts` | canvas glyph rain (drift, copy modes, matter-conserving morphs) | | `src/components/shared/PrismaticField.tsx` | the chroma wash | | `src/lib/dither.ts` | CPU 1-bit Bayer field renderer + field factories | | `src/lib/studio-field.ts` | GPU Bayer family, the `BAYER_PRESETS` roster | | `src/app/d/toolchain/diagrams/iso.ts` | the isometric kit (boxes, prisms, plane, markPath) | | `src/app/d/toolchain/diagrams/DitheredMark.tsx` | masked logo + Bayer shimmer | | `src/components/shared/diagrams/DoubledLine.tsx` | the two-thread stroke (two-tone capable) | | `src/app/d/toolchain/diagrams/EdgeGlobe.tsx` | the delivery globe | | `src/app/d/toolchain/components/LocaleTag.tsx` | the locale pill | | `src/app/d/toolchain/sections/RevealSeam.tsx` | the slide-to-reveal seam | | `src/components/shared/EverySentence.tsx` | the sentence-rewriting morph | When a page needs one of these behaviors, mount the component and do not re-implement it locally. When an engine gains an option, update its build log entry (body and snippet) in the same round. ## The gallery pipeline [#the-gallery-pipeline] The index's anatomy wall is fed by one shooter, and the file names are the contract between the two ends: * **The shooter** (`scripts/check/gallery-shoot.mjs`) shoots the flagship home section by section across desktop and mobile cuts and both themes. It takes element screenshots anchored on each section's own landmark selector, never scroll depths, so side-by-side pairs align at any viewport. An `addInitScript` writes `localStorage['gt-theme']` before first paint, which the root inline script applies, and one full scroll pass settles every lazy section before the first shot. A selector that misses is reported and skipped. * **The manifest**: the shooter writes `manifest.json` beside the tiles, `{ flagship, generatedFor, sections: [{ key, label, cut, theme, file }] }`. Nothing reads it today; it records what the last run shot. * **The tile names** are what the anatomy wall (`src/app/AnatomyWall.tsx`) reads: `sec---.jpg` under `public/shots/gallery/` (key is hero, customers, story, developer, locadex, context, global, deploy or footer; cut is desk or mob; theme is light or dark). The wall accepts a `.png` under the same stem, and a missing tile leaves hatched ground. The `var-*` tiles still in the folder are the retired directions' last dark captures; nothing renders them. * **`/compare`** puts two directions side by side as synced same-origin iframes. Same origin is what lets the route drive both frames' scroll and theme together. `src/app/SiteCompare.tsx` is the still-image version: two captures under the house seam, the cut held in one CSS variable. ## Skills and docs [#skills-and-docs] * `AGENTS.md`: the entry point for an agent, here or in a project that imported the hub (served at /docs/handbook/agents; `CLAUDE.md` imports it). * `BRAND.md`: the identity canon (the brand book the agency reads; served at /docs/handbook/brand and /brand). * `DESIGN.md`: the visual canon (this repo's law book). * `docs/SHIP-LOOP.md`: the verify/ship procedure every round runs. * `docs/LIBRARIES.md`: the library index (defers to the build log after it on `/docs/handbook/libraries` for depth). * `docs/GRAPHICS.md`: the graphics pipeline: how the blog illustrations are made with `graphics/`, and the rules that came out of review. * `docs/handbook/`: the handbook, what spans the skills (principles, the quality bar, the multi-session playbook, the product map, the glossary, the decisions log). `src/app/handbook/registry.ts` lists it with the canon after it (`AGENTS.md`, `BRAND.md`, `DESIGN.md`, this file, the ship loop, the tools, the libraries, the graphics pipeline and the artifact pictures), and `/docs/handbook` shows each file on a docs page of its own (`content/docs/(guide)/handbook/`, through fumadocs-mdx's include tag; `src/lib/remark-handbook.ts` drops the file's h1 and prints raw HTML as text). A relative link in any shown document resolves the way GitHub resolves it (`src/components/book/links.ts`): a shown document opens its page, a skill its page, any other file GitHub. `/handbook` and every `/handbook/` answer 308 with the page under `/docs/handbook`. * `content/docs/`: the docs site, rendered at `/docs` by fumadocs (`source.config.ts`, `src/lib/source.ts`, `src/app/docs`, `src/components/docs`) in five tabs, each a folder whose `meta.json` sets `"root": true`: the Guide (`(guide)/`, with the handbook), the Components (`components/`, every instrument live with its controls, props and reduced-motion still, and the blog globe figure with all three rounds live), Craft (`craft/`), Engineering (`engineering/`) and Internals (`(internals)/`). A folder named in parentheses groups pages in the sidebar and adds no URL segment. `/docs/template` explains how to lift it into another project and how a page picks its tab and folder. * `src/lib/seo/`: the SEO system. Every route's head comes from one `seoRoute()` call under the policy in `policy.ts`; `/docs/seo` explains it. * `skills//` is the one copy of each curated skill: SKILL.md with its frontmatter contract (name, description with "Use when", metadata title, areas, updated and origin), its references and scripts. `scripts/build/skills.mjs` validates the set and writes `src/lib/skills.ts` and the folder's index, `skills/README.md`; `/skills` and `/skills/` render it, the pages read each body from the folder on the server, and `src/app/skills/[slug]/[...path]/route.ts` serves the raw files. * `scripts/skills/install.mjs` links or copies the set into any project (`--project `, `--user`, `--into `, with `--dry-run`). This repository's `.claude/skills` and `.agents/skills` are its relative links (`--project .`); README.md's Skills section has the rules. ## The mirror [#the-mirror] Prototemplate `main` has been the only copy of this code since 2026-09-08, when gt-cloud's `apps/redesign` stopped being one, so nothing is copied or rsynced into it from another tree (`docs/SHIP-LOOP.md` section 7). --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Ship loop > The verify-and-ship procedure every round runs: the line audit, the page check, the ratchets, types, captures and the gated build. URL: https://www.prototemplate.com/docs/handbook/ship-loop The verify-and-ship procedure every round of work in Prototemplate runs before it lands on `main`. Each check below must pass: the line audit, the page check, the ratchets, the type checker, the captures and the gated build. ## 0. Ground rules [#0-ground-rules] * The dev server runs at `http://localhost:3005` in the shared checkout. A worktree runs its own server on another port, and `PT_BASE` points the live lints, the page check and the captures at it. * A concurrent session may be editing the same worktree. Check `git status` before staging; commit only your own files. Expect the other session to absorb your changes into its commits. When that happens, verify by content, not by diff, and push the backup branch from HEAD. * `pnpm lint:all` may be red on files you don't own; ship anyway when your own diff is clean under the checks below. ## 1. The line audit [#1-the-line-audit] ```bash node scripts/lint/lines.mjs http://localhost:3005/ --theme light node scripts/lint/lines.mjs http://localhost:3005/ --theme dark ``` * Audits at 1440 and 1280; expects **zero** findings in all four classes (doubles, missing, selfStacks, invisibles) in both themes. * The standing battery: `/`, `/skills`, the three singularity homes (`dossier`, `orbit`, `signal`) and every page the round touched. * Deliberate devices live on the ALLOW list inside the script; add an owner there only for a sanctioned device, never to silence a real double. * The auditor reconstructs lines from computed CSS and cannot see SVG strokes. Figures get verified by eye with 2× pixel crops of junctions. ## 2. The page check [#2-the-page-check] ```bash pnpm check:pages --preset quick --pages , # the round's touched pages pnpm check:pages # every page on every device, before a release ``` `scripts/check/pagecheck/` (its README explains the tool) loads every named page on the dev server on the device table in `scripts/lib/site-pages.mjs`: phones from 320x568 to 430x932 and on their sides, tablets from 768x1024 to 1366x1024 in both orientations, desktops from 1280x720 to the 3440x1440 ultrawide, Kevin's 1527x814 laptop and 1440x900 at 200% zoom. A phone or a tablet is read as a touch device whatever its width. The quick preset reads eight devices in dark and 1440x900 in light; the full preset every device in dark and three in light. Each cell is read once the page's own ready signal has fired and its layout holds still: horizontal overflow, boxes past the viewport edge, text clipped mid-word, console errors and failed resources, the theme applied, the tap targets' hit areas on touch devices, the layout shift score, and the site invariants `hooks.mjs` names (the 52px toolbar row, the stage rows, the sidebar column, the deck's sheet, the docs contents grid, the gallery's tiles). The declared interactions run in the same queue: the theme flip, the index panel and the preview, the search, the deck's arrow key, every deck slide in its sheet and the deck's grid and book, a docs contents link, the presenter's dock, and the presenter walked slide by slide on every device (each title visible, the "So I built 12" close beat and the grid at 16 prototypes). It writes `.pagecheck/REPORT.md` with a page by device grid and the run's timing. * Thresholds: a tap target under 40px on a phone is a defect, 40 to 43 a note (44 is the target), a tablet's under 40 a note until Kevin decides on tablet touch sizing; a layout shift score over 0.1 is a defect and over 0.05 a note; an ellipsis truncation is a note, a clip without one a defect. * Each defect row names the file and line to change (the rule declaring the element's class, the line rendering its text, or the line naming a missing file) and a fix written from the element's kind; a folder stands in when no line matched. * Expects **zero** defects and every interaction passing; the run exits 1 otherwise. Notes are read, not fixed on sight. * A round's touched pages run the quick preset before shipping; the whole site runs the full preset before a release. * Captures of the first screen live under `.pagecheck/shots/` (ignored by git), with the full page for a failing cell; `--sheets` adds a contact sheet per device for a quick look at every page. ## 3. The practices ratchet [#3-the-practices-ratchet] `scripts/lint/practices.mjs` counts button types, bare effects, any-types, raw hex in TS/TSX (`'#xxxxxx'`-quoted; unquoted hex inside template CSS snippets doesn't count), and `!important`. It refuses anything that adds to `lint/practices.baseline.json`. When files are deleted, prune their baseline entries in the same commit. `scripts/lint/type.mjs` holds the type to DESIGN.md section 4 ("Book type"): statically on every `pnpm build` and `pnpm lint:type` (family, stack, next/font binding, features, weight, heading and tracking rules, plus a per-file ratchet of literal sizes in `lint/type.baseline.json`), and against the dev server with `pnpm lint:type:live` (the face Chrome rendered, the computed features, tracking, optical size and weight). `pnpm test:type` runs its tests; `--update-baseline` records a burn-down. `scripts/lint/radius.mjs` holds the corners to DESIGN.md section 2 ("Corners: rounded controls, square shells"): statically on every `pnpm build` and `pnpm lint:radius` (every radius reads one of the six `--pt-radius-` tokens, shells and rows stay square, controls are never square, chips read the chip corner, the token values are pinned), and against the dev server with `pnpm lint:radius:live` (the computed corners at 1440 and 390, the overlays on /brand and the toolbar's hover boxes: square shells, round controls, a picture clipped by its frame). A named exception carries `/* lint-radius: allow */`. `pnpm test:radius` runs its tests. `scripts/lint/heads.mjs` holds every book page to DESIGN.md section 4 ("The book page"): statically (`pnpm lint:heads`, in the build: BookHead's props, page titles from `src/lib/page-names.ts`, no route restyling the shared head, band or dividers, no second hatch, no guide over a title, the spaces on their tokens), and against the dev server with `pnpm lint:heads:live` (each head route at 1440 and 390 in both themes: the structure, the title's clearance, the lead, the panel and its Updated day, the mast rule, the band's two rules and the dividers, measured). `pnpm test:heads` runs its tests. ## 4. Types [#4-types] ```bash pnpm exec tsc -p tsconfig.json --noEmit ``` ## 5. Film it [#5-film-it] Screenshot every changed visual with the external harness (the in-app browser pane pauses rAF, so shader canvases come out blank): * Driver: the repository's `playwright-core`, launched against Chrome for Testing: `CHROME_PATH`, else the build playwright-core installs (`pnpm exec playwright-core install chromium`). `pnpm capture:pages` and `pnpm check:pages` launch it the same way. * Dark shots: seed `localStorage['gt-theme'] = 'dark'` in an init script. * Zoom junctions at `deviceScaleFactor: 2`+ and crop, because full-page shots hide 1px defects. * Scroll through the page first so IntersectionObserver-armed plates mount; wait out arm delays before shooting animated engines. ## 6. Commit and back up [#6-commit-and-back-up] * Commit only your files, by explicit path. Commits author as Kevin with his GitHub noreply address (`66856750+Kevin-Liu-01@users.noreply.github.com`) and end with the session's `Co-Authored-By:` trailer (decisions log, 2026-10-10). * Run `pnpm build:updated --staged` after staging the change, and stage `src/lib/updated.ts` with it. `pnpm lint:updated` (in the build and `pnpm lint:all`) fails when a commit touched a book page and the file was not regenerated with it. * Push the round to a branch of this repository from HEAD (`git push origin HEAD:`), so the work survives a wiped scratch folder or a reset checkout. Main takes only work Kevin approved (section 7). ## 7. Push to main [#7-push-to-main] Prototemplate `main` is the only copy of this code. The public site, [www.prototemplate.com](http://www.prototemplate.com), deploys from it through the General Translation team's Vercel project. gt-cloud's `apps/redesign` stopped being a copy on 2026-09-08, so nothing is copied in from another tree, and no `rsync --delete` runs toward this repository: it would erase the routes that exist only here. Kevin decides what lands. Exploration and redesign rounds stay on a branch or on the local dev server until he says to land them (decisions log, 2026-09-14). Approved work lands in this order: ```bash git fetch origin && git log --oneline HEAD..origin/main # other sessions push to main too: take their commits first git grep -n '^<<<<<<< ' -- src docs deck skills scripts # any hit stops the push pnpm build > "$TMPDIR/pt-build.log" 2>&1 && git push origin HEAD:main || tail -40 "$TMPDIR/pt-build.log" ``` * The build gates the push with `&&`. A pipe such as `pnpm build | tail` returns `tail`'s exit code and lets a failed build through, which happened on 2026-08-07 and 2026-08-11. A flaky failure with a clean log gets one re-run before diagnosis. * Commit with explicit paths, never `git add -A` or `git add .`: the shared checkout holds other sessions' files, including the untracked `motion/` folder. * Push with `git push origin HEAD:main` when HEAD is a fast-forward of `origin/main`. Rewriting shared history needs Kevin. * After the push, read the commit's deployment statuses on GitHub and the new build on [www.prototemplate.com](http://www.prototemplate.com). A failed Preview is redeployed, never pushed again with an empty commit. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Tools > Every pnpm command in the repository and the scripts the skills bundle, each with what it does. Written by pnpm build:tools. URL: https://www.prototemplate.com/docs/handbook/tools Every command this repository runs, by `pnpm ` (`pnpm run doctor`, since a bare `pnpm doctor` is a command of pnpm itself), and the scripts the skills bundle. Skills, documents and the `/skills` install line name these commands, never a script path, so a script can move without breaking them. Run any script with `--help` for its full usage. This file is written by `pnpm build:tools` from `package.json` and the opening comment of each file a command runs; `pnpm lint:tools` fails while it is stale. To change a line, edit that comment, or `DESCRIBE` in `scripts/build/tools.mjs` for a command that runs no file of its own. ## Site [#site] | Command | What it does | Runs | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | `pnpm build` | Runs the key-shape scan and the static lints that guard a deploy, checks src/lib/updated.ts, builds the site with next build, then checks every prerendered head with lint:seo --built. | `node scripts/lint/public.mjs --keys && node scripts/lint/...` | | `pnpm dev` | Starts the dev server on port 3005. | `next dev --turbopack --port 3005` | | `pnpm start` | Serves the production build. | `next start` | ## Setup [#setup] | Command | What it does | Runs | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------- | | `pnpm run doctor` | Checks that this machine can run the repository's tools from a fresh clone. | `scripts/doctor.mjs` | | `pnpm postinstall` | Runs after every install: compiles the docs site's MDX collection (source.config.ts, content/docs) into .source/ with fumadocs-mdx, which the type check and the docs routes import. | `fumadocs-mdx` | ## Lints [#lints] | Command | What it does | Runs | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | `pnpm lint:all` | Runs lint:static, then the live lints against PT_BASE (default `http://localhost:3005`). | `pnpm lint:static && pnpm lint:lines:shell && pnpm lint:ty...` | | `pnpm lint:code` | Runs oxlint with the gt-ui plugin (scripts/lint/oxlint-plugins/gt-ui.ts) over the live surfaces under src/. | `oxlint` | | `pnpm lint:copies` | Holds every copy in this repository to its record in scripts/lint/copies.json. | `scripts/lint/copies.mjs` | | `pnpm lint:heads` | Holds every book page to one structure (DESIGN.md section 4, The book page): BookHead renders the front matter (the mast with the title, the lead and the panel, one rule, the note, the contents and the hatch band), every section opens with the shared divider, and every space ... | `scripts/lint/heads.mjs` | | `pnpm lint:heads:live` | Holds every book page to one structure (DESIGN.md section 4, The book page): BookHead renders the front matter (the mast with the title, the lead and the panel, one rule, the note, the contents and the hatch band), every section opens with the shared divider, and every space ... | `scripts/lint/heads.mjs --live` | | `pnpm lint:lines` | Renders the pages it is given and audits the hairlines they draw: doubled lines from two owners, missing seams between sections and two owners on one seam. | `scripts/lint/lines.mjs` | | `pnpm lint:lines:shell` | Audits the shell routes at PT_BASE the same way, and holds every chrome border to its three line tokens. | `scripts/lint/lines.mjs --shell` | | `pnpm lint:pictures` | Holds the artifact pictures to the standard (docs/ARTIFACT-PICTURES.md, scripts/media/mood-tone/standard.json). | `scripts/lint/pictures.mjs` | | `pnpm lint:practices` | Practice lint with a ratchet: every check counts its violations and records their locations in scripts/lint/practices.baseline.json. | `scripts/lint/practices.mjs` | | `pnpm lint:public` | Scans the checkout for what must never be public: key shapes, machine paths and the terms on a private denylist. | `scripts/lint/public.mjs` | | `pnpm lint:radius` | Holds the site's corners to one law: rounded controls, square shells (DESIGN.md section 2, Corners). | `scripts/lint/radius.mjs` | | `pnpm lint:radius:live` | Holds the site's corners to one law: rounded controls, square shells (DESIGN.md section 2, Corners). | `scripts/lint/radius.mjs --live` | | `pnpm lint:registries` | Reports drift between the registries a Prototemplate page, document or skill has to appear in. | `scripts/lint/registries.mjs` | | `pnpm lint:seo` | Holds every route's head to the SEO system: each route gets its title, description, canonical, robots, card and JSON-LD from one call to seoRoute() (src/lib/seo/route.ts), under the policy in src/lib/seo/policy.ts. | `scripts/lint/seo.mjs` | | `pnpm lint:seo:live` | Holds every route's head to the SEO system: each route gets its title, description, canonical, robots, card and JSON-LD from one call to seoRoute() (src/lib/seo/route.ts), under the policy in src/lib/seo/policy.ts. | `scripts/lint/seo.mjs --live` | | `pnpm lint:shell` | Token discipline for the shell layer: no raw color literals, every stroke and fill must draw a token (var(--shell-*), var(--tc-*), or a local custom property defined at a root class). | `scripts/lint/shell.mjs` | | `pnpm lint:skills` | Builds src/lib/skills.ts, the typed registry behind /skills, /skills/\ and the raw files under /skills/\/, from the curated skills in this repository: skills/\/SKILL.md and the supporting files beside it. | `scripts/build/skills.mjs --check` | | `pnpm lint:static` | Runs every lint and test that needs no running server; the gate CI and each lane run. | `pnpm lint:shell && pnpm lint:practices && pnpm lint:type ...` | | `pnpm lint:tools` | Builds docs/TOOLS.md, served at /docs/handbook/tools: every pnpm command in package.json with the first sentence of the opening comment of the file it runs, grouped by its prefix, then every script a skill bundles (skills/\/scripts/) and the environment the tools read. | `scripts/build/tools.mjs --check` | | `pnpm lint:type` | Holds the site's type to one system: the rsms InterVariable through the tokens in src/components/viewer/tokens.css (DESIGN.md section 4, "Book type"; BRAND.md section 6). | `scripts/lint/type.mjs` | | `pnpm lint:type:live` | Holds the site's type to one system: the rsms InterVariable through the tokens in src/components/viewer/tokens.css (DESIGN.md section 4, "Book type"; BRAND.md section 6). | `scripts/lint/type.mjs --live` | | `pnpm lint:updated` | Builds src/lib/updated.ts: the date each page with a book head last changed, shown in the Updated row of the head's panel (DESIGN.md section 4, The book page). | `scripts/build/updated.mjs --check` | ## Tests [#tests] | Command | What it does | Runs | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | `pnpm test:copies` | Tests for scripts/lint/copies.mjs: node --test scripts/lint/copies.test.mjs (pnpm test:copies). | `scripts/lint/copies.test.mjs` | | `pnpm test:heads` | Tests for scripts/lint/heads.mjs: a passing and a failing fixture per static rule, and each live judge fed recorded geometry (the judges are pure functions over what collectHead measures, so no browser runs here). | `scripts/lint/heads.test.mjs` | | `pnpm test:pictures` | Tests for scripts/lint/pictures.mjs: the repository passes, and each kind of defect in a fixture copy of the picture files is reported. | `scripts/lint/pictures.test.mjs` | | `pnpm test:public` | Tests for scripts/lint/public.mjs: node --test scripts/lint/public.test.mjs (pnpm test:public). | `scripts/lint/public.test.mjs` | | `pnpm test:radius` | Tests for scripts/lint/radius.mjs: a passing and a failing string per static rule, the live judges over recorded corners, and the repository. | `scripts/lint/radius.test.mjs` | | `pnpm test:seo` | Tests for scripts/lint/seo.mjs: a passing and a failing fixture per static rule (the passing page fixture is the real src/app/page.tsx), the ratchet, the head and sitemap judges on fixture HTML, the image header reader, and one test that the repository passes. | `scripts/lint/seo.test.mjs scripts/lint/seo-*.test.mjs` | | `pnpm test:skill-scripts` | Runs the offline test of every script a skill bundles: each skills/\/scripts/\.test.mjs with node --test, \.test.py with python3 and \.test.sh with sh, from the checkout root. | `scripts/skills/script-tests.mjs` | | `pnpm test:skills` | Tests for scripts/skills/install.mjs: node --test scripts/skills/install.test.mjs (pnpm test:skills). | `scripts/skills/install.test.mjs scripts/skills/script-tes...` | | `pnpm test:type` | Tests for scripts/lint/type.mjs: the repository passes, and each rule reports its defect in a fixture string and passes the fixed string. | `scripts/lint/type.test.mjs` | | `pnpm test:updated` | Tests for scripts/build/updated.mjs, each in a throwaway git repository with two pages, so nothing here reads or writes the checkout. | `scripts/build/updated.test.mjs` | ## Checks and captures [#checks-and-captures] | Command | What it does | Runs | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | | `pnpm capture:pages` | Captures the first fold of every page the index panel and the sidebar preview (directive 8.6) against the running dev server, in both themes, into public/shots/pages/\-light.jpg and \-dark.jpg, where \ is the surface id in src/lib/surfaces.ts. | `scripts/check/capture-pages.mjs` | | `pnpm check:pages` | The page check: one line of automatic reads and a capture of the first screen per (page, device, theme) cell on the running site, the declared interactions with before and after captures, and REPORT.md with the defects, the notes, a page by device grid, the presenter's and the ... | `scripts/check/pagecheck/pagecheck.mjs` | ## Generators [#generators] | Command | What it does | Runs | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | | `pnpm build:deck` | Builds public/brand-deck.html, the standalone brand deck viewer, from the deck source checked in at deck/. | `scripts/build/deck.mjs` | | `pnpm build:inter` | Split the rsms InterVariable roman and italic into unicode-range subsets, and cut the two static Latin TTF instances the social card route draws with (pnpm build:inter; needs fonttools and brotli). | `scripts/build/subset-inter.py` | | `pnpm build:marks` | Builds the speed marks under public/marks: the race-type set for General Translation (wide letters, a forward slant, one horizontal cut through the letters, speed bars into the first letter), every file one color in currentColor with a tight viewBox, so the marks page inlines ... | `scripts/build/speed-marks.mjs` | | `pnpm build:motion` | Builds src/lib/motion.ts, the typed data behind /motion and /motion/\, and public/motion/\.md, the research package each translation-series page renders, from the motion folder: the roster in motion/MOTION.md (its "## The films" section, one "### \: \ ... | `scripts/build/motion.mjs` | | `pnpm build:skills` | Builds src/lib/skills.ts, the typed registry behind /skills, /skills/\<slug> and the raw files under /skills/\<slug>/, from the curated skills in this repository: skills/\<slug>/SKILL.md and the supporting files beside it. | `scripts/build/skills.mjs` | | `pnpm build:thumbs` | Builds the preview thumbnails the preview layer reads (directive 8.6): public/shots/thumb/\<stem>.webp and \<stem>-dark.webp at 640x360 (the 320x180 card at 2x), cut from the 1440-wide exhibit captures under public/shots. | `scripts/build/thumbs.mjs` | | `pnpm build:tools` | Builds docs/TOOLS.md, served at /docs/handbook/tools: every pnpm command in package.json with the first sentence of the opening comment of the file it runs, grouped by its prefix, then every script a skill bundles (skills/\<slug>/scripts/) and the environment the tools read. | `scripts/build/tools.mjs` | | `pnpm build:updated` | Builds src/lib/updated.ts: the date each page with a book head last changed, shown in the Updated row of the head's panel (DESIGN.md section 4, The book page). | `scripts/build/updated.mjs` | | `pnpm gen:all` | Runs every generator in write mode. | `scripts/build/gen-all.mjs` | | `pnpm mood-tone` | Cuts the artifact pictures' tone grids to the house standard and writes the manifest the lint reads (docs/ARTIFACT-PICTURES.md). | `scripts/media/mood-tone/mood-tone.mjs` | ## Skills [#skills] | Command | What it does | Runs | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | | `pnpm skills:install` | Installs Prototemplate's curated skills (skills/\<slug>/, the canonical copy) into any project or agent home, so Claude Code, Codex and other agents load them there. | `scripts/skills/install.mjs` | | `pnpm skills:stale` | Counts how often each skill loaded in Claude Code sessions on this machine. | `scripts/skills/usage.mjs --stale` | | `pnpm skills:usage` | Counts how often each skill loaded in Claude Code sessions on this machine. | `scripts/skills/usage.mjs` | ## Graphics [#graphics] | Command | What it does | Runs | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | | `pnpm graphics:audit` | Audit the generated visuals in a real browser: every visible text must be at least MIN_TEXT px on the 1600px stage after zoom-to-fit (declared size × fit scale), and content boxes should not overlap. | `graphics/build/audit.js` | | `pnpm graphics:export` | Export the rendered visuals for the gt-cloud blog post. | `graphics/build/export-blog.py` | | `pnpm graphics:gen` | Writes the illustration pages from graphics/build/manifest.json. | `graphics/build/gen-visuals.js` | | `pnpm graphics:render` | Renders the illustrations through agent-browser (docs/GRAPHICS.md). | `graphics/build/render.sh` | | `pnpm graphics:serve` | Serves graphics/ on port 8765 for the illustration toolchain (docs/GRAPHICS.md). | `graphics/serve/server.js graphics` | ## Skill scripts [#skill-scripts] Each skill carries the scripts its procedure calls, self-contained so an installed copy runs on its own. Run one from a checkout with `node skills/<slug>/scripts/<file>`; `pnpm test:skill-scripts` runs their offline tests. | Skill | Script | What it does | | ------------------ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `gt-aesthetic` | `measure-type.mjs` | measure-type.mjs: reads the type of a page's headings and leads the way Kevin reviews it, at each width and theme, and flags what the gt-aesthetic review standard calls a defect. | | `gt-deck` | `check-deck.mjs` | Reads the brand deck's source in a Prototemplate checkout and reports where the slide files and the places that restate them disagree. | | `gt-deck` | `heroicon-symbol.mjs` | Prints the sprite \<symbol> for one or more Heroicons 20 solid glyphs, in the form deck/parts/head.html uses, read from the @heroicons/react package in a Prototemplate checkout. | | `gt-diagrams` | `deck-page.mjs` | Writes the brand deck as one HTML file with its fonts inlined, assembled the way deck/shoot-slide.mjs assembles it (parts/head.html, every slide in order, parts/tail.html, deck/fonts/deck-fonts.css in place of \<!--FONTS-->), so figure-check.mjs can open one slide in present mode ... | | `gt-diagrams` | `figure-check.mjs` | Captures one figure at 2x device pixels in the light and dark themes and reports the SVG facts the line auditor cannot see, because scripts/lint/lines.mjs skips every element inside an svg or a canvas: - labels whose rendered size is under the surface's floor (--min); - labels ... | | `gt-explorations` | `distinct-set.mjs` | distinct-set.mjs: checks that a set of options actually differ before Kevin sees them. | | `gt-films` | `measure-render.mjs` | Measures a General Translation film render against the motion brief's delivery targets (motion/MOTION.md, "Sound" and "What each film delivers"): H.264 at 1920 x 1080, 60 fps for a final (30 for a draft), AAC audio as long as the video, integrated loudness near -16 LUFS and a ... | | `gt-films` | `scan.mjs` | The critic's whole-film scan (skills/gt-films/references/critic.md). | | `gt-orchestration` | `replay-edits.py` | Rebuild files from the Write and Edit calls recorded in agent transcripts. | | `gt-performance` | `frame-probe.mjs` | frame-probe.mjs: measures how smoothly a page animates, the way the gt-performance skill asks for before and after numbers. | | `gt-performance` | `pixel-diff.mjs` | pixel-diff.mjs: proves that a performance change left the picture alone. | | `gt-reporting` | `pr-slate.mjs` | Drafts Kevin's PR slate: every open PR he authored in gt-cloud, gt and content, sorted into the groups the slate uses, with the full link, the purpose (the title without its type), the line counts and the facts that put it in its group. | | `gt-ship` | `contact-sheet.py` | contact-sheet.py: lays labelled screenshots out on one PNG grid, for a review page or a PR comment that has to show many states at once (gt-ship section 4). | | `gt-ship` | `patch-body.py` | patch-body.py: replaces one marked section of a PR body (the screenshots, by default) and leaves every other byte of the body as it was, the bot summaries included (gt-ship section 4). | | `gt-ship` | `pr-assets.sh` | pr-assets.sh: uploads PR screenshots to the repository's pr-assets orphan branch under screenshots/pr-\<n>/ and prints the link for each file, without touching the checkout's index, working tree or branch (gt-ship section 4, references/pr-body.md "Where the images live"). | | `gt-ship` | `pr-bots.mjs` | Reads one PR's review state through the GitHub CLI and reports what still stands between it and a merge: the title policy, the bot summaries and the commit each one reviewed, the unresolved and unanswered review threads, and the checks. | | `gt-ship` | `pr-size.mjs` | Groups a branch's diff by kind so every large group in a PR can be justified before it is pushed. | | `gt-verify` | `compare-signatures.py` | compare-signatures.py: fetches the same routes from two servers and reports every structural difference between the two renders, page by page (gt-verify references/parity-review\.md). | | `gt-verify` | `page-signature.py` | page-signature.py: reads the structural signature of one server-rendered page, the unit a parity review compares (gt-verify references/parity-review\.md). | | `gt-verify` | `probe.mjs` | probe.mjs: measures what renders at one spot of a page, the way a fix is verified before it is reported as done (gt-verify sections 1 and 4). | | `gt-verify` | `row-diff.py` | row-diff.py: compares before and after screenshots of the same pages row by row and writes a side-by-side crop of every changed block (gt-verify references/parity-review\.md). | | `gt-verify` | `stress.mjs` | stress.mjs: walks a scroll story or an animated section through the stress matrix of gt-verify section 3 at several viewports and writes the readings and the captures a reviewer looks at. | | `gt-website` | `routing-matrix.sh` | routing-matrix.sh: checks generaltranslation.com's routing cases (real pages, near-miss corrections, section fallbacks, locale prefixes) by the status and redirect target each path answers, so a routing change is gated before it ships (gt-website section 4). | ## Environment [#environment] Each tool reads its setting from a flag first, then the variable, then the default. No tracked file names a machine path. | Variable | What it sets | Default | | ------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- | | `PT_BASE` | The base URL for the live lints, the page check and the captures; CAPTURE_BASE and REDESIGN_BASE are older names for it. | `http://localhost:3005` | | `CHROME_PATH` | The Chrome for Testing binary the browser tools launch. | playwright-core's `chromium.executablePath()` | | `GT_CLOUD` | A gt-cloud checkout, for the tools that compare with it (lint:copies notes). | none | | `MOTION_DIR` | The motion folder build:motion reads. | `<repo>/motion` | | `PT_DENYLIST` | The private term list lint:public reads; skipped under VERCEL or CI. | none | --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # The component library > Prototemplate's live component library: every instrument mounted on its page with controls, its import path, its props and its reduced-motion still. URL: https://www.prototemplate.com/docs/components These pages mount the real components of the hub: the dithered fields, the light fields, the glyph rain, the doubled line, the isometric kit, the globes, the locale chips, the moving text, the marks and the heads. Every plate is the component itself, running, with controls for its props. Each section gives the import path, the props, when to use the component and what it does under reduced motion. The plate below is the studio field, the dithered material of the landing hero, with its ten presets. <StudioFieldDemo /> ## The pages [#the-pages] The pages sit in ten groups, as the sidebar shows them: the instruments first (fields, the dither lab, drawings, 2D and 3D), then the pieces a reader handles (interactables, language and motion, marks and heads), then what the system makes and where it puts it (artifacts, page layouts, pages). Each card names the components its page mounts. ### Fields [#fields] <Cards> <Card title="Dither and the studio field" href="/docs/components/dither" description="The ordered Bayer screens, the CPU dither loop and its field factories, the ten studio field presets." /> <Card title="Horizon and prismatic fields" href="/docs/components/fields" description="The event-horizon shader and the prismatic burst, with their cursor effects." /> <Card title="Glyph field and ink field" href="/docs/components/glyph-field" description="The eight-script glyph rain, the ink field and its pointer play." /> </Cards> ### Dither lab [#dither-lab] <Cards> <Card title="The dither lab" href="/docs/components/dither-lab" description="Ordered dither, error diffusion and halftone side by side on pictures, gradients and noise, or a picture you drop in." /> <Card title="Dithered type and marks" href="/docs/components/dither-type" description="Words and marks set in dither cells, a mark that erodes and resolves, and a screen of characters." /> <Card title="Dithered transitions and brushes" href="/docs/components/dither-motion" description="Tone-mix wipes on one grid, a lens under the pointer and a brush that paints in cells." /> </Cards> ### Drawings [#drawings] <Cards> <Card title="Doubled lines and connectors" href="/docs/components/lines" description="DoubledLine with its pulse, and TranslationFlow, the connector in production." /> <Card title="Isometric drawings" href="/docs/components/iso" description="The isometric kit: boxes, prisms, faces and a seated mark, with the numbers that make separate drawings one set." /> <Card title="Globes and maps" href="/docs/components/globes" description="EdgeGlobe, the dithered globe field, the blog globe figures in all three rounds." /> <Card title="The blog globe figure" href="/docs/components/blog-globe" description="Four directions for a blog figure that turns a globe along the path of Euclid's Elements, with all three rounds live and their ranking." /> </Cards> ### 2D and 3D [#2d-and-3d] <Cards> <Card title="3D scenes" href="/docs/components/three-scenes" description="three.js scenes through a Bayer post pass: an extruded GT mark, a lit globe and an isometric stack." /> <Card title="2D canvas pieces" href="/docs/components/canvas-2d" description="Particle fields, generative lines and glyph rain on canvas 2D, each with its frame budget." /> </Cards> ### Interactables [#interactables] <Cards> <Card title="Controls" href="/docs/components/controls" description="Segmented switches, toggles, sliders, steppers, the install field and copy buttons, with every state." /> <Card title="Search, filters and menus" href="/docs/components/navigation" description="The command search, the list filter, the language selector, the help card and the toast." /> <Card title="Kinetic text and carousels" href="/docs/components/kinetic" description="Split-flap text, the language wheel and the carousel, with their timing and stills." /> </Cards> ### Language and motion [#language-and-motion] <Cards> <Card title="Locale tags and language pieces" href="/docs/components/language" description="LocaleTag against LocaleFlag, and the seven language diagrams." /> <Card title="Reveal seam and sentence motion" href="/docs/components/motion" description="RevealSeam and EverySentence." /> </Cards> ### Marks and heads [#marks-and-heads] <Cards> <Card title="Speed marks and the GT mark" href="/docs/components/marks" description="The speed marks and the globe concepts, GtWord and gtText(), DitheredMark." /> <Card title="Badges and heads" href="/docs/components/heads" description="BadgeCycle, badgeMark() and BookHead." /> </Cards> ### Artifacts [#artifacts] <Cards> <Card title="Live artifact pictures" href="/docs/components/artifact-pictures" description="Dithered photographs under the Blue Marble standard, screened live from their tone grids." /> <Card title="Covers and social cards" href="/docs/components/covers-and-cards" description="Post covers, social cards and banners generated from a title and a field at their export sizes." /> <Card title="Films and contact sheets" href="/docs/components/films-and-sheets" description="The film player, scrubbable contact sheets and the report card." /> </Cards> ### Page layouts [#page-layouts] <Cards> <Card title="Rails, bands and spacers" href="/docs/components/rails-and-bands" description="The landing grammar as parts: a page skeleton built from bands at any width from 390 to 1440." /> <Card title="Grids and bentos" href="/docs/components/grids-and-bentos" description="Bento and feature grids with columns, gaps and spans to adjust, lines drawn once at each junction." /> <Card title="Shells and frames" href="/docs/components/shells" description="The viewer shell, the sheet frame and the plate, taken apart region by region." /> </Cards> ### Pages [#pages] <Cards> <Card title="Page gallery" href="/docs/components/page-gallery" description="Every shipped page live in a frame at the desktop and the phone width, in both themes." /> <Card title="Page templates" href="/docs/components/page-templates" description="The book page, the docs page, the article and the error page, each with an anatomy overlay." /> </Cards> ## Every component [#every-component] | Component | File | Client | Page | | ------------------------------------------ | ------------------------------------------------------------------------ | --------------------- | ------------------------------------------- | | `createDitherLoop` and the field factories | `src/lib/dither.ts` | yes | [Dither](https://www.prototemplate.com/docs/components/dither) | | `BAYER_8`, `BAYER4` | `src/lib/dither.ts`, `src/app/d/toolchain/diagrams/DitheredMark.tsx` | no | [Dither](https://www.prototemplate.com/docs/components/dither) | | `StudioField`, `createStudioField` | `src/components/shared/StudioField.tsx`, `src/lib/studio-field.ts` | yes | [Dither](https://www.prototemplate.com/docs/components/dither) | | `createHorizonField` | `src/lib/horizon-field.ts` | yes | [Fields](https://www.prototemplate.com/docs/components/fields) | | `PrismaticField`, `createPrismaticField` | `src/components/shared/PrismaticField.tsx`, `src/lib/prismatic-field.ts` | yes | [Fields](https://www.prototemplate.com/docs/components/fields) | | `createGlyphField` | `src/lib/glyph-field.ts` | yes | [Glyph field](https://www.prototemplate.com/docs/components/glyph-field) | | `createInkField` | `src/app/d/glyph-rain/sections/band/inkField.ts` | yes | [Glyph field](https://www.prototemplate.com/docs/components/glyph-field) | | `DoubledLine` | `src/components/shared/diagrams/DoubledLine.tsx` | no | [Lines](https://www.prototemplate.com/docs/components/lines) | | `TranslationFlow` | `src/app/d/toolchain/diagrams/TranslationFlow.tsx` | yes | [Lines](https://www.prototemplate.com/docs/components/lines) | | The isometric kit, `IsoFrame` | `src/app/d/toolchain/diagrams/iso.ts`, `IsoFrame.tsx` | no | [Isometric](https://www.prototemplate.com/docs/components/iso) | | `EdgeGlobe` | `src/app/d/toolchain/diagrams/EdgeGlobe.tsx` | yes | [Globes](https://www.prototemplate.com/docs/components/globes) | | `globe()` | `src/lib/dither.ts` | yes | [Globes](https://www.prototemplate.com/docs/components/globes) | | `GlobeFrame` | `src/components/blog-globe/GlobeFrame.tsx` | yes | [Globes](https://www.prototemplate.com/docs/components/globes) | | `LocaleTag` | `src/app/d/toolchain/components/LocaleTag.tsx` | no | [Language](https://www.prototemplate.com/docs/components/language) | | `LocaleFlag` | `src/components/shared/LocaleFlag.tsx` | no | [Language](https://www.prototemplate.com/docs/components/language) | | The language diagrams | `src/app/d/toolchain/diagrams/lang/` | yes | [Language](https://www.prototemplate.com/docs/components/language) | | `RevealSeam` | `src/app/d/toolchain/sections/RevealSeam.tsx` | yes | [Motion](https://www.prototemplate.com/docs/components/motion) | | `EverySentence` | `src/components/shared/EverySentence.tsx` | yes | [Motion](https://www.prototemplate.com/docs/components/motion) | | `MARKS`, `GLOBE_CONCEPTS` | `src/lib/marks.ts`, `public/marks/` | no | [Marks](https://www.prototemplate.com/docs/components/marks) | | `GtWord`, `gtText`, `GtMark` | `src/components/viewer/GtWord.tsx`, `src/components/viewer/GtMark.tsx` | no | [Marks](https://www.prototemplate.com/docs/components/marks) | | `DitheredMark` | `src/app/d/toolchain/diagrams/DitheredMark.tsx` | no, the host animates | [Marks](https://www.prototemplate.com/docs/components/marks) | | `BadgeCycle`, `badgeMark` | `src/components/viewer/BadgeCycle.tsx` | no | [Heads](https://www.prototemplate.com/docs/components/heads) | | `BookHead` | `src/components/viewer/BookView.tsx` | yes | [Heads](https://www.prototemplate.com/docs/components/heads) | Client means the module is a `'use client'` component or a canvas engine that needs the DOM. The rest render on the server and ship no JavaScript of their own. Several components live in `src/app/d/toolchain`, the direction that the later directions fork. The radius, type and oxlint gates skip that folder, and the site imports the components from there. ## The lifecycle every instrument keeps [#the-lifecycle-every-instrument-keeps] 1. **Mount late.** A canvas or WebGL engine starts when its plate comes within 180 pixels of the viewport, through one IntersectionObserver that disconnects after it fires. 2. **Pause when unseen.** From then on the engine pauses itself when it leaves the viewport and when the tab is hidden. 3. **Draw one still under reduced motion.** With `prefers-reduced-motion: reduce` an engine draws one designed frame and schedules no animation frames. A Still control on a plate shows that frame without changing the system setting. 4. **Release on unmount.** `destroy()` disconnects every observer and listener. The WebGL contexts of the studio, horizon and prismatic fields persist for the session, one per engine, because a context per mount exhausts the browser's budget. 5. **Follow the theme.** The theme is `html[data-theme]`. An engine reads its ink from a custom property and reads it again when the attribute changes; an SVG drawing reads `currentColor` and the tokens. The fields tuned to an ink ground, the studio, horizon, prismatic and ink fields, sit on an ink plate in both themes. Everything else follows the docs theme. ### The numbers behind it [#the-numbers-behind-it] The browser keeps a fixed budget of live WebGL contexts, and a page that runs past it gets a black canvas and no error in the console. The lifecycle above keeps every page inside that budget, and these numbers set it. | Number | Value | What it decides | Where it is held | | ----------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Live WebGL contexts in Chrome | about 16 | Past the budget `getContext` returns null and a field draws black. On 2026-07-29 a direction's shader rendered on a fresh load and was black after 12 client-side switches between directions. | One module-level engine per engine type, each drawing into one offscreen GL canvas and blitting into every mount's 2D canvas: `src/lib/studio-field.ts`, `horizon-field.ts`, `prismatic-field.ts` | | Contexts per engine module | 1 for the session | The context survives a mount's `destroy()`, so route changes never create another. | The same three modules | | Contexts a probe may create | 0 | A probe that calls `getContext` on the page's canvases to count them creates contexts and exhausts the budget it measures. | Wrap `getContext` before the page's scripts run, or screenshot after real navigation | | Contexts a component may leak | 0 | A component that owns its context deletes its buffer, its program and both shaders, then calls `WEBGL_lose_context.loseContext()` on unmount. A footer mark that skipped this leaked one context per mount (2026-08-08). | The component's own cleanup | | Arm margin | 180 px | A lazy plate mounts its engine when it first comes within 180 px of the viewport. A plate the reader never scrolls near never starts its engine. | `rootMargin: '180px'` in `src/components/docs/demos/hooks.ts` | | Live previews on one page | stills until hover | 22 live iframes running shaders made the presenter wall lag (2026-07-31). A gallery that froze each frame 2.8 s after load still held 12 live pages, 4 WebGL contexts and about 1.3 GB of renderer memory, and stayed busy about 950 ms of every second at rest (2026-10-08). A frozen same-origin frame still hydrates, composites and runs its CSS animations on the parent's main thread. | `src/app/present/viewer/LazyFrame.tsx` shows a thumbnail and mounts the live page only while a mouse is over the card | | An off-screen live stage | about 110 ms of every second | The presenter's live prototype stage cost that much through the slides before it, so it is frozen whenever no part of its section is on screen. | The rAF gate in `src/app/layout.tsx`, driven by `postMessage({ type: 'gt:freeze', frozen })` | The rAF gate wraps `requestAnimationFrame` only, so a loop on `setInterval` or `setTimeout` keeps running inside a frozen preview. [Canvas and WebGL engines](https://www.prototemplate.com/docs/engineering/engines#the-context-budget) explains each of these with the shared canvas that only grows, the readback canvases, zoom and the quality governor. ## Mounting a component in a docs page [#mounting-a-component-in-a-docs-page] A docs page imports a component at the top of its MDX file and renders it as JSX. The page and its MDX are server components, so a component with state, a canvas or GSAP must be a `'use client'` module. fumadocs-mdx imports every docs page into the one `/docs/[[...slug]]` route, so a plate imported directly would put its code and its stylesheets into every docs page's first load. The pages therefore import their plates from `src/components/docs/demos/lazy.tsx`, which wraps each one in `next/dynamic`: the server still renders the plate, and its chunk and CSS load only on the page that shows it. ```mdx import { StudioFieldDemo } from '@/components/docs/demos/lazy'; The plate runs the studio field with its ten presets. <StudioFieldDemo /> ``` Put a sentence beside every plate that says what it shows. The Markdown twin of the page and `/llms-full.txt` drop the import and keep the plate as a bare tag, such as `<StudioFieldDemo />`, so the sentence is what a reader of the twin gets. The plates on these pages are built from one small kit in `src/components/docs/demos/`: | Part | What it does | | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Plate` | The card: the stage, the entry point's name in its corner, and the controls row. `night` pins the stage to ink, `lazy` mounts the children when the plate nears the viewport, `fit` takes the content's height, and `moves` adds the reduced-motion note. | | `Choice`, `Range`, `Toggle`, `Action` | The controls: a radio group drawn as a segmented row, a range with its value, a pressed button and a plain button. | | `useArmed`, `useReducedMotion`, `useTheme`, `readToken`, `useReplay` | The lifecycle hooks behind the list above. | | `lazy.tsx` | Every plate wrapped in `next/dynamic`, with a fallback at about the plate's height for a client navigation. A new plate gets its line here. | ## The build log [#the-build-log] The handbook's Libraries document indexes the same instruments and ends with the build log, where every library runs with its plates and API snippets. <Cards> <Card title="Libraries" href="/docs/handbook/libraries" description="The index of the componentized instruments, then the build log: every library running live with its plates and API snippets." /> </Cards> --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Dither and the studio field > The 1-bit material live: the CPU dither loop with its field factories, the Bayer screen's coverage tiers, and the ten studio field presets on WebGL. URL: https://www.prototemplate.com/docs/components/dither General Translation draws its signature texture as 1-bit cells: every cell is either ink or ground, and an ordered Bayer screen decides which. Prototemplate has two engines for it. The CPU dither loop in `src/lib/dither.ts` thresholds any function of position and time on a 2D canvas. The studio field in `src/lib/studio-field.ts` runs ten tuned presets as WebGL shaders on one shared context. The `gt-dither` skill holds the rules for where each one goes. ## The ordered screen [#the-ordered-screen] The plate below shows the threshold matrix, one tile repeated at the chosen coverage, and the ramp of every tier. Move the coverage to light more cells, and switch between the 4 by 4 and the 8 by 8 matrix. <BayerTiersDemo /> A cell is lit when its threshold sits under the coverage. Every lit cell of one tier stays lit in every higher tier, so the tiers nest. That property lets a figure fill neighboring regions with different tiers on one shared grid without drawing any cell twice, which is how the edge globe's halo and the dithered shine are built. ### Import [#import] ```ts import { BAYER_8 } from '@/lib/dither'; // 8 by 8, thresholds 0 to 63 import { BAYER4 } from '@/app/d/toolchain/diagrams/DitheredMark'; // 4 by 4, thresholds 0 to 15 ``` ### Where each screen runs [#where-each-screen-runs] | Screen | Levels | Where it runs | | ------ | ------ | ---------------------------------------------------------------------------------------------------- | | 4 by 4 | 17 | The studio field's `bayer4` and most of its presets, the edge globe's halo, the dithered shine | | 8 by 8 | 65 | The CPU dither loop, the picture cutter, the deck's ramp, studio presets 02 and 10 and `bayerSphere` | | 2 by 2 | 5 | Studio preset 07, `bayer-chunk`, only | Cells are square screen pixels. A canvas engine writes one buffer pixel per cell and lets CSS upscale the buffer with `image-rendering: pixelated`, and an SVG dither sets `shape-rendering: crispEdges`. ## The dither loop [#the-dither-loop] The plate runs `createDitherLoop` on a canvas with five field factories: a radial burst, a lit globe, streak bands, a gradient ramp and a dithered letterform. The cell, bias, gamma, speed and invert controls change the running loop through `setOptions`. A field change hands a new field to the same loop through `setField`, so the canvas is never rebuilt. <DitherLoopDemo /> The ink is the docs theme's ink, read from the stage's `--dmo-ink` token and read again whenever `html[data-theme]` changes. The paper is transparent, so the stage's own ground shows through the unlit cells. ### Import and props [#import-and-props] ```ts import { createDitherLoop, globe, gradientRamp, makeGlyphField, mixFields, multiplyFields, radialBurst, streakBands, } from '@/lib/dither'; const loop = createDitherLoop(canvas, globe({ aspect: 2 }), { scale: 3, ink: 'rgb(7, 7, 7)', paper: 'transparent' }); loop.setOptions({ bias: 0.1 }); loop.destroy(); ``` A field is a function `(u, v, t) => number` that returns 0 to 1 for a position in the box and a time in seconds. The factories take an options object whose fields are all optional, and `multiplyFields`, `maxFields`, `mixFields` and `mapField` combine fields. | Option | Type | Default | What it does | | ------------------- | ---------- | ---------------- | ------------------------------------------------------------------------------ | | `scale` | number | `3` | Width of one cell in CSS pixels. It sets both the look and the cost. | | `ink` | CSS color | `'#fff'` | Color of a lit cell. | | `paper` | CSS color | `'#000'` | Color of an unlit cell. `'transparent'` composites over the page. | | `bias` | number | `0` | Added to the field before thresholding: lighter or darker without a new field. | | `gamma` | number | `1` | Exponent on the field. Above 1 darkens the midtones. | | `invert` | boolean | `false` | Swaps ink and paper. | | `phase` | `{ x, y }` | `{ x: 0, y: 0 }` | Offsets the tile in cells, so two canvases can share one page grid. | | `speed` | number | `1` | Multiplier on elapsed seconds. | | `fps` | number | `30` | Frame budget. The loop skips ticks to hold it. | | `reducedMotionTime` | number | `0` | The time of the one frame drawn under reduced motion. | | `observeResize` | boolean | `true` | Draws again when the canvas resizes. | | `pauseOffscreen` | boolean | `true` | Stops while the canvas is out of view. | The handle returns `render(time)`, `start()`, `stop()`, `running`, `setField(fn)`, `setOptions(patch)` and `destroy()`. ### When to use it [#when-to-use-it] Use the dither loop for a texture whose field is a formula: a globe, a burst, a ramp, a letterform, or a picture cut into a tone grid. It runs on the main thread and its cost grows with the number of cells, so a larger cell or a smaller canvas costs less. Use the studio field instead for a full-bleed hero, where the shader runs on the GPU. ### Reduced motion and themes [#reduced-motion-and-themes] Under `prefers-reduced-motion: reduce` the loop draws one frame at `reducedMotionTime` and never schedules an animation frame. Otherwise it stops when the canvas leaves the viewport or the tab is hidden. The loop does not watch the theme by itself: the host reads its ink token and calls `setOptions({ ink })` when `html[data-theme]` changes, as this plate does. `redrawDithers()` does the same for every `canvas.dither` the brand deck draws. ## The studio field [#the-studio-field] The plate runs `createStudioField` with the ten presets of `BAYER_PRESETS`. Grain, strength, rotation and brightness go to the running engine through `setParams`. A preset or speed change remounts the engine, and Still pauses it and draws its static frame. <StudioFieldDemo /> The presets vary the matrix order (2 by 2, 4 by 4 or 8 by 8), the cell scale, the tone field under the screen, the motion and the balance of ink and blue. Preset 02, `bayer-8x8`, is the default and the material of the landing hero. ### Import and props [#import-and-props-1] ```tsx import StudioField from '@/components/shared/StudioField'; import { BAYER_PRESETS, createStudioField } from '@/lib/studio-field'; <StudioField className='hero-field' preset='bayer8' speed={1} />; const field = createStudioField(canvas, { preset: 'bayerHot', params: { grain: 40 } }); field?.setParams({ rotation: 30 }); field?.destroy(); ``` | Prop | Type | Default | What it does | | ----------- | ----------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `preset` | `StudioPreset` | `'bayer'` | One of the eleven shaders: the ten in `BAYER_PRESETS` and `bayerSphere`. | | `params` | `Partial<StudioParams>` | the preset's | Overrides at mount: `colorA`, `colorB`, `colorC`, `strength`, `detail`, `frequency`, `grain`, `amplitude`, `brightness`, `rotation`. | | `speed` | number | `1` | Multiplier on the shader clock. | | `dpr` | number | `min(devicePixelRatio, 2)` | The device pixel cap. The cells are per device pixel, so the cap stays at 2. | | `className` | string | none | The canvas's class. The caller positions it and keeps a dark plate behind it. | `createStudioField` returns `setParams`, `pause`, `resume`, `renderStatic` and `destroy`, or null when WebGL is missing. The wrapper takes its options once, so a change of preset is a remount with a new `key`. ### When to use it [#when-to-use-it-1] Use the studio field for the one dithered material of a product surface or a hero band, on an ink ground. Its palettes are the house blues over ink, so the plate stays dark in both themes. Use the CPU dither loop instead on paper, or where the field has to follow the page's ink. ### Reduced motion and themes [#reduced-motion-and-themes-1] Under reduced motion the engine draws one static frame and stops. It also pauses when the canvas leaves the viewport or the tab is hidden. Every mount shares one WebGL context for the session, and `destroy()` releases the mount but keeps the context, because creating a context per mount exhausts the browser's budget. ## What a field costs [#what-a-field-costs] The CPU loop calls the field once per cell per frame, so its cost is linear in the number of cells. The cell count grows with the square of the inverse cell size: halving `scale` quadruples the work. These are the field evaluation times per frame at 144k cells, `scale` 3 on a 1440 by 900 box: | Field | Milliseconds a frame | | ---------------- | ------------------------------------------------ | | `gradientRamp` | 0.8 | | `makeGlyphField` | 1.9 | | `globe` | 4.8, or 1.4 with `graticule` and `landmass` at 0 | | `streakBands` | 5.3 | | `radialBurst` | 15.1 | * The same box at `scale` 1 is 1.3 million field calls a frame, so a full-bleed field never animates at scale 1. The artifact pictures run at scale 1 because they are stills between short mixes. * `radialBurst` animates at `scale` 4 or more, which costs about 8 ms, or it renders once. Its dials are `layers` and `warpOctaves`; the globe's are `graticule` and `landmass`. * The loop writes every cell through one reused `ImageData` with a `Uint32Array` view and blits once with `putImageData`. That costs about 1 ms, where a `fillRect` per cell on 144k cells costs tens of milliseconds, so `src/lib/dither.ts` has no per-cell `fillRect`. ## Dither on a moving sprite [#dither-on-a-moving-sprite] A dither pattern on something that moves flickers unless the pattern moves by whole cells, and in practice it flickers even then. The glyph rain first moved its dithered sprites by whole CSS pixels to keep the Bayer pattern rigid, and slow rises stepped at about 15 Hz. Kevin reported them as looking "less than 60fps". The atlas then moved to device-pixel cells blitted on whole device pixels, so each step moved the pattern by whole cells, and the rain still flickered. On 2026-08-06 the dither came off the moving rain. Its depth now reads by glyph size and a quantized alpha (`TIER_COVER` in `src/lib/glyph-field.ts`), and the dither stays for still consumers. A later pass that redrew the rain as two-tone pixel type was reverted. Put dither on a field or a still. A moving sprite carries solid ink. ## Engine copies and canvas traps [#engine-copies-and-canvas-traps] * The two copies of the CPU engine differ. gt-cloud's `packages/ui/src/lib/dither.ts` keeps field time across pauses, so a loop that was off screen resumes where it stopped, and its `stop()` holds the loop so the observers cannot restart it. Prototemplate's `src/lib/dither.ts` restarts field time at 0 on every `start()` and has no hold: its IntersectionObserver and its `visibilitychange` handler restart a stopped loop. Only Prototemplate's copy has the `phase` option. * A canvas that was never drawn keeps its default 300 by 150 backing store, and CSS stretches that store across the box for a frame. `createDitherLoop` paints one frame synchronously before it schedules any `requestAnimationFrame` for this reason. * A cloned canvas carries no bitmap. `redrawDithers()` therefore draws the deck's ramp canvases again from the computed `--pt-paper` and `--pt-ink` tokens, and again on every `html[data-theme]` change. It sizes each canvas from `clientWidth` and `clientHeight`, because a clone inside a scaled thumbnail is laid out at full size and only transformed down. * Two canvases share one page grid only when their phases differ by their offset. A canvas whose top-left cell sits `n` cells below another's takes `phase: { x: 0, y: n % 8 }`, or one tone lights different cells on each side of the seam. [Dither in practice](https://www.prototemplate.com/docs/craft/dither) gives the reasons behind ordered dither, cell sizes, transitions on one grid and the picture standard. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Horizon and prismatic fields > The two light fields on the shared WebGL engine, live: the event-horizon shader and the prismatic burst, with their cursor effects and their stills. URL: https://www.prototemplate.com/docs/components/fields Two more fields draw light instead of dither. The horizon field bends a page's ruled lines and an accretion glow around a circular rim. The prismatic field converges streaks of thin-film spectrum on a dark center. Both run on the same architecture as the studio field: one WebGL context for the session, an offscreen GL canvas, and a blit into each mount's own 2D canvas. Both can opt into cursor effects that bend or requantize the light around the pointer. ## The horizon field [#the-horizon-field] The plate runs `createHorizonField` on an ink stage with white rules. Move the pointer over the stage: with Lens the light bends and swirls around the pointer, with Dither the field is requantized through the 8 by 8 screen around it, and with Redshift the bright flank turns toward it. Radius sizes the horizon, and exposure, chroma, doppler and swirl go to the running engine through `setParams`. <HorizonFieldDemo /> The shader draws nothing until it has a center and a radius in CSS pixels, so the host measures its canvas and passes both, and passes them again on every resize. ### Import and props [#import-and-props] ```ts import { createHorizonField } from '@/lib/horizon-field'; const field = createHorizonField(canvas, { speed: 0.5, params: { ink: [1, 1, 1], exposure: 2.6 }, effects: { host: plate, modes: ['lens', 'dither', 'redshift'] }, }); field?.setParams({ center: [w / 2, h / 2], radius: Math.min(w, h) * 0.3 }); ``` | Param | Default | What it does | | ----------------------------- | ----------------- | -------------------------------------------------------------------------------------------- | | `center`, `radius` | none | The horizon in canvas pixels. Below a radius of 2 the shader draws nothing. | | `exposure` | `2.1` | The tone-map knee. Higher pushes the field back under content. | | `chroma` | `0.55` | The arcs' spectral saturation, from 0 white to 1 the full palette. | | `doppler`, `dopplerAngle` | `0.62`, `2.55` | How much brighter one flank runs, and the direction of that flank in radians. | | `swirl` | `1.15` | The twist of the streaks near the ring. | | `pitch`, `gauge`, `ruleAlpha` | `44`, `1`, `0.07` | The page's ruled lines that the lens bends, matched to the CSS rule layer around the canvas. | | `ink`, `core` | near-black | The rules' ink and the dark core, as RGB triples from 0 to 1. | The handle returns `setParams`, `pause`, `resume`, `renderStatic`, `setEffectMode`, `previewPulse`, `previewRelease` and `destroy`, or null when WebGL is missing. ### When to use it [#when-to-use-it] Use the horizon field in a hero built around one dark disc, where the page's own ruled lines continue into the shader and bend at the rim. It needs the CSS rule layer around it to match its `pitch`, `gauge` and `ruleAlpha`, so it belongs in a hero that draws those rules. ### Reduced motion and themes [#reduced-motion-and-themes] Under reduced motion the engine draws one static frame at an instant chosen mid-arc, and the cursor effects never attach. It pauses offscreen. Its palette is tuned to its own core, so the plate stays dark in both themes. ## The prismatic field [#the-prismatic-field] The plate runs `createPrismaticField`. Preset 1 is a wide horizontal burst and preset 2 an arc over a dark core. Exposure scale is the dimmer, and depth moves the camera through the field. Move the pointer over the stage to see the cursor effect: Lens bends the burst around the pointer, Dither requantizes it through the Bayer screen, and Chroma splits its color. <PrismaticFieldDemo /> The shader is a raw WebGL port of a reference shader with its GLSL kept verbatim. The effects compose with the burst: they bend its input coordinates or requantize its output, and a field with no cursor engaged costs what it always did. ### Import and props [#import-and-props-1] ```tsx import PrismaticField from '@/components/shared/PrismaticField'; <PrismaticField preset='2' params={{ exposureScale: 4200 }} effects={['lens', 'dither', 'chroma']} />; ``` | Prop | Type | Default | What it does | | -------------------------------------------- | -------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `preset` | `'1'` or `'2'` | `'1'` | The wide burst or the arc over a dark core. | | `params` | `Partial<PrismaticParams>` | the preset's | `exposureScale` above all: 4200 or more behind hero content. | | `effects` | modes | none | Opts the field into `'lens'`, `'dither'` and `'chroma'`, with the docked effects menu. | | `defaultEffect`, `effectsMenu`, `persistKey` | | | The mode on mount, the menu's presence and the key the chosen mode is stored under. | | `offThread` | boolean | `false` | Draws the field in a worker, so main-thread work never stalls it. | | `dpr`, `speed`, `paused`, `onDrawn` | | `1`, `1`, `false` | The pixel cap (the soft upscale is part of the look), the clock, a pause for a worker field, and a callback after the first frame. | ### When to use it [#when-to-use-it-1] Use the prismatic field as a band's light behind content, with the exposure scale raised until the dark center frames the content. At the preset values the burst floods the canvas and the dark center disappears. Use the studio field instead where the surface should read as print. ### Reduced motion and themes [#reduced-motion-and-themes-1] Under reduced motion the engine draws one static frame and the cursor effects never attach. Offscreen fields are skipped by the shared engine. The palette is the burst's own spectrum on ink, so the plate stays dark in both themes. ## Traps [#traps] * Setting a canvas's width or height reallocates its drawing buffer. The prismatic engine first resized its one GL canvas to each field in turn, so with a full-bleed field and two 125 px logo fields on the presenter's intro it reallocated twice a frame and kept the main thread busy about 1,000 ms of every second (2026-10-08). The canvas now only grows to the largest field, each field draws into the bottom-left corner, GL's origin, and the blit reads that rectangle: `drawImage(source, 0, source.height - height, width, height, 0, 0, width, height)` in `src/lib/prismatic-field.ts` ([Canvas and WebGL engines](https://www.prototemplate.com/docs/engineering/engines#a-shared-canvas-that-only-grows)). --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Glyph field and ink field > The eight-script glyph rain that condenses into the word for language, and its inverse on ink that bursts where the reader clicks, both live. URL: https://www.prototemplate.com/docs/components/glyph-field Two canvas engines draw language as a material. The glyph field rains letters from eight scripts and condenses them into the word for language in one script after another. The ink field is its inverse for an ink band: paper glyphs rise slowly and keep a measured block of content clear. Both draw every glyph as a pre-measured sprite from one atlas and allocate nothing per frame. ## The glyph field [#the-glyph-field] The plate runs `createGlyphField` with no copy block, so the rain runs edge to edge and the word prints in the middle. Drift turns the fall into a rise, Copy reserves a block on the left or across the top as a hero's text would take, and Glyph scale sizes the rain without changing the printed word. Each option is read once at mount, so every change remounts the engine, and Replay restarts it on the first script. <GlyphFieldDemo /> The status line follows the engine's `onScript` callback, which fires each time the field commits to the next script. The order is Han, Arabic, Devanagari, Cyrillic, Hangul, Greek, Thai and Latin, and each word is a real translation of "language". ### Import and props [#import-and-props] ```tsx import { createGlyphField, SCRIPTS } from '@/lib/glyph-field'; const field = createGlyphField({ canvas, drift: 'rise', copy: 'none', displayFamily: getComputedStyle(canvas).fontFamily, onScript: (index) => console.info(SCRIPTS[index]?.script), }); field?.destroy(); ``` | Option | Type | Default | What it does | | ----------------------------- | --------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `canvas` | `HTMLCanvasElement` | required | The canvas it draws on, sized by CSS. | | `drift` | `'fall'` or `'rise'` | `'fall'` | The rain's direction. The landing's `GlyphRain` runs `'rise'`. | | `copy` | `'auto'`, `'left'`, `'top'` or `'none'` | `'auto'` | Where the host's copy block sits. `'auto'` picks left or top from the width, and `'none'` is a standalone plate. | | `copyBottom` | `() => number` | none | The copy block's bottom edge in canvas pixels, read at every layout, so a long locale never crosses into the rain on the narrow fold. | | `glyphScale` | number | `1` | Multiplier on the rain's glyph sizes. The printed word keeps its size. | | `displayFamily`, `monoFamily` | string | Inter, then the system mono | The faces the canvas draws with, read off the page so the canvas type matches it. | | `onScript` | `(index) => void` | none | Fires when the field commits to a script, as an index into `SCRIPTS`. | The function returns `{ destroy }`, or null when the canvas has no 2D context. ### When to use it [#when-to-use-it] Use the glyph field behind a hero or a closing band whose subject is language, with the copy block reserved through `copy` and `copyBottom`. Its frame-time governor lowers the work per frame on a slow device, and it pauses offscreen, so it can stay mounted for the life of the page. Use the studio field for a texture that carries no words, and the ink field on an ink band. ### Reduced motion and themes [#reduced-motion-and-themes] Under reduced motion the engine draws one frame, the printed word with its caliper, and never starts the loop. It pauses when the canvas leaves the viewport. Its ink is the `--tc-ink` token read off the canvas, and it reads the token again when `html[data-theme]` changes, so the rain turns from ink on paper to white on ink with the page. This plate sets `--tc-ink` from the docs theme's ink. ## The ink field [#the-ink-field] The plate runs `createInkField` in its interactive mode on an ink stage. Glyphs shiver as the pointer comes near, and a click bursts the nearest one, which pushes its neighbors aside before the field heals. Clearing switches between a flooded canvas and one that keeps a text block clear. <InkFieldDemo /> With the measured clearing the engine reads the block's box off the DOM and keeps the glyphs out of it, and the edge of the clearing is a dithered density falloff with no visible box. ### Import and props [#import-and-props-1] ```ts import { createInkField } from '@/app/d/glyph-rain/sections/band/inkField'; const field = createInkField({ canvas, clearEl: copyBlock, interactive: false, fpsCap: 30 }); field?.destroy(); ``` | Option | Type | Default | What it does | | ----------------------- | ------------------------ | ------------ | ------------------------------------------------------------------------------ | | `canvas` | `HTMLCanvasElement` | required | The canvas it draws on. | | `clearing` | `'measured'` or `'none'` | `'measured'` | Keep the content block clear, or flood the canvas. | | `clearEl`, `clearTopEl` | `HTMLElement` | none | The content block, and a head block whose full-width strip also stays clear. | | `interactive` | boolean | `false` | Pointer play: the shiver and the click burst. Ignored under reduced motion. | | `minWidth` | number | none | Below this canvas width the field parks: the loop stops and the canvas clears. | | `dprCap`, `fpsCap` | number | none | The device pixel ceiling and the frame cap. The band mounts pass 1.5 and 30. | | `displayFamily` | string | Inter | The face the glyphs draw with. | ### When to use it [#when-to-use-it-1] Use the ink field on a closing band of ink around a content column, and set `interactive` only on a plate made for play, as this one is. A band's margins are decoration, so pass `minWidth` to park the field on a phone. The glyphs are paper on ink, so the field never goes on a paper ground. ### Reduced motion and themes [#reduced-motion-and-themes-1] Under reduced motion the engine draws one composed frame and the pointer play is off. It pauses when the canvas leaves the viewport. Its glyphs are a fixed paper ink on an ink ground, so its plate stays dark in both themes. ## Traps [#traps] * A canvas created with `willReadFrequently` lives in CPU memory, so every `drawImage` from it uploads to the GPU. The glyph atlas is read back with `getImageData`, and blitting from it directly was the rain's lag on phones, worst on WebKit. Each build now snapshots the finished atlas into an `ImageBitmap` and the frame loop blits from the snapshot (`atlasSrc` in `src/lib/glyph-field.ts`) ([Canvas and WebGL engines](https://www.prototemplate.com/docs/engineering/engines#readback-canvases-and-the-blit-path)). * The rain once picked its quality tier from the window width, so a wide window on weak hardware got the heaviest tier. The governor judges windows of 120 counted frames after a 2.5 s warmup, skips any interval over 900 ms as a hidden tab, and trips when 35% or more of a window's frames exceed 22 ms, a sustained cadence under about 45 fps. Tier 1 lowers the pixel-ratio cap from 2 to 1.25, and tier 2 switches to the lean pool of 560 of the 1,280 glyphs. A first draft watched the median frame time and missed throttled hardware that missed the budget on 34% of its frames, so the governor counts slow frames ([Canvas and WebGL engines](https://www.prototemplate.com/docs/engineering/engines#quality-from-measured-frame-time)). --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # The dither lab > Pictures, gradients and noise through ordered dither, error diffusion and halftone, side by side and live, with every screen and threshold exposed. URL: https://www.prototemplate.com/docs/components/dither-lab A dither prints a continuous tone as cells that are either ink or paper. General Translation draws all of its texture with one kind of dither, an ordered Bayer screen, and these plates set that screen beside the other common kinds on the same pictures. Every plate runs on a 2D canvas in the browser: it samples its source into a grid of tones once, then screens the grid again whenever a control changes. The [dither and studio field](https://www.prototemplate.com/docs/components/dither) page covers the house engines themselves. ## Eight screens on one source [#eight-screens-on-one-source] The bench screens one source eight ways at once and prints, under each tile, the share of the tile's cells that came out as ink. Pick one of the five artifact pictures, the ramp or the noise, or open a picture of your own with Open Picture or by dropping it on the plate. <ScreenBenchDemo /> Threshold, Gamma and Ink act on every tile together. Gamma raises each tone to its power first, and Threshold then adds 0.5 minus its value, so the plain threshold tile cuts at exactly the value shown while the other screens print lighter or darker. Ink switches between the theme's ink and paper, black on white, white on black, and the docs blue on the theme's paper. The status line prints the mean tone after the curve. On all seven sources at a 1440px wide page, the three Bayer screens and Floyd-Steinberg print within 0.3 points of that mean, and white noise and the halftone within 1 point, because each of them keeps the average tone and only moves where the ink falls. Atkinson prints up to 3.2 points under the mean, on the Rosetta Stone, because the quarter of each error it drops never reaches the page. The plain threshold prints every cell above the cut and nothing else, so its share counts the bright cells and ignores how bright they are: 3.0 percent ink on the Rosetta Stone against a mean of 11.5. A picture you open stays in this browser. The plate reads it with `FileReader`, cover-fits it into the 1600 by 900 frame the house pictures use, converts it to gray by Rec. 709 luma, stretches its levels so 0.5 percent of the pixels clip at each end, and raises the result to the power 1.2, which is the curve the picture cutter applies. Nothing is uploaded, and the picture is gone when the page closes. ### Import and props [#import-and-props] ```ts import { screenTone } from '@/components/docs/demos/dither-lab/screens'; import { fillTone, loadSource } from '@/components/docs/demos/dither-lab/sources'; const source = await loadSource('rosetta', null); const tone = fillTone(source, cols, rows, 2, new Float32Array(cols * rows)); const bits = new Uint8Array(cols * rows); const lit = screenTone('bayer8', tone, cols, rows, bits, cols, 0, 0, { gamma: 1, bias: 0 }); ``` `screenTone(id, tone, w, h, out, stride, ox, oy, options)` reads a `w` by `h` grid of tones from 0 to 1 and writes 1 for ink and 0 for paper into `out`, a buffer `stride` cells wide, starting at cell `ox`, `oy`. It returns the number of ink cells. `fillTone` samples a source at the centre of every cell, and a cell that covers several pixels of a picture reads their area average. | Screen | `id` | Levels a tile can print | How a cell is decided | | --------------- | ------------- | ------------------------ | --------------------------------------------------------- | | Threshold | `'threshold'` | 2 | Its tone against 0.5 | | Bayer 2 by 2 | `'bayer2'` | 5 | Its tone against one of 4 thresholds | | Bayer 4 by 4 | `'bayer4'` | 17 | Its tone against one of 16 thresholds | | Bayer 8 by 8 | `'bayer8'` | 65 | Its tone against one of 64 thresholds | | White noise | `'random'` | Any, as an average | Its tone against a hash of its position | | Floyd-Steinberg | `'floyd'` | Any | Its tone plus the error of the cells before it | | Atkinson | `'atkinson'` | Any | The same, with a quarter of each error dropped | | Halftone | `'halftone'` | One round dot per period | Its tone against a ranked cosine spot on a 45 degree grid | | Option | Type | Default | What it does | | ------------------ | ------- | ------- | -------------------------------------------------------------------------------------------------------------------- | | `gamma` | number | `1` | Raises each tone to this power before screening. | | `bias` | number | `0` | Added to each tone after the gamma. | | `phaseX`, `phaseY` | number | `0` | Offsets the ordered and halftone tiles in cells, so a part of a grid can be screened at its place in the whole. | | `pitch` | number | `6` | The halftone's dot spacing in cells. | | `serpentine` | boolean | `true` | The diffusions walk odd rows from right to left, which breaks up the diagonal chains of cells a one-way walk leaves. | The Bayer matrices are built by one rule, M(2n) = \[\[4M, 4M + 2], \[4M + 3, 4M + 1]] from M(1) = \[0]. The 8 by 8 result equals `BAYER_8` in `src/lib/dither.ts` and the 4 by 4 equals the studio field's `bayer4`, so the lab's screens are the house screens. ### Numbers [#numbers] Measured in Node 24 on Apple silicon over a ramp of 180 by 170 cells, the size of one tile at 2px cells on a 1440px wide page: | Screen | Milliseconds a tile | | -------------------------------- | ------------------- | | Bayer 2 by 2, 4 by 4 or 8 by 8 | 0.8 to 1.0 | | Threshold, white noise, halftone | 1.5 to 1.8 | | Floyd-Steinberg | 6.3 | | Atkinson | 7.5 | The ordered screens cost one comparison a cell. A diffusion visits every cell in order and writes to four or six neighbours, so it costs six to eight times more, and it cannot be split across cells or threads, because each cell waits for the error of the cells before it. By these numbers the bench's eight tiles cost about 21 ms together at 2px cells and about four times that at 1px. ## Error diffusion, one cell at a time [#error-diffusion-one-cell-at-a-time] Error diffusion decides cells in reading order. A cell prints ink when its value is over 0.5, and the difference between its value and what it printed, its error, is added to cells the walk has not reached yet. This plate runs the walk on a coarse grid at 10 or 48 cells a second so each step can be followed. <DiffusionStepDemo /> A pending cell shows its tone plus the error it has received so far, printed as a small 8 by 8 patch. A decided cell is solid ink or paper. The accent frames mark the cell just decided and the cells its error went to, and the line under the plate prints the arithmetic of that step. Step decides one cell and pauses the walk, and Restart starts it again from the first cell. Floyd-Steinberg passes all of the error on, 7, 3, 5 and 1 sixteenths to the right, lower left, lower and lower right neighbours. Atkinson passes an eighth to each of six neighbours and drops the last quarter. A dropped error never reaches the page, so Atkinson prints near-white areas as paper and near-black areas as ink, with more contrast and less detail at both ends of the ramp. ### Import and props [#import-and-props-1] ```ts import { ATKINSON, FLOYD, type Kernel } from '@/components/docs/demos/dither-lab/screens'; // each entry is [dx, dy, weight]; dx flips on the rows a serpentine walk runs backward const kernel: Kernel = FLOYD; ``` | Kernel | Taps | Weights | Error passed on | | ---------- | ---- | ---------------------- | --------------- | | `FLOYD` | 4 | 7/16, 3/16, 5/16, 1/16 | All | | `ATKINSON` | 6 | 1/8 each | Three quarters | ### Reduced motion [#reduced-motion] The walk is created paused and plays only while the plate is on screen and the tab is visible. Under reduced motion the plate draws the walk stopped 45 percent of the way through, with that step's frames and arithmetic shown, and Step still works. ## Screens in motion [#screens-in-motion] The same moving field runs through the house 8 by 8 screen on the left and through error diffusion or white noise on the right. Each side prints the share of its cells that changed since the previous frame, averaged over the last few frames. <ShimmerDemo /> On the Bayer side a cell changes only when its own tone crosses its own fixed threshold, so a slow change touches a few cells along the moving edges of the field. On the diffusion side a small change anywhere alters the error carried to every cell after it in the walk, so about a third of the cells change every frame, all over the field. The house screen is ordered for this reason. ### Numbers [#numbers-1] Measured in Node 24 over 60 frames at 30 frames a second, on one half of the plate at a 1440px wide page: | Field | Bayer 8 by 8 | White noise | Floyd-Steinberg | Atkinson | | ------------------------- | ------------ | ----------- | --------------- | -------- | | Noise drifting, 3px cells | 0.76% | 0.74% | 33.0% | 24.6% | | Ramp scrolling, 3px cells | 0.26% | 0.26% | 31.5% | 25.2% | White noise changes as few cells as the Bayer screen, because it is also a fixed threshold map. Its still frame is worse: its thresholds have no structure, so neighbouring cells clump into grain, where the Bayer tile spreads every level as evenly as the grid allows. Earth sways its sampling window by up to 2.5 cells, under 3 percent of the half's width at a 1440px wide page, which is the drift the motion rules allow on a still field. Under reduced motion the plate draws one frame and prints the change between it and the frame 1/30 s before it. ### Traps [#traps] * A diffusion's pattern depends on the walk's start. Two canvases that cover halves of one field and run their own walks print different cells along the seam where they meet, even at the same tones. * A cell that never changes tone can still change under a diffusion, because its value includes the errors of the cells before it. Comparing the tone grids of two frames therefore says nothing about which cells a diffusion will flip. * A serpentine walk breaks up diagonal chains and leaves the change rate where it was: the rates above are with it on. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Dithered type and marks > Type and marks set in dither cells: words built from Bayer tiers, a mark that erodes and resolves, and glyph screens drawn from the house face. URL: https://www.prototemplate.com/docs/components/dither-type These plates print type and marks in dither cells. The first two rasterize a shape at three pixels a cell and measure its exact signed distance field: for every cell, the distance in cells to the shape's outline, negative inside. Every style below is a function of that one number per cell, so a style change never redraws the shape. The third plate prints a picture with a character in each cell. ## Words in the house face [#words-in-the-house-face] Type a line or pick the word "language" in one of eight scripts, then choose how the distance field becomes tone. The screen, the cell size and the ink change the print without touching the shape. <DitherWordDemo /> The plate draws the line at weight 500 in the page's own face, the rsms InterVariable that `--pt-text` resolves to, and waits for `document.fonts.ready` so the shape is measured from the webfont and never from a fallback. It fits the line to 88 percent of the plate's width and at most 64 percent of its height. Inter covers Latin, Greek and Cyrillic; Han, Arabic, Devanagari, Hangul and Thai come from the system's faces for those scripts, shaped by the canvas's own text engine. | Style | Tone of a cell | | ------ | --------------------------------------------------------------------------------------------------------------- | | Fringe | 1 inside and 0 outside, with a smoothstep 1.4 cells to each side of the outline, so the edge is a band of tiers | | Ramp | Inside the letters, from 1 at the line's left edge down to 0.08 at its right | | Shade | Inside the letters, from 0.14 at the outline up to 1 at the stroke's spine | | Glow | 1 inside, paper for 1.2 cells around the letters, then from 0.55 down to 0 by 45 percent of the cap height out | | Shadow | Solid letters, a gap of 1.5 cells, then a shadow at tone 0.42 offset by 9 percent of the cap height | ### Import and props [#import-and-props] ```ts import { textShape, wordTone } from '@/components/docs/demos/dither-lab/shapes'; import { screenTone } from '@/components/docs/demos/dither-lab/screens'; const family = getComputedStyle(canvas).fontFamily; const shape = textShape('Every language', family, cols, rows); const tone = wordTone(shape, 'shade', new Float32Array(cols * rows)); screenTone('bayer8', tone, cols, rows, bits, cols); ``` | Function | Returns | Notes | | ------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------- | | `textShape(text, family, cols, rows, ss?, fill?)` | `CellShape` | `ss` is the pixels drawn per cell (3), `fill` the share of the width the line may take (0.88). | | `wordTone(shape, style, out)` | the filled `out` | `style` is `'fringe'`, `'ramp'`, `'shade'`, `'glow'` or `'shadow'`. | A `CellShape` holds `cols`, `rows`, `sd` (the signed distance per cell, in cells), `box` (the ink's bounding box in cells) and `depth` (the deepest inside distance). ### Traps [#traps] * A canvas draws text in whatever face is loaded at the moment it draws. A shape built before the webfont arrives keeps the fallback's metrics, so the plate rebuilds when `document.fonts.ready` resolves. * The distance field is measured on the 3x mask and read at cell centres. Reading a 1x mask instead gives only a one-cell edge, and the fringe and shade styles lose their bands. ## A mark through the tiers [#a-mark-through-the-tiers] The plate holds a mark as a signed distance field and prints it at one level of the screen, from 0 to 64 on the 8 by 8 and 0 to 16 on the 4 by 4. Drag Level, or let Play run the cycle. <MarkTiersDemo /> With the Tiers order every cell inside the mark gets the level's tone, so level k on the 8 by 8 lights exactly k of the 64 cells in every whole tile inside the mark. The tiers nest: every cell lit at one level stays lit at every higher level. A step up therefore only adds cells and a step down only removes them, and no cell moves. The other orders spread the level across a band of 35 percent of the ramp. Core out fills each stroke from its spine to its outline, Edge in fills from the outline to the spine, and Sweep fills from left to right. Play runs one cycle on smoothsteps: the mark resolves in 1.4 s, holds for 1.2 s, erodes in 1.4 s and holds empty for 1.2 s. Dragging Level stops the cycle. Under reduced motion the plate draws the mark resolved, and Level still works. The marks are the GT mark from `REFERENCE_MARK` in `src/lib/marks.ts` and four marks from `public/marks`. ### Import and props [#import-and-props-1] ```ts import { loadMark, markShape } from '@/components/docs/demos/dither-lab/shapes'; const art = await loadMark('bar-monogram'); const shape = markShape(art, cols, rows); ``` | Function | Returns | Notes | | ---------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------- | | `loadMark(id)` | `Promise<MarkArt>` | `'gt'` draws the reference path with the even-odd rule; the others decode `/marks/<id>.svg` once at 2048px wide. | | `markShape(art, cols, rows, ss?, fill?)` | `CellShape` | Fits the mark to 80 percent of the width and 76 percent of the height, centred. | The marks in `public/marks` draw their cuts with SVG masks and white shapes over black. The plate rasterizes each file as an image and counts a pixel as ink only when it is opaque and dark, so a white knockout stays paper. ## A glyph screen [#a-glyph-screen] Each cell of this screen is a character. The source picker is the bench's: the five pictures, the ramp, the noise or your own picture. <GlyphScreenDemo /> The plate draws every glyph of the chosen set once in the house face, measures the share of its cell each glyph inks, and sorts them into a ladder. A glyph joins the ladder only when it inks at least 1.5 percent more than the step before it, measured against the densest glyph, so every step is a visible step in tone. A cell's tone then picks a step. With Dither on, a tone between two steps takes the upper step where the remainder is over the cell's 8 by 8 Bayer threshold, so the tile carries the tones between glyphs the same way it carries tone between ink and paper. With Dither off, each cell takes the nearer step, and the picture shows bands where the tone crosses a step. Cells are 1.5 times as tall as they are wide, close to a line of text. Eight scripts uses the glyph field's inventory of letters from the eight words for "language", and Digits uses the ten digits. ### Traps [#traps-1] * Glyphs here stay still. The house glyph rain moves its glyphs and keeps every one as solid type, because a dither pattern on moving type flickers with every step. * The canvas draws at the device pixel ratio, capped at 2. Each glyph is drawn once into an atlas at the cell's size in canvas pixels and copied to whole pixels of the canvas, so no glyph is resampled where it is placed. * A glyph that is wider than its cell would ink its neighbours. The plate measures the widest glyph of the set and shrinks the face until it fits within 104 percent of the cell's width. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Dithered transitions and brushes > Tone-mix wipes on one cell grid, a lens that reveals a picture under the screen, and a brush that paints in dither cells. URL: https://www.prototemplate.com/docs/components/dither-motion These plates change a dithered picture while it is on screen: a transition from one picture to another, a lens that shows what the screen hides, and a brush. All three hold the house rule for a dithered field in motion. The field stays on one grid of cells and one anchored tile, and it changes only by changing the tones the tile reads. ## Tone mix [#tone-mix] Two pictures share one grid of cells and one 8 by 8 tile. Mix moves every cell's tone from the first picture's tone to the second's on one smoothstep, and the screen prints the result. Drag Mix to scrub it, or press the button to run it at one of the house durations. <ToneMixDemo /> Each cell's tone moves in one direction past one fixed threshold, so a cell changes at most once during a tone mix. The line under the plate counts both numbers for each run: after a tone mix the count of changes equals the count of cells that changed. Pressing the button during a mix turns it around from the frame on screen, so the picture never jumps back to where the mix began. For comparison the plate also runs the two methods the gt-motion skill refuses for any dithered field. * Alpha fade screens both pictures and fades the second over the first. Every cell that differs between them passes through 32 steps of gray for the length of the fade. * Moved tile mixes by tone but moves the tile 3 cells right and 2 cells down halfway through, as two canvases with unaligned phases would. At that frame thousands of cells that the tone mix leaves alone are decided again against other thresholds, and the eye reads a flash. Measured on a 1440px wide page at 2px cells, one 2 s mix from the Earth to the Rosetta Stone changes the field this much: | Method | Cells that changed | Changes in all | | ---------- | ------------------ | -------------- | | Tone mix | 16,961 | 16,961 | | Moved tile | 27,391 | 38,034 | | Alpha fade | 16,963 | 542,816 | The durations are the house ones: 150 ms for a step from one picture to the next and 350 ms for a resolve. The 2 s choice is slow enough to watch the tiers fill. Under reduced motion the button draws the end state at once. ### Import and props [#import-and-props] ```ts import { createDitherLoop, mixFields } from '@/lib/dither'; const smoothstep = (k: number) => k * k * (3 - 2 * k); loop.setField(mixFields(fieldA, fieldB, smoothstep(elapsed / duration))); ``` In the house engine a transition is `mixFields(a, b, k)` handed to the running loop through `setField` on every tick, with `k` on a smoothstep of the elapsed time. Both fields are read at the loop's own cells, and the loop keeps its `phase`, so the tile never moves. | Rule | Held by | | ----------------------- | -------------------------------------------------------------------------------- | | One grid | Both pictures are sampled at the same cells, at one cell size for the whole mix. | | One anchored tile | The 8 by 8 tile keeps its phase from the first frame to the last. | | One smoothstep | `k = t * t * (3 - 2 * t)` drives the tone; ink and paper stay fixed. | | Restart from the screen | A new target starts from the current `k`. | | Reduced motion | The end state is drawn once. | ## A lens over the screen [#a-lens-over-the-screen] A picture prints through a coarse screen, and a round lens follows the pointer. Inside the lens the plate shows what the screen hides. On a keyboard, focus the picture with Tab and move the lens with the arrow keys. <ScreenLensDemo /> | Lens shows | What is drawn inside the lens | | ------------- | ---------------------------------------------------------------------------------------------------- | | Tone | The tone grid the screen reads, in 64 gray steps between paper and ink | | 8 by 8 at 1px | The same picture through the 8 by 8 screen at 1px cells, the screen of the house's artifact pictures | | Thresholds | The base screen's threshold at each cell, which is the tile itself, in the same 64 steps | | Loupe 4x | The base screen's cells under the lens at four times their size | The plate samples the picture once at 1px cells and builds the base screen, the 1px screen and the threshold map from that one grid. A move of the lens redraws only the rectangle under the old and the new lens: it copies the cached base back into that rectangle, draws the lens, and puts only that rectangle on the canvas with `putImageData`'s dirty rectangle. The lens radius runs from 32 to 130px, so a move rewrites at most two squares about 264 cells on a side, on a canvas of more than 200,000 cells at a 1440px wide page. ### Traps [#traps] * The tone view is a picture of the source, drawn for reading the screen. A house field never shows tone as gray: the screen prints every level as cells. * The lens follows a touch only on press, because the plate keeps vertical scrolling for the page (`touch-action: pan-y`). A tap moves the lens to the tapped point. ## A brush that paints in cells [#a-brush-that-paints-in-cells] The canvas holds one tone per cell. Paint raises the tone under the brush, Erase lowers it, and Reveal picture moves it toward the tone of Karahisari's calligraphy, so painting brings the calligraphy into the screen. <DitherBrushDemo /> A dab changes the tone under it by a falloff of (1 minus d²)² times Flow, where d is the distance from the dab's centre as a share of its radius. A stroke lays a dab every third of a radius along the pointer's path, and after each move the plate screens only the cells under the new dabs, at their own place in the tile. The rest of the canvas keeps its cells, so a stroke never shifts the dither around it. A new cell size keeps the painting: the plate reads the old tone at the centres of the new cells. Sample Stroke paints a wave whose width swells and thins, and Clear empties the canvas. ### Import and props [#import-and-props-1] ```ts import { screenTone } from '@/components/docs/demos/dither-lab/screens'; // re-screen the cells x0..x1, y0..y1 of a grid `cols` wide at their place in the tile screenTone('bayer8', sub, x1 - x0, y1 - y0, bits, cols, x0, y0, { phaseX: x0, phaseY: y0 }); ``` The phase options make a part of a grid print exactly the cells the whole grid would print there. Without them the tile would restart at the rectangle's corner, and every stroke would leave a seam where its cells meet the cells around it. ### Traps [#traps-1] * Painting needs the pointer, so the canvas sets `touch-action: none` and a finger on it paints instead of scrolling. The plate's height on a phone leaves room above and below it to scroll the page. * Flow adds up. A stroke lays about three dabs over each point, so a flow of 0.35 brings untouched paper to about 0.7 in one pass. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Doubled lines and connectors > The brand's doubled line as a component, with live gauge, gap, two-tone and pulse controls, and the production connector that carries a translation. URL: https://www.prototemplate.com/docs/components/lines The doubled line is the General Translation connector: two parallel hairlines at a constant gap along any curve. It is one center path stroked twice, a full-width stroke in ink and a narrower stroke in the ground's color on top, so the ground carves the ink into two threads. Both strokes share one geometry, so the gap never drifts on a bend. The `gt-diagrams` skill holds the rules for drawing with it. ## The doubled line [#the-doubled-line] The plate draws two sources that merge into a trunk and a trunk that fans out to three outputs, each connector one `DoubledLine`. Gauge and gap set the thread width and the space between the threads. Two-Tone draws the upper thread in full ink and the lower one at half ink. Show Core paints the carving stroke in the accent, so the construction is visible. <DoubledLineDemo /> The forks and the outputs draw first and the trunk draws last, so its core paints both junctions back into one clean pair with no offset-curve math. The moving pulse is geometry: each path is sampled once, and on every frame the pulse's `d` is rewritten to the slice under a traveling window. One window runs per journey, from a source through the trunk to one output, and it crosses each junction as one object because both slices come from one head coordinate. ### Import and props [#import-and-props] ```tsx import DoubledLine from '@/components/shared/diagrams/DoubledLine'; <svg viewBox='0 0 720 240' aria-hidden='true'> <DoubledLine d='M330 120 L680 120' core='var(--pt-paper)' ink='var(--pt-ink)'> {/* the pulse slot: a stroke at the full width, gauge * 2 + gap */} <path d='M470 120 L590 120' fill='none' stroke='var(--docs-accent)' strokeWidth={6} vectorEffect='non-scaling-stroke' /> </DoubledLine> </svg>; ``` | Prop | Type | Default | What it does | | ---------- | --------- | --------------------------- | -------------------------------------------------------------------------------------------------- | | `d` | string | required | The center path, the only geometry. | | `core` | CSS color | required | The color that carves. It must match the ground behind the line, or it shows as a stripe. | | `gauge` | number | `1.5` | Pixels per thread, held in screen pixels by `non-scaling-stroke`. | | `gap` | number | `3` | Pixels between the threads. | | `ink` | CSS color | `rgba(255, 255, 255, 0.88)` | The thread ink, white for an ink ground. Pass the ink token on paper. | | `inkB` | CSS color | none | A second ink for the other thread. Needs `splitD`. | | `splitD` | string | none | The center path closed off one side of the view box; the `ink` thread takes that side. | | `children` | SVG | none | The pulse layer, drawn between the threads and the core, so the core splits it into two hairlines. | `DoubledLine` is a server component: it renders three paths and ships no JavaScript. The moving pulse needs a client component that samples the path and rewrites the pulse's `d`, as this plate and `TranslationFlow` do. ### When to use it [#when-to-use-it] Use the doubled line for every connector in a GT diagram: a flow from a source to its outputs, a route, a merge. Keep the defaults, 1.5 and 3, on the site and in the deck. A pulse is the drawing's one accent only when nothing else in the drawing takes the accent; otherwise it runs in the page ink. Never use the doubled line as a page rail beside the reading column, because it is a connector. ### Reduced motion and themes [#reduced-motion-and-themes] `DoubledLine` itself does not move. The host's pulse ships hidden and never starts under reduced motion; the still is a static window on the trunk, which this plate shows in place of the moving pulse. The threads read the stage's ink token and the core reads its ground token, so the line follows the docs theme. ## The connector in production [#the-connector-in-production] The plate mounts `TranslationFlow` from the Toolchain direction as it ships. A real source component on the left forks into the three output files `gt.config.json` declares, one doubled trunk leaves the panel and splits at a drawn junction, and a pulse window carries each transfer from the trunk onto its branch. Replay remounts it, which restarts the staggered journeys. <ConnectorDemo /> Its three journeys leave 2.4 seconds apart and share one speed of 40 units a second, so branches of different lengths move alike. ### Import and props [#import-and-props-1] ```tsx import TranslationFlow from '@/app/d/toolchain/diagrams/TranslationFlow'; <TranslationFlow title='A source component forked into its translated output files' />; ``` | Prop | Type | Default | What it does | | ----------- | ------ | ------- | ------------------------- | | `title` | string | none | The accessible name. | | `className` | string | none | A class on the root grid. | The diagram reads `--tc-plate` as the ground its cores carve, `--tc-panel` as the source panel's ground and `--tc-ink` as the pulse's ink, so a host sets those three on an ancestor. ### When to use it [#when-to-use-it-1] Use it where a page explains what the build produces: one source file and the files it becomes. It is a whole figure with real content and a fixed set of files, so it takes a band or a plate of its own, up to 680 pixels wide. ### Reduced motion and themes [#reduced-motion-and-themes-1] Under reduced motion the loop never starts and the fork holds its still. The source panel stays ink in both themes, and the threads and the files follow the host's tokens. ## Traps [#traps] [SVG lines in motion](https://www.prototemplate.com/docs/craft/svg-motion) explains each of these with its proof. * Under `vector-effect: non-scaling-stroke`, Chromium measures dashes in screen pixels and ignores `pathLength`: `pathLength=100` with `dasharray 25 100` draws a 25-pixel fleck where a quarter of the path was meant. Author a progress ring's dash in user units ([pathLength under a non-scaling stroke](https://www.prototemplate.com/docs/craft/svg-motion#pathlength-under-a-non-scaling-stroke)). * A single travelling dash, its dasharray equal to the path length and its offset animated, tiles into several dashes on a scaled SVG under `non-scaling-stroke`. Translate a short `<line>` in user units instead ([a travelling dash that tiles](https://www.prototemplate.com/docs/craft/svg-motion#a-travelling-dash-that-tiles)). * A dash clips at the end of a closed subpath and never wraps the loop. Start the path where the arc starts, or use `dasharray d (L - d)` ([dashes on a closed path](https://www.prototemplate.com/docs/craft/svg-motion#dashes-on-a-closed-path)). * The sign of the parked offset picks the end a draw starts from: an offset tweened from +100 to 0 reveals from the path's first point, and from -100 from its last. A backwards draw was rejected three review rounds running in August 2026, so read the first point of `d` before touching the timing ([the sign of the parked offset](https://www.prototemplate.com/docs/craft/svg-motion#the-sign-of-the-parked-offset)). * A bare `100` dash bleeds a sub-pixel fleck at the path's end in Chromium. `strokeDasharray='100 200'` parked at 101 or -101 keeps both ends clear, and offset 0 still covers the whole path ([a fleck at the end of the path](https://www.prototemplate.com/docs/craft/svg-motion#a-fleck-at-the-end-of-the-path)). * GSAP rounds offsets to whole numbers, so fine dash motion normalizes `pathLength` to 1000 ([rounding, cached paths and empty paths](https://www.prototemplate.com/docs/craft/svg-motion#rounding-cached-paths-and-empty-paths)). * An SVG stretched with `preserveAspectRatio="none"` keeps straight segments straight, warps curves, turns circles into ellipses and distorts text. A stretched strip holds straight taps with one elbow and no text, and its labels live in HTML beside it. TranslationFlow's fork predates the rule and still bends its branches with cubic curves, which the gt-diagrams figure check warns on ([drawing under a stretched viewBox](https://www.prototemplate.com/docs/craft/svg-motion#drawing-under-a-stretched-viewbox)). * The line audit rebuilds lines from computed CSS and cannot see SVG strokes, so check every junction of a connector by eye in 2x crops. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Isometric drawings > The isometric kit live: one 30 degree projection, one corner radius and one light, with boxes and prisms built on a slab and a mark seated on a face. URL: https://www.prototemplate.com/docs/components/iso The isometric family draws GT's infrastructure: stacks, chips, plates and the Locadex iso. Every drawing goes through one module, `src/app/d/toolchain/diagrams/iso.ts`, with one 30 degree projection, one corner radius and one light from the upper left, so separate drawings read as one set. The `gt-diagrams` skill holds the rules, and its reference drawings include the Locadex iso. ## The kit [#the-kit] The plate builds a solid on a slab with the kit's own functions. Width, depth and height size a box, and Solid swaps the box for a hexagonal or octagonal prism extruded from its plan polygon the same way. Mark seats the GT mark on the solid's top face with `plane(z)`. <IsoKitDemo /> Each solid is drawn in the extrusion recipe: the opaque hull first, which hides whatever stands behind it, then the three visible faces, then only the hairlines that the silhouette and the top contour do not already draw. A later solid painted over an earlier one occludes it with no depth sorting. ### Import and functions [#import-and-functions] ```tsx import { frontEdge, leftFace, plane, rightFace, roundedPolygon, segment, silhouette, topFace, type IsoBox } from '@/app/d/toolchain/diagrams/iso'; const box: IsoBox = { x: -28, y: -24, z: 4.2, w: 56, d: 48, h: 18 }; const [a, b] = frontEdge(box); <g> <path className='hull' d={roundedPolygon(silhouette(box))} /> <path className='right' d={roundedPolygon(rightFace(box))} /> <path className='left' d={roundedPolygon(leftFace(box))} /> <path className='top' d={roundedPolygon(topFace(box))} /> <path className='line' d={roundedPolygon(silhouette(box))} /> <path className='hair' d={roundedPolygon(topFace(box))} /> <path className='line' d={segment(a, b)} /> </g>; ``` | Function | What it returns | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `project(x, y, z)` | A world point on screen: +x runs down to the right, +y down to the left, +z straight up. | | `topFace`, `leftFace`, `rightFace`, `silhouette`, `frontEdge` | The faces, the outline and the near vertical edge of an `IsoBox` `{ x, y, z, w, d, h }`. | | `prismTop`, `prismFaces`, `prismSilhouette`, `prismFrontEdges` | The same for an `IsoPrism` `{ points, z, h }`, any convex plan polygon. Each face says whether it takes the left or the right shade. | | `roundedPolygon(points, r)` | A path through the points with the family's corner radius, `ISO_RADIUS` (2.4). | | `plane(z, ox, oy)` | The SVG transform that lays plan content on the face at height z, for marks, labels and chip seats. | | `markPath(x, y, w, d, z)` | A flat rounded rectangle on a face: a content bar or a mark's plinth. | | `depth(x, y, z)` | How near the camera a point sits; its sign splits an object's front from its back. | `IsoFrame` wraps a drawing in the family's 240 by 180 frame and its props, `className`, `strokeWidth`, `accent` and `title`, which every drawing in the family takes. ### When to use it [#when-to-use-it] Use the kit for infrastructure: a stack of layers, a device, a plate of chips, a mark on a slab. Keep the light from the upper left and the shading in its order: the top lightest, the front-left face in the middle and the front-right face darkest. Spend the accent on one object at most. A diagram of a flow between parts uses the doubled line instead. ### Reduced motion and themes [#reduced-motion-and-themes] The kit draws still geometry; a host that animates it, as the stack tower does, runs its own timeline and holds a still under reduced motion. The plate's faces are the stage's ink at 4, 9 and 15 percent, so on paper they shade like daylight, and on ink they take the night shading the dark bands use. ## Numbers and traps [#numbers-and-traps] The projection and the light make the drawings agree on angle and shade. These proportions and alphas make them agree on weight, so a stack drawn for one page sits beside a chip drawn for another as one set. | What | Value | Measured on or held by | | --------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------- | | Plate thickness | about 4% of the footprint | The stack tower: footprint 104, thickness 4.2 | | Air between stacked plates | about 40% of the footprint | The stack tower: 42 between plates | | Corner radius | `ISO_RADIUS` 2.4, clamped to half of each adjoining edge | `roundedPolygon`, so a thin slab keeps its side faces | | Corner radius on diff slats | 1.2 | At 2.4 a thin slat reads as a capsule | | Rim alpha, plate `i` of `n` | 0.12 + 0.08 d, where d = i / (n - 1) | 0 at the bottom plate, 1 at the top | | Top edge alpha | 0.24 + 0.24 d | The same d | | Front edge alpha | 0.08 | Every plate | | Labels beside a drawing | sans at 9 to 10 user units, about 0.6 alpha, leaders at 0.14 | Mono only on numbers | | Glyphs on a chip | `<T>`, `en` and `ja` at 6.5 px | Short glyphs read at chip scale, and words do not | | The Locadex mark's box | a 500-unit viewBox around a glyph 199 by 222 | `markHalf = glyphWidth * (500 / 199) / 2` sizes the image box from the glyph | * Depth comes from each plate's stroke alpha. Group opacity carries no depth in this family. The pricing board's `is-dim` at opacity 0.32 marks a selection and says nothing about depth. * A hand-written `scale(1 0.5) rotate(45)` is the 2:1 dimetric squash. Its y to x ratio is 0.5 where the 30 degree face needs 0.577, so a mark placed that way lies flatter than the face under it. Seat flat art with `plane(z, ox, oy)`. Its matrix is the flat-to-iso map in one step, and it agrees with `project()` on that plane: rotate 45 degrees, scale y by tan 30 (0.577), then scale the whole by the square root of 1.5. * Every point goes through `project()` or `plane()`, the two functions that hold the 30 degree projection. * Plates are opaque. The first plates were frosted glass, a near-opaque hull under a diagonal white sheen, and BRAND.md section 9 lists glassmorphism under Avoid. The stack tower and the Locadex iso use opaque plates with no sheen. * The line audit reads lines from computed CSS and cannot see SVG strokes, so check every junction of a drawing by eye in 2x crops. The `gt-diagrams` skill's [isometric reference](https://www.prototemplate.com/skills/gt-diagrams/references/isometric.md) holds the face tones for each material, the extrusion recipe and the shimmer. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Globes and maps > Every globe in the library, live: the edge delivery globe, the dithered globe field in two looks, and the blog globe directions from all three rounds. URL: https://www.prototemplate.com/docs/components/globes Prototemplate draws the Earth three ways. The edge globe is an SVG drawing of the translation CDN. The globe field is a lit sphere thresholded by the CPU dither loop. The blog globe figures turn a globe to the city of each version of Euclid's Elements, and their four directions in three rounds are recorded on [The blog globe figure](https://www.prototemplate.com/docs/components/blog-globe). ## The edge globe [#the-edge-globe] The plate mounts `EdgeGlobe`: five points of presence stand on a sparse graticule, and one route carries a request from the user to the closest edge and the payload back in 12 ms. Accent spends the accent on the route and the arrival flashes, Halo adds the dithered atmosphere, Stroke width sets the line weight in view box units, and Replay restarts the loop. <EdgeGlobeDemo /> The halo is four rings outside the limb, each filled with one coverage tier of the 4 by 4 screen on one shared grid, so crossing a ring only ever turns cells off. ### Import and props [#import-and-props] ```tsx import EdgeGlobe from '@/app/d/toolchain/diagrams/EdgeGlobe'; <EdgeGlobe accent title='One request served from the closest edge in 12 ms' />; ``` | Prop | Type | Default | What it does | | ------------- | ------- | ------- | ------------------------------------------------------------------------------------------------ | | `accent` | boolean | `true` | Spends the accent on the route and the arrival flashes. Off, the whole drawing is the quiet ink. | | `strokeWidth` | number | `0.85` | The line weight in view box units, written to `--eg-sw`. | | `title` | string | none | The accessible name. Without one the drawing is decorative and hidden. | | `className` | string | none | A class on the SVG. | The drawing reads `currentColor` and the `--iso-ink`, `--iso-accent` and `--iso-mono` tokens, so one ancestor themes it. A fork with Heroicons glyphs lives at `src/app/d/production/sections/EdgeGlobe.tsx` for the production mirror. ### When to use it [#when-to-use-it] Use the edge globe where a page explains delivery: a request, the closest edge, a latency. It tells one sentence with one route, so it stands alone in a cell or a band. Use the globe field instead for a globe that is texture with no labels. ### Reduced motion and themes [#reduced-motion-and-themes] Under reduced motion the payload chip rests mid-route, the dot, the rings and the flashes are removed, and the still rests in the quiet ink. The drawing follows the docs theme through `currentColor`. ## The globe field [#the-globe-field] The plate runs `globe()` from `src/lib/dither.ts`: a lambert-shaded sphere with a graticule and a noise landmass, thresholded by the 8 by 8 screen. Each slider builds a new field and hands it to the running loop with `setField`, so the spin keeps its clock. The two looks load the field as the Dither field direction draws it and as the dashboard's sign-in plate draws it. <DitherGlobeDemo /> The Dither field look is `globe()` at a 3 pixel cell with a graticule of 0.3, twelve meridians, seven parallels and a spin of 0.18 radians a second. The sign-in look has no graticule, a landmass of 0.42 and a spin of 0.14 at a 2 pixel cell and 24 frames a second, in GT blue: `#2f5ce0` on paper and `#9db9ff` on ink. ### Import and props [#import-and-props-1] ```ts import { createDitherLoop, globe } from '@/lib/dither'; const loop = createDitherLoop(canvas, globe({ graticule: 0.3, landmass: 0.22, spin: 0.18 }), { scale: 3, paper: 'transparent' }); loop.setField(globe({ graticule: 0, landmass: 0.42 })); ``` | Option | Type | Default | What it does | | ---------------------------- | ------ | ------------------------ | ----------------------------------------------------------------------------- | | `cx`, `cy`, `radius` | number | `0.5`, `0.5`, `0.38` | The center and the radius in the field's 0 to 1 space. | | `lightX`, `lightY`, `lightZ` | number | `-0.45`, `-0.62`, `0.65` | The light's direction, normalized. The house light comes from the upper left. | | `ambient`, `rim` | number | `0.08`, `0.22` | The shading's floor and the rim light at the silhouette. | | `graticule` | number | `0.3` | The strength of the latitude and longitude lines. 0 removes them. | | `meridians`, `parallels` | number | `12`, `7` | How many lines the graticule draws. | | `landmass` | number | `0.22` | The contrast of the noise continents. 0 removes them. | | `spin` | number | `0.18` | Radians a second about the polar axis. | | `tilt` | number | `0.41` | The axial tilt in radians, about 23.4 degrees. | | `aspect` | number | `1` | The canvas's width over its height, so the sphere stays round. | ### When to use it [#when-to-use-it-1] Use the globe field for a turning Earth as material, behind a sign-in form or in a band, where its texture matters and its geography does not: the landmass is noise and carries no real coastlines. Use a blog globe direction for a globe that has to show real places. ### Reduced motion and themes [#reduced-motion-and-themes-1] Under reduced motion the loop draws one frame at its `reducedMotionTime` (8 seconds on the sign-in plate) and never starts. The host reads its ink token again on every theme change, as `FieldStack.tsx` does on the sign-in plate. ## The blog globe figures [#the-blog-globe-figures] The plate shows one blog globe direction live in its round's own document. Round picks the build, Direction picks the drawing, and the figure keeps its own controls: the underlined language names, the clickable cities and the arrow keys. Open Stage opens the figure on its stage page. <BlogGlobeDemo /> The four directions draw the same five stops of Euclid's Elements from Alexandria in 300 BCE to Beijing in 1606. A is a dithered globe, B a line globe, C a glyph globe and D a dot globe, and the design review ranked D first. Round 1 has no clickable cities, round 2 adds them, and round 3 zooms each globe in and sets all eight definitions. [The blog globe figure](https://www.prototemplate.com/docs/components/blog-globe) records the measurements and the ranking, and `/d/blog-globe/all` lays all twelve figures side by side. ### Import and props [#import-and-props-2] ```tsx import GlobeFrame from '@/components/blog-globe/GlobeFrame'; <GlobeFrame round='r3' variant='d' title='The dot globe, round 3' layout='fit' />; ``` | Prop | Type | Default | What it does | | --------- | ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `round` | `'r1'`, `'r2'` or `'r3'` | required | The round, each a frozen copy under `src/app/d/blog-globe/_rounds`. | | `variant` | `'a'`, `'b'`, `'c'` or `'d'` | required | The direction. | | `title` | string | required | The frame's accessible name. | | `layout` | `'scale'` or `'fit'` | `'fit'` | `'scale'` lays the figure out at the 712 pixel review column and scales it down. `'fit'` does the same from 600 pixels up and lays the figure out at the box's own width below that. | ### When to use it [#when-to-use-it-2] Use a frame wherever a page shows a round's figure as it was reviewed: each round keeps its own stylesheets, and the page loads none of the figures' code. The frame arms when it comes within 320 pixels of the viewport. ### Reduced motion and themes [#reduced-motion-and-themes-2] Under reduced motion every direction jumps the camera to the stop, draws once and cuts the text. With no JavaScript the radios and CSS `:has()` still switch the passage and the label. The frame passes the docs theme to the figure, and directions A and C read their inks again when it changes. ## Other globes [#other-globes] The GT Atlas magazine at [/atlas](https://www.prototemplate.com/atlas) draws a dithered orthographic Earth with a 4 by 4 screen and a pin with its coordinates. The magazine session keeps it, and it is not part of this library yet. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # The blog globe figure > Four directions for a blog figure that turns a globe to each city on the path of Euclid's Elements: the shared contract, the measurements and the ranking. URL: https://www.prototemplate.com/docs/components/blog-globe General Translation (GT) is building interactive figures for the posts on its blog. The Versions figure sets the first eight definitions of Euclid's Elements in five languages on one rounded plate, and a pill of language names switches between them. It has not been published yet. The globe figure adds where and when each version was made. When a reader picks a language, a globe turns to the city of that version, a label prints the city's coordinates and year, and the eight definitions change row by row into the new language. Read in order, the five stops follow the Elements from Alexandria in 300 BCE to Beijing in 1606. The four directions were built in three rounds on 2026-10-19 in gt-cloud, the repository that builds generaltranslation.com. A design review scored and ranked the first build, and its fixes and the clickable cities came next. Round 3 zoomed each globe in, set all eight definitions, replaced each language control with underlined names and printed the coordinates in degrees, minutes and seconds. Every measurement on this page comes from round 3. All three rounds of the four directions also run live on this site, copied from gt-cloud at commit `5ba413acfb`. [Globes and maps](https://www.prototemplate.com/docs/components/globes) mounts the same figures beside the edge globe and the globe field, with the props of `GlobeFrame`, the component every direction is built on. ## The rounds [#the-rounds] Each round runs on its own route of this site, so the rounds keep their own stylesheets and none of them changes when another does. The routes are left out of search results and the sitemap. | Round | Route | Code | What it shows | | ----- | ------------------------------------ | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | [/d/blog-globe/r1](https://www.prototemplate.com/d/blog-globe/r1) | frozen from `c7031e2ca8` | The whole globe, one line of the passage, a language control of its own in each direction (a pill, chips or a year timeline) and decimal coordinates | | 2 | [/d/blog-globe/r2](https://www.prototemplate.com/d/blog-globe/r2) | frozen from `ee19373e96` | Round 1 with a click target on every city on the globe | | 3 | [/d/blog-globe](https://www.prototemplate.com/d/blog-globe) | copied at `5ba413acfb` | A zoomed globe, the eight definitions changing row by row, underlined language names and coordinates in degrees, minutes and seconds | The grid below holds every round of every direction: a row per direction in the order the review reads them (D, A, B and C) and a column per round. Each figure runs live in a frame of its round's route, laid out at the 712 px column it was reviewed in and scaled down to its cell. A pick in the names or a click on a city works in every cell, and each caption opens its figure alone at full size. [/d/blog-globe/all](https://www.prototemplate.com/d/blog-globe/all) sets the same grid across a whole screen. <GlobeGrid /> ## The five stops [#the-five-stops] | Stop | Language | City | Coordinates | Year | | ---- | ---------------------- | ---------- | ---------------------------- | ------- | | 1 | Ἑλληνική (Greek) | Alexandria | 31° 12′ 0″ N 29° 55′ 8.4″ E | 300 BCE | | 2 | العربية (Arabic) | Baghdad | 33° 20′ 42″ N 44° 20′ 42″ E | 800 CE | | 3 | Latina (Latin) | Antioch | 36° 12′ 0″ N 36° 9′ 36″ E | 1130 | | 4 | English | London | 51° 31′ 1.2″ N 0° 5′ 49.2″ W | 1570 | | 5 | 文言 (Classical Chinese) | Beijing | 39° 54′ 0″ N 116° 22′ 12″ E | 1606 | Every direction prints the label on two lines under its globe: the coordinates, then the city and the year joined by a middle dot. The coordinates are degrees, minutes and seconds (DMS) in tabular figures. Each value keeps the precision of its source: 0.001° is 3.6″, so a coordinate given to three decimal places prints one decimal of a second. Antioch and Beijing are given to two places and print whole seconds. No-break spaces hold each coordinate together, so a coordinates line that wraps breaks between the latitude and the longitude. The passage is definitions 1 to 8 of book I, from the point to the plane angle, in the same five texts the Versions figure sets. Each text keeps its own markers: α΄ to η΄ in Greek, I to VIII in Latin, 1 to 8 in English and 第一界 to 第八界 in Chinese. The Arabic carries none. The hops between the stops are 12.4° from Alexandria to Baghdad, 7.3° on to Antioch, 29.9° to London and 73.2° to Beijing. ## Shared contract [#shared-contract] Every direction draws the same five stops with the same passage, language control and city targets. The directions differ in how they draw the globe and where they place the label. | Part | What every direction does | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Window | The globe is seen through a round window zoomed in on its surface, so a hop between neighboring cities moves the ground across a large share of the window. The cap the window shows is 2 asin(1 / zoom) across: A and C zoom 3 times and show about 39°, and B and D zoom 2.5 times and show about 47°. | | Turn | A pick turns the camera the short way round on power2.inOut with no overshoot, and the turn ends with the city at the window's center. A pick during a turn starts the new turn from the camera on screen. A longer arc takes a longer turn. | | Passage | The eight definitions of all five languages sit on shared grid rows in one box. Line N sits at the same height in every language, and the box keeps the tallest language's height. | | Dissolve | On a switch each row steps from the old language to the new one through the 8 by 8 Bayer dissolve of the Versions figure: CSS masks of 2 px cells in 65 levels, 150 ms a row, with rows starting 55 ms apart from the top. Eight rows take 535 ms, and the step is centered on the middle of the turn, where the ground moves fastest. No text fades, wipes or slides. | | Language control | The five native names run as plain text, each underlined like a link in the article: a solid line under the current name and a dotted line under the others. Native radio buttons sit under the words, so the choice works without JavaScript, with a screen reader and from the keyboard. Arrow, Home and End wrap at the ends and mirror in right-to-left text. | | Cities | Every city inside the window takes a click. A 24 by 24 px target over its mark checks that stop's radio, so a click runs the same turn as a pick in the names. The targets are hidden from assistive technology and left out of the tab order, because the radios already give that access. A city outside the window is neither drawn nor clickable. | | Reduced motion | Under `prefers-reduced-motion: reduce` the camera jumps to the stop, the globe draws once and the text cuts. | | No JavaScript | The server markup shows stop 1: Alexandria in the window, the Greek passage and its label. The radios and CSS `:has()` still switch the passage and the label. | | Height | The figure keeps one height at every stop and through every turn. The round 3 shoot recorded every height the figure took at 1440 and 375 px wide in both themes and at 320 px wide in the light theme, and each width gave a single height. No direction scrolls sideways at 320 px. | | Type and color | Inter is the only face. Arabic sets in the system face with no Inter in front, because Inter has no Arabic, and Chinese sets at a line height of 1.6. The plate is the Versions figure's: a 12 px corner, a 1 px hairline and a ground of ink at 3.5%. The figure has no flags and no eyebrow labels. | | Loading | The 130 dissolve masks decode when the figure comes within one viewport of the screen. Each engine mounts once on the client and releases its listeners and frames on unmount. A and C draw on a canvas, so they read their inks again when the theme changes. | ## Shared code [#shared-code] | File | What it does | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `stops.ts` | Holds the five stops: language, native name, writing direction, city, coordinates with their precision, year and the eight definitions. `formatCoordinate()` writes the DMS label. | | `ortho.ts` | Holds the orthographic projection with a camera that can face any point: `project()`, `unproject()` for drawing a disc cell by cell, `turnToward()` for a turn the short way round, and `greatCircle()` for a route between two places. | | `land.ts` | Exports `landAt(lat, lon)`, the share of land from 0 to 1 at any point. It reads a 1-bit mask at 1° (8,100 bytes) held inline as base64. The mask was cut from the grid of the unreleased World Language Map (`/world` on the branch `k/language-map`), which draws Natural Earth 4.1.0 country shapes. The mask carries no borders and no names. | | `Passage.tsx`, `mount-passage.ts` | Render the eight-line passage on shared rows and run its row dissolve on the passage's own clock or on the globe's. | | `LanguageLinks.tsx` | Renders the underlined names over native radios. | | `CityTargets.tsx`, `targets.ts` | Render the click targets and move them with the camera, with one update per drawn frame and no React render. | The shared files come to 1,253 lines with their three stylesheets. Every direction loads all of them, except that B draws baked coastlines and never loads `land.ts`. ## Measurements [#measurements] The round 3 shoot ran each direction in Chromium at 1440 and 375 px wide in both themes and at 320 px wide in the light theme. The review sets the figure in a 712 px column at 1440 px wide, and the window widths below are the drawn window's diameter. At 1440 px in the light theme the shoot timed five moves: Arabic to Greek, Greek to Arabic, Arabic to Latin, Latin to English and English to Chinese. No direction logged a console error, no timed move had a gap of more than 20 ms between frames, and every move settled. The longest frame callback is the longest `requestAnimationFrame` callback in the timed moves, which holds one frame's drawing work. | | A (dithered) | B (line) | C (glyph) | D (dot) | | ---------------------------------------------- | --------------------------------------------- | ------------------------------------- | ------------------------------ | ------------------------------------- | | Drawing | 1-bit canvas at 2 px cells | SVG lines | Canvas letters | SVG dots | | Window across at 1440 px wide | 272 px | 253 px | 272 px | 232 px | | Window across at 375 px wide | 240 px | 276 px | 221 px | 232 px | | Zoom and cap | 3 times on a 39° cap | 2.5 times on a 47° cap | 3 times on a 39° cap | 2.5 times on a 47° cap | | Turn rule | 0.7 s plus 5 ms a degree, within 0.8 to 1.4 s | 0.7 s plus 8 ms a degree, up to 1.5 s | 1 s under 60° and 1.5 s beyond | 0.5 s plus 8 ms a degree, up to 1.2 s | | Measured moves | 853 to 1,443 ms | 788 to 1,368 ms | 1,019 to 1,529 ms | 628 to 1,136 ms | | Longest frame callback | 5.1 ms | 1.5 ms | 19.8 ms | 2.3 ms | | Figure height at 1440, 375 and 320 px | 595, 900 and 1,021 px | 570, 973 and 1,065 px | 608, 914 and 1,061 px | 575, 921 and 1,064 px | | Code in the direction's folder, tests excluded | 1,239 lines | 1,199 lines | 1,201 lines | 671 lines | Each direction's section below runs its figure live in a frame of its round's route: at the 712 px column on a wide screen, scaled down a little on a narrower one, and at the column's own width on a phone. A switch over each figure loads its rounds 2 and 1. The round 3 shoot's contact sheets stay in `public/static/docs/blog-globe/`, one per direction. Each has five rows of stops at 1440 and 375 px wide in both themes, and one frame taken 450 ms into the turn from London to Beijing. A reader without JavaScript sees each direction's sheet in its section instead. ## The dot globe (D) [#the-dot-globe-d] D draws the globe as a screen of square dots in SVG. The drawing is 240 px wide on a 240 unit viewBox at 1440 and 375 px wide. Each 4 px cell prints a whole 2 by 2 px dot where the ground under it is land, and the round window inside the drawing is 232 px across. Every frame reads the land under each cell again, so the earth turns under a screen that stays still. Land prints at 42% of the text color in the light theme and 44% in the dark theme. The current city takes the accent with a ring and a cleared halo, and the other cities in the window print in the text color. The accent is `#2f5ce0` on paper and `#86a8ff` on ink. The label sits under the window, and the names and the passage sit beside it. On a phone the names and the passage sit under the globe. <GlobeFigure variant="d" fallback="{ src: '/static/docs/blog-globe/d-sheet.webp', alt: 'Contact sheet of direction D: a dot globe beside the eight definitions at the five stops in both themes at 1440 and 375 px, and one frame from the middle of the turn from London to Beijing', }" /> Round 3 of D runs live above, and its switch loads rounds 2 and 1. [/d/blog-globe?v=d](https://www.prototemplate.com/d/blog-globe?v=d) shows the figure alone. D reuses the Versions figure's plate and the shared passage, names and targets. A sea cell tests four points around its center for land, so Britain, Cyprus and other islands narrower than a cell still print. ### What works [#what-works] * D has the least code of the four directions: 671 lines, against 1,199 to 1,239 for the others. * It draws no canvas, and CSS alone sets both themes. * Its turns are the shortest (628 to 1,136 ms), and its longest frame callback took 2.3 ms. * The passage steps row by row and the label steps once, both centered on the middle of the turn. * Without JavaScript the radios switch the passage, the label and the lit city. ### What does not work [#what-does-not-work] * The review's main objection to D was that its globe read as a small locator beside the text. The drawing grew from 160 px to 240 px after the review. Its window is still the smallest of the four at 1440 px wide: 232 px across, against 253 px for B and 272 px for A and C. ## The dithered globe (A) [#the-dithered-globe-a] A prints the globe on a canvas in General Translation's 1-bit material. Each 2 px cell is screened through the 8 by 8 Bayer matrix in two nested inks: a light ink for the lit sphere and a strong ink for land. The light comes from the upper left with an ambient term and a rim, as on the sign-in page's globe. The globe is zoomed 3 times behind the window. The window grows with its column in steps of 4 px up to 320 px, and it is 272 px across in the 712 px column and 240 px at 375 px wide. A long turn pulls back on the way until both ends sit in the middle 80% of the window, then closes in again on the same curve. A pin at the window's center leaves while the ground moves and prints again on arrival. The names run across the top of the plate, the label sits under the window and the passage sits beside it. On a phone the names, the window, the label and the passage stack in one column. <GlobeFigure variant="a" fallback="{ src: '/static/docs/blog-globe/a-sheet.webp', alt: 'Contact sheet of direction A: a blue dithered globe beside the eight definitions at the five stops in both themes at 1440 and 375 px, and one frame from the middle of the turn from London to Beijing', }" /> Round 3 of A runs live above, and its switch loads rounds 2 and 1. [/d/blog-globe?v=a](https://www.prototemplate.com/d/blog-globe?v=a) shows the figure alone. A forks `globe()`, the lit sphere the sign-in globe draws, from `packages/ui/src/lib/dither.ts` in gt-cloud. The fork faces any point and reads `landAt()` in place of noise. Its two inks are the Versions figure's pair: `#b4c8ff` and `#2f5ce0` on paper, `#2448b0` and `#86a8ff` on ink. Without JavaScript the window shows an SVG still of the first stop, drawn from each ink's runs of cells on a 160 unit viewBox by the same functions as the canvas. ### What works [#what-works-1] * A reads most like a real globe. Europe, Africa, Arabia and Asia are clear at both widths and in both themes, and the light and the rim give the sphere volume. * The words change in the middle of the turn, while the ground moves fastest. * The pull back on a long turn shows where the globe is going. The 73° turn from London to Beijing is wider than the 39° window, and the reader sees both ends during the move. ### What does not work [#what-does-not-work-1] * The whole disc prints in the accent and its tint, and the pin prints in ink. The brand keeps the accent for small elements. The Versions figure prints its field in the same blue pair, so the choice has a precedent and is still open. * A has the most code of the four (1,239 lines), and its longest frame callback took 5.1 ms. ## The line globe (B) [#the-line-globe-b] B draws the globe in SVG lines through a round lens: coastlines, a 10° graticule cut at the lens, a single 1 px limb and the cities as small squares. A city the route has reached is filled, a city still ahead is open, and the current city takes the accent. The route from Alexandria to the current stop is a doubled line of two 1.5 px lines 3 px apart, and it draws on with the turn. During a move the label counts the coordinates the globe faces and names no city. The leaving city and year step out as the move starts, and the arriving ones step in as the globe lands. The globe is zoomed 2.5 times behind the lens, which is 253 px across in the 712 px column and 276 px at 375 px wide. <GlobeFigure variant="b" fallback="{ src: '/static/docs/blog-globe/b-sheet.webp', alt: 'Contact sheet of direction B: a line globe with coastlines, a graticule and a doubled route line beside the eight definitions at the five stops in both themes at 1440 and 375 px, and one frame from the middle of the turn from London to Beijing', }" /> Round 3 of B runs live above, and its switch loads rounds 2 and 1. [/d/blog-globe?v=b](https://www.prototemplate.com/d/blog-globe?v=b) shows the figure alone. B takes the line grammar of the delivery globe on generaltranslation.com's home page (EdgeGlobe) and gives it a moving camera. The route comes from `greatCircle()`. The coastlines were traced once from the shared land mask by marching squares and simplified by Douglas and Peucker at 0.3°, then baked into `coasts.ts` as 43 rings. Two rounds of corner cutting smooth each ring when the module reads it, so no tracing runs at load. ### What works [#what-works-2] * The coastlines read at a glance, and the accent marks only the current city. * The route shows the order of the stops as the globe turns. The review called it the best single idea of the round. * B has the lightest engine: its longest frame callback took 1.5 ms. CSS alone sets both themes. * Without JavaScript the radios switch the label, the passage, the route and the lit city. ### What does not work [#what-does-not-work-2] * B is the tallest figure on a phone: 973 px at 375 px wide and 1,065 px at 320 px. * The review scored B low on its first morph, a letter scramble, and on phones. The fixes after the review replaced the scramble with the shared dissolve and put the passage under the globe on a phone, and the scores were not taken again. ## The glyph globe (C) [#the-glyph-globe-c] C is a canvas port of the World Language Map's glyph globe. An even grid of upright cells, 36 across the window, is anchored to the window's center. Each cell reads the ground under it and prints a letter of the current language where the ground is land, or a small square where it is sea. The letters come from the stop's first definition and its native name, so the land is written in Greek, Arabic or Latin letters or in Han characters. Each mark's size follows the depth of a sphere seen through the window, so the marks shrink toward its edge. On a switch the letters on the land hand over to the new script through the same 8 by 8 screen as the passage, in the middle of the turn. The globe is zoomed 3 times behind the window, which is 272 px across in the 712 px column and 221 px at 375 px wide. <GlobeFigure variant="c" fallback="{ src: '/static/docs/blog-globe/c-sheet.webp', alt: 'Contact sheet of direction C: a globe drawn in letters of the current script beside the eight definitions at the five stops in both themes at 1440 and 375 px, and one frame from the middle of the turn from London to Beijing', }" /> Round 3 of C runs live above, and its switch loads rounds 2 and 1. [/d/blog-globe?v=c](https://www.prototemplate.com/d/blog-globe?v=c) shows the figure alone. C follows the method of `render-globe.ts` on gt-cloud's `k/language-map` branch, which inverts the projection under each screen cell and prints one upright glyph in it. Each cell keeps its letter while the ground turns under it, so only the coast moves. Before the review's fixes, letters changed on 42% of land cells a frame on the turn from London to Beijing, and on 81% at the peak. ### What works [#what-works-3] * C writes the land in the current script, so the words change on the land as well as in the passage. The review named this hand-over the second idea of the round worth keeping. * The passage, the land and the label all step in the middle of the turn, so the words change as the ground moves. * The server draws one SVG still for both themes. ### What does not work [#what-does-not-work-3] * At 36 marks across, each mark covers about 1.1° of ground at the window's center, so C is the coarsest globe of the four. The review saw 26 marks across and ranked C last because its globe read least as the Earth. * Its longest frame callback took 19.8 ms on the turn from London to Beijing. The longest callbacks of the other three took 1.5 to 5.1 ms. * Its turns are the longest of the four, at 1,019 to 1,529 ms. ## Ranking [#ranking] The design review of the first build scored each direction from 1 to 10 on eight criteria. The fixes that followed closed the defects it listed, and the scores were not taken again. | Criterion | A | B | C | D | | -------------------------------------------------------------- | -- | -- | -- | -- | | The General Translation look: material, color, type and rhythm | 7 | 8 | 6 | 8 | | Reads as a real globe and lands on the city | 9 | 9 | 5 | 7 | | Passage legible in all five scripts | 8 | 7 | 8 | 8 | | Arabic right alignment | 8 | 6 | 9 | 7 | | Morph quality | 8 | 4 | 6 | 8 | | Simplicity of code | 4 | 5 | 5 | 9 | | Both themes | 8 | 9 | 7 | 9 | | Phones | 6 | 5 | 7 | 8 | | Total of 80 | 58 | 53 | 53 | 64 | 1. D, the dot globe, scored 64. In the first build it sat closest to the Versions figure, and it has the least code. 2. A, the dithered globe, scored 58. It has the strongest globe. 3. B, the line globe, scored 53. Its doubled route line is the best single idea of the round. 4. C, the glyph globe, scored 53 and draws the land in the current script. B ranks above C at the same total because B's globe reads as the Earth. The review asked for D and A to be shown first. D is the closest to shipping, and A has the strongest globe. ## Status [#status] The globe figure is an exploration. No direction has shipped, and no published post carries the figure. Round 3's gates passed in gt-cloud: the lint, the type check and 746 tests in 77 files. ## Open choices [#open-choices] 1. The direction that ships is still to be chosen. The review recommends D, with A as the alternative. 2. If A ships, its ink is still to be chosen: the Versions figure's blue pair across the whole disc, or the globe in ink with the accent on the pin alone. 3. B's doubled route line could join the chosen direction, as the review proposed for D or A. 4. C's hand-over of the letters on the land could join the chosen direction. 5. If D ships, its window could grow. It is 232 px across in the 712 px column, against 272 px for A and C. 6. A and C support a guided tour that moves on to the next stop until the reader picks one. It is off by default in both, and the post that carries the figure can turn it on. After the pick, the other three direction folders leave the code and stay in the branch's history. The ease and arc helpers that each direction copies move into `ortho.ts`. The land comes from [Natural Earth](https://www.naturalearthdata.com), which is in the public domain. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # 3D scenes > three.js scenes in the house material: an extruded GT mark, a lit globe and an isometric stack, each through a Bayer post pass with live controls. URL: https://www.prototemplate.com/docs/components/three-scenes Three scenes draw General Translation's material in three.js. Each scene renders into an offscreen target, and a fullscreen pass prints that target through the house Bayer screen in the stage's ink and paper, so the 3D work reads as the same 1-bit print as the [dither engines](https://www.prototemplate.com/docs/components/dither). Each plate holds one WebGL context while it is near the viewport and releases it when you scroll away. The page loads three.js for itself, and no other docs page carries it. ## The extruded mark [#the-extruded-mark] The GT mark is parsed from its SVG outline, extruded and turned under one light. Pass shows the screen, the continuous light the screen reads, or both on either side of a seam, and a drag or the arrow keys turn the mark by hand. <MarkSceneDemo /> The outline is `REFERENCE_MARK` from `src/lib/marks.ts`, the evenodd path the deck draws as `#gt-mark`, so the gaps between the doubled lines come from the fill rule. The extrusion is 150 path units deep with no bevel. Every face is flat, and each inner wall of a doubled line takes the light on its own. Edges that turn 28 degrees or more are drawn as hairlines, which keeps the contours and the corners and drops the seams between the short facets of the G's arc. The mark swings 54 degrees each way on a 12.6 second sine. The light stays 38 degrees above the horizon while Light moves it around the mark. ### Import and props [#import-and-props] ```tsx import { MarkSceneDemo } from '@/components/docs/demos/2d-3d/lazy'; <MarkSceneDemo matrix={8} cell={2} light={-50} />; ``` | Prop | Type | Default | What it does | | -------- | --------------- | ------- | -------------------------------------------------------------------------------------------- | | `matrix` | `2`, `4` or `8` | `8` | The Bayer matrix's side at mount. | | `cell` | number | `2` | A cell's side in CSS pixels at mount. The pass rounds it to whole device pixels. | | `light` | number | `-50` | The light's azimuth in degrees. 0 faces the viewer and a negative angle comes from the left. | The scene is `buildMarkScene(env, read)` in `mark-scene.ts`. It parses and extrudes the outline once per page, and every later context uploads the same buffers. ## The lit globe [#the-lit-globe] The globe carries the real coastlines and a sun the reader can swing. The coast and a 15 degree graticule are hairlines one device pixel wide at every cell size. <EarthSceneDemo /> The land is the blog globe's 1 degree mask, drawn from Natural Earth 4.1.0 (public domain) and read into a 360 by 180 texture. The GPU's linear filter interpolates between the cells, and the shader draws the coast where the filtered value crosses 0.5, measured with `fwidth` so the line is one pixel wide wherever it falls. Land takes more of the light than the sea, the sea carries a specular glint, and the axis leans 23.44 degrees. A ring at the limb, sized for the perspective camera, keeps the night side's outline on a dark ground. The globe prints its light in ink on both grounds, as the [globe field](https://www.prototemplate.com/docs/components/globes) does, so on paper its night side is paper with a few cells of ink. ### Import and props [#import-and-props-1] ```tsx import { EarthSceneDemo } from '@/components/docs/demos/2d-3d/lazy'; <EarthSceneDemo matrix={8} cell={2} sun={-85} lines='both' />; ``` | Prop | Type | Default | What it does | | -------- | ----------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `matrix` | `2`, `4` or `8` | `8` | The Bayer matrix's side at mount. | | `cell` | number | `2` | A cell's side in CSS pixels at mount. | | `sun` | number | `-85` | The sun's azimuth in degrees, 18 degrees above the horizon. At -85 the sun stands almost square to the left and the terminator crosses the disc near its middle. | | `lines` | `'both'`, `'coast'`, `'grid'` or `'none'` | `'both'` | The hairlines the shader draws. | The scene is `buildEarthScene(env, read)` in `earth-scene.ts`. It turns at 0.16 radians a second, one turn in 39 seconds. ## The pipeline stack [#the-pipeline-stack] Content, translation and the CDN stand as three isometric plates that open and close on one clock. A drag turns the stack about its axis, and the stack eases back to the isometric view on release. <StackSceneDemo /> The camera is orthographic and looks down the (1, 1, 1) diagonal, which is the iso kit's 30 degree projection. The plates keep the isometric family's proportions: a footprint of 2 units, 4 percent of it thick, and 40 percent of it as air when the stack is open. Each face carries its artifact in its own plane. Content carries the `<T>` component, translation carries the `en`, `fr` and `ja` locale chips with `ja` as the drawing's one accent object, and the CDN carries a square marker for each point of presence. A leader leaves each plate's left vertex at mid-thickness and runs to its label. Light comes from the upper left. On paper the top face takes the least ink, the left face more and the right face the most, and on ink the top face carries the most lit cells, so the top reads lightest in both themes. The lap is 7.2 seconds: closed for 1.6, opening over 1.2 with the top plate leading by 90 ms a plate, open for 2.4, and closing over 1.2 with the bottom plate leading, so no plate passes through the plate below it. The still is the open stack. ### Import and props [#import-and-props-2] ```tsx import { StackSceneDemo } from '@/components/docs/demos/2d-3d/lazy'; <StackSceneDemo matrix={4} cell={2} light={-70} />; ``` | Prop | Type | Default | What it does | | -------- | --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------ | | `matrix` | `2`, `4` or `8` | `4` | The Bayer matrix's side at mount. | | `cell` | number | `2` | A cell's side in CSS pixels at mount. | | `light` | number | `-70` | The light's azimuth from the viewer's line of sight, in degrees, 58 degrees above the plates. The control runs from -135 to 0. | The scene is `buildStackScene(env, read, fonts)` in `stack-scene.ts`, and `openness(t, i)` returns how open plate `i` stands at time `t`, from 0 to 1. ## The stage and its pass [#the-stage-and-its-pass] All three plates run on `createDitherStage`. The scene renders into a target whose four channels hold data: red is the light a surface receives, green marks the accent object, blue is a hairline's strength, and alpha says a surface was drawn there. The pass divides the canvas into square cells of a whole number of device pixels, anchored at the top left so the screen's phase matches the CPU engines. It reads the light at each cell's centre and lights the cell when the coverage is over the matrix threshold. Hairlines add to blue through additive blending, so a line never changes the tone of a cell it crosses. The pass reads them per pixel: a line takes the ground where it crosses dense cells and the ink everywhere else. On ink the coverage is the light. On paper a solid prints its shade, one minus the light, so a lit face reads as paper with sparse ink. A floor of 1.5 percent on ink and 5 percent on paper keeps a few cells on the brightest face, and the accent object keeps at least 30 percent coverage so it reads on a lit face. The stage creates its renderer and a new canvas when the host comes within 300 pixels of the viewport. It builds the scene in the next task and compiles the shaders with `compileAsync` before the first frame, which uses `KHR_parallel_shader_compile` where the browser has it. It releases everything 1.5 seconds after the host leaves that margin, or on `destroy()`: the scene's `dispose()`, the target, the pass, `renderer.dispose()` and `renderer.forceContextLoss()`, and then the canvas leaves the DOM. The loop runs only while the host is on screen and the tab is visible. Under reduced motion the stage draws one still and draws again only when a control or a drag changes the picture. The pixel ratio is capped at 2. ### Import and props [#import-and-props-3] ```ts import { createDitherStage } from '@/components/docs/demos/2d-3d/dither-stage'; import { readInks } from '@/components/docs/demos/2d-3d/ink'; import { buildMarkScene } from '@/components/docs/demos/2d-3d/mark-scene'; const stage = createDitherStage({ host, build: (env) => buildMarkScene(env, () => state), pass: { matrix: 8, cell: 2, mode: 'dither', split: 0.5 }, inks: readInks(host), stillTime: 1.1, }); stage.setPass({ matrix: 4 }); stage.destroy(); ``` | Option | Type | Default | What it does | | ------------------ | ------------------------ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `host` | `HTMLElement` | required | The box the canvas fills. Its distance from the viewport decides when the context exists. | | `build` | `(env) => SceneInstance` | required | Builds the scene for each new context. `env` holds the renderer, the size and the pixel ratio. | | `pass` | `PassSettings` | required | `matrix` (2, 4 or 8), `cell` in CSS pixels, `mode` (`'dither'`, `'tone'` or `'split'`) and the seam's place `split` from 0 to 1. | | `inks` | `StageInks` | required | The ink, ground and accent as sRGB channels. `readInks(el)` reads them from the stage's `--dmo-ink`, `--dmo-ground` and `--dmo-accent`. | | `stillTime` | number | required | The instant on the scene's clock that reduced motion and a paused plate show. | | `speed`, `playing` | number, boolean | `1`, `true` | The clock's rate, and false for the still. | | `onCost`, `onFail` | callbacks | none | The smoothed main-thread time of a frame, at most twice a second, and a call when WebGL is unavailable. | The handle returns `setPass`, `setInks`, `setSpeed`, `setPlaying`, `invalidate` and `destroy`. A scene returns `scene`, `camera`, `update(time, delta)`, `resize(width, height)` and `dispose()`, with three optional fields: `afterRender()` for DOM labels that follow the scene, `gain` to scale the coverage on each ground, and `onPaper: 'light'` to print the light in ink on paper. Its materials come from `materials.ts`: `inkMaterial` for lit faces, `lineMaterial` for hairlines and `glyphMaterial` for artwork laid in a face. ## Numbers [#numbers] The numbers below come from headless Chrome for Testing drawing through ANGLE on Metal on an Apple M5 Max laptop, at 1440 by 900 and a pixel ratio of 2, with each plate scrolled into view and its frames sampled for 6 seconds. The machine was shared with other work, and its one-minute load average read between 33 and 106 during the runs. A frame's main-thread time is the plate's own measure, the one its controls row prints; the GPU's time is not in it. | Scene | Main thread a frame | Frame interval, median and p95 | Frames over 22 ms | Main thread at 4x CPU throttle | | ------------------ | ------------------- | ------------------------------ | ----------------- | ------------------------------ | | The extruded mark | 0.20 ms | 16.7 and 16.7 ms | 0 | 0.71 ms, no frame over 22 ms | | The lit globe | 0.21 ms | 16.7 and 16.7 ms | 0 | 0.63 ms, no frame over 22 ms | | The pipeline stack | 0.18 ms | 16.7 and 16.8 ms | 0 | 1.48 ms, no frame over 22 ms | * Each plate renders 1676 by 760 to 1676 by 880 device pixels twice a frame, once into the target and once through the pass, and held 60 frames a second at that size. * Creating a context takes three tasks. Over three runs the renderer took 9 to 29 ms, the scene and the shader compile 18 to 28 ms, and the first frame 9 to 24 ms. In one task the same work took 42 to 69 ms and landed on a single frame of the scroll. * A scroll from the top of the page to the bottom created 3 contexts and held at most 2 at once. The mark's context was released after the plate left the 300 pixel margin. ## Traps [#traps] * three.js r185 deprecates `SVGLoader.createShapes` and logs a warning for every call. `path.toShapes()` reads the path's `fill-rule`, so an evenodd outline keeps its holes. The type definitions still ask for an argument that the r185 code ignores. * A context lost through `forceContextLoss()` stays with its canvas, and `getContext` on that canvas returns the lost context. The stage makes a new canvas for every context. * A geometry cached for the page still needs `dispose()` when its context goes. The call unhooks the released renderer's listeners, and the next context uploads the same buffers again. * Hairlines drawn into the same target as the faces fight them for depth. The faces take a polygon offset of 1, so a line on a face's edge always passes the depth test. * A hairline that wrote the light channel would set the tone of every cell whose centre it crossed. The lines add to blue only and leave red and alpha as the face wrote them. * A cell of 2 CSS pixels at a pixel ratio of 1.25 is 2.5 device pixels, which prints columns of 2 and 3. The pass rounds every cell to whole device pixels. * Printing the shade on paper made the globe's night side a disc of solid ink. The globe sets `onPaper: 'light'` and prints its light in ink on both grounds, and the two solids keep the shade. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # 2D canvas pieces > Canvas 2D pieces drawn in the house ink: particle fields, generative line work and glyph rain, each with its frame budget and its still. URL: https://www.prototemplate.com/docs/components/canvas-2d Four pieces draw on a plain canvas 2D context in the stage's ink. Two of them print through the house Bayer screen at one buffer pixel per cell, which CSS scales up with `image-rendering: pixelated`. The other two draw hairlines and type at the device's pixel ratio. Each piece states its frame budget beside its measured cost, and the controls row of each animated piece prints its live cost. A piece runs only while it is on screen, and reduced motion shows its still. ## The smoke rake [#the-smoke-rake] Particles leave a rake of nozzles on the left edge and ride a stream that parts around the pointer. Each filament leaves its nozzle one cell wide and spreads into a dithered band as it travels. <FlowFieldDemo /> The stream is a uniform flow of 64 pixels a second plus three travelling cells of a stream function, whose velocity is exact and has no divergence, so particles never bunch or thin out on their own. Around the obstacle the uniform flow becomes the potential flow past a cylinder, and the cells fade out within 2.4 radii, so each filament splits ahead of the pointer and closes behind it. Spread sets a random walk's diffusivity: at 0 the filaments stay thin, and at 10 a filament spreads about 16 pixels either side over one crossing of the stage. Every particle adds to a density grid at its cell, the grid decays by e<sup>−3t</sup>, and the paint thresholds the grid through the matrix. The obstacle follows the pointer and stays where the pointer leaves it, and a tap places it on a phone. Before the first paint the field runs one crossing of the stage at 20 steps a second, so it starts full. The still is the obstacle at 30 percent across the stage with the field simulated around it, and with the plate still, a click simulates it again around the new spot. ### Import and props [#import-and-props] ```tsx import { FlowFieldDemo } from '@/components/docs/demos/2d-3d/lazy'; <FlowFieldDemo cell={2} count={2400} spread={2} matrix={8} />; ``` | Prop | Type | Default | What it does | | -------- | --------------- | ------- | ---------------------------------------------------------------------------------------- | | `cell` | number | `2` | A cell's side in CSS pixels. | | `count` | number | `2400` | Particles in the field, up to 6,000. | | `spread` | number | `2` | The random walk's diffusivity in square pixels a second. The control offers 0, 2 and 10. | | `matrix` | `2`, `4` or `8` | `8` | The Bayer matrix's side. | The engine is `createFlowField({ canvas, cell, count, spread, matrix, ink })` in `flow-field.ts`. It returns `point(x, y, settle)`, `set(patch)` and the `resize`, `step`, `paint` and `still` that `createCanvasLoop` drives. The budget is 2 ms of main thread a frame. The piece measures 1.1 ms at 1440 by 900 and 0.84 ms at 390, and about 14 percent of its cells are lit. ## Contours and hachures [#contours-and-hachures] Contour lines and hachures are the two ways engraved maps draw relief, and this plate draws one drifting height field with either or both. A hill rises under the pointer and stays where the pointer leaves it. <ContourDemo /> The field is four Gaussian hills and basins on slow Lissajous paths, two long waves and the pointer's hill, sampled every 6 CSS pixels (5 under 520). Marching squares turn each level into segments, linearly interpolated, with a saddle split by its cell's mean. Every fifth level is an index contour at 1.5 pixels in full ink, and the rest are 1 pixel lines at half strength. Hachures are short strokes down the slope on a jittered lattice, longer where the slope is steeper. A stroke stays where the slope faces away from a light at the upper left, and a 4 by 4 Bayer threshold per lattice point decides which strokes stay, so the hachures thin out in the Bayer order and the pattern holds still while the field moves. The still is the field at 18 seconds with the hill at 68 percent across. With the plate still, the hill jumps to the pointer and the frame is drawn again. ### Import and props [#import-and-props-1] ```tsx import { ContourDemo } from '@/components/docs/demos/2d-3d/lazy'; <ContourDemo mode='both' interval={0.05} />; ``` | Prop | Type | Default | What it does | | ---------- | -------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------- | | `mode` | `'both'`, `'contours'` or `'hachures'` | `'both'` | The line work drawn. | | `interval` | number | `0.05` | The height between two contours. The field spans about 0 to 1.2, and the control offers 0.035, 0.05 and 0.08. | The engine is `createContours({ canvas, mode, interval, ink })` in `contours.ts`. It returns `point(x, y, snap)`, `set(patch)` and `counts()`, the segments drawn in the last paint. The budget is 2 ms of main thread a frame. The piece measures 1.07 ms at 1440 by 900 and 0.67 to 0.81 ms at 390. ## Glyph rain [#glyph-rain] Sixteen scripts rain down a typeset grid. The cells around the pointer light up by their distance from it, and the plate names the script of the column under the pointer with the word for language in that script. <GlyphRainDemo /> Every column is set in one script, and every cell holds one glyph of it, so the stage is a grid of type before anything moves. A drop is a lit window that steps down its column one row at a time. Its head prints at full ink and its tail fades in six alpha steps. The glyphs never move, and a quarter of the cells change their glyph each second. Depth comes from two grids: a far grid of 10.5 pixel glyphs at a third of the ink under a near grid of 15 pixel glyphs. The scripts are Latin, Greek, Cyrillic, Armenian, Georgian, Hebrew, Arabic, Devanagari, Bengali, Tamil, Thai, Ethiopic, Khmer, Han, Kana and Hangul, each named by its ISO 15924 code. Every frame is a function of the clock alone: a drop's row is the clock times its speed plus its phase, modulo its lap. The still is any instant, and the plate shows 9 seconds, when the grid is already full. ### Import and props [#import-and-props-2] ```tsx import { GlyphRainDemo } from '@/components/docs/demos/2d-3d/lazy'; <GlyphRainDemo drops={1} />; ``` | Prop | Type | Default | What it does | | ------- | ------ | ------- | ---------------------------------------------- | | `drops` | number | `1` | Drops a column. The control offers 1, 2 and 3. | The engine is `createGlyphRain({ canvas, drops, ink, family })` in `glyph-rain.ts`, and `RAIN_SCRIPTS` lists each script with its code, its name, its word for language and the glyphs the rain draws. `point(x, y)` lights the cells around a point and returns the script under it, and `point(null, 0)` turns them off. The budget is 1 ms of main thread a frame. The piece measures 0.32 ms at 1440 by 900 and 0.17 to 0.20 ms at 390. ## The dither pad [#the-dither-pad] Paint tone into the pad and the screen prints it. A stroke adds tone or takes it away, and Matrix and Cell reprint the same tone through another screen. <DitherPadDemo /> The tone is a grid with one value a cell. A stroke lays a soft round brush 28 CSS pixels across every 7 pixels along the pointer's path, and the paint thresholds the grid at once. The tiers nest, so adding tone only turns cells on: a lit cell stays lit as the tone under it rises. Reset draws the starting picture, a ramp across the lower part and a soft disc above it, and the status line prints the share of lit cells. The pad has no loop and draws only on a stroke, so its still is the pad itself. ### Import and props [#import-and-props-3] ```tsx import { DitherPadDemo } from '@/components/docs/demos/2d-3d/lazy'; <DitherPadDemo matrix={8} cell={4} />; ``` | Prop | Type | Default | What it does | | -------- | --------------- | ------- | ----------------------------------------------------------------------- | | `matrix` | `2`, `4` or `8` | `8` | The Bayer matrix's side at mount. | | `cell` | number | `4` | A cell's side in CSS pixels at mount. The control offers 2, 4, 6 and 8. | The engine is `createDitherPad({ canvas, cell, matrix, ink })` in `dither-pad.ts`. It returns `dab`, `stroke`, `reset`, `clear`, `paint`, `set` and `coverage()`. The budget is 1 ms of main thread a pointer move. A move measures 0.13 ms on a pad of 210 by 80 cells, and the pad costs nothing at rest. ## The loop [#the-loop] The three animated pieces run on `createCanvasLoop({ host, engine, playing, onCost })` in `canvas-loop.ts`, the 2D counterpart of the 3D page's stage. The engine supplies `resize`, `step`, `paint` and `still`. The loop paints once at creation, so a piece never shows an empty frame, then steps and paints once per animation frame while the host is on screen, the tab is visible and the plate is playing. It reads the host's size and pixel ratio through a resize observer and a resolution query, so zooming the page resizes the backing store. The handle returns `setPlaying`, `setSpeed`, `invalidate`, `running` and `destroy`. ## Numbers [#numbers] The numbers come from headless Chrome for Testing on an Apple M5 Max laptop at a pixel ratio of 2, with each plate scrolled into view and its frames sampled for 6 seconds. The machine was shared with other work, and its one-minute load average read between 35 and 85 during the runs. A frame's main-thread time is the piece's own measure, the one its controls row prints. | Piece | Budget | Main thread at 1440 | Frames over 22 ms | Main thread at 4x CPU throttle | | --------------------- | ----------- | ------------------- | ----------------- | ----------------------------------------- | | The smoke rake | 2 ms | 1.10 ms | 0 | 5.39 ms, 0.6 percent of frames over 22 ms | | Contours and hachures | 2 ms | 1.07 ms | 0 | 5.64 ms, no frame over 22 ms | | Glyph rain | 1 ms | 0.32 ms | 0 | 1.39 ms, no frame over 22 ms | | The dither pad | 1 ms a move | 0.13 ms a move | no loop | not measured | Every frame interval had a median of 16.7 ms and a p95 of 16.7 or 16.8 ms at full speed. ## Traps [#traps] * The 8 by 8 screen's lowest threshold is 0.5 / 64, under 1 percent, so a density that decays toward zero keeps lighting cells long after it looks gone. The first draft of the flow field spread its particles evenly and decayed slowly, and most of the stage printed. Decaying faster left short dashes that read as noise at 7 percent of the cells. Emitting from a rake of nozzles gave the field lines that hold together, at 14 percent. * The contour field cost 3.0 ms a frame while each grid node evaluated every term with its own sines and exponentials. Each term separates into a column factor times a row factor, and computing those once per column and once per row brought the frame to 1.07 ms. * A dither pattern on a moving glyph shimmers with every step, so the rain moves no glyph. A drop lights cells in place, and depth comes from size and alpha. * The rain's atlas is drawn in white, and every frame ends with one `source-in` fill in the ink. A theme change sets one color, and the atlas is drawn again only when the pixel ratio or the layout changes, or when the page's faces finish loading. * A finger lifts and leaves the stage in the same gesture, so a `pointerleave` after a tap would turn the lit cells off at once. Only a mouse leaving turns them off. * The plates mount their stages only when they near the viewport. An engine created in the plate's own mount effect finds no canvas and never starts, so every engine is created in a child component that renders with the stage. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Controls > The shell and site controls live: segmented switches, toggles, sliders, steppers, the install field and copy buttons, with their states and keys. URL: https://www.prototemplate.com/docs/components/controls The shell's controls come from one small set in `src/components/viewer`: the tool button, the segmented switch built from tool buttons, the count that steps through items, the theme button and the install field. `tokens.css` owns their states, so a hover, a focus ring or a pressed frame looks the same in the toolbar, the index panel and a page head. The plates mount the real components. The toolbar, the install field and the panels read the shell's state through `usePtShell`, which throws outside `ViewerShell`, so those plates mount them inside a small provider in `src/components/docs/demos/interactables/demo-shell.tsx` that publishes the same state. ## The toolbar [#the-toolbar] The plate mounts `Toolbar` over a stage of the brand deck's first twelve slides, with the index panel, the help card, the progress line and the toast that the shell draws around it. Every button acts: the count steps, the seg changes the view, Index slides the panel in, and Theme flips this page. <ShellToolbarDemo /> The bar measures its two groups against its own box and collapses labels in tiers until they fit. Tier one takes the words off Copy link and Fullscreen, tier two off Previous and Next, tier three off Theme, and tier four off Index, Help and Present while the Search field narrows to 120px. Route views shows the bar adapting to the views a route offers: with two views Present leaves, and with one view the seg leaves too. At 900px and below the bar is one 52px row of icon squares, and at 600px and below Previous, Next and Copy link leave. The count is the stepper. Click it and type a number, and Enter goes there. Escape or a click away closes the field, and a number past the end says `No slide 40` in the toast. Previous and Next clamp at the first and last item, and the slider under the plate drives the same active slide. ### Import and props [#import-and-props] ```tsx import { Toolbar } from '@/components/viewer/Toolbar'; <Toolbar title='Brand deck' mark='gt' />; ``` | Prop | Type | What it does | | ------------ | ------------------------------ | ------------------------------------------------------------------------ | | `title` | string | The route's name, shown beside the mark while the list column is closed. | | `mark` | `'gt'` or `'pt'` | The mark beside the title. | | `slot` | node | The route's own controls, first in the right group. | | `modeLabels` | partial record of view to word | The route's own words for the seg, such as `{ slide: 'Live' }`. | Everything else comes from the shell's state: the views, the key table, the item noun, the active item and the open panels. `Progress` takes an optional `value` from 0 to 1; on a paged route it fills to the active item's place as a `scaleX` transform over 160ms. ### Keys [#keys] The plate binds these keys while focus is inside it. In the shell they act anywhere on the page, and Cmd K or Ctrl K opens the search; this page keeps Cmd K for its own search, so the plate opens it with the slash. | Key | Action | | ----------------------- | ------------------------------------------------------------- | | Right arrow, Space | Next slide | | Left arrow, Shift Space | Previous slide | | Home, End | First and last slide | | G, B | Grid view, book view | | / | Open the search | | R | Open or close the index panel | | ? | Open or close the keyboard shortcuts | | D | Flip the theme | | Esc | Back one layer: the shortcuts, the index, then the first view | ### Reduced motion and themes [#reduced-motion-and-themes] Under reduced motion every `--pt-dur-` token is 0ms, so the seg's indicator, the panel's slide, the progress fill and the toast land at once. Every color is a `--pt-` token, so the bar follows the theme. ## The segmented switch [#the-segmented-switch] `Seg` is a ruled group of tool buttons with one active option, shown here in the three forms the shell uses: the toolbar's view switch, the index panel's set switch and the sidebar's icon-only density toggle. Slow stretches the indicator's move to 900ms so it can be watched. <SegDemo /> One ink pill slides under the options. It sits 2px inside the active option with the chip corner, and its place and width move together over 180ms. The group measures the pill with a `ResizeObserver`, so the first measurement forces no layout; until then the active option fills itself, and a render without scripts still shows the choice. A click on the active option returns to the first option when the active one is not the first, as the deck viewer does. After a pick the option lets go of focus. ### Import and props [#import-and-props-1] ```tsx import { Seg } from '@/components/viewer/Seg'; <Seg label='View' options={options} value={mode} onChange={setMode} />; ``` | Prop | Type | What it does | | ----------- | ---------------------------------- | ------------------------------------------------------------------------------------------------- | | `options` | `{ value, label, icon?, title }[]` | The options in order. `title` is the tooltip and names the key, as in `Everything as a grid (G)`. | | `value` | string | The active option's value. | | `onChange` | `(value) => void` | Called with the picked value, or with the first value on a click of the active option. | | `label` | string | The group's accessible name. | | `iconOnly` | boolean | Icon squares with the label as each option's accessible name. | | `className` | string | Extra classes; `is-small` is the sidebar's 28px form. | ### Keys [#keys-1] | Key | Action | | ------------ | ---------------------------------------------------------- | | Tab | Moves to the next option; every option is in the tab order | | Enter, Space | Picks the focused option | ### States [#states] | State | What draws it | | ------ | ---------------------------------------------------------------------------- | | Rest | The option's text in `--pt-ink-2` | | Hover | A plate pill 2px inside the option | | Focus | A 1px ink ring on the option's pill; paper, 3px inside, on the active option | | Active | The shared ink pill under paper text | ## Tool buttons [#tool-buttons] `ToolButton` is every button in the shell: a 32px button whose `title` names its key. With a label it is a text button; without one it is a 32px icon square, and its accessible name is the title without the key. Point at a button in the row or focus it, and the readout prints its tooltip and the name a screen reader hears. The specimen under it draws each state at rest from the shell's own hover and focus rules. <ToolButtonDemo /> The tooltip is the native `title` attribute. A toggle carries `aria-pressed`; `quiet` keeps the attribute and drops the ink frame for a toggle whose state shows elsewhere, as the list toggle does while the list is open. `solid` is the one filled button, Present, which keeps the old nav's 8px corner and draws its play glyph after the word. A tool button has no disabled state: the shell leaves out a control that cannot act, and Previous and Next clamp at the ends. The theme button is a tool button with a text glyph: ◐ in the light theme and ◑ in the dark. Its accessible name says where it goes next (`Switch to light`). The choice lives on `html[data-theme]`, persists under `gt-theme` and reaches every same-origin frame on the page. ### Import and props [#import-and-props-2] ```tsx import { ThemeButton } from '@/components/viewer/ThemeButton'; import { ToolButton } from '@/components/viewer/ToolButton'; <ToolButton icon='index' label='Index' title='Show or hide the index (R)' pressed={open} onClick={toggle} />; <ThemeButton />; ``` | Prop | Type | What it does | | ----------- | ------------ | ------------------------------------------------------------------------------------- | | `title` | string | The tooltip; names the key in parentheses, as in `Dark or light (D)`. | | `onClick` | `() => void` | The action. | | `icon` | icon name | A 16px Heroicons solid glyph from `icons.tsx`. | | `label` | string | The word; absent makes the button an icon square. | | `pressed` | boolean | Sets `aria-pressed` and the ink frame; leave it out on a button that does not toggle. | | `quiet` | boolean | `aria-pressed` without the ink frame. | | `solid` | boolean | The filled call to action. | | `hideSm` | boolean | Hidden at 900px and below. | | `ariaLabel` | string | The accessible name, when the title without its key is not it. | | `children` | node | Replaces the icon, as the theme button's glyph does. | `ThemeButton` takes `label` (the word Theme, and the D key in the title) and `className`. The label is on unless a className is passed, so a className alone gives the direction navs' icon square. ### Keys [#keys-2] | Key | Action | | ------------ | -------------------------- | | Tab | Moves to the next button | | Enter, Space | Presses the focused button | ### States [#states-1] | State | What draws it | | ------- | ------------------------------------------------------------------------------------ | | Rest | `--pt-ink-2` text, no frame | | Hover | Ink text and a 1px `--pt-hair` frame | | Focus | A 1px ink outline on the button's edge | | Pressed | Ink text and a 1px ink frame | | Solid | An ink ground under paper text; hover drops the ground for ink text in the ink frame | Text color, ground and frame move over 120ms with `ease-out`, and nothing else on the button animates. ## The install field [#the-install-field] `InstallField` is the site's one copy control: the command in the mono face in a field at the control corner, with the copy button as the field's last segment. A command breaks only between its groups, and a flag keeps its value, so `--project <dir>` never splits. Width sets the field at the book head panel's 304px, a wider column or a 220px box. <InstallFieldDemo /> A click on the button writes the command to the clipboard, says so in the shell's toast and shows the check glyph for 1.6 seconds. Without a clipboard the toast shows the command itself. One click on the command selects all of it for a manual copy. At 900px and below the copy segment is a 44px target. ### Import and props [#import-and-props-3] ```tsx import { InstallField } from '@/components/viewer/InstallField'; <InstallField command='pnpm skills:install gt-voice --project <dir>' />; ``` | Prop | Type | What it does | | --------- | ------ | -------------------------------------- | | `command` | string | The command as a reader would type it. | The field calls the shell's `say()`, so it mounts under `ViewerShell` or a provider that publishes the same state. `commandGroups(command)` returns the groups a line may break between. ### Keys [#keys-3] | Key | Action | | ------------ | ----------------------------------------------------- | | Tab | Moves to the copy button; the field's frame turns ink | | Enter, Space | Copies the command | ### States [#states-2] | State | What draws it | | ------ | ------------------------------------------------ | | Rest | A 1px `--pt-hair` frame | | Hover | The frame in `--pt-ink-2` | | Focus | The frame in ink while the copy button has focus | | Copied | The check glyph in ink for 1.6 seconds | --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Search, filters and menus > The command search, the list filter, the language selector, the help card and the toast, each mounted with its keyboard behaviour. URL: https://www.prototemplate.com/docs/components/navigation The shell finds things in three ways: the search palette over every page, document, heading and slide; the filter at the head of a list; and the index panel of every page on the site. The help card lists the keys, and the toast confirms an action in one line. Each plate mounts the real component. The palette, the help card and the toast are fixed to the window in the shell, so each plate's stage is a containing block and they open inside it. The help card and the index panel read the shell's state, so their plates mount them in the small provider described on [Controls](https://www.prototemplate.com/docs/components/controls). The language selector comes from the production mirror of the live site. ## The search palette [#the-search-palette] The trigger is the toolbar's field-shaped button: the search glyph, the word Search and a key chip that reads ⌘K, or Ctrl K on Windows and Linux. It opens a card over a scrim with a 34px filter, a count and the results as ruled rows in groups, each row a glyph, a title and its address. Trigger switches to the labeled tool button the direction corner stacks. <SearchDemo /> The index covers pages, documents and their headings, directions, archive versions, brand sections, library anchors and the deck's slides. It is a chunk of its own, about 59K, loaded on the first open or as soon as the pointer or focus reaches the trigger. An empty query shows a short map of the site that fits the card without a scroll region. A plain click on a row opens it, and a click with a modifier keeps the browser's meaning. A row is a real link, so opening one leaves this page. ### Import and props [#import-and-props] ```tsx import { openSearch, Search } from '@/components/viewer/Search'; <Search />; openSearch(); // opens the mounted palette from a key owner ``` | Prop | Type | What it does | | ----------- | --------------------- | ------------------------------------------------------------------------------- | | `trigger` | `'field'` or `'tool'` | The toolbar's field-shaped button, or a labeled tool button. Default `'field'`. | | `className` | string | Extra classes on the overlay. | | `onOpen` | `() => void` | Called as the palette opens, from the button or from a key. | `openSearch()` fires one document event that every mounted palette answers, so the key owner never holds the palette's state. `preloadSearch()` starts loading the index ahead of the first open. ### Keys [#keys] | Key | Action | | --------------- | -------------------------------------------------------------------------------- | | Cmd K, Ctrl K | Opens the palette in the shell (on this page, / while focus is inside the plate) | | Up, Down arrows | Move the active row | | Home, End | First and last row, while the query is empty | | Enter | Opens the active row | | Esc | Closes the palette | The pointer makes the row under it the active one. ### Reduced motion and themes [#reduced-motion-and-themes] The scrim fades in over 160ms and the card fades and rises 8px over 200ms; under reduced motion both appear at once. The card is paper with a `--pt-hair` frame in both themes. At 900px and below the addresses leave the rows, and at 600px and below the key line at the foot leaves too. ## The list filter [#the-list-filter] `SidebarFilter` is the filter row at the head of a list: the field with the search glyph, a clear button while it holds text, and the count at the row's right end. It is controlled, so the host owns the query and the keys. The plate filters the docs pages by title and address. Size sets the field at the index panel's 34px and 13.5px or the sidebar's 30px and 13px. <FilterDemo /> In the sidebar, Enter opens the first match. On the plate, Enter marks the first match and moves focus to it, and Enter on a focused row follows its link. The clear button empties the field and puts focus back in it. ### Import and props [#import-and-props-1] ```tsx import { SidebarFilter } from '@/components/viewer/SidebarFilter'; <SidebarFilter value={query} onChange={setQuery} onKeyDown={onKey} placeholder='Filter pages' count='14 pages' inputRef={input} />; ``` | Prop | Type | What it does | | ------------- | ----------------- | --------------------------------------------------- | | `value` | string | The query. | | `onChange` | `(value) => void` | Called on every edit and on clear. | | `onKeyDown` | keyboard handler | The host's keys. | | `placeholder` | string | The field's placeholder, as in `Filter pages`. | | `label` | string | The accessible name; the placeholder when absent. | | `count` | string | The count at the row's right end, as in `14 pages`. | | `inputRef` | ref | The input, for the host's focus moves. | | `className` | string | Extra classes on the row. | `--pt-filter-h` and `--pt-filter-fs`, set on `.pt-filter`, size the field. ### Keys [#keys-1] These are the sidebar's keys, which the plate's host repeats. | Key | Action | | ------------------------ | -------------------------------------------------------- | | Esc | Clears the query; with the field empty, lets go of focus | | Down arrow | Moves focus to the first row | | Enter | Takes the first match while the field holds text | | Up, Down arrows on a row | Walk the rows; Up on the first row returns to the field | | Home, End on a row | First and last row | ### States [#states] | State | What draws it | | ----- | -------------------------------------------------------------- | | Rest | A 1px `--pt-hair` frame, the glyph and placeholder in titanium | | Focus | The frame and the glyph in ink | | Text | The clear button inside the field, 4px from its end | ## The language selector [#the-language-selector] The live site's footer seats its language selector at the right end of its last strip. The trigger is the language glyph, the current language's name and a chevron, and it opens a popover of the site's eight locales above itself. The plate mounts the production mirror's rebuild, which is a stub: choosing a language closes the popover and changes nothing. <LanguageSelectorDemo /> The popover's right edge sits on the trigger's right edge, 4px above it. Where that would put its left edge inside 16px of the window's edge, as on a phone, it moves right. Each flag is a flag-icons sprite at 16px, and the active row carries a check. ### Import and props [#import-and-props-2] ```tsx import LanguageSelector from '@/app/d/production/sections/LanguageSelector'; <div className='toolchain-root prod-root'> <LanguageSelector /> </div>; ``` The selector takes no props. Its rules live in `prod-chrome.css` under `.prod-root`, and its colors under `.toolchain-root.prod-root`, so the host carries both classes. The live site's selector is `LanguageSelector.tsx` in gt-cloud's shared UI package. ### Keys [#keys-2] | Key | Action | | --------------------- | --------------------------------------------------- | | Enter, Space | Opens the popover from the trigger | | Tab | Walks the rows | | Enter, Space on a row | Picks it and closes the popover | | Esc | Closes the popover and returns focus to the trigger | A pointer press outside the selector also closes it. The mirror's rows are radios in a radio group with no arrow-key movement, so Tab walks them. ## The help card and the toast [#the-help-card-and-the-toast] `HelpCard` lists the keys of the route it opens on. Its rows come from the shell's one key table (`shellKeyRows`), grouped as Move, View, Panels and Theme, so Route switches between a paged route and a flow route and the card follows: the paging rows and the view letters appear only where they act. `Toast` is the one-line notice over everything; Say shows it for its hold, and a second Say restarts the timer. <HelpToastDemo /> A click anywhere on the card closes it. The key owner handles Escape: in the shell that is `useShellKeys`, and on the plate it is the plate's own handler. ### Import and props [#import-and-props-3] ```tsx import { HelpCard } from '@/components/viewer/HelpCard'; import { Toast, useToast } from '@/components/viewer/Toast'; const toast = useToast(); <HelpCard />; <Toast message={toast.message} on={toast.on} />; toast.say('Link to slide 12 copied'); ``` | Export | Props | What it does | | ----------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------- | | `HelpCard` | `rows?`, `note?` | The card for the shell's route. `rows` replaces the derived rows and `note` the footnote; an empty note hides it. | | `Toast` | `message`, `on` | The notice. It keeps its text through the fade and is a polite live region. | | `useToast(hold?)` | hold in ms, default 1400 | Returns `{ message, on, say }`; `say(msg, hold?)` shows a message and restarts the timer. | Inside the shell, components call `say()` from `usePtShell()` and never mount a toast of their own. ### Keys [#keys-3] | Key | Action | | --- | ------------------------------------------------------------------------- | | ? | Opens and closes the card (on this page, while focus is inside the plate) | | Esc | Closes the card | ### Reduced motion and themes [#reduced-motion-and-themes-1] The card's scrim fades over 160ms and the card fades and rises 8px over 200ms. On close the card stays mounted for 160ms so both can fade back out. The toast fades and rises 4px over 160ms, centered 22px above the bottom edge, and at 600px and below it hangs 8px under the toolbar. Under reduced motion none of them moves. The toast is paper text on an ink plate with a 1px paper outline, so it separates from dark content in either theme. ## The index panel [#the-index-panel] `IndexPanel` is a 460px column that slides in from the right over a scrimmed stage, under the toolbar. It lists one of two sets from `src/lib/surfaces.ts` in groups, each row a 96 by 54 preview, a name, an address and a line of description, with a switch between the pages on this site and the public places General Translation is live. Opens on picks the set the panel starts on. <IndexPanelDemo /> The panel builds its rows on its first open, so a route where it stays closed computes nothing. It focuses its filter on every open at desktop width. The filter is a case-insensitive match on a row's text and address, and a group with no match hides. A row on this site opens in place, and a public place opens in a new tab, so a row on the plate leaves this page. ### Import and props [#import-and-props-4] ```tsx import { IndexPanel } from '@/components/viewer/IndexPanel'; <IndexPanel set='site' />; ``` | Prop | Type | What it does | | ----- | ---------------------- | -------------------------------------------------------- | | `set` | `'site'` or `'public'` | The set the panel opens on. | | `ref` | ref | The aside, for the shell's measure of the panel's width. | The panel reads `panelOpen` and `setPanel` from the shell. The shell draws the scrim (`.pt-panel-scrim`) inside the stage, so a click on it closes the panel. ### Keys [#keys-4] | Key | Action | | ------------------- | -------------------------------------------------------------------------- | | R | Opens and closes the panel (on this page, while focus is inside the plate) | | Enter in the filter | Opens the first match | | Esc | Closes the panel and lets go of focus | | Tab | Walks the set switch, the filter and the rows | ### Reduced motion and themes [#reduced-motion-and-themes-2] The panel and its scrim move over 180ms; under reduced motion the panel appears in place. Each preview shows its light or dark capture with the theme, and a row without a capture shows its initial on a blank plate. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Kinetic text and carousels > Split-flap text, the language wheel and the carousel: moving pieces with their timing, their reduced-motion stills and their controls. URL: https://www.prototemplate.com/docs/components/kinetic Three pieces on the site move on their own: the split-flap board that spells the blog's masthead, the language wheel with its orbiting scripts, and the blog's image gallery with its autoplay. Each plate mounts the real component with controls for its timing, and each has a still that a reader with reduced motion sees. The pieces follow the motion rules: one clock per piece, loops that pause off screen, and the designed still in the markup before any script runs. ## Split-flap text [#split-flap-text] `FlapChars` renders a string as cells, and the flap engine flips them. Each cell holds an invisible ghost of its settled glyph in the flow, which keeps the measure, and a face drawn over it, which is the only node the engine touches. The line cannot reflow during a flip, and it reads as plain text before any script runs. <FlapDemo /> `flipUp` folds each face shut over 26ms, swaps its glyph at the squeeze and unfolds it over 30ms, passing through Latin and Greek capitals, CJK, Hangul, digits and marks. Each tile folds `cycles` times, the last fold landing its own glyph, and every odd tile folds once more. The landing glyph unfolds over 90ms with a short overshoot as the flap hits its stop, the one overshoot the motion rules allow. With the flash, the landed glyph holds the accent for 120ms and cools to its ink over 850ms. The tiles start `per` seconds apart from left to right. At FlapPhrase's settings, 13ms apart and two cycles, the masthead's 22 tiles have landed after 0.5 seconds, and with the flash the last glyph has cooled at 1.41 seconds. Tiles draws the ghost tile the masthead sets under every settled character. ### Import and props [#import-and-props] ```tsx import FlapPhrase from '@/app/d/production/sections/FlapPhrase'; import { blankLine, flipUp } from '@/app/d/production/sections/flap'; import { FlapChars } from '@/app/d/production/sections/FlapText'; <FlapPhrase text='General Translation Blog' flash={false} />; <span ref={line} data-tb-line aria-hidden> <FlapChars text={text} /> </span>; blankLine(line.current); flipUp(line.current, { per: 0.013, cycles: 2, flash: true }); ``` | Export | Props | What it does | | ----------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `FlapPhrase` | `text`, `flash` (default true) | Flips the phrase once as it scrolls into view, at 88 percent of the viewport, with a text twin for screen readers. | | `FlapChars` | `text` | The cells; words wrap only at real spaces. | | `blankLine(line)` | an element | Empties every face; the ghosts keep the layout. | | `flipUp(line, options)` | `per` (seconds, default 0.016), `cycles` (default 2), `flash` (default true) | Flips every face to its target and returns the GSAP timeline. | The flash takes `--tb-flash`, then `--tc-accent`, then the face's own ink. The cells' rules (`.tb-ch`, `.tb-ch-g`, `.tb-ch-f`, `.tb-word`, `.tb-sr`) live in the host's stylesheet: the blog index scopes them under `.blog-root`, and this plate under its own stage. ### Reduced motion and themes [#reduced-motion-and-themes] Under reduced motion `FlapPhrase` never blanks the line, so the settled text is the still; Still on the plate shows it. The faces take the line's ink and the ghost tiles mix their tone from it, so both follow the theme. ## The language wheel [#the-language-wheel] `LanguageWheel` is a machined dial: a metal bezel, a bright arc that laps the ring, script glyphs that orbit and sink toward the bezel, and the GT mark at the center. Lap sets the seconds per revolution, Arc the share of the ring the arc covers, and Size the dial's width. <LanguageWheelDemo /> The arc is one circle stroke with a `pathLength` of 100 and a dash of the arc's share, rotated a full turn per lap with no easing. A ScrollTrigger plays it while the wheel is on screen and pauses it off screen. Each orbiting glyph keeps its own spin on GSAP's ticker, on an ellipse 0.62 as tall as it is wide; its radius shrinks 7px a second, it fades as it nears the bezel, and it starts again at the outside. The ticker runs while the wheel is mounted and stops on a hidden tab. The lens stacks three `backdrop-filter` shells with radial masks, so the plate runs a band of language names behind the wheel for it to bend. ### Import and props [#import-and-props-1] ```tsx import LanguageWheel from '@/components/shared/LanguageWheel'; <LanguageWheel size={244} arcDuration={3.6} arcSweep={17} />; ``` | Prop | Type | Default | What it does | | -------------------------------- | ---------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------ | | `size` | number | `--gtw-size`, 244px | The dial's diameter in pixels. The orbit radii are read from it on mount and on a window resize. | | `arcDuration` | number | 3.6 | Seconds for one lap of the arc. | | `arcSweep` | number | 17 | The arc's share of the ring, 0 to 100. | | `orbit` | boolean | true | The orbiting glyphs. | | `glyphs` | string | 20 script glyphs | The glyphs that orbit, one per character. | | `lens` | boolean | true | The three backdrop-filter shells. | | `logoSrc`, `logoAlt`, `logoSize` | string, string, number | the GT mark, empty, 34.4 percent of the dial | The mark at the center. | | `priority` | boolean | false | Loads the mark first, for a hero. | | `className` | string | none | Extra classes on the root. | The bezel, the arc, the track, the glyphs, the halo and the core read `--gtw-` custom properties, so a host restyles the wheel from its own sheet. The glyphs' face reads `--gtw-mono`, which this plate sets to the house face. The wheel is hidden from assistive technology. ### Reduced motion and themes [#reduced-motion-and-themes-1] Under reduced motion, and with the orbit off, the glyphs park at their starting places at 0.4 opacity and the arc holds still. The wheel's colors are tuned to an ink ground, so the plate is ink in both themes. ## The carousel [#the-carousel] `CarouselGallery` is the blog's image gallery: a scroll-snap track that loops, a click on the left half of the image that goes back and on the right half that goes forward, chevrons over soft scrims that surface on hover or focus, and dots. The plate shows the docs redesign post's four figures of its UI primitives. Autoplay sets the interval, and Images shows the one-image form, which has no arrows, no dots and no autoplay. <CarouselDemo /> The track is the source of truth for the visible slide, and a relative command steps from the last requested slide, so quick presses land one slide each. Two commands within 350ms are one, so a double click or a held key moves one slide. A press that travels more than 8px is a drag, and it does not step. Autoplay pauses while the pointer is over the gallery, while focus is inside it, while less than half of it is on screen, on a hidden tab, and for one interval after the reader's last command. ### Import and props [#import-and-props-2] ```tsx import CarouselGallery from '@/components/blog/CarouselGallery'; import Carousel, { CarouselItem } from '@/components/blog/Carousel'; <CarouselGallery label='UI primitives' items={[{ src, alt, width: 3840, height: 2160 }]} />; <Carousel label='UI primitives' width='3840' height='2160'> <CarouselItem src='/static/blogs/figure.webp' alt='What the figure shows' /> </Carousel>; ``` | Prop | Type | Default | What it does | | ---------- | --------------------------------- | -------- | -------------------------------------------------------------------------- | | `items` | `{ src, alt, width?, height? }[]` | required | The images in order. | | `label` | string | none | The gallery's accessible name. | | `interval` | number | 4500 | Milliseconds between automatic steps; 0 turns autoplay off. Read on mount. | `Carousel` is the MDX face. Blog MDX blocks JavaScript expressions, so a post lists its images as `CarouselItem` children with string attributes, and `Carousel` turns them into the items the gallery takes. The gallery's rules live in `src/app/blog/blog.css`. ### Keys [#keys] | Key | Action | | --------------------------------- | ------------------------------------------------------------------- | | Tab | Focuses the track, then the Previous and Next arrows, then each dot | | Left, Right arrows on the track | Previous and next image | | Enter, Space on an arrow or a dot | Steps, or jumps to that image | A live region reads the position, as in `2 / 4`, after each move. ### Reduced motion and themes [#reduced-motion-and-themes-2] Under reduced motion autoplay stays off, a step jumps to the image with no scroll animation, and the scrims, arrows and dots change state with no transition. The dots read the page tokens. The scrims stay dark and the arrows white over the image in both themes. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Locale tags and language pieces > Locale chips with their flags, the two flag resolvers side by side, and seven animated language diagrams built from real text in real scripts. URL: https://www.prototemplate.com/docs/components/language A locale code in a GT interface is a chip with its flag first and the code in mono after it. Two components draw the flag, and they answer from different data. The language diagrams next door show what localization does to text: width, plural rules, direction, context and script. They are built from HTML text, so the browser shapes Arabic, stacks Devanagari and resolves bidi. ## Locale tags and flags [#locale-tags-and-flags] The plate resolves one locale code with both components. Type any code or pick one of the examples, and switch the host chip between a pill and bare text. The set under the comparison shows LocaleTag across twelve codes in the chosen chip. <LocaleTagDemo /> `LocaleTag` reads its own map of 20 languages, and an explicit region in the code wins, so `en-GB` shows the British flag and `zh-Hant` resolves through `zh`. `LocaleFlag` asks `Intl.Locale` for the likely region, the CLDR data gt-cloud's own resolver reads. The two agree on most codes. They differ on a few, such as `ar`, for which LocaleTag shows the flag of Saudi Arabia and LocaleFlag the flag of Egypt. A code neither knows renders with no flag, as `tlh` does. ### Import and props [#import-and-props] ```tsx import LocaleTag from '@/app/d/toolchain/components/LocaleTag'; import LocaleFlag from '@/components/shared/LocaleFlag'; <span className='chip'> <LocaleTag code='ja' /> </span>; <LocaleFlag locale='pt-BR' />; ``` | Component | Prop | Type | What it does | | ------------ | -------------------------- | ------ | -------------------------------------------------------------------------------- | | `LocaleTag` | `code` | string | The locale code, printed as it is after its flag. | | `LocaleTag` | `className` | string | Added to the `.lct` span. The host supplies the chip's box. | | `LocaleFlag` | `locale` | string | The locale whose likely region's flag it draws. | | `LocaleFlag` | `className` and span props | | Passed to the `fi fi-<country>` span, which is hidden from assistive technology. | Both draw the flag-icons SVG pack through its `fi fi-<country>` classes, so every platform prints the same flag at text size, and neither ever draws an emoji. ### When to use it [#when-to-use-it] Use `LocaleTag` inside any chip, cell or label that shows a bare locale code. Use `LocaleFlag` beside a language's name in a selector, where it matches gt-cloud's shared `LocaleFlag` component. Flags render only through these two. A hand-written `fi` class fails the `gt-ui/no-raw-locale-flags` lint, and BRAND.md rules out an emoji flag. ### Reduced motion and themes [#reduced-motion-and-themes] Neither moves. The flag artwork has its own colors in both themes, and the code takes the ink of its host. ## Language diagrams [#language-diagrams] The plate mounts the Toolchain direction's seven language diagrams one at a time. Sentence width measures one sentence in four locales against the English width, Plurals cycles one count through the plural rules of English, Polish and Japanese, and Right to left mirrors one panel by its `dir` attribute alone. Context resolves "Save" to two German verbs, Expansion charts how much room each locale needs, Scripts sets the word for language in eight writing systems, and One term prints one interface term in every locale it ships in. <LangDiagramDemo /> Accent spends the accent hue on the diagram's one accent element, and Replay remounts the diagram, which restarts its loop. ### Import and props [#import-and-props-1] ```tsx import PluralForms from '@/app/d/toolchain/diagrams/lang/PluralForms'; <PluralForms accent title='One count against the plural rules of English, Polish and Japanese' />; ``` Every diagram takes the same three props, the `LangProps` contract in `src/app/d/toolchain/diagrams/lang/lang.ts`. | Prop | Type | Default | What it does | | ----------- | ------- | ------- | ---------------------------------------------------------------------- | | `accent` | boolean | `true` | Spends the accent on the diagram's one accent element. | | `title` | string | none | The accessible name. Without one the diagram is decorative and hidden. | | `className` | string | none | Added to the `.lang` root. | | Diagram | File | What it shows | | -------------- | -------------------- | ----------------------------------------------------------------------------------------------- | | Sentence width | `SentenceWidth.tsx` | One sentence in four locales, every width measured by the browser, Arabic anchored to the right | | Plurals | `PluralForms.tsx` | The plural form CLDR selects for one count in three languages | | Right to left | `RtlMirror.tsx` | One panel twice, identical markup with only `dir` changed | | Context | `ContextResolve.tsx` | "Save" as `speichern` for a file and `sparen` for money | | Expansion | `ExpansionBars.tsx` | Text expansion per locale as a diverging chart around English | | Scripts | `ScriptSampler.tsx` | The word for language in eight writing systems at once | | One term | `WordMorph.tsx` | One interface term in every locale it ships in | ### When to use it [#when-to-use-it-1] Use a language diagram to make one claim about localization in a feature cell or a docs page, with the claim written beside it. Each diagram is complete at rest, so a still or a screenshot reads on its own. ### Reduced motion and themes [#reduced-motion-and-themes-1] Under reduced motion every diagram freezes at its most legible frame. The ink derives from `currentColor` and the accent from `--iso-accent`, so the diagrams follow the docs theme, and the same ancestor themes the isometric drawings. ## Traps [#traps] * The fallback face next/font writes for the Inter binding is `local("Arial")` with `size-adjust: 107.89%`. Arial has Hebrew and Arabic glyphs, so in any stack that starts with the Inter variable those two scripts render in Arial and never reach a script face listed after it. CJK and Devanagari are absent from Arial and fall through to the listed faces. Set an Arabic or Hebrew stack on its own rule, a `:lang()` selector or a class, with no Inter variable in front, as the deck's `.lang .ar` does ([The correct Inter in practice](https://www.prototemplate.com/docs/craft/type#scripts-and-the-arial-fallback)). * The canvas renderer of lottie-web stops drawing a frame at a character missing from the animation's glyph list, and no layer after it draws. The `\r` that lottie-web writes where it wraps box text counts as such a character, so a longer translation can break where the source did not. The error surfaces only as the animation's `error` event and never in the console. Give every drawable character an empty glyph entry, plus `\r` and `\u0003` ([Lottie on canvas](https://www.prototemplate.com/docs/engineering/lottie#a-missing-glyph-stops-the-frame)). --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Reveal seam and sentence motion > The slide-to-reveal seam between a rendered string and its served file, and the headline that dissolves into glyphs and reassembles in the next language. URL: https://www.prototemplate.com/docs/components/motion Two instruments move text without moving layout. The reveal seam uncovers the source behind a rendered result as the reader drags its handle, and neither layer moves. The sentence reassembler changes a headline's language by turning its ink into glyph dust and printing the next sentence through it, and the box tweens its width once per change. ## The reveal seam [#the-reveal-seam] The plate stacks two full-width layers in one box: the rendered interface on top and the translation file the build served for it beneath. Drag the seam, or focus it and press the arrow keys, and the file is uncovered in place. Locale switches both layers, Tour drives the cut from outside the component as a guided intro would, and Reset returns it to rest. <RevealSeamDemo /> The seam writes one custom property, `--seam-cut`, on the box, and causes no React render while it moves. The lower layer clips itself with `clip-path: inset(0 0 0 var(--seam-cut))`, so the window moves and the picture stays put. ### Import and props [#import-and-props] ```tsx import RevealSeam from '@/app/d/toolchain/sections/RevealSeam'; const box = useRef<HTMLDivElement>(null); <div ref={box} className='seam-box' style={{ '--seam-cut': '58%' }}> <div className='seam-top'>{rendered}</div> <div className='seam-under'>{source}</div> <RevealSeam boxRef={box} ariaLabel='Reveal the served translation file' defaultCut={58} /> </div>; ``` | Prop | Type | Default | What it does | | ------------- | -------------------- | -------- | ---------------------------------------------------------------------------------------------------------- | | `boxRef` | ref | required | The stacked box the seam cuts. It receives `--seam-cut`. | | `ariaLabel` | string | required | The slider's accessible name. | | `defaultCut` | number | `70` | The resting cut in percent, for the initial `aria-valuenow`. Set the box's `--seam-cut` to the same value. | | `onInteract` | `() => void` | none | Fires on the first touch or key press, so a host can stop a guided tour. | | `onCutChange` | `(pct, box) => void` | none | Fires on every cut the seam applies, to drive companion values. | The handle is a `span` with the full slider role. The arrow keys move it 5 percent, and the pointer is captured so a fast drag and a touch drag hold. The host's stylesheet draws the handle (`.tc-seam`) and the clip, so each surface sets its own threads and grip. ### When to use it [#when-to-use-it] Use the seam wherever a page shows a result and the source behind it: a rendered string and its JSON, a page and its translation, a before and an after of one layout. Both layers have to be full width and pinned, so the comparison reads cell for cell. ### Reduced motion and themes [#reduced-motion-and-themes] The seam only moves when the reader moves it. The grip's hover scale has no transition under reduced motion, and a host's guided tour must not start, which is why Tour is off on this plate under reduced motion. The threads and the grip read this page's tokens, so the seam follows the docs theme. ## The sentence reassembler [#the-sentence-reassembler] The plate mounts `EverySentence` with the dossier hero's own headline in seven languages. On every change the sentence dissolves into glyph dust seated on its own ink, the dust disperses, and the next sentence prints through the settled swarm behind a hard clip front. Locale calls `setLocale` once, Hops sets how many arrangements the dust takes on the way, and Auto runs the host's clock, one locale every 3.8 seconds. <EverySentenceDemo /> The component runs no timer. The host owns the one clock and calls `setLocale` on the ref; calls are debounced by a quarter second, a call during the dissolve retargets the coming print, and a call during the print stops it and dissolves again. ### Import and props [#import-and-props-1] ```tsx import EverySentence, { type EverySentenceHandle } from '@/components/shared/EverySentence'; const every = useRef<EverySentenceHandle>(null); <EverySentence ref={every} initial='en' hops={2} words={{ en: { text: 'Launch in every language', lang: 'en' }, ja: { text: 'あらゆる言語でローンチ', lang: 'ja' }, }} />; every.current?.setLocale('ja'); ``` | Prop | Type | Default | What it does | | ---------- | -------------------------------------- | -------- | ------------------------------------------------------------------------------------------------ | | `words` | `Record<string, { text, lang, rtl? }>` | required | The sentence in every locale. An unknown locale is ignored. | | `initial` | string | required | The locale the server renders. It must be a key of `words`. | | `hops` | number | `2` | Arrangements the dust takes between sentences, 1 to 5. A change lands at the next print. | | `armDelay` | number | `1.8` | Seconds before the first morph may run. 0 arms at once. | | `ref` | `Ref<EverySentenceHandle>` | none | `setLocale(locale)`, the only way in. Calls before the engine boots are kept and served at boot. | ### When to use it [#when-to-use-it-1] Use the reassembler for one headline whose subject is that it reads in every language, driven by a locale belt or a slow clock. Its width follows the moving-type law: the whole sentence is measured from a hidden probe carrying its own `lang` and `dir`, and the box tweens once per change, so Arabic keeps its joins and Devanagari its conjuncts. Keep every beat longer than the print, about 3.2 seconds, so each morph completes. ### Reduced motion and themes [#reduced-motion-and-themes-1] Under reduced motion the change is a plain swap of the text, `lang` and `dir`, and this plate's Auto clock never starts. Under 720 pixels the dissolve runs as a cheaper cut with fewer motes. Every color derives from `currentColor` and the type from the host's computed font, so the sentence follows the docs theme and face. ## Traps [#traps] * The seam's `--seam-cut` is an inherited custom property, so every move of the seam restyles the whole subtree of its box. An SVG figure of 27,800 nodes under the seam dragged at 5 fps at 4x CPU throttle, and the same figure drawn on a canvas dragged at 56 fps. Keep the subtree under a moving variable small, and leave the shared seam as it is ([Canvas and WebGL engines](https://www.prototemplate.com/docs/engineering/engines#inherited-properties-on-a-moving-subtree)). * A standing `will-change` or a leftover 0 px transform makes a stacking context, and a stacking context paints over the 1 px seams beside it. Set `willChange` only for the length of a tween, and clear what the tween set (`transform`, `opacity`, `visibility`, `strokeDashoffset`) when it completes ([Lines and seams](https://www.prototemplate.com/docs/craft/lines#stacking-contexts-over-seams)). * GSAP cannot be frozen from outside the page. Its ESM build exposes no `window.gsap`, and its ticker keeps the `requestAnimationFrame` it found when it woke, so a test that overrides `requestAnimationFrame` changes nothing. A capture synced to a state polls a computed CSS variable and fires about 100 ms early, so the capture's own latency lands it inside the state ([SVG lines in motion](https://www.prototemplate.com/docs/craft/svg-motion#gsap-from-outside-the-page)). --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Speed marks and the GT mark > The speed marks and the globe concepts at every size on paper and ink, the GT mark set inline as a word, and a mark with a dithered shine. URL: https://www.prototemplate.com/docs/components/marks Every mark in Prototemplate is one color in `currentColor`, so the same file draws positive on paper and reversed on ink with no redraw. The speed marks and the globe concepts are SVG files under `public/marks`, listed with their notes in `src/lib/marks.ts` and shown in full at [/marks](https://www.prototemplate.com/marks). The current GT mark, the doubled-line monogram, is a path that `GtMark` and `GtWord` draw inline. ## The marks [#the-marks] The plate draws any mark at the heights `/marks` shows it: a monogram from the 16 pixel favicon to the 128 pixel app icon, a wordmark at 48, 96 and 144. Family switches between the seven speed marks, the two picture marks of the earlier round and the eight globe concepts. Ground switches paper and ink, and Construction shows the build overlay of a picture mark. <MarksDemo /> Under a monogram the plate repeats it at 16, 24 and 32 pixels at their true size, where a mark either holds up as a favicon or does not. The server half of the demo reads the files at build time and passes them to the client inline, as the `/marks` page does, so the page fetches no mark files. ### Import and data [#import-and-data] ```ts import { GLOBE_CONCEPTS, MARKS, MARK_SIZES, REFERENCE_MARK, WORDMARK_SIZES } from '@/lib/marks'; const speed = MARKS.filter((mark) => mark.family === 'speed'); // seven marks, each at /marks/<id>.svg ``` | Export | What it holds | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MARKS` | Nine marks: `id`, `name`, `family` (`'speed'` or `'picture'`), `kind` (`'monogram'` or `'wordmark'`), `thesis`, `construction`, `use` and the public `file` | | `GLOBE_CONCEPTS` | The eight globe marks that were not chosen, each a file under `/marks/globe/` | | `MARK_SIZES`, `WORDMARK_SIZES` | The heights in CSS pixels: 16, 24, 32, 64 and 128, then 48, 96 and 144 | | `REFERENCE_MARK` | The current doubled-line monogram as one even-odd path and its view box | `pnpm build:marks` generates the seven speed files from `scripts/build/speed-marks.mjs` with tight view boxes, so a viewer sizes each mark by height and its own aspect. ### When to use it [#when-to-use-it] Draw a mark inline, or as an alpha mask in `currentColor`, so it takes the ink of its ground. The register's one-color rule holds for every use: no second tone, no gradient, no stroke and no shadow. The plate mark is the one mark that brings its own ground: its letters and stripes are holes, so it is an ink plate on paper and a paper plate on ink. ### Reduced motion and themes [#reduced-motion-and-themes] The marks do not move. On the ink ground the same file draws in paper ink. ## The mark as a word [#the-mark-as-a-word] The plate runs `gtText()` over the copy in the field. Every standalone "GT" becomes `GtWord`, the monogram sized to the text around it. Type a sentence to test the rule, and switch the size from a caption to a page title. <GtWordDemo /> The mark is 0.74em tall with its foot on the baseline, so it sits in the line at any size. The letters stay in the DOM as hidden text, so copy, find in page and screen readers still read "GT". Lowercase package names such as `gt`, hyphen-joined forms such as GT-format and letters inside other words such as GTM stay text. The three marks under the sentence are `GtMark` at the sizes the chrome uses: 25 by 16 in the sidebar and the toolbar, 28 by 18 in the sheet frame, and 50 by 32. ### Import and props [#import-and-props] ```tsx import { GtMark } from '@/components/viewer/GtMark'; import { GtWord, gtText } from '@/components/viewer/GtWord'; <p> <GtWord /> ships every locale. </p>; <p>{gtText(skill.description)}</p>; <GtMark width={28} height={18} />; ``` | Export | Props | What it does | | ----------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------- | | `GtWord` | none | The mark as a word in the running text, with "GT" kept as hidden text | | `gtText(text, keyBase)` | a string, and a key prefix (default `'gt'`) | The string with every standalone GT replaced by `GtWord`. A string with none comes back unchanged. | | `GtMark` | `width` (25), `height` (16) | The mark alone as a block SVG in `currentColor` | ### When to use it [#when-to-use-it-1] Every standalone "GT" in rendered prose on the site is the mark. Place `GtWord` in JSX copy and pass any string that arrives as data, such as a skill's description, through `gtText()`. Code, URLs, attribute values such as `alt` and `title`, document titles and metadata keep the letters. ### Reduced motion and themes [#reduced-motion-and-themes-1] `GtWord` does not move. It fills `currentColor`, so it takes the ink of its line in both themes. ## The dithered shine [#the-dithered-shine] The plate seats the GT mark as an alpha mask and sweeps a specular band through it, quantized by the 4 by 4 screen into five nested coverage tiers. The cells never move; two clip windows carry the light across them, and the counter-sheen runs half a lap behind. Seat lays the mark flat or on the isometric top plane, Glint picks the light's ink, and the speed is in drawing units a second. <DitheredMarkDemo /> The mask holds the mark's seat transform inside it, so the masked rectangles stay in screen space and every cell stays a square screen pixel, even on the isometric plane. The tiers nest, so the band reads as one falloff from a solid core to a sparse fringe. ### Import and props [#import-and-props-1] ```tsx import DitheredMark, { shineTravel } from '@/app/d/toolchain/diagrams/DitheredMark'; import { plane } from '@/app/d/toolchain/diagrams/iso'; const cover = { x: -48, y: -37, w: 58, h: 37 }; const { from, to } = shineTravel(cover); // the sweep's endpoints <DitheredMark id='ldx' href='/brand/locadex-mark.svg' plane={plane(14)} markHalf={35} cover={cover} inkClassName='mark-ink' glintClassName='mark-glint' shineClassName='mark-shine' />; ``` | Prop | Type | Default | What it does | | -------------------------------------------------- | ------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `id` | string | required | A prefix for the mask, pattern and clip ids, which are global in the document. | | `href` or `maskContent` | string or SVG | one required | The mark as an image file, or any SVG content filled white. | | `plane` | string | required | The seat transform, applied inside the mask. `plane(z)` from `iso.ts` seats it on an isometric face. | | `markHalf` | number | required | Half the image box, for an `href` mark. | | `cover` | `{ x, y, w, h }` | required | The screen rectangle the ink and the shine span. The sweep derives from it. | | `maskBox` | `{ x, y, w, h }` | `-120, -80, 240, 160` | The mask's bounds in user space. | | `cell` | number | `1.05` | The dither cell's edge in drawing units. | | `tiers` | `ShineTier[]` | `SHINE_TIERS` | The band's coverage tiers, center out: 16, 10, 6, 3 and 1 of 16. | | `inkClassName`, `glintClassName`, `shineClassName` | string | required | The resting ink, the glint cells and the shine group. | | `stripeData` | two attribute names | `data-ldx-stripe`, `data-ldx-stripe2` | The attributes on the two bands that the host's tween targets. | The component draws and the host animates. The host tweens the elements carrying each stripe attribute along x from `from` to `to` with no easing on repeat, and starts the counter-sheen's tween half a lap in, as the stack tower does at 56 units a second. ### When to use it [#when-to-use-it-2] Use the shine on one mark that is the drawing's subject, such as the Locadex mark on the capstone of the stack tower. ### Reduced motion and themes [#reduced-motion-and-themes-2] Under reduced motion the host never starts the tweens, and the markup's own pose stays: the primary band mid-glyph and the counter-sheen parked clear. The resting ink and the glint read this page's ink, so the mark follows the docs theme. ## Traps [#traps] [SVG lines in motion](https://www.prototemplate.com/docs/craft/svg-motion) explains each of these with its fix. * The brand deck's `#gt-mark` symbol (`deck/parts/head.html`) carries its own viewBox, `-8 214 1213 771`. Copied onto the outer svg, that viewBox draws the mark 214 units too high and cuts off its top. Reference the symbol from an svg with a width and height in the mark's aspect, about 1.573 (132 by 84, for example), and no viewBox ([symbols and masks](https://www.prototemplate.com/docs/craft/svg-motion#symbols-and-masks)). * A mask with `maskUnits="userSpaceOnUse"` resolves in the user space of the element that uses it, so the mask's coordinates are written in the same plan coordinates as the masked rectangle ([symbols and masks](https://www.prototemplate.com/docs/craft/svg-motion#symbols-and-masks)). * SVG ids are global to the document. Two marks on one page with the same `id` share one mask, so each `DitheredMark` gets its own prefix ([symbols and masks](https://www.prototemplate.com/docs/craft/svg-motion#symbols-and-masks)). * A `rotate()` clip window moves with GSAP's SVG transform origin, and it shifted the shine's sweep about 180 units off the mark. `DitheredMark` sweeps windows drawn pre-rotated at 60 degrees by a pure horizontal translate for that reason ([GSAP from outside the page](https://www.prototemplate.com/docs/craft/svg-motion#gsap-from-outside-the-page)). --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Badges and heads > The badge that cuts through the GT monograms after the Brand title, and the book head every hub page opens with, live at two widths. URL: https://www.prototemplate.com/docs/components/heads Every page of the hub that reads as a book opens with the same head: the title, a lead of one to three lines, a panel of facts, and one hatch band. DESIGN.md section 4 sets the standard and `pnpm lint:heads` holds it. /brand alone sets a badge after its title, which cuts through the GT monograms one frame at a time. ## The head badge [#the-head-badge] The plate sets the Brand title with its badge as `BookHead` sets it: the badge follows the title's last word and never breaks away from it. Count sets how many monograms the strip holds, and Size sets the title at the page and section steps. Still applies the rule reduced motion applies. <BadgeCycleDemo /> Every frame sits side by side in one strip, each exactly one box wide, and a CSS animation steps the strip one box to the left every 1.4 seconds with `steps(n)`, so each change is a hard cut. A hidden sizer stacks every frame in one cell, so the box takes the widest frame's width and never changes size. `BookHead` hides the badge from the accessible name, so the h1 a crawler reads holds the title alone. ### Import and props [#import-and-props] ```tsx import { BadgeCycle, badgeMark } from '@/components/viewer/BadgeCycle'; <BookHead badge={<BadgeCycle frames={files.map((file) => badgeMark(file))} />} />; ``` | Export | Props | What it does | | ---------------- | ------------------------------ | ------------------------------------------------------------------ | | `BadgeCycle` | `frames`: `{ key, content }[]` | The strip of frames. With no frames it renders nothing. | | `badgeMark(src)` | a public file path | One frame that draws the file as an alpha mask in the title's ink. | The rules live in `src/components/viewer/BookView.css`, so a host outside a book head imports that sheet, as this plate does. ### When to use it [#when-to-use-it] Only /brand's head carries a badge (Kevin, 2026-10-21): the GT monogram, then the register's monograms. Every other book head sets its title alone, and `pnpm lint:heads` fails a badge outside `src/app/brand`. ### Reduced motion and themes [#reduced-motion-and-themes] Under reduced motion the strip stops on the first frame and the box takes that frame's width. The frames draw in the title's ink, so the badge follows the theme. ## The book head [#the-book-head] A page keeps one h1, so the plate shows the head in its own document: a live hub page in a frame, scaled to the plate. Page picks the route, and Width lays it out at 1440 or 390 pixels. The head is a container, so at the phone width the panel drops under the lead. The page inside stays live. <HeadFrameDemo /> The frame follows the docs theme through the root layout's theme script, and it loads only when the plate comes near the viewport. ### Import and props [#import-and-props-1] ```tsx import { BookHead } from '@/components/viewer/BookView'; <BookHead title={PAGE_NAMES.marks.name} lead='The speed set is seven General Translation marks in one register.' updated={updated} facts={[ { icon: 'swatch', key: 'Marks', value: 9 }, { icon: 'speed', key: 'Speed set', value: 7 }, { icon: 'archive', key: 'Earlier round', value: 2 }, ]} />; ``` | Prop | Type | What it does | | ---------- | --------------------------------------- | ------------------------------------------------------------------------------------------ | | `title` | string | The page's plain name from `PAGE_NAMES`, or a record's own title. It is the page's one h1. | | `badge` | node | /brand only: the badge after the title's last word, hidden from the accessible name. | | `lead` | node | One to three lines at the lead measure, 200 characters at most. | | `updated` | `PageUpdated` | The page's entry in `src/lib/updated.ts`, passed down from its server page. | | `facts` | three facts, or one fact with `install` | The panel's rows after Updated, each `{ icon, key, value }`. | | `install` | string | An install command in a copy field, in place of the second and third facts. | | `note` | node | The rest of the introduction, under the mast's rule. | | `contents` | node | The page's contents nav, left out on a page of fewer than two sections. | The server page calls `requireUpdated('<route>')` and passes the entry down, because a client module may not import `src/lib/updated.ts`. A new route also needs its paths in `scripts/build/updated.mjs`. ### When to use it [#when-to-use-it-1] Use `BookHead` on every hub page with a book head; a page differs from another only in its words and its facts. Pages outside the shell, such as these docs, the deck and the `/d/` directions, have heads of their own. ### Reduced motion and themes [#reduced-motion-and-themes-1] The head itself does not move; its badge holds its first frame under reduced motion. Every value is a `--pt-` token, so the head follows the theme. ## The head's numbers [#the-heads-numbers] The panel's column is 304 px (`--pt-head-panel` in `src/components/viewer/tokens.css`), sized for its longest value: a date with its hint, such as "Sep 10, 2026 4 weeks ago", which measures 168 px. The panel's rows sit on the lead's line pitch, 26.35 px, so a fact lines up with a line of the lead beside it. [Pages and the viewer shell](https://www.prototemplate.com/docs/pages) holds every number of the book page with what it measures. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Live artifact pictures > Dithered photographs under the Blue Marble standard: the tone grid, the screen, the presets and both themes, cut live from a picture. URL: https://www.prototemplate.com/docs/components/artifact-pictures An artifact picture is a photograph or a scan of an object, an artwork or a place, printed through the house ordered dither. The cutter turns the photograph into a tone grid once: an 8-bit gray JPEG with the crop, the channel and the tone curve already applied. The page then screens the grid live at its size on screen, one cell per CSS pixel, so the picture stays sharp at any width and redraws in either theme. The settings are the Blue Marble standard in `scripts/media/mood-tone/standard.json`, named after the picture they were measured on. Two sets of grids ship. The plate set in `public/brand/mood` holds the five pictures of the sign-in and onboarding field. The deck set in `deck/shots/tone` holds the ten mood slides of the brand deck, which the deck build serves from `public/deck-assets`. ## The screen [#the-screen] The plate screens one picture in both themes at once with the standard's settings. The cell size and each theme's opacity are controls, and Standard puts them back. <MoodScreenDemo /> A cell is lit when its tone is over the 8 by 8 Bayer threshold `(m + 0.5) / 64`. The screen runs at gamma 1 and bias 0, because the whole tone curve is already in the grid. Bytes at or under 10 read as 0, which keeps the JPEG's ringing on black from printing stray cells. An unlit cell is clear, so the ground shows through it. At a cell of 2 px or more, each cell reads the area average of the grid cells under it: the picture keeps its tone and loses its detail, which is why the standard holds the cell at 1 px. ### Import and props [#import-and-props] ```ts import { loadPictureTone, pictureField } from '@/components/plate/lib/picture-field'; import { ditherToCanvas } from '@/lib/dither'; const tone = await loadPictureTone('/brand/mood/mood-rosetta.jpg'); const field = pictureField(tone, { placement: { kind: 'cover', focusX: 0, focusY: 0.5 }, canvasWidth: canvas.clientWidth, canvasHeight: canvas.clientHeight, }); ditherToCanvas(canvas, field, { scale: 1, ink: 'white', paper: 'transparent' }); canvas.style.opacity = '0.62'; ``` `loadPictureTone(src)` decodes a grid once per address and returns its width, its height and its tone as numbers from 0 to 1, with the floor applied. `pictureField` returns a field that the dither engine reads once per cell. | Option | Type | Default | What it does | | ----------------------------- | --------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `placement` | `cover`, `disc` or `region` | none | Where the picture's 1600 by 900 file space lands on the canvas. `cover` fills the canvas and keeps `focusX` and `focusY` in view. `disc` scales the file so the circle at `cx`, `cy` of radius `r` is `diameter` CSS px across, centred on `centre`. `region` fits a rectangle of the file inside `frame`. | | `canvasWidth`, `canvasHeight` | number | none | The canvas box in CSS px. | | `cellSize` | number | `1` | The dither cell in CSS px. A cell that covers more than one grid cell reads their area average. | The plate on this page runs the plate port's sampler and `ditherToCanvas` once per change; nothing animates. The brand deck screens the same grids with its own engine in `deck/parts/tail.html`. There `moodTaps` lists, for each output cell on one axis, the grid cells it covers and their weights, and `paintMood` sums them and thresholds the result. The sign-in field draws with `createDitherLoop`. `pnpm lint:pictures` holds every surface's constants to the standard. ### The screen's numbers [#the-screens-numbers] | Setting | Value | | -------------- | ---------------------------------------------- | | Matrix | 8 by 8 Bayer, a cell lit over `(m + 0.5) / 64` | | Cell | 1 CSS px, screened at the size on screen | | Tone floor | 10 of 255 reads as 0 | | Gamma and bias | 1 and 0 | | Dark theme | `#ffffff` cells at 0.62 opacity over `#070707` | | Light theme | `#070707` cells at 0.7 opacity over `#ffffff` | ## Tone grid and screen [#tone-grid-and-screen] The plate splits one picture at a seam, with the tone grid on the left and its screen on the right at the same framing. Drag across the picture or use the Seam control to move the seam, and zoom to 4x or 8x to see single cells. <MoodCompareDemo /> The photograph itself never ships. The cutter, `pnpm mood-tone`, reads it from a sources folder outside the repository. It crops the photograph, takes the red or the gray channel, inverts it when the subject should print lit on black, solves the black and white points and writes the grid. The grid is therefore the first stage a page can show. A lit cell prints ink on paper in the light theme, so the plate inverts the gray grid there to match the screen. ### The grid's numbers [#the-grids-numbers] | Setting | Value | | ------------ | ------------------------------------------------------------ | | Blur | none | | Gamma | 1.2 | | Autocontrast | clips 0.5 percent at each tail | | File | 8-bit gray JPEG at quality 82, or 75 when 82 is over the cap | | Cover grid | 1600 by 900 | | Disc grid | 2400 by 1350 | | Byte cap | 573,440 | The cutter solves the black and white points per picture. A scene lands on the Blue Marble's mean tone of 0.361 and standard deviation of 0.342 over the part of the grid that is shown, or over its subject alone when the subject sits on a plain ground. A marks picture (writing, engraving or lines on a plain ground) puts the ground at black and the 90th percentile of the marks at white. The lint holds each grid to its kind's window: | Kind | Mean | Standard deviation | At white (250 and up) | At the floor (10 and under) | | ----- | ------------ | ------------------ | --------------------- | --------------------------- | | Scene | 0.30 to 0.42 | 0.28 to 0.44 | at least 2 percent | at least 5 percent | | Marks | at most 0.35 | any | at least 0.5 percent | at least 50 percent | ## The pictures [#the-pictures] A picture never shows a page of plain English prose, because readable copy competes with the text beside it. Writing is welcome when it is the subject: a carved inscription, calligraphy, a manuscript in another script, signs pressed in clay, or the lettering engraved on a chart. Each source is public domain or Creative Commons, and the credit is shown with the picture. The plate set, measured over the part of each grid the sign-in field shows right of its ramp, or over the subject alone where the cutter masks one (the earth's disc): | Picture | Kind | Grid | Mean | Std | Credit | | ------------------------ | ----- | ------------ | ----- | ----- | ---------------------------------------------------------- | | The Blue Marble | scene | 2400 by 1350 | 0.360 | 0.342 | Image: NASA, Reto Stöckli, 2007, public domain | | The Rosetta Stone | scene | 1600 by 900 | 0.362 | 0.345 | Photograph: Hans Hillewaert, CC BY-SA 4.0 | | Karahisari's Calligraphy | marks | 1600 by 900 | 0.191 | 0.337 | Calligraphy: Ahmed Karahisari, 16th century, public domain | | Proto-Cuneiform Tablet | scene | 1600 by 900 | 0.365 | 0.335 | Photograph: The Metropolitan Museum of Art, public domain | | A Marginal Gloss | marks | 1600 by 900 | 0.141 | 0.288 | MS f Med. 23, Boston Public Library, public domain | The deck set, all 1600 by 900, measured over the frame, or over the subject alone where the cutter masks one: | Picture | Kind | Mean | Std | Credit | | --------------------------------- | ----- | ----- | ----- | ---------------------------------------------------------------------- | | The Blue Marble | scene | 0.358 | 0.342 | Image: NASA, Reto Stöckli, 2007, public domain | | The Rosetta Stone | scene | 0.365 | 0.339 | Photograph: Hans Hillewaert, CC BY-SA 4.0 | | A proto-cuneiform tablet | scene | 0.361 | 0.342 | Photograph: The Metropolitan Museum of Art, Open Access, public domain | | Karahisari's calligraphy | marks | 0.176 | 0.326 | Calligraphy: Ahmed Karahisari, 16th century, public domain | | Louisbourg lighthouse | scene | 0.334 | 0.397 | Photograph: Ken Heaton, CC BY-SA 4.0 | | A Devanagari manuscript | marks | 0.305 | 0.429 | Photograph: Ms Sarah Welch, CC BY-SA 4.0 | | A marginal gloss | marks | 0.140 | 0.300 | Image: MS f Med. 23, Boston Public Library, public domain | | The Eastern Telegraph cable chart | marks | 0.121 | 0.254 | Map: Eastern Telegraph Company, 1901, public domain | | The Great Wave off Kanagawa | scene | 0.358 | 0.401 | Print: Katsushika Hokusai, about 1831, public domain | | Bowen's compass rose | marks | 0.103 | 0.244 | Engraving: Emanuel Bowen, 1748, public domain | `docs/ARTIFACT-PICTURES.md` gives the steps to add or retire a picture, the licence order and the full list of what `pnpm lint:pictures` checks. [Dither and the studio field](https://www.prototemplate.com/docs/components/dither) covers the Bayer screen and the dither engine these plates run on. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Covers and social cards > Post covers, social cards and banners generated from a title and a field, at their export sizes, with the type and the mark set by the rules. URL: https://www.prototemplate.com/docs/components/covers-and-cards Three exports carry a title outside the page it belongs to. The post cover sits at the head of a blog post and on its card in the index. The social card is the image X, LinkedIn and Slack draw when someone shares a link. The X banner is the header of the General Translation profile. Each one is a still at a fixed size, made in a dark and a light version at the same framing. ## Generator [#generator] Type a title and pick a field and a theme. The plate draws the post cover, the social card and the X banner from it, each at its export aspect ratio. <CoverGeneratorDemo /> Every length inside a frame is written in the export's own pixels, so the type, the mark and the rules keep their export sizes at any preview width. The field takes one side of the frame and the type sits on solid ground: a dithered cover shades one large form on a quiet ground and keeps the type on solid ink or paper. The dither field is the sign-in globe at the settings of the partnership globes, with cells of 6 export px drawn at each frame's scale. The glyph field is the glyph rain, and the horizon is the careers page's horizon in its still frame. The card keeps the rails and rules of the site's own card, and the banner keeps the shipped banner's split, with the field on the left 30 percent. ### The exports [#the-exports] | Export | Size | File | Field | Title | Mark and name | | ----------- | ------------ | ------------------------------------------------ | -------------------------------------- | ----- | --------------------------- | | Post cover | 1920 by 1080 | 3840 by 2160 WebP at quality 95, dark and light | right 864 px | 80 px | mark 40 px tall, name 32 px | | Social card | 1200 by 630 | 2400 by 1260 PNG, dark and light | 472 by 340 px in the body's right half | 64 px | mark 24 px tall, name 26 px | | X banner | 1500 by 500 | 3000 by 1000 lossless WebP and a 1500 by 500 PNG | left 450 px | 64 px | mark 24 px tall, name 26 px | ### The type [#the-type] | Rule | Value | | ------------- | ------------------------------------------------------------------------------------------------------------------- | | Face | Inter at weight 500 for the title and the name, 400 for the address | | Title | display features, tracking -0.025em, line height 1.04, two lines at most | | Smallest text | 26 px on a 1600 px stage after it is fit to the blog column, which is 31 px on the 1920 px cover; 22 px on the card | | Doubled line | two 1 px lines 4 px apart, 120 px long on the card and 180 px on the cover | | Mark | the doubled-line GT monogram in the frame's one ink | | Inks | `#f2f2f0` on `#070707` in the dark version, `#070707` on `#ffffff` in the light one | A cover at the head of a post is about 700 px wide in the article column, so a 1920 px cover shows at about 0.36x. The 80 px title then reads at 29 px. ## Social card [#social-card] The site draws its social card on request at `GET /og` with next/og. Type a title and a description, and the plate asks the route for the card a page with those fields carries. <SocialCardDemo /> `seoRoute()` builds each page's card address with `cardPath()`, and the route parses it with `parseCard()`, so the two always agree. The card is a pure function of its query, and `v` changes whenever the design changes, so each card is cached for a year. A title over 90 characters and a description over 200 are cut on a word with an ellipsis. A direction's card also draws that direction's dark capture in the right half of its body. ### Import and props [#import-and-props] ```ts import { cardPath } from '@/lib/seo/card-url'; const src = cardPath({ title: 'Covers and social cards', description: 'Post covers, social cards and banners generated from a title and a field.', section: 'Docs', kind: 'page', path: '/docs/components/covers-and-cards', }); // /og?v=1&t=Covers+and+social+cards&s=Docs&d=…&k=page&p=%2Fdocs%2Fcomponents%2Fcovers-and-cards ``` | Field | Type | What it does | | ------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `title` | string | The card's title, cut to 90 characters. | | `description` | string | Two lines under the title, cut to 200 characters. | | `section` | `SeoSection` | The label at the top right with its accent square: Docs, Handbook, Skills, Directions, Shipped, Blog, Motion, Archive or Deck. Any other value draws none. | | `kind` | `SeoKind` | `page`, `article`, `skill`, `post`, `direction`, `mirror` or `error`. A mirror's footer names generaltranslation.com, and a direction draws its capture. | | `path` | string | The page's path, printed in the footer after `www.prototemplate.com`. | ### The card's numbers [#the-cards-numbers] | Part | Value | | ------------------ | ------------------------------------------------------ | | Size | 1200 by 630, 1.91 to 1 | | Rails | 1 px at x 64 and x 1136, the full height | | Rules | 1 px at y 96 and y 534, the full width | | Mark and name | the Prototemplate mark at 32 px, its name at 26 px | | Title | 64 px Inter Display 500 at -1.6 px tracking, two lines | | Description | 28 px at line height 1.36, two lines | | Section and footer | 22 px in titanium `#8a8f98` | | Colors | `#070707` ground, `#f2f2f0` text, one `#86a8ff` square | ## Fallback cover [#fallback-cover] A blog post without a cover image gets a motif plate: one word from its title, cut out of a ramp of Bayer coverage tiers. Type a post title to see the word and the ramp it picks. <FallbackCoverDemo /> `motifFor` reads the title first and the tags second. A release title such as `gt-react@11.1.2` speaks its package name. A topic speaks its word, from SEO, Next.js, i18n, JSX, AI, plurals and React, and any other post about localization speaks 語. The word is sized by its length to fill the 400 by 225 plate, and the ramp turns by an angle seeded from the slug, so every post keeps its own plate. ### Import and props [#import-and-props-1] ```tsx import BlogFeatureDither, { motifFor } from '@/app/d/production/sections/BlogFeatureDither'; <BlogFeatureDither id='plurals-in-react' motif={motifFor({ slug, title, tags })} />; ``` | Prop | Type | Default | What it does | | ----------- | -------------- | ------- | ----------------------------------------------------------------------------------------------- | | `motif` | `FeatureMotif` | none | The word, its size on the plate, the ramp's angle in degrees and its tracking, from `motifFor`. | | `id` | string | none | Unique on the page. It namespaces the SVG pattern and mask ids. | | `scale` | number | `1` | Shrinks the word for a short plate, so the slice never cuts it. | | `className` | string | none | Added to the plate's span. | ### The ramp's numbers [#the-ramps-numbers] | Part | Value | | --------- | ----------------------------------------------------------------------------------------------- | | Plate | 400 by 225, drawn with `preserveAspectRatio` slice | | Ramp | solid (64 of 64) for 260 units, then 58, 52, 46, 40, 34, 28, 22, 16 and 12 of 64, 38 units each | | Cell | 1 unit, an 8 by 8 tile | | Angle | 12 to 23 degrees, either way, from the slug | | Word size | 170 for a CJK word; by length 155, 138, 132, 100, 80, 66, 56, 48 and 42 | ## Post covers on the blog [#post-covers-on-the-blog] `PostCover` draws a post's cover at the head of the post. When the post has a light cover too, both images are in the document and the stylesheet shows the one for the current theme, so a theme switch swaps them without a request. The optimizer serves each at the width it is shown from the 3840 px master, at quality 95. `BlogPostCover` is the index card's cover on the shipped blog: the post's own image when the frontmatter names one, and the fallback cover when it does not. ```tsx import PostCover from '@/components/blog/PostCover'; <PostCover post={{ image: '/static/blogs/post-dark.webp', imageLight: '/static/blogs/post-light.webp' }} priority />; ``` | Prop | Type | Default | What it does | | ----------- | ----------------------- | ------------------- | --------------------------------------------------------------------------- | | `post` | `{ image, imageLight }` | none | The dark cover and the optional light one. Nothing renders without `image`. | | `priority` | boolean | `false` | Loads the cover first. The header cover sets it; cards load lazily. | | `sizes` | string | `BLOG_COLUMN_SIZES` | The width the cover renders at, for the optimizer. | | `className` | string | none | Added to each image. | --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Films and contact sheets > The film player, contact sheets of frames, and the report card: how the system presents a finished artifact on the page. URL: https://www.prototemplate.com/docs/components/films-and-sheets A finished film reaches the site as three files: the web copy that plays, its poster (the film's title card) and a contact sheet of its frames. `public/motion/published.json` pins the cut of each published film by the hashes of its video and audio streams, and `pnpm build:motion` writes the registry the pages read, `src/lib/motion.ts`. The site publishes 39 cuts: 9 films, 6 films of the translation series and 24 knowledge base films, with the German, Spanish and Japanese cuts of one film counted among the 9. ## Film player [#film-player] Pick a published film. The player shows its poster until the first press, then plays the web copy with the browser's own controls. <FilmPlayerDemo films="PUBLISHED_FILMS" /> The poster is a lazy image inside a play button, and the video has no `poster` attribute until it plays, because a browser fetches that attribute at page load wherever the video sits. The video preloads nothing. The first press starts the film, turns on the native controls and moves focus to the video, so the keyboard reaches those controls next. The width and height attributes reserve the 16:9 box before anything arrives, so nothing shifts. Captions are offered in the native menu and stay off until the viewer chooses them. ### Import and props [#import-and-props] ```tsx import BrandFilm from '@/app/brand/BrandFilm'; <BrandFilm src='/media/designing-docs-film.mp4' poster='/media/designing-docs-poster.jpg' name='the Designing docs for humans film' />; ``` | Prop | Type | Default | What it does | | -------------- | ------------------- | ------- | ---------------------------------------------------------------------------------------------------------------- | | `src` | string | none | The web copy under `/media`, as a root path. | | `poster` | string | none | The title card, shown in the play button until the first press. | | `name` | string | none | What the film is called to a screen reader. The play button reads `play` and this name. | | `captions` | string | none | A WebVTT file, offered in the native controls and off until chosen. | | `captionsLang` | `{ locale, label }` | English | The captions' locale and the name the native menu shows. | | `autoStart` | boolean | `false` | Starts the film on mount without moving focus. A language toggle passes it when the cut it replaces was playing. | The host sizes and frames the video. The player's own stylesheet draws only the play button and its badge, a 56 px paper plate on the poster, which moves to the lower right corner at 28 px on a phone so it never covers a title card's title. ### The published films [#the-published-films] | Part | Value | | -------- | ------------------------------------------------------------ | | Video | 1920 by 1080 MP4 under `/media`, 15 to 99.8 seconds | | Poster | 1920 px JPEG, 60 KB to 505 KB | | Captions | WebVTT under `/motion/captions`, on 24 of the 39 cuts | | Films | 9 films, 6 translation series films, 24 knowledge base films | ## Contact sheet [#contact-sheet] A contact sheet holds two frames a second of a cut, eight to a row. Drag across the frame or the strip under it, step with the Frame control, or play the sheet at the rate it was cut at. <ContactSheetDemo films="PUBLISHED_FILMS" /> The sheet is one WebP. The plate shows one tile at a time as a crop of that image, sized so a 384 px tile spans the frame and moved to the tile's column and row, so scrubbing loads nothing new. Frame k is taken from the middle of the half second from k / 2 to (k + 1) / 2 seconds. `pnpm build:motion` counts a sheet's frames only when its size matches the grid for its cut's length, so a page never states a count it cannot read from the picture. ### Import and props [#import-and-props-1] ```ts import { SHEET_TILE } from '@/components/docs/demos/artifacts/film-list'; /* frame k of a sheet with `columns` tiles to a row, cropped into a box one tile wide */ const scale = box.clientWidth / SHEET_TILE.width; const x = (k % columns) * (SHEET_TILE.width + SHEET_TILE.gap) * scale; const y = Math.floor(k / columns) * (SHEET_TILE.height + SHEET_TILE.gap) * scale; box.style.backgroundImage = `url(${sheet.src})`; box.style.backgroundSize = `${sheet.width * scale}px auto`; box.style.backgroundPosition = `${-x}px ${-y}px`; ``` | Field | Type | What it holds | | ----------------- | ------ | ------------------------------------------------------- | | `src` | string | The sheet's WebP under `/motion/sheets`. | | `width`, `height` | number | The sheet's pixel size. | | `frames` | number | Two a second for the cut's length, rounded up. | | `columns`, `rows` | number | The grid: `(width + 3) / 387` and `(height + 3) / 219`. | ### The sheet's numbers [#the-sheets-numbers] | Part | Value | | ------ | --------------------------------------------------------------------------------- | | Tile | 384 by 216 px | | Gap | 3 px | | Row | 8 tiles, 3093 px wide; the How General Translation works sheet has 6 to a row | | Rate | 2 frames a second from frame 0 | | Frames | 30 to 200 per sheet | | File | WebP, 0.2 MB to 2.4 MB, with the full PNG beside it when the PNG is at most 20 MB | ## Report card [#report-card] The report card is the result of the `/report-card` page, which grades how well a site is localized. Here it stands alone with a sample report for example.com; switch its state to see the empty card, the wait and the filled card. <ReportCardDemo /> The card draws its whole structure from the first paint: the meter, a ghost score and the six named rows with their grade scales unlit. While the grader runs, the rows wait in place, and the report fills them without moving anything below. A filled row opens its evidence in a dialog: the captured markup, the expanded findings and a prompt to paste into a coding agent. The empty card is `inert` and hidden from assistive technology, so a screen reader never hears ghost rows. ### Import and props [#import-and-props-2] ```tsx import ReportCard from '@/components/try/ReportCard'; import '@/components/try/try.css'; <div className='try-root'> <ReportCard report={report} state='done' /> </div>; ``` | Prop | Type | What it does | | -------- | ------------------------------------------------ | ---------------------------------------------------------------- | | `report` | `Report` or `null` | The graded report from `analyze()`; `null` draws the empty card. | | `state` | `idle`, `loading`, `settling`, `done` or `error` | The run's phase. `loading` and `settling` hold the waiting look. | The card's stylesheet scopes every rule under `.try-root` and reads the toolchain's color tokens (`--tc-paper`, `--tc-ink`, `--tc-hair`, `--tc-accent`), so a host sets those or sits inside a page that does. ### The card's numbers [#the-cards-numbers] | Part | Value | | ----------- | ----------------------------------------------------------------------------------------------------------------- | | Score | 0 to 100, with a grade from A to F | | Meter ticks | 0, 18, 38, 63, 88 and 100, the grade boundaries | | Categories | hreflang tags, language declaration, locale routing, translated metadata, content language, charset and direction | | Evidence | at most 4 snippets of 1200 characters and 12 findings per category | --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Rails, bands and spacers > The landing grammar as live parts: one rail, full-width bands, hatch spacers and heads, with the widths and paddings at every breakpoint. URL: https://www.prototemplate.com/docs/components/rails-and-bands A General Translation landing page is one ruled column. The column draws its two lines once, the bands inside it close each seam with one rule, hatch spacers sit between the bands, and each section opens with a head: a heading and one lead. The production mirror at [/d/production](https://www.prototemplate.com/d/production) rebuilds the shipped site page for page, and every number on this page comes from its stylesheets and the toolchain engine under them. ## The page skeleton [#the-page-skeleton] Add, remove and reorder the parts of a page, then move the width between 390 and 1440 pixels. Each part stands at the height the engine's numbers give it at that width, and the plate checks every seam for one owner. <PageSkeletonDemo /> Rail picks the column: 1104 pixels for the production mirror, or 1170 for the toolchain engine and the hub's own article. Rules switches between the shipped ownership, where a section gives up its bottom rule to a spacer or a band under it, and a page where every part draws its own edges. A doubled seam is marked in the accent on the skeleton and in the list of parts. Measures tints each part's padding, and the readout under the skeleton lists the numbers at the current width. Text is drawn as bars, so the skeleton reads at any scale. ### The column [#the-column] | Column | Width | Lines | Source | | ----------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------- | | Production mirror | `min(1104px, 100%)` | `border-inline` on the wrapper, removed at 620px and under | `prod-shell.css` | | Toolchain engine | `min(1170px, 100%)` | the same | `toolchain/styles.css` | | Hub article | `min(1170px, 100%)` (`--pt-rail`) | `border-inline` on the wrapper | `prototemplate.css` | | Reading column | 1280px at most, 56px padding a side (16px at 900px and under) | none: the toolbar's rule and the sidebar's edge are the only lines beside it | `Sheet.css` | The wrapper draws both lines with its own `border-inline`, and a band inside it draws no side rails. A full-bleed band sits outside the wrapper and draws the pair from its own inner column at the same two x positions, so each line runs through the band unbroken. On production pages a registration cross sits where each seam meets a line: 9 pixels across, at 38 percent ink in light and 34 percent in dark. ### Bands and spacers [#bands-and-spacers] Every value below is a `clamp()` of the viewport width, so a band's padding moves continuously between the breakpoints. The columns give the resolved values at four widths. | Value | Rule | 1440 | 1024 | 720 | 390 | | ------------------------------ | ---------------------------------------------------------------------------------- | ------ | ---------- | ------ | ------ | | Gutter (`--tc-gut`) | `clamp(20px, 3.1vw, 36px)` | 36 | 31.7 | 22.3 | 20 | | Head, over (production) | `clamp(26px, 2.8vw, 36px)`, 72px at 720 and under | 36 | 28.7 | 72 | 72 | | Head, over (toolchain) | `clamp(54px, 6.4vw, 82px)`, 72px at 720 and under | 82 | 65.5 | 72 | 72 | | Head, under | `clamp(28px, 3.2vw, 38px)`, 36px at 720 and under | 38 | 32.8 | 36 | 36 | | Hatch spacer | `clamp(28px, 3.4vw, 40px)`; 36px (production) or 40px (toolchain) at 720 and under | 40 | 34.8 | 36 | 36 | | Cell, over | `clamp(26px, 3vw, 36px)`, `clamp(34px, 6vw, 42px)` at 720 and under | 36 | 30.7 | 42 | 34 | | Cell, under | `clamp(30px, 3.4vw, 40px)`, `clamp(38px, 6.5vw, 46px)` at 720 and under | 40 | 34.8 | 46 | 38 | | Band column, over (production) | `clamp(28px, 3vw, 40px)`, 36px at 720 and under | 40 | 30.7 | 36 | 36 | | Band column, over (toolchain) | `clamp(60px, 7vw, 96px)` | 96 | 71.7 | 60 | 60 | | Band column, under | `clamp(56px, 7vw, 88px)` | 88 | 71.7 | 56 | 56 | | Hero, over and under | `clamp(44px, 5.6vw, 72px)` and `clamp(40px, 5vw, 64px)` | 72, 64 | 57.3, 51.2 | 44, 40 | 44, 40 | | Width | What changes | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1023px and under | Bento rows stack to one column: `BentoRow`, `shell.css` and every `.tc-row` variant fold at the same width. | | 760px and up | A production head holds one line (`white-space: nowrap`); an authored break still splits it in two. | | 720px and under | The mobile ladder: heads 72px over and 36px under, the heading at 36px on 1.18, the lead at 17px on 1.55 and 18px under the heading, buttons at 44px, the head's watermark and its authored break hidden. | | 620px and under | The column's two lines are removed, and the hero's buttons fill the column up to 440px. | The production spacer (`.v0-hatch`) is a 45 degree diagonal, 1 pixel in every 8 at 8.5 percent ink, between two rules it draws itself. The section above it gives up its bottom rule, and the spacer gives up its own bottom rule to a band below it, whose top rule closes that seam. The hub article's spacer is 42 pixels at minus 45 degrees and draws its bottom rule only, because every section of that article closes itself with its own bottom rule. ## The head and the Cta row [#the-head-and-the-cta-row] A head is a heading and at most one lead in a ruled box. The plate sets the production heads in real type at the width the slider gives and reads the heading's lines back: two at most, and one at desktop on production pages unless an authored break splits it. <HeadCtaDemo /> Cta Row shows the hero's two buttons. The solid face with the ring is the page's primary action, and the outline face sits beside it. The two sit in equal columns as wide as the wider label, and at 620 pixels and under they fill the column up to 440 pixels, half the row each when both labels fit. | Part | Desktop | 720px and under | | --------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------- | | Heading | `clamp(1.62rem, 2.5vw, 2.2rem)` on 1.06, -0.028em, weight 500 | 36px on 1.18 | | Lead | 15px on 1.62 in ink-2, 60ch wide, 13px under the heading | 17px on 1.55, 18px under the heading | | Watermark | a Heroicons solid glyph at 240px and 5 percent ink, 48px off the right edge; the Locadex mark at 343px | hidden | | Button | 38px tall, a 13.5px label, 17px of side padding, the 6px control corner | 44px tall, a 15px label | | Cta row | equal columns, a 10px gap, centered | fills the column up to 440px at 620px and under | | Ring | a blurred spectrum 2px outside the face at 60 percent opacity, on an 8px corner | the same | ### Import and props [#import-and-props] The shipped site renders every button through gt-cloud's `Cta`; the mirror on this site writes the same `tc-btn` faces directly. ```tsx import Cta from '@/components/landing/shared/Cta'; <Cta ring href={SIGN_IN_HREF}>Get Started</Cta>; <Cta variant='outline' href='/docs'>Read the Docs</Cta>; ``` | Prop | Values | What it does | | ------------------ | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | | `variant` | `'solid'`, `'outline'`, `'on-ink-solid'`, `'on-ink-outline'` | The ink face (white in dark), the hairline face, or the two faces for a hero on the black-hole field. | | `ring` | boolean | The spectrum ring that marks the page's primary action, once per band at most. | | `size` | `'sm'`, `'md'`, `'lg'` | 32, 38 or 44 pixels; at 720px and under every face is 44px and `sm` is 38px. | | `href`, `external` | string, boolean | A link, an external anchor, or a `<button>` when `href` is absent. | | `tracked` | an existing location slug | Fires the `cta_clicked` event with that slug; the slugs are fixed. | Labels are Title Case, with short function words lowercase after the first: Get Started, Get a Demo, Talk to Us, Read the Docs. ## The line law [#the-line-law] Every seam is drawn once, by one owner. The plate draws one junction a pixel at a time, at about 20 times its size: the column's left line, a section above and a hatch spacer below. Each pixel is composited by the browser from the shell's own token alphas, so the plate reads the same in both themes. The inset at the corner is the same crop at its real size. <JunctionDemo /> Seam picks who draws the rule. Spacer and Section are the two shipped arrangements, and the other three are the defect classes the line auditor (`scripts/lint/lines.mjs`) reports. Clip Ground turns off `background-clip: padding-box`, so the spacer's diagonals run under its own rule. | Case | What the auditor sees | The fix | | ---------- | ----------------------------------------------------------------------- | ---------------------------------- | | Doubled | two strokes from two owners 1 to 4 pixels apart along most of their run | one owner yields its edge | | Coincident | two strokes on one row, whose translucent inks stack darker | one owner yields its edge | | Missing | a junction between two blocks that no stroke closes | the owner the table names draws it | | Self-stack | a translucent border over the element's own translucent ground | `background-clip: padding-box` | | Junction | Owner | The other side | | -------------------------------- | ---------------------------------------------- | --------------------------------- | | The column and a band inside it | the wrapper's `border-inline` | the band draws no side rails | | The column and a full-bleed band | the band's inner column, at the column's edges | nothing beside the column | | A section and the next section | the upper section's bottom rule | the lower section draws no top | | A section and a spacer | the spacer's top rule | the section drops its bottom rule | | A spacer and a band | the band's top rule | the spacer drops its bottom rule | | A head and its bento row | the row's top rule | the head draws none | | Two cells in a row | the row's 1px gap over the hairline ground | the cells draw no borders | --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Grids and bentos > Bento grids, feature grids and the viewer grid view, with their columns, gaps and spans adjustable and the line law holding at every junction. URL: https://www.prototemplate.com/docs/components/grids-and-bentos A bento is a grid of cells whose seams are hairlines. Neighboring cells touch, so every seam is shared by two of them, and the grammar gives it one owner: the row. The shell's `BentoRow` draws every seam as a 1px gap over a hairline ground, and its cells have no border props at all, so a doubled line cannot be written with it. ## Seams [#seams] Seams picks who draws the lines. Each setting opens in the shape its code was written for, and Columns, Cells and Gap then change the grid; a click on a cell widens it or makes it taller. The plate audits itself the way `scripts/lint/lines.mjs` audits a page: it reads every border from computed CSS, counts the strips of a row's ground as strokes, and marks each fault in the accent. <GridSeamsDemo /> Row Ground mounts the live `BentoRow` and `BentoCell`. Their 1px gap is the seam, so a wider gap shows the ground as a bar, and a last row that does not fill shows it as a block. Below 1024 pixels of viewport the row stacks its cells, as the shipped pages do. Row Rules is the toolchain's `.tc-row`, which draws a rule on the left of every cell after the first: it holds one line of cells, and once it wraps, the first cell of the next line draws a rule against the column's line and nothing separates the two lines. Cell Borders is `FeatureBento`'s rule set: the grid draws its own frame, and each cell its right and bottom edges, with `nth-child` exclusions written for eight cells at 2, 3 or 4 columns. Any other count, column number or span leaves a cell edge on the frame. The plate keeps `FeatureBento`'s desktop rules at every width. Cell Frames gives every cell its own box: two frames under 4 pixels apart double, and further apart the cells read as separate bordered cards, which the landing grammar does not use. | Fault | What the audit marks | | ------------------------- | ----------------------------------------------------------------------- | | Doubled line | two strokes from two owners 1 to 4 pixels apart along most of their run | | Coincident pair | two strokes from two owners on one row | | Missing seam | two touching cells with no stroke between them | | Seam wider than 1px | a strip of the row's ground wider than a hairline | | Ground showing as a block | grid area no cell covers, painted in the ground's hairline ink | ### Import and props [#import-and-props] ```tsx import { BentoCell, BentoRow } from '@/components/shell/Bento'; <BentoRow cols='minmax(0, 1.32fr) minmax(0, 1fr)'> <BentoCell framed={false} sub='One sentence about the cell.' title='Code'> {copy} </BentoCell> <BentoCell cell='is-tall'>{figure}</BentoCell> </BentoRow>; ``` | Export | Prop | Type | Default | What it does | | ----------- | ---------------------- | ------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------- | | `BentoRow` | `cols` | a `grid-template-columns` value | two equal columns | The row's template; one column at 1023px and under. | | | `eqHeads` | boolean | `true` | Gives every cell head in the row one height, so the first line under each head lands at one y. | | | `headH` | a length | `112px` | The aligned head height. | | | `className` | string | none | The page's variant classes, such as `is-lead`. | | `BentoCell` | `framed` | boolean | `true` | Mounts the nested `.tc-card` and carries `.is-framed`, so the row's ground shows as a 1px strip around the card. | | | `cell` | string | none | Variant classes on `.tc-cell`, such as `is-tall`. | | | `title`, `sub`, `head` | node | none | The cell head: an h3, a paragraph, and anything under them. | | | `style` | `CSSProperties` | none | Spans and other per-cell layout. | | `Rails` | none | | | Draws the column's pair for a full-bleed band outside a wrapper; a band inside a wrapper renders none. | The row and the cell write the class grammar (`.tc-row`, `.tc-cell`, `.tc-card`) that each page's stylesheet styles, so a cell's ground, padding and card corner come from the page. ### Row templates [#row-templates] The toolchain engine names its rows by their templates. Every one stacks to a single column at 1023 pixels and under, the same width `BentoRow` folds at. | Class | `grid-template-columns` | Where the toolchain uses it | | --------------- | ----------------------- | ------------------------------------- | | `is-lead` | `1.32fr 1fr` | a wide first cell beside a narrow one | | `is-split` | `1.04fr 0.96fr` | copy beside a framed artifact | | `is-three` | `1.13fr 0.93fr 0.94fr` | three diagrams of different widths | | `is-wide-left` | `1.58fr 1fr` | a wide figure beside a short list | | `is-wide-right` | `1fr 1.34fr` | the mirror of the above | | `is-even` | `1fr 1fr` | two equal cells | | `is-tilt` | `0.82fr 1fr` | a stat list beside a wider field | | `is-grammar` | `0.93fr 1fr` | the two grammar diagrams | | `is-one` | `1fr` | one cell across the column | A flat cell pads `clamp(26px, 3vw, 36px)` over and `clamp(30px, 3.4vw, 40px)` under, with the gutter at each side. A framed cell has no padding: its card sits on the hairline ground with a 1px strip around it, and the card pads itself by `clamp(17px, 1.9vw, 21px)`, 36px over and 38px under at 720px and under. Where a framed row meets the column's line, the strip on that side is dropped, so no strip runs beside a line. ### The feature grid [#the-feature-grid] `FeatureBento` sets eight modules, each a heading, a sentence and a line drawing, at 4, 3 or 2 columns: two at 1023px and under, and one at 560px and under. Its cells pad 26px, 24px and 30px with a 268px floor. It draws its seams with the cell borders the plate's Cell Borders setting copies, in two inks: the grid's frame at `--gtb-line` and the cell seams at `--gtb-line-2`. Its stylesheet is skinned through `--gtb-*` custom properties, and a pointer-tracked wash lights each cell unless `spotlight` is off. No page on this site mounts it today. ```tsx import FeatureBento from '@/components/shared/FeatureBento'; <FeatureBento columns={3} heading='The full stack for localization' spotlight={false} />; ``` ## The grid view [#the-grid-view] The shell's grid mode lays a route's items out as captured frames over the stage. The plate mounts the live `GridView` with the ten /brand sections in it and reads the columns the browser resolved, so the readout is the grid's own answer. <GridViewDemo /> The grid fills its columns with `repeat(auto-fill, minmax(300px, 1fr))` and a 26px gap, inside 22px of padding over, 34px at each side and 56px under. A section label spans every column over a `--pt-hair` rule, and the first label has none. Each item is a 26px number column beside a 16:9 frame with a `--pt-edge` border on the card corner, so every tile owns its frame and no two tiles share an edge. The active tile draws its frame in ink with a 1px ink outline 2px outside it. | Window | Stage | Columns | | ------------------------------- | ------ | ------------ | | 1720, list closed | 1720px | 5 at 308.8px | | 1440, list in outline density | 1232px | 3 at 369.3px | | 1440, list in thumbnail density | 1184px | 3 at 353.3px | | 1440, list closed | 1440px | 4 at 322.5px | | 1280, list in outline density | 1072px | 3 at 316px | | 1024, list in outline density | 816px | 2 at 359px | | 390, a phone | 390px | 1 at 318px | The grid is a scroll region, so its 4px scrollbar gutter comes off the width before the padding does. Under a 372px stage the content is narrower than one 300px column, and the column keeps its 300px, so the frames run past the right edge. ```tsx import { GridView } from '@/components/viewer/GridView'; <GridView> <ThumbList density='thumbs' sections={sections} thumb='shot' /> </GridView>; ``` `GridView` takes its children and an accessible `label`, which reads Every item as a grid when it is left out. `ViewerShell` renders it over the stage while the mode is grid, with a `ThumbList` of the route's sections inside. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Shells and frames > The viewer shell, the sheet frame and the plate: the frames every hub page and figure sits in, taken apart and adjustable. URL: https://www.prototemplate.com/docs/components/shells Every page of the hub sits in the viewer shell: a sidebar, a 52px toolbar, a stage and an index panel that slides over it. A slide on the stage sits in the fixed sheet, which scales a 1600 by 900 artifact to fit and draws the deck's edge grid on it. Every live figure on these docs pages sits in a plate. This page takes the three frames apart. ## The viewer shell [#the-viewer-shell] `ViewerShell` is a fixed frame over the whole viewport, so the plate draws a model of it at a window size: every region stands where the shell's stylesheets put it and draws only the edges the junction table gives it. The stage holds the live parts: the fixed `Sheet` with its `SheetFrame` in slide mode, the `GridView` in grid mode, and the flow `Sheet` holding a book page in book mode. <ShellAnatomyDemo /> Window sets the model's size, and Stage switches the mode as the toolbar's segmented control does. List density picks the sidebar's two widths, List and Index open and close the sidebar and the index panel, and Present takes the chrome away. At 900 pixels and under the list starts closed and floats over the stage above a scrim when it opens, and the sheet pads 12 pixels and sits under the toolbar. A click on either half of the slide pages it. | Region | Size | The edge it owns | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------- | | Sidebar | 208px in outline density, 256px in thumbnail density; at 900px and under an overlay `min(84vw, 300px)` wide | its right edge | | Sidebar head | 52px: the mark, the title and the density toggle | its bottom edge | | Filter row | 40px | its bottom edge | | List rows | 28px in outline density with one rail in the row role down each group; 44px with a 64 by 36 capture in thumbnail density; group headers 24px with 16px above | none | | Toolbar | 52px, its two groups 8px apart inside | its bottom edge | | Stage | the main region between the toolbar and the progress line | none | | Index panel | 460px from the toolbar's rule to the main region's foot, over the progress line, on a 28 percent ink scrim | its left edge | | Progress line | 2px: a `--pt-hair-soft` track with an ink fill | none | The shell counts as narrow at 900 pixels and under, and on a phone on its side, which matches `(pointer: coarse) and (max-height: 500px)`. Present closes the sidebar's column and removes the toolbar and the progress line, so the stage is the window. | Junction | Owner | The other side | | ------------------------ | ----------------------------- | ----------------------------------------------------------------- | | Sidebar and stage | the sidebar's right edge | the main region draws no left edge | | Toolbar and stage | the toolbar's bottom edge | the stage and the index panel draw no top edge | | Index panel and stage | the panel's left edge | a fixed sheet's ring runs under the panel | | Sidebar head and toolbar | each its own side of the seam | the two bottom rules meet at the sidebar's edge and never overlap | ### Import and props [#import-and-props] ```tsx import { ViewerShell } from '@/components/viewer/ViewerShell'; <ViewerShell count='52 slides' id='deck' keys='paged' mark='gt' modes={['slide', 'grid', 'book']} sections={sections} surfaces={surfaces} thumb='shot' title='Deck' > <Sheet variant='fixed'>{slides}</Sheet> </ViewerShell>; ``` | Prop | Type | What it does | | ------------------------ | --------------------------------------------- | ----------------------------------------------------------------------------------- | | `id` | string | The storage namespace for the saved mode, and `body[data-shell]`. | | `title`, `mark`, `count` | string, `'gt'` or `'pt'`, string | The sidebar head and the toolbar's brand; the count is already worded. | | `sections` | `ShellSection[]` | The route's items, grouped; the sidebar, the grid and the count read them. | | `modes` | `ShellMode[]` | The modes the route offers, in the toolbar's fixed order; the first is the default. | | `keys` | `'paged'`, `'flow'` or a function of the mode | Paged routes take the arrows and Space; flow routes let them scroll. | | `thumb` | `'shot'` or `'row'` | Captured frames or frameless rows in the list and the grid. | | `surfaces` | `SurfaceSet` | The registry the index panel opens on. | | `toolbarSlot` | node | The route's own controls, first in the toolbar's right group. | | `children` | node | The stage content: a `Sheet`, and a `BookView` while the mode is book. | Children read the shell's state with `usePtShell()`: the mode, the active item, the count, `narrow` and `present`, and the setters. ## The fixed sheet [#the-fixed-sheet] The fixed sheet is the edge of a fixed-size artifact: a slide, a site exhibit at 1440 pixels, a pane of the compare rig. The plate mounts the live `Sheet` and `SheetFrame` on a stage of the size the sliders give, inside the two contexts `ViewerShell` would publish, and prints what `fitSheet()` returns. Previous and Next page it, and so does a click on either half of the sheet. <SheetDemo /> `fitSheet()` scales the 1600 by 900 stage to the space left after the pad and centers it, then places the sheet's border box one pixel out, so the border sits around the scaled content and never over it. The ring is drawn as a mat with no shadow: a 1px `--pt-hair` border on the sheet, a 1px gap of paper, and a 1px `--pt-hair-soft` outline. The stage around it paints `--pt-plate`, the letterbox. At 900 pixels and under the pad drops to 12 pixels and the sheet sits under the toolbar, and Present removes the ring, the plate and the pad. | Part | Value | | -------- | ----------------------------------------------------------------------------------------- | | Stage | 1600 by 900 for a slide, 1440 by 900 for a site exhibit, scaled by `fitSheet()` | | Pad | 28px, 12px at 900px and under, 0 while presenting | | Ring | a 1px `--pt-hair` border, a 1px paper gap, a 1px `--pt-hair-soft` outline | | Rails | 1px `--pt-hair`, 56px in from the left and right edges | | Rules | 1px `--pt-hair`, 56px in from the top and bottom edges | | Crosses | 11 by 11 at `--pt-cross`, centered on the four meeting points | | Wordmark | the GT mark at 28 by 18, 72px from the left and 18px from the bottom, in titanium | | Counter | 13px tabular figures in titanium, 72px from the right and 22px from the bottom | | Paging | a click on either half of the sheet, or a 40px swipe; chevrons show at the edges on hover | ### Import and props [#import-and-props-1] ```tsx import { Sheet, fitSheet } from '@/components/viewer/Sheet'; <Sheet variant='fixed' w={1600} h={900}> {slides} </Sheet>; <Sheet variant='flow' scrollRef={region}> {book} </Sheet>; const fit = fitSheet({ aw: 1232, ah: 846, w: 1600, h: 900, pad: 28 }); ``` | Prop | Type | Default | What it does | | ----------- | ------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------ | | `variant` | `'fixed'` or `'flow'` | required | The scaled artifact on the plate, or a reading page in the book column. | | `w`, `h` | number | 1600, 900 | The fixed stage's size before scaling. | | `frame` | boolean | `true` | Mounts `SheetFrame`: the rails, rules, crosses, wordmark and counter. Site exhibits turn it off. | | `fit` | `'contain'` or `'height'` | `'contain'` | `height` fills the stage's height and pans sideways. | | `caption` | node or `false` | the item and its width | The 24px line under a frameless sheet. | | `itemKey` | string | none | Keys the children by item, so the leaving slide fades while the next one rises in. | | `scrollRef` | ref | none | The flow sheet's scroll region, for the progress line and scroll spies. | `SheetFrame` takes `wordmark` and `counter`, both on by default; thumbnail and page clones turn both off to carry the grid alone. ### The reading column [#the-reading-column] The flow sheet holds the deck's book column: 1280 pixels at most, padded by `--pt-col-pad-x` at each side, `--pt-title-clear` over the title and 120 pixels under the last block. The column draws no border, no ground and no mat, and every structural rule of a book runs across the whole stage. The book mode of the plate above draws a book page as a skeleton at these values. | Token | Value | 900px and under | | -------------------------------------- | --------------------------------------------- | --------------------------------------------------------- | | `--pt-col-pad-x` | 56px | 16px | | `--pt-title-clear` | 44px | 24px | | `--pt-d1` (the title) | 44px on 1.04, -0.025em | 32px | | `--pt-head-gap` | 14px from the title to the lead | 14px | | `--pt-head-col-gap`, `--pt-head-panel` | 56px, then a 304px panel beside the lead | the panel stacks under the lead below 880px of head width | | `--pt-row` | 26.35px, one lead line, the panel's row pitch | the same | | `--pt-head-rule-pad` | 26px to the mast's rule | 26px | | `--pt-sec-over` | 48px from the block above to the band | 40px | | `--pt-band-h` | 42px, both rules included | 42px | | `--pt-sec-pad` | 30px from a divider's rule to its h2 | 30px | ## The plate [#the-plate] Every live demo on these pages is a `Plate`: a card with the stage on top and the controls row under it, one hairline between them. This plate's own props are its controls, so the stage, the corner tag and the ground change as they are set. <PlateDemo /> The stage follows the docs theme unless `night` pins it to ink, which the GPU fields need because their palettes are tuned to an ink ground. A `fit` stage takes its content's height, padded 52 pixels over so the corner tag clears it. A `lazy` plate mounts its children only when it comes within 180 pixels of the viewport, and a `moves` plate says in its controls row when reduced motion holds it still. | Part | Value | | ------------ | ------------------------------------------------------------------------------------- | | Frame | 1px `--pt-hair` on the 6px card corner, 24px over the plate and 12px under it | | Stage | 300px tall by default, `phoneHeight` under 640px; `fit` pads 52px, 20px and 24px | | Tag | 10px from the stage's corner, a chip on the stage's ground, in mono at the group size | | Controls row | a `--pt-hair` rule over it, 12px and 18px gaps, 12px, 14px and 14px of padding | ### Import and props [#import-and-props-2] ```tsx import { Choice, Range, Toggle } from '@/components/docs/demos/controls'; import Plate from '@/components/docs/demos/Plate'; <Plate controls={<Range label='Gap' max={7} min={1} step={0.5} value={gap} onChange={setGap} />} height={280} moves tag='<DoubledLine />'> {drawing} </Plate>; ``` | Prop | Type | Default | What it does | | ----------------------- | ------- | --------------- | ------------------------------------------------------------------------------- | | `tag` | string | required | The entry point's name in the stage's corner. | | `label` | string | required | What the plate shows, for assistive technology. | | `controls` | node | none | The controls row: `Choice`, `Range`, `Toggle` and `Action` from `controls.tsx`. | | `height`, `phoneHeight` | number | 300, the height | The stage's height at desktop width and under 640px. | | `night` | boolean | `false` | An ink stage in both themes. | | `fit` | boolean | `false` | The stage takes its content's height. | | `bare` | boolean | `false` | No corner tag, for a drawing that fills the stage. | | `lazy` | boolean | `false` | Mounts the children only once the plate nears the viewport. | | `moves` | boolean | `false` | Under reduced motion, the controls row says the plate shows its still. | | `stageClassName` | string | none | A class on the stage, for the demo's own layout. | The plates on the page layout pages lay a page part out at its own width and scale it into the stage, so a 1440 pixel page reads inside the docs column without overflowing it. The media queries inside a model still follow the real viewport, so each model reads its breakpoint values from the width it is given. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Page gallery > Every shipped page of the production mirror, live in a frame at the desktop and the phone width in both themes, picked from one list. URL: https://www.prototemplate.com/docs/components/page-gallery The production mirror at [/d/production](https://www.prototemplate.com/d/production) rebuilds generaltranslation.com page for page, together with the dashboard's sign-in and onboarding states. The gallery holds every page the mirror ships and the hub's own pages, and it shows one of them at a time as the live page. ## Gallery [#gallery] Pick a page in the list under the frame to load it. Width lays the page out at 1440 or 390 pixels, and Theme starts on the docs theme and follows it. <PageGalleryDemo /> Each tile shows the page's still in the plate's theme, and only the page you pick runs live. A sign-in or onboarding page adds State, which opens any state of the journey through the page's `?state=` address. A page with no still shows its path on a blank tile. Open Page leaves the docs for the page itself. ### Kinds [#kinds] | Kind | Pages | Routes | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- | | Home | Home | `/d/production` | | Product | Report card, supported locales | `/report-card`, `/supported-locales` | | Pricing | Pricing, usage rates | `/pricing`, `/pricing/usage` | | Enterprise | Enterprise, enterprise contact and the seven partner credit pages | `/enterprise`, `/enterprise/contact`, `/enterprise/contact/<partner>` | | Company | Careers, contact | `/careers`, `/contact` | | Blog | The index, a post and a release | `/blog`, `/blog/<slug>` | | Legal | The index and two of the seven documents | `/legal`, `/legal/<document>` | | Sign-in and onboarding | Sign in, onboarding, consent, device code and CLI, each with every state | `/signin`, `/onboarding`, `/consent`, `/device`, `/cli` | | Errors | The 404 page and the blog's missing post | any address the mirror does not hold, `/blog/<unknown slug>` | | Hub | Gallery, brand, docs, deck, press, present, compare, skills, handbook, marks, blog, atlas, graphics, motion, transcreate | `/`, `/brand`, `/docs` and the rest | Every route in the first nine rows sits under `/d/production`. The 404 page answers with HTTP status 404, as the shipped site does, and a missing post renders the blog's own not-found page with the closest posts. ### Frames and stills [#frames-and-stills] A plate keeps at most one live page. The frame is created when the plate comes within 180 pixels of the viewport and removed when the plate leaves it, so a page you have scrolled past holds no memory and runs no scripts. In the presenter, 12 live pages held about 1.3 GB of renderer memory and a wall of 22 made it lag, so the list is drawn from stills. The stills are the 640 by 360 cuts of each page's 1440 first fold that `pnpm build:thumbs` writes to `public/shots/thumb`, one for each theme. They load lazily at their own size. The picked page's still also stands in the frame's place until the page has loaded. The frame is a same-origin iframe scaled to the plate, on the pattern of the [book head plate](https://www.prototemplate.com/docs/components/heads). The plate sends the page the theme with the root layout's `gt-theme` message on load and on every change, so the page shows the plate's theme even when no theme is saved in storage. A mirror page loads with `?chrome=0`, which hides the direction corner. ### New pages [#new-pages] A page added to the mirror takes a row in `siteRoutes()` and in `src/lib/surfaces.ts`, then its captures with `pnpm capture:pages --only <id>` and `pnpm build:thumbs`. Its row in the gallery is one line in `src/components/docs/demos/pages/gallery-pages.ts`, under its kind, with `still` set once the captures exist. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Page templates > The page types the hub builds from: the book page, the docs page, the article and the error page, each assembled from its parts. URL: https://www.prototemplate.com/docs/components/page-templates The hub builds its pages from four types. A book page is a hub route inside the viewer shell, a docs page is the layout you are reading, an article is a post on the production mirror's blog, and the error page is the mirror's 404 page. Each plate frames a live page of one type. Anatomy outlines every part, draws its spacing as a hatched band, and labels the part with its spacing token and the value the page computes at the frame's width. ## Book page [#book-page] Every hub page with a book head has one structure, set by DESIGN.md section 4 and held by `pnpm lint:heads`. Page picks a route built on it, and the labels change with Width because the tokens step down under 900 pixels. <PageTemplateDemo template="book" /> | Part | Component | File | Spacing token | | ----------- | ----------------------------- | ------------------------------------ | ----------------------------------------------------------------- | | Sidebar | `Sidebar` | `src/components/viewer/Sidebar.tsx` | `--pt-sb-w`, 208px | | Toolbar | `Toolbar` | `src/components/viewer/Toolbar.tsx` | `--pt-bar-h`, 52px | | Title | `BookHead` | `src/components/viewer/BookView.tsx` | `--pt-title-clear`, 44px and 24px under 900px | | Lead | `BookHead` | `src/components/viewer/BookView.tsx` | `--pt-head-gap`, 14px | | Panel | `BookHead` | `src/components/viewer/BookView.tsx` | `--pt-head-col-gap`, 56px, in a `--pt-head-panel` column of 304px | | Mast rule | `BookHead` | `src/components/viewer/BookView.css` | `--pt-head-rule-pad`, 26px | | Contents | the route's `nav.pt-book-toc` | `src/components/viewer/BookView.css` | `--pt-book-gap`, 32px and 24px under 900px | | Hatch band | `BookHead` | `src/components/viewer/BookView.css` | `--pt-sec-over`, 48px and 40px under 900px | | Divider | the route's `.pt-book-sec` | `src/components/viewer/BookView.css` | `--pt-sec-pad`, 30px | | Gutter note | the divider's `small` | `src/components/viewer/BookView.css` | `--pt-gutter`, 128px, with a `--pt-gutter-gap` of 28px | Under 880 pixels of head width the panel stacks under the lead, and under 900 pixels the gutter note moves above the section title, so the plate leaves the panel's gap and the gutter out of the phone frame. The sidebar folds into an overlay at the phone width. ## Docs page [#docs-page] The docs site runs fumadocs' notebook layout, the one generaltranslation.com runs at /docs, and `docs.css` puts its parts on the hub's type steps. Page picks a page with prose and one from the handbook. <PageTemplateDemo template="docs" /> | Part | Component | File | Spacing token | | -------- | --------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------ | | Sidebar | `DocsLayout` with `DocsNameplate` and `DocsSidebarFooter` | `src/app/docs/layout.tsx` | `--fd-sidebar-width`, 268px | | Header | `DocsLayout` | `src/app/docs/layout.tsx` | `--fd-header-height`, 56px | | Page | `DocsPage` | `src/app/docs/[[...slug]]/page.tsx` | `padding-top` from fumadocs' `py-6 md:pt-8 xl:pt-14`: 24px, 32px from 768px and 56px from 1280px | | Title | `DocsTitle` | `src/app/docs/[[...slug]]/page.tsx` | the page column's `gap-4`, 16px | | Lead | `DocsDescription` | `src/app/docs/docs.css` | `--pt-measure-lead`, 30em | | Meta row | `.docs-meta` with `DocsPageActions` | `src/app/docs/docs.css` | `padding-bottom`, 16px | | Body | `DocsBody` | `src/app/docs/docs.css` | `--pt-measure`, 32em, on paragraphs, lists and quotes | | Contents | fumadocs' table of contents | `src/app/docs/layout.tsx` | `--fd-toc-width`, 268px | Under 1280 pixels the contents move into a bar over the page, and under 768 pixels the sidebar moves into the header's menu. Tables, code and plates keep the full column, and only running text holds to the measure. ## Article [#article] A post on the production mirror's blog renders the shipped article page section for section: the nav, the header with its back rail, its copy and its cover, the contents and share rail beside the prose, and the footer. Page picks a post or a release, whose rail also carries its tags. <PageTemplateDemo template="article" /> | Part | Component | File | Spacing token | | --------- | -------------------------- | ------------------------------------------------------ | ---------------------------------------------------- | | Nav | `ProdNav` | `src/app/d/production/sections/ProdNav.tsx` | `height: 56px` and a 1px rule | | Rail | the route's `main.tc-rail` | `src/app/d/production/blog/[slug]/page.tsx` | `--tc-rail`, 1104px | | Header | `BlogArticle` | `src/app/d/production/sections/BlogArticle.tsx` | `padding-top: clamp(48px, 6vw, 76px)` | | Back rail | `BlogArticle` | `src/app/d/production/sections/BlogArticle.tsx` | a 220px column, `column-gap: clamp(28px, 4vw, 56px)` | | Cover | `BlogArticleCover` | `src/app/d/production/sections/BlogArticleCover.tsx` | `margin-top: 22px` | | Sidebar | `BlogArticleSidebar` | `src/app/d/production/sections/BlogArticleSidebar.tsx` | sticky at `top: 76px` | | Body | `BlogPostBody` | `src/app/d/production/sections/BlogPostBody.tsx` | `padding-top: 30px` | | Footer | `BlogArticleFooter` | `src/app/d/production/sections/BlogArticleFooter.tsx` | `padding: clamp(40px, 5vw, 64px)` | The mirror ports the shipped article's stylesheet rule for rule into `blog-article.css`, and that sheet writes its spacing as pixel values and `clamp()` ranges, so the table gives the declaration. The footer sits under the body, so scroll the frame to bring it into view. Under 981 pixels the contents and share rail becomes a bar pinned under the nav. At 390 pixels the rail fills the width, so the plate labels the rail, the back rail and the sidebar at 1440 only. ## Error page [#error-page] An address the production mirror does not hold renders the shipped 404 page and answers with HTTP status 404: the nav, one band with the horizon, the apology inside its hole, the two exits, the code in the corner, and the site footer. <PageTemplateDemo template="error" /> | Part | Component | File | Spacing token | | ------- | ----------------- | --------------------------------------------------- | -------------------------------------------------------------------- | | Nav | `ProdNav` | `src/app/d/production/sections/ProdNav.tsx` | `height: 56px` and a 1px rule | | Band | `NotFound` | `src/app/d/production/sections/not-found.css` | `min-height: clamp(560px, calc(100svh - 58px), 920px)` | | Horizon | `NotFoundHorizon` | `src/app/d/production/sections/NotFoundHorizon.tsx` | `--pnf-hole-radius`, placed at `--pnf-hole-x` and `--pnf-hole-y` | | Copy | `NotFound` | `src/app/d/production/sections/NotFound.tsx` | `width: min(calc(var(--pnf-hole-radius) * 1.55), calc(100% - 40px))` | | Exits | `NotFound` | `src/app/d/production/sections/NotFound.tsx` | `margin-top: 28px` | | Code | `NotFound` | `src/app/d/production/sections/not-found.css` | `right: 24px` and `bottom: 18px` | | Footer | `ProdFooter` | `src/app/d/production/sections/ProdFooter.tsx` | `--tc-rail`, 1104px | The horizon computes the hole's center and radius from the band's width and height in `NotFoundHorizon.tsx` and sets them as `--pnf-hole-x`, `--pnf-hole-y` and `--pnf-hole-radius`, and the copy is placed on that center. The overlay reads the same three properties and draws the hole as a circle. The footer fills the width at 390 pixels, so the plate labels it at 1440 only. ## Frames [#frames] Each plate keeps one live page, created when the plate comes within 180 pixels of the viewport and removed when it leaves, as the [page gallery](https://www.prototemplate.com/docs/components/page-gallery) does. The overlay reads the framed document directly, because the frame is same-origin. It measures the parts on load, on every scroll inside the page and on every resize, and it never takes a pointer, so the page under it stays live. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Design numbers and traps > The numbers, traps and reasons behind the rules the hub’s instruments follow: type, lines, SVG motion, dither, writing, interface details and films. URL: https://www.prototemplate.com/docs/craft The Components tab shows each of the hub's instruments running. This tab gives the measurements, the failure modes and the reasons behind the rules those instruments and General Translation's sites follow. Each page states a rule a stranger can reuse, then the number that settled it and the fix, and names the file, lint or skill that holds it. The facts come from renders, browser probes and review rounds between August and October 2026. <Cards> <Card title="The correct Inter in practice" href="/docs/craft/type" description="Binding a self-hosted variable font, contextual alternates under tracking, optical size, measure, the Arial fallback, preloads and reading the rendered instance." /> <Card title="Lines and seams" href="/docs/craft/lines" description="Reading a 1px seam by its composite grey, self-stacks, rails drawn as borders, full-width rules, stacking contexts and what the line auditor counts." /> <Card title="SVG lines in motion" href="/docs/craft/svg-motion" description="Five dash traps with their proofs, GSAP seen from a test harness, the timing numbers, stretched viewBoxes, symbols and masks." /> <Card title="Dither in practice" href="/docs/craft/dither" description="Why the screen is ordered, why cells are screen pixels, transitions on one grid, the hero's material strength and the picture standard." /> <Card title="Writing" href="/docs/craft/writing" description="The plain technical register with before and after rewrites, sentence shape, writing for a stranger, and type on slides and cards." /> <Card title="Interface details" href="/docs/craft/interface-details" description="Glyph gaps, the copy control, tap targets, measured layout shifts, third-party themes, contrast ratios, carousels and hit areas." /> <Card title="Deterministic films and sound" href="/docs/craft/films" description="Render pairs compared by framemd5, the image race, fonts, chroma on odd pixels, thin lines over dither, the mix targets and speech synthesis." /> </Cards> ## Ten numbers [#ten-numbers] | Number | What it means | Page | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | 398.55px and 380.81px | A line of type tracked -0.025em at 44px, without and with `'calt' 1, 'liga' 1`: Chrome turns contextual alternates off under any letter-spacing | [Type](https://www.prototemplate.com/docs/craft/type#contextual-alternates-under-letter-spacing) | | 83 to 86 characters | What `max-width: 62ch` holds at 17px, because `ch` is the width of a zero; 30em holds about 66 | [Type](https://www.prototemplate.com/docs/craft/type#measure) | | 14 to 32 | The optical-size range Inter's axis follows the font size through; from 32px the Display design is in effect | [Type](https://www.prototemplate.com/docs/craft/type#optical-size) | | 59 and 99 | The grey of one and of two stacked hairlines in the dark theme, so a seam's grey counts its strokes | [Lines and seams](https://www.prototemplate.com/docs/craft/lines#reading-a-seam-by-its-grey) | | -101 with `'100 200'` | Where a draw-on dash parks so it grows from the path's end with no fleck; +101 grows from the start | [SVG lines in motion](https://www.prototemplate.com/docs/craft/svg-motion#the-sign-of-the-parked-offset) | | 350ms and 150ms | A dithered resolve and a dithered step, on one cell grid with one anchored tile | [Dither in practice](https://www.prototemplate.com/docs/craft/dither#transitions-on-one-grid) | | 0.361 and 0.342 | The mean tone and standard deviation every dithered photograph is cut to | [Dither in practice](https://www.prototemplate.com/docs/craft/dither#the-picture-standard) | | 44px and 40px | The phone tap target, and the size under which the page check fails one | [Interface details](https://www.prototemplate.com/docs/craft/interface-details#tap-targets-that-keep-the-look) | | 0.001 | The layout-shift entry above which a jump is a defect, including the ones within 500ms of an input that CLS leaves out | [Interface details](https://www.prototemplate.com/docs/craft/interface-details#layout-shifts) | | -16 LUFS and -1 dBTP | A film's loudness within 0.5 and its true-peak ceiling | [Deterministic films and sound](https://www.prototemplate.com/docs/craft/films#the-mix-and-its-traps) | --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # The correct Inter in practice > Keeping the browser from replacing or degrading the rsms InterVariable 4.1: binding, features, optical size, tracking, measure, scripts and checks. URL: https://www.prototemplate.com/docs/craft/type Prototemplate sets one face, the rsms InterVariable 4.1, self-hosted from `public/fonts/`. The roman file is 352,240 bytes with sha1 `f55b18fd`, and it has two axes: optical size (`opsz`, 14 to 32) and weight (`wght`, 100 to 900). Most of the work is keeping the browser from replacing that file or degrading it, and the browser reports none of these failures. The measurements below come from a type audit on 2026-10-05 (Chrome for Testing, dark theme, 1440 and 390 wide, read after `document.fonts.ready`) and from a probe of these docs on 2026-10-21. The [gt-brand](https://www.prototemplate.com/skills/gt-brand) skill holds the rules, and `scripts/lint/type.mjs` enforces them. ## The binding [#the-binding] Bind a self-hosted font to an identifier that no installed font carries. next/font under Turbopack names the CSS family after the JavaScript identifier, so a binding named `inter` produces `font-family: inter`. CSS family names are case-insensitive, which makes that family the same name as a desktop "Inter". | State of the web font | What the page renders | | --------------------- | ------------------------------------------------------------------------------------------------------- | | Loading | The web font hides the installed family at every weight | | Failed to load | The installed family takes the name back, and next/font's metric-matched `inter Fallback` never engages | On a machine with desktop Inter 3 installed, a failed file therefore renders Inter 3, which has no optical-size axis and different metrics. The same collision happens between pages of one site. Eight of the hub's `/d/` direction pages declared `const display = localFont(...)`, Turbopack gave every one the family `display`, and a client-side navigation could swap one page's face for another's. Give every `localFont` call a unique identifier. The hub binds the roman's latin subset as `ptInter` in `src/lib/fonts.ts` and publishes it as `--font-inter`. In a page, `getComputedStyle(document.body).getPropertyValue('--font-inter')` starts with `"ptInter"`. The type lint's rule T3 fails any other binding in that file. ## Subsets and glyph names [#subsets-and-glyph-names] Keep glyph names when you subset a variable font. pyftsubset's default writes a format 3 `post` table with no glyph names, and Chrome on macOS rasterises such a subset a shade differently from the full file at every weight. `scripts/build/subset-inter.py` passes `glyph_names`, and its subsets match the full file pixel for pixel. | File | Bytes | How it loads | | ----------------------------------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | Full roman | 352,240 | The source of the subsets; the brand deck inlines it whole as a data URI | | Roman latin subset | 112,748 | Preloaded on every route through `ptInter` | | Roman latin-ext, latin-ext-a, Greek, Cyrillic, Vietnamese and symbols subsets | 15,000 to 130,984 each | `@font-face` rules with a `unicode-range` in `src/app/inter-subsets.css`, fetched only where a page draws a code point in the range | | Italic subsets | 15,016 to 143,800 each | The same, in the same `ptInter` family | The latin subset also carries the arrows, the command key, the check, the play glyph, the star, the bullet and the comparison signs that the shell draws beside Latin text, so a page of Latin text needs no symbols file. `pnpm build:inter` writes the subsets. Every `localFont` call outside `src/lib/fonts.ts` and the nameplate's `src/lib/brand-fonts.ts` sets `preload: false`, and lint rule T3 holds that. ## Contextual alternates under letter-spacing [#contextual-alternates-under-letter-spacing] Name `calt` in the feature list of any tracked text. Chrome turns off contextual alternates under any non-zero letter-spacing unless the feature list names `calt`. Without them a tracked heading loses the `->` arrow, the colon centred between digits, the hyphen raised between capitals and the parentheses fitted to capitals. | Setting at 44px, weight 500 | Width of `a -> b => c 10:30 A-B` | | ------------------------------------------ | -------------------------------- | | Untracked | 399.94px | | Untracked, `calt` off | 421.64px | | Tracked -0.025em | 398.55px | | Tracked -0.025em, `calt` off | 398.55px | | Tracked -0.025em with `'calt' 1, 'liga' 1` | 380.81px | The tracked width equals the tracked width with `calt` off, so the tracking had already turned the feature off. rsms.me's own stylesheet writes `font-feature-settings: 'liga' 1, 'calt' 1` with the comment "fix for Chrome". `font-feature-settings` is one property, and a rule that sets it replaces the inherited list. Every element in the hub therefore reads one of two tokens from `src/components/viewer/tokens.css`. | Token | Value | Where it applies | | ----------------- | ---------------------------------------- | ------------------------------------------------------------------------------------ | | `--pt-ff-text` | `'liga' 1, 'calt' 1` | Every element | | `--pt-ff-display` | `'liga' 1, 'calt' 1, 'cv11' 1, 'ss01' 1` | h1, h2 and display sentences (`cv11` is the single-storey a, `ss01` the open digits) | A rule that needs aligned figures adds `font-variant-numeric: tabular-nums`, which composes with either token. The brand deck's headings still list `'cv11', 'ss01'` without `calt`, and the 2026-10-05 audit named that as the deck's only type defect. Lint rule T4 fails a feature list outside the two tokens, and the live rule L7 fails tracking without `calt`. ## Optical size [#optical-size] Let the optical-size axis do the tracking. With `font-optical-sizing: auto` the browser sets opsz to the font size in CSS pixels, clamped to 14 to 32, and the audit found that mapping on every run. From 32px the Display design is in effect, and the axis narrows glyphs and spacing together. | Size | Width at opsz auto | Width at opsz pinned to 14 | Width at opsz pinned to 32 | | ---- | ------------------ | -------------------------- | -------------------------- | | 14px | 223.03px | 223.03px | 205.67px | | 24px | 365.80px | 382.34px | 352.56px | | 32px | 470.09px | 509.80px | 470.09px | | 44px | 646.38px | 700.97px | 646.38px | The widths are for "General Translation Motion brand" at weight 500. The axis tightens the spacing by about -0.004em a character at 16px, -0.022em at 24px and -0.039em from 32px. Inter 3's Dynamic Metrics formula is obsolete under Inter 4, and rsms.me no longer serves that page. Never write `font-variation-settings`. It pins opsz and wght for the element and everything that inherits from it, and the automatic mapping stops. `font-optical-sizing: none` stops it too. Lint rule T5 fails both. ## Tracking by role [#tracking-by-role] Tracking tightens with size and weight, and weight 400 text is never tracked. Nothing on Inter is tracked positive in the hub's shell, and lint rule T8 fails a positive value. | Role | Size and weight | Tracking | Token | | -------------------------------- | ------------------- | -------- | --------------------------------------- | | Page and section titles | 32px and up at 500 | -0.025em | `--pt-d1-track`, `--pt-d2-track` | | Figures and 24px heads | 24px at 500 | -0.02em | `--pt-d3-track` | | Display quotes | 27 to 34px at 500 | -0.015em | none in the shell; the deck's `.say .q` | | Labels, row keys, contents links | 13 to 20px at 500 | -0.01em | `--pt-track-label` | | Group labels | 12 to 12.5px at 500 | -0.005em | `--pt-track-group` | | Running text | any size at 400 | 0 | none | The deck's -0.025em on display sizes is a house choice on top of the Display design, and nothing goes tighter. A first pass of a dashboard used one flat -0.028em at every size, and Kevin's review of 2026-09-25 read "kerning needs to be adjusted". Literal tracking values are ratcheted per file by the lint's rule R2. ## Measure [#measure] Set a measure in em. The `ch` unit is the advance of the zero, about 0.63em in Inter, while Inter prose averages about 0.45em a character, so a measure in `ch` holds about 35 to 40% more characters than its number suggests. | Measure | Font size | Characters a line | | ---------------------------- | --------- | ---------------------------------------- | | `max-width: 62ch` (662px) | 17px | 83 to 86 | | 30em (`--pt-measure-lead`) | 17px | about 66 | | 32em (`--pt-measure`, 512px) | 16px | 61 to 71, measured in the `/brand` prose | The brand deck's book-view lead used `62ch` and measured 84 characters a line over four lines at 1440. On 2026-10-05 the `/motion` lead ran seven lines at 1440 and fourteen at 390. A lead should read in two or three lines. `skills/gt-aesthetic/scripts/measure-type.mjs --strict` exits 1 on prose wider than 75 characters, and the live lint's L8 warns on a head lead past three lines. ## Scripts and the Arial fallback [#scripts-and-the-arial-fallback] Set a script's stack on its own rule when the script needs its own face. next/font writes a fallback face for each binding (`ptInter Fallback`) as `local("Arial")` with `size-adjust: 107.89%`. Arial has Hebrew and Arabic glyphs, so in any stack that starts with the Inter variable those scripts render in Arial, and a Hebrew or Arabic face listed later never sees them. Chinese, Japanese, Korean and Devanagari are absent from Arial and fall through to the faces listed after it. The fix is a stack with no Inter variable in front, applied by a `:lang()` selector or a class. The brand deck's `.lang .ar` rule does this for Arabic. On `/motion`, Hebrew renders in Arial by design, and `src/app/motion/motion.css` says so. Inter itself covers Latin, Greek and Cyrillic. ## Weight and stack traps [#weight-and-stack-traps] Each of these rendered a different weight or face than the stylesheet appeared to ask for. | Symptom | Cause | Fix | Found | | --------------------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------- | | Section numerals in a weight 500 block render at 700 | The browser's `bolder` computes `b` and `strong` to 700 | A base rule sets `b, strong` to 500 | The brand book, 2026-10-05 type audit | | A ported blog's headings render at 600 | The port kept its old heading rules | Port the content and read the system's tokens | The blog port, 2026-10-05 type audit | | Docs headings render at 600 and 700 | fumadocs and its typography plugin set those weights | A weight cap in the docs stylesheet ([template](https://www.prototemplate.com/docs/template)) | The docs site's first build, 2026-10-15 | | A label draws a different `a` from the row beside it | A feature list set on the label alone | Labels read the text token, and a state changes weight or colour without touching letterforms | 2026-10-05 type audit | | A second Inter appears only while the page loads | A bare `'Inter'` in a stack is a second binding | Stacks read `var(--font-inter), system-ui, sans-serif` | 2026-10-05 type audit | | `'Helvetica Neue', Arial` after the variable never render | next/font's fallback is already a metric-matched Arial | Remove them | 2026-10-05 type audit | Lint rule T6 fails a weight above 500 outside the nameplate and the specimens, and T2 fails a bare Inter. ## Seating display type [#seating-display-type] Seat each line of a heading by its first glyph's ink. A heading box is not where its ink sits, and Inter's metrics give the offsets. | Metric | Value | | --------------------------------------------- | --------------------------- | | Cap height | 1490 of 2048 units, 0.728em | | Baseline below the top of a line-height-1 box | 0.8638em | | Side bearing of `t` and `v` at 120 to 130px | 7px from the box edge | Seating only the first line of a two-line heading leaves the second wherever its first glyph's side bearing puts it. Set the heading at the leftmost line's position and step the other lines in. Chrome rounds the ascent and descent to whole pixels and floors the half-leading, so a heading's lift above its label steps with its size. On a GT blog page the descent of an h2 over 26.95px rounds from 6 to 7px, which happens from 1348px wide. There the heading sat 1.85px above its label instead of 1px. A 0.15px trim inside that media query measured 1.0px at 1360 and up. Fix a rounding step with a trim in the range where it occurs, and leave the cap ratio alone, because no single ratio fits a step function. ## Canvas text [#canvas-text] A 2D canvas ignores `font-feature-settings`. A heading with two single-storey a's painted on a canvas with plain Inter came out 12px shorter than the same heading in the DOM. Build a `FontFace` from the Inter file with `featureSettings` (`cv11` for headings, `tnum` for figures) and load it before the first paint. WebKit adds two more differences. Its canvas picks the optical size from the painted pixel size, so an export paints with copies of the font pinned to one opsz. `text-rendering: optimizeLegibility` turns automatic optical sizing on in WebKit even under `font-optical-sizing: none`, so canvas text uses `text-rendering: auto`. `geometricPrecision` softens Chromium's small text. The [gt-films](https://www.prototemplate.com/skills/gt-films) skill carries the film kit's version of this rule. ## Preloads that leak across routes [#preloads-that-leak-across-routes] Preload only the face every route draws. next/font preloads every face a module declares on every page that imports the module, and Turbopack shares CSS chunks between routes, so a face meant for one page reaches others. | Case | Measured | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | generaltranslation.com's docs on 2026-09-25 | 9 font preloads, 858KB; the 379KB italic never rendered, because its family re-point did not reach the prose | | A second display face on that site's landing | 6 files, 112,676 bytes, preloaded on every landing route | | An italic joined to the main family | Fetched by the home page for its empty icon `<i>` elements, which are italic by the browser's default style | Of Inter, the hub preloads only the roman latin subset, and the nameplate's two faces are its only other preloads. Every other face sets `preload: false`, and a face used on one route gets a family name that only that route's CSS names, so it cannot leak. The [performance page](https://www.prototemplate.com/docs/engineering/performance) has the measurement recipe. ## The rendered instance [#the-rendered-instance] Read the instance Chrome rasterised. The DevTools protocol's `CSS.getPlatformFontsForNode` names it, with both axes in 16.16 fixed point. | Instance name | opsz | wght | Element | | -------------------------------------- | ----------------------- | ------------------------- | ----------------------------------------- | | `InterVariable_opsz200000_wght1F40000` | 0x200000 / 0x10000 = 32 | 0x1F40000 / 0x10000 = 500 | The 44px page title on `/skills/gt-brand` | | `InterVariable_opsz180000_wght1F40000` | 0x180000 / 0x10000 = 24 | 500 | A 24px prose h2 on the same page | The 17px lead on that page reads `opsz110000`, which is 17. A run that reports any other family is a defect unless the exceptions below name it. Check the files with `shasum public/fonts/InterVariable*.woff2`. The brand deck inlines the roman as a data URI, and decoding it gives the same bytes. The live lint (`pnpm lint:type:live`) reads the same values at 1440 and 390 for every visible text group. ## Glyphs outside the subsets [#glyphs-outside-the-subsets] A character the font does not map falls through to a system face without any warning. The theme button's half-disc `◑` (U+25D1) is outside every Inter subset and renders in Hiragino Sans on macOS, on the hub and in these docs. The live lint's rule L1 reports a Latin run in another face, and its range (`INTER_RANGE` in `scripts/lint/type.mjs`) stops at U+25A0 to U+25A1 in that block, so it never reports this glyph. Read a symbol's face with `CSS.getPlatformFontsForNode`. ## The nameplate and the docs header [#the-nameplate-and-the-docs-header] The hub's nameplate is the one place its chrome leaves Inter. `span.pt-brand-word` sets `proto` in Fraunces 600 and `template` in Space Grotesk 500 at 14.5px, loaded by `src/lib/brand-fonts.ts`. The probe read `proto` at -0.2175px tracking in Fraunces SemiBold and `template` at -0.435px in Space Grotesk Medium, both with `font-feature-settings: normal`. On 2026-10-21 the docs header drew `[data-docs-nameplate]`, the word "Prototemplate" in Inter 15px at weight 500, so these pages did not show the nameplate. [The design canon](https://www.prototemplate.com/docs/handbook/design) lists every chrome exception in its section 15. | Exception | Where | Reason | | ---------------------------------- | ------------------------------------------- | ----------------------------------------------------------- | | Fraunces 600 and Space Grotesk 500 | The nameplate | Kevin asked for the old nameplate back | | Grotesk labels and mono numerals | The gallery on `/` | The Prototemplate chrome, kept apart from the product brand | | Mono | Hex values, locale codes, code names, paths | They are code artifacts | | Their own faces | The `/d/` direction pages | Self-contained explorations | | Weights 300 to 800 | Deck slide 27 and the specimen on `/brand` | They show the weights on purpose | | Sora and Instrument Sans | The presenter's intro lockup | Its type beats set other faces beside Inter on purpose | | Michroma, Orbitron and Anybody | The speed marks | Converted to outlines at build time, so no page loads them | ## Where these docs stand [#where-these-docs-stand] On 2026-10-21 these docs drew the right file and missed the house type. The type base in `tokens.css` sits on the shell's roots, and fumadocs' root (`#nd-notebook-layout`) is not one of them. Every docs element therefore inherits Tailwind preflight's `html` rule, `font-feature-settings: normal`. The live lint found 288 problems on five docs routes at 1440 and 390. | Live lint rule | Findings | Cause | | --------------------------------------- | -------- | --------------------------------------------------------------------- | | L2, features that do not match the role | 261 | Every element inherits `normal` | | L3, a display size off the ladder | 10 | fumadocs' title is `text-[1.75em]`, 28px | | L6, an unbalanced heading | 17 | The title, the contents title and the card titles on `/docs/handbook` | A 28px title also lands at opsz 28, an intermediate instance short of the Display design. A fix that adds the docs root to the base rules and gives `docs.css` a ladder section was injected into the rendered pages and measured on `/docs/handbook/design` at 1440. | Part | Before | With the fix | | ------------------ | ---------------------------------------------- | --------------------------------------------------- | | Title | 28/42px, untracked, features `normal`, opsz 28 | 44/45.76px, -1.1px, display list, balanced, opsz 32 | | Lead | 18/28px, 840px wide, 69 characters a line | 17/26.35px, 510px wide | | Body | 16/28px, 840px wide, 107 characters a line | 16/25.6px, 512px wide, 64 characters a line | | Prose h2 | 24/32px, untracked | 24/30px, -0.48px, display list, balanced | | Table cells | 14/24px | 14/21.7px, tabular figures | | Live lint findings | 288 | 0 | The cascade behind the `normal` features, and why the obvious one-token fix leaks to every route after a client navigation, are on the [docs site traps](https://www.prototemplate.com/docs/engineering/docs-traps) page. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Lines and seams > The arithmetic of 1px rules: composite greys, self-stacks, rails as borders, full-width rules, stacking contexts over seams and what the auditor counts. URL: https://www.prototemplate.com/docs/craft/lines Structure on these pages comes from 1px hairlines, and the design canon's line law says every line is drawn exactly once. The hairlines are translucent, so a line drawn twice composites darker than its neighbours and reads as another colour. Most doubles come from two elements that each draw a correct line on the same edge, and a full-page screenshot hides them. This page gives the arithmetic for reading a seam, the cases that stack strokes without an obvious second border, and what the line auditor counts. [The design canon](https://www.prototemplate.com/docs/handbook/design) states the law in its sections 2 and 3. ## Reading a seam by its grey [#reading-a-seam-by-its-grey] A seam's grey says how many strokes drew it. A stroke of alpha `a` and colour `L` over a ground `G` composites to `a*L + (1 - a)*G`, and two stacked strokes composite at alpha `1 - (1 - a)^2`. With the shell's hairline token the expected readings are fixed numbers. | Line | One stroke | Two strokes | | ---------------------------------------- | --------------------- | ----------- | | `--pt-hair` in dark: 242 at 0.22 over 7 | 59 | 99 | | `--pt-hair` in light: 7 at 0.18 over 255 | 210 | 174 | | A titanium stroke at 0.5 over ink | about rgb(72, 75, 80) | 101 to 110 | A reading near the two-stroke value means a double line or one stroke over a brighter backing such as a shader glow. Check the neighbouring pixels to tell them apart: a glow raises them too, and a double leaves them at the ground. On 2026-10-05 a scan across the toolbar's bottom rule read 59 in Chromium and 58 in WebKit, which is one stroke. An opaque hairline keeps its grey when it is doubled. The GT landing's main hairline `--tc-hair` is opaque, so there a double shows as a run twice as wide: 4 device pixels at 2x for two 1px rules. Read every seam at a device scale factor of 2. The probe in the [gt-verify](https://www.prototemplate.com/skills/gt-verify) skill reads seams this way. `node skills/gt-verify/scripts/probe.mjs <url> --scan x,y,down,24 --dsf 2` groups the device pixels along a 24px segment into runs of one grey, prints each run's width in device pixels and marks the runs that are lines. The coordinates are CSS pixels in the viewport. ## Self-stacks and stacked grounds [#self-stacks-and-stacked-grounds] A background paints to the border box by default, so a translucent border over the element's own translucent fill paints darker than every other hairline on the page. This is the self-stack, one of the line law's four defect classes. Two stacked translucent grounds do the same: two grounds at 0.26 alpha composite to about 0.45. | Case | Fix | | ----------------------------------------------- | ---------------------------------------------------------------------------------- | | A bordered element with a translucent fill | `background-clip: padding-box`, so the fill stops inside the border | | A cell with a ground inside a row with a ground | Cells stay transparent, and the row paints the one ground | | A framed cell in a ruled row | A 1px padding gap shows the row's ground as the frame, so the cell draws no border | Every button face on the GT landing carries `background-clip: padding-box`, because a hair border over a hair fill composited darker than the page's other hairlines. The line auditor gates a self-stack only when the fill alpha is 0.12 to 0.95 and the edge is 120px or longer, so chips and small controls are reported without failing. ## Rails drawn as borders [#rails-drawn-as-borders] Draw a page's column lines as a border. A border rounds to whole device pixels at any zoom, and a background-filled box does not. The column wrapper draws its two lines once with `border-inline`, and a band inside the wrapper draws no side lines of its own. A full-bleed band outside the wrapper draws its own pair once, at the column edges, from its inner column. The GT landing's rail token is 1104px because its original pages ran `max-w-[1120px] px-2`, which put their side lines 1104px apart. A first rail of 1100px doubled every hairline against the old sections by 2px a side. The retired second pair of lines at plus or minus 10px came from a `::before` inset -10px from the padding box, and the padding box sits 1px inside the border, so its real offset was 9px. Any band pseudo-element that did not use the shared token jogged 1px at the junctions. The practices lint (`scripts/lint/practices.mjs`) fails a pseudo-element that pushes an inline border pair outside its box, the retired token and prose that still recommends the retired pair. ## Rules that run the full width [#rules-that-run-the-full-width] A structural rule can run across the whole stage while its element keeps the column's layout. The element keeps its 1px border for layout and paints it as a border image outset 100vmax past both ends. The outset never scrolls, and the scroll region clips it at the stage's edges. Kevin asked for full-width rules on 2026-10-07. | Trap | Cause | Fix | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- | | A rule that should be gone still paints | Lightning CSS, Turbopack's CSS compiler, writes `border-image: none` out as `border-image: ;`, which browsers drop | Remove a border image with `border-image-source: none` | | A border image paints on an element with a border of 0 | An explicit pixel border-image width ignores the border width | The same longhand removal | | Rules keep their author colour in forced colours | Chromium keeps author colours in border images | The hairline tokens become `CanvasText` under `forced-colors` | | A grid of links draws stacked lines | Every link in a row drew the same rule | Only the first link of each row draws it (`4n+1`, and `2n+1` under 900px) | | A hover-scale reset in CSS does nothing | Lightning CSS folds a standalone `scale` into `transform` | Reset the scale in the markup | The practices lint's `border-image-none` check fails the shorthand in any stylesheet under `src/`. ## Stacking contexts over seams [#stacking-contexts-over-seams] A stacking context paints over neighbouring 1px seams. A standing `will-change` or a leftover transform of 0px creates one. On the GT landing, reveal tweens left transforms on framed cards, and the cards then painted over the top rule of the hatch band below them. | Fix | Where | | ---------------------------------------------------------------------------------------- | ------------------------------------------------------- | | Set `will-change` only for the length of the tween and clear it on complete | The tween's `onComplete` | | Clear exactly what a tween set: `strokeDashoffset`, `transform`, `opacity`, `visibility` | The cleanup of any band that wrote them | | Give a band that must stay on top its own context | The hatch band carries `position: relative; z-index: 2` | The [gt-motion](https://www.prototemplate.com/skills/gt-motion) skill lists this trap with the GSAP ones, and [SVG lines in motion](https://www.prototemplate.com/docs/craft/svg-motion) covers the rest of them. ## Registration crosses and horizontal clipping [#registration-crosses-and-horizontal-clipping] A registration cross marks the point where a section seam meets the rail. The GT landing draws it with 9px arms, 1px thick, hanging 5px past the rail, in ink at 0.38 in light and white at 0.34 in dark. One pseudo-element per seam is enough, because the next section's top corners are the same points. A full-bleed band clips its own pseudo-elements, so the section after it paints its own top pair. The arms reach 5px past the rail, so the page root needs horizontal clipping. `overflow: hidden` makes the root a scroll container, and every `position: sticky` figure further down the page stops sticking. The root takes `overflow-x: clip`, which clips without creating a scroll container. ## The line auditor's thresholds [#the-line-auditors-thresholds] `scripts/lint/lines.mjs` renders pages in Chrome for Testing and rebuilds lines from computed CSS. Each element is first clamped to its nearest clipping ancestor, so the audit reads the geometry the page shows. Construction lines drawn at plus or minus 100vh once produced phantom doubles, and the clamp removed them. | Counted as a line | Threshold | | ------------------------------------ | ------------------------------------------------------------------ | | A border side | 1 to 2.5px wide, at least 24px long, in a visible colour | | An outline ring | 1 to 2.5px wide, at its offset | | A box-shadow used as a border | No offset, blur of 1.5px or less, spread of 1 to 2.5px | | A thin filled box | 2.5px thick or less and longer than 24px | | An exposed-ground strip | 1 to 2.5px of padding over a visible fill on a side with no border | | A positioned `::before` or `::after` | A border or a thin fill, including `translateX(-50%)` centring | | Finding | Rule | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Double | Two parallel lines from different owners 1 to 4px apart, overlapping 75% of the shorter line and at least 80px, both visible at two of three sample points | | Junction | The same pair under 1px apart: two owners draw one seam and the alphas stack | | Invisible seam | A 1 to 2.5px strip within 45 summed RGB steps of the page's ground | | Self-stack | A border with alpha under 0.95 over the element's own fill at 0.12 to 0.95, on an edge of 120px or longer | It skips SVG and canvas subtrees, elements under a `matrix3d` transform or a mask image, hidden elements and anything at opacity 0.05 or less. SVG junctions are read by eye at 2x crops. Three incidents set its exit codes, because each one made a broken page pass. The shell mode adds a fourth exit 2, for a state such as the open search that did not open. | Symptom | Cause | Fix | Found | | --------------------------------------- | ------------------------------------------------------- | --------------------------------------------------- | ---------- | | A run over several pages reported clean | It audited only the first URL given and passed the rest | Every positional URL is audited | 2026-08-02 | | A missing page reported clean | A 404 page audits at about one line | An HTTP status of 400 or more exits 2 | 2026-08-02 | | A list of URLs ran as one malformed URL | zsh does not split an unquoted variable | A URL with whitespace exits 2; pass lists as arrays | 2026-08-02 | After those fixes the first full battery, 25 pages in two themes at two widths, was 100 audits with zero findings. The [gt-lints](https://www.prototemplate.com/skills/gt-lints) skill holds the auditor's reference, and [the gates page](https://www.prototemplate.com/docs/gates) shows where it runs. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # SVG lines in motion > Dash traps with their proofs, GSAP seen from outside the page, timing numbers, first-paint transitions, stretched viewBoxes, symbols and masks. URL: https://www.prototemplate.com/docs/craft/svg-motion The hub's diagrams draw their connectors as SVG strokes and animate them with dash offsets and GSAP tweens. SVG dashes behave in ways a reader of the specification would not predict, and none of the failures raises an error: the line draws from the wrong end, tiles, or leaves a fleck. Each trap below was confirmed by a headless render, a measurement or a rejected review round. The [gt-motion](https://www.prototemplate.com/skills/gt-motion) and [gt-diagrams](https://www.prototemplate.com/skills/gt-diagrams) skills hold the rules, and the components that use them are on [Doubled lines and connectors](https://www.prototemplate.com/docs/components/lines) and [Speed marks and the GT mark](https://www.prototemplate.com/docs/components/marks). ## Five dash traps [#five-dash-traps] ### pathLength under a non-scaling stroke [#pathlength-under-a-non-scaling-stroke] A dash meant as a fraction of a path drew as a short fleck. Chromium computes dash patterns in screen space under `vector-effect: non-scaling-stroke` and ignores `pathLength` there, so `pathLength=100` with `stroke-dasharray: 25 100` drew a fleck 25 screen pixels long where a quarter of the path was intended. A progress ring therefore drops both attributes and writes its dash in user units, because in SVG a CSS pixel is one user unit. The Locadex progress ring's path has a perimeter of 402.832 units, and the dash rides a GSAP-tweened custom property: ```css /* 4.02832px is one percent of the ring's 402.832-unit perimeter */ .v0-ldx-ring { stroke-dasharray: calc(var(--ldx-fill) * 4.02832px) 403px; stroke-dashoffset: calc(var(--ldx-off) * -4.02832px); } ``` Found on the Locadex border ring and the enterprise launch rail in August 2026. ### A travelling dash that tiles [#a-travelling-dash-that-tiles] A single pulse meant to travel along a line showed up as several pulses. The pulse was a dash as long as the path with an animated offset, and under a non-scaling stroke on a scaled SVG the screen-space period repeats along the path. Move a pulse as geometry instead: translate a short `<line>` segment in user units with `transform: translateX(...)`, and keep `non-scaling-stroke` for a constant weight. Dashed pulses on an SVG that scales uniformly can drop `non-scaling-stroke` instead. With it, a normalized `22 200` dash repeated about every 222 screen pixels and stranded accent fragments along the wires. Found in the same August 2026 work. ### Dashes on a closed path [#dashes-on-a-closed-path] An arc parked mid-perimeter on a closed shape left everything past the path's start unpainted. Dashes clip at the end of a closed subpath and never wrap around the loop. Start the path where the arc starts, as the Locadex ring's path does, or use a period equal to the perimeter: `stroke-dasharray: d (L - d)` for a dash `d` on a path of length `L`. ### The sign of the parked offset [#the-sign-of-the-parked-offset] A draw-on ran backwards, from the end of the line toward its owner. The sign of the offset a dash is parked at picks the end it reveals from. | Parked offset, tweened to 0 | Reveals from | Headless Chromium check on a 100-unit path | | --------------------------- | ---------------------- | ------------------------------------------ | | Positive, +100 | The path's first point | Offset 50 inks the first half | | Negative, -100 | The path's last point | Offset -50 inks the last half | The stack tower's rail taps are authored plate first and park at -101, so the bend grows out of the rail into the plate. The write wires run from the chip to the row and park at +101, so they draw out of the chip. Kevin rejected a backwards draw three rounds running in August 2026, and the check above was run on 2026-10-05. When a draw-on looks backwards, read the first point of `d` before touching the timing. ### A fleck at the end of the path [#a-fleck-at-the-end-of-the-path] A sub-pixel accent fleck stayed at the end of a line that should have been empty. With a bare `100` dash the pattern wraps at exactly 100, and Chromium's dash rounding bleeds the next dash's edge onto the path's end; at exactly -100 the fleck measured 2px. `stroke-dasharray='100 200'` parked at 101 or -101 keeps every dash edge clear of both ends, and offset 0 still covers the whole path. Found on the stack tower's taps. ### Rounding, cached paths and empty paths [#rounding-cached-paths-and-empty-paths] | Symptom | Cause | Fix | | ------------------------------------------------------------ | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | A dash froze for about four frames, then jumped a whole unit | GSAP integer-rounds pixel CSS properties | `autoRound: false` on every dash-offset tween, and `pathLength` 1000 on a looping dash so one unit is sub-pixel | | A rerun of an effect traced an empty path | A pulse that rewrites `d` every tick blanked the source | Store the authored `d` once in `el.dataset.traceD` and restore it before measuring | | An animation threw on mount | `getPointAtLength` throws on an empty path | Guard the empty path before sampling it | The line auditor cannot see SVG strokes, so every drawn junction is read by eye at 2x crops in both themes. [Lines and seams](https://www.prototemplate.com/docs/craft/lines) covers the auditor. ## GSAP from outside the page [#gsap-from-outside-the-page] A test harness cannot freeze GSAP from outside the page, so a capture follows the page's own clock. | Symptom | Cause | Fix | | ---------------------------------------------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `window.gsap` is undefined in the page | The ESM build does not expose a global | Read state from the page, such as a computed CSS-variable dial | | Overriding `requestAnimationFrame` does not freeze the animation | GSAP's ticker caches the original `requestAnimationFrame` when it wakes | Poll the dial and aim each capture about 100ms early, so capture latency lands the frame inside the target window | | A harness that seeks a timeline gets frames with stale state | `tl.seek(t)` suppresses callbacks by default | Call `tl.seek(t, false)` so `onUpdate` runs | | A shimmer's sweep landed about 180 units off its mark | A `rotate()` clip window moved with GSAP's SVG transform-origin compensation | Pre-rotated 60 degree window paths swept by a pure horizontal translate (`DitheredMark`) | Clearing what a tween set is part of the same discipline: a standing `will-change` or a leftover 0px transform creates a stacking context that paints over 1px seams. [Lines and seams](https://www.prototemplate.com/docs/craft/lines) has that case. ## Timing numbers [#timing-numbers] The house motion runs on a few fixed numbers. Bounce, elastic and back overshoot are not used. | Use | Value | | ------------------------------------------------------ | -------------------------------------------------------------------------- | | An arrival (fast in, long settle) | `expo.out` or `power3.out` in films; `power2.out` on the web | | A move from one place to another | `power2.inOut` | | A process: a scan, a pulse, a counter, a scrubbed rise | `none` | | Film beat grid | 0.5s; arrivals and scene changes land on beats | | Reading hold after a sentence has fully arrived | (words / 3) + 1s | | Stagger | 40 to 90ms, in reading direction | | Landing reveal | 0.62s with a 0.055s stagger, from 16px down | | Plate row reveal | 180ms `ease-out`, from 4px down | | Shell chrome | 120 to 220ms, 0 under reduced motion | | Camera drift | Scale 1.00 to 1.04 over a scene, on a picture or a field and never on type | A language morph must last longer than 0.5s so the glyphs visibly regroup, and it must finish inside the time its locale holds the screen. Kevin's review of 2026-08-05 found the morphs "still way too fast", under 0.5s, and later that day he called a slower morph too long. The reassembler keeps each beat longer than the print, about 3.2s. In an assembly, one group moves at a time with rests of 0.25s or more between groups. A film's triangle reassembly sent twenty slivers flying with 3D tilts while the camera moved, and four rounds of path planning still read as scatter. The cut that passed moves one pair in the plane, rests 0.25s and prints the rest in place. When path planning cannot make a move calm, change the idea. ## Transitions at first paint [#transitions-at-first-paint] A CSS `transition` alone never runs on the first frame of these sites. next-themes' `disableAnimation` sheet is in the head when the first frame lands, so the transition has nothing to animate from. A fade at first paint is a keyframe animation: the field region's 240ms `ease-out` opacity keyframe runs once, when the first tone is drawn. A control that mounts on the client also reserves its height in the server markup, or it shifts the layout under it; [Interface details](https://www.prototemplate.com/docs/craft/interface-details) has the measured jumps. ## Moving type [#moving-type] The moving type law in [the design canon](https://www.prototemplate.com/docs/handbook/design) section 8 says one shaped text node morphs and only its width animates. These are its working numbers. | Part | Value | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Width measurement | A hidden probe with the word's own `lang` and `dir`, the whole roster in one batched pass, cached, snapped to device pixels, measured again on a debounced resize and on `document.fonts.ready` | | Width tween | One per cycle, 0.7s `power2.inOut` | | Locale requests | Debounced 0.25s, leading and trailing | | A locale with the same text | Retags `lang` and `dir` only | | Under 720px | The em pins to the column, so a sentence that folds to two lines never tweens layout | | Glyph field words | Hold 4.0s and morph 2.4s | | Mobile hero h1 | Reserves two lines with `min-height: 2.2em`, sized `clamp(2.1rem, 10.9vw, 2.7rem)` | The width tween is off under 720px because a review found it slow and laggy on phones. The hero's size comes from the longest of its sixteen sentences: at 390px the French sentence tips to a third line at 2.75rem, so the clamp stays just under it. The glyph field conserves matter. The outgoing word's dust becomes the next word, only the deficit is recruited from rain visible on screen, and surplus dust flies home to its rain slot. On `document.fonts.ready` the field measures again and never resamples, because a resample deals the particles out again and glyphs jump across the screen. ## Drawing under a stretched viewBox [#drawing-under-a-stretched-viewbox] A stretched SVG holds straight lines and no text. `preserveAspectRatio="none"` lets a connector strip fill a fluid column, and under it straight segments stay straight while curves warp, because their control points stretch with the box. Circles become ellipses and text distorts. Strokes hold their width only with `vector-effect: non-scaling-stroke`. | Rule | Number | | ------------------------------------------------ | ---------------------------------------------------------------------------------------- | | Size text after the scale | An 800-unit viewBox that renders at 0.914 sets its labels at 22px so they land at 20.1px | | Put a 1px line at 1:1 on a half-pixel coordinate | `y1="70.5"` fills one row of device pixels | | Run a leader past the vertex it meets | 2 units, buried under the opaque hull drawn after it, so no anti-aliased seam shows | | Mark a step or a value | An 11px filled ink square or a short perpendicular tick; no arrowheads | A stretched strip uses straight taps with one elbow, and its labels live in HTML beside it. The sign-in workflow diagram replaced its curved connectors with straight taps for this reason. Under 620px the translation flow figure hides its fork and stacks its panels, because a figure that cannot hold its shape on a phone reflows. `skills/gt-diagrams/scripts/figure-check.mjs` prints every label's rendered size and fails a stroke in a stretched viewBox without `non-scaling-stroke`. ## Symbols and masks [#symbols-and-masks] SVG references resolve in coordinate systems that are easy to get wrong, and the result is a mark that renders off its box with no error. | Symptom | Cause | Fix | | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | The GT monogram rendered 214 units too high, with its top cut off | A `<use href="#gt-mark">` sat inside an svg that copied the symbol's own viewBox, `-8 214 1213 771` | Reference the symbol from an svg with a plain width and height in the mark's aspect (about 1.573, such as 132 by 84) and no viewBox, or render the path directly | | A mask landed away from the shape it masks | With `maskUnits="userSpaceOnUse"` the mask resolves in the user space of the element that uses it | Write mask coordinates in the same plan coordinates as the masked rect | | A second copy of a figure drew the first copy's mask or pattern | SVG ids are global to the document | Give every mask, pattern and clip id a unique prefix (`DitheredMark` takes an `id` prop for this) | Brand marks render as alpha masks so the shape takes the surface's ink, and the mask reads alpha only, so the light and dark variants of a mark image give the same result. The [gt-diagrams](https://www.prototemplate.com/skills/gt-diagrams) skill covers seating a mask on an isometric face. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Dither in practice > Why ordered dither, why cells are screen pixels, transitions on one grid, material strength, and the story and placement of dithered pictures. URL: https://www.prototemplate.com/docs/craft/dither General Translation draws its signature texture as 1-bit cells: every cell is ink or ground, and an ordered Bayer screen decides which. The texture appears as animated fields behind product pages, as a dithered globe and dithered photographs on the sign-in and onboarding plates, and in the brand deck and the films. This page gives the reasons behind the screen, the numbers that keep cells crisp, the rules for changing one picture into another, and the standard every dithered photograph is cut to. The engines and their props are on [Dither and the studio field](https://www.prototemplate.com/docs/components/dither), which also has what a field costs per frame and why a dither pattern on a moving sprite flickers. The [gt-dither](https://www.prototemplate.com/skills/gt-dither) skill holds the rules. ## Ordered dither [#ordered-dither] Use an ordered screen for any texture that moves or has to read as print. Error diffusion (Floyd-Steinberg) is serial, because each pixel's rounding error is added to the pixels after it, so its output reads as noise and shimmers when it animates. A Bayer matrix is a fixed threshold map: each cell is decided alone, the same tone always lights the same cells, and the repeating structure reads as a print halftone. | Screen | Construction | Levels | Where it runs | | ------------------ | --------------------------------------------------------------------------------- | ------ | --------------------------------------------------------------------------- | | 8 by 8 (`BAYER_8`) | M(2n) = \[\[4M, 4M+2], \[4M+3, 4M+1]], an exact permutation of 0 to 63 | 65 | The CPU dither loop, the picture cutter, the deck, studio presets 02 and 10 | | 8 by 8 on the GPU | Three octaves of the 2 by 2: `bayer2(p / 4) / 16 + bayer2(p / 2) / 4 + bayer2(p)` | 65 | The studio field's `bayer8` | | 4 by 4 | Rows `0 8 2 10`, `12 4 14 6`, `3 11 1 9`, `15 7 13 5` | 17 | Most studio presets, the dithered shine on marks | A constant tone k/64 lights exactly k cells in every 8 by 8 tile, so the screen has 65 tonally linear levels. A cell is lit when its tone exceeds `(m + 0.5) / 64` for its threshold `m`. The tiers nest: every cell lit at one tone stays lit at every higher tone, so neighbouring regions filled with different tiers compose an exact ramp on one grid. ## Cells are screen pixels [#cells-are-screen-pixels] A dither cell must land on whole screen pixels, or the screen collapses into flat grey. The CPU engine therefore never renders at `devicePixelRatio`: on a retina screen a cell would be subpixel. It writes one buffer pixel per cell and lets CSS upscale the canvas with `image-rendering: pixelated`. The GPU studio field computes cells from device pixels (`gl_FragCoord`), so its `dpr` defaults to `min(devicePixelRatio, 2)`, and a dpr of 1 upscaled softly blurs the cells together. | Cell width | How it reads | Where it is used | | ---------- | ------------------------------ | ------------------------------------------------------------- | | 1 CSS px | Fidelity | Dithered photographs on the web | | 2 CSS px | Fine texture | The sign-in globe, the deck's ramp | | 3 CSS px | Halftone | The default for a full-bleed field; every film | | 4 CSS px | Halftone | An animated radial burst, which costs too much per frame at 3 | | 6 CSS px | Halftone that reads when small | Small still images such as the partnership globes | Eight times the cell width is the grain the eye reads, and at 8px or more a screen reads as pixel art. Every film uses 3px cells, set on 2026-10-08 when Kevin found the films' coarser screens "a lil TOO dithered". Blog graphics met the same rule in a different form. At 1x the dither's one-pixel dots landed on less than a device pixel and aliased into noise, which first looked like a resolution problem. Drawn at 2x the stage height with `image-rendering: pixelated`, a 1920 export shows as crisp 2px dots. SVG dithers take `shape-rendering: crispEdges`, and a transform that would foreshorten cells, such as an isometric plane, goes inside the alpha mask so the cells stay square. ## Transitions on one grid [#transitions-on-one-grid] A dithered field changes from one state to another by five rules, which the build log on [/docs/handbook/libraries](https://www.prototemplate.com/docs/handbook/libraries) states as `TRANSITION_RULES`. 1. One cell grid: both states are read at the same cells and one cell size, and the size never changes inside a transition. 2. One anchored tile: the Bayer tile keeps its phase from one page cell, so a transition's first frame is the last frame of the state before it. 3. One smoothstep: the tone mixes on `k * k * (3 - 2 * k)`, and the ink is interpolated on the same curve. 4. Brightness held across a resolve: the incoming picture's curve is solved so its mean tone equals the outgoing field's. 5. The end state under reduced motion, drawn once as a still. A resolve from the globe into a picture takes 350ms and a step from picture to picture 150ms. Both were halved from 700 and 300ms on Kevin's request of 2026-09-29: "make the dither transitions 2x faster". | Symptom | Cause | Fix | | ------------------------------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | | A flash at the start of a transition | An unanchored tile re-dithers the same tone into other cells | A canvas `n` cells below another takes `phase.y = n % 8`, so both share the page's tile | | Quick steps flash back to a picture that has gone | A new mix started from the previous target | Each mix frame is a fresh closure, and a new target takes `from: current`, the frame on screen | | A step through a new level became a cut | A CSS mask image that is still loading counts as transparent | Decode all 65 mask levels with `decode()` before the first step and keep the images referenced | | A picture's edge fades in grey cells | A fading CSS mask prints the cells at partial alpha | The picture layer takes a hard cut at the plate's edge and dissolves by multiplying its tone in the field | | A field arrives as a fade | An opacity change on the layer | A field enters by raising its tone from 0 and leaves by lowering it to 0 | | A film's mix eases twice | The film kit's `mix` applies the smoothstep itself | Drive its progress with ease `none` | The [gt-motion](https://www.prototemplate.com/skills/gt-motion) skill holds these with the rest of the motion rules. ## Material strength [#material-strength] A product surface carries one dithered material at the strength of the GT landing hero or under it. Kevin set that bar on 2026-09-25: the fields "should be as tasteful as our dither shader in hero of landing". The hero's composite is measured, so new work copies its numbers. | Part | Value | | ----------------- | -------------------------------------------------------------------------------------------------------------- | | Shader | Studio preset 02, `bayer-8x8` | | Canvas | Opacity 0.55, `mix-blend-mode: screen`, inside an isolated dark cell | | Mask | Seven horizontal stops, from 0.72 at the edges to 0.12 behind the terminal window in the centre | | Light theme | `filter: invert(1) hue-rotate(180deg) brightness(1.07) saturate(1.15)`, so the clouds print pale blue on paper | | Pictures in dark | White cells at opacity 0.62 over `#070707` | | Pictures in light | `#070707` cells at opacity 0.7 over white | The light theme uses a filter because a screen blend is inert in an isolated host with a transparent backdrop. A horizon ring drawn at page scale was judged too heavy and deleted. The material never becomes a pattern, a border, a boxed plate or a heavier form. Glyph rain stays solid type with no dither, because a dither pattern on moving glyphs flickers with every step. ## The picture standard [#the-picture-standard] Every dithered photograph is cut to the statistics of one reference, NASA's Blue Marble on the onboarding field, and only four things are chosen per picture: the crop, the channel, the polarity and the kind. [The artifact pictures document](https://www.prototemplate.com/docs/handbook/artifact-pictures) states the standard in full. | Setting | Value | Reason | | --------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | Target for a scene | Mean tone 0.361, standard deviation 0.342 | The Blue Marble's own statistics | | Accepted window for a scene | Mean 0.30 to 0.42, standard deviation 0.28 to 0.44, measured over the region shown | The picture lint holds every grid to it | | Black and white points | Solved by the cutter, never set by hand; levels 80 and 230 for the earth | Every picture lands on the same tone | | Gamma | 1.2 | Fixed for every picture | | Autocontrast | 0.5% clipped at each tail | Fixed for every picture | | Blur | None | A blur with a sigma of 3 cost the Rosetta Stone its inscriptions, as the next paragraph tells | | File | 8-bit grey JPEG at quality 82, 75 when 82 is over the 573,440-byte cap | The cap holds every grid under 560KB | | Tone floor | Bytes at or under 10 read as 0 | The tile's lowest threshold, 1/128, would print the JPEG's ringing on black as stray cells | | Cell | 1 CSS px, screened live at display size | A pre-screened bitmap that is then scaled loses its cells | The standard came from a failure. An earlier cutter blurred the deck's two-tone files back to tone with a sigma of 3, then baked in gamma 1.3 and 1.6 and a gain. The Rosetta Stone printed 5.7% of its cells lit against the deck's 8.9%, and its inscriptions disappeared. The next round printed the deck's own cells, and those 2px two-tone cells read as blocky on manuscripts. On 2026-09-29 Kevin asked to "make the dither a lot less strong so the images are more high fidelity". The pictures moved to continuous-tone grids cut from the original photographs and screened at 1px cells, and the solver replaced hand-set levels. Sources are pinned. Each source's sha256 is recorded, the originals stay outside the repository, and the cutter (`pnpm mood-tone`) refuses a file whose hash differs. The Metropolitan Museum of Art later served the tablet picture with different bytes, and the cutter refused them. `node scripts/lint/pictures.mjs` runs before every build and fails a grid whose hash, recipe settings or region statistics are off the standard. ## Placing a picture on a field [#placing-a-picture-on-a-field] Anchor a picture to a share of the field's ramp. On the onboarding plate the field fades in from the plate's edge across a ramp, and the earth is drawn as a disc 1.7 times the stack's height with its left limb five sevenths of the way along the ramp. The first placement put the limb on the ramp's start, so the left third of the disc sat under the thinnest part of the ramp, and a 2000 by 1163 window read as empty with a sliver at the right. Two sampling rules keep the picture on the loop's own cells. | Rule | Reason | | ---------------------------------------------------------------------- | ------------------------------------------------------------------ | | Snap every read to the loop's cell centres (`atCellCentres`) | The loop's `u = x / (cols - 1)` drifts by a cell across the canvas | | A loop cell that covers more than one grid cell reads the area average | A grid drawn smaller re-screens at the loop's cell without moire | The picture sampler is `src/components/plate/lib/picture-field.ts`, and its placements (`cover`, `disc` and `region`) are stated in the 1600 by 900 file space. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Writing > The house register with before and after rewrites, writing for a reader who knows nothing about the work, and the rules for slides and cards. URL: https://www.prototemplate.com/docs/craft/writing General Translation copy reads like a technical specification: complete declarative sentences, concrete nouns, and a number or a mechanism where marketing would put an adjective. The site, these docs, the brand deck, the films, UI strings and pull request text all use this register. The rules below come from Kevin's reviews of GT writing between August and October 2026. The [gt-voice](https://www.prototemplate.com/skills/gt-voice) skill holds them with their dates, and every page in this tab was written to them. ## The hard rules [#the-hard-rules] Each rule removes a pattern that reads as machine-written or as marketing. | Rule | Breaks it | Meets it | | -------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------- | | No em dashes; an en dash or a double hyphen counts too, and ranges read "X to Y" | Two slide numbers joined by an en dash | "slides 17 to 23" | | No metaphors or analogies | "Localization is a growth lever." | "Numbers, currency, and dates localize too." | | No contrast pairs such as "X, not Y" or "not just X but Y" | "It's not a library, it's a platform." | State the positive claim, with an example when the contrast carries information | | No fragment rhythm | "Open-source libraries and a closed platform, built together." | "GT builds both the open-source libraries and the closed platform." | | No signposts or content labels | "Three facts matter." followed by the facts | The three facts, each its own sentence | | No eyebrows: a small uppercase tracked line above a heading | "REACH EVERY USER" above a heading | The heading and one lead paragraph, nothing above | | No marketing vocabulary | "Supercharge your global growth with cutting-edge AI." | "Translations are generated at build time and deploy with the app." | | No exclamation marks, rhetorical questions or emojis | "Ready to go global?" | A statement of what the product does | The one sanctioned comparison is the company's own positioning, stated plainly: the Vercel model applied to localization. Headings use sentence case, end without a period and name a thing; the claim moves into the first body sentence. Title Case appears only on buttons. ## Sentence shape [#sentence-shape] Every sentence has a subject and a verb, captions included, and a few ordering rules keep it readable. | Rule | Before | After | | ---------------------------------------------------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | | A data label carries no comma tail | "16px, favicon" | "16px favicon" | | A subject longer than about six words is reordered: short frame first, long content last | "The detail that X is one most people would miss." | "The detail most people would miss is that X." | | No comma-bounded aside between subject and verb or before a colon | "The rail, which the wrapper draws once, is a border." | "The wrapper draws the rail once. The rail is a border." | | A title is a plain noun | "The identity has to work at small sizes" | "Small sizes" | | A metaphor becomes the mechanism | "The ground is the seam." | "A framed cell has a 1px padding gap that shows the surface underneath, so the cell draws no border." | Kevin's review of 2026-09-08 named the comma-tail heading and strings of short fragments as patterns that read as AI to him. The rules on long subjects and asides followed on 2026-09-20. ## Code comments [#code-comments] A code comment states the code's contract or a constraint the code cannot show, in one or two lines of present tense. The justification for a feature belongs in the pull request. | Before | After | | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `// Identity values pass through untranslated, since translating a model_uid would corrupt data, which is worse than the bill this feature saves.` | `// 'identity': every value of the field is byte-identical to its translation. 'translated': at least one value differs.` | Kevin reads comment lines as part of a pull request's size. On 2026-10-02 he sent back a pull request whose product comments were 20% of its added lines. ## The humanizer pass [#the-humanizer-pass] Every draft gets one pass against the pattern catalog before anyone reviews it. The pass reads the draft once, marks every pattern and broken rule, rewrites, then scores five axes from 1 to 10: directness, rhythm, trust, authenticity and density. A total under 35 of 50 means another revision. | Pattern group | Tells | | -------------------- | ------------------------------------------------------------------------------------------ | | Inflation | stands as, is a testament to, pivotal, crucial, underscores | | Superficial analysis | a trailing "-ing" clause: highlighting, showcasing, reflecting | | AI vocabulary | delve, tapestry, landscape (abstract), interplay, intricate, enhance, foster, additionally | | Copula avoidance | serves as, boasts, features, where "is" or "has" would do | | Forced structure | the rule of three, synonym cycling, false ranges such as "from startups to enterprises" | | Formatting | mechanical bold, a bold label at the start of every bullet, Title Case headings | The lint `gt-ui/no-em-dash` reads JavaScript and TypeScript strings only. Markdown, MDX, the deck's HTML and pull request bodies are checked by hand: ```sh grep -rnE $'\xe2\x80\x94|\xe2\x80\x93' content deck/slides docs DESIGN.md BRAND.md ``` ## Writing for a stranger [#writing-for-a-stranger] Public writing is for a developer who has never heard of GT or of the people who built the page. Kevin wanted it "so much more straightforward and clear to a user who has no idea who I am" (2026-09-10). * Define the product and the site once, in plain words. * Open each section with the point a stranger can reuse, and give the implementation and the numbers after it as the evidence. * Cut pull request numbers, colleagues' names, process narration and file names that teach nothing. * Keep an API or a component name when the reader can act on it, such as Next.js `notFound()` or the `Accept: text/markdown` header. * Keep every fact verified: an edit may reword, cut or add context, and it never changes a fact. Every line should state a number or a mechanism. A line that could appear unchanged in a competitor's copy is cut. The [quality bar](https://www.prototemplate.com/docs/handbook/quality-bar) lists this test in its copy rows. ## Slides and cards [#slides-and-cards] A slide or a card carries one statement, and the headline leads. On 2026-10-13 Kevin wrote "i hate eyebrow text" about a set of generated slides, and the rules below follow from that review. | Rule | Number | | --------------------------------------------------- | ------------------------------------------------ | | Title size on a 16:9 slide | About 6 to 8% of the slide's height per line | | Tracking on large display type | -0.01 to -0.025em | | Tracking on body text | 0 | | Tracking on lowercase | Never positive | | Kerning | On, with real glyph pairs checked at export size | | Smallest text on the brand deck's 1600 by 900 sheet | 15px, and 18px for SVG labels | Metadata such as a product name, a date or a number goes after the content or in a footer, never stacked above a headline. A film's mid-film card stacked the GT mark and a product label over a 140px question and an 80px answer. Four mock-ups were compared, and the answer alone, with the mark small in a corner, was chosen. The [gt-deck](https://www.prototemplate.com/skills/gt-deck) skill holds the deck's own type sizes. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Interface details > Glyph gaps, copy controls, tap slabs, layout-shift cases with measured jumps, third-party themes, contrast numbers, carousels and hit areas. URL: https://www.prototemplate.com/docs/craft/interface-details These are the small behaviours that decide whether an interface feels finished: gaps that look equal, controls that never move, hit areas that fit a thumb, and widgets that follow the site's theme. Each one came from a review of GT's landing, its dashboard's sign-in and onboarding plates, or this hub, and most carry the measurement that settled them. The [gt-components](https://www.prototemplate.com/skills/gt-components) skill holds the behaviour rules, and [gt-aesthetic](https://www.prototemplate.com/skills/gt-aesthetic) holds the measures. ## Gaps beside a glyph [#gaps-beside-a-glyph] Match the gap the eye sees between a label and its icon. Lucide glyphs sit differently inside their 24-unit boxes: ArrowRight draws from unit 5 and Copy from unit 2, so two buttons with the same CSS gap show different visible gaps. Measure from the label's right edge to the glyph's first drawn column, and from the glyph's last drawn column to the button's edge, then match those numbers across the buttons in a row. A report of the fix states the measured value, such as "both buttons read 9.5px from label to glyph". Kevin set this rule on 2026-10-01. A button whose label changes with its state, such as a hero button that reads Copied or Copy Failed after a click, sizes its face from the idle label through a grid sizer. The face keeps its width, so a centred row of buttons never moves. The [gt-landing-pages](https://www.prototemplate.com/skills/gt-landing-pages) skill holds both rules. ## The copy control [#the-copy-control] A copy control shows its state in the icon and leaves the label alone. | Part | Rule | | ------------------------------ | --------------------------------------------------------------------------------------- | | Label | Never changes to "Copied" | | Icon | The copy icon becomes a check, and the check turns blue | | Layout | The copy and check icons share one box, so nothing moves | | Click | Copies without selecting text; selection is the fallback when the clipboard write fails | | Copied state | Holds about 1.6s and is announced to screen readers | | Strict Content Security Policy | The one copy script is allowed by its sha256 hash | Test a copy button in Chrome. The in-app browser pane of a desktop agent app blocks clipboard writes, so a copy that fails there proves nothing. In the hub the one copy control is `InstallField` (`src/components/viewer/InstallField.tsx`). ## Tap targets that keep the look [#tap-targets-that-keep-the-look] Grow the hit area and leave the paint where it is. The page check fails a phone target under 40px, notes 40 to 43px as a warning and aims at 44px. | Case | Drawing | Hit area | How | | ------------------------------------------------------ | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | A marker on the GT landing's translation window figure | An 11px badge | 25px | An invisible button paints its badge with `::before` inset 7px, so hover and pin rules target the pseudo-element and the box-centre math still works | | The hub toolbar's icon buttons on a touch phone | 32px | 44px | A transparent `::after` reaches into the gap beside the button | ## Layout shifts [#layout-shifts] A control that appears, changes size or remounts after the first paint moves everything under it. Each case below was measured. | Symptom | Cause | Fix | Found | | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | --------------------- | | The button and the foot of the device page jumped 30px | The one-time code slots mounted at hydration in a row the server drew 18px tall | The container reserves `min-h-12` in the server markup | 2026-09-30 | | The foot of the payment step jumped 340px | A 27rem spinner box collapsed to one error row | One 44px row is held while loading or failed, and the tall box wraps only a mounted form | The onboarding plates | | The theme and sign-out controls moved when the language changed | A locale switch changes the `[locale]` segment and remounts the page, and a variable-width label sat left of fixed controls | The language anchors left and the theme toggle right, with the copyright or sign-out between them | 2026-09-29 | | A card grew when its description expanded | The card took its content's height | Cards keep fixed heights | 2026-08-13 | Two rules come out of these cases. A highlight turns blue and does not expand or scale, so nothing shifts. An auto height animates with a CSS grid row from `0fr` to `1fr`, which needs no ResizeObserver and no component state. A caption card that measured its own height drew Kevin's reply of 2026-10-06, "Beautiful but don't complicate. Always simplify." Layout shifts within 500ms of an input carry `hadRecentInput` and stay out of the CLS score, but a reader still sees them. Measure the entries around one interaction with a `PerformanceObserver`, and treat every entry over 0.001 as a defect. The [gt-verify](https://www.prototemplate.com/skills/gt-verify) skill has the recipe, and [Proving a visual change](https://www.prototemplate.com/docs/engineering/verification) covers the rest of the checks. ## Third-party widgets and the site's theme [#third-party-widgets-and-the-sites-theme] A third-party widget follows the site's theme toggle. Cloudflare Turnstile's `theme: 'auto'` reads `prefers-color-scheme`, so on the sign-in form it drew a white card on a dark page whenever the visitor's system theme and the site's theme disagreed. The fix passes next-themes' `resolvedTheme` as the widget's theme, defaults to light before mount, and keys the widget on the theme so it remounts when the theme loads or flips. This is the one place the resolved theme is read in JavaScript, because the widget takes no CSS. Found on 2026-08-14. | Widget or field | Rule | | -------------------------------- | ----------------------------------------------------- | | Stripe Elements | Takes the house field look through its appearance API | | Inputs below the `md` breakpoint | 16px or larger, so iOS does not zoom on focus | ## Contrast [#contrast] These ratios were computed with the WCAG formula on 2026-10-05. Text under 24px needs 4.5:1. | Text | Ground | Ratio | | ----------------------------------- | ------------- | --------------------- | | Ink `#070707` | White | 20.14 | | Ink-2 `#3a3d44` | White | 10.88 | | Titanium `#8a8f98` | White | 3.25, under the floor | | A darker light titanium `#6e737c` | White | 4.77 | | Titanium `#8a8f98` | Ink `#070707` | 6.20 | | GT blue `#2f5ce0` | White | 5.63 | | GT blue's dark-theme lift `#86a8ff` | Ink `#070707` | 8.69 | Titanium fails the floor for small text in the light theme, so the dashboard's plate pages set their 13px captions in `#6e737c`, and other light surfaces set text a reader needs in ink-2. The GT landing holds three more floors. | Floor | Value | | ------------------------------------------------------------- | --------------------------- | | Dim text on the ink panel | At least 0.5 white | | Dim text where the light skin turns the panel white | At least 0.58 black | | The quietest tone real text may take in the language diagrams | A 62% mix of `currentColor` | The tower figure's inactive beats at opacity 0.38 fail AA on purpose. They are dimmed so the current step stands out, and raising them is a design decision. ## Carousels and galleries [#carousels-and-galleries] A carousel accepts one command at a time and stops moving when a reader is busy with it. | Behaviour | Number | | ----------------------------------------- | ------------------------------------------------------------------------------ | | Commands within 350ms of the previous one | Dropped, so a double click moves one slide | | Autoplay | Every 4.5s; pauses on hover, on focus and off screen; off under reduced motion | | Click | The left half goes back and the right half goes forward | Hover and click are one interaction: hover reveals, a click pins, leaving hides and Escape closes. A tooltip with a separate card is two interactions for one thing. Dropdowns are built as custom components with real icons or flags, and a native select is not used. ## A still hit area for a moving target [#a-still-hit-area-for-a-moving-target] An element that moves on hover slides out from under the pointer, loses the hover, moves back and flickers. Give each slot a still hit area and draw the moving part inside it, so the pointer stays over the same box while the drawing moves. Found on 2026-10-20 on a shelf of book spines that lifted on hover. ## Shadows and blur from a component library [#shadows-and-blur-from-a-component-library] A component library brings its own shadows and blurs, and a scoped rule misses the ones it portals out of the page. The house look is flat: the brand takes depth from lines and material and never from shadows, and its avoid list includes glassmorphism. These docs remove decorative shadows with a rule scoped to `#nd-page`, the article. On 2026-10-21 three surfaces outside that scope still drew them. | Surface | Classes it carried | Why the page rule missed it | | --------------------------- | ---------------------------- | ------------------------------------------ | | The search dialog | `shadow-2xl shadow-black/50` | fumadocs portals the dialog into `body` | | The search dialog's overlay | `backdrop-blur-xs` | The same portal | | The phone drawer | `shadow-lg` | It sits in the layout, outside the article | A flat rule for a fumadocs site needs selectors for the dialog, its overlay and the drawer themselves. The [docs site traps](https://www.prototemplate.com/docs/engineering/docs-traps) page covers the other fumadocs traps. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Deterministic films and sound > Rendering films that match frame for frame: framemd5 pairs, the image race, fonts, chroma on odd pixels, thin lines over dither, the mix and speech. URL: https://www.prototemplate.com/docs/craft/films General Translation makes its blog films, announcement films and translation-history films as HyperFrames compositions: HTML pages with one paused GSAP timeline that the HyperFrames command-line tool seeks and renders frame by frame to MP4. The published cuts are on [/motion](https://www.prototemplate.com/motion). A render can come out different from its twin, lose a layer, fetch the wrong font or grey a coloured line, and the renderer reports success in every case. The numbers below were measured on HyperFrames 0.8.106 while the films were built in October 2026. The [gt-films](https://www.prototemplate.com/skills/gt-films) skill holds the full rules. ## Compare every render pair [#compare-every-render-pair] Render every delivery twice and compare the pair frame by frame. HyperFrames renders are not guaranteed identical when the machine is under load, on either rendering path. | Path and load | What differed | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Hardware GPU, load average near 240 | 240 of 5,738 frames, by a mean of 0.002 to 0.037 levels and at most 34, starting on a large character whose font size changed every frame; the audio was identical | | `--no-browser-gpu` on the same film | Nothing: the pair was byte-identical, and it rendered faster | | `--no-browser-gpu`, load average near 120 | 116 of 5,989 frames on a moving globe overlay, PSNR at least 52.7dB, largest difference 45 of 255 | | The same build rendered in two sittings | 867 frames | Compare each pair by `framemd5` and by decoded audio. When they differ, keep both, take the one that matches the previous master frame for frame where the round changed nothing, and record the PSNR and the frame ranges. Compare builds rendered in one sitting. Films with large animated type render on the software path. Films with the gem smoke shader stay on the GPU path, because the software path moved the smoke by up to 7 levels. Two Chrome behaviours make renders drift, and the film kit works around both. Whether Chrome puts a large 2D canvas on the GPU depends on its memory budget at that moment, so every 2D canvas is created with `willReadFrequently: true`, which keeps it in software. Inside a moving 3D layer Chrome re-rasters an `<img>` or an SVG at a scale that depends on what the worker drew before, so scans and marks are painted into canvas textures in the timeline's `onUpdate`. ## Images and the first timeline update [#images-and-the-first-timeline-update] Never cache a read from an image until it is complete. HyperFrames 0.8.106 opens a composition at `domcontentloaded`, runs one timeline update so `onUpdate` fires, and only then waits for images. A picture still downloading already reports `naturalWidth`, but `drawImage` draws nothing from it until it is complete. A film's dither kit cached its picture read on that first update and held black for the whole render. One of two renders lost its dither field in 179 of 180 frames, PSNR 14dB against its pair, and the render reported success. The fix guards every cached picture read with `img.complete && img.naturalWidth > 0`. A pair in which one render lost a whole layer is this race, and GPU noise never removes a layer. Found on 2026-10-11. ## Fonts in a composition [#fonts-in-a-composition] Type in a composition goes through the kit's `var(--font)`. Any literal `font-family: 'Inter'` in a composition's CSS makes the HyperFrames compiler fetch that family from Google Fonts, and the log reads "Fetched 11 font face(s)". The fetched static Inter overrides the self-hosted variable face, comes out about 5% wider, and clips the films' line masks. | Rule | Reason | | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | Grep every render log for "Google Fonts" | The log line is where the swap is reported, and the kit refuses a master whose log has it | | Set fallback families for CJK, Arabic and Devanagari glyph text from JavaScript | The compiler reads family names from `<style>` text only | | Build a `FontFace` with `featureSettings` for canvas text | A canvas ignores `font-feature-settings`, and a heading with two single-storey a's came out 12px short | | Seat headings from Inter's metrics: the baseline 0.8638em below a line-height-1 box's top, the cap height 0.7275em | A clip's elements may not be laid out while the clip is outside its window | [The correct Inter in practice](https://www.prototemplate.com/docs/craft/type) covers the same face on the web. ## Thin coloured strokes in H.264 [#thin-coloured-strokes-in-h264] Put a thin coloured stroke on even frame pixels. H.264 at 4:2:0 stores colour for 2 by 2 pixel blocks, so a 2px line that starts on an odd pixel half-covers two blocks, and its colour averages with the ground. A 2px `#2f5ce0` box stroke read (47, 92, 224) in the composition snapshot and slate grey, from (65, 91, 113) to (84, 101, 155), in the encoded draft wherever it started on an odd pixel. Sample stroke colours in the encoded file. Text and hairline grey strokes are barely affected. Found on 2026-10-12. ## Thin lines over a dithered ground [#thin-lines-over-a-dithered-ground] Check every thin line at 640 by 360, the size a film plays at on a phone. On a lit dithered globe, 1px routes disappeared at that size. Routes drawn at 2.5px with 2px crosses over a 1px keyline in the film's olive at 60% on each side read. Every GT film uses the 8 by 8 Bayer screen at 3px cells. | Rule | Number | | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | Distances on a cell grid run centre to centre | A 40px clearance on screen needs 44px between cell centres, because a cell's nearest pixel can sit 3px nearer than its centre | | Lower the tone in a clearance ramp and never scale it | Scaling and subtracting together compressed a taper to about 20px with a hard edge; lowering alone keeps the taper as long as the ramp | | A soft knee at the bottom of the tone | Nothing under 0.014, then a smoothstep to 0.07, so thin wisps leave no lone cells | [Dither in practice](https://www.prototemplate.com/docs/craft/dither) has the screen itself. ## The mix and its traps [#the-mix-and-its-traps] A film's sound meets fixed loudness targets, and the renderer has several behaviours that move a mix away from them without an error. | Target | Value | | -------------------------- | -------------------------------------------- | | The film | -16 LUFS within 0.5 | | True peak | At or below -1 dBTP | | The music bed under speech | About -26 LUFS | | The bed alone | About -20 LUFS, a 6dB duck in loudness terms | | Fades | 0.3 to 0.8s | The final check is `ffmpeg -i out.mp4 -af ebur128=peak=true -f null -`. | Symptom | Cause | Fix | | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | A louder mix came out quieter | HyperFrames measures the mixed AAC and, when its true peak passes -1 dBTP, lowers the whole track to -1.5 dBTP | Compare an audio-only render with the sum of its stems; a uniform gap means the correction fired | | Peaks pass the renderer's limiter | The limiter has no lookahead, so waveform peaks pass by their crest factor | Put peak control in the ffmpeg masters | | A bump at the start of a clip | A clip at full level from its first sample with any gain gets a 5ms low-frequency bump of up to about 9dB at about 0.14 and 0.37s | Start every file with 0.1s of silence | | The encoder overshoots on downbeats | Sub-bass downbeats made the AAC encoder overshoot by up to 2.6dB | A -9dB low shelf at 100Hz | | Clicks on a gain ramp | ffmpeg's `volume` with `eval=frame` updates once per decoded frame (1,152 samples for MP3), so a ramp becomes a staircase | Use `aeval` for any gain curve | | About 56ms missing from the head | `adelay` then `apad` in one filter graph | Pad in a separate pass | | One channel's drone notched by 21dB at a join | The join was chosen on the mono fold-down while the two channels were near anti-phase | Choose joins on each channel and on the fold-down | | The bed pumps between lines | A duck that releases in gaps under about 1.2s | Hold the duck across short gaps | | A take ends on the narrator's next breath | The take ran past the line | End a take no later than 0.4s past its last word | ## Speech synthesis [#speech-synthesis] These facts were measured on 2026-10-06 and matter when a film is voiced again in another language or a script is written to a time budget. | Fact | Measurement | | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Never generate below speed 1.0 | Every measurement found slowing a voice raised its pitch wobble; slowed takes read 1.6 to 1.7% median frame-to-frame wobble | | ElevenLabs `eleven_v4` ignores `voice_settings.speed` | One English sentence measured 2.80s unset, at 0.8 and at 1.2; a German one 3.52s at all three | | `eleven_flash_v2_5` and `eleven_multilingual_v2` follow it | Within 7% of the default span divided by the speed, from 0.7 to 1.2 | | Inline pace tags on `eleven_v4` | `[fast-paced]` shortened a line by about x0.83 and `[slowly]` lengthened it by about x1.15; one pass brought 14 of 18 lines within 10% of their slots | | Natural-language pace instructions on Gemini TTS | "15 percent faster" gave x0.72 to x0.78 of the default span, "15 percent slower" x1.60 to x1.91 | | A seed on `gemini-3.8-flash-tts` | Does not reproduce audio; no take of 16 lines was byte-identical across three requests | | A prompt's length floor | Asked for translations no shorter than 0.85 of a target length, a language model still wrote 26.5% (German) and 22.0% (Japanese) of its candidates below it, so selection must reject short candidates | | The narrator's rate | 2.60 words a second; scripts are planned on it, and a take is never sped up or time-stretched | Respell a name only in the request text, and keep the script and its captions as written. Vercel is sent as "Vursell" and shadcn as "shad C N". `npx gt` is sent as "NPX GT", because the spelled-out "N P X G T" was read as "N P", a pause, then "X G T". JSON is sent as "jaysonn". Open-weight licences decide which engines a commercial film may use. OmniVoice, F5-TTS, E2-TTS, MaskGCT, Llasa, Voxtral TTS, Spark-TTS and XTTS-v2 have non-commercial weights. MOSS-TTS v1.5, VoxCPM2, Qwen3-TTS, CosyVoice and Kokoro are Apache-2.0. Rubber Band is GPL-2.0-or-later, and ffmpeg's `rubberband` filter makes the FFmpeg build GPL. ## Frames, probes and disks [#frames-probes-and-disks] Each of these made a check read the wrong frame or a render fail for reasons outside the film. | Symptom | Cause | Fix | | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | A still pulled from a render came out near black | `ffmpeg -ss t -frames:v 1` on these renders | Select by index: `-vf "select='eq(n\,N)'" -vsync 0` with N = round(t x fps) | | A render looked fine in the log and was an error page | The page behind it failed to load | A flat error page has a grey standard deviation under about 20; check every frame set in one pass | | The disk filled during a render | With several workers a render writes every frame before it encodes, about 0.33GB for a 40s film | `--workers 1` streams frames; free 5GiB before a render, 8GiB for several workers | | Every clip showed at once in a Playwright probe | A plain Playwright page has no HyperFrames runtime | Set each clip's visibility from `data-start` and `data-duration` before seeking | | `page.evaluate(() => tl.seek(t))` hung | It returns the timeline, and serialising it never ends | Return a number from `evaluate` | | `hyperframes check` reported a DOM read inside a timeline callback that never happens | The check follows calls by scanning text and reads an expression-bodied arrow as running to the next top-level comma | Write helpers as function declarations or block-bodied arrows | | A master over 100MB could not be committed | GitHub refuses files over 100MB | A share copy at `libx264 -crf 22` with faststart took a 153MB master to 39MB | Stop only the process IDs you started. Parallel render jobs run the same command, so a `pkill` by pattern on 2026-10-10 stopped every other job's render. ## Pacing and scripts [#pacing-and-scripts] A film states its stakes early, moves one thing at a time and never hangs on a still. These rules come from Kevin's notes on the films between 2026-10-01 and 2026-10-16. | Rule | Number | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | The stakes land | In the first 6 to 8s | | Headings in a film | 4 to 6, each a 2 to 6 word title that arrives whole on a beat, written as a header and never as an excerpt of the spoken line | | Quotes | At most one in a film | | Picture events | About two a second at most, one main motion at a time | | A shot's hold once it reads | About 0.5s | | The longest still | About 2s, except the end card | | A connecting element | Never waits about 2s for what it connects | | Reading hold for on-screen text | (words / 3) + 1s after it has fully arrived | Lengthen the film with pauses between lines before cramming events into it. When one object leads into the next shot, it moves physically: it slides, passes behind something or is hidden by it. A film never shrinks an object to a dot or morphs a card into a globe. A revision keeps every set piece Kevin has already seen and retimes or reorders it instead. The narration tells its own story, and on 2026-10-10 Kevin rejected a film that read an article's points as headings over code panels even though every fact in it was right. A title card carries one statement with the mark small in a corner, and [Writing](https://www.prototemplate.com/docs/craft/writing) has the mock-ups that settled it. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Browser and build traps > What breaks in a Next.js 16, fumadocs, canvas or SVG site, learned on this one: each trap with its symptom, its cause, the fix and the numbers. URL: https://www.prototemplate.com/docs/engineering A Next.js 16 site with fumadocs docs and live canvas fields fails in ways the browser rarely reports: a field draws black, a click paints an empty layout, two builds of one page differ. This site and generaltranslation.com, General Translation's site, met each of these traps between July and October 2026, and every fix comes with its numbers and the file, lint or skill that keeps it from coming back. <Cards> <Card title="Docs site traps on Next.js 16 and fumadocs" href="/docs/engineering/docs-traps" description="Intermediate frames from in-render redirects, sidebar marks at hydration, the fumadocs 16.16 sidebar, Markdown twins, Shiki's time limit, the lint findings fumadocs leaves, the cascade and builds." /> <Card title="Canvas and WebGL engines" href="/docs/engineering/engines" description="Chrome's budget of about 16 contexts, a shared canvas that only grows, readback canvases, zoom, a frame-time governor, shader weight, galleries of live previews and live values on static pages." /> <Card title="Proving a visual change" href="/docs/engineering/verification" description="Seam greys that count strokes, probes before edits, the blank in-app pane, the stress matrix, real gestures, WebKit, parity across an upgrade and the live deployment stamp." /> <Card title="Performance without visual loss" href="/docs/engineering/performance" description="Measurements that lie under load, field data against lab data on a real docs site, first-load bytes, module weight with a pixel-identity proof and dependencies judged one by one." /> <Card title="Lottie on canvas" href="/docs/engineering/lottie" description="Eight lottie-web canvas traps, from the missing glyph that stops a frame to matte buffers sized once, with the seam drag at 5 and 56 fps and the proof recipe." /> <Card title="Localized routing" href="/docs/engineering/i18n-routing" description="A locale guard ahead of every rewrite, the routing matrix, the gt-next Link, strings the extractor misses and a start prompt that has to run unchanged." /> <Card title="Dev servers and builds" href="/docs/engineering/dev-servers" description="Every next dev failure mode by its symptom, the listen-only kill, filtered installs, CI without the content submodule and pnpm's minimum release age." /> </Cards> ## Ten traps [#ten-traps] | Symptom | Cause | Fix | | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------- | | [A WebGL field draws black after a dozen client-side navigations, with no error](https://www.prototemplate.com/docs/engineering/engines#the-context-budget) | Chrome keeps about 16 live WebGL contexts, and each mount made one | One engine and one context per engine module for the session | | [Every route of the dev server answers 500 with "Unexpected non-whitespace character after JSON"](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) | `next dev` under load left its prerender manifest half written | Stop the server by PID, delete `.next/dev`, start it again | | [A click on a docs folder paints the layout with no article for about 280 ms](https://www.prototemplate.com/docs/engineering/docs-traps#links-that-point-at-leaf-pages) | The page threw `redirect()` during render | Link to the leaf page; redirect in `next.config.ts` | | [Two builds of one page ship different code colours](https://www.prototemplate.com/docs/engineering/docs-traps#code-highlighting-across-builds) | Shiki stops tokenizing a line after 500 ms on a loaded machine | `tokenizeTimeLimit: 0` | | [A Markdown twin answers 200 locally and 500 in production](https://www.prototemplate.com/docs/engineering/docs-traps#markdown-twins-in-production) | The route read MDX from disk, which the deployed function does not have | Prerender every twin with `dynamicParams = false` | | [Utilities such as `font-semibold` and `mt-6` do nothing on headings](https://www.prototemplate.com/docs/engineering/docs-traps#unlayered-rules-and-layered-utilities) | An unlayered reset outranks Tailwind 4's layered utilities | Metrics in the section's stylesheet, gaps on the parent | | [A canvas goes soft after browser zoom](https://www.prototemplate.com/docs/engineering/engines#browser-zoom) | Zoom changes `devicePixelRatio` without resizing the box | Watch the resolution media query and re-arm it on every change | | [Shader and dither canvases come back blank in an embedded browser pane](https://www.prototemplate.com/docs/engineering/verification#the-in-app-browser-pane) | The pane reports the page hidden and pauses `requestAnimationFrame` | Take pixels with Playwright in Chrome for Testing | | [A translated Lottie animation draws nothing after one text layer](https://www.prototemplate.com/docs/engineering/lottie#a-missing-glyph-stops-the-frame) | A character missing from the glyph list throws inside lottie-web's canvas text | An empty glyph entry for every drawable character | | [Strings render in English in every locale](https://www.prototemplate.com/docs/engineering/i18n-routing#links-routers-and-strings-in-gt-next) | The `gt generate` command does not follow a `gt` destructured out of `Promise.all` | `const gt = await getGT();` on its own line | --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Docs site traps on Next.js 16 and fumadocs > Intermediate frames, marks lost at hydration, sidebar fixes, Markdown twins, stable highlighting, the cascade and builds on a fumadocs site. URL: https://www.prototemplate.com/docs/engineering/docs-traps A docs site on Next.js 16 and fumadocs paints intermediate frames, drops marks at hydration and differs between builds unless a few rules hold. The cases come from this site, which runs fumadocs 16.16 on Next.js 16.2, and from the docs at generaltranslation.com, which General Translation (GT) builds on the same stack in gt-cloud, its product repository. ## Links that point at leaf pages [#links-that-point-at-leaf-pages] A click on a docs folder URL painted the docs layout with the section picker and no article, and the article replaced it about 280 ms later. The page threw `redirect()` during its render, and Next.js commits the layout above the page at the old URL before the redirect target arrives. A redirect in `next.config.ts` is answered before any layout renders and paints no such frame. The fix is to make every link the site renders point at the resolved leaf page, and to keep the in-render redirect only for crawlers and old links. A folder that needs a redirect for typed URLs gets it in the config: ```ts // next.config.ts const nextConfig = { async redirects() { return [{ source: '/docs/guides', destination: '/docs/guides/install', permanent: false }]; }, }; export default nextConfig; ``` Found on generaltranslation.com's docs on 2026-09-02, where a test now holds the footer to leaf URLs. The trap is recorded in the first-paint section of the [gt-motion skill](https://www.prototemplate.com/skills/gt-motion). ## Marks that exist at first paint [#marks-that-exist-at-first-paint] The docs sidebar showed no rail and no current-row tint until hydration, then popped them in. The sidebar's motion layer (`SidebarMotion`) draws its rail, pill and thumb in a mount effect, and a mount effect runs after the first paint. The rails fumadocs draws itself sit about 10 px inboard of the drawn path, so showing them before hydration made the rail jump sideways and read as two parallel lines. The fix is to gate only the mark whose CSS geometry matches the drawn one. The row tint fumadocs paints stays until the mount effect finishes and sets `data-sb-ready` on the sidebar, and the drawn pill takes over from there: ```css #nd-sidebar[data-sb-ready] [data-radix-scroll-area-viewport] :is(a[href], button[type='button']):hover, #nd-sidebar[data-sb-ready] [data-radix-scroll-area-viewport] a[data-active='true'] { background-color: transparent; } ``` Check sidebar work on a page with a nested folder open. A top-level page has no open folder and hides every nested defect. Found on generaltranslation.com's docs in September 2026; the rule is in `src/app/docs/docs.css` and `src/components/docs/SidebarMotion.tsx`. ## The sidebar after fumadocs 16.16 [#the-sidebar-after-fumadocs-1616] Version 16.16 of fumadocs changed how the notebook layout's folders collapse and scroll, and a sidebar drawn over it needed six fixes. The docs at generaltranslation.com moved to 16.16 on 2026-10-07, and the last fix landed on 2026-10-13. | Symptom | Cause | Fix | | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | The rail runs past the remaining rows while a folder folds | 16.16 added a list div that keeps its height while the collapsible animates to 0 | Read whether a folder is open from the collapsible: `:scope > div[data-state]` with `offsetHeight > 0` | | The pill and thumb stay on a row that is folding away | The folded row stays mounted until the collapse animation ends | Treat an active link inside `div[data-state] > div[data-state="closed"]` as not current | | The list jumps when a folder reopens | fumadocs calls the viewport's `scroll()` whenever an active row mounts, reopened folders included | Replace the desktop viewport's `scroll()` with a no-op and place the viewport by writing `scrollTop` | | The list scrolls when a folder opens near its end | Scroll anchoring | `overflow-anchor: none` on the viewport | | The sticky sidebar lifts at the end of the page | A page height that ends in half a pixel | `margin-bottom: -1px` on the sidebar's grid column from 768 px | | A loading row, and the thumb on a top-level row, vanish | The motion layer skipped every node whose class contained `docs-sb-` | Skip the motion layer's own nodes by exact class | This site's `src/components/docs/SidebarMotion.tsx` carries the four script fixes (rows 1, 2, 3 and 6). The full record is in the docs reference of the [gt-website skill](https://www.prototemplate.com/skills/gt-website). ## Markdown twins in production [#markdown-twins-in-production] A Markdown twin route (the page's address plus `.md`) worked locally and returned 500 from the deployed function. The route read the page's MDX from disk on demand, and the deployed function has no copy of the content. The fix is to prerender every twin and refuse every other path: ```ts // src/app/md/[...path]/route.ts export const dynamic = 'force-static'; export const dynamicParams = false; export const revalidate = false; export function generateStaticParams(): { path: string[] }[] { return docsPages().map((page) => ({ path: ['docs', ...page.slugs] })); } ``` A middleware matcher that excludes dotted paths skips every `.md` URL, so the rules for twins live in `next.config.ts` rewrites or in the route itself. Agent-readability checkers count links in the server-rendered body, and a Copy page menu that unmounts its rows while closed hides them, so the page also renders machine-only anchors with `sr-only`, `aria-hidden`, `tabIndex={-1}` and `data-pagefind-ignore`. Found on generaltranslation.com in September 2026. [The SEO system](https://www.prototemplate.com/docs/seo) describes this site's twins, its agent files and the shallow metadata merge behind its canonicals. ## Code highlighting across builds [#code-highlighting-across-builds] Two builds of the same page shipped different code colours. Shiki stops tokenizing a line after 500 ms by default (`tokenizeTimeLimit`), and on a loaded build machine the limit fires, so the rest of the line ships as one plain span or in the previous token's colour. | Run on the ten code fences of one page | Coloured spans | | -------------------------------------- | -------------- | | A forced limit, first pipeline | 64 | | A forced limit, second pipeline | 47 | | `tokenizeTimeLimit: 0`, every run | 73 | When the limit does not fire, the output with 0 is byte-identical to the default. Set the option wherever Shiki runs, in the fumadocs collection and on every rehype Shiki call: ```ts // source.config.ts import { rehypeCodeDefaultOptions } from 'fumadocs-core/mdx-plugins'; import { defineConfig, defineDocs } from 'fumadocs-mdx/config'; export const docs = defineDocs({ dir: 'content/docs' }); export default defineConfig({ mdxOptions: { rehypeCodeOptions: { ...rehypeCodeDefaultOptions, tokenizeTimeLimit: 0 }, }, }); ``` Measured on generaltranslation.com's docs in October 2026; this site sets it in `source.config.ts`. ## MDX that drops props [#mdx-that-drops-props] A blog carousel rendered with its `items` prop undefined, and the build reported nothing. The blog compiles MDX through `next-mdx-remote` with JavaScript expressions blocked, which removes every `prop={expression}` and every `{expression}` before compiling. Docs pages compile through fumadocs and keep expressions. The fix is components that take string attributes and child elements. Three publishing rules follow from the same pipeline on generaltranslation.com, where the posts live in a separate content repository checked out as a submodule: * The content repository's preview app needs a stand-in for every new component, or its build fails. * Every deploy checks out the tip of the content repository's main branch whatever the submodule pointer records, so merging the content pull request publishes the post, and the code side merges first. * The content repository squash-merges, so a pointer at a branch commit never becomes an ancestor of its main branch. The handbook's [graphics document](https://www.prototemplate.com/docs/handbook/graphics) carries the hand-off to a post. ## What fumadocs leaves for the lints [#what-fumadocs-leaves-for-the-lints] A fumadocs page draws the right font file and still fails a strict type system, because fumadocs' elements carry their own sizes and feature settings. This site's live type lint read five docs routes at 1440 and 390 px on 2026-10-21 and found 288 problems; the live radius lint found 5. | Lint and rule | Findings | What it read | | ------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | | type L2, features that match the role | 261 | `font-feature-settings: normal` on the search button, the nameplate, the sidebar rows, the prose, the tables and the table of contents | | type L3, the h1 on the display ladder | 10 | The page title at fumadocs' `text-[1.75em]`, 28 px where the ladder's steps are 44, 32 and 24 px | | type L6, balanced headings | 17 | `text-wrap: auto` on the title, the "On this page" heading and the card titles | | radius L3, legal corners | 5 | The table-of-contents dot's `rounded-full`, computed as 33,554,400 px | On `/docs/handbook/design` at 1440 px in the dark theme, the fix below changes these values when it is injected into the rendered page: | Part | fumadocs alone | With the fix | | --------------------- | -------------------------------------------- | --------------------------------------------------------- | | Title | 28/42, untracked, features `normal`, opsz 28 | 44/45.76 at -0.025em, display features, balanced, opsz 32 | | Lead | 18/28, 840 px wide, 69 characters a line | 17/26.35, 510 px wide | | Body | 16/28, 840 px wide, 107 characters a line | 16/25.6, 512 px wide, 64 characters a line | | Table cells | 14/24 | 14/21.7 with tabular figures | | Sidebar rows | 13/18.57 on a 30.6 px pitch | 13/17 on a 28 px pitch | | Table-of-contents dot | 33,554,400 px corner | 50% on its 4 by 4 box | The fix gives the docs root a class that joins the roots `tokens.css` already types, puts the title, lead, prose and tables on the ladder in `docs.css`, and gives the leftover corners legal values: ```tsx // src/app/docs/layout.tsx <DocsLayout tree={source.getPageTree()} containerProps={{ className: 'pt-docs' }}> ``` ```css /* tokens.css: .pt-docs joins each base list */ :is(.pt-viewer, .pt-root, .pt-docs) :is(h1, h2) { font-family: var(--pt-display); font-feature-settings: var(--pt-ff-display); text-wrap: balance; } /* docs.css: the ladder and the corners */ #nd-page > h1 { font-size: var(--pt-d1); line-height: var(--pt-d1-lh); letter-spacing: var(--pt-d1-track); } #nd-toc [style*='offset-path'] { border-radius: var(--pt-radius-round); } ``` Injected into the rendered pages on 2026-10-21, the rules took the live type lint from 288 findings to 0 and the live radius lint from 5 to 0 on the same routes, at rest and with the search dialog open. The 28 px title is the `DocsTitle` of fumadocs-ui 16.16.1. The rules are held by `scripts/lint/type.mjs` and `scripts/lint/radius.mjs`, which [Gates](https://www.prototemplate.com/docs/gates) runs. [Type](https://www.prototemplate.com/docs/craft/type) explains the ladder and the features. ## Features that compute normal [#features-that-compute-normal] Every docs element computed `font-feature-settings: normal` while the family was right. Tailwind 4's preflight sets `font-feature-settings: --theme(--default-font-feature-settings, normal)` on `html`, and the theme resolves that variable from `--font-sans--font-feature-settings`. The docs sheet sets `--font-sans: var(--pt-text)` and no features sub-token, so `html` computes `normal` and every docs element inherits it. Setting `--font-sans--font-feature-settings` in the docs sheet's `@theme` fixes the docs and leaks. React keeps a route's stylesheet in the document after a client navigation, so the token reaches `html` on every route the reader visits afterwards, including design explorations that keep their own type. To see it, open `/brand`, click Docs in the sidebar and go back: the docs chunk is still linked on `/brand`. The fix scopes every docs rule to docs-only ids, attributes or `:root:has(#nd-notebook-layout)`, and types the docs through a class on their root. Found on this site on 2026-10-21; the header of `src/app/docs/docs.css` states the scoping rule. ## Markup that hides from a lint [#markup-that-hides-from-a-lint] A live lint reads the elements present when it runs, and fumadocs renders four kinds of markup that a rest-state reading misses. | Markup | Why a lint misses it | How to read it | | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | Prose h2 and h3 text inside `a[data-card]` with a copy-link button | A lint that reads only elements whose own text sits in a heading never checks prose heading tracking | Read a heading through an anchor child that holds all its text | | The search dialog, portaled into `body` outside the layout | No lint opens it. Open, its ESC key renders in Menlo, 10 findings across five routes at two widths | Open it with the shortcut (`Meta+k`) before collecting | | The sidebar's scroll thumb (Radix ScrollArea) | It mounts only while the pointer is on the list, and its `rounded-full` computes 33,554,400 px | Hover the list, wheel 300 px, wait up to 5 s for `[data-orientation="vertical"] > *` | | Tailwind type utilities in component class strings (`text-[13px]`, `text-xs`) | The type lint reads stylesheets and inline style objects | A class-string rule like the radius lint's check on `rounded-*` | Every row of the table of contents also carries the `prose` class, so a prose override scoped to `.prose` alone restyles the table of contents. Scope it to `#nd-page .prose`. Found on this site on 2026-10-21; the live lints' collectors are in `scripts/lint/type.mjs` and `scripts/lint/radius.mjs`. ## The cascade [#the-cascade] ### Unlayered rules and layered utilities [#unlayered-rules-and-layered-utilities] Tailwind 4 puts its utilities in a cascade layer, and any unlayered rule outranks every layered rule whatever their specificities. For normal declarations the order is unlayered, then utilities, components, base and theme. An unlayered heading reset such as `h1 { font-weight: 500; letter-spacing: -0.028em; margin: 0 }` silently defeats `font-semibold`, `tracking-*` and margin utilities on every h1. Heading and paragraph metrics therefore live in the section's own stylesheet, and siblings are spaced by a gap on the parent. The docs adapters use the same rule on purpose: every override in `docs.css` is unlayered, so it outranks fumadocs' utilities with no `!important`. DESIGN.md section 12 and the [gt-landing-pages skill](https://www.prototemplate.com/skills/gt-landing-pages) hold the rule. ### Specificity of an is() list [#specificity-of-an-is-list] A `:is()` list takes the specificity of its most specific argument. One ID added to a list of root classes, such as `:is(.pt-viewer, .pt-root, #nd-notebook-layout)`, lifts every rule built on the list from (0,1,1) to (1,0,1), and those base rules then override component rules across the site. The docs root joins the lists as the class `.pt-docs`, at (0,1,0) like every root already listed. Found on this site on 2026-10-21. ### The typography plugin in the utilities layer [#the-typography-plugin-in-the-utilities-layer] The docs sheet loads Tailwind's theme and utilities layers and no components layer, so the typography plugin's `.prose` rules land in the utilities layer. There they tie with utilities at (0,1,0), and the rule emitted later wins: a table-of-contents row carries both `prose` and `text-sm`, and `text-sm` wins. Found on this site on 2026-10-21; the head of `src/app/docs/docs.css` lists the layers it imports. ### The full round corner [#the-full-round-corner] Tailwind 4's `rounded-full` is `calc(infinity * 1px)`, which Chrome computes as 33,554,400 px, so a corner lint that lists legal values reports that exact number. A square box takes `50%`: the table-of-contents dot is 4 by 4 px. The sidebar's scroll thumb is 6 px wide and taller than that, so `50%` would draw an ellipse, and it takes the square shell corner. The legal corners are pinned in `scripts/lint/radius.mjs`. ### Tailwind 4 build traps [#tailwind-4-build-traps] | Symptom | Cause | Fix | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | A token is missing from the built CSS | A token in a plain `@theme` block that no utility uses is removed | Declare tokens read by inline styles or third-party CSS in `@theme static`; grep the built chunk for the name | | The CSS build breaks after an edit to a comment | Tailwind scans comments for class candidates, and a bracketed utility written in a comment is one | Fix the comment, then `rm -rf .next`, because the candidate cache persists | | Classes added to a workspace package never compile | The app's dev server has not rescanned its sources | Run `touch` on the app's `globals.css` | | A colour token is invalid | The tokens are bare HSL triples | Read them as `hsl(var(--border))` | | A size from a variable becomes a colour | Tailwind 4 reads `text-[var(--x)]` as a colour | `text-[length:var(--x)]` | The callouts and diff lines of fumadocs read `--color-fd-info`, `--color-fd-warning` and the other status colours, which no utility on this site uses, so `src/app/docs/docs.css` declares them in `@theme static`. The traps were met on generaltranslation.com in 2026 and are recorded in the deploys reference of the [gt-website skill](https://www.prototemplate.com/skills/gt-website). ## Builds [#builds] ### Inline CSS, built and reverted [#inline-css-built-and-reverted] The Next.js option `experimental.inlineCss` made the docs heavier. Next.js 16.2 embeds the entry CSS twice, once in a style tag and once in the flight data, and generaltranslation.com's docs HTML grew from 245 KB to 731 KB. The change was built, measured, reverted and listed in the pull request body, which is the rule for any idea that measures worse. Measured in September 2026; [Performance without visual loss](https://www.prototemplate.com/docs/engineering/performance) has the rest of that round. ### Prerender memory on Next.js 16.3 [#prerender-memory-on-nextjs-163] Next.js 16.3 retains memory for every prerendered page (vercel/next.js#97464). On generaltranslation.com the build worker was killed at page 1,779 of 7,117, where 16.2 builds the same pages in under a minute. The fix pins `next` 16.2.12 in the app's own `package.json` while the workspace catalog moves on, with the trade-off recorded: 16.3.8 is a security release for image optimization with allow-listed remote hosts. Found on 2026-10-01. This site pins the same version, and [Use these docs as a template](https://www.prototemplate.com/docs/template) names the pin. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Canvas and WebGL engines > The context budget, shared canvases, readback, quality from frame time, what a field costs, galleries of live previews and engine lifecycles. URL: https://www.prototemplate.com/docs/engineering/engines An engine that draws into a canvas has a budget of contexts, a cost per cell and a lifecycle, and the browser reports none of its failures. The lessons come from the engines behind this site's live fields (the studio field, the horizon and prismatic shaders and the glyph field, each a module in `src/lib/`) and from their copies on generaltranslation.com, General Translation's site. The [gt-performance skill](https://www.prototemplate.com/skills/gt-performance) holds every rule below with its date. ## The context budget [#the-context-budget] Chrome keeps about 16 live WebGL contexts. Past that, `getContext` returns null and a field draws black with no error in the console. On 2026-07-29 a design exploration's shader rendered on a fresh load and was black after 12 client-side switches between explorations, because each mount had created a context of its own. The fix is one engine per engine module for the whole session: * One offscreen GL canvas draws every field and blits into each field's own 2D canvas from one `requestAnimationFrame` loop. * The context stays when a field's `destroy()` runs, so it survives route switches. * `preserveDrawingBuffer: true` lets the blit read the frame in the same task as the draw. * A lost context sets the engine to null, and the next draw rebuilds it. * A preset whose shader fails to compile stays failed for the session, and the other presets keep working. * With no field on screen, the loop stops. A component that owns its context releases all of it on unmount. A footer mark on generaltranslation.com leaked one context per mount until a review caught it on 2026-08-08. ```ts function releaseGl(gl: WebGL2RenderingContext, program: WebGLProgram, shaders: WebGLShader[], buffer: WebGLBuffer): void { gl.deleteBuffer(buffer); gl.deleteProgram(program); for (const shader of shaders) gl.deleteShader(shader); gl.getExtension('WEBGL_lose_context')?.loseContext(); } ``` Count contexts without creating any. A probe that calls `getContext` on the page's canvases creates contexts and exhausts the budget it measures, so wrap `getContext` before the page's scripts run: ```js // Playwright: install before page.goto, read with page.evaluate(() => window.__glContexts) await page.addInitScript(() => { const seen = new WeakSet(); window.__glContexts = 0; for (const proto of [HTMLCanvasElement.prototype, globalThis.OffscreenCanvas?.prototype]) { if (!proto?.getContext) continue; const original = proto.getContext; proto.getContext = function (type, ...rest) { const ctx = original.call(this, type, ...rest); if (ctx && /webgl/i.test(String(type)) && !seen.has(ctx)) { seen.add(ctx); window.__glContexts += 1; } return ctx; }; } }); ``` The studio, prismatic and horizon engines in `src/lib/` follow the one-engine rule, and the frame probe in the gt-performance skill counts contexts this way in its `gl` column. ## A shared canvas that only grows [#a-shared-canvas-that-only-grows] Setting a canvas's `width` or `height` reallocates its drawing buffer. An engine that resized its one GL canvas to each field reallocated it twice a frame once two fields of different sizes were on screen. On 2026-10-08 a full-bleed field and two 125 px logo fields kept the main thread busy about 1,000 ms of every second for that alone. The fix grows the canvas to the largest field, draws each field into the bottom-left corner, which is GL's origin, and blits that rectangle: ```ts const glCanvas = gl.canvas as HTMLCanvasElement; if (glCanvas.width < width) glCanvas.width = width; if (glCanvas.height < height) glCanvas.height = height; gl.viewport(0, 0, width, height); // ...draw the field... target.drawImage(glCanvas, 0, glCanvas.height - height, width, height, 0, 0, width, height); ``` The pattern is in `src/lib/prismatic-field.ts`. ## Readback canvases and the blit path [#readback-canvases-and-the-blit-path] A canvas created with `willReadFrequently` stays in CPU memory, so every `drawImage` from it uploads its pixels to the GPU. That upload was the glyph rain's lag on phones, worst on WebKit. The glyph atlas needs `getImageData` while it is built, so it keeps the flag, and each build snapshots the finished atlas into an `ImageBitmap` for the per-frame blits: ```ts let atlasSource: CanvasImageSource = atlas; let generation = 0; function snapshotAtlas(): void { if (atlasSource instanceof ImageBitmap) atlasSource.close(); atlasSource = atlas; // blit from the canvas until the snapshot lands const current = ++generation; createImageBitmap(atlas).then((bitmap) => { if (current === generation) atlasSource = bitmap; else bitmap.close(); }); } ``` The shipped version is `atlasSrc` in `src/lib/glyph-field.ts`. Films set the same flag on purpose, to keep a canvas in software so every render is identical ([Films](https://www.prototemplate.com/docs/craft/films)). ## Browser zoom [#browser-zoom] Browser zoom changes `devicePixelRatio` and leaves the CSS box alone, so an engine that only listens to a ResizeObserver keeps blitting at the old ratio and goes soft. A media query on the current resolution fires once when the ratio changes, so it is re-armed on every change: ```ts function watchPixelRatio(onChange: () => void): () => void { let query = matchMedia(`(resolution: ${devicePixelRatio}dppx)`); const handle = (): void => { query.removeEventListener('change', handle); onChange(); query = matchMedia(`(resolution: ${devicePixelRatio}dppx)`); query.addEventListener('change', handle); }; query.addEventListener('change', handle); return () => query.removeEventListener('change', handle); } ``` Coalesce the ResizeObserver callbacks into one animation frame, keep `resize()` idempotent for when both fire, and remap positions that are in flight by the ratio of the old box to the new one. gt-cloud's `glyph-field.ts` in `packages/ui` holds the zoom watcher, as the gt-performance skill records. ## Quality from measured frame time [#quality-from-measured-frame-time] A quality tier chosen from the window width gives a wide window on weak hardware the heaviest tier. The glyph rain now picks its tier from measured frame time, with these numbers: | Setting | Value | | ----------------------------------------------- | ----------------------------------------------------------------------------- | | Warmup before judging | 2.5 s | | Window | 120 counted frames | | An interval treated as a hidden tab and skipped | over 900 ms | | A slow frame | over 22 ms | | Trip | 35% or more of a window's frames slow, a sustained cadence under about 45 fps | | Tier 1 | the device-pixel cap drops from 2 to 1.25 through one `resize()` | | Tier 2 | the lean pool of 560 glyphs out of 1,280, switched once no glyph is in flight | The trip counts slow frames. A first draft watched a rolling median near 20 ms and missed throttled hardware that went over budget on 34% of its frames while the median stayed healthy. On 2026-08-07 a 12x CPU throttle stepped down to tier 2 inside one window, and a 10 s unthrottled control never tripped. ```ts const BUDGET_MS = 22, WINDOW = 120, TRIP = 0.35, WARMUP_MS = 2500, GAP_MS = 900; let bornAt = 0, counted = 0, slow = 0, tier = 0; function govern(now: number, intervalMs: number, stepDown: (tier: number) => void): void { if (tier >= 2 || intervalMs > GAP_MS) return; if (bornAt === 0) bornAt = now; if (now - bornAt < WARMUP_MS) return; counted += 1; if (intervalMs > BUDGET_MS) slow += 1; if (counted < WINDOW) return; const trip = slow / counted >= TRIP; counted = 0; slow = 0; if (trip) stepDown(++tier); } ``` The ladder only steps down, so it never oscillates, and the pool switch waits until no glyph is moving. A loop capped at 30 fps judges the raw `requestAnimationFrame` cadence, or each skipped draw reads as a slow frame. The shipped version is `govern()` in `src/lib/glyph-field.ts`, and the frame probe's `--cpu 12` reproduces the proof. ## Shader source weight [#shader-source-weight] A minifier cannot remove text inside a template literal, so every comment in GLSL source ships to every visitor. Moving the shader's notes into a TypeScript comment beside the literal cut the shipped bytes with no change to the output. | Measure | Before | After | | ------------------------------- | ------------ | ------ | | Horizon fragment shader literal | 6.5 KB | 4.4 KB | | Horizon module, gzipped | about 7.4 KB | 4.6 KB | ```ts // The notes live in this comment, which the minifier removes. The literal holds GLSL only, // and #version must be its first characters. const FRAG = `#version 300 es precision highp float; out vec4 color; void main() { color = vec4(1.0); }`; ``` A newline before `#version 300 es` failed the compile silently, and only the fallback disc rendered until a screenshot pass caught it. After any edit to shader text, check that the shader draws and the fallback is hidden. Report weight as the source, the gzipped module and the tree-shaken bytes a page ships: on 2026-08-13 the dither library (`src/lib/dither.ts`) was 47.3 KB of source, 4.8 KB gzipped, and 2.8 KB gzipped for what the sign-in page imports. One module can also sit in several route chunks; on 2026-10-01 the horizon shader sat in four chunks of 11.7 to 19.3 KB gzipped. A cut that folded about twelve uniforms into constants and deleted `pause`, `resume` and `renderStatic` saved 2.5 KB gzipped and was reversed, because the engines are a library and every parameter stays a uniform with a setter. Identity is proven with a pixel diff of a deterministic frame, with the baseline captured twice first; a constant fold left 4 of 4,039,200 channels off by 1/255, because GPU drivers can fuse multiply-adds differently after any source change. [Performance without visual loss](https://www.prototemplate.com/docs/engineering/performance#shader-and-module-weight) has the commands. ## Galleries of live previews [#galleries-of-live-previews] A wall of live previews lags even when every preview is frozen. Twenty-two live iframes running shaders made a presenter wall lag on 2026-07-31, and every page on this site now carries a gate that queues `requestAnimationFrame` callbacks while a parent says so: ```js // In each page's head, before other scripts (function () { const native = window.requestAnimationFrame.bind(window); let queue = []; let frozen = window.parent !== window && /[?&]frozen=1(&|$)/.test(location.search); window.requestAnimationFrame = (callback) => { if (frozen) { queue.push(callback); return -1; } return native(callback); }; window.addEventListener('message', (event) => { if (event.data?.type !== 'gt:freeze') return; frozen = Boolean(event.data.frozen); if (frozen) return; const pending = queue; queue = []; for (const callback of pending) native(callback); }); })(); ``` The parent freezes a frame with `frame.contentWindow.postMessage({ type: 'gt:freeze', frozen: true }, location.origin)`. A frozen same-origin frame still hydrates, composites and runs its CSS animations on the parent's main thread, so freezing was not enough: | Measure on 2026-10-08 | Value | | ------------------------------------------------------------------- | ---------------------------- | | Live pages held by a gallery that froze each frame 2.8 s after load | 12 | | WebGL contexts in that gallery | 4 | | Renderer memory | about 1.3 GB | | Main thread busy at rest | about 950 ms of every second | | One stage left live while off screen | about 110 ms of every second | The fix shows a thumbnail on every card and mounts the live page only while the pointer is over it, and freezes a stage whenever no part of it is on screen. The gate wraps `requestAnimationFrame` only, so a loop on `setInterval` or `setTimeout` keeps running inside a frozen preview. The root layout (`src/app/layout.tsx`) installs the gate, and `src/app/present/viewer/LazyFrame.tsx` mounts on hover. ## Strict mode and effects [#strict-mode-and-effects] React strict mode runs an effect's setup, cleanup and setup again in development, so a mount effect that appends nodes and does not remove them leaves duplicates. On 2026-08-07 a headline engine left a dead duplicate canvas and a pair of guide elements in the DOM. Every effect removes every node it creates, and a check after mount and after a hot reload confirms exactly one canvas exists. Both gt-cloud and this repository fail a bare `useEffect` in their lints (`gt-ui/no-use-effect` and `pnpm lint:practices`), and each case has its own form: | Situation | Write this | | ----------------------------------------------- | ----------------------------------------------------------------------------- | | A value derived from props or state | Compute it during render | | An expensive derivation | `useMemo` | | Reset state when a prop changes | A `key` on the component | | Respond to a click, submit or drag | The event handler | | Sync with something outside React once on mount | `useMountEffect` | | A mount effect that runs only under a condition | A wrapper that checks the condition around a child that runs `useMountEffect` | | A fresh mount effect when an id changes | `key={id}` on the child | This site's `src/lib/use-mount-effect.ts` defers its cleanup by one task, so strict mode's simulated rerun cancels it and a real unmount still cleans up once. gt-cloud's version is a plain mount-only effect, and neither is copied over the other. The [gt-components skill](https://www.prototemplate.com/skills/gt-components) holds the table. ## Inherited properties on a moving subtree [#inherited-properties-on-a-moving-subtree] Writing an inherited custom property on an element restyles its whole subtree on every write. The reveal seam keeps its position in `--seam-cut` on its box, so each drag frame restyles everything under it. A figure drawn as SVG with 27,800 nodes under the seam dragged at 5 fps at 4x CPU throttle; drawn into one canvas, the same drag reached 56 fps. The fix keeps the subtree under a moving variable small, such as one canvas, and leaves the shared seam component as it is. [Lottie on canvas](https://www.prototemplate.com/docs/engineering/lottie) has the rest of that figure's round, and the gt-motion skill's web reference holds the rule. ## Static pages with live values [#static-pages-with-live-values] A prerendered page bakes whatever its build fetched into the HTML. On 2026-08-18 a GitHub star badge rendered on localhost and was empty in production, because the prerender's fetch had failed. The fix keeps the page static with a known minimum as its first label, and fetches the live value through a cached same-origin route: ```ts // app/api/stars/route.ts const CACHE = 'public, max-age=0, s-maxage=3600, stale-while-revalidate=86400'; export async function GET(): Promise<Response> { try { const res = await fetch('https://api.github.com/repos/<owner>/<repo>', { next: { revalidate: 3600 }, signal: AbortSignal.timeout(5000), }); const body: unknown = await res.json(); const count = typeof body === 'object' && body !== null && 'stargazers_count' in body ? body.stargazers_count : null; if (!res.ok || typeof count !== 'number') throw new Error('unexpected response'); return Response.json({ count }, { headers: { 'Cache-Control': CACHE } }); } catch { return Response.json({ count: null }, { status: 503, headers: { 'Cache-Control': 'no-store' } }); } } ``` The client fetches the route once on mount, aborts on unmount, ignores a value under the known minimum and holds its slot invisible while the label resolves, so nothing shifts. Any count, status or price that changes after the build uses the same shape, as section 7 of the gt-performance skill records. ## Dither fields [#dither-fields] What a dither field costs per frame, and why a dithered pattern on a moving sprite flickers unless it moves by whole cells, are measured on [Dither and the studio field](https://www.prototemplate.com/docs/components/dither). --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Proving a visual change > Turning it looks right into readings: seam greys, probes before edits, the stress matrix, real gestures, WebKit and parity across an upgrade. URL: https://www.prototemplate.com/docs/engineering/verification A visual change is proven by readings taken from the rendered page. A typecheck, a clean lint and a passing structural audit are gates, and none of them says what renders or what a reader can do, so an item that passed only gates is reported as unverified. The scripts named below are in this repository under `skills/gt-verify/scripts/` and run headless from a checkout of it, which has playwright-core. The [gt-verify skill](https://www.prototemplate.com/skills/gt-verify) holds the definition of done and every case with its date. ## A seam's composite grey [#a-seams-composite-grey] The grey a seam renders says how many strokes drew it. A stroke of alpha `a` and colour `L` over a ground `G` composites to `a*L + (1 - a)*G`, and two stacked strokes composite at alpha `1 - (1 - a)^2`. The site's hairline token `--pt-hair` reads: | Theme | Stroke over ground | One stroke | Two strokes | | ----- | ------------------ | ---------- | ----------- | | Dark | 242 at 0.22 over 7 | 59 | 99 | | Light | 7 at 0.18 over 255 | 210 | 174 | A reading near the two-stroke value is a double line or one stroke over a brighter backing such as a shader glow. A glow raises the neighbouring pixels as well, and a double line leaves them at the ground. An opaque hairline keeps its grey when two rules touch, so there a double line shows as a run twice as wide: 4 device pixels at 2x for two 1 px rules. On 2026-10-05 the toolbar's bottom rule on the brand page read 59 in Chromium and 58 in WebKit, which is one stroke. ```bash # runs of one grey along 24 CSS px downward from (280, 40), at 4x node skills/gt-verify/scripts/probe.mjs http://localhost:3005/ --point 280,52 --scan 280,40,down,24 --dsf 4 ``` The `--point` flag lists every element that paints at a viewport point, with its borders, background, opacity, shadow and outline. [Lines and seams](https://www.prototemplate.com/docs/craft/lines) has the line law these readings check. ## The spot before the edit [#the-spot-before-the-edit] Computed values are the evidence, and class names only say what was intended. On 2026-08-05 padding was trimmed in three sections when the gap came from a margin, and all of it was undone. On 2026-08-01 the gaps between locale words on an orbit were evened from a capture at rest, and a wider gap appeared on the flank, where one word had rolled over and its neighbour had not. | Flag | What it reads | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `--sel <css>` | The box model per side, type and paint values, the parent's gap and the space to each sibling, with a crop plus 24 px | | `--point x,y` | Every element under a point, top first, with what each paints | | `--scan x,y,down,24` | Device pixels along a segment, grouped into runs, with the lines marked | | `--frames 12 --every 120` | Twelve readings 120 ms apart, for every state of a moving element | | `--a11y --strict` | Controls with no accessible name, the name and state of every icon-only control, `user-select: none` text | | `--browser webkit`, `--dsf 4`, `--theme light` | The engine, the device scale and the seeded theme | ```bash node skills/gt-verify/scripts/probe.mjs http://localhost:3005/brand --sel ".pt-book-title h1" --theme dark --out <scratch>/probe ``` Quote the box model before editing, then crop corners, junctions and seams at 2x or 4x, where one 1 px line is 2 or 4 device pixels. ## The in-app browser pane [#the-in-app-browser-pane] An embedded browser pane in a desktop app can report `document.hidden` as true and pause `requestAnimationFrame`. Shader and dither canvases then come back blank, scroll-triggered reveals below the fold never fire, and GSAP entrance tweens still complete through their timeout fallback, so the page looks loaded with every field missing. Measure with JavaScript in the pane and take pixels with Playwright driving Chrome for Testing. Four capture rules apply on the Playwright side: * Seed the theme before load and assert it took. Call `context.addInitScript(() => localStorage.setItem('gt-theme', 'dark'))` before `goto`, then read `document.documentElement.dataset.theme`. This site's key is `gt-theme`, and another app's theme provider names its own. * Emulate reduced motion before `goto` with `page.emulateMedia({ reducedMotion: 'reduce' })`, then check that the page shows its designed still and no loop ticks. * Slow `requestAnimationFrame` 12x inside the capture script to see a dither step's cells, with the page's code unchanged. * Scroll through the page before a full-page capture, so lazy engines and reveals have armed. The traps are recorded in the verification section of the [gt-motion skill](https://www.prototemplate.com/skills/gt-motion). ## The stress matrix [#the-stress-matrix] A scroll story or a pinned stage is verified through the whole matrix before it is called fixed. On 2026-08-08 three failures surfaced in one evening: three layers stacked for a moment after a reload, a story that locked in a different place scrolling up than scrolling down, and text rewriting that looked wrong under 10x CPU throttling. | Case | What must hold | | ---------------------------------------------- | ---------------------------------------------------------------------------- | | Every beat, the last ones included | Each beat locks at its read line | | Slow, fast and reverse scrolling | The same position shows the same state from either direction | | Reload mid-page, or a restored scroll position | The first painted frame is already right | | A deep link into the story | The linked beat shows lit with no extra scroll | | Zoom out and back in | The story reads as it did before | | CPU throttled 10x | Settled states equal the unthrottled run; only the frame rate drops | | Every width | One composition per width, with no hybrid of desktop logic and mobile layout | The stress script (`stress.mjs`) walks the phases first, slow, fast, reverse, reload, deep link, zoom and throttle at 1440 by 900, 1920 by 1080, 1100 by 800, 868 by 525 and 390 by 844, captures the first frames 0, 150, 400, 1000 and 2500 ms after load, and writes `REPORT.md` with a sheet of every capture: ```bash node skills/gt-verify/scripts/stress.mjs http://localhost:3001/en-US \ --copy ".v0-stack-beat" --story "#platform" --hash platform --theme dark --out <scratch>/stress ``` A throttled run is judged by the timing of the story's events against the wall clock. On 2026-08-08 a headline rewrite followed each crossing of a locale belt by 2.46 s at 1x and by 2.43 to 2.50 s at 10x while frames fell to 4 to 6 a second, so the logic was right and the run was frame-starved. A run whose settled states or timings differ is wrong. The standing page check misses these sizes, so add them: | Case | Size | | -------------------- | --------------------------------------------------------- | | Phones on their side | 844 by 390, 932 by 430 | | Intermediate widths | 900, 1100 and 1279 wide, one pixel under the 1280 devices | | Short windows | 1280 by 600, 1440 by 700 | | 150% browser zoom | about 868 by 525 CSS px | At 150% zoom a band fell to one column that was shorter than its stage's 640 px floor, and the desktop scroll logic drove the one-column layout. On 2026-10-05 a consent banner covered the lit beat of a story at 390 by 844 on every step of a first visit; report such an overlay, then pass `--hide "<css>"` to read the story under it. ## Real gestures [#real-gestures] A gesture is proven by performing it. On 2026-09-02 a label fix was first shown with a Range API selection, which painted a highlight on the fixed label and proved nothing about a drag. A real drag read back the text: ```js const box = await page.getByText('Project name', { exact: true }).boundingBox(); await page.mouse.move(box.x + 2, box.y + box.height / 2); await page.mouse.down(); await page.mouse.move(box.x + box.width - 2, box.y + box.height / 2, { steps: 12 }); await page.mouse.up(); console.log(await page.evaluate(() => window.getSelection().toString())); // "Project name" ``` The same drag returned an empty string with `select-none` restored, which proved the fix. Three more rules come from the same kind of miss: * Never pass `click({ force: true })` in a verification run. It skips the checks a covered or disabled control fails. * Build every harness URL as `new URL(ORIGIN + path)`. With `new URL(path, ORIGIN)`, a path that starts with `//` is protocol-relative and leaves the origin; a routing review on 2026-09-09 first read an off-origin redirect that was its own request. * Expected values in a test come from real output, and a test is seen failing with the fix reversed before it counts. ```bash git diff -- <fix files> > <scratch>/fix.patch git apply -R <scratch>/fix.patch && pnpm exec vitest run <test file> # must fail git apply <scratch>/fix.patch && pnpm exec vitest run <test file> # must pass ``` ## WebKit [#webkit] A layout can break in one engine only. On 2026-08-18 the blog index's lead card broke in Safari alone, because WebKit resolved the cover's `height: 100%` against the whole two-row subgrid. The fix was verified with one invariant in Chromium and WebKit at desktop and tablet widths: the cover ends at its frame, and the post body starts after it. Both scripts take `--browser webkit`, and Playwright launches it with `require('playwright-core').webkit.launch({ headless: true })` once `pnpm exec playwright-core install chromium webkit` has downloaded the browsers. Layout-shift entries exist in Chromium only, so shift is measured there. ## States that mount on demand [#states-that-mount-on-demand] A lint that reads a page at rest misses every element that mounts later. On this site's docs the search dialog mounts when it opens, and the sidebar's scroll thumb mounts only while the pointer is on the list. With the dialog open, its ESC key rendered in a system mono face. The thumb carried a 33,554,400 px corner. A rest-state run saw neither. The live lints take a list of keys to press per route (`LIVE_KEYS` in `scripts/lint/type.mjs`), and the thumb needs a hover, a 300 px wheel and a wait of up to 5 s for `[data-orientation="vertical"] > *`. [Docs site traps](https://www.prototemplate.com/docs/engineering/docs-traps#markup-that-hides-from-a-lint) lists the fumadocs markup that hides this way. ## Parity across an upgrade [#parity-across-an-upgrade] A change that touches every page at once, such as a framework upgrade or a layout rewrite, is checked route by route against the reference build, with production read as a third side. A review of a fumadocs upgrade on generaltranslation.com on 2026-10-07 compared 475 page fetches a side. ```bash python3 skills/gt-verify/scripts/compare-signatures.py fetch \ --before http://localhost:3051 --after http://localhost:3052 \ --routes <scratch>/routes.txt --dir <scratch>/parity python3 skills/gt-verify/scripts/compare-signatures.py compare --dir <scratch>/parity python3 skills/gt-verify/scripts/row-diff.py <scratch>/shots <scratch>/row-diff ``` What that review taught: * Take the route list from the source or the sitemap. A dev server rewrites its prerender manifest while it serves, and the list was read truncated. * Compare machine files (`llms.txt`, the sitemap, the twins) byte for byte. A changed order in an index whose order carries meaning is a finding. * Diff screenshots row by row after aligning the rows (SequenceMatcher over row hashes), so an inserted band does not mark everything below it, with a second before capture as the noise pair. * Count the client code each page downloads. The upgrade made every docs page download a component that 24 pages use, which no structural diff shows. * Read the server HTML before hydration. A sample host read `example.com` until hydration swapped it. * Compare code highlighting on production builds. A dev server highlights nondeterministically, and a dark screenshot showed two code blocks as identical while their token spans differed. Each difference gets one label: introduced by the change, inherited, kept on purpose, pre-existing, or no fix needed. The method is in the parity reference of the gt-verify skill. ## Whether it is live [#whether-it-is-live] A merged change is live when a production build of its commit serves the page. While builds fail, the site keeps serving the last good deployment, and a merge stays invisible. Three readings settle it: ```bash git merge-base --is-ancestor <your sha> origin/main && echo ON_MAIN curl -s https://www.prototemplate.com/ | grep -oE 'dpl_[A-Za-z0-9]+' | sort -u vercel inspect <deployment url> --scope <team> ``` The longest `dpl_` id on the live page names the deployment that serves it, and it must equal the newest Ready production deployment built from a commit that contains yours. Read the stamp on the production host itself, because another alias can serve another project's build. Social cards are read live after the deploy, answer `200 image/png`, and get opened and looked at. A failed preview is retried with `vercel redeploy`, which posts fresh statuses. This site self-hosts its Google faces because Turbopack's Google font loader failed builds at random (vercel/next.js#99114). The deploy check is in the Prototemplate reference of the [gt-ship skill](https://www.prototemplate.com/skills/gt-ship). ## A red browser gate [#a-red-browser-gate] The live audits, the page check and the captures each launch Chrome for Testing, and two at once compete for the machine; on 2026-10-01 a parallel capture made the line audit fail on pages that were correct. [Gates](https://www.prototemplate.com/docs/gates#browser-gates-run-one-at-a-time) runs them one at a time, and its exit codes tell a finding from a run that could not judge. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Performance without visual loss > Measuring honestly, field against lab on a real docs site, bytes before scripts, shader and module weight, and judging dependencies on their merits. URL: https://www.prototemplate.com/docs/engineering/performance Performance work on this site and on generaltranslation.com, General Translation's site, makes a surface faster and leaves every pixel where it was. The rule comes from Kevin Liu, who reviews the work, on 2026-08-05: "fix the lighthouse without changing anything aesthetically". An audit whose fix would change the design, such as a contrast or a tap-target change, is listed with its measured gain for him to decide. The [gt-performance skill](https://www.prototemplate.com/skills/gt-performance) holds every rule below with its date. ## Measure honestly [#measure-honestly] A timing is evidence only together with the conditions it was taken under. Each trap below produced a number that looked like a regression or a win and was neither. | Trap | What it did | What to do | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | | Machine load | Parallel agent pipelines pushed the one-minute load average to between 100 and 370 on an 18-core machine in September 2026, and every timing read slow | Record `uptime` beside every timing, and take the before and after runs back to back under similar load | | A route's first compile | The first probe of a hero after the dev server compiled it read 21 fps with a 1.6 s frame; unthrottled repeats read 118 fps (2026-08-07) | Discard the first probe after a compile | | Lighthouse on a dev server | A run against the dev server in an everyday Chrome scored 0.65, and Lighthouse warned that extensions slowed the load (2026-08-05) | Run a production build in a clean profile, three runs a cell, and report medians with the range | | Cache replay | Playwright's `response` events replay memory-cache hits across loads in one context | Count requests in a fresh context with `Network.setCacheDisabled` | | Bot protection | About 200 automated loads put a machine behind Vercel's Security Checkpoint (2026-10-08) | Measure bytes against a local `next start` | | Software WebGL | A headless Chrome that renders WebGL in SwiftShader measures the CPU | Read the renderer string: headless Chrome on a Mac reads ANGLE Metal, and SwiftShader means rerun headed | | A paused pane | An embedded browser pane that reports `document.hidden` pauses `requestAnimationFrame` | Measure motion in a real Chrome ([Proving a visual change](https://www.prototemplate.com/docs/engineering/verification#the-in-app-browser-pane)) | Frame time comes from the frame probe in the gt-performance skill, run from a checkout of this repository, which has playwright-core: ```bash F=skills/gt-performance/scripts/frame-probe.mjs node $F http://localhost:3005/<path> --runs 3 # median, p95, worst, slow share, long tasks, contexts, load node $F <url> --cpu 12 --seconds 12 # a weak device node $F <url> --scroll --size 390x844 --dpr 3 # a phone, scrolling through the window node $F <url> --runs 3 --max-median 17.5 --max-contexts 1 # a budget: exits 1 on a breach ``` Lighthouse runs against a production build with a fresh profile, which the CLI launches itself: ```bash pnpm build && pnpm start mkdir -p lh CHROME_PATH="<path to Chrome>" \ npx -y lighthouse@12 http://localhost:3000/<path> \ --output=json --output=html --output-path=./lh/<name> # add --preset=desktop for desktop; mobile is the default # add --throttling-method=devtools for real throttling in place of the simulation ``` The [quality bar](https://www.prototemplate.com/docs/handbook/quality-bar) requires the frame probe before and after a change and the load average beside every timing. ## Field data against lab data [#field-data-against-lab-data] A complaint about a slow first paint is answered with field data. On 2026-09-14 Kevin flagged the first contentful paint and the server response time of generaltranslation.com's docs as slow. His numbers came from PageSpeed Insights field data, the Chrome UX Report's 75th percentile for the origin, which includes redirect time. A lab run of the same page on desktop scored 95 with a 0.3 s first contentful paint and did not answer the complaint. The production docs, read on 2026-09-25, paid for three things on every page: * The locale `not-found.tsx` was serialized into every docs page's React Server Components payload, together with the marketing 404's CSS and scripts. * The full sidebar tree was in the payload of every page. * The sidebar prefetched every link it rendered. The pull request was cut to the changes that carried the measured win: the heavy 404 moved under a catch-all route, sidebar prefetch moved to hover and touch intent, and Inter became one preloaded subset with the italic loaded on demand. Kevin removed a redirect map, a font split and the stripping of the sidebar tree from it as "doing too much for such little changes". Over three runs each, production read 73 to 75 on mobile Lighthouse and the branch 79 to 80. Two readings needed a cause before they could be trusted: * Lighthouse's simulated mobile LCP charges hydration JavaScript to the largest paint. A throttled real Chrome (`--throttling-method=devtools`) painted at 1,284 ms in every run, so mobile gains past that point need cuts to framework JavaScript. * A late font split one branch's score between 79 and 70. On localhost the italic file finished just before the observed first paint in five of seven runs, and the simulation charged it to the paint. On a Vercel site the cache is checked per variant and locale with one request each. The docs at generaltranslation.com pick their framework variant from a `gt-fw-pref` cookie: ```bash curl -sI -b 'gt-fw-pref=next' -w '%header{x-vercel-cache} %header{x-matched-path}\n' -o /dev/null \ https://generaltranslation.com/en-US/docs/overview/get-started ``` A concrete matched path with `HIT` is the goal. The record of the round is in the docs reference of the [gt-website skill](https://www.prototemplate.com/skills/gt-website), and [Docs site traps](https://www.prototemplate.com/docs/engineering/docs-traps#inline-css-built-and-reverted) has the experiment that was built and reverted. ## Bytes before scripts [#bytes-before-scripts] A first load that is slow on a fast machine is usually paying for bytes. A round on this site on 2026-10-08 measured every route cold under `next start` on a production build, in one fresh browser context per run, at 1440 by 900 and at 390 by 844 with touch, three runs each, and found the cost in bytes. Bytes are deterministic, and timings move with the load average. ```js // Playwright and CDP: encoded bytes by resource type for one cold load const context = await browser.newContext({ viewport: { width: 1440, height: 900 } }); const page = await context.newPage(); const cdp = await context.newCDPSession(page); await cdp.send('Network.enable'); await cdp.send('Network.setCacheDisabled', { cacheDisabled: true }); const types = new Map(); const bytes = {}; cdp.on('Network.responseReceived', (e) => types.set(e.requestId, e.type)); cdp.on('Network.loadingFinished', (e) => { const type = types.get(e.requestId) ?? 'Other'; bytes[type] = (bytes[type] ?? 0) + e.encodedDataLength; }); await page.goto('http://localhost:3000/', { waitUntil: 'networkidle' }); console.log(bytes); await context.close(); ``` | Cause | Fix | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | next/font preloads every face a module declares on every page that imports the module, and Turbopack shares CSS chunks between routes, so a display face used on one page loaded on all of them | Only the roman Inter preloads; every other `localFont` sets `preload: false`; a face used on one route gets a family name that only that route's CSS names | | A family re-pointed for one route leaked: a face joined to `inter` in blog CSS was fetched by the home page | Scope a route's rules under its root class | | React 19 hoists an eager `<img>` into a high-priority preload | `loading='lazy'` on every picture outside the first view | | A `<video poster>` is fetched at load wherever the video sits | Paint the poster as a lazy image and preload nothing until play | | Work for hover devices, such as preview preloads, ran on phones | Check `(hover: hover) and (pointer: fine)` first | | Data and panels used only after an open shipped with the page | `import()` on the first open, or on the first hover or focus of the trigger | | GSAP loaded on routes with no tween | Load it only where a tween runs | This site preloads one font file, the roman Inter's latin subset at 112,748 bytes, out of a 352,240-byte full roman. The other subsets and the italic (387,976 bytes whole) are `@font-face` rules with a `unicode-range` in the same family, so a browser fetches one only where a page draws a character in its range. The binding is in `src/lib/fonts.ts` and `src/app/inter-subsets.css`, and lint-type's T3 rule checks every next/font binding. [Type](https://www.prototemplate.com/docs/craft/type) covers the subsets. ## Shader and module weight [#shader-and-module-weight] Weight is reported as the source, the gzipped module and the tree-shaken bytes a page ships, with the number of route chunks that carry the module. Find the chunks by a string the minifier keeps and only that module holds, such as a GLSL uniform name: ```bash pnpm build for f in $(grep -l '<token>' .next/static/chunks/*.js); do printf '%s raw %s gzip %s\n' "$f" "$(wc -c < "$f" | tr -d ' ')" "$(gzip -9c "$f" | wc -c | tr -d ' ')" done ``` A cut to shader or module code is proven with a pixel diff of a deterministic frame. Reduced motion renders one such frame for every engine that follows the lifecycle contract. Capture the baseline twice and confirm the two match at zero, because a baseline that differs from itself proves nothing: ```bash P=skills/gt-performance/scripts/pixel-diff.mjs node $P capture http://localhost:3005/<path> --out before.png node $P capture http://localhost:3005/<path> --out before-2.png node $P compare before.png before-2.png # must read 0 channels # make the change, then node $P capture http://localhost:3005/<path> --out after.png node $P compare before.png after.png --diff diff.png # exits 1 on any difference ``` [Canvas and WebGL engines](https://www.prototemplate.com/docs/engineering/engines#shader-source-weight) has the measured cuts, the `#version` trap and the residue a GPU driver leaves. ## Every dependency on its own merits [#every-dependency-on-its-own-merits] A package added for one feature is judged alone, and each one that stays carries a one-line comment saying why. Three packages arrived on generaltranslation.com for one feature in August 2026, and Kevin answered that he would "prefer to not have package bloat": | Package | Verdict | Reason | | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | | cheerio | Replaced | A small tag scanner, verified byte-identical against an 11-case reference harness and the live results; ten packages left the lockfile | | franc | Kept | Nothing else in the repository detected language | | undici | Kept | Its DNS-pinning `Agent` is missing from the global `fetch` | The rule sits in section 5 of the gt-performance skill, with the ledger to give when a weight target is out of reach without loss. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Lottie on canvas > Eight lottie-web canvas traps, each proven, with the before and after numbers and the recipe that proves a fix. URL: https://www.prototemplate.com/docs/engineering/lottie The canvas renderer of lottie-web 5.13 draws a whole Lottie animation into one canvas element, where its SVG renderer builds a DOM node for every shape. In October 2026 a figure moved from the SVG renderer to the canvas renderer. It plays one animation in several translated versions and compares two of them under a draggable seam. The move met eight traps, and each was proven by a render or a measurement before its fix went in. The traps and their proofs are recorded in the [gt-motion skill](https://www.prototemplate.com/skills/gt-motion). ## A missing glyph stops the frame [#a-missing-glyph-stops-the-frame] A translated animation drew nothing after one of its text layers, and the console stayed empty. A Lottie file with glyph-based text lists every character it can draw. For a character missing from that list, lottie-web's `FontManager.getCharData` returns a placeholder with an empty `shapes` array, `CVTextElement.buildNewText` reads `shapes[0].it` and throws, and no layer after it draws. The SVG renderer skips such a character, so the bug appears only on canvas. The error surfaces as the animation's `error` event and never in the console. Two inputs trigger it: a `\r` used as a line break with no glyph entry, and the `\r` that lottie-web writes where it wraps box text. A longer translation wraps where the source did not, so it breaks where the source played. The fix gives every character the text can draw an empty entry, plus `\r`, `\u0003` and the capitals that all-caps text draws: ```ts type Glyph = { ch: string; style: string; fFamily: string; size: number; w: number }; function fillGlyphs(chars: Glyph[], text: string, style: string, fFamily: string): void { const known = new Set(chars.map((g) => `${g.ch}\u0000${g.style}\u0000${g.fFamily}`)); for (const ch of new Set([...text, ...text.toUpperCase(), '\r', '\u0003'])) { const key = `${ch}\u0000${style}\u0000${fFamily}`; if (known.has(key)) continue; known.add(key); chars.push({ ch, style, fFamily, size: 0, w: 0 }); } } ``` An entry with no `data` draws nothing and advances nothing, which is what the SVG renderer did for the same character. ## Track-matte buffers are sized once [#track-matte-buffers-are-sized-once] After the canvas grew, everything under a track matte stayed clipped to the canvas's first size. The canvas renderer makes two offscreen buffers per matte layer at the canvas size when it builds the layer, and never resizes them. A canvas parked at 0 by 0 makes `drawImage` throw outside lottie-web's own try and catch. The fix sets every buffer to the canvas size after each resize and redraws once more: ```ts import type { AnimationItem } from 'lottie-web'; type MatteElement = { buffers?: { width: number; height: number }[] } | null; type CanvasInternals = { renderer: { elements?: MatteElement[] } }; function fitMatteBuffers(anim: AnimationItem, canvas: HTMLCanvasElement): void { const { renderer } = anim as unknown as CanvasInternals; for (const element of renderer.elements ?? []) { for (const buffer of element?.buffers ?? []) { buffer.width = canvas.width; buffer.height = canvas.height; } } anim.resize(); } ``` A hidden player parks at `resize(1, 1)`. The proof builds the figure at 700 px, widens the window to 1440 px and diffs the result against a figure built at 1440 px under reduced motion: identical with the fit, different without it. ## Subframes redraw at display rate [#subframes-redraw-at-display-rate] A playing animation redrew on every animation frame, 120 times a second in headless Chromium, although the file has far fewer frames. By default the player renders interpolated subframes. Calling `setSubframe(false)` floors the frame number, and the renderer then skips a frame it has already drawn. The `enterFrame` event still fires on every animation frame, so per-frame work runs only when `currentFrame` changes: ```ts anim.setSubframe(false); let lastFrame = -1; anim.addEventListener('enterFrame', () => { if (anim.currentFrame === lastFrame) return; lastFrame = anim.currentFrame; // work for a new frame only }); ``` ## Hidden layers that keep their box [#hidden-layers-that-keep-their-box] A layer hidden with the `hidden` attribute built its canvas at the wrong size. Tailwind 4's preflight sets `[hidden]:where(:not([hidden='until-found'])) { display: none !important }`, so a hidden layer has no box, and lottie-web sizes its canvas from the box when it builds. A layer that must keep its box while hidden takes a class with `visibility: hidden` instead of the attribute. ## Two players on one clock drift [#two-players-on-one-clock-drift] Two players meant to show the same moment, such as the shown language and the compared one, stayed up to a frame apart for good. Starting one player from another's `currentFrame`, which is floored once subframes are off, loses the fraction. Resyncing one player from inside the other's `enterFrame` advances it twice in that tick. The leader keeps the clock, and every follower stays paused and is set from the leader's raw frame: ```ts let lastFrame = -1; leader.addEventListener('enterFrame', () => { // enterFrame fires every animation frame even with subframes off if (leader.currentFrame === lastFrame) return; lastFrame = leader.currentFrame; for (const follower of followers) follower.goToAndStop(leader.currentRawFrame, true); }); ``` A newly shown player starts from the old one's `currentRawFrame`. ## The loop is a cut [#the-loop-is-a-cut] An animation whose last frame differs from its first jumps at every loop. The figure plays with `loop: false` and listens for `complete`. On `complete` it copies the canvas with `drawImage` into a still laid over the layer, restarts the animation at 0, and dithers the still away. The player draws frame `totalFrames - 1` before it fires `complete`, so the copy holds a full picture. ## A poster frame for reduced motion [#a-poster-frame-for-reduced-motion] Frame 0 of these animations is empty. Under reduced motion the figure holds a poster frame: the middle of the longest stretch where its strings are on screen. ## Canvas text costs JavaScript [#canvas-text-costs-javascript] On canvas, lottie-web replays the glyph paths of every character on every frame, so the cost of text is script time on the main thread. Lowering the canvas resolution barely helps, because the cost is in replaying the glyphs. ## Before and after [#before-and-after] The figure's heaviest animation measured at 4x CPU throttle while dragging the compare seam: | Measure | SVG renderer | Canvas renderer | | -------------------- | ------------ | --------------- | | Nodes under the seam | 27,800 | one canvas | | Seam drag | 5 fps | 56 fps | The seam writes an inherited custom property on its box, so every drag frame restyles the subtree under it; a canvas keeps that subtree to one element. [Canvas and WebGL engines](https://www.prototemplate.com/docs/engineering/engines#inherited-properties-on-a-moving-subtree) has that rule. ## The proof recipe [#the-proof-recipe] A renderer change is proven by pixels: 1. Load the SVG build and the canvas build of the same animation into one page, side by side, with autoplay off. 2. Step both with `goToAndStop(frame, true)` to the same fixed frames, including one with every string on screen. 3. Capture each container and diff the pair. A difference is a trap until it is explained. 4. For matte and resize fixes, build at 700 px, widen to 1440 px, and diff against a build at 1440 px under reduced motion. The pixel-diff script in the [gt-performance skill](https://www.prototemplate.com/skills/gt-performance) captures and compares in one command each. The dither step between two languages has a trap of its own, a mask image that counts as transparent while it decodes, and [Dither in practice](https://www.prototemplate.com/docs/craft/dither) covers it. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Localized routing > Locale guards, the gt-next Link and an agent prompt that must run unchanged: routing traps on a localized Next.js site. URL: https://www.prototemplate.com/docs/engineering/i18n-routing A localized Next.js site sends every request through a locale layer, and its routing traps show up as 500s, 404s, extra redirect hops and English strings on translated pages. The cases come from generaltranslation.com, which General Translation (GT) localizes with its own open-source library, `gt-next`, and from fitting that library into another Next.js site. The [gt-website skill](https://www.prototemplate.com/skills/gt-website) holds each rule with its date. ## Middleware order [#middleware-order] A locale prefix outside the roster reached a docs rewrite and answered 500. On 2026-09-04 `/en/docs/platform` failed while the roster held `en-US` and `en-GB`: a docs rewrite in the middleware returned before the internationalization step, so the docs source was asked for a page tree in a locale it does not have. Paths outside the docs never hit that rewrite and were canonicalized, and that difference pointed at the rewrite. The fix is an unsupported-locale guard as the first step, with every docs rewrite below it: | Prefix | Answer | | -------------------------------------------------------------------------------- | --------------------------------------- | | A roster tag | Passes, or 301s to its canonical casing | | A region variant of a published language, such as `fr-FR` when `fr` is published | 307 to the language | | Anything else | 301 to the default locale | Four more rules apply to the layer: * Redirects in `next.config.ts` run before the middleware, so a bare `/en/docs` takes two hops: one config redirect, then the guard. * A middleware matcher that skips dotted paths never sees `.md`, `.json` or asset URLs, so rules for those live in `next.config.ts` or the route. * Every branch that returns early carries the ad click id onto its response, because ad landing URLs point at docs pages and the redirect is the last chance to keep it. * A redirect for moved content deploys only after the moved pages exist. On 2026-09-03 301s shipped before the content they pointed at, and every old URL redirected to a 404. Every routing change passes a routing matrix before its pull request and again on production after the deploy. Each case is one GET with no redirect following, read as a status and a target, and the script exits with the number of failed cases: ```text # label | path | expected status | expected target (- for none, * for any) real page | /en-US/docs/react/react-quickstart | 200 | - plural stripped | /en-US/docs/quickstarts | 307 | /en-US/docs/overview/get-started ``` ```bash sh skills/gt-website/scripts/routing-matrix.sh --base http://localhost:3001 # before the pull request sh skills/gt-website/scripts/routing-matrix.sh # production, after the deploy echo $? # failed cases, 0 when all hold # one case by hand curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://generaltranslation.com/en/docs ``` Routing defects reached production three times in September 2026, before the matrix gated every routing change: 301s into 404s on 2026-09-03, the 500 on 2026-09-04, and a legacy redirect that sent a CLI page to a dead URL on 2026-09-25. The matrix holds 41 cases, and its last run against production on 2026-10-10 passed all of them. The script and its cases are public in the gt-website skill. ## Links, routers and strings in gt-next [#links-routers-and-strings-in-gt-next] The library's components and its string extraction have three traps that look like bugs elsewhere. | Symptom | Cause | Fix | | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------- | | Links to files at the app root (`/openapi.json`, `/sitemap.xml`, a `.md` twin) answer 404 | The library's `Link` adds the locale to every href that starts with `/` | A plain `<a>` for root files | | A programmatic navigation loses the locale | The library exports no router, and Next's `useRouter().push` drops the locale prefix | Push a path that already carries the locale | | Strings render in English in every locale | The `gt generate` command does not follow a `gt` destructured out of a `Promise.all` | Call `getGT()` on its own line | The extractor misses strings in this form: ```tsx const [posts, gt] = await Promise.all([loadPosts(), getGT()]); ``` and finds them in this one: ```tsx const gt = await getGT(); const posts = await loadPosts(); ``` One sweep of generaltranslation.com in October 2026 found 65 of 96 literals in 26 files missing from the source table, which grew from 739 to 799 entries once they were extracted. Fitting the library into another Next.js site in October 2026 met five more: * The config wrapper `withGTConfig` refuses a prop that conflicts with `gt.config.json`. Keep the locale roster in `gt.config.json` for the CLI, give `withGTConfig` a routing-only config file of its own, and pass the URL locales as props. * Importing `withGTConfig` into `next.config.mjs` fails with "require is not defined", because the library's ESM entry calls `require`. A `next.config.ts` works. * Static generation needs `experimental.rootParams`, a `getLocale` that reads `next/root-params`, a `getRegion` that returns `undefined`, and `enableSmartRouting: false`, so an unprefixed URL stays in the default locale. * Behind the proxy, `usePathname()` returns the internal path the default locale is served from. Client code that builds links or marks the current page maps it back to the public path; missing this broke the language menu on every default-locale page. Test the language switcher from default-locale pages as well as translated ones. * Translated MDX left the frontmatter untranslated on about 40 pages per locale, and `**` or `_` next to CJK text came back escaped. Check titles and descriptions per locale, and write that emphasis as `<strong>` and `<em>`. A hydration mismatch that appeared at random under `next dev` left the production build clean, so judge one on a production build before chasing it. The integration notes are in the gt-next reference of the gt-website skill. ## A start prompt that must run unchanged [#a-start-prompt-that-must-run-unchanged] A prompt that takes a coding agent from an empty project to a first translation is proven by handing it unchanged to a fresh agent on a fresh app until a real translation renders. The site at generaltranslation.com serves such a prompt at `/agent-prompt.md`, and the home page's Setup for Agents button copies it. Kevin Liu set the rule on 2026-09-30: "test it yourself". Two runs that September found nine defects in the first draft and none in the second. The prompt depends on these facts about the `gt` CLI, checked against version 2.23.1 on 2026-10-01: | CLI behaviour | What the prompt does | | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | The commands `gt init` and `gt configure` ask unanswered questions interactively and wait forever under an agent's terminal | Every step runs with `--no-interactive` and a flag for each answer | | With `--no-interactive`, a missing answer exits 1 and lists the missing flags | The agent reads the list and adds the flags | | There is no `--version` flag; each command's banner prints the version | The agent reads the banner | | A development key in `.env.local` makes `next build` fail | The key goes in `.env.development.local` | | The command `gt login --no-browser` prints the device URL | The agent can sign in without opening a browser | ```bash npx gt@latest init --no-interactive --json --defaults --default-locale en --locales fr \ --no-locadex --react-setup --file-formats none --no-dev-credentials --package-manager npm ``` Each run records whether it used an existing CLI login, which skips steps a new user meets, and a separate verifier grades the result and the wording. Any change to the prompt, the CLI or the quickstarts is followed by another run. The case is in the agent prompts section of the [gt-verify skill](https://www.prototemplate.com/skills/gt-verify). --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Dev servers and builds > How next dev fails under load and after large pulls, with the symptom of each failure and the one-line fix. URL: https://www.prototemplate.com/docs/engineering/dev-servers The Next.js 16 dev server fails in a few repeatable ways under load, after large pulls and when two processes share one build folder, and each failure has a symptom that names it. The cases come from this site's shared dev server, which several agent sessions use at once, and from the worktrees of generaltranslation.com, General Translation's site. The [gt-local-dev skill](https://www.prototemplate.com/skills/gt-local-dev) holds the order of checks when a change does not show. ## Failure modes of next dev [#failure-modes-of-next-dev] | Symptom | Cause | Fix | | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | | A second `next dev` for the same checkout refuses to start | Next.js 16 holds `.next/dev/lock` while a dev server runs | One server per checkout; another worktree runs its own server on its own port | | Every route answers 404 after a restart | Two `next dev` processes share one `.next` | Stop both by PID, `rm -rf .next`, start one | | Every route answers 500 with "Unexpected non-whitespace character after JSON" | Under load, with several sessions compiling routes at once, `next dev` leaves `.next/dev/prerender-manifest.json` half written | Stop the server by PID, delete `.next/dev`, start it again | | The server answers nothing after a large fast-forward (47 commits on 2026-10-09) | The dev server's state no longer matches the tree | The same restart | | The disk fills | A long-running shared server's `.next/dev` grew to 64 GB | Delete `.next/dev` at the restart | | A changed image still shows the old version | `next dev` keeps old `_next/image` variants | `rm -rf .next` and restart | | A cold image request hangs for 900 s or more and times out a page check | The image optimizer on a loaded dev server | Server state: retry once the load drops | | `next start` serves the old build after a rebuild reported success | A stale `.next` | Delete `.next` in the scratch tree before diagnosing | | Every docs page answers 404 after a merge, with nothing in the log | A dependency missing from the worktree (2026-09-24) | `pnpm install --frozen-lockfile` and a restart | | `Module not found` for a workspace package after its sources changed | The package resolves from a stale `dist/` | Build it, for example `pnpm turbo run build --filter='<app>^...'` | | A pending navigation lasts longer than in production | `next dev` never prefetches `<Link>` | Judge navigation timing on a production build | A server is ready when a real page returns 200 with no build error in its body. The first request to a route compiles it, so a status alone can pass while the page body holds the error overlay: ```bash curl -sL -o page.html -w '%{http_code}\n' http://localhost:<port>/<page> grep -c 'Module not found\|Build Error' page.html # a 200 with 0 here passes ``` Stop a server by the process that listens on its port: ```bash lsof -nP -iTCP:<port> -sTCP:LISTEN # who listens, with the PID kill $(lsof -ti tcp:<port> -sTCP:LISTEN) # stop that listener only ``` Without `-sTCP:LISTEN`, `lsof -ti` also lists a browser connected to the port, and the kill closes it. On a machine shared by several sessions, a process is stopped by its PID, and only by the session that started it. A kill by name or pattern, such as `pkill` or `killall`, stops other sessions' servers as well (2026-10-10). Section 2 of the gt-local-dev skill and section 8 of the [prototemplate skill](https://www.prototemplate.com/skills/prototemplate) hold the restart recipe for this site's shared server. ## Type checks and the scratch build [#type-checks-and-the-scratch-build] The production build never type-checks here, because `next.config.ts` sets `typescript.ignoreBuildErrors`, so `pnpm exec tsc --noEmit` is a gate of its own. The build gate runs in a scratch worktree, because the dev server owns the shared `.next`, and `tsconfig.json` includes the types the dev server generates under `.next/dev/types`. [Gates](https://www.prototemplate.com/docs/gates) has both commands and their order. ## Filtered installs and project references [#filtered-installs-and-project-references] Every deploy of generaltranslation.com failed at the start of October 2026 with `TS6306: Referenced project packages/email must have setting composite`. The app's `tsconfig.json` referenced a workspace package it never imported. The deploy installs with a filter (`pnpm i --filter=landing... --filter=. --frozen-lockfile`), which installs only the app and the packages it depends on, so the referenced package had no `node_modules`, and its own config could not resolve. The fix is that an app's `tsconfig.json` references only the packages the app imports. The trap is recorded in the deploys reference of the [gt-website skill](https://www.prototemplate.com/skills/gt-website). ## CI without the content submodule [#ci-without-the-content-submodule] The site at generaltranslation.com keeps its docs and blog posts in a separate content repository checked out as a submodule, and its CI never initializes that submodule. Code that reads content at build time returns null when the content is absent, and a test that reads real pages skips when the file is missing. The guard sits on the read itself, because `describe.skipIf` still runs its callback to collect the tests: ```ts import { existsSync, readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; const page = 'content/docs/en-US/overview/for-coding-agents.mdx'; const present = existsSync(page); describe.skipIf(!present)('the agent prompt page', () => { const source = present ? readFileSync(page, 'utf8') : ''; it('holds the fenced prompt', () => { expect(source).toContain('agent-prompt.md'); }); }); ``` A build-time read that returns null degrades its feature: on generaltranslation.com the Setup for Agents button becomes a link to the docs when the prompt cannot be read, so a docs edit never fails the site's build. The rule is in the pages reference of the gt-website skill. ## A minimum release age for dependencies [#a-minimum-release-age-for-dependencies] With `minimumReleaseAge: 2880` in `pnpm-workspace.yaml`, pnpm refuses any version published less than 2,880 minutes (two days) ago, so `pnpm add` can fail on an unrelated dependency that had a recent release. A full re-resolve to get past it churns lockfile lines that have nothing to do with the change. The fix adds the one dependency by hand: ```bash # add the dependency to package.json first, then pnpm install --config.minimumReleaseAge=0 git diff pnpm-lock.yaml # keep only the lines the new dependency needs, restore the rest pnpm install --frozen-lockfile # confirms the trimmed lockfile matches package.json ``` Restart any dev server that ran during the relink. Until then it fails with "Can't resolve 'gt-next/link'" or a similar import. Section 2 of the gt-website skill holds the steps. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # How Prototemplate is built > How the hub is built and changed: its routes and the deck, the skills, scripts and gates, the SEO system and the docs template. URL: https://www.prototemplate.com/docs/internals Prototemplate is a Next.js 16 site in one repository. This tab explains how its parts are built and what a change to each one touches. ## Routes [#routes] <Cards> <Card title="Pages and the viewer shell" href="/docs/pages"> The routes, the shell every hub page renders in, and the book page standard. </Card> <Card title="The design lab and the directions" href="/docs/directions"> The gallery at `/` , the `/d/` directions and the production mirror. </Card> <Card title="The brand deck" href="/docs/deck"> The 93 slides, the viewer around them and the build that serves `/deck` . </Card> </Cards> ## Tooling [#tooling] <Cards> <Card title="Skills" href="/docs/skills"> The curated GT skills, their contract and how they install. </Card> <Card title="Scripts and generated files" href="/docs/scripts"> The build scripts and the files they write. </Card> <Card title="Gates" href="/docs/gates"> The lints and checks a change passes before it lands. </Card> </Cards> ## Publishing [#publishing] <Cards> <Card title="The SEO system" href="/docs/seo"> Metadata, social cards, the sitemap and the agent files. </Card> <Card title="Use these docs as a template" href="/docs/template"> How this docs site is built on fumadocs, and how to reuse it. </Card> </Cards> ## The canon behind it [#the-canon-behind-it] <Cards> <Card title="Architecture" href="/docs/handbook/architecture"> How the repository is laid out and why. </Card> <Card title="Ship loop" href="/docs/handbook/ship-loop"> How a change moves from a worktree to production. </Card> <Card title="Tools" href="/docs/handbook/tools"> Every script in the repository and what it does. </Card> </Cards> ## What a change touches [#what-a-change-touches] A page, a document or a skill appears in several hand-kept lists, and a change updates every list that names it in the same commit. Each row below gives the files a kind of change touches and the check that fails when one is missed. `pnpm lint:static` runs every lint named in the last column, and the [prototemplate skill](https://www.prototemplate.com/skills/prototemplate) gives each list step by step. | Change | Files and registries it touches | What catches a miss | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | A page on the shell | `src/app/<route>/page.tsx` with its head from `seoRoute()`; a row in `PAGES` or `KNOWLEDGE` of `src/lib/surfaces.ts`; `PAGE_ICON` in `src/components/viewer/Sidebar.tsx`; `PAGE_ICON` and `PAGE_KEYWORDS` in `src/lib/search-index.ts`; `under` on each of its sections; its paths in `scripts/build/updated.mjs`; a row in `siteRoutes()` of `scripts/lib/site-pages.mjs`; the route list in `README.md`; its captures and thumbnails under `public/shots/` | `pnpm lint:registries` checks the surface row against the sitemap, the route list, both icon maps, the keywords and the thumbnail. `pnpm lint:seo` fails a head that does not come from `seoRoute()`, and `pnpm lint:updated` a stale `src/lib/updated.ts`. | | A docs page | `<slug>.mdx` in a tab's folder or folder group, that folder's `meta.json`, its row in `DOCS_PAGES` (`src/components/docs/pages.ts`), `pnpm build:updated`, and a `docs-<slug>` capture with a folder's slash as a hyphen | `pnpm lint:registries` follows every `meta.json` from the root and fails when `DOCS_PAGES` differs from the files in order, title or description, or when an `.mdx` file has no row. | | A handbook document | `docs/handbook/<slug>.md`; a `HANDBOOK` entry in `src/app/handbook/registry.ts`; its page under `content/docs/(guide)/handbook/` with the include tag, placed in that folder's `meta.json` in the registry's order; its h2s in `HANDBOOK_HEADINGS` of `src/lib/search-index.ts`; its row in `docs/handbook/README.md` and the handbook list in `AGENTS.md`; a `handbook-<slug>` capture | `pnpm lint:registries` fails a heading missing from `HANDBOOK_HEADINGS` or a row with no heading, and a page whose title, description, include tag or order differs from the registry. The build fails when a page that includes its file by headings leaves one out. | | A skill | `skills/<slug>/SKILL.md` to the contract, with `references/sources.md` citing every line; its row in the Skills table of `README.md`; its slug in `ORDER` of `scripts/build/skills.mjs`; then `pnpm build:skills`, which writes `src/lib/skills.ts` and `skills/README.md`, and the installer's dry run | `pnpm lint:skills` fails a skill off the contract, a body over 24,000 bytes and a stale `src/lib/skills.ts` or `skills/README.md`. `pnpm lint:registries` checks the contract too. | | A direction | An entry in `DIRECTIONS` of `src/lib/directions.ts`, its route under `src/app/d/<slug>/`, its first fold captured in both themes under `public/shots/light/` and `public/shots/dark/`, then `pnpm build:thumbs` | `pnpm lint:registries` checks the expression that finds the first exploration, which every browser gate reads. The page check covers the route. | | A copy of a gt-cloud file | The copy, and its entry in `scripts/lint/copies.json` as `pin`, `source` or `fork` | `pnpm lint:copies` compares a pinned copy with its recorded sha256. | | A generated file | Its script, never the file: `pnpm build:skills`, `build:deck`, `build:marks`, `build:inter`, `build:thumbs`, `build:motion`, `build:tools` or `build:updated`. `pnpm gen:all` runs them in order and dates pages last. | `pnpm lint:skills`, `pnpm lint:tools` and `pnpm lint:updated` exit 1 while `src/lib/skills.ts`, `docs/TOOLS.md` or `src/lib/updated.ts` is stale, and the build checks `src/lib/updated.ts` again. | --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Pages and the viewer shell > Every route and what it holds, the viewer shell that frames most of them, the book page standard, and the registries a new page touches. URL: https://www.prototemplate.com/docs/pages ## The routes [#the-routes] | Route | What it holds | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `/` | The design lab: the directions read as an article, one live exhibit at a time, or as a grid of captures | | `/brand` | The brand book, the identity canon in ten sections | | `/docs` | These docs, in five tabs: the Guide with the handbook, Components with the blog globe figure, Craft, Engineering and Internals | | `/deck` | The brand deck, served as its own document | | `/present` | The presenter: a full-screen walkthrough of the redesign with the first 16 prototypes live | | `/compare` | Two directions side by side in scroll-synced frames | | `/skills`, `/skills/<slug>` | The curated skills, one page each, with the raw files beside them | | `/docs/handbook`, `/docs/handbook/<slug>` | The handbook and the canon, a section of these docs; `/handbook` and `/handbook/<slug>` answer 308 with them | | `/marks` | The mark explorations, each drawn in one color | | `/blog`, `/blog/<slug>` | The docs-redesign series as GT published it | | `/graphics` | Every illustration of the series, by area | | `/motion`, `/motion/<slug>` | The film roster and the research packages of the translation series | | `/directions/<slug>` | One direction presented with its captures | | `/archive/<slug>` | A retired direction, kept as a full-page capture | | `/d/<slug>` | The directions themselves, self-contained | ## The viewer shell [#the-viewer-shell] `ViewerShell` in `src/components/viewer` frames every route except `/deck`, `/present`, `/blog`, `/docs` and the `/d/` pages. Kevin made the deck's viewer the frame for the site on 2026-09-08. The shell owns the state and draws the chrome: the sidebar, the 52 px toolbar, the stage, the index panel, the search, the help card, the toast and the hover preview. A route renders its content as the shell's children. * The theme is `html[data-theme]`, dark when nothing is saved, stamped by a script in the root layout before first paint. Dark mode is a remap of the tokens in `src/components/viewer/tokens.css`. These docs read the same attribute. * Shell code runs mount work in `useMountEffect` and dependency work in `useLayoutWork`. `pnpm lint:practices` fails a new bare `useEffect`. * Chrome draws its colors from `tokens.css`, its corners from the radius tokens and its icons from the Heroicons 20 solid set in `icons.tsx`. The docs sit outside the shell so that they are the same docs site generaltranslation.com runs, and so that the folder can be lifted into another project ([Use these docs as a template](https://www.prototemplate.com/docs/template)). The shell's sidebar keeps a Docs row that opens them, and the docs sidebar links back. ### State before first paint [#state-before-first-paint] The root layout (`src/app/layout.tsx`) runs a small inline script before the first paint. It stamps `html[data-theme]` from the saved `gt-theme` choice, dark when nothing is saved, so the first frame is in the right theme and the shell's check after hydration agrees with it. It also stamps the saved list state, `gt-shell-sb` and `gt-shell-density`, as `data-shell-sb` and `data-shell-density`. `ViewerShell.css` reads those attributes until the shell has settled, so a reader who closed the list or chose thumbnails sees that layout in the first frame, and hydration never shifts it. * The mirror under `/d/production` follows `prefers-color-scheme`, as generaltranslation.com does, until the visitor picks a theme. It marks the root `data-theme-from='system'`. * A same-origin frame, such as a gallery exhibit or a compare pane, follows the parent's toggle through the `storage` event with no code of its own. In a private window, where storage cannot carry it, the parent also posts `{ type: 'gt-theme', theme }` to every frame. * The root layout also installs the rAF gate: a parent can freeze a framed page's animation loops with `postMessage({ type: 'gt:freeze', frozen })`, and the queued callbacks run on resume. ### Effects under strict mode [#effects-under-strict-mode] React strict mode, which `next.config.ts` turns on, runs an effect's setup, its cleanup and its setup again on mount in development. A plain "ran once" guard would skip the second setup and leave the effect torn down, so every listener it registered would be dead in development. `useMountEffect` (`src/lib/use-mount-effect.ts`) defers its cleanup by one task: the simulated second run lands first and cancels it, and a real unmount, which has no second run, lets it fire. An effect still removes every node it creates, and a check in development finds exactly one canvas after mount and after a hot reload. [Canvas and WebGL engines](https://www.prototemplate.com/docs/engineering/engines#strict-mode-and-effects) records the duplicate canvas that strict mode left behind and the alternatives to `useEffect`. ## The book page [#the-book-page] Every page with a book head renders one structure, set out in [DESIGN.md section 4](https://www.prototemplate.com/docs/handbook/design): `BookHead` draws the title from `PAGE_NAMES` in `src/lib/page-names.ts`, a lead of one to three lines, a panel with Updated first and three facts after it, then one hatch band. Updated comes from `src/lib/updated.ts`, which `scripts/build/updated.mjs` generates from each page's last commit. `pnpm lint:heads` holds the structure in the source and `pnpm lint:heads:live` in the rendered page. ### The book page's numbers [#the-book-pages-numbers] Every space of the book page is a token in `src/components/viewer/tokens.css`, and `BookView.css` and `Sheet.css` read them. The rule S6 of `pnpm lint:heads` fails a book stylesheet that writes the literal pixel value where its token belongs, so a change to the standard is one edit. The values in brackets apply under 900 px wide, and on a phone on its side. | Token | Value | What it measures | | -------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------ | | `--pt-title-clear` | 44 px (24) | The stage's top, under the toolbar's rule, to the h1's box. Nothing is drawn in it, and no line crosses a title. | | `--pt-d1` | 44 px (32) | The page title, at line height 1.04 and tracking -0.025em | | `--pt-head-gap` | 14 px | The h1's box to the lead's box | | `--pt-head-col-gap` | 56 px | The lead's column to the panel | | `--pt-head-panel` | 304 px | The panel's column. Its longest value, a date with its hint such as "Sep 10, 2026 4 weeks ago", measures 168 px. | | `--pt-row` | 26.35 px | One line of the lead, 17 px at line height 1.55, and the panel's row pitch, so every fact sits on a baseline of the lead | | `--pt-head-rule-pad` | 26 px | The taller of the lead and the panel to the mast's rule | | `--pt-band-h` | 42 px | The hatch band, both of its rules included | | `--pt-sec-over` | 48 px (40) | The block above a rule to the rule: the contents to the band, a section to the next divider | | `--pt-sec-pad` | 30 px | A rule to the h2's box under it | | `--pt-book-gap` | 32 px (24) | Between the blocks of a book column | | `--pt-col-pad-x` | 56 px (16) | The reading column's side padding | `pnpm lint:heads:live` measures the result on the rendered pages at 1440 by 900 and 390 by 844 in both themes. At 1440 the mast's rule under a one-line title sits 193.16 px below the title's top on every page. The panel's four rows, 4 by 26.35 or 105.4 px, are the tallest part of that sum, so the panel sets where the rule falls. Every section's h2 sits 35.5 px under its rule at 1440 and 79 px at 390. A page whose numbers differ fails the run. ## The registries a new page touches [#the-registries-a-new-page-touches] A page appears in several lists, and a change updates each list that names it in the same commit: * `src/lib/surfaces.ts`: the sidebar's site map, the index panel and the preview layer. * `src/lib/search-index.ts`: the search palette, with the icon and keywords of each page and the headings of each handbook document. * `src/lib/page-names.ts`: the page's plain name. * `scripts/build/updated.mjs`: the paths whose last commit dates the page, which also give its `lastmod` in the sitemap. * `siteRoutes()` in `scripts/lib/site-pages.mjs`: the route list every browser tool reads. * The page's own head, from one `seoRoute()` call ([The SEO system](https://www.prototemplate.com/docs/seo)). `pnpm lint:registries` reports the lists a change missed. The [prototemplate skill](https://www.prototemplate.com/skills/prototemplate) gives the checklist file by file. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # The design lab and the directions > The 27 directions under /d: the explorations, the three site concepts, the production mirror, and how the gallery presents them. URL: https://www.prototemplate.com/docs/directions The design lab holds twenty-seven directions for the General Translation website. Each one is a built page that runs live. * **23 explorations**: single-page directions, each a different answer to the same brief. * **3 site concepts** on the Toolchain foundation: Dossier, Orbit and Signal, each a home page plus an enterprise page. * **The production mirror** at `/d/production`: the site that shipped at generaltranslation.com, rebuilt page for page, with the dashboard's sign-in and onboarding states (`/d/production/signin`, `/onboarding`, `/consent`, `/device` and `/cli`; `?state=<id>` opens any state). `src/lib/directions.ts` is the registry: the slug, the name, the concept and the tone of each. The gallery, the presenter, `/directions/<slug>`, the sitemap and the sidebar's Sites and Explorations rows all read it. ## Self-contained by design [#self-contained-by-design] Each direction lives under `src/app/d/<slug>` with its own type, colors and stylesheets. The shell's rules do not reach it, and the type lint skips the files its allow list names. A direction can be read, changed or deleted without touching another. `src/app/d/toolchain` is the single source the three site concepts fork. A fork imports Toolchain's sections and re-skins them by rescoping its root class, and it never edits Toolchain's files. [ARCHITECTURE.md](https://www.prototemplate.com/docs/handbook/architecture) explains the rule. ## Forking Toolchain [#forking-toolchain] 1. Give the fork's root element its own class, such as `.terminusboard-root`, and set the fork's tokens on it. 2. Import Toolchain's sections, diagrams and components from `@/app/d/toolchain/...`. Imports run one way, from a fork to the source, so a fix in Toolchain reaches every fork. Canvas and shader engines that several forks share live once in `src/lib`. 3. A component whose stylesheet is scoped by its own class (`.tflow`, `.eg`, `.iso`) and reads `var(--tc-*)` with fallbacks needs nothing more. A stylesheet scoped to `.toolchain-root` needs a rescoped copy in the fork, because its selectors match nothing under another root. 4. Dark mode is a token remap under `[data-theme='dark'] .<fork>-root` and nothing else. A plate that is always dark never remaps, and a canvas reads its ink from a CSS variable and reads it again when `html[data-theme]` changes. 5. Change Toolchain itself only when the change is meant for every fork. Terminus Board shows step 3. It imports `LocaleTag` from Toolchain, and the tag's rules live in `toolchain/components/icons.css` under `.toolchain-root`, so none of those rules would match on the fork. The fork carries the same rules under its own root in `terminus-board/sections/locale-icons.css`: ```css /* before: src/app/d/toolchain/components/icons.css, matches only under Toolchain */ .toolchain-root .lct { display: inline-flex; align-items: baseline; gap: 4px; font-style: normal; } /* after: src/app/d/terminus-board/sections/locale-icons.css, the same rule rescoped */ .terminusboard-root .lct { display: inline-flex; align-items: baseline; gap: 4px; font-style: normal; } ``` On 2026-10-08 the frozen local copies of Toolchain sections that two forks still carried were deleted, since nothing imported them. Before adding a copy, import the source and rescope only the CSS. ## How the hub presents them [#how-the-hub-presents-them] * `/` reads the directions as one article, shows one live exhibit at a time, or lays them out as a grid of captures. * `/directions/<slug>` presents one direction on the shell: the prototype in a frame, its captures in both themes, and what it was for. * `/compare` runs two directions side by side in scroll-synced frames. * `/present` is the full-screen walkthrough with the first 16 prototypes live. * `/archive/<slug>` keeps a retired direction as a full-page capture with the commit that last held its code. ## Keeping the mirror in step [#keeping-the-mirror-in-step] `/d/production` follows generaltranslation.com. After a gt-cloud pull request changes a page, the change is ported here: one root class per page, the gt-cloud stylesheets as rescoped copies, and every copy recorded in `scripts/lint/copies.json`, which `pnpm lint:copies` checks. ### The port recipe [#the-port-recipe] 1. Put the whole port under one root class on its root element, and scope every rule it adds under that class. 2. Copy each gt-cloud stylesheet the page uses whole, then transform it: its source roots (`.enterprise-root`, `.toolchain-root`) become the port's root, a doubled root collapses to one, `--color-emphasis` becomes `--tc-accent`, and `!important` is stripped. A mirror holds nothing else, so a re-sync replaces it in one copy. 3. Put everything the port adds, such as bridges to the hub's tokens and explicit form margins, in one compatibility sheet, `port-compat.css`. Mirrors are never edited by hand. 4. Pass each section file through the same transform: `gt-next` is stripped, so `<T>` and `gt()` become plain strings; `LocaleFlag` becomes `flag-icons`; and `Cta` is ported locally with link shims. 5. Record the copy in `scripts/lint/copies.json` in one of three modes: `pin`, where gt-cloud is the source and the copy is frozen at a sha256; `source`, where this repository is the source; or `fork`, where the copies differ on purpose and `from` and `reason` say why. Two traps meet every port. Tailwind's margin utilities are dead on a ported route, because the family's unlayered reset outranks Tailwind's utilities layer, so margins go in the compatibility sheet. Lightning CSS folds a standalone `scale` into `transform`, so a hover-scale reset happens in the markup. ## Search engines [#search-engines] The directions reassemble GT's marketing copy, prices and legal text into design studies, so every `/d/` page is `noindex, follow`. One layout, `src/app/d/layout.tsx`, sets it for the whole folder. The mirror's pages name their generaltranslation.com page as the canonical, since the live page is the original. The `/directions/<slug>` pages hold original writing and captures, and they are indexed. [The SEO system](https://www.prototemplate.com/docs/seo) gives the reasons. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # The brand deck > The brand deck in deck/: its slides on a 1600 by 900 sheet, the grammar they follow, and the build that serves it at /deck. URL: https://www.prototemplate.com/docs/deck The brand deck is General Translation's identity in 93 slides. Its sources live in `deck/` and its built file is `public/brand-deck.html`. ## The sheet and the grammar [#the-sheet-and-the-grammar] * Every slide is one HTML file in `deck/slides/`, drawn on a 1600 by 900 sheet that scales to the window. * `deck/parts/` holds the head, the tail and the shared styles: the Inter type sizes, the color tokens for both themes and the layout classes. * Icons come from the Heroicons 20 solid sprite. Mood slides carry dithered artifact pictures ([Artifact pictures](https://www.prototemplate.com/docs/handbook/artifact-pictures)). * `deck/DECK-GRAMMAR.md` is the written grammar: what a slide may hold and how a story is told across slides. The deck's heading rule in `deck/parts/head.html` tracks h1 and h2 at -0.025em and writes `font-feature-settings: 'cv11', 'ss01'` without `liga` and `calt`. Chrome turns contextual alternates off under any letter-spacing unless the feature list names `calt`, so the deck's headings lose the `->` arrow, the colon centred between digits and the hyphen raised between capitals. The type audit of 2026-10-05 named this the deck's one type defect. `head.html` changes only on its owner's instruction, so the fix waits for the deck's owner. [The correct Inter in practice](https://www.prototemplate.com/docs/craft/type#contextual-alternates-under-letter-spacing) gives the measurement and the two feature tokens the hub uses instead. ## The viewer [#the-viewer] The viewer around the sheet follows the one Turboslide draws for the same deck at [turboslide.com/deck/gt-brand](https://www.turboslide.com/deck/gt-brand), the editor the deck grew into. * The list is 256px wide with thumbnails and 208px in outline density. Its nameplate links to the Prototemplate home at `/`, a filter row narrows the slides by title or number, and each section folds under its head. * The toolbar holds Previous and Next around the slide counter, the deck's name, the Slide, Grid and Book switch, Surfaces, Theme, a Turboslide link that opens the deck there, and Present. When the bar runs out of room, `tighten()` in `deck/parts/tail.html` drops labels in three steps. ## The build [#the-build] `pnpm build:deck` (`scripts/build/deck.mjs`) assembles the slides into `public/brand-deck.html` and writes each picture to `public/deck-assets/` under a name made from a hash of its bytes, so a changed picture never shows its old bytes from a cache. The build also writes the deck's head: its title, its description, its canonical (`https://www.prototemplate.com/deck`), `og:url` and its social card. ## Where it is served [#where-it-is-served] * `/deck` is a rewrite to `public/brand-deck.html` in `next.config.ts`. A link to `/deck` loads it as a document (`src/lib/document-routes.ts`), since a router navigation would fetch the whole file once as a page payload and again as a document. * `/brand-deck.html` answers the same file with `X-Robots-Tag: noindex`, so only `/deck` is indexed. ## Who changes it [#who-changes-it] The onboarding and dashboard session owns `deck/` and pushes its changes from its own worktree. Another session that needs a slide changed asks that session by name. The [gt-deck skill](https://www.prototemplate.com/skills/gt-deck) holds the procedure for adding, editing and checking a slide. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Skills > The curated agent skills in skills/: the contract each one keeps, the build that checks it, where they are served and how to install them. URL: https://www.prototemplate.com/docs/skills The skills record how General Translation work is done. Each is a folder under `skills/<slug>/` in the SKILL.md format that Claude Code, Codex and other agents load. The set is curated by use: twenty-two skills, each one Kevin's sessions actually load. ## The contract [#the-contract] * **Frontmatter.** `name` (the folder's slug), `description` (what the skill covers, then "Use when", at most 1024 characters) and a `metadata` block of strings: `title`, `areas` (the first files the skill on `/skills`), `updated`, `origin` and `owner`. * **Owner.** The session lane that keeps the skill and reviews every change to it: `P` for the Prototemplate session, `V` for the Videos session and `O` for the onboarding and dashboard session. * **Body.** An h1 equal to the title and a short lead, the sections, a Review checklist where the skill governs work someone reviews, a Related skills line, and `## Sources` last. The body stays under 24,000 bytes; detail moves to `references/`. * **Supporting files** are `.md`, `.mjs`, `.json`, `.py`, `.sh`, `.txt` or `.js`. A script carries a header with its purpose, usage and requirements, and an offline test that `pnpm test:skill-scripts` runs. A skill over the 24,000-byte body budget fails `pnpm lint:skills` unless `scripts/build/skills.budget.json` exempts it with a written reason and an expiry date. An entry that has expired, has no reason, or names a skill already under the budget also fails, so the list only shrinks, and the commit that trims a skill removes its entry. Trimming moves a rule into `references/` and never deletes it. `pnpm build:skills` (`scripts/build/skills.mjs`) checks the contract and writes `src/lib/skills.ts`, the registry the pages read, and `skills/README.md`, the list by area. `pnpm lint:skills` fails while either is stale. ## Where they are served [#where-they-are-served] * `/skills` lists the set by area; `/skills/<slug>` renders one skill with its files and its install line. * `/skills/<slug>/SKILL.md` and `/skills/<slug>/references/<file>` serve the raw files. Each skill page names its SKILL.md as its Markdown twin. * `/skills/index.json` lists the whole set with every file and install line. ## Installing them [#installing-them] From a checkout, `pnpm skills:install --project <dir>` links every skill into `<dir>/.claude/skills` and `<dir>/.agents/skills`. Name slugs to install fewer, add `--copy` to vendor the folders, `--user` to install into the agent home, and `--dry-run` to see each step first. This repository's own `.claude/skills` and `.agents/skills` hold relative links made the same way. ## Slug collisions [#slug-collisions] A skill's folder name is its slug, so a skills folder holds one skill per slug. An install that writes over an entry of the same name replaces a skill the project already depends on, and nothing reports it. The installer and the set's naming rule prevent that. * The installer owns an entry only when it is a link that resolves to the canonical `skills/<slug>/` folder, or a folder whose SKILL.md carries `origin: prototemplate`. It replaces its own entries in place. * Any other entry with the same name is left alone and counted as refused, and the run exits 1. `--force` moves that entry aside to `<slug>.replaced-<timestamp>` before it installs, and never deletes it. * The home-folder guard resolves each target folder to its real path. When the path lies inside another git work tree, as it does on a machine whose agent home links into a wiki checkout, the installer refuses to write there unless `--into` names that folder. * No slug in the set exists in the agent home folders (`~/.claude/skills`, `~/.agents/skills`) or in gt-cloud's `.agents/skills`. A new slug is checked against all three before it is added, and `--dry-run` shows every action before anything is written. ## Kept by use [#kept-by-use] `pnpm skills:usage` counts how often each skill is loaded over the last thirty days. Thirty days after distribution, a skill with no loads is cut unless Kevin keeps it by name. The [prototemplate skill](https://www.prototemplate.com/skills/prototemplate) holds the full contract and the steps for adding one. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Scripts and generated files > The scripts/ folders, the files the scripts generate, the rule that only a script changes them, and the one route list every browser tool reads. URL: https://www.prototemplate.com/docs/scripts Every command runs as `pnpm <name>`, and [the tools index](https://www.prototemplate.com/docs/handbook/tools) lists each one with what it does. Skills and documents name commands, never script paths, so a script can move without breaking them. Every script prints its usage with `--help`. ## The folders [#the-folders] | Folder | What it holds | | ---------------- | -------------------------------------------------------------------------------------------------------------- | | `scripts/lib` | Shared code: the repository root, the help printer and `site-pages.mjs`, the route list | | `scripts/lint` | The lints and their tests: lines, type, radius, heads, practices, registries, copies, public, pictures and SEO | | `scripts/check` | The page check (`pagecheck/`), the captures and the gallery shoot | | `scripts/build` | The generators: skills, deck, marks, the Inter subsets, thumbnails, motion, tools, updated dates | | `scripts/skills` | The skill installer, the usage counter and the skill-script test runner | | `scripts/media` | The artifact picture cutter, `mood-tone` | ## Generated files [#generated-files] These files are committed, so the site builds from them alone, and they change only through their scripts: | File | Written by | | ------------------------------------------------------------- | ----------------------------------------------------------------------- | | `src/lib/updated.ts` | `pnpm build:updated`: each page's last change, from git | | `src/lib/skills.ts`, `skills/README.md` | `pnpm build:skills` | | `docs/TOOLS.md` | `pnpm build:tools` | | `public/brand-deck.html`, `public/deck-assets/` | `pnpm build:deck` | | `public/marks/` | `pnpm build:marks` | | `public/fonts/inter/`, the social card faces in `src/app/og/` | `pnpm build:inter` | | `public/shots/thumb/` | `pnpm build:thumbs`, from the captures `pnpm capture:pages` takes | | `src/lib/motion.ts` | `pnpm build:motion`, run only when the Videos session hands a film over | `pnpm gen:all` runs them in this order: skills, deck, marks, Inter, thumbnails, motion (skipped where the full `motion/` folder is absent), tools, then updated dates last, since it dates the changes the others leave uncommitted. A merge runs it and commits what it writes. ## The route list [#the-route-list] `siteRoutes()` in `scripts/lib/site-pages.mjs` is the one list of routes every browser tool walks. Each row names its route, its source folders and its tools: `check` (the page check), `live` (the live lints), `lines` (the line audit of the shell) and `capture` (the thumbnails). The page check, `lint:lines:shell`, the live lints and the captures all read it, so a new page is added in one place. ### First slugs read by regex [#first-slugs-read-by-regex] Five rows of the route list point at a page whose slug comes from data: the first skill, the first motion package, the first archive entry, the first exploration and the newest post. `siteRoutes()` reads each slug from its source file with a regular expression, so the list follows the data with no second registry to keep. | Route | Read from | What the expression matches | | ------------------------ | ------------------------------ | ----------------------------------------------------------------------- | | The first skill | `src/lib/skills.ts`, generated | the first `id: '<slug>'` after `export const SKILLS` | | The first motion package | `src/lib/motion.ts`, generated | the first quoted string after `export const MOTION_PACKAGE_SLUGS` | | The first archive entry | `src/lib/archive.ts` | the first `entry('<slug>'` | | The first exploration | `src/lib/directions.ts` | the first `slug: '<slug>'` in a `DIRECTIONS` entry without `site: true` | | The newest post | `content/blog/*.mdx` | the latest `date:` in the frontmatter | The expressions read single quotes and exact field names. A generator that changes its output shape, by writing double quotes or renaming a field, breaks every browser gate at once: the page check, `lint:lines:shell`, the live lints and the captures all start from this list. `firstSlug()` throws when a file holds no match, so a renamed registry fails the run with the file's name instead of auditing a 404 page as if it were clean. Check 3 of `pnpm lint:registries` runs the four registry expressions in `lint:static`, so the break shows before any browser starts. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Gates > The checks that run before work lands: the static lints, the live audits, the page check, the type check and the build, with their exit codes. URL: https://www.prototemplate.com/docs/gates Work lands on `main` only after it passes these gates and Kevin has reviewed it on localhost. The [gt-lints skill](https://www.prototemplate.com/skills/gt-lints) explains each lint and how to fix a failure. ## The gates, in order [#the-gates-in-order] 1. **`pnpm lint:static`**: every lint and test that needs no server. It chains the shell, practices, type, radius, heads, skills, registries, updated, tools, copies, public, SEO, oxlint and picture checks, then their tests. It is the gate every lane runs. 2. **`pnpm exec tsc --noEmit`**: the type check, three to five minutes. `next build` never type-checks (`typescript.ignoreBuildErrors` is on), so this is a gate of its own. 3. **`pnpm lint:all`**: `lint:static`, then the live audits against a running server at `PT_BASE` (default `http://localhost:3005`): `lint:lines:shell`, `lint:type:live`, `lint:radius:live` and `lint:heads:live`. 4. **`pnpm check:pages --preset quick --pages <ids>`** on the touched pages: eight devices in the dark theme, from phones to the ultrawide, and 1440 by 900 in the light theme, with every interaction run. A page passes with zero defects. 5. **`pnpm build`** in a scratch worktree, chained with `&&` and never piped, so a failing step stops the chain. The build runs the key scan and the static lints that guard a deploy, checks `src/lib/updated.ts`, runs `next build`, then `node scripts/lint/seo.mjs --built` over every prerendered page. ```bash git worktree add --detach ../scratch-build HEAD cd ../scratch-build && pnpm install --frozen-lockfile && pnpm build > build.log 2>&1; echo $? ``` A pipe reports the status of its last command. On 2026-08-11 `pnpm build 2>&1 | tail -2 && git ...` pushed through a failing build, because zsh without `pipefail` returned the status of `tail`, and on 2026-08-07 a `;` after the build let a failed build reach the push. Capture the output to a file and chain the next step with `&&`, or `set -o pipefail` first. [Agents on one machine](https://www.prototemplate.com/docs/handbook/shared-machine#exit-status-gates) holds the same rule for every gated command. ## Exit codes [#exit-codes] Every lint prints `file:line rule message` and exits **0** on a pass, **1** on findings and **2** when it could not judge: no build to read, no server to fetch, or a file it needs is missing. `--report` prints the findings without failing where a report is useful. A gate never passes when it could not judge. Exit 2 exists so that a missing server, a missing build or a page that did not open stops the chain as loudly as a finding does. Under `--report` the line audit drops both its exit 1 and its exit 2 for a state that did not open, so a clean exit under `--report` proves nothing about the page. ## Ratchets [#ratchets] Some rules arrived after the code they judge. Their lints keep a baseline of the findings that predate the rule (`scripts/lint/practices.baseline.json`, `type.baseline.json`, `seo.baseline.json`): a finding in the baseline is known debt, a new one fails the run, and the list only shrinks. `--update-baseline` rewrites it after a deliberate cleanup. * The baselines count findings per file, never per line. Parallel sessions move lines all the time, and a per-file count stays the same when a line moves, so a moved line never reads as a new finding. * A baseline above the real count lets a new violation into that file without a failure. Lower the baseline in the commit that cleans a file up, and read the JSON diff before committing it, because `--update-baseline` also records any new violation present at that moment. * A deleted file or direction takes its baseline entries with it in the same commit. * Every exemption carries its reason in the config beside it: `.oxlintrc.json` takes a comment above each override, and the line audit's allow list names the device and why its parallel strokes are one drawing. ## Browser gates run one at a time [#browser-gates-run-one-at-a-time] The live audits, the page check and the captures each launch Chrome for Testing. Run them one after another: two at once compete for the machine, and on 2026-10-01 a parallel capture made `lint:lines:shell` fail on pages that were correct. On Kevin's machine each heavy command is gated with `headroom can-start`, which reports whether memory allows it. ### Before trusting a red browser gate [#before-trusting-a-red-browser-gate] A red browser gate can mean the page is wrong, or that the machine or the server was. The capture above failed `lint:lines:shell` on correct pages, and a rerun on its own passed. Load one route by hand before reading the findings. A dev server that answers 500 makes the line audit exit 2 on its first route: on 2026-10-05 the cause was a Turbopack worker that timed out on `globals.css`. Restart the server only when your session owns it, and rerun the gate alone. ## Before a push [#before-a-push] The pre-push hook runs `pnpm lint:public`, which scans the change for key shapes, machine paths and, with `PT_DENYLIST` set, the private denylist. A finding stops the push. ## A new rule becomes a lint [#a-new-rule-becomes-a-lint] A defect fixed once is fixed as a class, with the lint that keeps it from coming back ([operating principle 4](https://www.prototemplate.com/docs/handbook/operating-principles)). A lint has the law in its header with its source, pure checkers that tests import, the `file:line rule message` output and the three exit codes, a passing and a failing fixture per rule, and a line in `lint:static` or the build. Section 4 of the gt-lints skill gives the steps. ## What the line auditor counts [#what-the-line-auditor-counts] `scripts/lint/lines.mjs` is the line auditor. It walks every element under `body` and rebuilds lines from computed CSS, so it judges what the page paints rather than what the class names say. It counts these as lines: * a border side 1 to 2.5 px wide and at least 24 px long, in a visible color; * an outline ring 1 to 2.5 px wide, placed at its offset; * a box-shadow with no offset, a blur of 1.5 px or less and a spread of 1 to 2.5 px, which is a border drawn by other means; * a filled box 2.5 px thick or less and longer than 24 px; * an exposed-ground strip, where a box with a visible fill has 1 to 2.5 px of padding on a side with no border; * an absolutely positioned `::before` or `::after` with a border or a thin fill, its `translateX(-50%)` centering included. Each element is clamped to its nearest clipping ancestor first, and an edge the clip removes is dropped, so the audit reads the geometry the eye sees and never the layout box under a clip. | Finding | Rule | Gates | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------- | | Double | Two parallel lines from different owners 1 to 4 px apart, overlapping 75% of the shorter line and at least 80 px, both visible at two of three sample points | Yes | | Junction | The same pair under 1 px apart: two owners draw one seam and their alphas stack | Yes | | Color | A chrome border off `--pt-hair`, `--pt-hair-soft` and `--pt-edge`, or an ink border away from an active state | Yes, shell mode | | Self-stack | A translucent border (alpha under 0.95) over the element's own translucent fill (alpha 0.12 to 0.95) with no `background-clip: padding-box` | Only when the edge is 120 px or longer | | Invisible seam | An opaque fill with a 1 to 2.5 px padding strip within 45 summed RGB steps of the page's ground | Yes | * It skips elements inside `svg` or `canvas`, under a `matrix3d` transform or a mask image, hidden ones, and anything at opacity 0.05 or less. SVG strokes and canvas pixels are checked by eye in 2x crops of every junction. * It never hovers, so a hover border is checked by hand. * An earlier version audited only the first URL on its command line and passed the rest unread. Every positional URL is now audited. * A URL that contains whitespace exits 2. It is a quoting accident: zsh does not split an unquoted variable, so a list of URLs arrives as one argument and would load as a 404. An HTTP status of 400 or more also exits 2, because a 404 page has almost no lines and would pass. The [gt-lints skill](https://www.prototemplate.com/skills/gt-lints) holds the allow list with its reasons, and [Lines and seams](https://www.prototemplate.com/docs/craft/lines) gives the line law the auditor holds. ## The type check [#the-type-check] `next build` never type-checks here, because `next.config.ts` sets `typescript.ignoreBuildErrors`, so `pnpm exec tsc --noEmit` is a gate of its own. `tsconfig.json` includes `.next/dev/types/**/*.ts`, the route types a running dev server writes. A dev server can leave one of those files half written while it serves, and `tsc` then reports errors in a generated file that no change touched. Run the type check in the scratch worktree that runs the build, where no dev server writes, or rerun it after the server finishes writing. [Dev servers and builds](https://www.prototemplate.com/docs/engineering/dev-servers#failure-modes-of-next-dev) covers the other states a dev server leaves in `.next`. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # The SEO system > How every route gets its title, canonical, robots, card and structured data from one call, the indexing policy, the sitemap and the agent files. URL: https://www.prototemplate.com/docs/seo Every route's head comes from one function, `seoRoute()` in `src/lib/seo/route.ts`. It returns the route's complete Next.js metadata and the JSON-LD nodes for its kind, and it writes every field every time. Next.js merges metadata shallowly: a field a route leaves out is inherited from a layout, and a partial `openGraph` object replaces the parent's whole one. Before this system, the root layout's canonical of `/` was inherited by every page on the site. ```tsx import JsonLd from '@/lib/seo/JsonLd'; import { seoRoute } from '@/lib/seo/route'; const route = seoRoute({ path: '/docs/handbook/design', title: 'Design system', section: 'Docs', kind: 'article', description: 'The design canon: the four-color system, the line law, rails and seams, the doubled line and motion.', markdown: '/docs/handbook/design.md', }); export const metadata = route.metadata; export default function Page() { return <JsonLd nodes={route.jsonLd} />; } ``` A dynamic route calls it in `generateMetadata` and again in the page body with the same input. ## What one call writes [#what-one-call-writes] * **The title**: `<title>, <section>, Prototemplate`, dropping the section and then the site name when the result passes 60 characters. The home page is `Prototemplate`. * **The description**: 50 to 160 characters, the length a search snippet shows whole. * **The canonical**: an absolute URL from the policy below. * **Robots**: `index, follow` with large image previews, or `noindex, follow`. * **Open Graph and the X card**: the title, the description, the canonical as `og:url`, the card with its size and alt text, `twitter:site` @generaltxn and `twitter:creator` @kevskgs. * **The icon**: the Prototemplate mark on the hub, the GT mark on the `/d/` pages and the presenter. * **The Markdown alternate**, when the page has a twin, and an RSS alternate where a page names a feed. ## The policy [#the-policy] `policyFor(path)` in `src/lib/seo/policy.ts` is a pure function from a pathname to whether the page is indexed, its canonical and whether the sitemap lists it. | Paths | Robots | Canonical | Sitemap | | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------- | ---------------------------------- | ------- | | The hub's own pages: `/`, `/brand`, `/docs` (the handbook under `/docs/handbook`), `/skills`, `/motion`, `/directions/<slug>` and the rest | index, follow | itself on www | yes | | `/blog/<slug>` | index, follow | the post on generaltranslation.com | no | | `/d/production` and each mirror page with a live counterpart | index, follow | the page on generaltranslation.com | no | | The dashboard states under `/d/production` | noindex, follow | itself | no | | Every other `/d/` page | noindex, follow | itself | no | * **One canonical host.** [www.prototemplate.com](http://www.prototemplate.com) is canonical. The two aliases serve the same deployment, and the absolute canonical in every head consolidates them without a redirect. * **Mirrors point at the original.** A mirror page carries the same copy as its live page, and GT's domain is the one that should rank for it. The mirror stays crawlable, because a crawler must read a page to honor its canonical. * **Explorations stay out of results.** A direction reassembles GT's copy, prices and legal text into a design study. Indexed, it would put stale claims under GT's name on another domain. `src/app/d/layout.tsx` gives the whole folder `noindex, follow`. * **Posts point at GT.** The three posts are the files generaltranslation.com publishes. * **robots.txt never carries the policy.** A disallowed URL can still be indexed from links, and the crawler never reads its `noindex`. robots.txt allows every agent and names the sitemap. ## The social card [#the-social-card] `/og?v=&t=&s=&d=&k=&p=` (`src/app/og/route.tsx`) draws a 1200 by 630 PNG with `next/og` from the query: the version, the title, the section, the description, the kind and the path. The renderer reads TTF only and applies no variable axes, so `pnpm build:inter` cuts two static instances from the self-hosted InterVariable into `src/app/og/`: weight 500 for the title and labels, weight 400 for the text. The card is a pure function of its query, so it is cached for a year, and `OG_VERSION` changes when the design does. ## Structured data [#structured-data] Every page carries the `WebSite` node from the root layout. A page adds a `BreadcrumbList`; an article (a docs page or a handbook document) adds a `TechArticle`; a skill adds a `TechArticle` and a `SoftwareSourceCode` for its folder; a post adds a `BlogPosting` with GT's URL. `serializeJsonLd` escapes every block so no text can close the script tag early. ## The sitemap [#the-sitemap] `src/app/sitemap.ts` lists the indexable pages: a short list of singleton routes, every page `src/lib/updated.ts` dates (the docs pages, the handbook's among them), and the archive entries, each filtered through the policy. A page's `lastmod` is the date of its last commit, from `updated.ts`, and is left out where there is none, since a build date tells a crawler nothing. A new indexable page gets an entry in `scripts/build/updated.mjs`. ## The agent files [#the-agent-files] | Address | What it serves | | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `/llms.txt` | The site for agents, generated from the registries: the docs, the components, the handbook, the skills, the routes and the directions | | `/llms-full.txt` | The text of every docs page and handbook document in reading order | | `/AGENTS.md` | The agent guide with its links resolved to site addresses | | A docs page or handbook document plus `.md` | Its Markdown twin, with a `Link` header naming the HTML page as canonical | | `/skills/<slug>/SKILL.md` | A skill page's twin | The twins are prerendered and never rendered on demand. Each docs page also links `/llms.txt` with `rel="describedby"` in its head and carries one machine-only `llms.txt` link that readers never see. ### Why the twins are prerendered [#why-the-twins-are-prerendered] A twin route reads the page's MDX from disk at request time unless it is prerendered. On generaltranslation.com the deployed function had no copy of the content, so a twin rendered on demand answered 500 in production while it worked locally. The twin route here, `src/app/md/[...path]/route.ts`, sets `dynamic = 'force-static'`, `dynamicParams = false` and `revalidate = false`, and its `generateStaticParams` lists every docs page. Every twin is written at build time, and an unknown path is a 404 that reads no file. `next.config.ts` also keeps the route's file trace from taking in the whole checkout (`outputFileTracingExcludes`), which is safe only while the route prerenders, so a twin route that turns dynamic must stop reading the excluded paths. * A middleware matcher that excludes dotted paths never sees a `.md` URL, so the twin rules live in the `TWIN_REWRITES` of `next.config.ts`. They cover `/docs.md`, one segment and two segments under `/docs`; the deepest docs URL is two segments, so a page in a deeper folder would need a third rule. * Agent-readability checkers count links in the server-rendered body, and a menu that renders its rows only while it is open adds none. The machine-only anchor is therefore in the body itself, with `className='sr-only'`, `aria-hidden='true'` and `tabIndex={-1}`, so screen readers and the tab order skip it. ## The lint [#the-lint] `scripts/lint/seo.mjs` holds every route to the system in three modes: * **Static** (`pnpm lint:seo`, in `lint:static`): every route gets its metadata from `seoRoute`; a static route's path matches its file; titles and descriptions hold no em dash and descriptions run 50 to 160 characters; the root layout sets no canonical; no file names an alias host; no page writes its own icons or cards. * **Built** (after `next build` in `pnpm build`): every prerendered page has one title, one description, an absolute canonical on the right host, its card with its size, the robots its policy gives and JSON-LD that parses; the sitemap lists only what the policy lists; indexable titles and descriptions are unique. * **Live** (`pnpm lint:seo:live` against `next start` or www): every sitemap URL answers 200, every card is the size its head declares, every twin answers Markdown with the right canonical, the agent files answer, and no internal link redirects or breaks. ## Adding a route [#adding-a-route] 1. Call `seoRoute` with the route's path, title, kind and a description of 50 to 160 characters, and render its `jsonLd`. 2. Add the route's paths to `scripts/build/updated.mjs` and run `pnpm build:updated`, so its `lastmod` is true. 3. Run `pnpm lint:seo`, then the build, whose built check reads the new page. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt # Use these docs as a template > Lift this docs site into another Next.js 16 project: the dependencies, the files to copy, the adapters for a design system, and adding a page. URL: https://www.prototemplate.com/docs/template This docs site is built on the stack generaltranslation.com/docs runs: fumadocs with the notebook layout and MDX pages. It imports nothing from the hub's shell or the directions, so the folder lifts into another Next.js 16 project, and every pattern carries over to gt-cloud and back. ## The dependencies [#the-dependencies] ```bash pnpm add fumadocs-core@16.16.1 fumadocs-ui@16.16.1 fumadocs-mdx@15.4.6 pnpm add -D @types/mdx ``` These are the versions gt-cloud pins. fumadocs-ui 16 needs Next.js 16 and React 19.2, and it is Tailwind 4 only. Prototemplate pins `next` to 16.2.12 so an install cannot move it to 16.3, which holds memory for every prerendered page during a build. Add `"postinstall": "fumadocs-mdx"` to `package.json`, so a fresh install writes `.source/` before the first type check. ## The files to copy [#the-files-to-copy] | Path | What it is | | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `source.config.ts` | The collection: `content/docs`, processed Markdown kept for the twins, `tokenizeTimeLimit: 0` for Shiki, and the remark plugin for pages that include repository files | | `src/lib/remark-handbook.ts` | The plugin a page that shows a repository Markdown file needs: it drops the file's h1, resolves its relative links and prints raw HTML as text | | `src/lib/source.ts` | The loader at `/docs`, with the sidebar's icon names | | `content/docs/` | The MDX pages and a `meta.json` per folder: the sidebar's tabs, folders and order | | `src/app/docs/` | `layout.tsx` (the notebook layout and the provider), `[[...slug]]/page.tsx` (one page) and `docs.css` | | `src/components/docs/` | The sidebar footer, the theme button, the page actions and the sidebar motion; `demos/` holds the live plates, each loaded through `demos/lazy.tsx` | | `src/app/api/search/route.ts` | The static search index | | `src/app/md/`, `src/app/llms.txt/`, `src/app/llms-full.txt/` | The Markdown twins and the agent files | | `src/lib/seo/` | `seoRoute`, the policy, the JSON-LD builders and the card | In `next.config.ts`, wrap the config with `createMDX()` from `fumadocs-mdx/next` and add the twin rewrites, which send `/docs.md`, `/docs/<slug>.md` and `/docs/<folder>/<slug>.md` to the twin route under `/md/docs`. In `tsconfig.json`, map `@/.source/*` to `./.source/*`, and add `.source/` to `.gitignore`. ## The adapters [#the-adapters] A project with its own design system needs these adapters, all in `docs.css`, all unlayered rules so they outrank Tailwind's layered utilities without `!important`. The header of `docs.css` lists them in order. 1. **The font tokens.** Tailwind's theme layer would reset `--font-sans` and `--font-mono` to its own stacks for the whole document, so the sheet's `@theme` block points them at the site's type tokens (`--pt-text`, `--pt-mono`, `--pt-display`). 2. **The theme switch.** fumadocs styles its dark mode on a `.dark` class that next-themes writes. Prototemplate's theme is `html[data-theme]`, so `RootProvider` gets `theme={{ enabled: false }}`, `docs.css` redefines the `dark` variant on `:root[data-theme='dark']`, and fumadocs' two dark Shiki rules are repeated under that selector. `pnpm lint:practices` fails a `.dark` selector anywhere in `src/`. 3. **The token bridge.** fumadocs reads `--color-fd-background`, `--color-fd-foreground`, `--color-fd-border` and the rest. `docs.css` sets them from the site's tokens on `:root:has(#nd-notebook-layout)`, so they exist only while the docs layout is in the document. 4. **The weight cap.** fumadocs and its typography plugin draw headings at 600 and 700. Prototemplate's Inter stops at 500, so one rule sets 500 on the docs' headings, labels and strong text. 5. **The radius map.** fumadocs rounds its panels with `rounded-lg` to `rounded-2xl`; inside the docs those resolve to the site's card corner. 6. **Flat surfaces and the sidebar's marks.** The docs' surfaces are flat, and the sheet draws the rails and pills that `SidebarMotion` moves in the sidebar. ## Adding a page [#adding-a-page] A page lives in a tab, and inside a tab in a folder. Choose both first, because the folder decides the URL and the sidebar row. | Tab | Folder | URL of `<slug>.mdx` | | ----------- | -------------------------------------------------------------------------------------------------------------- | -------------------------- | | Guide | `content/docs/(guide)/` | `/docs/<slug>` | | Components | `content/docs/components/`, then one of `(fields)`, `(drawings)`, `(language-and-motion)`, `(marks-and-heads)` | `/docs/components/<slug>` | | Craft | `content/docs/craft/` | `/docs/craft/<slug>` | | Engineering | `content/docs/engineering/` | `/docs/engineering/<slug>` | | Internals | `content/docs/(internals)/`, then one of `(routes)`, `(tooling)`, `(publishing)` | `/docs/<slug>` | * A folder named in parentheses, such as `(tooling)`, is a folder group: it draws a sidebar folder and adds no URL segment, so `content/docs/(internals)/(tooling)/gates.mdx` serves `/docs/gates`. A plain folder such as `craft` adds its name to the URL. * A tab is a folder whose `meta.json` sets `"root": true`. fumadocs draws every root folder as a tab in the menu at the top of the sidebar, with the `title`, `description` and `icon` of that `meta.json`. The root `content/docs/meta.json` lists the five tabs in order. * Write `<slug>.mdx` in the chosen folder with a `title` and a `description` of 50 to 160 characters, then add the slug to that folder's `meta.json` `pages` list where the sidebar should show it. A root folder lists its own `index` as its first row. A folder that is not a root takes its `index.mdx` as the folder row's link, so its `meta.json` leaves `index` out. * A new folder group gets its own `meta.json` with a `title`, the `pages` in order and `"defaultOpen": true` when it should start open, and the parent's `meta.json` lists it by its name, parentheses included. * A script that turns a file path into a route drops every `(name)` segment, as `scripts/build/updated.mjs` and `scripts/lint/registries.mjs` do. A script that keeps it looks for `/docs/(internals)/gates`, a page that does not exist. The route, the sidebar, the search index, the table of contents, the Markdown twin, `/llms.txt`, the sitemap and the social card follow from the file. In this repository also add its row to `src/components/docs/pages.ts` in sidebar order, with the URL after `/docs/` as its slug (`components/dither`, `gates`), and run `pnpm build:updated`. `pnpm lint:registries` follows every `meta.json` from the root and fails when the rows, their titles or their descriptions differ from the files. A page under `/docs/handbook` is held to the handbook registry instead. A page that shows a repository Markdown file, as each handbook page does, includes it with fumadocs-mdx's tag, `<include cwd>docs/handbook/glossary.md</include>`, so the file stays the one source. To set a live figure between two sections, include the file one heading at a time; the build then fails if the page leaves a heading of the file out. A page that mounts a live component imports it from `src/components/docs/demos/lazy.tsx`, which wraps each plate in `next/dynamic`. fumadocs-mdx imports every page into one route, so a plate imported directly would load its code and its stylesheet on every docs page. A sentence beside each plate says what it shows, since the Markdown twin prints the plate as a bare tag. List the files the plate runs under `sources:` in the frontmatter, so `pnpm build:updated` dates the page when they change. ## Traps [#traps] This site met each of these traps on fumadocs and Next.js 16, and each line gives the fix it uses. [Docs site traps on Next.js 16 and fumadocs](https://www.prototemplate.com/docs/engineering/docs-traps) explains each one with its measurements. * A `redirect()` thrown inside a page render paints the layout above the page at the old URL first. A click on a docs folder URL painted the layout with no article, then replaced it about 280 ms later, so every link the site renders points at the leaf page ([links that point at leaf pages](https://www.prototemplate.com/docs/engineering/docs-traps#links-that-point-at-leaf-pages)). * A sidebar whose rail and current-row tint are drawn in a mount effect shows neither until hydration. fumadocs' own rails sit inboard of the drawn path and jump sideways at hydration, so only the mark whose geometry matches, the row tint, is shown before `#nd-sidebar[data-sb-ready]` ([marks that exist at first paint](https://www.prototemplate.com/docs/engineering/docs-traps#marks-that-exist-at-first-paint)). * Shiki stops tokenizing a line after 500 ms by default, so two builds of one page can differ. On ten code fences a forced limit gave 64 coloured spans in one pipeline and 47 in another, and `tokenizeTimeLimit: 0` in `source.config.ts` gives the same output on every run ([code highlighting across builds](https://www.prototemplate.com/docs/engineering/docs-traps#code-highlighting-across-builds)). * Tailwind's preflight sets `font-feature-settings` on `html` from a theme token the docs sheet never set, so every docs element computed `normal` and lost `liga` and `calt` while the family was right ([features that compute normal](https://www.prototemplate.com/docs/engineering/docs-traps#features-that-compute-normal)). * A docs rule that sets a global token leaks to other routes. React keeps a route's stylesheet in the document after a client navigation, so the token bridge is scoped to `:root:has(#nd-notebook-layout)` and its values exist only while the docs layout is in the document ([features that compute normal](https://www.prototemplate.com/docs/engineering/docs-traps#features-that-compute-normal)). * An ID inside an `:is()` list lifts the whole list's specificity: one `#nd-notebook-layout` among the shell's root classes would raise every rule built on the list from (0,1,1) to (1,0,1), and those base rules would then beat component rules across the site. A docs root that joins those lists joins as a class, at (0,1,0) like the roots already there ([specificity of an is() list](https://www.prototemplate.com/docs/engineering/docs-traps#specificity-of-an-is-list)). * The docs sheet loads no components layer, so the typography plugin's `.prose` rules land in the utilities layer and tie with utilities at (0,1,0). The rule emitted later wins, and a table-of-contents row that carries both `prose` and `text-sm` gets `text-sm` ([the typography plugin in the utilities layer](https://www.prototemplate.com/docs/engineering/docs-traps#the-typography-plugin-in-the-utilities-layer)). * Tailwind 4's `rounded-full` is `calc(infinity * 1px)`, which Chrome computes as a 33,554,400 px corner, so a corner lint reports that number. A 4 by 4 dot takes `50%` ([the full round corner](https://www.prototemplate.com/docs/engineering/docs-traps#the-full-round-corner)). * fumadocs and its typography plugin draw headings at 600 and 700, and a `b` or `strong` inside weight 500 computes 700 through the browser's `bolder`. The weight cap sets 500 on the docs' headings, labels and strong text ([what fumadocs leaves for the lints](https://www.prototemplate.com/docs/engineering/docs-traps#what-fumadocs-leaves-for-the-lints)). * fumadocs portals the search dialog and its popovers into `body`, outside the layout. An adapter scoped to `#nd-notebook-layout` alone misses them, so the weight cap also names `[role='dialog']` and `[data-radix-popper-content-wrapper]`, and a lint that reads the page at rest never opens them ([markup that hides from a lint](https://www.prototemplate.com/docs/engineering/docs-traps#markup-that-hides-from-a-lint)). * The Markdown twin prints a live plate as a bare tag, so every plate has a sentence beside it that says what it shows. ## Search [#search] The search is fumadocs' Orama index, served as one static JSON file built with the site and searched in the browser. For a few dozen pages that is the whole job, and it works in development. gt-cloud runs Pagefind after `next build` instead, because it indexes hundreds of pages in many locales; switch when the index grows past what one download should carry. ## The checks [#the-checks] Run `pnpm lint:registries`, `pnpm lint:seo`, `pnpm exec tsc --noEmit` and the build. On the running site, `pnpm lint:type:live` and `pnpm lint:radius:live` confirm the adapters hold, and `pnpm lint:seo:live` against `next start` reads every page's head, card and twin. --- The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt