The repository’s documents: the readme and its build log, the brand and design canons, the architecture map, the ship loop, the libraries, the graphics pipeline and the agent guide.
Prototemplate is Kevin Liu's hub and working wiki for how General Translation work is done. General Translation (GT) is the localization platform for developers. The repository holds the brand book, the brand directives, the design lab with its directions and sites, the repository documents, the curated skills, the handbook, the mark explorations, the blog graphics and the films, and serves all of it as live pages in one viewer. The skills under skills/ install into any other project with one command, and AGENTS.md and the handbook copy in beside them (see Import this into another project below), so an agent working in another repository reads the same rules, and a new project can start from this one and build on it. An agent starts at AGENTS.md.
Every direction is a built page that runs live. The Dossier (/d/singularity-dossier) is the site concept Kevin called complete on 2026-08-06; Signal and Orbit are the two earlier site concepts, each keeping its own hero and the sections the Dossier retired; /d/production is the site that shipped, rebuilt page for page. Its Shipped section also carries the dashboard's sign-in and onboarding system, which lives under src/components/plate with its state console (/d/production/signin, /onboarding, /consent, /device, /cli; ?state=<id> opens any state).
/: the design lab, the twenty-seven directions read as an article, one live exhibit at a time, or as a grid of captures, with the anatomy wall (the flagship cut into section tiles, light and dark, desktop and mobile) and the capabilities ledger (what the system can do, each entry pointing at where it runs live)/brand: the brand book, the identity canon in ten sections/docs: the repository documents read in the browser, one address per document, with the build log under the readme/deck: the General Translation brand deck, 93 slides in its own viewer/skills: the curated skills, one page per skill with its SKILL.md, its files and its install line, and the raw files at /skills/<slug>/SKILL.md/handbook: how Kevin runs General Translation work, read as one book: the operating principles, the quality bar, the multi-session playbook, the product map, the glossary and the decisions log, one address per document (/handbook/<slug>)/marks: the mark explorations, every candidate drawn in one color/blog: the docs-redesign series as General Translation published it, three posts with their sources under content//graphics: every illustration of the series, by area, with what it shows and the glyphfield export it sits on; made with the toolchain in graphics//motion: every film on the motion roster with its length and status, the research package of each film in the translation series (/motion/<slug>), and the contact sheet and script of each published cut; generated from motion/ by pnpm build:motion, which publishes a film's credits, sheet and script only for the cut public/motion/published.json pins and lists a newer cut as in review/compare: two directions side by side in scroll-synced frames/present: the presenter, a full-screen walkthrough of the redesign with every prototype live/archive: the retired directions, each kept as a full-page capture with the commit that last held its codepnpm install
pnpm dev # http://localhost:3005| doc | what it holds |
|---|---|
AGENTS.md | 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 use the hub in another project (CLAUDE.md imports it) |
docs/handbook/ | how Kevin runs GT work: the operating principles, the quality bar, the multi-session playbook, the product map, the glossary and the decisions log; the live version is /handbook |
BRAND.md | the identity canon: the name, the idea, the character and voice, the mark, color, type, language as material, and the context for partners |
DESIGN.md | the visual canon: the four-color system, the line law, rails/grounds/seams, the doubled line, iso, the 1-bit language, moving type, motion discipline, the mobile type ladder, the svh/dvh law, the two read lines |
ARCHITECTURE.md | the code map: directions registry, the toolchain SSOT + fork rescoping, the component inventory |
docs/SHIP-LOOP.md | the verify/ship procedure every round runs (line audit, page check, ratchet, tsc, filming, mirror build) |
docs/LIBRARIES.md | the library index; the live version is /craft |
public/media/ | finished artwork made with the system: the Open Source announcement reel, the X banner, two blog films and three partnership globes, shown live in /brand, and the three translation series films, which play on /motion |
docs/GRAPHICS.md | the graphics pipeline: how the blog illustrations are captured, composed, rendered, clipped and handed to a post; the live version is /docs/graphics, and the set is /graphics |
graphics/ | the toolchain itself: the generator, the renderer, the exports, the captures and the recordings |
The skills under skills/ record how Kevin does General Translation work: the rules, the tokens, the commands, the files, the traps and the review standard for each area of it. Each is a folder in the standard SKILL.md format (frontmatter name and description, a metadata block with the title, the areas and the last update, then the body), with its references and scripts beside it, so Claude Code, Codex and other agents load it as it is. /skills lists them by area, each skill's page shows its body, its files and its install line, and /skills/<slug>/SKILL.md serves the raw file (/skills/index.json lists the whole set for an agent). skills/README.md indexes the folder by area for a reader on GitHub or in an imported copy; pnpm build:skills writes it.
| area | skill | folder | also in |
|---|---|---|---|
| Voice | Voice and the humanizer | gt-voice | |
| Website | The GT website | gt-website | |
| Website | Working in Prototemplate | prototemplate | Components |
| Website | Performance without visual loss | gt-performance | Landing pages, Motion |
| Landing pages | The landing page grammar | gt-landing-pages | Aesthetic |
| Aesthetic | Taste and the review standard | gt-aesthetic | Website, Landing pages, Components |
| Aesthetic | The brand and the correct Inter | gt-brand | |
| Aesthetic | The brand deck | gt-deck | Graphics |
| Aesthetic | Exploration rounds and convergence | gt-explorations | Website, Landing pages, Graphics |
| Lints | Lints and gates | gt-lints | |
| Motion | Motion rules | gt-motion | Landing pages, Videos, Diagrams |
| Graphics | Blog and brand graphics | gt-graphics | |
| Graphics | Dither and artifact pictures | gt-dither | Aesthetic |
| Videos | Making a film | gt-films | Motion |
| Diagrams | Drawing diagrams | gt-diagrams | |
| Isometry | Isometric drawings | gt-isometric | Diagrams |
| Components | Components to reuse | gt-components | Landing pages, Website |
| Workflow | Review servers and local environments | gt-local-dev | Website |
| Workflow | Proving work is done | gt-verify | Lints, Website, Aesthetic, Motion |
| Workflow | Branches, PRs and landing | gt-ship | Lints |
| Workflow | Reporting to Kevin | gt-reporting | Voice |
| Workflow | Running agent fleets and long autonomous runs | gt-orchestration |
Install them into any project from a checkout of this repository. The script needs Node and nothing else:
node scripts/install-skills.mjs --list # the set
node scripts/install-skills.mjs --project ~/code/app --dry-run # every step, nothing written
node scripts/install-skills.mjs --project ~/code/app # link all of them
node scripts/install-skills.mjs gt-voice gt-brand --project ~/code/app --copy # vendor two--project <dir> writes <dir>/.claude/skills/<slug> and <dir>/.agents/skills/<slug>; --agents claude,agents,codex adds .codex/skills. --user writes the same folders under the home directory, and --into <dir> writes one folder of your choice.git pull here updates every project that links it. Inside this repository the links are relative, which is how the committed .claude/skills and .agents/skills links are made (--project .). In a repository that teammates clone, use --copy; the script says so when git tracks the target.~/.claude/skills and ~/.agents/skills are links into his wiki's runtime list, so --user refuses there and says why; --into names a folder when that write is wanted.origin: prototemplate). It skips anything else with the same name, and --force moves that entry aside, never deleting it. --uninstall removes its own entries and nothing else.How the set relates to the other skills Kevin uses:
~/.claude/skills and ~/.agents/skills. The skills here are the General Translation layer on top of them: each names the wiki skills it builds on, and no slug here exists in the wiki, so an install never shadows one. Prototemplate work never edits the wiki..agents/skills (gt-landing, gt-ui, gt-dashboard, glyphfield, code-comments) stay authoritative for gt-cloud's code maps. A skill here points to them and copies none of their file maps.To change or add a skill, edit skills/<slug>/SKILL.md and run pnpm build:skills, which validates every skill against the contract and regenerates src/lib/skills.ts for the site and skills/README.md; pnpm lint:skills fails while either is stale or a skill breaks the contract. The contract and the steps for a new skill are in skills/prototemplate.
The hub is built to be copied into another repository so that project's agents work the GT way. What travels: the skills with their installer, AGENTS.md, the handbook, and the two canon files the skills cite (BRAND.md, DESIGN.md). Copied into the same places, every relative link between them keeps working. From a checkout of this repository:
# 1. the skills and their installer, at the project root as they sit here
cp -R skills ~/code/app/skills
mkdir -p ~/code/app/scripts && cp scripts/install-skills.mjs ~/code/app/scripts/
# 2. the agent guide, the canon the skills cite, and the handbook
cp AGENTS.md BRAND.md DESIGN.md ~/code/app/
mkdir -p ~/code/app/docs && cp -R docs/handbook ~/code/app/docs/handbook
# 3. link the skills where agents look for them (see every step first),
# and point Claude Code at AGENTS.md
cd ~/code/app
node scripts/install-skills.mjs --project . --dry-run
node scripts/install-skills.mjs --project .
printf '# CLAUDE.md\n\n@AGENTS.md\n' > CLAUDE.md.claude/skills and .agents/skills to the copied skills/, the layout this repository commits, so they work for everyone who clones the project.node scripts/install-skills.mjs --project ~/code/app from here, so a git pull here updates them all. The handbook's links to skills/ then resolve only on the site and on GitHub.AGENTS.md or a CLAUDE.md, merge the sections in by hand and keep the project's own.AGENTS.md for the new project: its name and what it is, its own commands, ports and gates in "Working in this repository", its session lanes, and any house rule that does not apply there (the line law and the radius law hold for GT surfaces). Keep the read order, the principles and the routing table.ARCHITECTURE.md, LICENSE, docs/ARTIFACT-PICTURES.md). Read them on the site or on GitHub. To take later changes, copy the folders again; to read the current canon without copying, use its address on the site (www.prototemplate.com/docs/design, /docs/brand, /handbook, /skills/<slug>).pnpm check:pages --base <url> --pages-module <file> and node scripts/lint-lines.mjs <url> (the prototemplate skill, "Using the hub from another project"). On another machine set CHROME_PATH to a local Chrome for Testing first.Every page runs on the laws: hairlines drawn exactly once (scripts/lint-lines.mjs fails the round otherwise; pnpm check:pages, scripts/pagecheck/, reads every page at ten viewports in both themes and reports what did not hold), four absolute colors plus one spectral accent per page, dark mode as a pure token remap, and one mobile type ladder (DESIGN.md §12). src/app/d/toolchain is the single source of truth the fork directions import and re-skin by root-class rescoping; src/lib holds the visual engines; src/components/shared holds the instruments. src/lib/directions.ts registers every direction, and the index, the presenter and the sitemap all follow it. The anatomy wall's tiles come from docs/harness/gallery-shoot.mjs under deterministic names (ARCHITECTURE.md, "The gallery pipeline"), and a missing tile drops from the wall.
Prototemplate is public so that anyone can read it, but it is not open source. The code, the writing, the brand and the designs are copyright General Translation, Inc., all rights reserved, and no reuse is granted without written permission. Third-party fonts, icons, photographs and adapted skills keep their own licenses. See LICENSE.
Sixteen directions and three full sites is the visible output. Underneath them is the part I actually spent the time on: a set of laws about lines and color, the tooling that enforces those laws mechanically, and a family of visual engines built as standalone libraries. What follows is the inventory, with the diagrams, the invocations, and the engines themselves running live.
Every direction runs on one typographic rule: structure comes from hairlines, and every line is drawn exactly once. That is easy to say and impossible to maintain by eye, so the repo lints its own pixels. The line auditor loads every page in a real browser (both themes, two widths), reconstructs every rendered line from computed styles (borders, outlines, spread shadows, thin filled boxes, pseudo rails clamped to their clipping ancestors, and the 1px ground reveals framed rows use as seams), then fails the round on four classes of defect:
One command audits a page; a finding names the two owners, the gap between their strokes, and the span over which they run in parallel. The round below is staged (this page audits clean in both themes), but the shape is the auditor’s own:
$ node scripts/lint-lines.mjs http://localhost:3006/craft --theme dark
{
"1440": {
"total": 212,
"doubles": [
{ "orient": "h", "at": 1284, "gap": 1.5,
"a": "pt-sec pt-post-sec", "b": "pt-hatch", "span": 1170 }
],
"missing": [
{ "kind": "section", "between": "pt-sec → pt-foot", "at": 4620 }
],
"selfStacks": [
{ "owner": "pt-compare-tag", "side": "top", "at": 3892, "len": 118 }
],
"invisibles": []
},
"1280": { "total": 208, "doubles": [], "missing": [], "selfStacks": [], "invisibles": [] }
}Deliberate devices (the brand’s doubled threads, marquee rails, terminal frames) live on a small allow list, so the auditor stays strict everywhere else. Two more linters ride along: a shell linter that bans raw color literals in the component layer (every stroke goes through the hairline tokens), and a practices ratchet that counts button types, bare effects, any-types, raw hex in markup and !important in CSS, and refuses any commit that adds to the count.
A dithered field changes state by changing tone on one cell grid; nothing crossfades in alpha, nothing wipes, nothing slides. Both states share the cell size and the phase of the Bayer tile, anchored to one page cell, so the first frame of a transition is the last frame of the state before it, cell for cell. An unanchored tile re-dithers the same tone into different cells, and the eye reads that as a flash even when no tone has moved.
The globe resolves into the Blue Marble on that grid. The globe’s field and the picture’s tone are read at the same cells; one smoothstep over 350 ms mixes the two tones, and the ink is interpolated from the globe’s blue to the picture’s ink on the same curve. The picture’s curve is solved so the mean tone inside its disc equals the globe’s, and brightness holds while the texture changes. Measured on the dashboard build, the two states have identical cells at t0 and the disc’s lit ratio stays within one percent across the resolve.
A step from one picture to the next is the same mix over 150 ms. An interrupted mix restarts from the frame on screen, so a run of quick steps never returns to a picture that has already gone. The system refuses alpha fades, wipes, masks that move, content entrance animation, and any change of cell size inside a transition. Under reduced motion the field shows the end state. Five rules cover all of it:
The plate below runs the loop live on the CPU renderer at 1px cells. The globe turns for two seconds, resolves into the Blue Marble over 350 ms, holds for a second and a half, steps into the Rosetta Stone over 150 ms, holds again, and mixes back into the globe over 350 ms. Both pictures are the tone grids of the deck’s mood slides, 1600 by 900 cells of continuous tone cut to the artifact picture standard, decoded in the browser through a canvas, and their cells print white at 0.62 over the plate’s ground. The plate is 280px tall, so each picture is scaled to cover it; a loop cell that covers one grid cell reads that cell, and a loop cell that covers more reads their area average, so the screen re-dithers the grid’s density at its own cell. The globe is drawn on the disc the Blue Marble occupies. As the resolve begins, the globe’s tone is scaled to the picture’s mean over that disc, so the mix changes texture and ink and holds brightness. Under reduced motion the plate shows the Blue Marble as a still.
The mix is one combinator in the dither module and one clock in the plate. The clock sets the field and the ink together, and the loop draws whatever it holds at its next frame:
/* src/lib/dither.ts: a linear blend of two fields at one amount */
export function mixFields(a: FieldFn, b: FieldFn, amount: number): FieldFn {
return (u, v, t) => {
const av = a(u, v, t);
return av + (b(u, v, t) - av) * amount;
};
}
/* TransitionDemo.tsx: one smoothstep over 350 ms sets the field and the ink together;
the picture ink is the standard's dark screen, white at 0.62 */
const k = smoothstep((now - t0) / RESOLVE_MS);
loop.setField(mixFields(turning, earth, k));
loop.setOptions({ ink: lerpInk(inks.globe, inks.picture, k) });The page’s spine is a ruled column with one rail on each side, drawn once by the column’s own edges and by nothing else. What looks like a simple frame is an ownership system:
The overlap is the named antipattern all of this exists to kill: two owners each drawing a real line along the same edge, compositing into one band darker and thicker than any line the system allows. This page carried one. The diagram above used to run the row’s 1px ground reveal against the inner rail, so the reveal and the rail hairline stacked into a literal doubled border wherever the framed cells met the column. The fix is now drawn into the figure: 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.
Two devices keep the law workable at junctions. Between sections, the diagonal-hatch spacer owns the boundary once (one hairline under a 45° hatch band, the strip you see between every part of this page), so two closes never argue over the same edge. And where two hairlines must legitimately cross, a border cross can be added: a small plus seated exactly on the intersection, the way a printer’s registration mark declares a crossing deliberate. The diagram wears two, where the nav’s close meets the rails.
The ownership system is not a convention to remember. It is componentized, so the wrong line is impossible to draw. A rails wrapper renders the page pair; the row renders one hair-colored ground under 1px gaps; cells have no border props at all:
import { BentoCell, BentoRow, Rails } from '@/components/shell/Bento';
<section className='relative'>
{/* the wrapper draws the page rails, once */}
<Rails />
{/* the row owns the seams: gap-px cells over the one hair ground */}
<BentoRow cols='7fr 5fr'>
<BentoCell title='Ship in every language' sub='One pipeline'>
<LocaleLedger />
</BentoCell>
<BentoCell framed={false} cell='is-terminal'>
<Terminal />
</BentoCell>
</BentoRow>
</section>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. Sections separate with hatch spacer bands (the diagonal you see between every part of this page) rather than empty margin, and light diagrams sit on exactly one sanctioned second surface, mirroring the ink and raised-ink pair that dark mode runs on. The four-color palette (ink, raised ink, titanium, paper) allows one bright white, one spectral accent per page, and earns depth with lines and material instead of shadows.
The signature visuals are not page code, and neither are the instruments around them. Each entry below is root-agnostic with a real contract (options in and a handle out, or one component with one prop surface), living in its own module. Once they are perfected they will be open-sourced on GitHub as individual libraries. Every plate is the real thing, mounted the way a consumer mounts it: engines are created when the plate first scrolls near and paused by their own observers when it leaves, a single still under reduced motion; the drawings are the actual geometry modules; the seam is the actual slider.
The lensing black hole: a photon ring, wrapped accretion arcs, and the page’s own ruled lines bending into the mass, in one WebGL fragment shader on one quad. Twenty-one tunable parameters (geometry, doppler, chroma, exposure, breathing), a full runtime handle (setParams, pause, resume, renderStatic, destroy), and GLSL kept comment-free by design so the shipped source stays a fraction of the page it lights.
import { createHorizonField } from '@/lib/horizon-field';
const field = createHorizonField(canvas, {
speed: 0.5,
params: { ink: [1, 1, 1], exposure: 2.6 },
});
/* the shader draws nothing until it is given a geometry */
const fit = () => {
field?.setParams({
center: [canvas.clientWidth / 2, canvas.clientHeight / 2],
radius: Math.min(canvas.clientWidth, canvas.clientHeight) * 0.32,
});
};
fit();
new ResizeObserver(fit).observe(canvas);
/* handle: setParams · pause · resume · renderStatic · destroy */A canvas-2D particle field of 1,280 glyphs from eight writing systems that condenses into the word "language" in script after script. One preallocated typed-array pool, a 1-bit Bayer-dithered atlas, and morphs that conserve matter: the outgoing word’s dust is the next word’s material, and nothing ever spawns mid-air or vanishes mid-flight. The dossier home hardened it into a real library: a drift option runs the rain down or up on the same negative-safe wrap math (its closing band rises), per-sprite ink bounds cut the blitted area several-fold, depth alpha is quantized so the loop touches globalAlpha a handful of times per pass instead of per glyph, the field is host-aspect-true (homes live in unit coordinates and re-lay onto the live box every resize, so nothing about it is square), and a frame-time governor watches the real cadence: a wide viewport on weak silicon steps down the same quality ladder the phone cut uses (lean device-pixel cap, then the lean pool), down only, with pool cuts waiting for idle rain.
import { createGlyphField } from '@/lib/glyph-field';
const field = createGlyphField({
canvas,
drift: 'rise', // the library's fall, or a rising field (same wrap math)
copy: 'none', // 'auto' infers the copy fold; 'none' is a standalone plate
displayFamily: getComputedStyle(canvas).fontFamily,
monoFamily: getComputedStyle(canvas).getPropertyValue('--pt-mono'),
onScript: (index) => setActive(index),
});
/* the whole teardown: observers, loop, theme watcher */
field?.destroy();The closing band’s material as its own engine: the glyph-field’s eight-script inventory rising off the ink in the same set 34px columns, with a content clearing measured off the live DOM box so glyphs own only the margins. The band mounts run it gated: parked entirely below the tc narrow cut, the header’s full-width strip kept clear so glyphs own only the sides and the foot, a 1.5x device-pixel ceiling and a 30fps cap for the slow drift, and the frame-time governor underneath. It inherited every one of the hero’s flicker lessons: solid tiers on a tier-batched alpha ramp (dither never rides moving glyphs), integer-snapped device-px blits, hysteresis on the clearing rim. On this plate it plays: glyphs shiver as the pointer nears, and a click blows the nearest one up, its shockwave shoving the column neighbors before the field heals.
import { createInkField } from '@/app/d/glyph-rain/sections/band/inkField';
const field = createInkField({
canvas,
clearEl: contentBox, // the field keeps clear of this box, measured live
clearing: 'none', // or flood the whole canvas (this plate)
interactive: true, // pointer wobble + click bursts; bands never set it
displayFamily: getComputedStyle(canvas).fontFamily,
});
field?.destroy();The spectral light behind every dark terminal and band: a flowing wide-gamut wash with presets, speed and exposure control, masked so the light owns the edges and the content owns the dark center.
import PrismaticField from '@/components/shared/PrismaticField';
<PrismaticField
preset='2'
speed={0.5}
params={{ exposureScale: 4200 }}
className='plate-field'
/>
/* presets: '1' wide burst · '2' arc over a dark core.
exposureScale is the dimmer. Raise it under content. */Any continuous field fn(u, v, t) → 0..1, rendered as pure 1-bit ordered dither: each cell compares the field against the 8×8 Bayer matrix, an exact permutation of 0..63, so a flat field lights exactly k pixels per tile and the ramp has 65 tonally linear levels. It draws one device pixel per cell into a small buffer and lets CSS upscale it pixelated, writes every frame through one reused Uint32 view, and ships field factories (radial bursts, a lit globe, streak bands, ramps, a true text SDF) plus combinators to multiply, max and mix them. The loop caps at 30fps, pauses offscreen and on hidden tabs, and renders exactly one still under reduced motion.
import { createDitherLoop, gradientRamp, streakBands } from '@/lib/dither';
const loop = createDitherLoop(
canvas,
streakBands({ bands: 22, waviness: 0.13, taper: 0.5 }),
{ scale: 3, paper: 'transparent', fps: 30 }
);
/* fields swap live; ink re-resolves on theme flips */
loop.setField(gradientRamp({ angle: Math.PI / 2, smooth: true }));
loop.setOptions({ ink: getComputedStyle(canvas).color });
loop.destroy();The GPU sibling of the CPU renderer above: the house Bayer looks themselves, codified. Ten variants live in one module as fragment shaders on a single session-singleton WebGL context, exported as the BAYER_PRESETS roster: slot 01 is the founder’s first-survey pick untouched, and the rest move along real axes: matrix order (2×2, 4×4, 8×8), cell scale from poster to near-grain, the tone field under the matrix (flow, contours, flank radials, sweeps, interference, breath), motion, and palette balance from ink-dominant to the one white-hot variant. Anything that shows or switches the family maps over that one list, the hero review rig and this plate included. Switching is a remount: programs compile lazily and cache for the session, destroy() keeps the shared context, so cycling never grows the context count.
import StudioField from '@/components/shared/StudioField';
import { BAYER_DEFAULT_ID, BAYER_PRESETS } from '@/lib/studio-field';
const [id, setId] = useState(BAYER_DEFAULT_ID);
const active = BAYER_PRESETS.find((v) => v.id === id);
/* switching is a remount: key the stage, and the outgoing field's
destroy() runs before the incoming one draws. One shared GL
context, one program per preset cached for the session */
<StudioField key={active.id} preset={active.preset} className='plate-field' />
{BAYER_PRESETS.map((v) => (
<button data-on={v.id === id} key={v.id} onClick={() => setId(v.id)}>
<i>{v.id}</i> <span>{v.name}</span>
</button>
))}Every isometric illustration in the family goes through one 30° axonometric map: project(x, y, z) seats the camera at (+,+,+), so exactly three faces of any solid are visible, always lit in the same order from the upper left. A solid is extruded by recipe: an opaque hull from the rounded silhouette occludes whatever sits below, face fills shade it, then the hairlines: rim, top contour, and the interior front edges the silhouette does not already draw. Boxes are no longer the whole kit: IsoPrism extrudes any convex plan polygon with the same visibility and three-tone law, plane(z) is the exported matrix that seats whole flat drawings (glyph strokes, masked brand marks) into any surface, markPath() lays rounded bars in a face, and DitheredMark renders a logo as masked ink with the Bayer-quantized shimmer the tower’s capstone wears. Thickness runs about four percent of footprint, one corner radius serves the whole family, and every drawing spends its accent on exactly one element.
import {
frontEdge, leftFace, rightFace, roundedPolygon,
segment, silhouette, topFace, type IsoBox,
} from '@/app/d/toolchain/diagrams/iso';
/* a raised plate: hull occludes, faces lit top/left/right, then hairlines */
const box: IsoBox = { x: -42, y: -42, z: 0, w: 84, d: 84, h: 3.2 };
const [a, b] = frontEdge(box);
<path className='iso-face-right' d={roundedPolygon(rightFace(box))} />
<path className='iso-face-left' d={roundedPolygon(leftFace(box))} />
<path className='iso-face-top' d={roundedPolygon(topFace(box))} />
<path className='iso-line' d={roundedPolygon(silhouette(box))} />
<path className='iso-line' d={segment(a, b)} />The brand’s connector is one SVG path stroked twice: a full-gauge ink stroke underneath and a narrower surface-colored core on top, carving the ink into two parallel hairline threads at a constant gap along any curve. Because both strokes share one geometry the gap cannot drift on a bend, and non-scaling-stroke holds the gauge in screen pixels even under a stretched viewBox. Now a component: DoubledLine takes the center path, the carving surface, the gauges, and a pulse slot, and it two-tones the pair, one white thread and one gray, by clipping the white copy to a half-plane closed along the same geometry, so the split seam hides inside the carve on every bend (offset clones collapse on curves; concentric restrokes can only make symmetric rings). Draw a later one over an earlier one and the junction re-carves itself into one clean pair: merges cost zero parallel-curve math. This is also the one sanctioned double: one owner, one path, stroked twice; the auditor’s allow list holds it by name.
import DoubledLine from '@/components/shared/diagrams/DoubledLine';
const TRUNK = 'M330 120 L680 120';
/* the split region: the SAME center path, closed off the top edge.
The clip boundary is the line itself, hidden inside the carve */
const SPLIT = TRUNK + ' L680 -20 L330 -20 Z';
<svg viewBox='0 0 720 240' aria-hidden='true'>
<DoubledLine
d={TRUNK}
core='var(--color-ink)' /* the surface that carves */
ink='rgba(255, 255, 255, 0.88)' /* the white thread */
inkB='rgba(255, 255, 255, 0.42)' /* the gray thread */
splitD={SPLIT}
gauge={1}
gap={2}
>
{/* the pulse slot, between threads and core, carved to two
accent hairlines by the same core */}
<path className='pulse' d={PULSE} />
</DoubledLine>
</svg>The translation CDN as an orthographic globe, drawn entirely in ink: front graticule arcs at the family’s regular weight, far arcs as dashed hairlines. Depth is said once, with ink, no fills and no shading. Five points of presence stand on graticule intersections with corner-routed leaders that never cross; one bowed great-circle route from the user to the answering PoP is the drawing’s single accent, and one GSAP loop rides it: request dot out, arrival ring, payload chip back. Behind it the dossier home seats a static Bayer atmosphere: four non-overlapping annuli filled with nested coverage tiers of the ordered matrix on one shared grid, so crossing a ring boundary only ever turns dots off and no cell is painted twice.
import EdgeGlobe from '@/app/d/toolchain/diagrams/EdgeGlobe';
/* the dossier mount: a static Bayer atmosphere hugs the limb. Four
annuli of nested coverage tiers on one shared grid, so crossing a
ring boundary only turns dots off and no cell is painted twice */
<div className='v0-glob-stage'>
<GlobeAtmosphere />
<EdgeGlobe title='Five PoPs, one request served 12 ms away' />
</div>Every bare locale code on every page renders through one component: flag first (a fixed 15×11 print from an SVG flag pack, corners barely eased, never a shadow), then the code in the host surface’s own mono, seated on the text baseline with the flag centered against it. The component carries no box of its own; hosts supply the chip (the hairline pill on light surfaces, the translucent-bordered terminal pill on dark), so the same tag drops into ledgers, capability marquees, diagram key columns and the infinite locale belt. Explicit region subtags fly their own flag (en-GB, ar-EG), bases resolve through a twenty-language map, and an unknown locale degrades to a flagless code rather than a wrong flag.
import LocaleTag from '@/app/d/toolchain/components/LocaleTag';
// bare flag+code chip in a code→output ledger row
<div className='tcb-out-row'>
<span><LocaleTag code='es' /></span>
<b lang='es'>¡Hola, mundo!</b>
</div>
// bordered transcript pill in the hero terminal's ✓-row.
// The host supplies the box, the tag only fills it
<LocaleTag code={loc} className='tc-termloc' />A hundred-line dependency-free slider that drags one CSS custom property, --seam-cut, onto a host box; the top layer clips to it, so the underlayer is revealed in place and content never travels a pixel with the handle. State lives in the CSS var rather than React state: drags cause zero re-renders, GSAP intros and cinema beat scrubs drive the same dial by writing the var directly, and the keyboard path re-reads the live computed value so every writer stays in agreement. The handle is the brand’s doubled line in the dossier refit: two solid threads at the house gap, the run between them filled with the payload’s own ink, bridged by a rectangular grip tab on a 64px hitbox, with a full slider role, arrow-key nudges, and a statically parked cut under reduced motion.
Empieza en minutos.
Publica en cada idioma.
import RevealSeam from '@/app/d/toolchain/sections/RevealSeam';
<div className='tct-app' ref={app} style={{ '--seam-cut': '70%' }}>
<div className='tct-app-main'>{/* rendered UI, full width */}</div>
<div className='tct-payload' aria-hidden>
{/* clip-path: inset(0 0 0 var(--seam-cut)), revealed in place */}
<PayloadJson loc={ploc} />
</div>
<RevealSeam
boxRef={app}
ariaLabel='Reveal the served translation file'
onInteract={() => endTour()}
/>
</div>Now a component: EverySentence owns the dossier hero’s headline engine. One shaped sentence morphs between locales by dissolving into 440 canvas-drawn glyph motes seated on the outgoing text’s own sampled ink, dispersing into a cloud, then reassembling: every glyph flies to exactly one point sampled on a brick lattice and the real text node prints through the settled swarm behind a hard clip front that absorbs each glyph as it passes, mirrored for RTL. It never runs on a timer. The host owns the one clock through a ref handle’s setLocale. Requests debounce a quarter second; one landing mid-dissolve retargets the form boundary, one landing mid-form kills the timeline and re-dissolves, and a same-text locale change retags lang and dir only. Width follows the moving-type law: one shaped probe measure per word, cached, device-pixel snapped, tweened once per cycle. The dossier hero drives it from the locale belt; the plate below drives the same handle from a plain interval.
import EverySentence, {
type EverySentenceHandle,
} from '@/components/shared/EverySentence';
/* the host owns the one clock. The component never runs a timer.
setLocale(loc) is the only intake: debounced a quarter second,
mid-dissolve retargets the form boundary, mid-form kills the
timeline and re-dissolves, same text with a new lang retags only. */
const every = useRef<EverySentenceHandle>(null);
<h1><span>
<EverySentence ref={every} words={WORDS} initial='en' />
</span></h1>
{/* the dossier hero: the locale belt is the clock */}
<TranslateWindow onLocaleChange={(loc) => every.current?.setLocale(loc)} />The translate window’s strip zone is an infinite conveyor of locale chips, and the belt is the demo’s clock, not its decoration: the track carries the fifteen-locale roster twice and its position wraps at one run’s width (the house marquee, ticker-driven so the wrap and the centre logic share one clock), and whichever chip crosses the zone’s centre becomes the active locale, so the rendered page and the payload’s translated leaves retype from the belt’s own output, never a timer’s. Each chip owns the centre for six seconds; hovering pauses the belt, any manual interaction holds it until a quiet spell passes, and a pinned inspector or the Terminal face freezes it outright. Clicking a chip slides the belt the short way round (jumping the position by exactly one run is frame-identical, so the recentre picks the least-travel pairing that stays on the doubled track), with crossings suppressed while it travels, so only the picked locale fires. The window carries one additive vent, onLocaleChange, for a host that wants to run on the belt’s clock (the dossier hero’s morphing headline does); passed nothing, nothing changes. Reduced motion parks the belt with es centred, and clicks recentre instantly.
import TranslateWindow from '@/app/d/_v0/TranslateWindow';
/* the belt is the window's clock: the track carries the roster twice,
wraps at one run's width, and whichever chip crosses the strip
zone's centre fires the rewrite, never a timer */
<TranslateWindow onLocaleChange={(loc) => every.current?.setLocale(loc)} />
/* pacing: BELT_DWELL seconds a chip; any interaction holds the belt
(BELT_IDLE of quiet before it resumes); a pinned inspector or the
Terminal face freezes it outright */The nameplate as a working type-specimen sheet: prototype in serif off the destination line’s upper left, template in grotesk off its lower right, the destination between them as empty outlined text, and every word held by a crop frame, four border-touching rules that move with their word. The take is physical: type falls out and the serif frame’s right rule slides in to re-hug the shorter word; both words then travel inward, frames riding along, until they park inside the outline, and the parked words ARE the nameplate, lowercase, so nothing ever crossfades. Every travel delta is measured from live rects, so the module never encodes where the sources sit; fonts are awaited before anything is measured, a resize rebuilds the sheet, and reduced motion holds the settled nameplate rather than replaying the take.
import PrototemplateHero from '@/app/PrototemplateHero';
/* the take is physical: 'type' falls out, the serif frame's right
rule re-hugs the shorter word, both words travel to the outline
and PARK. The parked words ARE the nameplate, lowercase, so
nothing ever crossfades */
<PrototemplateHero />
/* travel deltas are measured from live rects. The module never
encodes where the sources sit; fonts are awaited before measuring,
resize rebuilds, reduced motion holds the settled sheet */The stack story’s scroll engine, and the two laws that keep a pinned read honest. One scrubbed dial spans the whole read, and a piecewise time map (measured lock-ins in, beat end-times out) converts scroll into story time, so every layer seats exactly as its beat’s copy reaches the read line whatever the beats’ real heights are. A lock-in is the copy block’s centre taking the 55% read line, measured from flow geometry (the finale is sticky, so its own rect would report the stuck pose), and the whole map re-anchors on every refresh. Between scroll and paint sits the stage’s damper: the trigger only moves a target, and one gsap.ticker lerp chases it, calling apply() once per frame. Taps, cap wires, typing and rail all ride one damped clock, the stage’s equivalent of scrub: 0.35. The ticker detaches when settled, parking dead on target so no residue leaks into the beat math, and re-attaches on new input, so a resting stage burns nothing.
/* one dial spans the read; a piecewise map (measured lock-ins in,
beat end-times out) converts it to story time, re-anchored on
every refresh, lock-ins read from FLOW geometry */
const READ_LINE = 0.55; // a beat locks as its copy centre takes it
/* the scrub's damper: the trigger only moves a TARGET; one ticker
lerp chases it and calls apply() once per frame */
const retarget = (p: number) => {
pTarget = p;
if (!scrubOn && Math.abs(pTarget - pCur) >= SCRUB_EPS) {
scrubOn = true;
gsap.ticker.add(scrubTick); // detaches itself when settled
}
};The landing wall’s compare instrument: two faces of one site (the home and the enterprise page) overlaid with the house seam between them, the reveal-seam recipe refit for a wall of stills. The cut lives in one CSS custom property on the stage, so a drag re-renders nothing; pointer capture on the stage keeps the sweep when the pointer leaves the box mid-drag; and a hidden range control drives the same var, so keyboards and readers get a real slider rather than a pantomime of one. Every face image carries draggable={false}, since one native image-drag would steal the pointer stream and freeze the seam mid-sweep, and each face is a light/dark pair, so the rig follows the page theme without a re-shoot.
import SiteCompare from '@/app/SiteCompare';
/* the cut is one CSS var on the stage. Dragging re-renders nothing */
<SiteCompare slug={site.slug} name={site.name} />
/* inside: pointer capture keeps the sweep when the pointer leaves
the box; a hidden range control drives the same var for keyboards;
draggable={false} on every face, since one native image-drag would
steal the stream and freeze the seam mid-sweep */The harness that stocks the landing wall: a real Chromium at the dev server, shooting the flagship home’s sections and every variant home’s hero, both themes at both cuts. Element screenshots, never scroll depths: each tile is one section’s own box, so side-by-side pairs align whatever the viewport was. Theme is seeded through the same pre-boot door the site itself uses (an init script writes gt-theme to localStorage before navigation, the root script stamps it before first paint), one full scroll pass settles every lazy-armed section before the first shot, and a selector that misses is reported and skipped, never fatal. The run’s last act writes the manifest the gallery page imports. The wall renders what was actually shot, so a missing tile is a skipped cell rather than a broken image.
$ node docs/harness/gallery-shoot.mjs public/shots/gallery
/* element screenshots, never scroll depths: a tile is one section's
own box, so side-by-side pairs align at any viewport */
const file = `sec-${sec.key}-${cut.key}-${theme}.jpg`;
await page.locator(sec.sel).first().screenshot({ path: file });
/* dark cuts ride the pre-boot door: theme seeded before first paint */
await context.addInitScript((t) => localStorage.setItem('gt-theme', t), theme);
/* the run ends by writing the manifest the gallery imports. A
missed selector is a reported skip, never a broken tile */Not an engine: the system that makes a whole page’s mobile typography one decision. Under 720px the page root publishes a ladder of slots: heading sizes with their own line heights, lead, body, small and kicker steps, head and cell paddings, box air, and a 20px seam floor, the least air card copy may keep off a ground strip. Every floor below consumes the slots instead of restating sizes, so a compact-cut change is one token edit rather than a hunt across sections. The block’s position is itself a law: it is the last 720px block in the sheet, because several base rules it overrides appear after the earlier one, and these same-specificity declarations only win by following them. The ladder outranks by order, never by escalating selectors.
/* styles.css: the LAST 720px block; every floor consumes the slots */
@media (max-width: 720px) {
.toolchain-root {
--tcm-h2: 2.25rem; --tcm-h2-lh: 1.18;
--tcm-lead: 17px; --tcm-body: 16px;
--tcm-head-pt: 72px; --tcm-box-gap: 28px;
--tc-card-pad: 20px; /* the seam floor */
}
/* same specificity as the base rules. The ladder wins by ORDER,
so it lives after every rule it must override */
.toolchain-root .tc-head h2 {
font-size: var(--tcm-h2, 36px);
line-height: var(--tcm-h2-lh, 1.18);
}
}Two inline scripts in the document head run before the app exists, and everything that films or embeds the site leans on them. The first stamps the persisted theme onto the root element before first paint. Dark mode is a token remap, so one attribute set early is the difference between no flash and a white flash on every dark load; the screenshot harness enters by the same door, seeding localStorage before navigation. The second is the rAF gate: requestAnimationFrame is wrapped so an embedding parent can freeze and resume every animation loop on the page with one postMessage. Queued callbacks flush on resume, so shaders and scroll loops pick up exactly where they stopped rather than rebooting. That gate is how the presenter idles a whole wall of live thumbnails without touching any engine’s own code.
{/* layout.tsx: two scripts that run before the app exists */}
{/* the persisted theme, stamped before first paint: no flash */}
<script dangerouslySetInnerHTML={{ __html:
"try{var t=localStorage.getItem('gt-theme');" +
"if(t)document.documentElement.dataset.theme=t}catch(e){}" }} />
{/* the rAF gate: postMessage({type:'gt:freeze',frozen}) pauses every
loop on the page; queued callbacks flush on resume */}Not an engine: the ground the engines stand on. Four absolute colors: ink, raised ink, titanium, paper. Structural color everywhere derives from these (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), and each page adds exactly one spectral accent on top. Pages never touch the raw values: each root class publishes a semantic layer (surfaces, ink steps, hairlines, hatch), and dark mode is a pure custom-property remap in which the paper family collapses onto ink, ink flips to white, and hairline alphas rise so a 1px seam still survives between two ink surfaces.
/* globals.css: the four colors, absolute by design */
@theme {
--color-ink: #070707;
--color-ink-raised: #101010;
--color-titanium: #8a8f98;
--color-paper: #ffffff;
}
/* each root publishes a semantic layer of the four (+ alpha) … */
.toolchain-root { --tc-paper: #ffffff; --tc-ink: #070707;
--tc-hair: rgba(138, 143, 152, 0.26); --tc-accent: #2f5ce0; }
/* … and dark mode is a token remap, nothing else */
[data-theme='dark'] .toolchain-root { --tc-paper: #070707;
--tc-ink: #ffffff; --tc-hair: rgba(138, 143, 152, 0.5); }Every animation obeys the same discipline as the lines. The morphing headline is a single shaped text node (never per-character spans, which would break Arabic joining and Devanagari matras), with its width measured from a hidden probe and tweened once per cycle, device-pixel snapped. The locale belt seats each glyph on the orbit’s tangent and rolls words over at the sides so text never inverts, with the flag guiding each rewrite. And the compare seams on the index are the same slide-to-reveal instrument the toolchain hero uses to pull its rendered app back to the payload underneath.
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 page, the visual laws live in DESIGN.md, and the completed reference application is the Dossier (/d/singularity-dossier). Treat the Dossier as the finished statement of this identity, not a concept.
General Translation was chosen deliberately, in this order:
| 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) |
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:
DESIGN.md.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.
| 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 |
| 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.
Measured, declarative, precise, quietly confident. Captions state laws: "the ground is the seam." Short sentences carry their own weight, with no exclamation marks doing the work, no hedging, and no marketing adjectives where a fact would do. Wit is allowed as precision, never as decoration. 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: product focus over performative marketing.
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:
DitheredMark), never a GIF, never a filter glow.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.
The signature device: glyphs, characters that make up greater wholes. Writing systems are the raw material the brand keeps returning to:
EverySentence): a headline dissolves into glyph dust and reassembles in the next language. Matter is conserved; the same swarm becomes the next sentence.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.All of them run live on /craft with their APIs.
The Dossier (/d/singularity-dossier, with /enterprise) is the completed version of this identity in application: the belt-driven morphing headline, the translate window, the stack tower with the Locadex shimmer, the edge globe with its dithered atmosphere, the four-color dark band. When in doubt about how the brand behaves in product, the Dossier is the answer. The other directions are the working record of how we got there.
The canon for every page in apps/redesign — the laws the sixteen directions run on, distilled from the founder rounds. The /craft page 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.
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 |
#2f5ce0; its dark-band lift: #86a8ff). One bright white. An accent is a controlled edge, never a wash..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.[data-theme='dark'] <root> 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.--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.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:
background-clip: padding-box, everywhere.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.
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./directions/<slug> (.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.A reading page sits straight on the stage's paper. That covers the book mode of /brand, /docs, /handbook, /motion, /graphics, /skills and /marks, their record pages, /directions/<slug> and /archive/<slug>: 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 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.
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 only the dividers between them; the last draws none |
| 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 /, /docs, /brand, /compare, /archive/<first slug>, /directions/<first slug>, /skills, /skills/<first slug>, /handbook, /motion, /motion/<first package>, /graphics, /marks, /d/production and /deck (the iframe's 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.
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: a segmented control's end options, the install field's copy segment |
| chip | --pt-radius-chip | 4px | key caps, chips, pills, tags, inline code, 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 |
calc(var(--pt-radius-<role>) - npx).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.
The page's spine is a ruled column with ONE rail on each side.
border-inline.-in column, in the band's rule ink. It never draws a pair beside the column.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.
letter-spacing: 0.06em), the mono for numbers and tokens only.The shell's type is one family, the rsms InterVariable (v4.1) through next/font, bound as ptInter in src/lib/fonts.ts so its family name matches no installed Inter. 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.
--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..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.strong. Nothing above 500.| 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 |
--pt-measure (32em, about 66 characters); a head's lead at --pt-measure-lead (30em). Tables, figures, code and ledgers keep the full column.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; a page may set one badge after the name on the title's line (BookHead's badge: a 1px --pt-hair box at the card corner, its figure the cap height tall on the baseline, hidden from the accessible name). /brand's badge is the traced GT monogram in dotted outline (src/app/brand/GtOutline.tsx); 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 so its 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 with a row rule over its first row, and the mast's rule closes its last row. 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.
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).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.
vector-effect: non-scaling-stroke.stroke-dasharray — dash distances drift under anisotropic stretch.src/components/shared/diagrams/DoubledLine.tsx. Offset clones collapse on curves; concentric restrokes only make symmetric rings.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 (+,+,+).
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.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.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.Density ramps render as ordered dither, never alpha veils.
[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.crispEdges keeps cells 1-bit at any zoom; CSS upscales with image-rendering: pixelated.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).docs/ARTIFACT-PICTURES.md, held by scripts/lint-pictures.mjs.lang + dir on the node; unicode-bidi: isolate on the container.fonts.ready.src/components/shared/EverySentence.tsx.LocaleTag): flag first (fixed 15×11 SVG print), code in the host's mono on the baseline; the host supplies the box.prefers-reduced-motion short-circuits setup entirely; the markup pose IS the still (static accent pair, parked seam cut, single dither frame).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.d (el.dataset.traceD) before an animation blanks it per tick — a re-run would otherwise trace an emptied path.beamAt(t)), never a translated constant.--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.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).
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.
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 |
var(--tcm-X, <px fallback>) 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.@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.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.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).
A mobile stage measures the viewport twice, and the two units never trade jobs:
100dvh, the real viewport with browser chrome collapse included.100svh, the stable viewport: a dvh layout height grows the document on every chrome toggle and jitters the scroll under it.--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.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):
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.
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 /docs 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.
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/<slug>, an archive row /archive/<slug>, a skill row /skills/<slug>. 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, the documents on /docs, whose book scrolls and writes the address itself), 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, docs, handbook, 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, the deck's column.
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/<slug> the skill's row, on /directions/<slug> 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/<slug>). 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.
How apps/redesign is put together, and the rules that keep sixteen directions coherent. Read DESIGN.md for the visual laws; this file is the code map.
src/
app/
page.tsx the index, the working file of the redesign
craft/ the build log: laws, auditors, live library plates
present/ the presenter deck (intro → prototypes → scoreboard)
d/<slug>/ one route per direction (see 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
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
docs/ the documents book: registry.ts (the documents),
book.tsx (reads and renders them on the server),
markdown.tsx and links.ts (the renderer and where
a repository path opens on the site), DocsShell
(the shell for both books)
handbook/ /handbook: the handbook's registry and its two
routes, rendered by the same DocsShell
prototemplate.css the pt grammar (index + craft chrome)
globals.css the four color tokens
components/
shared/ cross-page instruments: EverySentence, StudioField,
PrismaticField, HeroFieldSwitcher, TcMobileNav,
diagrams/ (DoubledLine, …)
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 engines: dither.ts, studio-field.ts,
glyph-field.ts, horizon-field.ts, prismatic-field.ts,
directions.ts (the direction registry),
page-names.ts (each page's plain name),
updated.ts (generated by build-updated.mjs; server
modules only) and page-updated.ts (its type)
scripts/
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
lint-type.baseline.json and its tests
lint-radius.mjs the radius lint: rounded controls, square shells,
every corner through the six --pt-radius-<role>
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
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
pagecheck/ the page check: every page at ten viewports in
both themes, layout shifts, interactions, a
report (pnpm check:pages; its README explains)
site-pages.mjs page discovery and the theme door, shared by
capture-pages.mjs and pagecheck/
shoot-route.mjs screenshot harness (external playwright-core)
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)
install-skills.mjs links or copies skills/<slug> into a project's
.claude/skills and .agents/skills (its tests:
install-skills.test.mjs, pnpm test:skills)
docs/
handbook/ how Kevin runs GT work, served at /handbook:
the operating principles, the quality bar, the
multi-session playbook, the product map, the
glossary and the decisions log, with README.md
harness/gallery-shoot.mjs the gallery shooter (see "The gallery
pipeline" below; the rest of harness/ is
one-off probes)
graphics/ the blog-illustration toolchain (docs/GRAPHICS.md)
content/ the three docs-redesign posts and their authors
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/<slug>, made by
install-skills.mjs --project .
AGENTS.md the agent entry point (CLAUDE.md imports it);
served at /docs/agentssrc/lib/directions.ts is the single source of truth for what exists: sixteen directions, three of them full site pairs (site: true): singularity-dossier, singularity-orbit, singularity-signal. singularity-dossier is the completed direction; signal and orbit keep their own heroes and carry the previous-generation sections the dossier retired, as exploration showcases. The index, the presenter, and the sitemaps all map over DIRECTIONS — add or remove a direction there and everything follows. When a direction is deleted, also sweep scripts/lint-practices.baseline.json for its paths and re-check stated counts (index funnel, layout description, craft intro).
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…) 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 copies only the CSS it must re-scope.
glyph-rain/diagrams/lang/*). Before adding a new copy, don't: import the SSOT and rescope CSS.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.The signature pieces live as libraries, not page code (each has a live plate + API snippet on /craft):
| 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 — 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 — do not re-implement it locally. When an engine gains an option, update its craft entry (body + snippet) in the same round.
The index's anatomy wall and the variant gallery are fed by one harness, and the file names are the contract between the two ends:
docs/harness/gallery-shoot.mjs) shoots the flagship home section by section — element shots anchored on each section's own landmark selector, never scroll depths, so side-by-side pairs align regardless of viewport — across desktop/mobile cuts and both themes, plus one hero viewport shot per variant home. Theme is set before first paint by an addInitScript that writes localStorage['gt-theme'], which the root inline script applies; one full scroll pass settles every lazy/armed section before shooting. A selector that misses is reported, never fatal — the wall just skips that tile.manifest.json beside the tiles — { flagship, sections: [{ key, label, cut, theme, file }], variants: [{ slug, theme, file }] } — so a consumer can import the set instead of globbing the directory.sec-<key>-<cut>-<theme>.png under public/shots/gallery/ (key ∈ hero, customers, story, developer, locadex, context, global, deploy, footer; cut ∈ desk, mob; theme ∈ light, dark), and var-<slug>-<theme>.png for the variant heroes (slugs from src/lib/directions.ts). Any single tile may be missing — consumers hide on onError or render from a known-good list, never a broken image./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 in lockstep. (The index's home/enterprise sweep, src/app/SiteCompare.tsx, is the still-image cousin: two shots under the house seam, the cut living in one CSS var.)AGENTS.md: the entry point for an agent, here or in a project that imported the hub (served at /docs/agents; CLAUDE.md imports it).BRAND.md: the identity canon (the basement-facing brand book; served at /docs/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 /craft 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 and /handbook renders it through the docs shell (src/app/docs/DocsShell.tsx with book='handbook'). A relative link in any rendered document resolves the way GitHub resolves it (src/app/docs/links.ts): a rendered document opens its route, a skill its page, any other file GitHub.skills/<slug>/ 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/<slug> 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/install-skills.mjs links or copies the set into any project (--project <dir>, --user, --into <dir>, with --dry-run). This repository's .claude/skills and .agents/skills are its relative links (--project .); README.md's Skills section has the rules.Prototemplate main is the primary repository for this code. The public site builds and deploys from it, and the routes that exist only here (/docs, /brand, /deck, /compare, /present) have no counterpart in apps/redesign. apps/redesign in the gt-cloud monorepo is a downstream copy of the direction pages: when a direction changes there, the changed files are copied into Prototemplate one at a time and pnpm build must pass before the commit. No bulk rsync --delete runs toward Prototemplate from any tree. Root docs (BRAND.md, DESIGN.md, ARCHITECTURE.md, README.md, docs/) are edited here first. See docs/SHIP-LOOP.md section 7 (the mirror step) for the sequence.
The verify-and-ship procedure every round of work on apps/redesign runs before it lands. Nothing ships on faith: the auditor, the type checker, the camera, and the mirror build all get a vote.
http://localhost:3005.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.node scripts/lint-lines.mjs http://localhost:3005/<page> --theme light
node scripts/lint-lines.mjs http://localhost:3005/<page> --theme dark/, /craft, and the three singularity homes (dossier, orbit, signal) — plus every page the round touched.pnpm check:pages --pages <id>,<id> # the round's touched pages
pnpm check:pages # the whole site, before a releasescripts/pagecheck/ (its README explains the tool) loads every named page on the dev server at ten viewports (360x800, 390x844, 430x932, 768x1024, 1024x768, 1280x720, 1440x900, 1527x814, 1920x1080, 2560x1440) in both themes and reads each cell: horizontal overflow, boxes past the viewport edge, text clipped mid-word, console errors and failed resources, the theme applied, the phone tap targets 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). It then runs a layout-shift observer on every page at four viewports, the declared interactions (the theme flip, the index panel and the preview, the search, the deck's arrow key, a docs contents link, the presenter's dock) with before and after captures, and writes .pagecheck/REPORT.md.
.pagecheck/shots/ (ignored by git); --sheets adds a contact sheet per viewport for a quick look at every page.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-<role> 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 <reason> */. 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.
pnpm exec tsc -p tsconfig.json --noEmitScreenshot every changed visual with the external harness (the in-app browser pane pauses rAF — shader canvases come out blank):
playwright-core from scripts/node_modules, launched against the Chrome for Testing binary.localStorage['gt-theme'] = 'dark' in an init script.deviceScaleFactor: 2+ and crop — full-page shots hide 1px defects.Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>.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.redesign/diagram-standard-v1<next-letter>.Prototemplate main (~/repos/Prototemplate) is the primary repository. The public site builds and deploys from it, and it carries routes that apps/redesign does not have (/docs, /brand, /deck, /compare, /present). Nothing is rsynced from apps/redesign into Prototemplate with --delete: that command would erase every one of those routes.
Work that lands in Prototemplate main stays there. When a direction page is still edited in apps/redesign, the changed files move into Prototemplate one at a time, and the build is the gate:
cd ~/repos/Prototemplate
git checkout main && git pull --ff-only
cp <monorepo>/apps/redesign/src/app/d/<slug>/<file> src/app/d/<slug>/<file> # only the files the round touched
pnpm build > /tmp/proto-build.log 2>&1; echo $? # capture the REAL exit codecmd | tail reports tail's exit, so capture as above. A flaky exit-1 with a clean log warrants one re-run before diagnosing.main ("push to main" always means this repo).apps/redesign from Prototemplate, copy in the other direction, again file by file. Root docs (BRAND.md, DESIGN.md, ARCHITECTURE.md, README.md, docs/) are edited in Prototemplate first and copied outward.The index of the componentized instruments. The living version — bodies, live plates, and API snippets — is the /craft page ("The libraries" section); keep the two in step: when an engine gains an option, update its craft entry 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 | docs/harness/gallery-shoot.mjs | The anatomy wall's tile factory: section-anchored element shots of the flagship (desk/mob × light/dark) plus every variant hero, 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, <px fallback>). 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.
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.
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:
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.
| 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.
pnpm install at the root (heroicons for the label icons), pip install pillow, and agent-browser and ffmpeg on the PATH.pnpm graphics:serve keeps graphics/ on port 8765.graphics/shots/hi/ (see "Capturing") and register them in SHOTS with their density.gen-visuals.js.add(id, area, name, bg, why, () => html). Assign it its own export in ASSIGN.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.graphics/build/composite-videos.sh.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./graphics and the post on /blog.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.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.
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.
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.
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 90. Posts live in the generaltranslation/content submodule, so a change is two pull requests: content first, then gt-cloud pointing at the content merge commit. The content repository's preview app needs a simple version of every component or its build fails. 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.
<rect> 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.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 90 |
| "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 |
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. 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".
The operating principles hold all eighteen with their sources.
The skills live in skills/<slug>/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 lists them by area.
| Task | Skill |
|---|---|
| Writing any text: copy, docs, captions, PRs, posts, Slack | gt-voice |
| Ending a turn, reporting state, the PR slate, decision lists | gt-reporting |
| A page, doc, route or build of generaltranslation.com | gt-website (with gt-cloud's own gt-landing) |
| A landing, pricing, enterprise or careers page | gt-landing-pages |
| Anything in this repository | prototemplate |
| Running or debugging a local server, letting Kevin try a build | gt-local-dev |
| Lag, frame cost, Lighthouse, a heavy shader or canvas | gt-performance |
| Design taste, polish, a surface Kevin calls bad or busy | gt-aesthetic |
| The brand, the one Inter, marks and third-party logos | gt-brand |
| A slide, the brand deck or any GT presentation | gt-deck |
| Options, directions, variants, converging on a pick | gt-explorations |
| A lint, a gate, a new rule to enforce | gt-lints |
| Proving a fix or feature is done | gt-verify |
| Commits, PRs, review bots, stacks, landing | gt-ship |
| Workflows, subagents, briefs, handoffs, resuming after a stop | gt-orchestration |
| Animation of any kind | gt-motion |
| A blog or launch graphic | gt-graphics |
| Dithered fields and artifact pictures | gt-dither |
| A film, trailer, promo or demo GIF | gt-films |
| A diagram | gt-diagrams |
| An isometric drawing | gt-isometric |
| UI components and how product UI behaves | gt-components |
docs/handbook/, also served at /handbook:
gt-voice).pnpm lint:type holds it (gt-brand).pnpm lint:lines:shell holds it.pnpm lint:radius holds it.pnpm lint:heads holds it.gt-explorations, gt-ship section 8).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).gt-ship section 2).pnpm install
pnpm dev # http://localhost:3005
pnpm exec tsc --noEmit # 3 to 5 minutes
pnpm lint:all # every static lint, the live audits against 3005 and the lint tests
pnpm check:pages --pages <id> # ten viewports in both themes on the touched pages.next. The build gate runs in a scratch worktree with && (gt-ship section 8).playwright-core. On a machine other than Kevin's, set CHROME_PATH to a local Chrome for Testing.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: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.README.md's "Import this into another project" section gives the commands. In short:
skills/ and scripts/install-skills.mjs to the project's root, then run node scripts/install-skills.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.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.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.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.