# pages — landing, docs, studio, play

## Purpose

The routes of dither-ui.com. Each page is a thin composition over
widgets/features; page-specific conventions live here.

## Local Contracts

### landing/

- Direction (the user's picks): the page is PRINTED in the dark, ALWAYS,
  whatever theme the docs or the Studio saved: one bone ink on obsidian
  with a fine grain inside a hairline frame,
  and colour only in the kit's own art, which keeps its Bayer screen. It
  wears a reference print site's TASTE, never its devices (the user's
  call: it must not read as a copy; see root `AGENTS.md`). The dither and
  `--font-mono` stay the signature; it keeps the
  chrome display type and the chrome primary key in the dark finish of
  the site voice (`app/styles.css`, see `src/AGENTS.md`). Its HERO is our
  own (the statement beside a live print the kit makes); under it, the page layout the user picked from a reference,
  kept unnamed (see root `AGENTS.md`): sections under a title with a lead
  beside it, a bento of example cards, a stage of what an ask makes,
  columns, a centred install, link columns over a giant wordmark. Never a
  reference's hero, art, devices, assets, marks, names or words: filled
  with the kit and our own copy. The user's slop bans (root `AGENTS.md`)
  hold here as lifted for the print: the hairline frame is the page's
  only rule, and the kit's art (the poster among it) may print more than
  two inks. Otherwise: no tags,
  no italic or slanted type, no negative tracking, no over-detailed UI, no
  other dividers, two inks (`--ink`, `--ink-3`) on two grounds (the obsidian,
  `--surface`) with no alpha tints, no bordered containers, no diagrams,
  no code paths, tool names or commands as labels, no numbered sections.
  Still banned too: red (the kit's ember is the one warm light: brand
  core, install prompt, the agent's checks, glyphs in "What's inside"),
  an eyebrow over a title, and in copy middle
  dots, em dashes and arrow-ended links. Additions must remove something
  or justify their presence.
- Voice: calm and procedural. Copy says what the kit is and what to do
  next ("Copy the folder", "use it here", "ask for a screen"), never what
  it will feel like; every figure it states is read from the kit through
  `facts.ts` or `hub.ts`, never typed.
- Copy and markup follow the design.how bans the user picked: sentence
  case for headings, keys and links, no uppercase anywhere in the page's
  own copy; every link names where it goes (an
  external one says "(opens in a new tab)" to screen readers). No space
  is left empty. Everything that moves is the kit itself (real
  components, hands on); no film player or animation runtime ships.
- The landing layer lives ONCE in `chapters.css` (plain CSS under
  `.landing`, imported by `LandingPage`) over the site voice (only
  `.nav-a`, `.led`, `.chrome-type` and `.key-chrome` from it): the ground
  on `:root:has(.landing)` (obsidian `#101214`, bone `#e4e1d8`, the muted
  bone `#9c998f` at AA on both grounds, `color-scheme: dark`), so what the
  page teleports to the body (the film's dialog) is printed too; the dark
  theme stays on (`index.html` ships `class="dark"` and `LandingPage`
  re-adds it on mount, since a light theme saved on the docs or the Studio
  persists on `<html>` in this one SPA); the two inks mapped to theme
  tokens (`--ink`, `--ink-3`), `--surface` (the one raised ground, 7% of
  the ink mixed into the obsidian) and `--signal` (the dark theme's ember,
  8.6:1 on the obsidian); the grain (a 96px PNG tile of single cells a
  tone under and over the obsidian, never a grid); the frame (`--frame`,
  a fixed hairline down each side of the page's column from 900px, above
  the page, never taking a pointer); the section head (`.sec-head`: the
  `.section-title` left, in the display face (Michroma 400,
  `clamp(1.6rem, 3.4vw, 2.75rem)`) cast in `.chrome-type`; `.sec-side`
  right, on its baseline, a `.sec-lede` and a `.more-link`; stacked under
  900px), `.lead` and `.more-link` (an underline that inks on hover, a
  24px target through padding cancelled by a negative margin), `.key` (a
  filled shape on `--surface`, never a ring), `.seg` (`aria-pressed`
  buttons, quiet labels, the pressed one filled with `--surface`), and
  `[id]` scroll margins (76px) clear of the header. A card or panel whose
  own ground is `--surface` resets `--surface` to the page's ground on
  what it holds, so its keys and pressed segments stay shapes. Page geometry (`--page` 80rem, `--rhythm`) lives on
  `LandingPage`'s `.landing`; the sections are one flex column on it.
- Header: a floating capsule (`--surface`, `--float-shadow`, no rim), sticky 8px
  under the top and centred (up to 48rem): the 13px diamond mark (ember core) + "dither-ui", the `.nav-a`
  links (Components → `#examples`, Docs, Agents → `#ask`, GitHub under its
  mark in a new tab; Components/Agents drop under sm, GitHub under md) and
  the `.key-chrome` "Open studio". It MUST stay positioned with `z-10`:
  `main` is `relative isolate`, so a positioned sibling paints over a
  static header.
- Hero (`Showcase.vue`, our own): the first screen's height under the
  header (`clamp(34rem, 100svh - 64px, 52rem)`) on the page's own ground,
  holding on the page width the statement beside the print (columns
  1 : 1.2, even from 900 to 1199px so the keys keep one row). Left
  (`.statement`): the h1 in the display face (Michroma 400) and chrome at
  display size (`clamp(2rem, 3.9vw, 3.5rem)`, two or three short lines),
  the lede (three or four lines), the keys (`.key-chrome` "Open the
  studio", `.key` "Read the docs" and "Watch the film" under the Play
  glyph, which opens the trailer; `TrailerDialog` lives in the hero),
  then a quiet line of what the kit works with (`WORKS`: Vue 3, Svelte 5,
  Tailwind, StyleX, CSS Modules, CSS, each under its mark, after "Works
  with"). Right, in `.print-col`: the PRINT, a live `DitherPoster` (seed
  5, cell 4, `INKS` from `plate.ts`: the kit's blue, the ember and the page's bone ink;
  the ember diamond whole on the blue, half knocked out in bone, the open
  cells showing the obsidian like a print on black stock; its screen
  slides 1.5 cells a second inside the mark) as a portrait sheet (4:5, at
  most 30rem wide) set on the obsidian at the column's right edge with
  breathing room above and below, square cornered and flat: never a
  panel run to the frame or under the header (that is the reference's
  hero). Its pause ("Pause the print") sits on its top right corner.
  Under it the print stands over its own floor (`.print-floor`, 40px,
  out of the flow): the same poster at the same moment, turned over and
  thinned through `DitherReveal`'s down wipe held at a third of its travel
  (band 20 cells), so only the wipe's 40% and 20% steps show. A shadow
  cannot show on black; the floor is the print's own cells, never a tint.
  The column keeps 48px under the print below 900px for it. The
  statement signs off with the brand's moment (`.hero-brand`, 20px under
  the works line, `STAGINGS.hero`: the gather), its field the page's column
  (`.hero-in`, positioned): loose in the open rows under it (some cells
  under the print, which paints over them), thrown no higher than just
  above it, clear of the copy; it plays as the page opens and again under
  a resting mouse (`replay`), and the print (and its floor) hold their
  frame until it is home each time (`moving` holds, `settled` frees), one
  moving layer at a time. No
  component tour, slides or picker in the hero (the user's call: they
  took the love out of it); the components live in the bento. Under
  900px the statement comes first and the print takes the width under
  it, landscape (5:4); on a phone both keep the page's 16px gutter. The
  h1 AND the keys must clear the fold at 1440×900, 1280×800 and
  1920×1080.
- The trailer (`TrailerDialog.vue`): the kit's `DitherDialog` (focus
  trapped and handed back, Escape and the backdrop close it) titled "The
  dither-ui film", its description the film in words (no speech in it).
  The `<video>` exists only while the dialog is open: `public/trailer/`
  `dither-ui.mp4` (H.264 + AAC) first, `dither-ui.webm` (VP9 + Opus) for a
  browser built without H.264, `poster.webp`; it starts with `play()` in
  the opening tap's own tick (opening it was the ask, and iOS lets a film
  with sound start only from a gesture), `playsinline`, except under
  reduced motion, where it waits on its poster for play.
  Retake all three from the trailer's edit when the film changes (1080p,
  each about 8 MB).
