Design system

The design canon: the four-color system, the line law, rails and seams, the doubled line, iso, the 1-bit language, moving type and motion.

Updated

The canon for every page in Prototemplate: the laws the directions and the shipped site run on, distilled from the founder rounds. The build log on /docs/handbook/libraries is the living version of this document, with the diagrams drawn and the engines running. This file is the reference you read before touching a page.


1. The four-color system

Four absolute colors, declared once in src/app/globals.css:

tokenvaluename
--color-ink#070707ink
--color-ink-raised#101010raised ink
--color-titanium#8a8f98titanium
--color-paper#ffffffpaper
  • Structural color everywhere derives from these plus alpha: every text step is ink or white at some alpha, every hairline is titanium at some alpha, every hatch is a thin ink or white veil.
  • Each page adds exactly one spectral accent (toolchain: #2f5ce0; its dark-band lift: #86a8ff). One bright white. An accent is a controlled edge, never a wash.
  • Pages never touch the raw values. Each root class (.pt-root, .toolchain-root, fork roots) publishes a semantic layer — surfaces, ink steps (-2/-3/-4), hairlines (--*-hair, --*-hair-2), hatch — and everything downstream reads those.
  • Dark mode is a token remap, nothing else: under [data-theme='dark'] <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.
  • The second surface: light diagrams sit on exactly one sanctioned second surface (--tc-plate), mirroring the ink / raised-ink pair dark mode runs on. --tc-panel (#101010) is the one dark artifact surface for code, config, and diffs.

2. The line law

Structure comes from hairlines, and every line is drawn exactly once. The auditor (scripts/lint/lines.mjs) enforces it mechanically — see docs/SHIP-LOOP.md. Four defect classes:

  1. Doubled lines — two parallel strokes from different owners within 4px, including coincident strokes that composite darker than either.
  2. Missing seams — section and row junctions no rule closes.
  3. Self-stacks — a translucent border over the element's own translucent background. Fix: background-clip: padding-box, everywhere.
  4. Invisible seams — rules that exist geometrically but sit within a few RGB steps of the surface they cross.

Ownership

  • Exactly one thing draws the page rails for any section — a rails wrapper, or the section's own full-bleed pair. Never both. The pair is the column's inner pair only: one hairline at each column edge, nothing outside it.
  • The row owns every structural line; cells never draw borders that parallel a row seam. A framed cell has a 1px padding reveal that shows the ground underneath, so the cell draws no border.
  • Flush at the rail: where a row meets a line that already exists, the cell sits flush and that side's reveal is dropped. A reveal never runs beside a rail; a border never runs beside a seam.

The named antipattern

The overlap: two owners each drawing a real line along the same edge, compositing into a band darker and thicker than any line the system allows. The auditor cannot see SVG strokes — reconstructed lines come from computed CSS — so figures must be checked by eye at 2× pixel crops of the junctions.

Sanctioned devices

  • The diagonal-hatch band marks the boundary between a book's front matter and its first section, once per book: 42px of repeating-linear-gradient(-45deg, transparent 0 6px, var(--pt-hatch) 6px 7px) between two hairlines it draws itself, border-block: 1px solid var(--pt-hair) (.pt-book-band, BookView.css). Its rules and its hatch run across the whole stage, as every rule of a book does (see The reading column), and nothing else draws a line within 4px of either rule; the first section's divider draws no rule of its own. The gallery's article (.pt-hatch, prototemplate.css) is the one place a band draws only its bottom rule, because every section there draws its own bottom rule and owns the band's top seam.
  • Border crosses: where two hairlines must legitimately cross, a small plus seated exactly on the intersection (ink, 1px, non-scaling) declares the crossing deliberate — the one ornament a junction is allowed.
  • The doubled line is the one sanctioned double — one owner, one path, stroked twice (see §5). The auditor's allow list holds deliberate devices by name; everything else stays strict.
  • The fixed sheet's ring is the one sanctioned doubled line in chrome. It is the edge of a fixed-size artifact: a slide, the gallery's live 1440x900 exhibit, the compare rig's panes, and the live page on /directions/<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.

The reading column

A reading page sits straight on the stage's paper. That covers the book mode of /brand, /motion, /graphics, /skills and /marks, their record pages, /directions/<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 shell rule written for 900px and under also matches a phone on its side, (pointer: coarse) and (max-height: 500px), and ViewerShell.tsx isNarrow() reads the same query: a 932x430 phone gets the phone's composition and touch sizes (the list as the overlay, 44px book links) instead of the desktop list column of 28px rows. A tablet is taller and keeps the desktop composition.

Every structural horizontal rule of a book runs across the whole stage, from the sidebar's edge to the stage's right edge: the mast's rule, the contents rows, the band and the dividers, the full-width list rows of /skills, /graphics, /motion, /marks and a direction page, an archive record's rows, and the rule over a blog post's footer. The element keeps its 1px border for layout and paints it as a border image outset past both ends (--pt-bleed-top, --pt-bleed-bottom and --pt-bleed-block in tokens.css); the outset never scrolls, and the scroll region clips it at the stage's edges. A rule that drops its border also writes border-image-source: none, because the image ignores border-width and the build empties the shorthand border-image: none (lint:practices holds this). Under forced colors the hairs take CanvasText. A contents row is one line: the first link of each row draws it and the others draw none. Rules inside the content keep their measure: the head panel's rows, tables, prose lists, the --- rule, ledgers and file lists. Kevin asked for full-width rules on 2026-10-07.

Line law for chrome

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:

roletokendraws
structural--pt-hairlarge 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-softlist 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-edgeframes 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:

junctionownerthe other side
sidebar and stagethe sidebar's right edgethe main region draws no left edge
toolbar and stagethe toolbar's bottom edgethe stage, the hint row and the index panel draw no top edge
index panel and stagethe panel's left edgea fixed sheet's ring runs under the panel; the panel covers the progress track while open
group header and its first rowno one: the header draws no rule, and the group's boundary is the 16px gap above its headerthe first row draws no top rule
a page row and its runthe 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 guidethe page row draws no bottom rule; run rows and labels draw no left border
a run and the deep run in itthe same rail, one more bend; in thumbnail density each run its own guide, 16px or more apartno row draws a left border
the marked row and the railthe rail; the 2px ink thumb is a state over itthe rail under the thumb is covered, not doubled; the row draws no bar
a row and the pillsno one: the pills are grounds and draw no linethe row draws no box
a page row and its foldno one: the fold draws no border at rest, and its focus ring is a statethe row draws no right rule
last row of a group and what followsthe last row's bottom edgethe next header carries no top rule
a fixed sheet and its contentthe ring (hair border, paper gap, hair-soft outline)content draws no outer border
a reading column and the stageno one: the column draws no edgethe 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 contentsthe mast's rule (--pt-hair), then the note when there is onethe note draws no rule; the contents grid draws no top rule; its rows draw their own bottom rules
the title and what is above itno onenothing 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 panelthe panel's rows draw --pt-hair-soft under every row but the lastthe 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 sectionthe band, both rulesthe contents grid draws no rule against it; the first section's divider draws no rule
two sectionsthe lower section's divider rulethe upper section draws no bottom rule
tile and its shotthe tile's framethe shot draws no border
segmented control and its optionsthe control's outer borderoptions draw no dividers; the active option is an ink pill 2px inside it with the chip corner, and the hover and the focus ring trace the same pill
stacked corner buttonsthe upper button's bottom edgethe lower button's top edge is transparent at rest
sidebar head and toolbareach owns its own side of the vertical seamthe two bottom rules meet at the sidebar's edge and never overlap

The auditor enforces this from computed CSS. pnpm lint:lines:shell walks /, /brand, /compare, /archive/<first slug>, /directions/<first slug>, /skills, /skills/<first slug>, /motion, /motion/<first package>, /graphics, /marks, /d/production and /deck (the deck's own document) at 1440, 1280 and 390 in both themes against the dev server on port 3005, with the list toggled, the index panel open, the search open, and the grid and book modes on / and /deck. It fails on any doubled line (two owners within 4px), any junction (two owners coincident on one seam), and any border color in chrome outside the three roles. The sidebar's rail is a masked box the walk cannot see, so the auditor reads its vertical runs from its path (data-rail-path), holds its ink to the row role, and fails a state audited with the list open in outline density when the rail layer is not live. pnpm lint:all runs it. The allow list in scripts/lint/lines.mjs names every sanctioned multi-stroke device with its reason inline; for chrome those are the fixed sheet's ring (sheet: a hair border, a paper gap, a hair-soft outline, on /compare, in slide mode and around a direction's live page), the active frames (thumb-frame, page-frame, the sidebar's site tiles as pt-tile) and the hover preview's mat (pt-preview).

Scroll regions belong to the same law: one thin scrollbar everywhere in chrome, a 4px gutter with no track rule and a 2px thumb in --pt-thumb that widens to 4px on hover, owned by .pt-scroll in src/components/viewer/tokens.css and mirrored by the deck's .scroll rule. No scroll region draws a rule beside its thumb.

Corners: rounded controls, square shells

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.

classtokenvaluemembers
shell--pt-radius-shell0the 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-control6pxevery 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-inner5pxa part flush inside a control's 1px border: the install field's copy segment
chip--pt-radius-chip4pxkey caps, chips, pills, tags, inline code, a segmented control's active and hover pill (2px inside its option), and a control set 2 to 7px inside a field
card--pt-radius-card6pxcards, tiles, thumbnails, figures, and popovers: the palette, the help card, the toast, the hover preview
round--pt-radius-round50%dots and avatars on a square box
  • A part n px inside a rounded box (n from 1 to 3) takes calc(var(--pt-radius-<role>) - npx).
  • A picture in a rounded frame is clipped by the frame or takes the concentric radius.
  • Named exceptions: Present keeps 8px (section 15); the marks page's app tile keeps the platform's 22% and its browser tab mock stays square, because they are specimens. The /d/ directions, the deck, the presenter, the craft article and the dashboard plate keep their own grammar.
  • scripts/lint/radius.mjs holds the law: statically in pnpm lint:radius and the build, and on the rendered pages in pnpm lint:radius:live.

The corner notches on hero cards are not drawn — they are the ground showing through, so a corner can never disagree with the seam that meets it.

3. The rails

The page's spine is a ruled column with ONE rail on each side.

  • The column wrapper draws the pair once, with its own border-inline.
  • A band mounted inside the wrapper draws no side rails of its own: the wrapper already owns them.
  • A full-bleed band that sits outside the wrapper draws its own inner pair once, at the column edges, from its own -in column, in the band's rule ink. It never draws a pair beside the column.
  • Retired everywhere: the outer pair at ±10px that earlier directions drew from a wrapper pseudo (the "doubled rail"). A second stroke beside a rail is defect class 1 above, and a coincident second stroke stacks alphas and reads as a different color.

The doubled LINE of section 5 is a different device: one path stroked twice, a connector, never a page rail.

The sidebar's rail (section 16) is a different device: a row-weight line inside the list that marks the place, never a page rail.

4. Typography and voices

  • Three voices: the serif face for brand moments, the grotesk for labels (11px, letter-spacing: 0.06em), the mono for numbers and tokens only.
  • Hairline-boxed code blocks; figcaptions under figures; one type scale per page. Labels never shout; the active element is the only color.

Book type

The shell's type is one family, the rsms InterVariable (v4.1), cut into unicode-range subsets by scripts/build/subset-inter.py (pnpm build:inter): Google Fonts' latin, latin-ext, vietnamese, greek, greek-ext, cyrillic and cyrillic-ext, plus Latin Extended-A, the private use area and the remaining symbols, each with every feature and both axes. The roman's latin subset comes through next/font, bound as ptInter in src/lib/fonts.ts so its family name matches no installed Inter, and every route preloads it. The other subsets and the italic are plain @font-face rules in the same ptInter family in src/app/inter-subsets.css, each fetched only where the page draws a code point in its range. Any other localFont call sets preload: false (pnpm lint:type, T3). Every stylesheet reads the type tokens of src/components/viewer/tokens.css and declares no family, feature list or display size of its own. The /d/ directions keep their own type.

  • Stacks. --pt-text is var(--font-inter), system-ui, sans-serif; --pt-display reads it. --pt-mono is for code, numbers and tokens only. The per-language stacks (--pt-text-hant, -hans, -ja, -he) name system faces after Inter. The nameplate's faces are --pt-face-serif and --pt-face-grot.

  • Features. h1 and h2 (and a display sentence such as the brand plate's .ptc-every) take --pt-ff-display: the single-storey a (cv11) and the open digits (ss01). Everything else takes --pt-ff-text. Both name 'liga' 1, 'calt' 1, because Chrome turns contextual forms off under any letter-spacing. The base rules on the shell's roots set them; a rule never writes a feature list.

  • Weight. 400 for text, 500 for labels, headings and strong. Nothing above 500.

  • The display ladder. Optical size follows the size, so 32px and up is the Display design.

    steprolesize / linetracking900px and under
    d1page title (the book head's h1)44 / 1.04-0.025em32
    d2section title (a divider's h2, a brand chapter)32 / 1.05-0.025em26 / 1.15
    d3h2 in prose, a record's title24 / 1.25-0.02em21
  • The text ladder. Title 18/1.35 at 500 and -0.01em (h3, a row's title); lead 17/1.55 in ink; body 16/1.6 in ink-2; small 14/1.55 (table cells, contents links, ledgers); meta 13/1.5 in titanium (meta tables, gutter notes, captions, th); group labels 12.5 at 500 and -0.005em. Weight 400 text is never tracked, and nothing on Inter is tracked positive.

  • Measure. Body text at --pt-measure (32em, about 66 characters); a head's lead at --pt-measure-lead (30em). Tables, figures, code and ledgers keep the full column.

  • The book page. Every page with a book head has one structure, rendered by BookHead (BookView.tsx), and differs from another only in its words and its facts:

    1. nothing is drawn in the --pt-title-clear space between the toolbar's bottom rule and the title (the reading column's top padding), and no line crosses a title;
    2. the title, the page's plain name from src/lib/page-names.ts (Brand, Documentation, Motion) or a record's own title, at d1 across the mast; on /brand alone (Kevin, 2026-10-21), a badge after the name on the title's line (BookHead's badge: a 1px --pt-hair box at the card corner, its content at most the cap height tall and set on the baseline, hidden from the accessible name) cuts every 1.4s through the GT monogram and the register's monograms (src/components/viewer/BadgeCycle.tsx, steps(n) on one strip, so the count lives in the frames; under reduced motion the first frame stands), and pnpm lint:heads fails a badge on any other page;
    3. the lead (one to three lines at --pt-measure-lead, at most 200 characters, plain declarative sentences) and the panel on one row under the title, both starting on the lead's first line;
    4. the panel: Updated first (the day of the last commit that touched the page's own sources, from src/lib/updated.ts, linked to its commit, with a relative hint after hydration), then three facts, or one fact and the install field; each row a Heroicons 20 solid glyph in titanium, a label in ink-2 and a value in ink at 500, one lead line tall with its text centered between 2px of air above and below, and a --pt-hair-soft rule between rows, none over the first row or under the last; beside the lead the panel starts 2px under the lead's top, so each row's baseline lands on a lead baseline;
    5. one --pt-hair rule --pt-head-rule-pad under the taller of lead and panel, across the stage;
    6. the note at the body step, then the contents;
    7. one hatch band ruled on both edges and running across the stage, --pt-sec-over under the block above it;
    8. the sections, each a section.pt-book-part opened by a divider: a --pt-hair rule (the first section's rule is the band's), --pt-sec-pad, a gutter note of two lines in titanium (Section n, then one fact) on the h2's last baseline, the h2 at d2.

    Under 880px of head width the panel stacks under the lead, ruled the same way, and the mast's rule sits --pt-head-rule-pad under it. The gallery (/) is the site's front page, the nameplate hero and the redesign post in the article's own grammar, and has no book head; /compare is a tool and has none. scripts/lint/heads.mjs holds the structure, statically and on the rendered pages.

  • The gutter. Sections and rows keep the deck's 128px gutter and 28px gap. A gutter number takes the size, line height and baseline of the title beside it, in titanium (ink while active); under 900px it sits above the title at the meta step.

  • The lint. scripts/lint/type.mjs holds all of this: statically in pnpm lint:type and the build, and on the rendered pages with pnpm lint:type:live. Its allowlist names every exception with its reason: the nameplate, the gallery's grotesk labels, the mono numbers and tokens, the specimens, and other sessions' code. scripts/lint/heads.mjs holds the book page (pnpm lint:heads, pnpm lint:heads:live).

5. The doubled line (thread grammar)

The brand's connector: one SVG path stroked twice — a full-gauge ink stroke under a narrower surface-colored core (stroke-width: gap), carving two parallel hairline threads at a constant gap along any curve. Tokens: --thread-gauge: 1.5px, --thread-gap: 3px.

  • All three roles carry vector-effect: non-scaling-stroke.
  • The core's stroke must match the actual surface behind the diagram, or the carve reads as a painted stripe.
  • Layer order is load-bearing: threads first, pulses next, cores last. Draw a later sandwich over an earlier one and the junction re-carves into one clean pair — merges cost zero parallel-curve math.
  • The live pulse is a third copy of the same path in accent at full sandwich gauge, between threads and cores — carved into two accent hairlines, never a filled band. The traveling window is real geometry (a sub-polyline rewritten per tick), never stroke-dasharray — dash distances drift under anisotropic stretch.
  • Two-tone (one white thread, one gray): clip the white copy to a half-plane closed along the same center path — the split seam lies inside the carve. Componentized as src/components/shared/diagrams/DoubledLine.tsx. Offset clones collapse on curves; concentric restrokes only make symmetric rings.

This plate draws DoubledLine. Two source paths merge into one trunk, each path is one stroke carved into two hairlines by the plate's ink, and the trunk carries a static accent window in its pulse slot.

6. The isometric family

One 30° axonometric map for every iso drawing (src/app/d/toolchain/diagrams/iso.ts): project(x, y, z) = [(x−y)·cos30, (x+y)·sin30 − z], camera at (+,+,+).

  • Light from the upper left, always: exactly three faces visible, shaded top 4% / left 9% / right 15% (currentColor mixes; accent faces keep the same order). Reordering is forbidden.
  • The extrusion recipe: opaque hull from roundedPolygon(silhouette(...)) (the occluder), then right/left/top face fills, then hairlines — rim, top contour, and the interior front edge(s) the silhouette doesn't draw.
  • IsoPrism extrudes any convex plan polygon under the same visibility and tone law; plane(z, ox, oy) seats whole flat drawings into a surface with one matrix; markPath() lays rounded bars in a face; brand marks render as alpha masks so the shape takes the surface's ink.
  • One corner radius family-wide (ISO_RADIUS = 2.4); plate thickness ≈4% of footprint, air between stacked plates ≈40%; depth cues live in per-plate stroke alphas, never group opacity; each drawing spends its accent on exactly one element.
  • The mark shimmer is DitheredMark (§7's 1-bit language as a specular band): nested Bayer coverage tiers windowed by pre-rotated 60° clip paths, swept by pure horizontal translate — rotate() windows are GSAP-origin fragile.

This plate draws with the isometric kit: a slab extruded from its rounded silhouette carries two boxes and one hexagonal prism, every face lit from the upper left.

7. The 1-bit language (Bayer dither)

Density ramps render as ordered dither, never alpha veils.

  • The 4×4 matrix ([0,8,2,10 / 12,4,14,6 / 3,11,1,9 / 15,7,13,5]) and the 8×8 (an exact permutation of 0..63 — 65 tonally linear levels) are the house screens.
  • Coverage tiers nest by construction — every tier's lit cells are a subset of the next — so non-overlapping regions filled with different tiers compose exact ramps, and opaque glint ink means doubled cells never brighten.
  • Cells are square screen pixels: transforms that would foreshorten them (a plane seat) go inside the alpha mask; crispEdges keeps cells 1-bit at any zoom; CSS upscales with image-rendering: pixelated.
  • Engines: src/lib/dither.ts (CPU, any scalar field, 1 device px per cell) and src/lib/studio-field.ts (GPU, the authentic BAYER_PRESETS roster of ten variants, one shared GL context, switch by remount).
  • Artifact pictures (the dithered photographs and scans on the plate's field, the deck's mood slides and the transition demo on /docs/handbook/libraries) follow the Blue Marble standard in docs/ARTIFACT-PICTURES.md, held by scripts/lint/pictures.mjs.

This plate runs createDitherLoop(): wavy streak bands pass through the 8x8 Bayer screen, and every dot is one cell thresholded against the matrix.

8. The moving type law

  • The morphing unit is one shaped text node — never per-character spans, which break Arabic joining and Devanagari matras. lang + dir on the node; unicode-bidi: isolate on the container.
  • Width is measured from a hidden probe carrying the word's own lang/dir — never a sum of per-character boxes — cached, device-pixel snapped, and the container's width is the only layout-affecting property that animates, one continuous tween per cycle. Re-measure on resize and fonts.ready.
  • One clock per page: the component never runs a timer; the host drives it (the dossier's locale belt, a craft plate's interval). Engine: src/components/shared/EverySentence.tsx.
  • Locale pills render through one component (LocaleTag): flag first (fixed 15×11 SVG print), code in the host's mono on the baseline; the host supplies the box.

This plate runs EverySentence: one shaped sentence breaks into glyphs and reassembles in the next language, and its width moves once per change, measured from a hidden probe. The chips set the number of re-spread beats from 1 to 5.

9. Motion discipline

  • Every loop is created paused; ScrollTrigger (or an IntersectionObserver) plays/pauses it while the section is on screen; multi-phase choreography lives on ONE timeline so phases can never drift.
  • prefers-reduced-motion short-circuits setup entirely; the markup pose IS the still (static accent pair, parked seam cut, single dither frame).
  • Dash gotchas: normalize pathLength to 1000 (GSAP integer-rounds offsets); dashes clip at a subpath's end rather than wrapping closed loops; Chromium computes dash patterns in screen space under non-scaling-stroke and ignores pathLength there — rings that dash use raw user units with neither.
  • Cache a path's source d (el.dataset.traceD) before an animation blanks it per tick — a re-run would otherwise trace an emptied path.
  • Leader joints overshoot into opaque hulls so joints are gapless; leaning shapes (scan beams) are re-projected per phase (beamAt(t)), never a translated constant.
  • The sidebar's thumbs and pills move on transform only, by WAAPI, at --pt-dur-sb (a pick) and --pt-dur-toast (the pointer), eased by --pt-ease. Their geometry is read once per layout change (a ResizeObserver per group, a MutationObserver for rows and marks), never per frame and never on hover. Before the shell settles they are placed, not moved. Reduced motion places them at once.

10. The seam (slide-to-reveal)

Both layers full-width and pinned; the handle writes one CSS var (--seam-cut) and the top layer clips to it — content never travels with the handle. State lives in the var: drags cause zero re-renders, GSAP tweens drive the same dial, the keyboard path re-reads the live computed value. The handle is the dossier refit: a 64px transparent hitbox, solid two-tone threads with the payload's ink filling the run, and a 16×40 rectangular grip tab — the light surface wears the dark chip, the dark surface the light chip. Component: src/app/d/toolchain/sections/RevealSeam.tsx (host CSS supplies the skin).

This plate mounts RevealSeam. Drag the handle, or focus it and press the arrow keys, to reveal the served translation payload under the rendered string.

11. Engine lifecycle

Every canvas/GL engine follows one contract: mount lazily (an IntersectionObserver arms the plate), pause offscreen and on hidden tabs by its own observer, render exactly one still under reduced motion, and destroy() releases everything the instance owns (shared GL contexts persist for the session by design). Ink re-resolves on data-theme flips. Where an engine has quality tiers, quality follows measured frame cost, not just the window: glyph-field's frame-time governor ratchets a slow device down the phone cut's own ladder (lean device-pixel cap, then the lean pool), down only, with pool cuts applied at idle.

12. The mobile type ladder

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:

slotvalue
--tcm-h2 / -h2-lh2.25rem / 1.18
--tcm-h3 / -h3-lh1.375rem / 1.3
--tcm-h4 / -h4-lh1.125rem / 1.35
--tcm-lead / -lead-lh17px / 1.55
--tcm-body / -body-lh16px / 1.6
--tcm-small / -small-lh14px / 1.55
--tcm-kick13px
--tcm-quote / -quote-lh22px / 1.4
--tcm-head-pt / -head-pb72px / 36px
--tcm-gap-h2 / --tcm-gap-h318px / 14px
--tcm-cell-pad28px
--tcm-box-pt / -box-pb / -box-gap36px / 38px / 28px
  • The px-fallback consumption law: every consumer reads 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.
  • Unlayered engine beats utilities: the engine's plain CSS is unlayered and Tailwind utilities live in a @layer, so engine floors outrank utility metrics (the box-gap rule beats a head's max-lg:mb-6). Heading and paragraph metrics therefore live in engine CSS, never in page utilities.
  • The late block: the ladder and its floors sit in the engine's LAST 720px block because several base rules they override appear after the earlier 720px block — same-specificity declarations only win by following them. New mobile floors go there, not in the early block.
  • The sg cut outranks the engine: v0-pages.css's mobile cut (.toolchain-root:is(.sgdh-root, …)) carries higher specificity than any engine floor, so it must consume the same tokens — an engine-only raise silently fails to render on the singularity homes. Change one, change both.

The box-air standard

Boxed cells — framed cards, bento plates, stacked dark-band cells — breathe alike on mobile: block padding --tcm-box-pt/--tcm-box-pb (36/38), and --tcm-box-gap (28px) between a box's head and its artifact. The gap is card-scoped (.tc-card > .shell-cell-head) on purpose: cells are flex columns, margins never collapse, so an unscoped head margin would stack onto the copy cells' own body margins. The seam floor rides alongside: copy never sits within 20px of a hairline (--tc-card-pad: 20px; the trust lead's 20px bottom pad).

13. The svh/dvh law

A mobile stage measures the viewport twice, and the two units never trade jobs:

  • Reach is dvh. Whatever must touch the true screen bottom — the stage's rails, the text zone — sizes from 100dvh, the real viewport with browser chrome collapse included.
  • Layout is svh. Any height that participates in layout rides 100svh, the stable viewport: a dvh layout height grows the document on every chrome toggle and jitters the scroll under it.
  • The difference is a token — the stack stage's --v0sm-bar-gap (dvh − svh, 0 while the URL bar shows) — and only overhangs (the rails' lower reach) spend it. Anything SIZED from the stage derives from the stable var, or it breathes with the URL bar.

14. The two read lines

The stack story runs on two ladders of scroll anchors, measured per beat and re-anchored on every ScrollTrigger refresh (src/app/d/_v0/sections/FullStack.tsx):

  • The read line (55%) is structural. A beat LOCKS IN when its COPY BLOCK's center takes the 55% line — the same line the sticky figure's seat centers the tower on, and, for the finale, its pin-engage. The tower's build clock and the capstone scrub both key on it. Anchors are measured on the copy itself, from FLOW geometry (the sticky finale's own rect would read the stuck pose on a mid-dwell refresh) — never on the beat window's top, which runs nearly half a viewport ahead of the copy and fires every arrival early.
  • The highlight line (80%) is the spotlight's alone. A beat's copy lights as its center rises through the 80% line — just after entering the screen, while the arriving layer is still mid-build beside it.

One scrubbed dial spans the whole read; a piecewise map (measured lock-ins in, story time out) HOLDS the clock while a row is being read and spends each gap hold → build → lock, so a layer stands locked before its own copy takes the line.

15. Chrome exceptions Kevin asked for

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.

elementexceptionowner
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 takesToolbar.css, .pt-toolbar .pt-search-btn
Search key chipkbd.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
Presentthe 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 430e3c7ToolButton.css, .pt-ib.is-solid; Toolbar.tsx passes solid
Prototemplate markthe 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-fillPtMark.tsx, PtMark.css
Sidebar nameplatethe name beside the mark on every Prototemplate route is span.pt-brand-word with b.pt-face-serif proto in Fraunces 600 and b.pt-face-grot template in Space Grotesk 500 at 14.5px, two faces outside Inter and a weight above 500; the fonts load from src/lib/brand-fonts.ts with their variable classes on the span, so /skills and /brand carry them too. The deck keeps its Inter title beside the GT markSidebar.tsx (head), Sidebar.css (head rules)

Everything else in chrome keeps the rules above: every other toolbar button takes the control corner, the palette and the help card take the card corner, the index panel is a shell column and stays square, and no other element carries a chroma or a face outside Inter.

16. The sidebar's rows

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, for a page whose own scroll writes the address), or the item of the page the reader is already on. No row scrolls the gallery. The grid's tiles and the filter's Enter follow the same rule.

A page's sections open under its row. On the page's own routes, its sections open as a run under the page's row, in the row's group (ShellSection.under: brand, atlas, marks, graphics, motion, skills). A run is flush under the row. Each row of the run draws the route's own number in the icon's slot, in the deck's number style, and a name that wraps (a run row to two lines at most, a deep heading to three) and is never cut; a title that is a sentence takes a short name (ShellItem.short). When two or more of the page's sections hold more than one row, each gets a label with a chevron and a count inside the run; a section of one row is that row. The headings of the item being read hang under it as a deep run, at 12.5px titanium, ink while read. A chevron button at the right end of the page row folds the run for the visit; the run opens again on the page's next visit. A labelled group opens when the run has 40 rows or fewer, and past 40 only the group holding the active item opens; those folds persist under gt-shell-groups. A route section that stands for a site map group (Shipped, Sites, Explorations, Archive) keeps replacing that group. The sidebar is 208px in outline density and 256px with thumbnails, the same two columns as the deck's list.

The rail marks the place. In outline density each top-level group carries one rail: a 1px --pt-hair-soft line drawn by .pt-sb-rail (src/components/viewer/SidebarRails.ts) and masked to an SVG path down the group's rows. The rail runs in a column 16px before each level's first glyph: x 18, under the group header's chevron, for the tree rows; x 34 for a run's rows and labels, a site's enterprise page and the live site's rows, whose icons and numbers start at 50; x 50 for the deep headings, which start at 66. Where the level changes, the path bends at 45 degrees across one 16px step. The marked row carries a 2px ink thumb on the rail and a --pt-sb-pill-current ground with the control corner. The row under the pointer, or under keyboard focus, carries a --pt-thumb thumb and a --pt-plate ground. The marked row is the deep heading being read when the deep run is shown, else the active item when its row is shown, else the current page's row, else the page row the reader is inside. A plain click moves both marks to the clicked row over --pt-dur-sb and turns it ink before the page arrives. When the click opens another route, the new page's list keeps the clicked row where it was and the marks travel from it to that page's mark once the shell settles. Thumbnail density has no rail: its rows lead with their captures, each run keeps its guide and the active row its 2px bar.

The current page is always marked. The row whose path is the longest one covering the pathname is current, site map row or route item alike: on /skills/<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.

On this page