# Design system

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

URL: https://www.prototemplate.com/handbook/design

The canon for every page in Prototemplate: the laws the directions and the
shipped site run on, distilled from the founder rounds. The build log on
`/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`:

| token | value | name |
| --- | --- | --- |
| `--color-ink` | `#070707` | ink |
| `--color-ink-raised` | `#101010` | raised ink |
| `--color-titanium` | `#8a8f98` | titanium |
| `--color-paper` | `#ffffff` | paper |

- Structural color everywhere derives from these plus alpha: every text step
  is ink or white at some alpha, every hairline is titanium at some alpha,
  every hatch is a thin ink or white veil.
- **Each page adds exactly one spectral accent** (toolchain: `#2f5ce0`;
  its dark-band lift: `#86a8ff`). One bright white. An accent is a
  controlled edge, never a wash.
- Pages never touch the raw values. Each root class (`.pt-root`,
  `.toolchain-root`, fork roots) publishes a **semantic layer** — surfaces,
  ink steps (`-2/-3/-4`), hairlines (`--*-hair`, `--*-hair-2`), hatch — and
  everything downstream reads those.
- **Dark mode is a token remap, nothing else**: under `[data-theme='dark']
  <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, /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 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:

| role | token | draws |
| --- | --- | --- |
| structural | `--pt-hair` | large surfaces and the lines that divide the shell: the fixed sheet's ring, the search card, the index panel's left edge, the toolbar bottom, the sidebar right edge, the deck surface index's group headers (the sidebar's group headers draw no rule), the book head's rule, the hatch band's two rules, the section dividers, the segmented control, the field boxes at rest, the install field at rest |
| row | `--pt-hair-soft` | list rows, search results, panel rows, the book head's panel rows, the help card's table rows, the fixed sheet's outer ring, the progress track, the sidebar's rail (`.pt-sb-rail`, outline density) and the run guides (thumbnail density) |
| frame | `--pt-edge` | frames of images and tiles only: thumbnails, the book's page frames, the grid tiles, the 96x54 and 64x36 captures, the hover preview's frame, and the help card |

The weights carry meaning. A frame at 0.62 alpha is the darkest line in
chrome because it holds a picture in place: the eye must find the edge of
a capture against the paper around it. A large surface (a fixed sheet, a card,
a panel, a head) is not a picture; it is the paper itself, so its edge is
the structural hairline, and its outer ring (where it has one) steps down
again to the row weight. A fixed sheet drawn in the frame weight reads as a
boxed image, which is the bug Kevin named in round six ("make the borders
around these areas the proper border colors"). The help card is the one
card that keeps the frame weight: it floats over a scrim, where the
hairline would vanish.

Nothing in chrome sets a border color from any other token or literal.
`--pt-ink` appears on a border only as a state: a pressed button
(`.is-on`), the active thumbnail or page frame (`.is-active`), the count
while it is being edited, the solid call to action, the sidebar's current
thumb on its rail, and a field while it holds focus. Book heads, table headings and the deck surface index's group
headers draw `--pt-hair`, never ink; the sidebar's group headers draw no
rule at all. Outlines are rings: the three roles, ink for focus and active
rings, paper for a ring on an ink plate.

Where two bordered components touch, exactly one draws the line:

| junction | owner | the other side |
| --- | --- | --- |
| sidebar and stage | the sidebar's right edge | the main region draws no left edge |
| toolbar and stage | the toolbar's bottom edge | the stage, the hint row and the index panel draw no top edge |
| index panel and stage | the panel's left edge | a fixed sheet's ring runs under the panel; the panel covers the progress track while open |
| group header and its first row | no one: the header draws no rule, and the group's boundary is the 16px gap above its header | the first row draws no top rule |
| a page row and its run | the group's rail (`.pt-sb-rail`, `--pt-hair-soft`), bending 45 degrees under the page row into the run; in thumbnail density the run's guide | the page row draws no bottom rule; run rows and labels draw no left border |
| a run and the deep run in it | the same rail, one more bend; in thumbnail density each run its own guide, 16px or more apart | no row draws a left border |
| the marked row and the rail | the rail; the 2px ink thumb is a state over it | the rail under the thumb is covered, not doubled; the row draws no bar |
| a row and the pills | no one: the pills are grounds and draw no line | the row draws no box |
| a page row and its fold | no one: the fold draws no border at rest, and its focus ring is a state | the row draws no right rule |
| last row of a group and what follows | the last row's bottom edge | the next header carries no top rule |
| a fixed sheet and its content | the ring (hair border, paper gap, hair-soft outline) | content draws no outer border |
| a reading column and the stage | no one: the column draws no edge | the toolbar's bottom rule is the only line above the page and the sidebar's right edge the only line beside it; content draws no outer border |
| book head and its contents | the mast's rule (`--pt-hair`), then the note when there is one | the note draws no rule; the contents grid draws no top rule; its rows draw their own bottom rules |
| the title and what is above it | no one | nothing is drawn in the `--pt-title-clear` space between the toolbar's bottom rule (the stage's top) and the h1's box, and no line crosses a title |
| book head and its panel | the panel's rows draw `--pt-hair-soft` under every row but the last | the panel draws no frame; side by side, the mast's rule closes the mast under the taller of lead and panel; on a narrow head the panel draws a row rule over its first row and the mast's rule is its last row's rule; the install field draws its own `--pt-hair` box and the row above it drops its rule |
| front matter and the first section | the band, both rules | the contents grid draws no rule against it; the first section's divider draws no rule |
| two sections | the lower section's divider rule | the upper section draws no bottom rule |
| tile and its shot | the tile's frame | the shot draws no border |
| segmented control and its options | the control's outer border | options draw no dividers; the active option is an ink pill 2px inside it with the chip corner, and the hover and the focus ring trace the same pill |
| stacked corner buttons | the upper button's bottom edge | the lower button's top edge is transparent at rest |
| sidebar head and toolbar | each owns its own side of the vertical seam | the two bottom rules meet at the sidebar's edge and never overlap |

The auditor enforces this from computed CSS. `pnpm lint:lines:shell` walks
`/`, `/brand`, `/compare`, `/archive/<first slug>`,
`/directions/<first slug>`, `/skills`, `/skills/<first slug>`,
`/handbook`, `/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.