- Pixel type (`pixel-type.ts`): a proportional 5×7 bitmap face, a string
  in and its lit cells out; the brand's lockup sets "DITHER-UI" in it. A
  string with a glyph the face lacks throws; every string the page sets
  is covered by `tests/pixel-type.spec.ts`.
- Facts are claims: `facts.ts` holds `COMPONENT_COUNT` (the hero's and
  the examples' ledes), `MATRIX` (the threshold matrix as the page writes
  it, read from `SCREENS`), `RUNTIME_DEPS` (the packages beside Vue, which
  the install bar's commands and the install lede's count are built from)
  and `SPECS` (the spec sheet closing the
  install: label and value rows on the raised ground, two columns from
  640px, values read from the kit where they can be: the counts, the
  screens, the license; the facts spec holds the rest to the kit's peers,
  matrices and styling systems); `tests/landing-facts.spec.ts` recounts
  the first from `dither-kit/tailwind/index.ts` and holds the second to
  the kit's required `peerDependencies` (a styling layer's optional one
  aside), and the plates' stock to the raised ground's mix; section counts
  come from `hub.ts`. A change that moves one
  fails until the page says it.
- Examples (`Examples.vue`, `#examples`, "Every component, live."): a
  section head (the component count from `facts.ts`, "See every section in
  the docs"), then the bento: `BENTO` (`hub.ts`), twenty live kit
  components on a 12-column grid row by row: the 3D model (8, two rows
  tall, flush; `sampleWorld` of a seed on a picked ramp: blue, metal or
  ember, and "New model" for the next seed) beside the rating and the
  stat card; print screens (the full row, flush: one bone-to-obsidian
  `DitherGradient` printed through a `DitherScreen` under the same ramp
  raw, the raw one in linear light so the printed cells average to it,
  clipped at a split a full-size range drags or the arrows step, its
  handle a bone disc; the five screens switched in place); the area chart (8; one seed for every fill, "Roll a seed")
  and the button (its four fills and five hues); the slider (a dither's
  settings: cell size with ticks, contrast, each with its value), the pie
  chart and the avatars; the shader (flush; the kit's seeded sample, mono
  blue, a palette ramp or its own colours, "New shader" the next seed) and
  the ferrofluid (flush; blue, ember or grey); the styling card (8, flush,
  `ExportScene.vue`: one snippet rendered live beside its code in the
  picked system through `restyle`, StyleX first; its code alone when the
  card is narrow, a size container) and the slide action ("Slide to
  deploy", its count); the bar chart and the meta balls; progress, the
  radar chart and the hold action ("Hold to purge cache", its count);
  silk and the decrypted text. Each card's art is a `DitherReveal` (the
  8×8 Bayer dissolve, 0.72s, linear): it prints itself once as it comes
  into view, at once under reduced motion. Of the surfaces that move by
  themselves (`MOVING`: the model, the shader, the fluid, the meta balls,
  the silk) ONE plays at a time (`spotlight.ts`): the card a mouse or pen
  hovers or keyboard focus is in; else the reader's last choice while any
  of it shows; else the first, in page order, crossing the middle tenth of
  the viewport; else the last that played. The rest hold a still frame.
  The shader's WebGL engine, the
  translator's utility map and the fluid's engine are async chunks a
  card loads as it nears the viewport (one IntersectionObserver, 600px
  ahead; without one, at once); their card art keeps a wrapper so the
  flush rule never stretches the pause chip. The demos speak the kit's
  blue, the ember for a second voice and grey for a third (avatars
  `color="blue"`, the silk and meta balls on blue stops; the button's hues
  are its subject); no chart grid. Two columns under 1024px (the wide
  cards and the halves span both, the rest pack densely), one under
  640px. A card is the raised ground (`--surface`, no border, 20px
  radius) holding its art over its name, the link into `/docs/<id>` (no
  family label). Interactive art keeps its own controls, so the card
  itself is never a link; on flush art the pause chip comes before the
  tools in the DOM, as it sits above them.
- `hub.ts` is static data (the landing never loads the docs IA): `FAMILIES`
  (every docs family and its size), `DOCS_FAMILIES` (the five big
  families, a line each and the section each opens on) and `BENTO`, held
  honest by `tests/hub.spec.ts` (families and counts equal `GROUPS`, every
  card and first section is a section of the family it names).
- Ask (`#ask`, "Your agent builds the screen."): a section head ("Read the
  agent skill"), then `AgentStory.vue`, one agent session played as the
  page scrolls: a stage pinned under the header (`--pin`, at most 30rem)
  for a run of a notch of scroll (40svh) per step, seven steps
  (`story.ts`). The stage is one raised ground: left, the screen the
  agent lays out (the page's ground, no ring or handles) building row by
  row (the heading and count, the meter, the switch; every part a live
  kit component), and before it lands a line saying where it lands;
  right, the ask as a sentence in ink, then `DOES`, what the agent does in
  words, each line under the ember check as it happens: reads every
  component and its props (`get_registry`), lays out a Deploys screen
  (`add_screen`), writes it as a Vue file (`get_code`), renders it as an
  MP4 (`export_video`). No call times, arguments, tool names, paths or
  commands show. The step is the marker crossing the viewport's middle
  (one IntersectionObserver, armed after the markers render; no scroll
  listener). What has not happened yet keeps its place, hidden and out of
  reach, except the screen's switch: a real control, it keeps its tab stop
  whatever the scroll (out of the pointer's reach until it shows), and a
  keyboard that reaches it lands the whole screen at once. Under 900px, or without an observer, nothing pins: the whole
  session shows at once, the screen over the session. Screen readers get
  the whole session as a caption; the stage is decoration. A screen is
  made ONLY of registry components (`add_screen` lays them out), so every
  line is true.
- Inside (`#inside`, "Three parts, one engine."): three columns (one
  under 900px), each under its kit glyph in the ember with a paragraph and
  its link: the kit, the Studio, the agent bridge (with the harnesses
  `bridge/dither-bridge.mjs` knows, its `HARNESSES` ids and names in
  order, each under its own mark; keep in sync, `tests/marks.spec.ts`
  checks it). Under them the five big families (`DOCS_FAMILIES`): five
  columns (three under 1024px, two under 640px), each the family's name
  and size as the link to its first section, and its line.
- The brand's moment (`brand.ts` + `BrandGather.vue`, decorative): the
  brand as `LOCKUP`, `MARK_CELLS` (the diamond's 25 cells around its ember
  core, which the header draws too), three clear columns, then
  "DITHER-UI" in the pixel face, 4px cells seven tall, as spans, each
  knowing its glyph (the mark 0, the letters from 1). Its parent places
  and positions it; its FIELD is its nearest positioned ancestor, and the
  parent stages it (`Staging`: the field's height and the lockup's top in,
  a `Scene` out: its `move` and, in px of that field, the rows its cells
  lie loose in, the rows the gather's burst may throw them to, a seed).
  ONE table, `STAGINGS` in `brand.ts`, stages the hero's and the
  footer's; `plate.ts`'s `sceneOf` the plates'. Each place forms the brand
  its OWN way (the user's call: never the same moment twice on the page),
  one of `MOVES`:
  - `gather` (the hero): the mark's cells lie loose like stars, gather
    left to right into the mark, which holds a beat and bursts to the
    right in a widening cone (a cell thrown past the burst rows is turned
    back), the wordmark's cells, carried unseen on its bone cells until
    then, flying out with it; then each settles home in order across.
  - `rain` (the plate after the bento): the cells hang in one dotted line
    on the scene's first row and fall column by column, left to right,
    each column landing from the bottom up.
  - `sort` (the plate after the ask): a scrambled cloud collapses into the
    lockup's seven rows, top row first, then each row sorts sideways into
    place in order across (a refinement).
  - `converge` (the plate before the install): the seven rows slide in
    whole, even rows from the field's left edge and odd rows from its
    right, top row first, and interlock.
  - `strike` (the footer): a typewriter; the glyphs wait stacked at the
    carriage on the scene's first row, and one by one each slides out to
    its column and strikes down onto the line.
  It plays the same at every width, phones included (nothing gates it on
  pointer or size), once, when the lockup stands in view clear of the
  viewport's bottom tenth, and stays; with `replay` (the hero's alone) a
  mouse or pen coming to rest on the lockup plays it again from the top,
  once it is home (never over a moment still playing or still held for its
  first view, never for a finger, never once reduced motion is asked for,
  read at the hover). `choreograph` is pure (a path of
  waypoints per cell, each a whole number of cells from its slot; one
  function per move in a table) on `MOMENT`'s clock, played as two stepped
  animations per cell (an axis each, `steps()` one cell a step). Home by
  4.6s: under five seconds, so no pause; under reduced motion, without
  `IntersectionObserver` or `animate`, and in the prerendered bytes, the
  brand is simply assembled (and a hover replays nothing). It emits
  `moving` as its moment starts (the first time or again) and `settled`
  when home (at once when it does not move), so a parent keeps one moving
  layer at a time; `tests/brand-replay.spec.ts` holds the replay.
  `tests/landing-facts.spec.ts` holds the mark's cells to the diamond, the
  five places to five different moves, each move's opening (rain's one
  line, converge's rows at the edges, sort's scramble, strike's stack at
  the carriage), and every place's moment: home inside five seconds, every
  path in time order, on its grid, inside the field, starting in its
  scene's rows; the gather's burst inside its rows and each wordmark cell
  on its host's squares until it.
- The plates (decorative): a series of print sheets between the sections,
  after the bento, after the ask and before the install, each the brand
  small in a wide field. ONE table, `PLATES` in `plate.ts`, sets each
  plate's tone at the top edge, its clear band and its tone at the foot,
  where the mark's centre stands in the band (left of 0.7, the wordmark
  reading on to its right) and its move; `sceneOf` gives its scene (loose in
  the band and the tone's thin edges, thrown anywhere on the sheet,
  seeded by where the mark stands). `Plate.vue` prints a row: a 2.35:1
  sheet on the page's width, square cornered and flat like the hero's
  print, on `STOCK` (the `--surface` mix as a colour the kit prints with,
  so its open cells are the plate, never a tint), two `DitherGradient`s in
  the bone ink through the Bayer screen showing only the thin ends of
  their ramps, and the brand's moment in the band, pulled in from the
  right edge (16px clear) where the plate is narrow, while the tone prints
  in through `DitherReveal` (0.72s, linear, the lights coming on).
  `tests/landing-facts.spec.ts` holds `STOCK` to the token's mix and every
  plate's mark inside its band, its tones thin.
  A new plate is a row in the table and a `<Plate>` at a section break.
- Install (`#install`, "Copy one folder. Every pixel is yours."): the
  head centred (the lede, then the `.key` "Copy agent prompt" through
  `DitherClipboard`: the prompt names this origin's `SKILL.md`, the degit
  command, the four packages and the alias), then `InstallBlock.vue`: a
  box on the raised ground whose top row is the `.seg` step picker (the
  kit's Folder glyph, then npm, pnpm and Bun's own marks) and the Copy
  key (both filled with the page's ground), the command under them in
  mono with the ember `$`. A copy is ONE gesture: while copied the
  command shows selected (the page's ground behind it) as the key's label
  rolls to "Copied" under a check; both let go together; reduced motion
  swaps both at once. Its four steps (degit folder + npm/pnpm/bun deps)
  stay in sync with `docs/install.ts` `INSTALL.vue` (the landing never
  imports the docs). Then one centred line for Svelte 5
  (`dither-kit-svelte`, the quick start).
- Marks: tool logos (the package managers, Node, Vue, Svelte, Tailwind,
  StyleX, CSS Modules, CSS, GitHub and the coding agents) come ONLY from
  `src/shared/ui/marks.ts` through
  `ToolMark`, one-colour in `currentColor` beside the tool's name, never
  restyled or dithered; everything else is a kit glyph (`DitherIcon`).
- No entrance choreography but the bento's art printing itself once as it
  nears and the brand's moment (the hero's sign-off as the page opens,
  each plate's and the footer's brand as it nears); the hero's print waits for it, and the type
  simply is. Motion is the live kit
  surfaces, one moving layer at a time in the bento. Pause chips
  (`PauseChip.vue`, WCAG 2.2.2): every surface that moves by itself for
  more than five seconds carries one in its corner, named for it ("Pause
  the print", "Pause the model", "Pause the shader", "Pause the fluid",
  "Pause the meta balls", "Pause the silk"), `aria-pressed` while the
  surface is still, wired to its `paused` (the model's turn is its
  `auto-rotate`): a disc of the page's ink with its glyph in the ground.
  In the bento a chip shows whether its surface plays: a pause is the
  reader's and outlasts the spotlight; play on a surface the spotlight
  holds makes it the reader's choice. A touch chooses only through the
  chip, since a tap also focuses and would point at the card and then
  toggle what it just started. Chips stay under reduced motion, where
  every surface paints one frame and holds it.
- Footer ground (`road.ts`): the kit's art under the whole footer but the
  wordmark (`.foot-art`), one `DitherShader` (loaded with the footer, cell
  `ROAD.cell` CSS px at dpr 1, so the print reads the same on every screen
  and its width in cells is the art's CSS width) whose GLSL `road.ts`
  writes from `ROAD`: a star grain, rare
  and blue over the columns, and on the road (`.foot-road`, the width's
  `ROAD.aspect` share, `narrowAspect` under `ROAD.narrow` px of the art's
  own width: a container query the shader matches by its width in cells,
  pinned by `tests/landing-facts.spec.ts`) a bundle of light trails that
  runs under the brand's diamond (`BrandMark`, placed at `ROAD.mark`) and
  sweeps out and down to the right, widening, a spray of the grain along
  it. Luminance picks the ink through `ROAD_RAMP` (the page's ground, then
  the print's `INKS`), so the trails burn bone at their heads and core and
  cool through the ember to blue. The art dissolves at its edges into the
  page (in from the footer's top, out toward the wordmark, the trails'
  tail out of the dark at the left), never showing a box. Only the heads
  move, and only while the reader moves the page (a scroll, a pointer
  across the footer), settling 240ms after: motion the reader drives, so
  no pause chip (the user's call); they hold while the footer's brand
  forms (`moving` holds, `settled` frees); under reduced motion one frame.
  Decorative (`aria-hidden`); no rule, no boxed icons.
- Footer (on the page width, no rule above it): the brand (the brand's
  moment in the home link, whose name is "dither-ui", `STAGINGS.foot`:
  the strike, its carriage on the clear band above it, the footer's top
  padding, clear of every line of text; its field the page's column,
  `.foot-in`, positioned; then one line, GitHub under its mark) beside three link columns
  (`COLUMNS`: Get started, closing on Verification; Docs, the big families each with its size; For
  agents: SKILL.md, llms.txt, the registry, the Studio; 28px hit areas), the
  copyright line as separate items (the year and dither-ui.com,
  `v<version>`, MIT license), the road, then the line-screen wordmark. Three columns
  under the brand under 900px, two under 640px.
- Footer signature: the line-screen wordmark (`LineWordmark.vue`):
  "dither-ui" in the display face across the page width, filled by 24
  rows of ink that thicken toward the baseline (30% of the row's pitch at
  the top to 92% at the foot, each bar at the foot of its pitch), a
  vertical ramp printed through a screen of lines. One SVG: the text is a
  `clipPath` (its `textLength` holds the word to the box's width whichever
  face has loaded) and the rows are rects in `--ink`; flat, one ink,
  decorative (`aria-hidden`), nothing that glows or moves.

### docs/

- Sidebar IA (after Base UI): Overview · Handbook (Styling, Composition,
  Seeds, Engine, Verification, Animation, Accessibility — prose +
  CodeBlock, no DemoCard; Engine, `handbook/EngineSection.vue`, names the
  parts under every surface, each with a figure read from the kit's own
  constants and a link to its section, a dead id failing the render;
  Verification, `handbook/VerificationSection.vue`, is a lab report: each
  claim of the kit (`CHECKS` in `handbook/lab.ts`: what, how, against
  what, the spec that holds it) measured with the kit's engine in the
  reader's browser as the section nears, one check per task, again on
  "Measure again", the engine and time as its conditions, the results
  copied as a Markdown table; the static page ships it unmeasured, and
  linked from the landing footer) · Examples ·
  Components · Backgrounds · Text · Animations · Utils. Section ids are permanent deep links — relabel freely
  (`motion` → "Animation") but never rename an id.
- Component section anatomy: `<section id>` → heading row (h2 in ink,
  `text-lg`, + optional "Open in the Studio") → muted description →
  `DemoCard` (Preview/Code tabs as machined segments in a `.track`, the
  picked one `.ring-on`; Inspect → the workbench, Code → the source panel, a
  `.hardshell` cut at 12px) → optional picker gallery (an `h3` in
  sentence case, 12px muted, + grid) → `PropsTable`, a spec sheet (a
  `.hardshell` cut at 10px, its column heads `.spec`; a row's `default` is
  its value as code; "" prints "none"). The preview frame is a gel well
  (`.demo-well` + `.rim`: the pebble radius, the well's ground, a gloss on
  its top edge, recessed into the page) whose ground MUST stay one plain
  colour (the workbench's contrast reads the background through
  `backgroundBehind`, which gives up on any ancestor's image), that never
  clips (the workbench draws past it) and keeps its 1px border, clear
  (captures add it). Its padding is 16px on phones and 40px from sm (the
  docs column is 16px from the screen's edge on phones, 24px from sm, as
  the user's design system sets), and every demo fits its well from
  320px up: a wide one (pagination, the bracket, the ASCII art) is
  centred inside an `overflow-x-auto` row by an `mx-auto w-max` box, so
  it centres while it fits and scrolls on the narrowest phones; a fixed
  width becomes `w-full max-w-*`. Chips (framework, styling, Inspect) sit
  in tracks.
- Docs copy follows the landing's bans: no em dashes or middle dots in
  prose, prop `type` notes as `type: note` or `type (note)`, no uppercase
  or tracked label, no arrow-ended link; a snippet's copy matches what its
  demo renders (change both together). Kit default strings quoted in a
  table stay the kit's (the marquee texts keep their dots). The only
  uppercase in the docs' own chrome is `.spec` (the sidebar's group
  counts, the API table's heads) and `.speed-tag` (the title's tags).
- DemoCard is the page's perf seam: its preview `<slot>` mounts only as the
  card approaches the viewport (IO, 600px rootMargin) and LATCHES — once
  revealed it stays, so demo interaction state never resets on scroll; the
  Code tab mounts `CodeBlock` on first open (`v-if` — under the old `v-show`
  every hidden code block highlighted at initial mount; the panel then
  stays mounted under `v-show`, so edits survive the Preview tab). Section text,
  standalone Handbook CodeBlocks and PropsTables render eagerly, and the
  preview frame keeps `min-h-[280px]` so the box exists from first paint and
  below-fold growth never shifts visible content. Without IO (jsdom) the
  slot renders immediately — tests stay on the eager path.
- Inspect: the `aria-pressed` Inspect chip on the Preview tab mounts the
  workbench (`widgets/workbench`, loaded lazily on first use) over that
  card's preview — Kobra's inspector and tool tray plus the kit's own
  tools (see `src/widgets/workbench/AGENTS.md`). The card marks itself
  `data-demo-card`; its workspace key is `<section id>:<n>` (its place
  among the section's cards); while inspecting the frame takes a bottom
  lane for the tray (`pb-16`) and `role="group"` + tabindex; the Code tab
  closes it, and its exit returns focus to the chip.
- Code: the source panel (`widgets/source`, lazy) — the example as a
  complete file, the kit files it imports, `theme.css` and `prompt.md`,
  editable, copied, zipped and run. The card hands it the authored
  snippet, `docsCode`, the framework, the styling system and where it sits
  (section, place, the section's h2 and first line, its URL, `INSTALL`). A
  run replaces the demo in the frame (shown above the source while it
  runs, with "Restore the demo"), carries its CSS in one `<style>` per card
  and its tokens on the frame (`data-run-*`); the workbench inspects a run
  like the demo.
- Interface sounds (`shared/lib/sound.ts`): the header's "Interface sounds"
  toggle (`aria-pressed`, remembered) beside the theme toggle (which plays
  its own chord); the Preview/Code tabs, the framework and styling chips
  (only when they change something), Inspect on/off and copies sound.
- Galleries and chip rows are PICKERS, not decoration: `aria-pressed`
  buttons drive the main preview's props; chart previews also bump a
  replay token so the kit's dither entrance is the transition. Code tabs
  are computed from the picked state — what you see is what you copy.
- Docs serve Vue AND Svelte: snippets are authored once in Vue; `docs/svelte.ts`
  derives the Svelte flavor (`toSvelteCode`) for DemoCard code tabs, Handbook
  CodeBlocks (`fw()`), and PropsTable's modelValue/v-model display mapping. The
  global `docsFramework` ref (header toggle + per-card chips, persisted to
  localStorage) drives all of them. The translator parses real export names
  from `dither-kit-svelte/index.ts?raw` (plain-named background/text/animation
  families rename automatically); `V_MODEL_TARGETS` maps the two non-`value`
  bindables (Sidebar→collapsed, SidebarSub→open). New Vue idioms in snippets
  need a translator rule + a `tests/svelte-code.spec.ts` case — never a
  hand-forked Svelte snippet.
- Docs serve any styling system: `docs/svelte.ts` `docsCode()` is the
  framework translation followed by `shared/lib/restyle.ts` with the
  reader's `styling` (header switch at md+, per-card chips beside
  Vue/Svelte; `docsStylings` hides CSS Modules under Svelte and
  `docsStyling` shows CSS in its place). Every DemoCard code tab and
  Handbook `fw()` block goes through it, so a snippet idiom the translator
  mangles gets a rule in `restyle.ts` + a `tests/restyle.spec.ts` case.
- `SNIPPETS`/computed code must match what the demo renders — it now
  RUNS in the source panel, so a snippet that leaves out what the demo
  has (a chart's height: the chart snippets carry `class="h-64"`) renders
  differently there; API tables
  mirror actual kit prop defaults — update both when the kit API changes.
  Core form controls share Field-generated IDs, help/error relationships, and
  unified focus/invalid/disabled states; docs examples should show that path.
- Wayfinding: scroll-spy (IntersectionObserver, rootMargin -56px top) sets
  `activeId` + `aria-current`; clean `/docs/<id>`, its directory form
  `/docs/<id>/` (where a static host serves the section's prerendered
  page) and legacy `#/docs/<id>` deep links all restore and remain
  shareable.
- Narrow chrome: under sm the Vue/Svelte switch shows its marks and Search
  its glyph (the words stay for screen readers), the studio pill drops (it
  lives on every chart section's "Open in the Studio") and the ⌘K kbd
  hides; the styling switch (on every card anyway) and GitHub (in the
  footer) return at lg. The section list is a grouped disclosure
  ("Browse sections" button with aria-expanded/aria-controls, group headings,
  two-column links, closes on tap) instead of the old flat wall of all ~220
  links. A `CodeBlock`'s copy chip takes its own band over the first line
  under sm (a long first line ran under it there).
- `scrollTo(id)` is computed `window.scrollTo` + a settle loop (re-align after
  motion stops, ≤8 tries, bottom-clamp aware): lazy demo reveals and
  content-visibility seeds move the target while the smooth flight is still
  running, and Chromium's `scrollIntoView` refused the short re-aligns — taps
  landed a screen short. Deep links keep the `jump()` path.
- Share cards (`docs/og-card.ts`, the art; `crawler-files.ts`, the cards,
  their embeds and the renderer page): a card is the page's title (ember
  stop) over the house dither in motion, the brand top left, a section's
  family as a hairline tag top right, its address under the title; a
  section's field of three seeded bodies stays out of the title's corner,
  the routes roll two horizons (SKY, then the kit's purple seed as a
  ramp) under stars. Every motion turns a whole number of times per loop
  (36 frames at 10 fps), so the GIF's last frame meets its first; the PNG
  is frame 0. Copy follows the bans (no uppercase, no eyebrow over the
  title, no middle dot or em dash, buttons that name where they go).
  `ROUTE_CARDS` holds the route cards' copy; their ids may never be
  section ids. Guarded by `tests/og-card.spec.ts` (loop, determinism, the
  clear title corner, the lattice, Bayer order, balanced wrap) and the
  share-card cases in `tests/crawler-files.spec.ts` (every page's embed
  under Discord's 3,000 byte cap with link buttons only, the oEmbed photo,
  each route head carrying its card's tags, the bans).
- Section IA lives in `docs/groups.ts` — the single source for the sidebar,
  the `/docs/<id>` deep links, and the build-time crawl files
  (`crawler-files.ts` generates `dist/sitemap.xml`, `dist/robots.txt`, and
  `dist/llms.txt` via the `crawlFiles` plugin in `rstack.config.ts`; no
  hand-maintained
  public copies). New sections are added to a `*-nav.ts` pack and spread
  into a group there — never to the sitemap or llms.txt.
- Search metadata: `docs/seo.ts` derives per-section title, canonical,
  description, and BreadcrumbList JSON-LD from `GROUPS`. `DocsPage` applies
  them to the DOM as `activeId` changes (scroll-spy, deep links, search), so
  Google's renderer sees unique metadata per `/docs/<id>` page. Unknown or
  empty ids fall back to the generic `/docs` metadata; keep ids unique.
- Chrome: the header is the site's floating `.capsule` (`.docs-capsule`,
  44px in a 56px band so the rails' `top-14` and the scroll-spy line
  hold; its smoked polymer at 93% under 640px, where dense tables pass
  under it); the toggles in it are tracks, the picked one `.ring-on`,
  Search a `.nav-pill`, "Open studio" the `.key-chrome`; the "Docs" label
  shows from xl, so the capsule fits from 360px up. The search
  hint names the visitor's modifier (`⌘K` on Apple platforms, `Ctrl K`
  elsewhere — the handler accepts both). The brand carries the landing's
  7x7 diamond mark, and the studio link shows from sm. The Vue/Svelte
  switch carries the tools' marks
  (`ToolMark`), the styling switch their names (`STYLING_LABELS`), sound
  and theme the kit's glyphs (Volume/VolumeOff, Sun/Moon), GitHub its
  mark. The sidebar's groups sit under their glyphs (`GROUP_ICON`), each
  with its section count as a `.spec`; its links and the "On this page"
  rail's are 25px targets at a 27px pitch. The page title is the h1
  "Documentation" in the display face (`font-display`, 1.6rem) and
  `.chrome-type`, no eyebrow; on its
  baseline, the kit's spec as `.speed-tag`s (the section count, Vue 3,
  Svelte 5, Tailwind or StyleX). The "Browse sections" disclosure is a
  `.nav-pill`. At xl the rail right of
  the content is "On this page": the section in view, its gallery headings
  and its API table, read from its DOM as the scroll-spy moves (headings
  inside a preview are the demo's), each a button that scrolls to it.
  Under it, Share (`SectionShare.vue`, a disclosure: `aria-expanded`, the
  name "Share this section"): opened, the section's share card playing
  (`og/<id>.gif`, loaded only then, hidden where no build drew it) and
  three copies with the copy sound and a polite announcement: the link,
  the card as a Markdown image and as an HTML image, each linking back to
  the section (absolute `dither-ui.com` URLs, so they embed anywhere). The
  footer: dither-ui.com, GitHub (its mark), `v<version>` and MIT license as
  separate items.
- Chart sections link to `/studio#new/<type>` — keep in sync with `CHART_TYPES`;
  Studio also accepts legacy `#/studio/new/<type>` links.
- Section packs live in subfolders as self-contained components (sections +
  snippets + local state) with a sibling `*-nav.ts` exporting nav items;
  DocsPage imports both and spreads the nav into the right group.
  `docs/examples/` = Examples packs, `docs/components/` = component-doc
  packs (form/feedback/structure), `docs/backgrounds/` = the full-bleed
  generative canvas surfaces (aurora, faulty-terminal, ferrofluid, ...),
  `docs/text/` = DOM/CSS text animations (gradient/shiny/glitch/split/...),
  `docs/animations/` = interaction/motion effects (content reveals, animated
  borders, cursor + hover effects; a section with its own state lives in
  its own file, as `TransitionSection.vue`, the page transition's stage,
  mounted where the nav orders it; so does `backgrounds/PosterSection.vue`,
  the poster with a seed to roll and its screens, and
  `utils/ScreensSection.vue`, `print-screens`: one `DitherScreen` around a
  chart, an avatar and a gradient, its screen picked by chips). New
  packs follow this shape instead of growing DocsPage. `docs/primitives/` =
  the Primitives pack (Box, Text, Divider, VisuallyHidden — the paste-list
  foundation family; new plain primitives extend it + PRIMITIVES_NAV).

### studio/

- Boot order in `StudioPage.vue` matters: `hydrate()` → `startAutosave()` →
  `startHistory()` → deep-link handling (`/studio#new/<type>` or legacy
  `#/studio/new/<type>`), so deep-link artboards are part of the restored doc
  and undoable; the URL is cleaned via `replaceState` to prevent duplication.
  `#new/film` builds the launch film (`addLaunchFilm`, see `src/AGENTS.md`):
  five kit frames and the film cut from them, selected.
- The Studio's chrome speaks the SITE voice, like the landing and the
  docs: `font-mono` on the page root (the context menu and the kit's
  dialogs, teleported to body, carry it on their own panels), the 7×7 diamond brand, `.micro` panel and
  section labels in sentence case, `.ring-on` pressed states, neutral
  focus borders, and the look's materials: the floating toolbars, the
  selection toolbar and the zoom bar are `.capsule`s (their tools
  machined, 8px),
  the Library key and the empty canvas's "Add an artboard" are the
  `.key-chrome`, the Layers/Agent/Inspector panels and the dialogs
  (export, video, shortcuts) are `.hardshell`s (12px cut on panels;
  nothing in them pops out, the context menu is teleported), the video
  dialog's download is the `.key-chrome`, and the menus (project, library,
  context menu) are rounded cards with a `.rim`. Every glyph button is a
  kit icon (undo/redo, layers, properties, agent, theme, close, zoom, fit,
  replay, menus); the selection actions are a glyph and a sentence-case
  word (Duplicate, Group, Delete, Evolve, Reel, Data, Code, PNG, Video),
  the words hidden under 1024px with the name kept. Copy follows the
  landing's bans: no middle-dot separators (the Agent panel's status and
  key hints are layout items; its transcript markers stay the composer's
  glyphs), no em dashes, no uppercase label, no eyebrow (the phone notice
  is a plain h1). The artboard SURFACE
  (`[data-artboard-surface]`) stays `font-sans`, the player's font, so
  what is designed on the canvas is what the video export renders.
- Studio is canvas-first: Toolbar floats over the full-bleed canvas (a
  `1fr auto 1fr` grid, so the Library key stands at the true centre
  between the two capsules); Layers,
  Agent and Inspector are dismissible overlay panels (Layers and Agent share
  the left slot — opening one folds the other); the searchable Library is the
  single insertion surface for charts, bespoke widgets, every public kit
  component, screens, and presets. The selection toolbar's "evolve" places a
  generation of the primary selection.
- `StudioPage` opens the agent protocol on mount (`installStudioAgentApi`)
  and closes it on unmount; a project `.json` dropped anywhere on the studio
  loads through `importDocument`, the same path as Open file.
- The selection toolbar's `video` opens `VideoDialog`
  (`features/export-video`, lazy like `ExportDialog`): length, frame rate
  and theme, then download the self-contained HyperFrames composition,
  open the live player preview, or copy the render command.
- The Agent panel (`widgets/agent/AgentPanel.vue`, lazy) is a CONTROL
  PLANE for the user's own harness, in the pi / omp / Claude Code composer
  idiom, over `features/agent/acp.ts`. The head's harness chip (LED while
  one runs) opens the picker: every harness the bridge knows with its PATH
  status, a custom-command field, and stop; `/harness <id | command>` is
  the same. The choice persists and starts again on the next prompt. A
  mode chip appears when the harness has modes (click cycles, `/mode <id>`
  sets). One transcript: `›` your prompt, `⏺` prose streamed in place with
  a `▍` caret, `⇢` a follow-up that started, `⚙ name · args ⎿ result` as
  one `<details>` per Studio tool call (orange `×` and auto-open when the
  Studio rejected it), `⚙/✓/×` activity lines for the harness's own tool
  calls, `☰` plans with per-step status, `⚠` permission prompts answered
  inline ("auto" answers them with their allow option), `·` system lines,
  and a `✻ working… 4.2s · 1 queued (esc to stop)` line. Keys: Enter sends,
  or QUEUES a follow-up while the harness works (ACP has no steering);
  Alt+Up takes the newest queued message back; Esc stops and returns the
  queue to the editor; Shift+Enter newline; ↑/↓ prompt history on a single
  line; Ctrl+O folds every tool block; `/` opens the command listbox (↑/↓
  + Tab/Enter pick): ours (`/help /harness /connect /new /mode /auto
  /evolve /select /code /copy /session /undo /clear /stop /disconnect`)
  plus the harness's own advertised commands, which go to it as text.
  Follow-ups run one at a time, in order. The status line shows the
  harness, session tokens (summed from each turn's usage), the last turn's
  time and an `auto` badge. The conversation persists per project and
  follows the active project; unanswered approvals are not saved. ⚙ holds
  only the bridge address and the auto switch: no key or model setting
  exists in the app.
- Child-only kit exports render as the smallest valid parent composition; do not
  add broken isolated previews merely to satisfy registry coverage.
- `ShortcutsHelp` and lazy `ExportDialog` mount here; keep them on the page, not
  inside widgets. The export dialog's `styling` control (Segmented over
  `STYLINGS`) writes the SFC in the reader's system — the same preference
  the docs use.

### play/

- The player: ONE Studio frame rendered by the Studio's own renderers
  (`ChartRenderer`, `WidgetRenderer`) at its frame size with no editor
  around it — the page an exported HyperFrames composition carries and the
  page the Studio opens for a live preview. `source.ts` reads the document
  from the composition's embedded `#dither-document` JSON (with
  `data-artboard` / `data-theme` on `#dither-stage`) or else from the hash
  `#doc=<base64url document>&artboard=<id>&theme=<dark|light>`; documents
  are untrusted and go through `parseDocument` (persistence) before they
  render. `main.ts` installs `directFromHyperframes()` before mount and
  registers `window.__hf.buildReady["dither-ui"]` as fonts + Vue's mount
  flush + one macrotask — never an animation frame: the renderer drives
  Chrome with begin-frame control, so frames and rAF only happen when it
  captures one, and every surface paints its moment synchronously on seek
  (a chart that measures later repaints the moment through its `wake`).
  `play/index.html` is the route's entry (noindex, not prerendered) and the
  same `main.ts` is the entry of the single-file bundle
  (the `player` environment of `rstack.config.ts`). Nothing here reads the
  project store: the
  page provides `REEL_POOL` (its parsed document's artboards) so a reel
  cuts between the frames that travel with it. Besides `hf-seek`, `main.ts`
  exposes `window.ditherClock` (`seek(seconds)`, `release()`, `directed()`,
  `time()`) for any driver's `evaluate`; `scripts/render-frames.mjs`
  (`npm run frames`) renders a composition or player URL to PNGs and an MP4
  through that contract in Playwright's Chromium, WebKit or Firefox.

## Verification

- `tests/landing-facts.spec.ts` recounts `COMPONENT_COUNT` in `facts.ts`
  and holds every count the site states to it (`index.html`, the README,
  SKILL.md, `llms.txt`'s source, the Studio film's counters, which also
  count the sections and the Backgrounds);
  `tests/hub.spec.ts` holds `FAMILIES`, `DOCS_FAMILIES` and `BENTO` to the
  docs' `GROUPS`; `tests/pixel-type.spec.ts` every string the pixel face
  sets.
- `tests/spotlight.spec.ts` holds the bento's one-moving-layer rule (the
  pointed card, the reader's choice while it shows, the first crossing
  the middle, the last that played).
- `tests/lab.spec.ts` runs the Verification section's checks in Node,
  each one holding (the inks check among them: the landing's bone, muted
  bone and ember printed through the matrix on the obsidian, 4.5:1 at
  full tone and 3:1 at half, its colours held to `chapters.css` and the
  dark theme's ember), every named spec existing, and pins `POSTER_HASH`
  (the poster's bytes, the cross-engine row): a change to the poster's
  arithmetic fails it until the hash is recorded again.
- `tests/marks.spec.ts`: every harness the bridge starts has a mark under
  its id and name, the landing lists them in the bridge's order, every
  mark is path data in a positive box.
- Browser walk after changes: the landing at desktop AND phone widths (no
  horizontal scroll), the h1 and the keys above the fold at 1440x900,
  1280x800 and 1920x1080 with the print beside them, the
  obsidian, its grain and the frame's hairlines whatever theme the docs
  saved (dark on a light preference too),
  "Copy agent prompt" (the install's head) copying a prompt that names
  this origin's SKILL.md, no uppercase label, eyebrow, middle dot, em dash
  or arrow link in the rendered copy, every svg decorative or labelled,
  "Watch the film" (the dialog opens with focus in it, the film plays,
  Escape closes it, focus returns to the key, the video leaves the DOM),
  the bento's rows whole at 1440, 800 and 390px with every card drawn and
  its controls working (the model's ramps and "New model", "Roll a seed",
  the button's fills and hues, "New shader" and its outputs, the fluid's
  palettes, the styling systems, the slide and hold counts, the sliders'
  values, the rating), the lazy cards loading as they near, each pause
  chip stopping its surface, at most one bento surface playing at every
  scroll stop (a mouse hover, keyboard focus and a chip tap each taking
  it as set above), each card's art printing once as it nears,
  the ask story pinned and stepping its lines and the screen's parts as
  the page scrolls (the whole session at once under 900px), the "Works
  with" marks, none of the slop bans in the rendered page (no tag, italic
  or negative tracking outside a kit component, no rule but the frame, no
  bordered container), the header capsule floating over the page with an anchor
  landing clear of it, reduced motion (the surfaces
  still, the film waiting on its poster), and the workbench readings over
  the page (the inspection runner's contrast, audit and Tab walk) clean;
  canonical and legacy docs deep links (`/docs/avatar`, `#/docs/avatar`),
  and Studio deep links (`/studio#new/pie`, `#/studio/new/pie`) each
  create/select exactly one artboard (`#new/film` six: the film and its
  five frames).
- Docs and Studio copy scan (a headless page walk): the rendered text of
  `/docs` (every section) and of `/studio` with each panel and dialog open
  (layers, agent, library, project menu, code, video, data, shortcuts,
  context menu) holds no em dash, middle dot, uppercase label or
  arrow-ended link outside code, kit default strings and kit component
  looks; the docs header fits from 360px to 1280px; the "On this page"
  rail lists the section in view at xl, and its Share opens on the
  section's card playing and copies its link, Markdown and HTML.
- Share cards after a full `npm run build`: every id `window.og.ids()`
  lists has `dist/og/<id>.png` and `.gif` (the GIF's frames move and its
  first is the PNG's card, the ember stop and the mark's core included),
  `dist/embed/<id>.json` and `dist/oembed/<id>.json`, and a section page's
  head points at its own (Discord's Embed Debugger,
  discord.com/developers/embeds, reads a deployed URL as Discord will).

## Child DOX Index

- none