| class | token | value | members |
| --- | --- | --- | --- |
| shell | `--pt-radius-shell` | 0 | the viewer frame, the sidebar column, the toolbar bar, the fixed sheet and its ring, the reading column, the index panel's column, the overlay layers and scrims, the progress track, scroll thumbs, the book head, the contents, the dividers and the bands; full-width list rows, which own their seams and never draw a box; an input inside its field |
| control | `--pt-radius-control` | 6px | every button (`.pt-ib`), segmented groups, fields and field-shaped buttons (the search pill, the count, the filters, the palette's field, the install field), buttons on pages, the sidebar's pills, which are grounds the pointer is on |
| inner | `--pt-radius-inner` | 5px | a part flush inside a control's 1px border: the install field's copy segment |
| chip | `--pt-radius-chip` | 4px | key caps, chips, pills, tags, inline code, a segmented control's active and hover pill (2px inside its option), and a control set 2 to 7px inside a field |
| card | `--pt-radius-card` | 6px | cards, tiles, thumbnails, figures, and popovers: the palette, the help card, the toast, the hover preview |
| round | `--pt-radius-round` | 50% | dots and avatars on a square box |

- A part n px inside a rounded box (n from 1 to 3) takes `calc(var(--pt-radius-<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.

  | step | role | size / line | tracking | 900px and under |
  | --- | --- | --- | --- | --- |
  | d1 | page title (the book head's h1) | 44 / 1.04 | -0.025em | 32 |
  | d2 | section title (a divider's h2, a brand chapter) | 32 / 1.05 | -0.025em | 26 / 1.15 |
  | d3 | h2 in prose, a record's title | 24 / 1.25 | -0.02em | 21 |

- **The text ladder.** Title 18/1.35 at 500 and -0.01em (h3, a row's
  title); lead 17/1.55 in ink; body 16/1.6 in ink-2; small 14/1.55
  (table cells, contents links, ledgers); meta 13/1.5 in titanium (meta
  tables, gutter notes, captions, th); group labels 12.5 at 500 and
  -0.005em. Weight 400 text is never tracked, and nothing on Inter is
  tracked positive.
- **Measure.** Body text at `--pt-measure` (32em, about 66 characters);
  a head's lead at `--pt-measure-lead` (30em). Tables, figures, code and
  ledgers keep the full column.
- **The book page.** Every page with a book head has one structure, rendered by `BookHead` (BookView.tsx), and differs from another only in its words and its facts:
  1. nothing is drawn in the `--pt-title-clear` space between the toolbar's bottom rule and the title (the reading column's top padding), and no line crosses a title;
  2. the title, the page's plain name from `src/lib/page-names.ts` (Brand, Documentation, Motion) or a record's own title, at d1 across the mast; after the name, a badge 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 what the page holds (`src/components/viewer/BadgeCycle.tsx`, steps(n) on one strip, so the count lives in the frames; under reduced motion the first frame stands): /brand the GT monogram and the register's monograms, /marks the register's marks, /handbook its files, /skills the skill ids, a skill its files, /graphics the numbered visuals' file stems, /motion the film slugs, a translation film its vocabulary in its own script, /blog the post slugs, a direction its pages;
  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.

## 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.

## 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 /handbook/libraries) follow the Blue Marble standard in `docs/ARTIFACT-PICTURES.md`, held by `scripts/lint/pictures.mjs`.

## 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.

## 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).

## 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:

| slot | value |
| --- | --- |
| `--tcm-h2` / `-h2-lh` | 2.25rem / 1.18 |
| `--tcm-h3` / `-h3-lh` | 1.375rem / 1.3 |
| `--tcm-h4` / `-h4-lh` | 1.125rem / 1.35 |
| `--tcm-lead` / `-lead-lh` | 17px / 1.55 |
| `--tcm-body` / `-body-lh` | 16px / 1.6 |
| `--tcm-small` / `-small-lh` | 14px / 1.55 |
| `--tcm-kick` | 13px |
| `--tcm-quote` / `-quote-lh` | 22px / 1.4 |
| `--tcm-head-pt` / `-head-pb` | 72px / 36px |
| `--tcm-gap-h2` / `--tcm-gap-h3` | 18px / 14px |
| `--tcm-cell-pad` | 28px |
| `--tcm-box-pt` / `-box-pb` / `-box-gap` | 36px / 38px / 28px |

- **The px-fallback consumption law**: every consumer reads
  `var(--tcm-X, <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.

| 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 `/handbook` and `/brand` carry them too. The deck keeps its Inter title beside the GT mark | `Sidebar.tsx` (head), `Sidebar.css` (head rules) |

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

## 16. The sidebar's rows

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
`/handbook`, 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.

---

The index of every page on this site, for agents: https://www.prototemplate.com/llms.txt
