DESIGN.md

The constitution: the two vocabularies, the token rules, the ambient layer contract, and every governance decision in the log.

The file that actually governs: DESIGN.md history on GitHub. This copy ships with the release the site is built from.

ambientui Design System — Source of Truth

This file is the single source of truth for all design decisions in ambientui. Code implements it, Figma mirrors it, and every proposal to change the product's look or structure starts by checking — and possibly amending — this document.

Why this file exists. ambientui's thesis is that AI can build product UI safely only inside a governance layer: a fixed vocabulary of tokens, components, and patterns that the AI selects from instead of inventing. That governance applies to the AI building ambientui itself. Without this file, every generation drifts a little — a new gray here, a 6px gap there — and after fifty edits the system is fiction. This document is the drift boundary.

  • Token values live in tokens/tokens.json and are implemented in packages/ui/src/styles/globals.css (static scales) and packages/foundation/src/foundation-context.tsx (the Foundation config engine) — this file explains the rules and intent behind them.
  • The DS site (npm run dev/ds) renders everything live: Foundation, the component vocabulary with playgrounds and docs.
  • Change process: see Governance (§10).

1. Principles

  1. One design system, two vocabularies. The product vocabulary (shadcn/ui components in packages/ui) and the ambient vocabulary (the assistant's surfaces) are one system driven by the same tokens. The ambient layer must always look like it belongs to the product it sits on.
  2. The AI selects, it does not invent. Generated or AI-assisted UI composes existing components within existing tokens. A need the vocabulary cannot meet is a governance event (§10), never an inline improvisation.
  3. Foundation first. Accent, gray, radius, scaling are set once, globally, in the Foundation config — then everything inherits. No component re-decides them.
  4. One way to do a thing. One button system, one panel pattern, one processing indicator. Variation is a cost paid by every future screen and every future generation.
  5. Accessible by default. Every accent carries its own paired foreground (light accents pair with dark text — amber gets near-black, not white); focus rings on everything interactive; keyboard-first Radix primitives.
  6. The system is flexible where it says it is. Extension points are explicit: the Foundation config, the ambient extension tokens, the governance flow. Everything else is fixed on purpose.

2. Token architecture

shadcn CSS variables (--primary, --background, --radius …)   ← the base contract
        ↑ overridden by
Foundation config (accent / gray / radius / scaling)          ← one saved choice set,
                                                                compiled to one <style> tag
        + Tailwind's own scales                               ← the full color palette,
                                                                spacing × --spacing unit,
                                                                the type scale (all emitted
                                                                via theme(static))
        + ambient accent bridge                               ← --app-blue ties the
                                                                assistant to the accent
  • tokens/tokens.json is the serialized master: every accent (with light/dark values and paired foregrounds), every gray tint, the radius set, the scaling presets, and the ambient scales. foundation-context.tsx and globals.css must agree with it — when they diverge, tokens.json wins and the code is corrected.
  • The saved Foundation config (Save Theme on /ds → Foundation) selects among these values. Edits apply live; only Save persists. The saved config is what Figma Sync pushes.

⛔ THE PRIMARY RULE — a reference is a request for a CONFIGURATION

When anyone shows a target look — a screenshot, another product, "make it feel like Linear" — the answer is a Foundation configuration, never custom styling. Read the reference as config values: its accent hue, its gray family, its radius step, its density (spacing unit + scaling base), its appearance. Apply them, Save Theme, and the entire system — components, pages, ambient layer — moves there together.

Precedent: the Linear look is { accent: indigo, gray: gray, radius: 4, spacingGrid: default, scaling: 95, light }. Five values, zero CSS.

If the configuration cannot reach the look, that is a governance event (extend the system's legal values), never a CSS patch on components. This is the layer's reason to exist: matching any look by selection keeps the AI, the theme, and Figma in one system; matching it by overrides is drift.

⛔ STRICT RULE — only values that exist on a scale

Every dimension in component code MUST be a step on a defined scale. We do not invent values.

PropertyLegal values
colorTailwind's color palette is the palette of record — but components consume semantic roles only (--primary, --muted-foreground, --border, --app-blue, --viz-*). The Foundation maps roles to palette steps through the ROLE MAP — every semantic token's light/dark step is itself configurable in the live editor on the Foundation page (/ds → Foundation → Semantic mapping; defaults are shadcn's shape); the palette grid is documented at /ds → Colors. Never a hex, never a raw --color-* step in component code
spacingTailwind's spacing scale (p-N, gap-N, m-N, h-N — every utility is step × --spacing). The Foundation sets the unit (Default 4px — Tailwind's own — or Spacious 6px), emitted in rem over the base. Arbitrary values (p-[10px]) are violations
font sizeTailwind's type scale (text-xs 12 · sm 14 · base 16 · lg 18 · xl 20 · 2xl 24 · 3xl 30 · …, at the 100% base) — rem-based, rides the scaling preset
radiusTailwind's radius scale — none/xs/sm/md/lg/xl/2xl/3xl/4xl (0, 2, 4, 6, 8, 12, 16, 24, 32 px). Foundation picks one step; it becomes --radius (and rounded-lg) and the named steps slide with it as a window on the SAME ramp. Reached through --radius and rounded-*, never as a literal
shadowTailwind's shadow scale (shadow-2xs…2xl), used as-is — documented at /ds → Shadows with per-step usage. Never a literal box-shadow
blur--ambient-blur — the one glass blur radius (backdrop-blur-[var(--ambient-blur)]). Never a literal blur value

Why this is strict and not a preference. Every scale step is mirrored into Figma as a variable. A value that isn't on a scale has nowhere to land: the sync drops it, or binds it to a same-named variable that resolves to something else. And each off-scale value teaches the AI that off-scale values are normal — drift compounds through generation.

Consequences. Need 12px of space? Use 8 or 16. Need a 15px label? Use 14 or 16. Need a pill? That's a shape decision (rounded-full), not a new radius step.

⛔ A stated pixel or colour is a REQUEST FOR A TOKEN, not a literal

"Make the corners 8px", "48px padding", "a softer grey" describe the value someone wants to see — not permission to hardcode. The procedure, every time:

  1. Pick the scale from the property — spacing → Tailwind's spacing utilities, text → Tailwind's type scale, corners → the radius set, color → a semantic role over the Tailwind palette.
  2. Exact match → use that step. 16px padding is p-4 (at the default unit).
  3. No exact match → nearest legal step, and say so. "10px padding" has no step; use p-2 or p-3 and tell the user which you chose. Never reach for an arbitrary value (p-[10px]) to make a stated number fit.
  4. Report the mapping back — "16px → p-4". The person asked for a look; they're entitled to know which rung now carries it.

For color: prefer the semantic role (--muted-foreground, not a gray literal). A stated hex is a starting point — map it to the nearest role or accent and say which. The only raw color values in the system live in tokens/tokens.json and foundation-context.tsx (the accent/gray definitions themselves).

⛔ Saving the theme must restyle EVERY component — propagation is a rule, not a hope

A Foundation change that some component ignores is a bug in the system, not a quirk of the component.

Scaling is the base layer. Every dimension in the system is nominal px at the 100% base (16px) and emitted in rem — spacing steps, Tailwind's --spacing, radius, and type all ride the root font-size the scaling preset sets. Changing scaling re-derives the entire app; nothing is allowed to opt out by being raw px.

On top of that base, every dimension in component code must resolve from a foundation-driven variable —

  • spacing: Tailwind's core --spacing is driven by the saved unit (in rem), so p-4, gap-2, h-9 in every component re-densify when the unit changes.
  • radius: --radius and its derived sm…4xl steps.
  • type: rem sizes over the scaling preset's root font-size.
  • color: semantic tokens and the accent's paired foregrounds.

Consequence: hardcoded px in component code (text-[13px], px-[18px], fixed heights) are propagation leaks — the Foundation cannot reach them. The assistant's stance-era [13px]-style values are grandfathered debt: fix on touch; new code may not add any.

⛔ Ambient tokens derive from base tokens

Tokens the ambient layer needs that shadcn doesn't define (--app-blue, glow and surface treatments) get their defaults computed from the Foundation config — the accent maps into --app-blue per mode. A preset swap must restyle the ambient layer with zero extra work. A new --ambient-* token whose default is a literal unrelated to the base theme is a rule violation.

3. Typography

  • One family: Geist Variable (--font-sans), loaded in packages/ui.
  • The family is a Foundation choice: Geist (local) or a curated Google Fonts set (Inter, DM Sans, Manrope, Space Grotesk, IBM Plex Sans) — --font-sans product-wide, loaded on demand via one managed <link>.
  • The ramp is Tailwind's type scale (text-xstext-4xl and beyond), rem-based: the Foundation scaling preset sets the root font-size (90% → 12px … 100% → 16px … 110% → 20px) and the whole ramp follows.
  • Roles: xssm captions, meta, and UI controls; base body; lgxl section titles; 2xl+ page titles and display.

4. Iconography

The icon library is a Foundation choice — Lucide, Tabler, HugeIcons, Phosphor, or Remix. Components name icons semantically through <Icon name="…" /> (site:src/components/icon.tsx); the configured library draws them everywhere. A new icon name must be mapped in every library or it doesn't exist. Standard sizes 14/15/16 in controls, strokeWidth={1.8} where the library supports it. No direct library imports in components, no ad-hoc SVGs. (The assistant's direct HugeIcons usages are grandfathered — fix on touch.)

5. Motion

Motion is a Foundation dimension, configured like color and spacing and documented live at /ds → Motion.

  • The four motion roles — components consume a role, never a literal duration, easing, or spring. A stated "200ms" is a request for the nearest role:
    RoleTokenJob
    Micro feedback--motion-microhover/press/focus ticks — the default for every transition-* utility
    Control state--motion-controlchecks, switches, selection moves
    Surface--motion-surfacemenus, popovers, sheets, tooltips entering/leaving
    Page--motion-pagesection- and page-level moves
  • Character + pace decide what every role feels like (Foundation → Motion): a character is an easing/duration/spring family (Productive / Smooth / Expressive); pace scales all timings together. Saving restyles every transition product-wide — the propagation rule applies to time.
  • Consumption seams: CSS rides the emitted variables — Tailwind's default transition duration/easing map onto --motion-micro / --motion-ease in globals.css, and explicit sites name their role (duration-(--motion-surface) ease-(--motion-ease)). Framer Motion consumers use useMotionTransition(role) / useMotionSpring() from foundation-context.tsx. One-off keyframes in component files and raw spring configs are governance events.
  • Simple motion still lives in CSS first: keyframes in site:src/styles/theme.css (ambient-shimmer) and Tailwind transition-* utilities. Framer Motion (sanctioned, §12) is for what CSS can't express: interruptible/gestural animation, layout and presence transitions, springs — timed through the motion roles above.
  • The sanctioned shader surfaces are the OrbCharacter and the ambient field (OrbField — the same heat engine as a surface background, one per open AI surface, mounted after the entrance). Beyond these: the OrbCharacter (orb-character.tsx), implemented with @paper-design/shaders-react (the heatmap shader wrapped around a circle), because the character's fluid identity cannot be expressed in CSS. Its identity springs are the one sanctioned exception to role-based timing (its cadence is its own Foundation config). No other component may use canvas/WebGL/shader libraries without governance.
  • The beam glow was removed by decision (§12) — no glow effects, on the assistant or anywhere else, without governance.

6. Component inventory — the two vocabularies

The vocabulary is documented in packages/docs/src/catalog.ts (the prose) with its live demos in site:src/components/ds/stories.tsx, joined by site:src/components/ds/entries.ts and rendered at /ds. A rule is written once, where it lives — this file names the components and their contracts' locations; the registry holds per-component behavior, usage, and constraints. A rule written twice will disagree with itself.

Product vocabulary (packages/ui/src/components, 14): Badge, Button, Card, Checkbox, Collapsible, DropdownMenu, Input, Separator, Sheet, Sidebar, Skeleton, Table, Tabs, Tooltip. Installed from the shadcn radix-nova preset; extended only through the shadcn CLI or governance.

Ambient vocabulary (packages/ambient/src): the assistant's four surfaces — orb (line), panel, dock, spotlight — plus the objects a conversation is made of, in four groups at /ds → Ambient vocabulary:

  • Core: OrbCharacter (the animated identity), StreamingText, MessagePair, MessageBranches, ReferenceChips, ShimmerPlaceholder, ContextChip, CommandPalette.
  • Messages (message-kit.tsx): MessageActions, FollowUpSuggestions, ErrorState, MessageQueue, ReasoningPanel, ReasoningEffort, MessageAttachments, QuoteReply, FeedbackDialog, ReviewComment, DayDivider · MessageTime.
  • Tool use (tool-kit.tsx): ToolCall, ToolTimeline, TerminalBlock, CodeDiff, ReviewableDiff, ParallelTools, ToolFailure, CodeRunner.
  • Knowledge (knowledge-kit.tsx): WebSearch, InlineCitation, ResearchReport. See §8 for its contract.

Known gaps (the registry documents these so the AI cannot compose what does not exist): no Select (use DropdownMenu), no Switch (use a labeled Checkbox), no Textarea, no Dialog (use Sheet or the spotlight).

Every component has: a registry entry (summary, behavior, when to use, when not to), stories, and — where the prop surface warrants — a playground whose controls render in the /ds Inspect rail.

7. Layout standards

  • The app is a desktop-app shell: the document never scrolls; panes scroll internally (theme.css).
  • /ds is the sanctioned three-pane frame: component list rail (240px) · canvas · Inspect rail (288px, controls only). New pages that need a different pane arrangement go through governance first.
  • The canvas (/) is deliberately bare — it is the host page the ambient layer sits on.
  • Pages declare themselves to the assistant via setPageChip on mount and selection change, and clear it on unmount. A page that doesn't is invisible to the ambient layer — that's a bug, not a choice.
  • A page that knows more says more, through setPageIntel: the questions worth asking here, the working history this surface would have, where "Jump to" should go, and the words the palette uses to offer them. The layer renders it and keeps its generic defaults for pages that stay quiet.

8. The Ambient Layer contract — the seven things that must not drift

  1. One surface, five modes: line (orb, which expands in place into quick ask) · panel · dock · spotlight · history. New modes are governance events; extend an existing form before adding one. The first four are sized to how much attention one EXCHANGE deserves. history is the exception and the reason the rule survived it: it is not about an exchange at all but about the record of them, which is why it is the only mode that takes the whole screen — and it is still translucent over the product, because the work is the reason you opened the record. A new mode is a new GEOMETRY, never new parts: history composes the sanctioned Sidebar, the same transcript and the same Composer as every other mode.
  2. Drag is the mode switcher. Panel header drags; right-edge hot zone docks; top-center hot zone opens spotlight; the dock tears off into a panel in one gesture. ⌘K toggles spotlight; Esc clears, then closes.
  3. An answer arrives in ORDER, never all at once. Thinking finishes, then the next evidence block works, then the prose streams, then artifacts land. Enforced in useStagedReveal via the stage queue (stage-queue.ts), never per block: a staged block claims its slot by being staged at all, so there is nothing to remember and nothing to opt out of by accident. A block outside a response (a /ds playground) has no queue and stages on its own.
  4. Context is ambient. The page chip arrives from the page (setPageChip), user chips attach explicitly; the user never re-explains where they are. Conversations keep their context. What the palette OFFERS is context too: setPageIntel carries the page's live suggestions and recents, and the chip itself may carry an icon — every page is kind "page", too coarse to tell an editor from a colour map, and the page is the only thing that knows which. So the spotlight opens onto this environment's real work rather than a generic menu. The dev tool publishes its problem inventory there — every suggestion is a question one of its errors deserves, and a fixed problem leaves the palette the moment it heals.
  5. Nothing inside an ambient surface is opaque, and the veil always sits over the field. Every mode wears the layer's material: a glass recipe, the heat field behind it, and a translucent layer ON TOP of that field. renderField(place) names the three combinations that mean anything — panel (low presence, full frost: atmosphere behind content), ground (full presence, thin veil: the field IS the wallpaper, as on the canvas), and screen (full presence, the heavier .ambient-screen-frost: a full-screen surface, where the field carries a whole window and the veil has to be weighted for a full-presence shader — the panel's frost is tuned for a field at low presence and leaves a stage-strength one glowing through the content). ONE veil per surface, owned by the surface: never one per bar, which makes each bar a solid pane and still leaves the content unveiled — but an overlaying BAR still carries its own, because the surface's veil covers the field while the bar's covers the content scrolling behind it, which is painted above that veil and out of its reach. Two jobs, two layers; confusing them removes the bar's ground and the transcript passes straight through the composer. And the field is scoped to the CONVERSATION, not the window: run behind the record and the chrome it lights lists, which are read rather than felt, and a shader under text is noise. The field is the ground an answer arrives on, so it covers exactly that. A place, not two knobs, because every wrong combination has been built at least once — full presence under the thin veil leaves the shader raw, which is the ground's recipe applied where it does not belong. A pane WITHIN a surface uses the layer's wash, never a product ground: bg-sidebar on history's rail read correctly as a navigable pane and punched a solid hole through the field and the work behind it, which is the one thing every surface of this layer exists to keep.
  6. No glows. The beam was removed; nothing glows without governance. The orb's face is the OrbCharacter — four states (still / listening / thinking / answer) driven through orbState in the assistant context. The response pipeline sets the state; components only read it. The character's palette derives from the accent bridge; it renders via the system's one sanctioned WebGL shader (§5).
  7. The response kit seam. send() appends the user message and stops — the canned plan/answer machinery was deliberately removed. Responses will be composed from the component vocabulary (plan → streamed progress → composed answer). Do not reintroduce mock responses outside that kit.

9. Theming — the Foundation config

  • Foundation lives at /ds → Foundation. Scaling comes first — it is the base layer every other dimension derives from. Then: accent (all 17 Tailwind hues + neutral, each with paired foregrounds — light hues get dark text), gray family (Tailwind's five — it defines every surface token), appearance (light/dark), radius (Tailwind's 9-step scale), spacing unit (Tailwind's 4px default / 6px Spacious — drives --spacing). Customization means choosing among all legal values — never typing one that isn't on a ramp.
  • Compiled to one injected <style> tag by foundation-context.tsx. No other runtime theme mutation exists; never set theme variables ad hoc.
  • Save semantics: edits apply live; only Save Theme persists. Reload without saving returns to the last saved theme. The saved config is the input to Figma Sync.
  • Appearance (light/dark) is the theme provider's .dark class on <html>.

The role glossary — what each semantic token actually paints

The role map (edited live on the Foundation page, Semantic mapping section) is the entire color contract between the Foundation and the UI. Each role is one visual job; changing its step changes every place that job appears, both vocabularies at once. Aliases are the same value under shadcn's other names — never set them independently. Light is the design decision: picking a role's light step derives the dark one automatically (deriveDark — gray roles mirror across the scale, accent roles keep their default offset, and each role's designed deviation from the pure mirror is preserved). Setting dark explicitly overrides, until the next light pick re-derives.

RoleToken (+ aliases)Draws fromWhat changes on screen
Action color--primary + --sidebar-primaryaccent hueFilled buttons, switches and checks when on, the active nav accent — the color that marks the main action. --primary-foreground stays paired automatically (dark text on light hues)
Focus ring--ringaccent hueThe outline drawn around whichever control has keyboard focus
Ambient accent--app-blueaccent hueThe assistant layer's accent — its chips, links, and highlights bridge to the brand hue through this
Page background--backgroundgray familyThe ground every screen sits on; everything else stacks above it
Primary text--foreground + --card-foreground, --popover-foreground, --sidebar-foregroundgray familyHeadings and body copy everywhere — cards, popovers, and the sidebar inherit it
Raised surface--card + --popovergray familyThe face of anything lifted off the page — cards, popovers, menus, sheets
Quiet fill--muted + --accent, --sidebar-accentgray familyThe soft wash behind hover and selected states, subtle chips, and secondary surfaces
Secondary text--muted-foregroundgray familySupporting copy — descriptions, captions, placeholders, section labels
Text on quiet fill--accent-foreground + --sidebar-accent-foregroundgray familyText sitting on the quiet fill — a hovered menu item's label, a selected row's text
Hairline border--border + --sidebar-bordergray familyEvery hairline — card edges, dividers, table rules
Field border--inputgray familyForm-control borders at rest — inputs, selects, checkboxes
Sidebar ground--sidebargray familyThe navigation rail's tint, and the shell behind the inset content card

This table and ROLE_DEFS in foundation-context.tsx are the same list — the code carries each role's label and description, and the /ds editor renders them. Adding a role means adding it in both places plus tokens/tokens.json; they must not diverge.

10. Figma sync

Direction: code → Figma, variables only, agent-driven. The connected file (set on the Foundation page) mirrors tokens/tokens.json + the saved Foundation config. Procedure, mapping, drift check, and the code-wins conflict rule: figma/figma-sync.md. Components are never written by the sync; the alias structure means one accent change propagates through the file.

11. Governance — how design decisions are made

Two roles (Claude skills in .claude/skills/) gate changes:

  • ds-manager — guards the system: token/component/layout compliance, promote vs reject, impact analysis, keeps this file and Figma in sync.
  • product-design-manager — guards the product: right surface for the use case, flow consistency, UX heuristics, pushes back on one-off patterns.
  • product-copy — guards the words: microcopy, errors, empty states, voice.

Pattern watchlist protocol (always on). When any work produces UI that doesn't match an existing pattern — a new component shape, a new layout structure, a divergent interaction — the assistant must STOP and ask: "This looks like a new pattern — should it become part of the design system?"

  • Yes → implement in the proper vocabulary, document in the registry and here (§6/§13), log the decision (§12).
  • No → rebuild the use case with existing patterns.

One-off exceptions are not merged silently. Ever.

Owed: audit:docs. rg-design-proto proved that coverage must be a command, not a memory test — a script that fails when a component lacks a registry entry, rules, or states. ambientui does not have it yet; building it is logged debt.

12. Decision log

DateDecisionWhy
2026-08-21ambientui = one design system, two vocabularies (product + ambient), both driven by the same tokensThe ambient layer must inherit any product theme instead of being a second design system bolted on
2026-08-21Base = the stance-proto assistant architecture (five-mode surface, drag-as-mode-switch, beam), SaaS interface removed, all branding renamedThe interaction model was proven there; the product around it was not the product
2026-08-21Palette nav decoupled from any app sidebar into src/nav.tsThe assistant must not know about any particular product interface
2026-08-21Mock response machinery deleted from the assistant; send() ends at the response-kit seamThe response kit will compose real vocabulary components; canned answers teach the wrong pattern
2026-08-21Foundation config layer: accent/gray/appearance/radius/scaling compiled to one style tag; Save Theme persists (live edits don't)Global decisions are set once and inherited; save semantics make the committed theme explicit — it is also the Figma Sync input
2026-08-21Accent → --app-blue bridgeThe assistant's accents must follow the product accent with zero configuration
2026-08-21Ambient scale: spacing on an 8px grid from 2px (--ambient-space-1…10, px), type ramp --ambient-text-1…9 (rem)shadcn defines no scale of record; px spacing keeps the grid, rem text follows the scaling preset
2026-08-21Scaling presets map to concrete base px (100% = 16, 95% = 14, …)"95%" alone is not a design decision; a base size is
2026-08-21Radius steps spread to 0/4/10/16/244/7.2/12 were indistinguishable in the picker and in components
2026-08-21Every accent carries a paired foreground; amber pairs with near-blackWhite on amber fails WCAG; the token pair decides on-accent text, never the component
2026-08-21Inspect rail is the third pane of /ds; playground controls portal into itConfiguration is a surface, not an inline box — and the rail is the seam the ambient inspect state will later take over
2026-08-21Choice controls in the Inspect rail are dropdowns (DropdownMenu), not chip rowsUser decision; also respects the vocabulary — no Select exists, so the menu is the selection surface
2026-08-21Foundation exposes the FULL legal space: radius ramp 0–24 (8 steps) and a selectable spacing grid (Compact 4 / Default 8 / Spacious 12) whose unit resolves --ambient-space-1…10; the Spacing page re-derives from the saved gridComplete customization within governance — users choose among every legal value instead of five picks, and never type an off-ramp number
2026-08-21Propagation rule: the spacing grid drives Tailwind's core --spacing (unit ÷ 2, rem), so every utility-based dimension in every component follows a Foundation change; hardcoded px are propagation leaks (assistant's stance-era values grandfathered, fix on touch)User report: saving the theme didn't visibly update components — the grid only reached --ambient-space-*, which the shadcn components don't use. A theme system that some components ignore is not a theme system
2026-08-21Scaling is the base layer: spacing steps, --spacing, and radius are all emitted in rem over the scaling preset's root font-size (nominal px at the 100% base)The whole app configures on top of one base size — scaling must re-derive everything together, not just text
2026-08-21Scaling moved to the top of Foundation as the first decision; the 4px Compact grid removed (saved configs migrate to Default)The base layer is set before what derives from it; a 2px --spacing collapses real interfaces — Compact didn't survive contact with actual components
2026-08-21The AI beam removed completely — component, Beam Lab, config state/endpoint, keyframes, and the border-beam dependencyUser decision. The orb and avatars stand without a glow; engagement signaling is a question for the response kit, not a permanent halo
2026-08-21OrbCharacter shipped — the assistant's animated identity, four states: still (own movement) / listening (inward drift, digesting) / thinking (fast churn, retrieving) / answer (one-shot ring + flash — "found it"). Configurable at /ds → Ambient vocabulary; state drivable via orbState in the assistant contextThe AI needs a face whose motion IS its status — reference: the user's iridescent-orb video. States map 1:1 onto the response lifecycle the kit will drive
2026-08-21Orb transitions became second-order: every shader parameter runs two-stage exponential smoothing (target → intermediate → value) at 60fps with per-parameter time constants (flow speed 0.7s, glows 0.5s, contour 0.45s, wave angle 0.85s) — S-curved, velocity-continuous; the previous 12fps prop throttle and single-stage easing produced visible stepping"Very very smooth" is a spec: changes must lean in and settle out, and nothing may step
2026-08-21The character became the assistant's mark everywhere: panel/dock header, spotlight input avatar, palette avatar rows, AI Overview header, and the bar all render the OrbCharacter (via one AssistantMark component reading orbState + the saved orb config) — the sparkle icons retired from ambient surfaces; palette list-row icons stay static (a shader context per row would exhaust WebGL contexts)One assistant, one face — every surface it owns shows its state, live
2026-08-21Orb re-implemented on the Paper Design heatmap shader (user direction, shaders.paper.design/heatmap): the shader wraps a circle so heat flows around the orb's edge; per-state MOVEMENT via shader params — still: calm balanced rim; listening: heat drawn inward (innerGlow 0.8, waves angled in); thinking: hot racing edge (contour 0.92, noise, 2.2× flow); answer: one-shot outward bloom decaying ~1.4s. All params ease continuously; per-state speeds + palette (accent-linked thermal ramp or custom cold→hot stops) persist with the theme. Deps: @paper-design/shaders-react added by decision. Gotchas: the shape must be a DARK fill on transparent, served as a real file (data URIs rejected by the loader); pass stable props, not per-frame image objects; the frost-glass interior comes from ALPHA in the ramp's cold end (#RRGGBBAA is honored) + transparent colorBack over the backdrop-blur wrapper — never an opaque cold colorThird styling school for the character (vortex → pearl → thermal); the orb reactions remain the invariant
2026-08-21Orb restyled as a pearl (new reference studied): milky luminous core, ALL color at the boundary as thin-film iridescence drifting around the rim, interference micro-bands, bottom under-glow, soft-focus — no hard speculars. The reaction principles carry over: listening draws the film inward, thinking races and tightens it with core wisps, answer blooms one ring outward; transitions stay eased; custom colors become the iridescence stopsUser direction with a second reference — the character's school changed from vortex-glass to bubble-pearl; the state semantics (the "orb reactions") are the invariant, the styling is the variable
2026-08-21Orb glass + full configurability: the dark body became frosted glass (translucent, page blurred behind via the new --ambient-blur token); state changes became continuous transitions (eased weights + tempo, never a cut); per-state speeds and the palette are user-configurable on the /ds page and persist with Save Theme — accent-linked by default, or custom crest/body/tail/streak colors (1-4, add/remove) with the accent link offUser asks: glass over opaque black, blur as a managed token, colors beyond (or without) the accent, transitions, per-state speed config. The character is itself a governed, themeable component — configuration over hardcoding is the product's own thesis
2026-08-21OrbCharacter re-rendered as a WebGL fragment shader (studied against the reference video frame by frame, then corrected against a still): the structure is ONE dominant spiral arm — phase = angle − k·radius, organically bent by slow low-frequency wobble — silver crest folding into accent body and deep tail around a dark core, fine comb teeth along the arm's edges, chromatic fringing per channel, fresnel rim + specular crescent. The one sanctioned shader surface; CSS-gradient version retiredfbm blobs read as marble, not the reference; the reference is a spiral-phase field. Bug learned: never loseContext() in a React cleanup — StrictMode remounts get the dead context back from getContext forever
2026-08-21Tailwind became the system of record for color, spacing, and type. The full Tailwind palette (emitted via theme(static)) replaces the hand-picked accents/grays: accents = all 17 hues + neutral (steps 600/500, ambient 600/400, paired foregrounds), the gray family defines every surface token (50–950 mapping); the custom --ambient-space/text scales are retired — spacing is Tailwind's scale × the Foundation's --spacing unit, type is Tailwind's text scale; /ds gained a Colors page (the full grid + the semantic mapping) and Spacing re-documents Tailwind's stepsWhy maintain a parallel scale when the substrate ships one? The components already speak Tailwind — now the Foundation selects from it instead of overriding beside it. Gotcha: Tailwind v4 tree-shakes theme variables — theme(static) is required for the palette to exist as CSS vars
2026-08-21The primary rule established: a reference is a request for a configuration. Exercise: matched the Linear look purely by config — indigo accent, gray family, radius 4, default unit, 95% scaling, light — zero component styling touchedUser direction: this IS the governance layer's job. Any target look is read as Foundation values; a look the config cannot reach extends the system, never patches components
2026-08-21Three more Foundation dimensions: shadows (later reverted to docs-only — see below), the icon library (Lucide/Tabler/HugeIcons/Phosphor/Remix via a semantic <Icon name> component), and the font (--font-sans: Geist local or Google Fonts loaded on demand)User direction: every visual dimension a reference could differ on should be a config value — same propagation guarantees as color and spacing. Icons follow the same shape: semantic names, library selected once
2026-08-21The settings-page pattern promoted (components/ds/settings-kit.tsx: SettingsTitle / SettingsSection / SettingsCard / SettingsRow — title+description left, control right, or a full-width picker zone; hairline-divided cards per topic). The Foundation page rebuilt on it; the /ds rail moved to sentence-case group labelsReference: Linear's preferences page. Note the primary rule held for the THEME (nothing recolored); the reference's page STRUCTURE is a pattern, and patterns get promoted through governance, not improvised
2026-08-21Text tones became Foundation config: primary text (gray-family step 950/900/800) and secondary text (600/500/400), driving --foreground/--card-foreground/--popover-foreground/--sidebar-foreground and --muted-foreground, with automatic dark-mode mirrors (950↔50 … 400↔500)User request — text color is a foundation decision like everything else, and staying on the gray family's steps keeps it on-palette in both modes
2026-08-21Shadow config reverted to documentation: the elevation presets (Flat/Subtle/Default/Elevated) removed — components barely vary by them, so the knob had no payoff; Tailwind's shadow scale stands as-is, documented at /ds → Shadows with per-step semantics (2xs pressed controls → 2xl takeovers)User call: "I wanted to just document the shadows properly." A config dimension must visibly propagate to earn its place; elevation choices belong to the components that make them
2026-08-21The semantic mapping became a live editor (ROLE_DEFS + config.roles): every semantic token's light/dark palette step is configurable on /ds → Colors with swatch previews and per-mode dropdowns; the Foundation's text-tone rows are quick presets over the same config; --primary-foreground stays auto-pairedUser call: "this whole thing should be configurable visibly rather than just variables shown, or else the whole configurable purpose goes away." Documentation of a mapping IS the mapping's editor in this product
2026-08-21Inset-card canvas promoted as the app-shell layout pattern (via the watchlist): the page ground is bg-sidebar (rails consume the --sidebar role directly, no separating borders), and the content area is a bg-background card — rounded-lg border with a p-2 gutter (shadowless by a later decision: the shell separates by tint and hairline alone). Rail group labels use the primary text tone; items stay muted until activeUser asked for Linear-style separation between sidebar and content; the watchlist question was asked and answered yes. The separation is now pure config: retint it by editing the --sidebar role at /ds → Colors
2026-08-21Every role in the map carries a human label and a plain-language description (RoleDef.label / RoleDef.description): the /ds Colors editor leads with "Action color — filled buttons, switches…" and demotes the token name to a mono footnote; the same glossary is documented in §9User feedback: "it's hard to understand what the person is actually updating." A configurable mapping is only governable if the person configuring it can tell what each row does
2026-08-21Config-layer audit — four propagation leaks fixed. (1) Controls sat on the top of the radius ramp (rounded-4xl ≈ radius × 2.6), which exceeds half a control's height from step 8 up — the browser clamps to a pill, so most of the ramp looked dead. Buttons/inputs now use rounded-lg (= --radius), badges rounded-md, menus rounded-xl/rounded-md, cards rounded-2xl; the whole ramp now visibly restyles them. (2) The semantic <Icon> moved into packages/ui (IconLibraryProvider fed by the Foundation) and the primitives — checkbox, dropdown, sheet, sidebar — plus the /ds registry now draw through it; before, only the Foundation-page preview strips consumed the icon config. New names chevron-right and sidebar mapped in all five libraries. (3) Dropdown menus hard-coded a dark class — they ignored the appearance and the role map; removed. (4) ~50 text-[10–13px] labels in /ds and Foundation pages didn't ride the scaling base; converted to text-xs/text-sm. Foundation text-tone previews also now show the mirrored step in dark modeUser audit request: "the save buttons are not reacting based on the config of radius… do a complete audit check across the app if the config layer is working." A config dimension that doesn't visibly propagate is indistinguishable from a broken one. Assistant's stance-era px remain grandfathered (fix on touch)
2026-08-22Framer Motion sanctioned as the animation library (framer-motion in apps/web), amending the motion-is-CSS-only rule: CSS stays the default for simple transitions; Framer Motion is for interruptible/gestural motion, layout/presence transitions, and springsOwner decision — installed at the user's direction. One library, not many: no other animation dependency without a new decision here
2026-08-22Orb state transitions moved to Framer Motion springs (first consumer of the sanctioned library): each shader parameter is a spring-driven motion value retargeted on state change, replacing the hand-rolled second-order easing loop; the answer bloom is a keyframe sequence from the current value. The /ds playground gained a lifecycle seek bar — play/scrub the whole still → listening → thinking → answer journey (~3s dwell per state); play/pause added to the icon vocabulary, mapped in all five librariesSprings carry velocity across retargets, which is exactly what the second-order loop hand-built; interruption mid-transition stays smooth for free
2026-08-22The role editor moved into the Foundation page (Semantic mapping section, between Color and Shape and density); the Colors page keeps the palette grid and points thereUser call: the Foundation is where ALL theme configuration lives — a config surface split across pages breaks the single-source story. RoleEditor is shared (colors-page exports it); Foundation embeds it without the inline save, the page footer's Save Theme commits
2026-08-22The motion system: motion became a Foundation dimension — four semantic roles (--motion-micro/control/surface/page) driven by a configured character (Productive/Smooth/Expressive: easing + duration + spring families) and pace (Relaxed/Default/Brisk). Tailwind's default transition duration/easing map onto the micro role so every transition-* utility re-times on Save; explicit sites converted to duration-(--motion-{role}); Framer consumers use useMotionTransition/useMotionSpring. Configured at Foundation → Motion, documented at /ds → MotionUser direction: "easier for anyone to configure overall motion system across pages and components micro interaction." Same shape as the role map: components name the job, the Foundation decides the feel
2026-08-22Motion-role adoption audit (user-requested): the assistant had NO transitions at all — every mode change (line/bar/panel/dock/spotlight) was a hard cut. All five surfaces now enter through the surface role via Framer (useMotionTransition("surface")); the orb's snap-to-anchor literal duration-300 became the page role; a leftover ease-linear on the sidebar rail rejoined --motion-ease. Verified live: menus, buttons, and surfaces all compute their durations from the emitted --motion-* vars. Accepted non-role timings, each sanctioned: the orb's identity springs and answer bloom (§5 exception), the Foundation's character-preview demos (each demos its own preset by definition), and the infinite idle loops ambient-shimmer/viz-pulse (decorative cadence, not state transitions)"The transition of the whole AI component seems to be not using it" — correct, and worse: it had none. Exit animations were completed the same day (see next entry)
2026-08-22Assistant exits + spring entrances: the per-mode early returns became one AnimatePresence over keyed surfaces, so every mode change animates out as well as in (verified: overlay opacity interpolates 1→0 before unmount). Entrance transforms moved from the surface tween to the configured character's spring (useMotionSpring), with opacity on the micro tween and quick micro-fade exits — springs respond instantly instead of the tween's perceptible driftUser feedback: ⌘K "feels like it is lagging rather than being smooth" — a 300ms soft-curve tween on transforms reads as drift; a spring reads as response. Enter slow-ish and settle, exit fast is the standard asymmetry
2026-08-22Surface jank fixed at the source — OrbGlyph: the AI's marks and avatars were full OrbCharacters, so every surface open created fresh WebGL contexts + shader compiles + 60fps loops mid-entrance (measured: 2 new contexts per ⌘K). Marks ≤32px now render OrbGlyph — a pure-CSS twin (glass shell + accent core, zero runtime); the shader lives only on the 52px floating orb and the /ds playground, and the floating orb stays MOUNTED across mode changes (hidden, not unmounted) so closing a surface no longer recompiles it. Verified: one canvas total in any mode; a full open/close cycle shows a single ~51ms taskUser: "the whole interaction of the AI still feels laggy." The lag wasn't the curve, it was work during the frames. Identity lives at identity scale; at glyph scale the anatomy is the identity
2026-08-22Dark derives from light in the role map (deriveDark): picking a light step auto-derives the dark one — gray roles mirror across the scale, accent roles keep their default offset, per-role deviations preserved; explicitly picking dark overrides until the next light pick. The Semantic mapping section gained a light/dark test toggle beside the descriptionUser call: "the user shouldn't be thinking so much to figure out basic stuff" — one decision (light), one derived consequence (dark), explicit override one click away
2026-08-22One glass recipe for the ambient layer (.ambient-glass in theme.css): popover tint at 80% over blur(var(--ambient-blur)) saturate(1.15); --ambient-blur unified at 24px. All AI surfaces wear it — the panel/dock chrome's one-off bg-popover/80 backdrop-blur-2xl and the solid spotlight card and bar input row all converged on the classUser call: the frost belongs to the whole AI layer, not one surface. A glass look that exists as a class is configurable; one that exists as scattered utilities is folklore
2026-08-22Translucency tokens + SectionRail + sticky save. (1) Every alpha collected into six tokens in theme.css (--glass-fill/core/border, --wash, --wash-strong, --scrim) — ~30 scattered /40-/80 modifiers and inline color-mix recipes swept onto them; they derive from semantic roles so the role map retints them. (2) SectionRail promoted into the product vocabulary (user-commissioned, from a story-rail reference): right-edge dash rail, active dash grows with its label, click smooth-scrolls; motion on the control role; documented in the /ds registry; the Foundation page wears it across its seven sections. (3) The Foundation's Save Theme/Reset footer is sticky at the card bottomTranslucency was folklore spread across ~30 call sites; now it is six decisions. The rail keeps a long config page navigable without a second sidebar
2026-08-22SaveReminder extracted into the settings-kit as the pattern's save affordance: open/saved + onSave/onDiscard with configurable copy; spring entrance, micro-fade exit; documented in the /ds registry with an interactive story. The Foundation page is its first consumerThe reminder replaced the sticky footer one turn earlier; a pattern used once is a one-off, extracted it is vocabulary — "turn this into a component for future purposes"
2026-08-22Sonner joins the vocabulary as the toast layer: one <Toaster> at the app root, dressed in the popover role (surface/text/border/radius follow the role map) and themed by the appearance. The save-feedback pattern is now: SaveReminder dismisses immediately on save, the toast confirms — the reminder no longer holds a "saved" confirmation state (prop removed). Documented in the /ds registryUser call: confirmation is transient feedback, not reminder chrome. Reminder = persistent state, toast = completed event — each surface does one job
2026-08-22Raycast-level glass for the command palette (reference answered as configuration + recipe tuning, per THE PRIMARY RULE): --glass-fill deepened to popover 65%, --ambient-blur 24→48px, saturation 1.15→1.5 — one recipe change upgraded every AI surface. New --glass-wash token (foreground 9%) for neutral fills ON glass: palette selection/hover moved off the accent wash onto it, plus Raycast-style keycap chips (PaletteKey) in the footer; palette card corners on the 2xl ramp step, input on text-baseUser: "same level and same glass effect… super important." Selection on glass is neutral in every reference-grade palette — accent is for actions, not focus position
2026-08-22Raycast pass 2 + the live border. Palette refinements toward the reference: neutral icons (accent reserved for actions), sentence-case section labels, rows on text-sm, context chips merged into the input band (one hairline), Raycast footer anatomy (brand left — OrbGlyph + name — primary action + toggle right), glass fill deepened to popover 55%. The live border: the orb's states projected onto every AI surface's border — .ambient-live-border, a conic comet on --app-blue masked to a 1.5px ring, driven by data-orb-state (still 14s/0.4 → listening 7s/0.65 → thinking 1.6s/1 → answer 3.5s/0.85). Pure CSS in theme.css — a WebGL border per surface would repeat the cost the OrbGlyph decision removedUser: "bring all the states of the Orb in the border." The orb is the identity; when a surface is open the identity lives in its edge
2026-08-22The response kit, v0 (user-commissioned — "let's start building the response kit"): response-kit.tsx fills the send() seam with composed answer OBJECTS — ResponseBlock (OrbGlyph author mark + frame-clock streamed text + settle callback) and ReferenceChips (numbered, on --glass-wash); composeResponse is a canned, context-grounded composer a model will replace. The full ambient pipeline is real: typing a question turns the state to LISTENING (the "Ask ambientui" activation), send → THINKING, streaming → ANSWER, completion → STILL — orb and live borders ride it. Live-border fixes: the ring mask moved to the border-box/padding-box exclude technique (the old one leaked the conic across the panel), zero-alpha stops pinned to the accent hue, still-state presence raised to 0.55. Streaming runs on the frame clock, not setInterval (timers throttle in hidden tabs)The kit's contract: a model replaces the composer, never the objects or the states
2026-08-22The heat field behind the glass (OrbField): the orb's shader — same engine, same states, extracted into a shared useHeatEngine hook — now renders as the BACKGROUND of AI surfaces, behind a dedicated frost layer (.ambient-field-frost on the new --glass-veil token) so the heat glows through the translucency. Wrapped around a rounded-rect shape (/orb-rect.svg); one field per open surface, mounted 400ms after the entrance (the shader compile can never jank the spring) and faded in on the surface role. §5's sanctioned shader surfaces widened to include itUser's idea: "bring the shader as background with all the states behind the translucency." The identity isn't a mascot in the corner — the surface itself is made of it
2026-08-22Quick ask — the orb's expanded form (user-commissioned): clicking the orb grows an input out of the character, sharing its glass and opening toward screen center; the character turns to listening as it opens; clicking the orb again, clicking outside, or Esc closes it; the thinking beat is held IN the pill and the panel takes over when the answer is ready (seedPrompt(text, autoSend, immediate)), and asking seeds the panel with seedPrompt(text, autoSend) so the panel arrives already answering. Implemented as the orb's own state, NOT a sixth mode — the machine still has five. Documented at /ds → Form factorsThe growth rule this sets: extend an existing form before adding one; add a mode only when the new surface would have its own lifecycle
2026-08-22The bar mode was removed (user-commissioned), taking the contract from five modes to four: line · panel · dock · spotlight. Quick ask supersedes it — a bottom-edge input that carried its own glass was the same job (one question, no surface) done further from the user's hand and with a second set of chrome. Its removal also retired the bare and showKbd props on the form row: bare existed only to distinguish the bar's self-glass from in-surface rows, so with the bar gone every AI form is in-surface and the row has one shapeThe growth rule cuts both ways: a form that duplicates another's job at a worse distance is removed, not kept for symmetry
2026-08-22MessageBranches (user-commissioned): regenerated answers join their predecessor rather than replacing it, and a quiet pager under the answer keeps every version reachable. The newest branch becomes the one you are looking at; stepping back shows settled text, because history is written, not replayed. Arrows disable at the ends instead of wrapping. Composed entirely from existing primitives — ghost icon Buttons, <Icon>, ResponseBlock — and the chevron-left name was added across all five icon libraries to serve itRegeneration that overwrites is a silent destructive act: the user may have preferred the answer the model just discarded. The pager is navigation, not content, so it is the quietest object in the transcript
2026-08-22Any Inspect-rail change raises the save reminder (user-commissioned, stated as a rule): dirty is now "the config differs from the saved theme OR a rail control was touched". Enforced in the rail's primitives — ControlRow and ChoiceControl call touch() — never in the playgrounds; ControlRow action exempts one-shot action rows only. Discard bumps a generation that remounts rail state, so it reverts playground props as well as configA rule that each new playground has to remember is a habit, not a rule. Enforcing it in the primitive means the next component written cannot silently drop a user's edit
2026-08-22One context chip, not one per surface: the palette's bordered icon-tile chip and the composer's small blue pill were two implementations of the same object. Unified into ContextChipView with two sizes (default, compact); the pill is deleted, and the /ds story now renders the real componentThe registry documented the pill while the product showed the chip — documentation that shows an approximation of a component is worse than none, because it teaches the AI layer a shape that does not exist
2026-08-22Reference chips carry a source mark (user-commissioned, for citations): one anatomy — number · mark · label — with the mark slot filled in a fixed precedence (the source's own logo, else a typed icon, else nothing), and an optional href that makes the chip a link. A logo that fails to load falls back to a monogram. Four icon names (document, link, globe, code) were mapped across all five libraries to serve typed sourcesA slot with a precedence keeps one object; a set of variants would have produced three chips that look like three kinds of claim. The logo is DATA the caller supplies — a design system that fetched favicons would be inventing provenance, and a mark makes a reference look authoritative, so it must never be added to a source the answer did not use
2026-08-22The status pair: the system had --destructive alone, which forced every positive signal (a passing test, an added diff line) to invent a green. Added --positive (Tailwind emerald 600/500, the palette of record) plus --positive-wash / --destructive-wash translucency tokensA single-sided status vocabulary guarantees drift: the moment a component needs "good", it hardcodes one. Status is now a pair, and every pass/fail surface — diffs, tool marks, terminal exits, report sections — reads from it
2026-08-22The conversation vocabulary tripled (user-commissioned, from the assistant-ui element list): 21 components in three kits — message-kit (actions, follow-ups, error state, queue, reasoning panel + effort, attachments, quote reply, feedback, timestamps), tool-kit (tool call, timeline, terminal, code diff, reviewable diff, parallel tools, tool failure, code runner), knowledge-kit (web search, inline citation, research report). All composed from existing primitives — Button, Input, Icon, tokens, motion roles — with no new dependency and no bespoke color. Ten icon names added across all five libraries. The /ds ambient rail now groups entries (ComponentEntry.group) because a flat list of 29 stopped being navigableTwo rules did the design work. A tool call is a claim, and a claim must be auditable — the collapsed row is the claim, the disclosure is the evidence, and failures open by default because a failure you have to find is a failure you miss. Provenance is shown, never implied — the search query is visible because it is where an answer first goes wrong, and a citation binds a source to the SENTENCE it supports rather than to the reply
2026-08-22The home surface became three views (user-commissioned): canvas (the presentation ground, unchanged and still the default), layer (what an ambient layer is and what follows from it), and devtool (a code-review product with the assistant docked beside it, composed entirely from documented components). The view lives in the URL as ?view=, and each one declares its own page chip, so the assistant knows which is open. Tabs was installed through the shadcn CLI — the sanctioned path — and documented at /ds before useThe dev-tool view is the argument's proof: if the vocabulary is real, a product using the ambient layer should be assemblable from it without one bespoke object. It was — the editor shell is tokens, and every object in the conversation column is a registry entry
2026-08-22An answer is a composed object with a grammar, not a paragraph (user-commissioned): KitResponse now carries evidence (reasoning, parallel calls, tool calls, searches — above the prose, because it is what the answer rests on), artifacts (a reviewable diff, a test run, a session timeline — below it, because they are consequences), followUps, and one place that maps a block to a component (KitBlockView). composeResponse shapes the blocks from the ATTACHED CONTEXT: a file earns tool calls and a diff, a page earns a search and citations. The real transcript now renders MessageBranches, so regenerate appends a version, and follow-ups ask the next questionThe seam only proves something if the objects reach the real surfaces. A model wiring in emits blocks; every form factor already knows how to render them, and no surface renders a block itself
2026-08-22The attach gesture became real (AttachMenu, AskAI, useAttachMenu): right-click anything → Explain with AI / Add to chat context, plus an inline "Ask AI" revealed on row hover. The capability existed in the context (explain, addChip) but no product surface had ever used it. Chip kinds gained file, symbol, selectionThe product side of the ambient contract is three calls — declare the page, offer to ask at the data, let anything become context. Until a surface actually made them, the layer was a demo of itself
2026-08-22The dev tool tab is a full-screen editor with no prose on it (user-commissioned, second revision): a task rail ("ready for review"), file tabs, a gutter, a problem strip, and a status bar — and the docked assistant as the right column, with the work reflowing to pe-[420px] instead of being covered. The explanatory copy is gone: a product that has to describe its own AI integration does not have oneThe tab has to be a product, not a diagram of one. Removing the prose is what forces the integration to carry the argument by itself — if the entry points are not discoverable in the UI, no paragraph above it will save them
2026-08-22The dev tool tab was rebuilt as a real editor (user-commissioned; the first version was a static conversation card beside a fake editor, which showed the components but not the integration). It now drives the GLOBAL assistant: the open file is the page chip, every file row and code line is attachable, the problem strip carries an inline Ask AI, and selecting code attaches the selectionA composition demo proves the components exist. A product proves the layer works. The rewrite also caught a real bug: the seed effect reached send through a ref refreshed by a LATER effect, so a prompt seeded in the same click as addChip composed against the previous chip list — an explain-this-file answer came back generic
2026-08-22Radius comes from Tailwind, like every other scale (user-commissioned): the ramp was 0/2/4/8/12/16/20/24 — 20 is on no scale, 6 and 32 were missing — and worse, globals.css defined the NAMED steps as multiples of the pick (--radius-xl: calc(var(--radius) * 1.4)), so rounded-xl in this app was not Tailwind's xl at all. Now the scale is Tailwind's verbatim and the Foundation slides a WINDOW along it: the chosen step becomes --radius/rounded-lg and the neighbours are the adjacent Tailwind values, clamped at the ends. At the default (8 = lg) the emitted map is Tailwind's own, unchangedThe rule was already written — the scales of record are Tailwind's — and radius was the one dimension quietly exempt from it. A picker offering 20px taught a value no utility could express, and multiplied steps meant rounded-2xl drifted further from the scale the more the base moved
2026-08-22Evidence blocks stage their arrival (user-commissioned): one shared primitive (staging.tsxuseStagedReveal, StageSkeleton, StagedItem) gives ReasoningPanel, ParallelTools, WebSearch and ResearchReport the same three beats — shimmer on the ambient accent, a hold, then items one at a time on the control motion role. staged={false} renders settledThese blocks are complete in the data and would paint in one frame. Painting them instantly is a small lie — it says the work was free — and it robs the reader of the one thing the block is for, which is watching the assistant think. It lives in ONE place so four blocks cannot drift into four ideas of what waiting looks like
2026-08-22FollowUpSuggestions' list variant became a titled group (user-commissioned, against a reference): rows with real padding that wrap to two lines, hairline dividers, a heading, and an open-arrow per row — instead of full-width xs buttons that could not hold a sentence. arrow-up-right mapped across all five icon librariesPills and rows are not one object at two sizes: a pill is an afterthought you can ignore, a row is a question you are meant to read. The old list was a pill stretched to full width, which gave it neither
2026-08-22Every block that took time shows that it took time (user-commissioned, stated repeatedly until it became a rule): all ten evidence/artifact blocks — ReasoningPanel, ParallelTools, WebSearch, ResearchReport, ToolCall, ToolTimeline, TerminalBlock, CodeDiff, ReviewableDiff, CodeRunner — stage through one shared vocabulary: shimmer on the ambient accent with a MEASURED elapsed count (useElapsedSeconds, not a prop), then content arriving progressively (useStagedReveal), with written text landing through StreamingText. staged={false} renders settled. Kit type moved off arbitrary px onto Tailwind's text scale and kit controls off xs onto sm, so both follow the FoundationThe counter is measured because a trace that claims "thought for 5s" when it took two is a decoration pretending to be a measurement. Arbitrary px was quietly exempt from the propagation rule — the one rule the system exists to keep
2026-08-22QuoteReply became a selection-anchored edit bar (user-commissioned, against a reference): the bar measures the selection and attaches beneath its last line, centred on the whole selection; a state machine idle → thinking (shimmer + real count) → streaming (the rewrite arrives INTO the selection) → result (Keep / Discard / Retry). Owning the prose is a prop (text + a rewrite seam); without the seam actions only report. One found bug worth recording: pressing a control collapses the selection, whose selectionchange would unmount the bar before the click lands — the bar preventDefaults pointerdown on itselfAn edit offered where the text is, in the state it is actually in, is the difference between a toolbar and an editor. And the pointerdown guard is the kind of invariant that dies silently if it is not written down
2026-08-22MessageQueue's arrow interrupts, ReasoningEffort collapses: the queue's ↑ sends a turn NOW and re-queues the running one (a queue that shuffles mid-answer implies the running turn can be overtaken quietly); the effort control lives behind a ghost trigger stating the current level, because it is a setting, not a statusBoth are honesty fixes: controls should say what they actually do to the running work
2026-08-22Messages no longer carry the orb (user-commissioned): ResponseBlock drops its per-answer OrbCharacter/OrbGlyph author mark. The identity lives in the SHELL — the floating orb, the live border, the heat field, a surface's brand row — and its behaviour during an answer is the shell's to performA mark on every message repeated the shell's signature once per reply; in a long transcript the identity became wallpaper. One signature per surface, alignment says who is speaking
2026-08-22The Composer is a component; send↔stop is one control (user-commissioned, revised once): the input row was extracted from the surfaces into composer.tsx — context chips · input · one control that is Send while idle (lit only when there is text) and STOP while an answer runs (cancel mid-compose; settle mid-stream). The orb was tried in the send slot and REMOVED on sight: the identity already lives in the shell, and a character where a control belongs read as decoration, not affordance. variant now covers panel and quick (quick ask's pill hands its input band to the same component); the spotlight band is the remaining variant. ToolCall also lost its inline argument chip — the exact argument is evidence, and evidence lives in the disclosureA control whose meaning flips with the moment is honest only if it lives in ONE place — three hand-rolled input rows would each drift their own way. And the orb lesson is worth keeping: identity marks and controls are different species, even at the same size
2026-08-22The dev tool became a scenario simulation (user-commissioned, from a written brief): composeResponse is now a SCENARIO ROUTER — intent detected from the question plus the attached context decides which blocks compose the answer (fix → reviewable diff; explain → light prose + refs; refactor → reasoning + parallel reads + two diffs + typecheck + timeline; run → failing terminal + failure with recovery follow-ups; fix-automatically → the same workflow continued; generate/compact → registry-grounded diffs; make-component → creation timeline through registry/vocabulary/Figma; token change → propagation timeline; figma sync → agentic steps; research → search + report with sources). The grammar gained failure and report kinds. FOLLOW-UPS ARE THE SEAMS: picking one hands the next intent back through the router, so scenarios chain — research → implement, run → fail → fix → tests → component → FigmaThe brief's goal, kept literally: demonstrate the vocabulary by making the user do real work. A few minutes of natural use walks nearly the whole library because the seams between scenarios are the suggestions, not a menu
2026-08-22The queue is real: sending while the agent is busy ENQUEUES (visible MessageQueue above the composer, editable, drains on settle); the composer gains a third state — busy WITH text shows a queue affordance beside Stop. An explicit Stop does not auto-drain: stopping means stop, and the next turn is sent deliberately. Thumbs-down now opens FeedbackDialog in placeQueue-on-send is what separates an agent from request/response chat; and a queue that keeps flowing after the user said stop would be the assistant overruling them
2026-08-22The Journey rail + the reactive workspace (user-commissioned): the dev tool's rail gains a Journey — ten numbered steps that drive the REAL assistant through the scenarios (attach the prescribed chip, ask the real question; the refactor step queues a second instruction mid-run to demonstrate the queue). Nothing is played back. And the workspace now RECEIVES the work: KitResponse.effect names what an answer did ("fix-composer"), the assistant relays it on settle through announceEffect, and the editor responds — the error line pulses on the quiet wash while the AI works (status bar: "AI is working on composer.tsx:4…"), then the source swaps to the fixed version, the wavy underline and problem strip clear, changed lines wear the positive wash, and the task/file rails flip to doneA simulation where the product never changes is a chat demo next to a screenshot. The effect channel keeps the layers honest: the assistant relays a NAME and the workspace decides what changing means — the layer still knows nothing about editors
2026-08-22The problem inventory — frontend and backend (user-commissioned): the workspace now carries five real errors across six files — two frontend (draft state lost in composer.tsx, an effect-without-deps render loop in thread-list.tsx) and three backend (an unawaited-promise 500 in api/messages.ts, a heartbeat-less SSE stream that proxies drop at 30s in api/stream.ts, an N+1 query at 340ms p95 in db/drafts.ts). A Problems panel lists them all with source badges; server/db files wear badges in the rail. Each has a router scenario with domain-true evidence (server log, curl transcript, EXPLAIN ANALYZE, subscription-leak metric) and its own effect — files heal INDIVIDUALLY, effects accumulate, and the problem count only goes down honestly. The Journey regrouped into Frontend errors / Backend errors / Agent workflows / Build & ship, and steps now SPOTLIGHT: open the file, ring the line, then askAn inventory of one error is an anecdote; five across both halves of the stack is an environment. And the spotlight is what makes a guided step feel like guidance rather than a button that does something elsewhere
2026-08-23The /ds rail eats its own cooking (user-commissioned): the design-system page's hand-rolled sidebar — ad-hoc headings and ~250 lines of repeated button markup — was rebuilt on the sanctioned Sidebar component (collapsible="none" embeds it as a static column; SidebarGroup/GroupLabel/Menu/MenuButton carry the structure, isActive carries selection). The kit groups (Messages / Tool use / Knowledge) keep their accent labels via SidebarGroupLabel className="text-primary"The registry documents Sidebar as a vocabulary component while the page listing it drew its own — the exact near-miss rule 4 forbids. 54 nav rows now restyle from one component
2026-08-23The /ds rail, evolved to the Sidebar's full anatomy (user-commissioned, against the shadcn/studio reference): brand header (OrbGlyph + name on a size="lg" menu button), a SidebarInput search that filters every list and holds the folds open while querying, icons on the page-level rows (three new names — palette, ruler, layers — mapped across all five libraries), the kit groups as Collapsible sections with live counts, and the assistant in the SidebarFooter ("Ask ambientui ⌘K" → spotlight). Collapsible was installed through the shadcn CLI and documented before useThe rail is now the component's whole vocabulary demonstrated in situ — header, input, groups, folds, badges, footer — which is what a design-system page's own chrome should be
2026-08-23The radius propagation leak, found by a question: the user asked whether a rounded-xl in the sidebar followed the Foundation. It did not — nothing rounded-* did. The radius steps sat as LITERALS inside @theme inline, and inline bakes whatever is written there into every utility (.rounded-xl { border-radius: 0.75rem }); the old shadcn calc(var(--radius)…) had survived inlining precisely because it referenced a runtime var, and the "verbatim values" rewrite silently lost that property. Fixed by routing the theme entries through runtime tokens (--radius-xs: var(--radius-window-xs)), defaults = Tailwind's ramp verbatim, and the Foundation re-emits --radius-window-* from radiusWindow. Verified live: picking 2xl moves a sidebar row from 7px to 21px, and .rounded-xl now compiles to var(--radius-window-xl)The propagation rule has a compiler-level failure mode: @theme inline freezes literals. The invariant worth writing down — anything in @theme inline that the Foundation must drive has to be a var() reference, never a value. Also removed the kit-group icons in the rail; group chevrons now use the app's one disclosure pattern (right closed, down open)
2026-08-23The evidence blocks learn to move like one system: StagedItem — the one primitive every staged block lands through — now animates height alongside opacity, because a row that appears at full height shoves the list in a single frame and reads as jank regardless of its own fade. The staging hook gained a replay key so a block can perform its arrival again (CodeRunner's play now runs: output clears, the clock restarts, the result streams back). WebSearch's query shimmers while sources are read. ToolTimeline's steps became content-hugging ghost Buttons with an onStepSelect jump seam; ToolFailure gained onFeedback, opening the same inline FeedbackDialog the message actions use. The /ds rail flattened to one row size (default)Smoothness fixes belong in the shared primitive, never per block — one edit moved every staged component at once, which is the reason StagedItem exists. And a failure is a moment the user knows exactly what went wrong: capture feedback there, in the ambient language, not in a detached modal
2026-08-23The palette learns where it is: the spotlight's Recent chats and Suggested prompts were canvas-era constants shown everywhere, so opening ⌘K inside the dev environment offered to "summarize what's on this canvas" while five real errors sat unfixed two panes away. Added setPageIntel alongside setPageChip — the same announce-from-the-page shape — and the dev tool now publishes its LIVE problem inventory as the suggestions (each already phrased as the question the scenario router recognizes), plus a plausible dev working history. A healed problem leaves the palette. With everything fixed it offers the run / refactor / research scenarios insteadIntelligence in an ambient layer is not a better model in the answer — it is the surface knowing what is true right now before it is asked. The suggestions being DERIVED from workspace state (not a second hardcoded list) is what makes it hold: there is no way for the palette to promise a problem the editor has already fixed
2026-08-23One answer, one sequence — and the palette becomes the workspace's. Every staged block ran a private timer, so a reasoning panel, a tool call and the answer text all raced: the sentence explaining the work appeared while the work was still visibly spinning. Added stage-queue.ts — blocks claim a slot in mount order inside useStagedReveal and hold at their shimmer until every earlier block has finished; ResponseBlock owns the queue and the prose does not mount until it settles. Elapsed counters gate on the turn (a queued block must not quote someone else's seconds), and KitBlockView now passes staged={live}, which also fixed settled history replaying its evidence. Separately the dev tool took over "Jump to" (its files, each subtitled with its actual problem), gained a Fix all suggestion whose fix-all effect heals the whole inventory in one run, and got its own ask-line and section labels via PageIntelThe ordering rule had to live in the primitive, not in the blocks — the previous behavior was not a bug in any one component, it was the absence of anyone owning the sequence. The copy lesson is the same shape: "Open chat with page context" named a surface the system does not have (it is the panel), which is exactly the drift the terminology register exists to catch
2026-08-23The dev tool becomes a change under review, and the shell stops contradicting itself. From a reference: the workspace now names the change it is (id, branch, +/−), the editor shows the pending diff with signed gutters, and a teammate's note threads under the line it questions — ReviewComment, a new pattern, whose reply row is the Composer's new inline variant and whose reply hands the work to the panel with that line attached. The view switcher became ViewMenu, a disclosure that morphs pill→card (a tab strip spends room permanently on a surface whose argument is that chrome should get out of the way). The rail hides on ] with a visible control beside it, and every strip scrolls instead of squeezing. Three shell bugs fixed: the input-watching effect was overwriting the pipeline's thinking state the instant send cleared the field; a settled answer re-streamed whenever the surface changed (panel→dock remounts) — messages now record settled; and the thinking counter never froze because its flag was staged, true forever. Thinking now holds a real floor (THINKING_FLOOR_MS, 15s) and the orb stays in thinking until the prose actually starts (onAnswerStart), not until the answer was merely composedThree of these were the same class of bug: two things owning one piece of state, with no rule about which wins. The ambient effect and the response pipeline both drove the orb; position and history both decided live; staged was standing in for working. The fix each time was to name the owner. And the reference decomposed exactly as §2 predicts — most of it was configuration and existing vocabulary, with one genuinely new component that went through governance rather than being styled in place
2026-08-23The dev tool goes responsive, and the stream's comment stops lying. The rail hides on ] with a visible control beside it (a shortcut nobody can discover is not an affordance), auto-hides below lg where it would cover the work, and floats over rather than squeezing; every strip — header, file tabs, problems — scrolls internally so the page itself never overflows at 375px. Also: StreamingText's comment claimed the frame clock survives a hidden tab. It does not — requestAnimationFrame is paused there, which is exactly what made an answer appear to stall during headless testing. What the frame clock actually buys is RECOVERY: the write head derives from elapsed time, so returning to the tab puts it where the clock says, rather than wherever a throttled timer had counted to. Comment corrected to say the true thingA comment that overstates a guarantee is worse than no comment: it sent me hunting a product bug that was an artifact of the test harness. The rule the codebase already follows elsewhere — state the constraint, not the reassurance
2026-08-23The panel's empty state stops being a second component. It hand-rolled its own list of suggestion buttons — a near-miss of FollowUpSuggestions that would drift the moment the real one changed (rule 4's "never re-implement a near-miss", violated by the layer's own surface). It now opens with the character, states what it can actually see by naming the page chip, and hands the offers to FollowUpSuggestions, drawing them from setPageIntel so the panel and the palette propose the same work. The Composer's mark also moved from an opt-in prop to a per-variant default, after the /ds playground showed a composer with no character — the one place the component is meant to be understood was showing the least representative version of it. Arbitrary px on the touched lines went onto Tailwind's scaleBoth are the same failure: a decision that should belong to the component was left to whoever used it. A default the caller must remember is a default that will be forgotten, and the registry is where you find out
2026-08-23Pasted material is context, not the question — and the landing page is built from the thing it argues for. Pasting bulk text into the composer now attaches it instead of filling the field: a stack trace dropped into a one-line input buries the sentence being written under material the user only meant to REFER to. MessageAttachments absorbed the staged case rather than a second component being written for it — onRemove present while staged, absent once sent, because removability is the only honest difference between the two moments — and gained a text kind for quoted material that is not a file. The ambient-layer page was rebuilt as a long-form explainer (concept → four shapes → ⌘K → context → answers → the system) whose every answer illustration is a live component rendered settled, with an exit to the registry and the repo. The orb's lifecycle playback no longer opens on a three-second hold in the state it was already resting in: a run plays the TRANSITIONS, so pressing play movesThe attachment case is the same lesson as the empty state two rows up: the near-miss component you are about to write is usually the one you already have, one prop away. And a page that argued for building the assistant from the product's own system, then illustrated itself with screenshots, would have been arguing against itself — so the illustrations had to be the components, which also means they cannot rot
2026-08-23The governing documents join /ds, rendered from their real bytes — DESIGN.md, CLAUDE.md, PAPER.md, README, the Figma and porting procedures, and the four skill files, imported with Vite's ?raw into a ~200-line renderer that composes the sanctioned Table. A hand-written summary of the rules at /ds would have been a second source of truth, and the second one is always the one that rots; a markdown library would have been a new dependency for one page. notes/ is deliberately excluded — gitignored provenance is not part of the system's public account of itself. Also: ONE CHARACTER PER SURFACE, in the row you speak to it. The orb left the AI Overview header (which now names the session, like the panel), the palette footer, and the view menu; it remains in the composer's mark, the quick-ask pill and the resting orb itself. Appearance switching moved into the view menu, stating what it will do rather than what is trueA shell that stamps its character on every bar is signing itself once per component. The mark means "the assistant is listening HERE", so it belongs where you type and nowhere else — the moment it appears in chrome it stops being a signal and becomes a logo
2026-08-23A fifth mode: history — full-screen, translucent, the record of what has been asked here beside a live transcript and composer. Adding a mode is the governance event §8 names, and the test it has to pass is whether the new thing is a new GEOMETRY or a new set of parts: this is the former, composing the sanctioned Sidebar with the same transcript and Composer every other mode uses. It earns the whole screen by not being about a single exchange — the other four are sized to how much attention one answer deserves, and "what have I asked here" is a different question. It stays translucent rather than navigating away, because the work is the reason you opened the record, and Esc returns you to the conversation rather than to rest. Picking a past conversation asks it again rather than restoring a transcript; the docs say so plainly instead of implying a persistence the layer does not have. New icon history, mapped across all five librariesThe honest limit is the interesting part. A history surface that pretended to restore transcripts would have been one mock away, and it would have been the kind of lie the response kit exists to avoid — so the record offers what it can actually do, and says which that is
2026-08-23Progressive edge blur: built, then removed — history's bars briefly traded their borders for a layered backdrop-blur falloff (three masked layers, because one has a visible seam where its mask ends). It worked and was still wrong: on a full-screen surface it competed with the scrim behind it for the same job, and a border says where a bar ends more cheaply and more quietly. Reverted. What replaced it is the thing the blur was reaching for: the composer now OVERLAYS the transcript on the layer's own glass, so the answer visibly moves underneath it — a bar the content cannot pass behind has nothing to be translucent about. Full-screen also earned its own translucency step, --glass-scrim / .ambient-scrim: heavier fill and stronger blur than the panel's glass, because a surface that covers the product has to carry its own legibility, while staying translucent so the work stays visible as ground. Kept from the same batch: the readable-column cap moved off the shared transcript onto the surface that needs itTwo lessons. The cap was the second time this session a constraint got attached to the wrong owner — the composer's mark was the first — so ask of any prop like that: whose problem is this? And the blur is the second effect built then cut for competing with something that already worked; the beam glow was the precedent, and the pattern is that an effect solving a problem another layer already solves reads as noise no matter how well it is made
2026-08-23⌘K becomes the way around the whole system — components, documentation, demos and the assistant's own forms, all reachable by typing. The commands are registered by the APP (command-registry.tsx) and merely rendered by the layer: the context-chip contract pointed the other way, which is what lets the palette reach a product the layer knows nothing about. Lists are derived from the registries that already document those things, so a command for something that does not exist cannot be written. Two fixes the build surfaced: matches are capped PER SECTION, because one cap across the whole list let forty components matching "panel" push Switch form off the end — the command the word most obviously meant was the one you could not reach; and the resting palette now states its own capabilities, with Switch form shown whole and the big families advertised by a counted hint derived from what is registered. Also: /ds selections live in the URL (?c=), and the governing documents render there from their real bytes via ?rawThe feature had been built and still was not real, because at rest the palette advertised none of it — a capability nobody can see does not exist as far as the user is concerned. Worth generalising: shipping a command surface means shipping its discoverability, and the hint has to be derived, or it will outlive the thing it describes
2026-08-23History wears the layer's material, and the rule is written down. The one full-screen surface had the scrim but no heat field — the loudest possible place to drop the identity. Getting it right took three wrong combinations, which is why renderField now takes a PLACE (panel / ground / screen) rather than two knobs: the panel recipe read flat at full-screen scale, the thin stage veil alone left the field invisible, and full presence under that same thin veil left the shader raw with no translucency over it at all. screen is full presence under the full frost — the field carries a whole window and the veil still sits on top. The rail's bg-sidebar went the same way: it read correctly as a navigable pane and punched a solid hole through the field and the work behind it. §8 gains the rule. Also: staged attachments render as compact chips in one scrolling row, and the Composer is now their ONLY owner — ContextRow was rendering them too, so every staged item appeared twice with two remove buttons for one thingThird time this session for the same class of bug: two components rendering one thing with no rule about which owns it (the composer's mark, the readable-column cap, now attachments). The tell is a prop threaded to two places "so either can show it"; the fix is to name the owner, which is whoever creates the thing. And when a knob has been set wrong in every possible way, it should not be a knob — name the combinations that mean something
2026-08-23One veil per surface, weighted for the field under it. Veiling each chrome bar individually was the wrong fix and lasted one commit: it made the header, rail and composer read as solid panes inside a translucent surface, and it left the CONTENT area — the largest part — still glowing, because that was the one region carrying a single veil over a full-presence shader. The veil belongs to the surface and its weight must match the field's strength: place="screen" uses .ambient-screen-frost (84%) where the panel's 62% was tuned for a field at low presence. The bars carry a border and nothing else. .ambient-clear (blur, no fill) was deleted rather than left lying around — one user, and once that user needed a veil, a fill-less recipe was only ever going to be picked wrongly again. Separately: the + moved into the composer beside Send, making the Composer the single owner of attaching as well as of attachments and the paste that creates them; the compact attachment row pages with arrows when it overflowsSymptom-chasing produced the wrong layer twice — first veiling each bar, then finding the content area was the actual gap. The question that skips both: which element OWNS covering this field? One surface, one veil. And a variant that exists to omit something will be reached for by someone who does not know what the omission costs — deleting ambient-clear beat documenting when not to use it
2026-08-23The field is scoped to the conversation, and the screen veil goes to 92%. Run across the whole full-screen surface, the shader lit the record rail and the chrome as well — both of them lists, which are read rather than felt, and a shader under text is noise. Bounded to the conversation column it does the job it is for: the ground an answer arrives on. Verified against the layout rather than by eye — the field's box starts at the rail's right edge and below the header, so it cannot reach either. The veil went 62 → 84 → 92% over three passes: the panel's weight is tuned for a field at low presence, and at full presence anything lighter reads as weather rather than as groundTwo different questions kept getting confused: how STRONG the veil is, and how FAR the field reaches. Darkening was never going to fix a field that was simply in the wrong places, and scoping was never going to fix a veil tuned for a different strength. Worth asking explicitly the next time a surface looks wrong: is this the wrong amount, or the wrong extent?
2026-08-23An overlaying bar veils the content, not the field. Removing the composer bar's frost (to stop it double-veiling the surface) took away the only thing standing between the input and the transcript scrolling behind it — the answer read straight through the composer. The surface's veil covers the FIELD; a bar's veil covers the CONTENT moving behind it, which is painted above the surface veil and cannot be reached by it. Both were called "the veil", which is how one got deleted to fix the otherThe one-veil-per-surface rule was right and I over-applied it. A rule about who owns a layer still has to survive the question "owns it against WHAT?" — same word, two backgrounds, opposite answers
2026-08-23Sparkles stops meaning two things. Context chips wore the sparkles mark for the page kind, and so did the "Attach context" button — but in this system sparkles means the ASSISTANT: its mark, AskAI, the follow-up heading. A context chip is not the assistant; it is the thing the assistant can see, so it wears the icon of the thing. The whole chipKindIcon map moved onto sanctioned IconNames at the same time (rule 6, fix-on-touch), which removed the last direct HugeIcons usage from that path. MessageAttachments is documented for what it now is: one component, two densities, with a compact story showing the staged row and its paging, and a whenNotToUse recording that the Composer owns staged attachments aloneAn icon is a word. Using the assistant's word for a page meant the layer was signing something it did not author, which is the same error as the orb appearing in headers — the mark belongs to one idea, and every borrowing of it costs that idea a little precision
2026-08-23The icon vocabulary becomes visible, and the chip map is documented. Rule 6 says a component may only use a name from the vocabulary and that adding one costs five mappings — but the legal set existed only in icon-library.ts, which makes the rule unenforceable by anyone who has not read that file. The Foundation's Icons section now renders the whole vocabulary (44 names) live, so it doubles as proof that the picked library covers every one. ContextChip's docs gained the kind→icon map, the per-chip icon override and the rule that sparkles is never a chip icon, with a story showing an overridden chip beside typed onesA constraint nobody can see is a constraint nobody can follow. The vocabulary had the same problem ⌘K did two days of work earlier: built, correct, and invisible — which for a rule means unenforced, and for a feature means absent
2026-08-23The canvas ground becomes abstract gradients (user direction), replacing the aerial-nature set. Twelve candidates all returned 200, and looking at them is what did the actual selecting: one was a photograph of an office, one a starfield, one hard-edged colour blocks, two ink-in-water. Seven survived the only test that matters for this ground — it sits under glass carrying text, so it has to read as a FIELD OF COLOUR rather than as a subject. family: light | aerial became tone: light | deep, which says what the ground does to the surfaces on it rather than what it depictsAn HTTP 200 is not verification of an image. The check that mattered could not be automated and took one screenshot: five of twelve loaded perfectly and were still wrong
2026-08-23The decision log is one table again, and the renderer stops eating escaped pipes. Blank lines had crept between rows, and /ds renders the real bytes: everything after the first twenty-four rows was falling out of the table and printing as raw pipe-text. Separately splitRow split on every `, so a cell holding a union type (light | deep`) silently grew a column and shifted the rest out of their headings
2026-08-23The stage veil carries more weight (user direction): 32% → 48%, blur ×0.5 → ×0.8, and saturate 1.35 → 1.1. The old numbers were tuned against aerial photography; abstract gradients carry far more saturation and put a light ground under the canvas text, and pushing saturation further was making it worse rather than richerA veil is tuned against the field it covers. Change the field and the veil is no longer tuned — the token did not become wrong, its subject did
2026-08-23One row above the input owns everything the question is about (user direction). Context chips and staged attachments were two stacked rows; they are now one ChipSlider, with AttachmentChip drawn from one place and rendered by the context row wherever a context row exists. Composer's own compact attachments remain for a composer standing alone, and the two are never both given attachmentsThird time this shape has appeared: two components rendering one thing with no rule about which owns it. The first fix here was to stop the double render by STACKING them, which traded two remove buttons for two levels. Separating is not the same as naming an owner
2026-08-23⌘K matches on terms, and the opening word must be a whole word. looksLikeQuestion used a bare prefix test, so every word beginning with an interrogative was read as one — "do" matched documentation, "can" matched canvas, "is" matched isolation — and typing the name of the thing you wanted hid the command that went there. Matching was also a substring test against the whole query, so nothing phrased the way people ask ("go to the documentation") could match anything. Now: whole-word interrogatives, term matching with filler words dropped, and a question no longer SUPPRESSES the commands it matched — Ask leads, they followI reported this as "the filter doesn't search section". It already did. The diagnosis was a guess from the symptom; reading the predicate took a minute and found two different bugs
2026-08-23Appearance moves to the head of the closed ViewMenu pill (user direction), as a ghost icon. It was a row inside the menu: one click deep, and drawn exactly like the three destinations above it, so a two-state setting read as a fourth place to goThe menu's list is destinations. A control that is not one does not become one by sitting in the list — it just stops looking like what it is
2026-08-23The ViewMenu pill becomes two icon peers with real tooltips (user direction). Menu/Close became a chevron/close icon, and appearance took the same treatment — they are peers, neither being a destination. Both moved off native title onto the sanctioned Tooltip, which its own docs already name icon-only buttons as the case for; the disclosure's is suppressed while opentitle was acceptable while the controls had text. The moment a control is icon-only its name stops being a nicety, and title cannot deliver one to a keyboard
2026-08-23The ViewMenu pill morphs to the menu's width (user direction), animated on the surface spring, so the pill and the list read as one object changing shape. The width is measured from the menu with a ResizeObserver rather than guessed, and the menu lost min-w-full — with the pill taking the menu's width, a menu also taking the pill's would leave neither with anything driving itThe old rule was "the trigger does not move". It survives on the axis that mattered — the list still drops rather than pushing, and the row's HEIGHT never changes — but stating an invariant more broadly than its reason held it made it look violated when it was not
2026-08-23menu and home enter the icon vocabulary (user direction), each mapped across all five libraries — hugeicons Menu01Icon/Home01Icon, lucide Menu/House, tabler IconMenu2/IconHome, phosphor List/House, remix RiMenuLine/RiHomeLine. The ViewMenu disclosure wears the menu mark, and a home control leads the pill: the app names where home is, the menu only knows how to get there. The vocabulary count in the row above moved 42 → 44Verified by cycling the Foundation through all five libraries and reading the rendered glyph, not by trusting that the export names existed. Lucide had already renamed Home to House; a mapping that type-checks against the wrong library still renders nothing
2026-08-23The repo is being restructured for public consumption — four doors (one component · the whole ambient layer · design infrastructure · the governance layer for your AI), distributed through a shadcn registry plus npm. Phase 0 lands here: MIT LICENSE, the @source repair below, and the unused zod dependency dropped from packages/uiThe project already contained a philosophy, a reference and a demo. What it lacked was a front door and any way to take the code — docs/porting-the-assistant.md being a 318-line manual port guide is the proof
2026-08-23The Tailwind @source globs were repaired. packages/ui/src/styles/globals.css resolved ../../../apps/** and ../../../components/** — but @source resolves relative to the CSS file, so from packages/ui/src/styles those pointed at packages/apps/** and packages/components/**, neither of which has ever existed. Now four levels up, and scoped to apps/*/src and packages/*/srcThe build was correct only by accident: Tailwind v4's automatic detection from the Vite root was covering every app class. That crutch vanishes the moment code moves into a package, and it fails silently — verified the fix is byte-identical today (127,128 bytes, same 12 .ambient-* classes) so the repair is provably inert until it is needed
2026-08-23docs/porting-the-assistant.md marked stale rather than shipped. It describes four source files, a 1,054-line assistant.tsx and a beam.tsx (18 mentions) that no longer exists; the layer is 17 files and 8,269 linesIt was about to become a public repo's only distribution document. A wrong instruction is worse than a missing one, and the registry is what actually retires it
2026-08-23The ambient layer's dependency on the Foundation is inverted. ambient-runtime.tsx states what the layer needs from a design system — seven values and two motion hooks — with a context that carries a REAL DEFAULT and a hook that never throws, the deliberate opposite of useFoundation(). FoundationProvider now implements that interface through a bridge, so the app is unchanged. Eight files stopped importing @/foundation/foundation-contextThe layer consumed 7 values out of 792 lines of configurator. That is not a dependency, it is an accident of where the two things happened to live. satisfies AmbientRuntime on the bridge means adding a required field breaks the Foundation's build rather than silently falling back to a default nobody chose
2026-08-23The palette is TOLD its destinations. assistant.tsx imported sections from @/nav — a UI layer reaching into one product's route table, and the single thing that made it un-liftable. AssistantProvider now takes navItems, defaulting to []; the app supplies its own. icon is typed unknown so no icon library leaks into the public contractSame shape as the context-chip contract and the command registry: the app hands the layer meaning, the layer never reaches for it. Empty is a valid state — the palette simply shows no Jump-to
2026-08-23A bare-mount harness now proves it (packages/ambient/dev/index.html + src/bare.tsx, dev-only — Vite builds index.html, so it never ships). It renders <Assistant /> with no Foundation, Icon-library or Tooltip provider. Verified: orb shader renders, palette opens with 7 items, .ambient-glass applies, 6 icons draw, zero errors on loadThe claim "the layer stands alone" was previously a code reading. Now it is a page you can open. A first attempt tested this by deleting FoundationProvider from App.tsx and proved nothing — the errors came from the app's own pages, not the layer. Isolation had to be real
2026-08-23The layer's material moves out of the app's theme. theme.css (320 lines) split: the glass recipes, translucency tokens, live border, glyph breath, stream edge and shimmer became components/assistant/ambient.css (298 lines); the app keeps 45 — its radius, its overflow: hidden shell, --app-chip and the charcoal .dark block. --ambient-blur also left packages/ui/globals.css, where an ambient token had no business sittingThe two shipped together, so nobody had to ask which file owned what. The split answers it: ambient.css DERIVES everything from standard shadcn roles, so an adopter's own theme retints the whole surface without editing it
2026-08-23--app-blue becomes --ambient-accent. It is public API — it drives the live border, the stream's leading edge and the shimmering placeholder — and a published token cannot be named after one app's blue. The layer's 8 references were renamed; --app-blue: var(--ambient-accent) stays in the app for one release so 8k lines need not move in the same commitVerified in the bare mount, where theme.css is absent: --app-blue is undefined, --ambient-accent resolves, the orb renders, zero errors. The alias is a courtesy to this app, not a dependency of the layer
2026-08-23Fallbacks for the two non-shadcn tokens. var(--positive, oklch(0.596 0.145 163.225)) and var(--spacing, 0.25rem) in ambient.cssAn undefined custom property inside color-mix() does not fall back — it makes the whole declaration invalid, so the surface renders TRANSPARENT. In someone else's app that reads as a rendering bug in our component, and they would be right to think so
2026-08-23The ambient layer becomes a package. All 19 files moved apps/site/src/components/assistant/packages/ambient/src/ as @ambientui/ambient, with its own manifest, tsconfig and eslint config. Ten app files, the Foundation bridge, the tsconfig paths and a Vite alias were repointed. CLAUDE.md, DESIGN.md §6 and two skills were updated in the same commitThe seam was already cut in P1 and P2; this makes it structural. The compiler is the enforcer now — @/ does not resolve from inside the package, so the decoupling cannot silently rot back
2026-08-23The @source repair from P0 paid for itself here. The layer's code left apps/** for packages/ambient/src, which is exactly the move that would have silently dropped every .ambient-* class from the production stylesheet under the old broken globs. Verified after the move: 127,364 bytes, all 12 ambient classes, zero selectors lost against the phase-0 baselineThis is why the repair was made inert-but-correct three commits early rather than at the point of need. A silent CSS failure discovered after a framework migration would have been attributed to the migration
2026-08-23The Foundation engine becomes a package, and its page does not. foundation-context.tsx (819 lines) → packages/foundation/src as @ambientui/foundation; foundation-page.tsx (665 lines) moved the OTHER way, into apps/site/src/components/ds/. The engine imports only React, the ambient runtime contract and one @ambientui/ui module; the page imports colors-page, theme-provider and the app's IconThey sat in one directory called "foundation" and were two different things: a configuration engine, and a configurator UI. Shipping the page inside the package would have dragged SectionRail, settings-kit and app routing into a library. The split is which one a stranger could use
2026-08-23@workspace/ui@ambientui/ui, 131 occurrences across 57 files, plus the governing docs. apps/web's package renamed to @ambientui/sitethe DIRECTORY was deliberately left alone: system-docs.ts carries ten ?raw imports at ../../../../../ depth and DESIGN.md, CLAUDE.md, PAPER.md and the skills reference apps/web/... in prose@workspace/* is scaffold naming — it says "this is a monorepo" to people who can already see that, and says nothing to a stranger reading the tree on GitHub. The directory rename is the opposite trade: churn buying one clearer filename at the cost of a hundred stale prose references in a repo whose docs ARE the product
2026-08-23The layer is installable. registry.json + scripts/build-registry.mjs produce four shadcn items — ambient-layer (18 files), ambient-styles, icon, governance — built to apps/web/public/r. registry:check joins the gate, so a hand-edited or stale registry cannot pass. The host is one env var (AMBIENTUI_REGISTRY_HOST) because published URLs are the hardest thing here to change laterdocs/porting-the-assistant.md was 318 lines of manual instructions. It is replaced by one command. Verified end to end into a scratch Vite app that is not this repo: 29 files installed, tsc clean, vite build green at 19,170 modules, and the spotlight rendering with the orb, glass and palette
2026-08-23Published files speak alias; the repo speaks package. The build stages a rewritten copy under .registry/: @ambientui/ui/components/*@/components/ui/*, lib and hooks likewise. In-repo the layer imports real package specifiers, which is what makes the boundary compiler-enforced; the shadcn CLI only rewrites @/ prefixes, so published verbatim every installed file failed to resolveTwo dialects, one source, transform at build. The first install proved it: 18 files landed and not one of them could find @ambientui/ui. Also surfaced that icon and icon-library are OURS and had to become a registry item — the layer's <Icon name> vocabulary does not exist in anyone else's app
2026-08-23A latent mask bug, found only by installing elsewhere. .ambient-live-border set maskmask-composite: exclude-webkit-mask-webkit-mask-composite. -webkit-mask is an ALIAS for the mask shorthand, so it resets mask-composite. This repo computed xor and drew the ring; the consumer computed add, the exclusion never happened, and a full-surface conic gradient painted over the spotlight. Reordered webkit-first so the standard longhand lands last; both now compute excludeThe CSS was identical in both apps. Only the pipeline differed. No amount of looking at our own screenshots would have found this — the test had to be a different app
2026-08-23The registry is generated from the documentation. scripts/extract-catalog.mjs reads all 50 documented components out of ds-docs.tsx with the TypeScript parser (not regex — the entries interleave prose with JSX) and hands them to the registry build IN MEMORY. ds-docs.tsx stays the single source; there is no catalog file on disk to go stale. 31 per-component aliases now exist, each carrying its own whenToUse / whenNotToUse as CLI docsCLAUDE.md rule 10 says undocumented components do not exist. This makes that mechanical: a component reaches the registry BECAUSE it is documented, through the documentation itself. And the rules now print in the terminal of everyone who installs one — the bounded space travels with the code instead of living on a page nobody reads
2026-08-23A component cannot fall out of the registry silently. Every documented ambient component must resolve to a real export or be listed in NOT_DISTRIBUTABLE with a reason, else the build fails. Only command-palette is listed — it is a surface of the Assistant, not a separate export. Verified by renaming an export in ds-docs.tsx and watching the build refuse with the file and the name it looked forIt also surfaced a real mismatch: the docs said ContextChip, the export is ContextChipView. Resolved with the existing sibling convention (ContextChip · ContextChipView) so what you read and what you import are both on the page
2026-08-23ambient-marks.tsx splits the shared marks out of the shell. ShimmerPlaceholder, AssistantMark, ContextChipView and IconTile lived in assistant.tsx — 1,700 lines of surface — and were used ONLY by composer.tsx. That single import edge meant every component in the layer transitively required the whole surface: add reasoning-panel produced 16 files. Now 11, with assistant.tsx gone from the closure; the smallest aliases are 2–5 filesNothing here is new code. They were always a separate thing; the shell just happened to be where they were written. The cost was invisible until something tried to install one piece — distribution is a design review you cannot argue with
2026-08-23The three packages are publishable, and the layer takes the unscoped name. ambientui, @ambientui/ui, @ambientui/foundation at 0.1.0, MIT, tsup builds with per-file output (bundle: false, because the exports map is per-module). React, react-dom, framer-motion and tailwind became peer dependencies; @paper-design/shaders-react an OPTIONAL peer, since exactly one file uses it and the layer degrades to the CSS twin without it. Internal deps got real semver ranges — npm does not rewrite "*" on publish the way pnpm rewrites workspace:*Verified by packing tarballs and installing them into a bare project: exactly ONE React in the tree, no nested node_modules under our packages, and a TypeScript file importing ambientui/assistant and @ambientui/ui/components/button compiling clean. npm i ambientui is the headline the scoped name could not be
2026-08-23"use client" on 25 files, and a build configured not to strip it. esbuild treats top-level directives as dead code, so tsup needs esbuild-plugin-preserve-directives — without it the build succeeds, publishes cleanly, and then fails in every Next.js App Router app that installs itA failure that far from its cause is worth a guard rather than a memory. Verified in the packed tarball, not the source: head -1 dist/assistant.js is "use client"
2026-08-23shadcn stops being a runtime dependency. globals.css imported shadcn/tailwind.css — 629 lines of CSS inside a 5.8 MB CLI whose own dependencies include @babel/core and an MCP SDK. Vendored to packages/ui/src/styles/shadcn-base.css (MIT, attributed, version-stamped) with vendor:check in the gate diffing it against the installed copyPublishing as it stood would have put a build tool in every consumer's node_modules to read a stylesheet. The drift check is the same idiom this repo already uses for the registry: copies are allowed, silent copies are not
2026-08-23/ becomes the argument, not an empty gradient. The landing view was ?view=layer — tab two of three — while the front door showed a bare canvas reading "Press ⌘K". The default view is now the layer, the ViewMenu lists destinations in the order a visitor meets them, and Home means the front doorAn invitation to press ⌘K, shown to someone who has not been told what ⌘K does, is not a front door. It was the single highest-leverage fix on the site and it is a one-line default
2026-08-23The site finally says how to take it (install-section.tsx). Leads with one command — the layer — then the namespace registration, then the narrower doors. Only commands that actually run appear: the design-infrastructure door has no registry item yet, so it links to the Foundation docs rather than printing a command that failsA broken install command on a landing page costs more trust than a missing one. The site had spent five sections demonstrating the layer and then offered no way to get it
2026-08-23The command block is composed, not promoted (user decision). TerminalBlock was the near-miss and the wrong meaning — it is ambient evidence of the ASSISTANT running something, not a command you type. Rather than a new vocabulary component, the treatment is composed from Button and tokens, written ONCE in the section that uses itPattern-watchlist call, answered by the owner. The mitigation for the drift risk is that it is written once rather than at each call site; if a third surface needs it, that is the moment to revisit promotion
2026-08-23The site can be served from a subpath. apps/site/src/base.ts is the one place that knows where the site is mounted; vite.config reads AMBIENTUI_BASE, and all four path sites (App.tsx routing, ds-route, home-page, command-registry) go through withBase / stripBase — no bare path literal survives outside the helpersectionFromPath matched on window.location.pathname, so under /ambientui/ it saw ambientui/ds and silently routed to the canvas. The failure mode of a missed prefix is a page that loads, renders, and routes to the wrong thing — which reads as a routing bug, not a deployment one. Verified by building with a base and serving it: /ambientui//ambientui/ds with the Foundation rendering
2026-08-23The favicon was pointing at a file this project has never had (/vite.svg), and index.html carried no description or social card. Now %BASE_URL%orb-circle.svg — the project's own mark, and the placeholder Vite actually substitutes — plus title, description and OG tags. og:image is deliberately ABSENT until a real 1200×630 raster existsA bare /orb-circle.svg in index.html is not rewritten by Vite, so it 404s from a subpath. And a social card pointing at a missing or SVG image renders as a broken preview, which is worse than the plain-text card you get with none
2026-08-23CI, and the README rewritten for a stranger. Two workflows: gate.yml runs the same npm run gate the pre-commit hook runs, so the rule holds for contributors without the hook installed; deploy.yml gates, regenerates the registry, builds with the base path, and publishes to Pages with .nojekyll and a 404.html copyThe old README opened with "Read DESIGN.md before any UI work" — addressed to an agent inside the repo, not to a visitor. GitHub is the other front door, and it was showing internal instructions
2026-08-23/gallery — the vocabulary in one scroll (reference: beautifului.dev, taken as INFORMATION ARCHITECTURE, not as a look). 32 numbered entries, each a live component with a one-line description and the exact command that installs it. Generated from AMBIENT_COMPONENTS, the same catalog that feeds the registry and /ds, so there is no gallery copy to write and none to go staleThe reference site is presentation-forward and distribution-absent — a beautiful gallery of 20 primitives with no install command anywhere and a "book a call" CTA. This project is the exact inverse: a verified registry behind a docs rail that asks a stranger to pick before knowing what anything is. The gallery is their IA over our distribution, which neither of us had
2026-08-23A gallery entry falls back to its playground when it has no story. Eleven of the 32 carry stories: [] and a playground instead — the interactive prop surface IS their demonstration. Their controls portal into the Inspect rail, and ControlsHostContext defaults to null with the portal guarded, so on the gallery the preview renders and the controls simply do notWithout the fallback those eleven rendered a heading, a description and an empty 156px gap. Caught by measuring every section's height rather than by scrolling past them
2026-08-23CommandLine extracted to components/command-line.tsx. The install section and the gallery are the first and second surfaces to need it; the note left in the first said the third would be the moment to revisit promotion. It is still composed rather than a vocabulary component (owner's call), but written onceThe mitigation for declining a component is writing the composition once. Two copies of a treatment is where the drift starts, and this repo has logged that shape three times already
2026-08-23The Figma file gains a component library (user direction). figma-sync.md says the SYNC writes variables and never components — and gives the reason: components should BIND to the variables so one config change re-themes every page. A hand-built library that binds is what that architecture is FOR; it simply must never become part of the idempotent sync. Built: Button (24 variants), Badge (6), Input (4), Checkbox (4), plus 13 text styles bound to Type/* and a 14-page structureThe rule was never "no components in Figma". It was "the sync does not write them", which is a different sentence and the difference matters
2026-08-23Two real drifts, found by building against the variables. (1) Radius/Active aliased to rounded-lg (8px) while the saved config is 4px — every Figma component would have shown the wrong corner. Repointed to rounded-sm; all 24 Button variants followed with no component edited, which is the alias chain proving itself. (2) Color/Input was scoped STROKE_COLOR only, but input.tsx uses bg-input/50 as a FILL — Figma bound it and silently refused to resolve it, rendering blackNeither was visible from the code side or the Figma side alone. They only surfaced when something tried to build with the variables, which is the argument for building the library at all
2026-08-23Foundation gained the nine roles the code already had — Card Foreground, Popover, Popover Foreground, Secondary, Secondary Foreground, Accent, Destructive, Positive, Sidebar Foreground, each an ALIAS into Palette. Code syntax set on all 40 Foundation variables (var(--primary)) and all 120 primitives (the utility class: p-4, gap-2). Palette deliberately has NONEFigma had 13 colour roles against the code's ~20, so Button could not have had a secondary or destructive variant using only variables. Palette is left without code syntax on purpose: DESIGN.md rule 1 forbids raw --color-* steps in component code, so emitting one in Dev Mode would instruct a developer to break the constitution
2026-08-23Ten component sets in Figma, 55 variants, plus Cover and Foundations pages. Button 24 · Badge 6 · Tooltip 4 · Input 4 · Checkbox 4 · Tab 3 (+ an assembled TabsList) · Table Row 3 (+ an assembled Table) · Skeleton 3 · Card 2 · Separator 2. Every fill, radius, gap and type size binds to a variable; the Foundations page generates its 22 swatches FROM the collection, so it cannot go staleThree Figma behaviours cost the most time and are worth writing down: paint opacity is discarded if passed in the same literal as the variable binding (bind first, then re-apply); textDecoration set on one variant propagates to every variant sharing a layer name; and a rotated rectangle keeps its unrotated bounding box, so a tooltip tail must be a vector, not a rotated square
2026-08-23A third drift: Color/Background and Color/Muted both alias neutral/100 in Light. Anything that lifts out of a muted ground — the active Tab, a hovered Table row — is invisible. In the running app the two are clearly distinct (14.5% vs 26.9%). Deliberately NOT hand-patched: the Foundation role map is generated by the sync from the saved config, so an edit in Figma would be silently overwritten on the next run. Fix belongs in the config or the syncThe temptation is to nudge the variable and move on. That would have hidden the bug and produced a file that disagrees with its own generator — the exact failure this project logs against
2026-08-23A full role-by-role drift check against the running app. All 20 semantic roles were read out of the live app in BOTH modes and matched to the Tailwind step with the same oklch value. 17 of 20 matched exactly. Color/Background was neutral/100 in Light where the app is #fff, and neutral/800 in Dark where the app is oklch(14.5%) = neutral/950; Color/Destructive dark was one step off. All three repointedThis is what "is Figma→code working" actually means — not whether Dev Mode emits a variable name, but whether the value behind it is the one the product renders. The Background error is why the active Tab, hovered Table rows and the Follow-up hover state were all invisible: Background and Muted resolved to the SAME step, so nothing could lift out of a muted ground
2026-08-23Text that describes a variant state must not be a component property. A Figma property is shared across every variant, so a single default overwrites all of them — Tool Call's Failed row read "412 lines" and Reasoning Panel's Thinking read "Thought for 15s". Only genuinely parameterised text (a label, a target) is a property; state copy stays per-variantThe property looked like the more "designed" choice and was actively wrong. A variant grid exists to show states differing; a property guarantees they cannot
2026-08-23The ambient tokens are missing from Figma entirely--wash, --glass-fill, --glass-border, --scrim, --ambient-blur are in packages/ambient/src/ambient.css and in no variable collection. Ambient components therefore cannot bind their signature material; Context Chip binds Color/Accent as the nearest legal step (DESIGN.md rule 3) and says so in its descriptionThe sync was built for the product vocabulary and never extended to the ambient one. An Ambient collection sourced from ambient.css is the fix, and it has to go through tokens.json first — code is master
2026-08-23Twenty-one component sets, 86 variants — the ambient vocabulary starts binding. Added Message Action 3 · Reference Chip 3 · Error State 3 · Streaming Text 2 · Shimmer Placeholder 3, on top of the sixteen product sets. Three of them encode a distinction the code makes and a picture usually loses: a Reference Chip is numbered (prose has to be able to point at its evidence), Streaming Text ends mid-sentence (a truncated clause is the signal, not trailing dots), and Error State says what was and was not changed before it offers a retryThese are the components where a Figma library normally goes decorative — it draws the shimmer and skips the reason. The description field is doing the load-bearing work: each set carries WHY the shape is that shape, so someone reading the library gets the constraint, not just the picture
2026-08-23One paint in the library is not bound to a variable, and it is written down. Figma's setBoundVariableForPaint accepts SOLID paints only — a GRADIENT_LINEAR stop has no binding. Shimmer Placeholder's travelling sweep is therefore literal neutral/200 → neutral/50, the two roles it interpolates between. Logged as F8, open. Figma also cannot express the sweep's MOTION, so the frame is mid-sweep by constructionThe instruction was "only and strictly the variables we created". This is the single place that could not be honoured, so it is stated rather than quietly shipped. The alternative — a flat Color/Muted bar — binds cleanly and destroys the distinction from Skeleton, which is the one thing the component exists to carry
2026-08-23The product vocabulary is complete in Figma at twenty-five sets, ninety-eight variants — Save Reminder 3 · Section Rail Item 3 (+ an assembled six-dash rail) · Sheet 4 · View Menu 2. Sheet is drawn WITH its scrim, because the scrim is the component: a sheet without one is a sidebar, and whether the page behind stays usable is the entire reason to pick one over the other. Its overlay is the one paint deliberately left literal — the code says bg-black/30, and a scrim that re-tinted with the accent would be a different design rather than a themed oneEvery one of these carries a decision that a screenshot loses. Save Reminder has no Closed variant because it has no resting state. Section Rail keeps inactive labels laid out at zero opacity so hovering the rail cannot shift the page it indexes. View Menu's trigger is one icon box in both states so it cannot move under the pointer as it opens
2026-08-23Collapsible gets no Figma component, on purpose. collapsible.tsx is a pure Radix passthrough — three wrappers, zero className. A Figma component for it would have to invent a look the code does not haveThis is the drift the library exists to prevent, arriving from the direction nobody guards: not a component that disagrees with its code, but a component whose code has nothing to disagree with. The honest Figma artifact for a behaviour-only primitive is its absence, and a line in the log saying why
2026-08-23Code Diff — twenty-six sets, one hundred variants. Settled and Working. The Working state shows ELAPSED SECONDS rather than a change count, because while lines are still arriving a count would have to be revised upward as you read it; the path shimmers instead of a spinner, so the thing being worked on is the thing that looks busy. The sign column stays even though the tint already says it — colour alone is not a signal everyone receives, and a diff is exactly where being wrong about which way a line went is expensiveA second wash pair is missing from Figma (--positive-wash, --destructive-wash, finding F9), so the rows bind Color/Positive and Color/Destructive at 12%. Same shape as F7 and the same fix: through tokens.json first, because code is master. Two independent findings now point at the same hole — the sync was built for the product vocabulary and the ambient one has no representation in Figma at all
2026-08-23The paint-opacity recipe was wrong in a second way, and cost the same bug twice. It was already logged that passing opacity inside the literal given to setBoundVariableForPaint discards it. Spreading that function's RETURN VALUE into a new object with an opacity discards it too — Code Diff's rows rendered as full-strength red and green bars with the code invisible on top. The only form that holds: assign the bound paint to the node, then read node.fills[0] back off it and re-assign with opacity. The binding survivesThe first version of the rule described the symptom I had hit rather than the mechanism, so it did not generalise to the next shape of the same mistake. A rule written from one failure predicts one failure
2026-08-23Thirty-one sets, 115 variants — the ambient half is under way. Terminal Block 6 (Paper/Ink × Running/Passed/Failed) · Tool Failure 2 · Inline Citation 2 · Reasoning Effort 2 · Day Divider 3. Two carry decisions worth keeping: Tool Failure quotes the error verbatim because the user is going to paste it somewhere and a friendlier rewording destroys the one searchable string, and its Exhausted case drops Retry entirely rather than dimming it — a button that has run out of attempts should stop being offered. Inline Citation is not superscript: align-super stretches the line box and pads every paragraph containing a citation, so the marker is baseline-aligned and nudged up 0.35em, and its preview opens UPWARD because a citation is read mid-sentence and a card below would cover the text still being readTerminal Block is the one place raw --color-zinc-* steps are correct, and the code already says so: a terminal in ink tone is a fixed dark surface, not a themed one. Binding it to Color/Card would have made it re-tint with the accent, which is exactly wrong for something meant to read as the machine itself
2026-08-23The paint-opacity rule was wrong a THIRD time, and the mechanism is an execution boundary. Three forms fail: opacity inside the literal given to setBoundVariableForPaint; spreading that function's return value; and — new — assigning the bound paint then re-reading node.fills[0] and re-assigning with opacity in the same plugin run. Only a LATER run works. Proven rather than guessed: Tool Failure's error ground rendered full-strength red, and reading the opacity back at the start of the next execution returned 1; the identical assignment then held at 0.12 with the binding intactTwice I rewrote this rule from the failure in front of me and twice it predicted only that failure. The practical consequence is a build cost, not a footnote: every tinted bound fill takes two calls, so a component with washed rows cannot be finished in one pass. Worth knowing before the remaining seventeen ambient sets, several of which are washes
2026-08-23Thirty-six sets, 128 variants. Message Pair 2 · Message Branches 3 · Tool Timeline 3 · Web Search 2 · Parallel Tools 3. Four of these encode the same principle from different angles — a count must describe what has happened, not what is planned: Tool Timeline reads "2 steps" and climbs rather than announcing the total; Parallel Tools' header count rises as calls land, because calls that go out together still come back one at a time; Web Search says "Reading sources… 4s" then "Read 3 sources". A count that starts at the end is a promise the component cannot keepThe inverse case is Parallel Tools' failure line. Collapsed by default is right for a batch — four rows for one event — but a collapsed batch must not be able to hide a failure inside itself, so one failed call rewrites the header to "3 done · 1 failed" in destructive. The summary earns the collapse
2026-08-23Per-child counter-axis alignment is not settable in Figma; a full-width wrapper row is the supported equivalent. layoutAlign read back as INHERIT immediately, before and after componentisation — only the parent's counterAxisAlignItems applies, and it applies to every child at once. That is no use where one child is right-aligned (ms-auto on the question) and its sibling is not (the answer). The fix is a full-width row whose OWN primaryAxisAlignItems does the work, one per aligned childFound on Message Pair and Message Branches, and worth writing down because the failure is silent: the property accepts the assignment and simply does not hold it. Nothing errors, the layout just stays left, and it reads as a layout mistake rather than an API one
2026-08-23Forty-four sets, 149 variants — every non-glass component is built. Reviewable Diff 2 · Code Runner 2 · Feedback Dialog 2 · Research Report 2 · Message Attachments 3 · Quote Reply 4 · Orb Character 4 · Sidebar 2. The three-state hunk in Reviewable Diff is the one to keep: kept, discarded, and UNDECIDED — a two-state control silently defaults every hunk the reviewer never looked at, which is the exact failure the component exists to prevent, hence "1 left to review" rather than a button that is merely enabled. Research Report declares its outline first and fills it in, because the only moment redirecting a piece of research is cheap is before it has been writtenOrb Character ships as an admitted approximation. The real orb is a WebGL shader; Figma can say WHICH state is which and cannot say how it moves. Rather than pretend, the four STATE_PARAMS rows travel beside it as a caption strip — speed · contour · innerGlow · outerGlow · angle — so the spec is in the file even though the motion cannot be
2026-08-23A fourth token gap, and this one is on the PRODUCT side. --sidebar-accent, --sidebar-accent-foreground, --sidebar-border and --sidebar-ring are absent from every Figma collection, so Sidebar binds Color/Accent and Color/Border as nearest legal steps (finding F11). Color/Sidebar and Color/Sidebar Foreground DO exist, which is what makes the gap easy to miss — the role map looks present until you need the statesF7, F9 and now F11 were each found by a component reaching for a token that was not there. The conclusion has changed shape: this is not "the sync never covered the ambient vocabulary", it is that the Figma role map was generated from the roles someone listed rather than from the roles the CSS actually defines. The fix is the same either way and it starts at tokens.json
2026-08-23Also found by building: a selected reason chip in FeedbackDialog is nearly invisible (finding F10). Selected renders variant="secondary", unselected renders variant="ghost" with bg-muted, and in the saved neutral theme those two are close enough that you cannot tell which reasons you picked. This is a CODE issue, not a Figma oneIt only became obvious because the library draws Empty and Filled side by side, which the running app never does. That is an argument for the variant grid as a review instrument rather than as documentation — states shown together are audited; states shown one at a time are merely used
2026-08-23A full audit of the Figma file found the worst possible result: instances: 0 across all 44 sets (user-reported, then measured). Every button, input, icon and chip was hand-built. The library looked like a design system and behaved like forty-four unrelated drawings — restyling Button changed nothing, and the same close glyph existed five times at five different weights. The root cause was a missing foundation, not carelessness: the Figma Button had four sizes against the code's eight and NO icon slot, so an icon button was literally impossible to instance and every component drew its ownThis is the exact drift the project exists to argue against, occurring in the artefact built to prevent it. Worth stating plainly rather than filing as a bug: I built forty-four components without once asking whether the primitives could actually be composed, and the answer was no from the first one
2026-08-23The fix was to build the missing layer first, then rebuild against it. New Icon set — 30 glyphs on one 16px grid, 1.5px, round cap and join, stroke bound to a role so an instance recolours by override. Button rebuilt as 6 × 8 = 48 variants with the four icon-only sizes the cva has (icon, icon-xs, icon-sm, icon-lg) and real slots: Label, Icon start + Icon, Icon end + Icon (end) — the pair the cva's data-icon=inline-start/inline-end exists for. Then fourteen consumer sets rebuilt from instances and thirty hand-drawn glyphs swapped. 281 instances where there were noneThe order matters and I had it backwards the first time. A component library is not a set of drawings that happen to agree; it is a dependency graph, and the leaves have to be able to carry every case before anything composes them. Every hand-drawn button in this file was a symptom of Button not having an icon slot
2026-08-23textCase silently detaches an applied text style in Figma (F13). Section Rail's three labels carried no style at all — applying UI/xs Medium and then setting UPPER drops the link without error, so no type change could ever reach them. Fixed by adding a real UI/xs Medium Caps style rather than an override. Also swept: every stray literal radius bound to its nearest existing step (20 nodes across 8 sets), and Context Chip's dismiss control and file mark replaced with Button and Icon instancesThe audit's value was in what it measured rather than what it looked at. Four categories of raw value survive on purpose and are now recorded as such — Tooltip's tails and three spinner arcs are GEOMETRY not glyphs, Sheet's scrim is literally bg-black/30, and Shimmer's gradients cannot bind at all (F8). Everything else is zero: no unbound radii, no unbound strokes, no missing text styles, no undescribed sets
2026-08-23The ambient-layer view and the Gallery page are removed (user direction). Home drops from three views to two — Dev tool and Canvas — with Dev tool as the default and as Home, because it is the one remaining view that shows the layer doing something; Canvas is the quieter claim and reads as an empty page until you already know what is meant to happen on it. /gallery is gone from nav.ts, so it falls through to home and the palette's Jump-to no longer offers it. Both files deleted, and the two comments that named the gallery as a consumer of CommandLine were rewritten rather than left pointing at nothingThe front door changed and that is worth stating. The layer view existed because a stranger used to arrive at an empty gradient reading "Press ⌘K" — an invitation to use a thing nobody had explained. Defaulting to the dev tool is the closest remaining answer: it demonstrates rather than asserts. The philosophy itself is not lost — PAPER.md is still rendered from its real bytes at /ds — but it is now behind a route rather than on the landing surface
2026-08-24The six ambient form factors are built in Figma, on their own page (user direction). One component set — Ambient Form Factor — with variants Orb · Quick ask · Panel · Dock · Spotlight · History: the layer's whole surface model (DESIGN.md §8) as one object, because the thesis is that these are one assistant changing geometry, not six features. Each floats over a faint product ground with a Figma background-blur, so the glass reads as glass rather than as an opaque card. Every control is a Button/Icon/Input/OrbCharacter instance, and the orb mark appears only where you speak to the assistant — the resting orb, the quick-ask end, the composer send, the spotlight search, the open row in HistoryThis is what the five "glass" components were waiting on, and the wait was based on a wrong premise
2026-08-24F7 is resolved, and it never needed new Figma variables. The glass tokens are DERIVED, not primitive: tokens.json/translucency defines them as a role at an opacity — --glass-fill = Popover 65%, --glass-border = Border 70%, --wash = Accent 40%, --scrim = Background 50%, --glass-scrim = Popover 82% for full-screen History. The faithful Figma form of "Popover at 65%" is a paint BOUND to Color/Popover with opacity 0.65 — which retints when the role changes, exactly as the CSS color-mix does. A baked Color/Glass Fill variable would have frozen the mix and broken that. So the form factors bind their signature material honestly, and the earlier F7/F9 panic about "ambient tokens absent from Figma" was a category error: opacity-bearing roles are paint recipes, not new tokensThe rule that generalises: before adding a variable for a value, ask whether the value is PRIMITIVE or DERIVED. A derived value belongs in the paint that consumes it, bound to the primitive it derives from. Adding a variable for it creates the second source of truth the whole project argues against — this time it would have been in the one collection meant to prevent exactly that
2026-08-24A swapped OrbCharacter instance accumulates rescales. The inline orbs (quick-ask end, spotlight search, History open-row) went faint after several rescale() passes on the same instance — the accent ring ended up clipped or off-centre, leaving only the pale inner glow. Rebuilding each from the Still/Thinking main with a SINGLE rescale(target / nativeWidth) fixed it. Companion rule: a shell should clip only enough to round its corners; size the orb near the shell width so its accent glow stays inside the frame instead of being cut away — the clip was eating the very ring that makes the orb readThe failure mode was invisible in the tree (the instance was present, sized, centred by its own box) and only showed in the render. It is the third time on this file that Figma's answer to a geometry question — rotated-rect bounds, absolute-child coordinates, and now compounded instance rescales — differed from the obvious mental model, and each cost a screenshot to catch
2026-08-24The new saved theme is synced to Figma — accent blue, scaling 100% (user direction; read from the running app's saved config, which is the sync's master). The change was six accent aliases repointed indigo → blue (Color/Primary Lblue/600·Dblue/500, Color/Ring same, Color/Ambient Accent Lblue/600·Dblue/400) plus Scale/Root font size 14→16 and Scale/Factor 0.875→1.0. Zero components were touched — the whole file re-themed because every component binds the Foundation aliases, so repointing three colours in two modes carried blue through every page and both the Button set and the orbs (Ambient Accent) now render blue. Primary Foreground stays white, gray stays neutral, radius stays rounded-smThis is the code→Figma architecture doing exactly what it was built for: the sync writes VARIABLES, never components, so a hue change is six alias edits rather than a file-wide repaint. Only three roles are accent-sourced (Primary, Ring, Ambient Accent), which is why an accent swap is so small. The first sync ran indigo/95%; the saved theme had since moved to the documented default, and nothing in Figma had been hand-edited, so there was no conflict to promote back — a clean forward sync
2026-08-27PAPER.md rewritten as a two-part paper (user-commissioned, from a team discussion held as gitignored provenance in notes/). Part I is Ambient UI: the layer-above-the-interface claim, the six shapes, declared context, answers composed from typed blocks, an identity with no look of its own. Part II is Design Architecture: the drift/slop diagnosis, the bounded configuration space, a file-by-file anatomy table of this repo, the escape-hatch-by-escape-hatch account of building without drift, and the token pipeline that makes code and Figma two projections of one value store. Every finding from v1 survives — the five moves, motion/translucency, the config-rot audit, the three seam failures — reorganised under the new structure. The voice is plainer: shorter sentences, simple verbs, no em dashes in body proseThe old paper argued the structure and never named the product; readers met an assistant on the site and a thesis in the paper and had to connect them alone. The two-part shape makes the dependency explicit: the assistant is the demand, the architecture is the supply. The anatomy table is new for the same reason the docs render real bytes — the paper should point at the actual files, so the argument stays checkable against the repo it lives in
2026-08-27The governance files audited, and the paper now explains how they load (user challenge: "are these even in use?"). The audit answered with evidence rather than assertion: the skill mechanism was fired live (ds-manager's full content loaded into context on invoke), CLAUDE.md provably injects at session start, and launch.json powers the agent's dev-server tooling. Then the contents were checked against the tree, and three rot findings came out: ds-manager's motion rule still said "theme.css keyframes + Tailwind transitions only", written before the motion-role system existed; its blast-radius instruction searched only apps/site/src after the vocabularies moved into packages; and product-copy's REFERENCE.md pointed at apps/site/src/foundation/foundation-page.tsx, a path that no longer exists. All three fixed. PAPER.md §10 gained "How the governance files actually load": the two load modes, why plain files make rules reviewable and installable, and the audit finding stated in the paper itself. Also: .agents/ and the humanizer symlink (a contributor's personal writing aid) are now gitignored — the project's governance is exactly four documents, and personal tooling stays out of the public repoThe config-rot lesson generalised one level up: the governance layer rots exactly like the configuration layer it governs, and for the same reason — every individual staleness is invisible because the file still loads, the skill still fires, and nothing errors. The same question applies: does what this file SAYS still match what the system DOES? Asking it of the three skills paid immediately, so the paper now records the practice where it records the mechanism
2026-08-27The staleness audit applied repo-wide (user direction, extending the governance-file finding). A mechanical check tested every path-like claim in every governing document against the tree, then a hand pass checked the claims that are not paths. Found and fixed: the moved Foundation engine path still cited in CLAUDE.md (×2), DESIGN.md's own intro, and PAPER.md's day-old anatomy table (two truncated paths — the audit caught its own author); figma-sync.md describing six collections where the file holds four, with a drift-check probe querying three collections that do not exist (the documented verification procedure would have returned MISSING COLLECTION); tokens.json translucency diverged from ambient.css (glass-fill 65↔55, glass-veil 35↔62, five tokens missing entirely) — reconciled to the CSS, whose values were the deliberately tuned ones; sync-payload.json carrying indigo-era example values and sync-request.json/tokens-figma-map.json still claiming no sync had ever run, two syncs later — all three now record the applied blue/100% reality with the verified collection shapes; and docs/porting-the-assistant.md, a 331-line manual whose own banner promised the registry would replace it, replaced by a 64-line accurate install guide now that the registry exists and is provenThe mechanism finding from the skills audit held at repo scale: every one of these files still loaded, rendered, or parsed without error, which is exactly why none of it had been noticed. The mechanical path check is cheap enough to be a habit; the value drift (tokens↔CSS) is the one class it cannot catch, and that one needs the ds-manager duty — update the value store and its implementation together — actually followed
2026-08-27docs/porting-the-assistant.md removed entirely (user direction: it existed to start the thing, and that is done). The file was born as a manual workaround for a missing install mechanism; the registry is that mechanism now, and the install story lives where a consumer actually meets it — the README's one command and each component's page at /ds. Its /ds entry and ?raw import in system-docs.ts went with it, so docs/ is gone from the treeA document whose reason to exist has been shipped does not get archived in place, because an archived guide still ranks in a stranger's search and still reads as instructions. The history stays in git and in this log; the tree only carries what is currently true
2026-08-27The doc-path check joins the gate (scripts/check-doc-paths.mjs, npm run docs:check). Every backtick path claim in the governing set — CLAUDE.md, README, PAPER.md, DESIGN.md (excluding this log, which is history and legitimately names dead files), figma-sync.md, and the project skills — is tested against the tree on every gate run. Symlinked skills (personal tooling) are skipped; globs and package-relative fragments are skipped as uncheckable. Verified in both directions before wiring: passes the clean tree, and a planted beam.tsx citation fails itThe audit that motivated this caught the same failure four times in one day, including in a file written the day before. A dead path in prose breaks nothing a compiler sees, so it was invisible until someone went looking. Now it is a red build, which is the difference between a rule and a habit
2026-08-27The quick-ask ghost wrapped at the 16px base (user report, minutes after seeing it). ShimmerPlaceholder — the shimmer that stands in for an input's placeholder and the page's Tab-suggestion — was absolutely positioned with left-0 only: no right bound, no truncation. At 95%/14px the devtool's suggestion ("Fix all the problems in this workspace") fit the pill's fixed 420px by luck; at 100%/16px it wrapped to two lines inside a 52px pill and clipped. Fixed to inset-0 with an inner truncate span: the ghost stands in for a single-line <input>, so it must be bounded by the field's box and ellipsize exactly as the input would. Verified in the running app against the reported string: single line, contained, right-bounded, truncatingThe scaling sync worked exactly as designed and that is what exposed the bug: propagation carried the new base into a component whose overlay had never stated its bounds. "Fit at one scale" is not "fits" — it is an untested assumption that the current scale happens to satisfy. The paper's line about verifying propagation at the pixel now has one more receipt
2026-08-27The 16px ghost fix is encoded in Figma — and it exposed a mislabeled component (user direction: match the behavior in Figma). Checking where the ghost lives revealed that the Figma set named "Shimmer Placeholder" was the WRONG component: it drew prose/line/block loading bars and cited <ShimmerPlaceholder shape="prose">, an API that does not exist — the registry defines ShimmerPlaceholder as the input ghost, and the bars correspond to StageSkeleton. Corrected both ways: the bars renamed Stage Skeleton and rebuilt to the true staging.tsx anatomy (dot + 38% title + 72/88% line, Color/Muted, pulse not sweep — which also removed the library's one unbindable gradient, resolving F8), and the real Shimmer Placeholder built in its place: States Resting · Offering · Typing, the ghost bounded by its field and ellipsizing (maxLines 1), Offering demonstrating the exact reported suggestion string with its Tab keycap, Resting carrying one still frame of the accent sweep. The Form Factors' prompt/field/query texts now truncate in context tooThe error's cause is worth the row: I built that set from the CONCEPT ("a shimmer placeholder must be loading bars") instead of from the registry entry, in a project whose rule is that undocumented components do not exist. The registry knew the truth the whole time. Building against the docs is not bureaucracy — it is where wrong ideas go to get caught before they ship
2026-08-27The ambient shell gets a full porting spec (docs/ambient-shell-spec.md, user-commissioned: "document the whole behavior properly, I want to build this somewhere else"). Nine sections derived from source, not memory: the state model and context API, all six shapes with their exact geometry and controls (including the fact that dock and panel are one surface in two containers, which is why detach-by-drag works), a complete transition map with the measured hot-zone bounds (right edge x > w−140, top-center y < 180 ± 320px), the quick-ask handoff timing, the answer stage contract, the icon inventory with the three grandfathered direct imports named, the glass token table, the identity's bodies and cost rules, and a 15-line acceptance checklist a porting agent can verify against. Rendered at /ds under Working rules, and added to the doc-path gate (10 documents now checked)This is not the deleted install guide reborn — that told you how to GET the layer; this tells an agent in another repo how to REBUILD its behavior faithfully. The spec quotes the scars on purpose (the stale-closure drag drop, the mask-composite ring, the ghost's wrap) because a port that only copies the happy path re-earns every one of them
2026-08-27The global motion spec joins the porting set (docs/motion-spec.md, user direction: motion first, before anything else ports). Seven sections from source: the four roles; the three characters with their exact beziers, duration tables and springs; the pace math with the detail that matters (stiffness/t², damping/t — brisker, not bouncier, damping ratio preserved); the two consumption seams including the framework-default mapping that re-times role-less components and the static fallbacks that make the zero-provider mount look right; the springs-vs-tweens rule with the shell's standing enterT convention; the identity's own clock as the one sanctioned exception, with the per-state loop table (listening reverses, thinking runs ~4× hot with a counter-comet, answer blooms once); and the full arrival choreography — StagedItem's height+opacity landing and every component's delay/interval cadence as one table. Ends in an acceptance checklist with greppable checks. Rendered at /ds, added to the doc-path gate (11 documents), cross-referenced from the shell spec as its required first readThe cadence table is the part a port would otherwise lose: those delays and intervals encode how each KIND of work actually arrives — a patch lands as lines at 140ms, a report writes sections at 1400ms — and none of them are written anywhere else but the call sites
2026-08-27PAPER.md revised for depth: the compressed heart restored (user direction: back to the paper, properly). Section 9 was a wall of bolded paragraphs; it is now six subsections, and the material the earlier compression LOST is back: the deleted shadow knob (a dimension that does not visibly propagate is indistinguishable from a broken one, so the correct fix was deleting the knob), the command-palette reference case (three system-level changes instead of one bespoke palette), the motion audit that found the assistant had no transitions at all, the spring-versus-drift mechanism, and the frame-budget lesson (a sluggish surface was compiling shaders during its entrance frames; motion quality is bounded by what else is happening in the frame). Also restored: the glyph-scale cost line in Part I §5, the reviewable diff named as the flagship block in §4, and the F7 near-miss written into the derived-values rule in §12 as the story that makes the rule persuasive. 557 → 654 lines; six stray em dashes normalized to the established plain voice; log count refreshedThe compression had optimized for shape and paid in evidence. The paper's authority comes from the failures it can cite, and three of the best had fallen out of it; a launch paper that states rules without their scars reads as opinion. Restored surgically rather than rewritten, because the two-part structure and voice were already right
2026-08-27The Playbook becomes the home page (user direction: a guide-styled front door, built from the system itself, with downloadable governing docs and scroll-staged sections; reference structure supplied). The reference was translated, not cloned, per the primary rule: its left TOC became SectionRail (the system's own in-page nav), its serif display became the configured type at the top of the Tailwind scale, its ground became .ambient-grid, and its per-entry pages became the real documents at /ds via ?c=doc-paper. Four parts mirror the paper (the layer · the architecture · one source of truth · the porting kit); every control is vocabulary (Button, CommandLine, Icon, SectionRail); reveals ride the motion roles; downloads are Blobs of the same ?raw bytes /ds renders, so there is no second copy to rot. The page is interactive through the REAL layer: try-buttons call setMode, and setPageIntel makes the quick-ask suggestion about the playbook. Playbook now leads the view order and is Home; Dev tool and Canvas remain. Two compositions kept local and watchlisted (§13): Reveal, DocDownload. One icon compromise: no arrow-down/download name exists in the icon vocabulary, so the download button speaks through its label and the scroll CTA wears chevron-down — adding a download name across all five libraries is left as a governance candidateVerified in the running app: entry click lands on /ds with the paper selected, back returns to the playbook, the try-button opens the real panel, the rail tracks scroll, and the kit downloads real files. The strongest sentence on the page is the one the page proves: it is made of the system it explains
2026-08-27The Playbook becomes the article itself (user direction: "bring the whole paper here… we need to write it like an article"). The entry index linking away to /ds is gone; the page now renders PAPER.md's full body inline, chunked at part and section headings, through the same ?raw-bytes Markdown reader /ds uses — one presentation transform only (the title block is sliced off because the hero renders it at display scale). Each chunk arrives on the surface spring as the reader reaches it. There is still no second copy: edit the paper and the home page changesAn index that links away asks the visitor to leave before they have read anything. The article form makes the front door the paper, and because the bytes are shared with /ds and the downloads, the three presentations of the paper cannot drift from each other
2026-08-27The article gets a reader mode, shell-framed demos, and a wireframe device (user direction: readable structure like the reference's reader mode, components shown in a shell as demos, wireframe background). Three additions, all vocabulary: (1) a Read · Browse Tabs toggle — Browse is a numbered index DERIVED from the same chunk split (headings + first lines parsed, never hand-written), each entry jumping back into Read at its anchor; a read-time line (~words/220) joins the hero. (2) Two in-article demos inside a new WireframeShell frame (dashed border, corner ticks, mono tag — drawn entirely from the border role and type scale): after §2, the live layer's shapes opened for real via setMode; after §4, the answer vocabulary itself — ReasoningPanel, ToolCall, CodeDiff, ReferenceChips — rendered settled (staged={false}) with honest example content, so the article shows the components it just argued for. (3) Demos attach by matching section headings (## 2., ## 4.), so a renumbered paper detaches them loudly rather than misplacing them. WireframeShell kept local and watchlisted (§13)Verified in the running app, both appearances: Browse derives 2 groups / 16 entries, a Browse click lands Read on the right anchor, the shapes demo opens the real spotlight, the answer demo renders all four blocks settled. A demo that is a screenshot can lie; these cannot, because they are the components
2026-08-27The article gets a contents column (user direction: the reference's left nav, dividing the paper logically). A fixed left column on wide screens: home line, the Read · Browse toggle (which moves up from the article on xl; the inline toggle remains for narrower screens), and the sixteen sections divided into six labeled groups — the layer · the answer · the problem · the architecture · the data layer · take it — each a vocabulary Collapsible. Groups name section NUMBERS and resolve against the live chunk split, so a renumbered paper drops a section from the nav loudly instead of pointing at the wrong prose. The accordion follows the reading: the group containing the section under the reading line opens itself (open state is derived from scroll position, with reader toggles as overrides — no state syncing, which is also what the lint rule demanded), and the active entry is bolded and aria-current. SectionRail left the page: two navigation devices for one article is one too many, and the column subsumes the rail's four coarse stops with sixteen precise onesVerified with real clicks in the running app: plain scrolling opens exactly the group being read, a folded group opens on click without disturbing the active one, and an entry click lands the article on its section. One lesson kept: synthetic pointer events in testing produced states no real user can reach — verify interactions with real clicks
2026-08-27Read · Browse removed; the contents go everywhere (user direction: "we don't need this at all", then "even in smaller screen… handle it"). The mode toggle and the Browse index are gone — the contents column already IS the browse surface, and a page with two ways to see the same index was carrying a control with no job. The article now always renders. The contents themselves were split into a reusable PlaybookContents body: from lg up it lives in the fixed left column (content offset by the column's width, then centered in the remainder), and below lg it renders IN FLOW between the hero and the article, exactly the reference's stacked small-screen layout — same groups, same accordion-follows-scroll, same anchorsVerified at 1440 (fixed column, no toggle), 820 and 375 (contents stacked above the article). Removing a control is a design act too: the toggle earned its place only while the index had nowhere else to live
2026-08-27§1 gets a wireframe diagram: embedded vs ambient (user direction: "make a wireframe screen diagram to explain this"). A third in-article demo inside the WireframeShell device — two miniature product screens drawn ENTIRELY from tokens (border role for strokes, muted for content bars, primary for every AI mark; no literal color anywhere). Left, the common way: the same AI bolted on three times — a chat tab in the sidebar, a sparkle dot on each feature card, a dashed assistant panel — each marked in the primary role and each blind to the others. Right, the ambient claim: the identical product wireframe carrying no AI at all, with one presence (orb dot · input bar · ⌘K) floating above the screen's top edge. Attached to ## 1. by the same heading-match mechanism as the other demosA diagram whose palette is the semantic roles re-themes with the Foundation like everything else on the page — change the accent and the argument's illustration follows. The alternative (an SVG or image export) would have been the page's first non-propagating pixel
2026-08-27Three more wireframe diagrams join the article (user direction: add them to the sections that need them). The sections chosen are the ones making spatial or structural arguments: §3 gets the context contract — a screen with its selected element outlined in the primary role, and the presence above already wearing that selection as a chip before a word is typed. §9 gets the bounded configuration space, and its left panel is not an illustration: it reads the LIVE saved Foundation config via useFoundation() (accent · gray · radius · spacing · scaling · motion), so the diagram shows the exact values the page is currently wearing and can never drift from the theme it explains — save a different config and the diagram restyles with everything else, its labels included. §12 gets the token pipeline: tokens/tokens.json as the one master, arrowed into its two projections — the running product (compiled to CSS variables on Save) and the Figma variables (written by the sync, variables only). All three use the same grammar as the §1 diagram: WireframeShell frame, border-role strokes, muted bars, primary marks, mono tags — zero literal colors, zero imagesThe §9 move is the one worth keeping: where a diagram CAN read the live system instead of depicting it, it should — a depiction is a second copy of the truth, and this article's whole argument is against second copies
2026-08-27The page divides into its two parts (user direction: Design Architecture and the philosophy of Ambient UI as the top-level structure). Two moves. In the article: the paper's # Part chunks stop rendering as raw markdown H1s and become designed chapter breaks — top border, the part number in the primary role as a mono overline, the part name at display scale, and a one-line frame under each (Part I "the philosophy: a presence above the product"; Part II "the structure: a bounded configuration space"). Still the same chunks, same anchors — only the presentation of a level-1 boundary changed. In the contents column: the six groups now nest under two part headers (Part I · Ambient UI / Part II · Design Architecture), each header jumping to its chapter break, the active part highlighted, groups indented behind a border-s ruleThe paper always had this structure; the page was flattening it. A two-part essay whose halves are the demand and the supply should read as two chapters, not sixteen equal sections
2026-08-27The framing turns outcome-first (user direction: the brief should say what you achieve, business POV, and the framework should read as a step-by-step guide — "the layer / the answer" was not understandable). Three coordinated edits. PAPER.md's opening rewritten: it now leads with the outcome (AI building interfaces at full speed without design quality falling apart), names the problem second (drift), then states what adopting the framework makes true — the assistant above the product, a bounded space a generator cannot drift out of, code and Figma reading one token master — and closes the case in business terms (experiments become configuration changes, restyles one save, review stops being archaeology). The nav groups renamed from structural labels to outcome labels: A presence above the product · Answers made of your UI · Name the enemy: drift · Bound the design space · One source of truth · Make it yours. Part II's chapter break now lists those four steps as a numbered, clickable list under its intro, so the framework announces its shape before asking to be readThe section headings in the paper are untouched — they were already concrete. What changed is the wrapper: a stranger deciding whether to read now meets "here is what you get and the four moves that get it" instead of a taxonomy. The nav labels live in one constant and the step list derives from it, so the guide and the nav cannot disagree
2026-08-27Part I opens with the philosophy (user direction: say what the philosophy of Ambient UI is first, then go part by part). PAPER.md gains two paragraphs under the Part I heading: the philosophy stated as one inversion (the AI is not a feature of your product, it is a presence above it, with the feature/presence contrast spelled out), then a roadmap sentence walking the six sections as consequences of that inversion. The chapter-break renderer needed one fix to carry it: it had only ever rendered a part chunk's HEADING, so prose inside a part chunk was silently dropped — it now renders the remainder through the same Markdown reader below the breakThe renderer bug is the part worth logging: the presentation layer had quietly narrowed what the paper was allowed to say at a part boundary. The paper is master; the page renders what is there, all of it
2026-08-27Setting up the Foundation becomes an explicit, early move (user direction: explain why setting up the Foundation matters because half the game is just that — and let the reader do it directly). In the paper: §9 gains a paragraph naming the Foundation as the menu's home and the first real act of adopting the framework — half the game is played there before a single screen is designed, because every later decision either resolves against it or invents a value the system cannot see; skipping it means building the drift you will spend the next year removing. On the page: the live config diagram in §9 gains a "Set up your Foundation" button that opens the actual Foundation page (/ds with no selection IS the Foundation, so the route needed nothing new). The reader goes from "here are the values this page is wearing" to changing them in one clickThe argument, the evidence, and the control now sit in the same place: the paper says the Foundation is half the game, the diagram proves the page is wearing it, and the button hands over the wheel. One testing note repeated itself: the scaled browser-pane click missed twice where a direct click succeeded — the button was never the problem
2026-08-27§10's anatomy table becomes cards with GitHub links (user direction: show it better, cards explaining each one, link to GitHub to go see it). The File/Job table stays exactly as it is in PAPER.md — the page now PARSES that table's bytes and renders each row as a card: the job's lead (up to the first period or colon) as the card's name, the path in mono, the rest of the job as the description, and a "View on GitHub" link resolved from the path (directories to tree links, files to blob links; the two rows whose cells are not a clean path — the elided ds-docs path and the gate — carry explicit targets). Twelve cards, twelve working links, verified in the running app. /ds and the downloads still show the table, because they render the file as a document; the article renders it as a map you can click intoSame principle as the Browse index and the §9 diagram: when the page wants a richer presentation, it derives it from the paper's bytes instead of writing a copy. Edit the table and the cards follow; a card cannot describe a file the paper stopped naming
2026-08-27A schematic kit, and the first two diagrams drawn with it (user direction: SVG-led engineering-style diagrams in a supplied line-work aesthetic — node circles, fanned curves, dotted leader annotations — built as a reusable kit, explicitly NOT part of the official design system, presentation-only). New file apps/site/src/components/home/schematic-kit.tsx: eight SVG primitives (Schematic surface, SNode circle-station, SDot waypoint, SLink bezier fan curve, SText mono caps, SLead dotted leader annotation, SScreen wireframe surface, SBar content stand-in). Every stroke and fill is a semantic role through a CSS variable, so the schematics re-theme with the Foundation; every primitive spreads SVG props so interactions can layer on later. Two compositions replaced their older forms: §1's div-built screens became annotated line-work (leader labels naming each embedded bolt-on; the ambient side with "one presence" led to the floating pill and a dashed context curve), and §12's ascii pipeline block renders as a flow schematic — five scales fanning into PRIMITIVES, a spine through SEMANTIC ROLES with the SAVE commit point marked in accent, COMPONENTS fanning out to five consumers. The ascii stays in PAPER.md; the page swaps it at render, same as the anatomy cardsThe kit's charter is written in its header: no /ds entry, no packages/ui residence, no product surfaces — if a product surface ever wants these, that is a governance event, not an import. Watchlisted in §13 under exactly those terms
2026-08-27The "one master, two projections" demo joins the schematic language (user direction: turn it into a diagram). The §12 demo's div-and-card layout redrawn with the kit: the token file's five keys fan into a TOKENS.JSON station, the flow splits at a waypoint ring into two labeled curves — compile · on save, and sync, both in the accent — each landing on a small wireframe of its projection (the product screen; the Figma variables panel). The footer line stays as prose. With this, every diagram on the page shares one drawing grammarTwo diagram grammars on one page was itself a small drift; the second composition existing made the kit cheaper than the inconsistency
2026-08-27The live config diagram joins the schematic language (user direction: this one too). §9's panel-and-components layout redrawn with the kit while KEEPING its live property: the six knob rows read the saved Foundation via useFoundation() and render as schematic text (accent blue · gray neutral · radius 4px · spacing default · scaling 100% · motion productive, as of this save), fanning into a FOUNDATION station ("one place, one save"), through the accent SAVE commit point, into EVERY COMPONENT ("nothing opts out") fanning out to five consumers. The caption and the "Set up your Foundation" button stay beneath the drawing. All four in-article diagrams now share the kit's grammarA schematic whose labels are live state is the kit's best trick so far: the drawing cannot lie about the theme because the theme is its data source
2026-08-27The paper becomes a follow-along, and the title becomes the outcome (user direction: write it like an article someone follows to bring this into their own codebase; title should be the click-worthy outcome — solving AI drift). Retitled: "Design Architecture" → "Stop AI drift", subtitle "How to let AI build your product at full speed without your design turning to slop: an architecture you can set up in your own codebase." The opening now promises the follow-along shape (four steps, each stretch ends with what to do in your codebase). Four "In your codebase:" action blocks close the four Part II stretches: §8 measure your drift (grep for hex, arbitrary values, raw durations — that count is your drift inventory), §9 name your scales and set up the Foundation once, §11 write the rules where the AI reads them and gate the build ("a rule the generator never reads is folklore; a rule the build enforces is architecture"), §13 one token file synced to Figma, then prove it with one alias change. And the hero stopped hardcoding the title: usePaper now returns the sliced title block and the hero renders it, so retitling the paper retitles the pageThe old title named the discipline; the new one names the problem it kills, which is what a stranger searches for. The hero hardcoding was a two-copy bug waiting to be noticed — this retitle would have silently missed the page without the fix
2026-08-27ViewMenu becomes a segmented pill: every destination visible, the active one wearing its name (user-designed in Figma, View Menu states 162:166/162:211, implemented from get_design_context). The disclosure form (pill + burger + drop-down list) is gone: each view is a permanent segment — collapsed to its icon in a secondary circle, expanded to icon + label when active — and selecting IS expanding, the label growing in on the surface spring with a micro fade. Collapsed segments carry real Tooltips and their label as aria-label. The pill trades ambient-glass for the popover ground: it is product chrome, and only the assistant's surfaces wear the ambient material (§8). Home folds in: the playbook is home, its VIEWS icon becomes the home glyph, and the separate home control went (the same destination twice is a trap); the home prop remains for apps where home is not a view. Appearance keeps its trailing circle. Registry entry and story rewritten (the story is now live state, so it demonstrates the expand); Figma's exact values landed on system steps: 32px segments are Button sm/icon-sm, 4px pill padding is p-1, the label is UI/sm MediumThe disclosure saved width but priced every switch at two clicks and hid the destination set behind a burger; with a handful of views, icons cost less than the menu did. The design was translated through the vocabulary — both Figma states reproduce from Button variants and motion roles with zero new CSS
2026-08-27Part I now opens with the full philosophy of Ambient UI (user direction: start with the philosophy — why it exists, why and how you need to think about it). The two-paragraph intro under the Part I chapter break grew into a complete statement in three movements: WHY IT EXISTS (every product is bolting AI into a spot in the interface; that quietly fails because AI is not a feature — pinning it fragments it into entry points sharing no memory, context, or identity; the placement is the mistake, not the model), THE INVERSION (a presence above the product, stated in bold, with the feature/presence contrast), and HOW TO THINK — four shifts in bold-lead paragraphs: think in attention not screens, in contracts not integrations, in components not chat, in inheritance not branding. The closing roadmap maps the six sections onto those shifts. Pure PAPER.md prose; the page needed no codeThe four "think in X, not Y" shifts are the part a reader keeps: each one names the default they will otherwise reach for (screens, integrations, chat, branding) next to what replaces it, and each maps onto sections that prove it. Philosophy that does not change what you do next is decoration
2026-08-27The philosophy gets its callout in the contents (user direction: call out the word philosophy in the nav). NAV_PARTS gains an optional lead — a named opening that precedes a part's numbered sections — and Part I sets it to "The philosophy". It renders as entry 00, ahead of 01, in the entry style but font-medium, jumping to the part-1 anchor where the philosophy prose lives. Numbered 00 because it comes before everything: the numbering language the nav already speaks, extended one step leftA reader scanning the contents now sees the philosophy as a destination, not just prose that happens to sit under a chapter break. The lead is part of the nav model, so a future Part II opening gets the same treatment by setting one field
2026-08-27A real home page, and the playbook becomes the Architecture (user direction: create a home page and call the playbook the architecture — completing the structure the Figma ViewMenu states already drew: Overview leading, Architecture beside it). New overview-view.tsx: one landing screen on the ambient grid — the AMBIENTUI overline, the tagline at display scale, the install command, two CTAs (Read: Stop AI drift · Open the design system), three door cards (the architecture / the dev tool / the canvas), and a footer noting the layer is live on the page. It is a landing, not a document: no contents column, the article lives behind the Architecture door. VIEWS grew to four with overview as Home at "/" and the playbook view renamed Architecture (segment icon: ruler — the closest the icon vocabulary has to the design's drawing compass; a compass name remains a governance candidate). Legacy ?view=playbook links land on architecture. Jump-to registry, page chips, the article's overline and breadcrumb all followThe front door and the argument were the same page, which made the argument the greeting. Now the greeting is one screen that opens doors, and the article is a destination with a name that matches what it argues
2026-08-27The home page empties to a wordmark (user direction: remove everything, bring the supplied reference — a giant low-contrast centered wordmark on a soft gradient — and write "Ambient UI"). The overview is now a single held breath: the wordmark, the ground, the presence. The reference translated per the primary rule: the ground is a gradient between two surface roles (muted → background), the wordmark is the foreground role fading through opacity steps as clipped gradient text, the type sits on the top steps of the scale of record (8xl/9xl, the nearest legal sizes to the reference's viewport-scale mark), and the entrance rides the surface spring. One control remains: the reference's scroll cue made honest — a chevron above the resting orb that opens the Architecture, because that is where reading on leads. The doors, CTAs, install command, and footer went; the article carries all of it behind the Architecture segment. Placed at bottom-20, not bottom-10: the resting orb owns bottom-centerVerified in both appearances — the same two-role gradient reads as mist in light and as ember-glow in dark, with zero mode-specific code. A front door that is nothing but the name works only because the layer is live on it: the pitch is the orb, not a paragraph
2026-08-27The wordmark becomes the identity at identity scale (user direction, three strokes: remove the top padding, make the text even bigger, bring the orb to the marked spot — the dot of the i — and give the text the shader). The Overview wordmark is now drawn as SVG (a wordmark is a drawing of a name, so the graphic scales with the viewport while the type scale keeps governing text): the glyphs are a clipPath in the Foundation's configured font, filled with the LIVE heat field — OrbField at stage strength riding the layer's real orbState, over a quiet role-gradient underlay so the name never goes blank while WebGL warms up — and the text is set with a dotless ı so the OrbCharacter itself takes the dot's place, driven by the same state. The name literally breathes, listens, and thinks with the assistant. Two rules touched, both sanctioned here and only here: the shader-surface rule and one-character-per-surface. The Overview wordmark is the one place where the mark IS the subject rather than chrome — this is exactly what the identity-scale cost rule exists to allow, and both bodies are the identity's own, driven by one state machine. Any other surface wanting either remains a governance event. The overview panel also dropped its pt-16 (the menu floats over the full-bleed gradient)Ask the assistant something from this page and watch the name think. The strongest identity claim the system can make is the one where the wordmark and the assistant are visibly the same organism
2026-08-27The orb leaves the wordmark (user direction, on seeing it: remove this). The dotless-ı trick and the embedded OrbCharacter went; the i has its dot back and the character stays where it lives — the resting orb. The heat-field fill remains. The sanctioned exception in the previous row narrows accordingly: the wordmark wears the identity's FIELD, not its character, and one-character-per-surface stands unbroken againSeen at size, two rendered characters on one page read as two assistants. The field carries the aliveness without claiming to be the being — which is the distinction §8 was drawing all along
2026-08-27The wordmark wears the orb's exact shader (user direction: bring the exact shader and the same animation as the orb component — the field variant read washed). New package export OrbHeat (packages/ambient/src/orb-character.tsx): the character's heat unwrapped — the same useHeatEngine springs, per-state params, palette, and circle image, with none of the glass shell — its header carrying the same sanctioned-surface charter as the character (any product surface wanting it is a governance event). In the wordmark it renders as the character's full circle with its TOP CAP under the text band: a circle's rim crosses a mid-band at only two points, but along the cap the arc sweeps through the whole name — the orb's rim as a horizon through the glyphs, state-driven like the character (still is quiet by design; thinking runs hot; answer blooms once). A 960 variant was tried and reverted: the rim scales with the circle, so bigger meant dimmer — the 640 cap keeps the character's actual intensity"The same animation" turned out to be the whole point: because it is the same engine, the wordmark did not get an animation — it got the character's nervous system, and the difference is visible the moment the assistant starts thinking
2026-08-27The wordmark fills edge to edge (user direction: the whole thing should be filled — the cap arc lit only the middle). Two mechanics: OrbHeat gains the shader's scale knob (>1 pushes the shape's rim toward the frame edges) and an optional image, and a new banner-ratio heat mask (apps/web/public/orb-rect-banner.svg, 320×75, matching the wordmark band's aspect) joins the three field rects. The wordmark now renders the heat shaped to its own band at scale 1.7: rim near the band's ends, warm interior glow through every glyph, same engine and states throughout. Empirically tuned — scale 2 pushed the rim out and left the interior cold, 0.5 shrank it to a bar, 1.7 fillsThe heat is a rim phenomenon: its color lives at the shape's edge and radiates inward. Filling a band means shaping the heat TO the band, not enlarging a circle — the mask is the knob, which is exactly how the field solved the same problem for surfaces
2026-08-27The fill reaches the extreme glyphs (user report: not going end to end from the A to the I). Scaling the mask further could not do it — past ~1.9 the band's short vertical rim leaves the frame and the interior goes cold. The fix is the field's own trick: OVERSIZE THE HEAT FRAME past the clip (832×195 behind a 640×150 band, same aspect, scale 1.7), so the warm span covers the whole name and the cool margins fall outside the glyphs. One knob was a dead end; the frame was the second knobSame lesson as the field's -inset oversizing, relearned at the wordmark: when a shader's edge behavior fights the crop, move the frame, not the shape
2026-08-27The home page gets the field as its ground, under a dark tint (user direction: shader background with a dark tinted layer on top). The overview's gradient ground became the identity's own OrbField at stage strength with a veil above it, per §8's every-ground-is-a-field-under-a-veil rule — but this hero commits to the dark look in BOTH modes, so the veil is not the stage frost (which follows the theme) but an explicit dark tint: the foreground role in light and the background role in dark, both resolving dark, both derived values. Tuned by eye through three stops: 60% read as a flat gray wall (the stage field's pale center under near-black is just gray), 80/75 lands it — a moody dark ground with the field's glow bleeding through, the wordmark riding above the veil with its own heatThe wrong first veil was instructive: a dark tint over a rim-heavy field dims the CENTER most, because that is where the field is palest. The look lives at the opacity where the rim glow survives and the center goes quiet
2026-08-27The home page gets a second screen: the layer arriving over any product (user direction, annotated below the wordmark: a SaaS app shell with the command palette opening over it as it comes into view, then a demo where typing hands off to AI mode). New ShellDemo on the overview: a wireframe SaaS product (traffic-light header, sidebar, five list rows — deliberately a drawing, because the point is that the product can be anything), and over it a scripted film of the spotlight's ask transition built from REAL parts: the glass material, the actual Composer (controlled, so the script types into the true component), and StreamingText for the answer. The choreography: section scrolls into view (IntersectionObserver) → the surface springs in on the surface role → the question types itself → the surface flips in place to thinking (the ambient shimmer) → the answer streams at the runtime's pace → holds, then loops; leaving the viewport resets it. The hero's chevron now scrolls here ("See the layer in action"), and a "Read: Stop AI drift" button closes the page. Demo cadences are choreography constants, commented as such — the surfaces themselves still enter on the motion roles. One lint rule earned its keep again: the reset lives in the observer callback, not the effect bodyThe near-miss rule shaped this: no fake palette was built — the input row IS the Composer, the stream IS StreamingText, the glass IS the material, and only the product beneath is wireframe. A demo made of real parts cannot drift from the components it advertises
2026-08-27The shell demo becomes the palette's real behavior over a real-looking product (user direction: use the actual component behavior — line items first, flipping to Ask AI on a long-tail query; a dashboard like the shared reference with names changed; ground the demo data in the UI itself; drop the arrow and start the UI right under the wordmark). The wireframe shell became a believable dashboard for a fictional product (northbeam / checkout-api: brand header, sidebar nav, tabs, a Versions list with one row marked failed). The film now acts out the spotlight's documented one-input-two-intents rule with the SAME heuristic the palette uses (4+ words, leading interrogative, trailing ?): while the typed query is short it ranks the product's line items; the moment it becomes a question, an Ask AI row takes the top slot, live, mid-typing — then the surface flips to thinking and streams an answer GROUNDED in the versions list beneath it (it cites 5831257 and ebf2e21, both visible in the dashboard). Layout: the chevron went; the hero is no longer a full screen; the UI starts directly under the wordmark; the CTA cleared to pb-40 because the resting orb owns the viewport's bottom-centerThe intent flip is the demo's whole argument, so it runs on the real heuristic rather than a scripted cue: anyone who reads the code finds the same rule the palette documents. And the answer citing rows the viewer can see beneath it is what "the answer knows the page" looks like when it is true
2026-08-27The demo becomes the actual component: a second real layer, mounted inside the frame (user direction, with Linear's homepage as the pattern: use our own ⌘K component itself). The film is gone. The northbeam frame now contains a full nested ambient layer — its own AssistantProvider, its own Assistant, its own resting orb — scoped to the frame by transform containment (a transformed ancestor is the containing block for fixed descendants, so every surface the layer owns renders inside the demo box). The embedded product declares itself to ITS layer through the same contracts every page uses: setPageChip (checkout-api · Overview), setPageIntel (grounded suggestions, the service's own placeholder), navItems (the dashboard's four destinations, which the real Jump-to then lists). The spotlight opens when the frame scrolls into view, and the visitor can genuinely type into it — real intent rule, real pipeline. Two supporting changes: the package's Assistant gains hotkeys (default true; the embedded instance passes false so it never fights the page's layer for ⌘K), and the dashboard grew Domains and Metrics cards so the palette has page to know. The frame lost its overflow clip — the dashboard clips itself and the layer's surfaces float past the edge the way real overlays do — and sits at z-10 over the page flowThe one-character rule holds by scope: the frame is a different PRODUCT, and its orb is that product's own. Embedding the real component was less code than the film it replaced — the film re-enacted contracts the layer already honors, and deleting the reenactment left only configuration
2026-08-27The demo goes full-bleed: the product is the ground (user direction: make the background UI full width and mount the component inside it — Ambient UI in action). The framed card became a viewport-tall, edge-to-edge app surface: the dashboard is now a full-height flex column with border-y only (no card rounding — an app, not a screenshot), and the demo section dropped its gutters. The embedded layer's surfaces center in the real app the way they would in production, and its resting orb sits at the app's own bottom-centerAt card scale the demo read as a picture of the idea; at viewport scale it reads as the idea. The only changes were containers — the layer needed nothing, because a layer that truly floats above a product does not care how big the product is
2026-08-27The demo app densifies to dev-tool grade (user comparison: the dev tool reads as a real full-screen UI; the dashboard read as a sparse mock). The height chain is completed (dashboard root h-full flex-col with border-y, body min-h-0 flex-1) and the main becomes a dense two-column body: eight version rows filling the left two-thirds, a right rail of Domains / Metrics / Next steps stretching to the bottom. No dead space; the app now fills the viewport the way the dev tool does, and the palette opens inside a screen that looks lived-inFullness is what sells "in action": an empty product under a palette reads as a stage set, a dense one as software. Also a process note — a multi-edit patch aborted mid-list earlier and silently dropped its first hunks; the screenshot caught what the assertion did not
2026-08-27The demo gets its presentation shell: a desktop window on the ground (user direction, with Cursor's marketing shot as the reference: a background, and the UI demo inside a shell). The full-bleed app moved into a floating desktop window — traffic-light dots, a centered "northbeam — checkout-api" title bar, rounded-2xl chrome, border and the shadow scale's deepest step — sitting with margins on the ambient field ground, which now reads as the presentation backdrop it always was. The window carries the transform containment AND the overflow clip, so every surface of the embedded layer (spotlight, panel, resting orb) lives inside the window the way a product's own layer would. The window fills the section's viewport height minus its frame padding — all scale steps, no arbitrary valuesThree groundings in one frame: the page's ground is the identity's field, the window is the product, and the layer floats inside the product. The reference's painterly backdrop is played here by the thing this system actually has — the living heat field
2026-08-27The film returns, driven through the component's own APIs (user direction: bring the layer-in-view demo back — the user writing, then it moving to AI mode by itself). When the window scrolls into view the embedded spotlight opens; then the question writes itself via seedPrompt — each progressive seed bumps seedVersion, and the Assistant's own seed-drain effect lands every prefix in the real input, so the "typing" is the surface consuming its actual handoff API forty times; the final seed passes autoSend and the REAL pipeline takes over: the thinking beat, the composed answer flipping the surface into the AI overview, references carrying the page chip, follow-up suggestions, a live composer. Zero puppeteering of DOM or internal state — the film is choreographed entirely through the context's public seam (setMode, seedPrompt)The earlier film simulated the component; this one OPERATES it. The distinction matters for trust: when the demo and the product share every code path, watching the demo is using the product
2026-08-27The film loops, the window matches the wordmark, the layer matches the app (user directions, three at once, plus: remove the caption and the CTA). (1) After the answer has been read the embedded layer goes back to rest (setMode("line")) and the film replays — a cycle counter re-arms the same effect, so the loop is the film's own script run again, not a second mechanism. (2) The window now spans the wordmark's exact width: the section wears the hero's own px-6 gutters and the max-w cap went — measured equal to the pixel. (3) The embedded layer renders at the demo app's density via zoom: 0.8 on its wrapper — zoom scales the layer's layout, fixed surfaces included, WITHOUT becoming a containing block, so the surfaces still position against the window; the factor is presentation choreography like the film's cadences, commented as such. The caption line and the "Read: Stop AI drift" button went — the demo argues without a placardTwo families in one frame only look like one product when they share density; the zoom is the demo's version of the scaling dimension. And zoom over transform was the whole trick: transform would have re-parented the fixed surfaces to a zero-height wrapper
2026-08-27The zoom comes back out: the layer's scale is the Foundation's (user correction: the scale needs to be what the Foundation sets; only the outer shell carries the decided width). The zoom: 0.8 wrapper went the same day it arrived. The embedded layer renders at the theme's own rem base — its size is a THEME decision made once on the Foundation's scaling dimension, not a per-demo knob — and the family resemblance is the shell's job: the window carries the presentation sizing, the component inside carries the system'sThe correction is the system winning an argument with its own author's demo: a presentation constant that duplicates a Foundation dimension is a second copy of a decision, and the §12 row above this one had already called it choreography when it was really scaling
2026-08-27The demo app itself moves to Foundation density, and the orb learns to measure its frame (user corrections: the dashboard's scale should also be the Foundation's, width maintained; and the embedded resting orb was half-hidden past the window's edge). Two fixes. The dashboard sheds its text-xs miniature world: header h-14, sidebar w-56, text-sm body, scale paddings throughout — a product built at the theme's own density, with only mono ids and tags stepping down as labels legitimately do. And a real package bug surfaced: the ORB POSITIONED FROM THE VIEWPORT (window.innerWidth/Height), which is correct at the page level and wrong inside any framed layer — its coordinates landed past the containing block and the clip took half the character. orb.tsx now measures its containing block via the fixed element's offsetParent (the transformed ancestor when framed, null at the viewport where the window numbers remain the truth), watched by a ResizeObserver. Verified: embedded orb resting fully inside the window, centered on the frame; the page's own orb unmovedThe demo keeps auditing the layer it advertises: "render inside any frame" was believed until a frame existed, and the first real frame found the one place the layer still assumed it owned the whole viewport
2026-08-27Three at once: the window aligns to the glyphs, every heat surface wakes on mount, and the spotlight gains its scrim (user directions). (1) The demo window's width now aligns to the wordmark's DRAWN glyphs, not its SVG box — measured from the text's live getBBox (re-run when fonts land) because the Foundation's font choice moves the glyph extents; measured, never guessed. (2) "The shader is not starting on load": diagnosed as the still state being calm enough to read as frozen — the shared heat engine now initializes at WAKE_PARAMS, hotter than any resting state, and the springs settle into the actual state on mount, so the first seconds of every heat surface (character, field, wordmark) are visibly alive. One change, every surface, because there is one engine. (3) The spotlight overlay trades its bare transparency for a glass-tinted scrim (--scrim + a whisper of backdrop blur): the product dims behind the veil and the palette reads as the hero. The best part: docs/ambient-shell-spec.md had ALWAYS said "over a scrim" — the code comment said no backdrop; the spec was right and the code had drifted from its own porting document. Registry entry and spec token table updated; registry rebuiltThe scrim change closed a drift the gate could not see: prose said scrim, code said none, and both shipped. A behavior claim in a spec is only checkable by a person reading both — which is what this log is for
2026-08-27The spotlight sizes against its containing block, so the demo shows properly at every screen size (user report: on smaller screens the palette outgrew the demo window). The same disease as the orb's viewport bug, in CSS form: the spotlight's mt-[9vh] / max-h-[72vh] / max-w-[92vw] measure the VIEWPORT even inside a framed layer, so the palette spilled past the window. The overlay (which IS the fixed element's containing block) becomes a flex column, the head margin becomes an h-1/12 fraction spacer, the wrapper caps at max-h-5/6 with max-w-11/12, and the glass takes max-h-full — all Tailwind fraction steps replacing three grandfathered arbitrary values, fixed on touch as the rule says. On the full page the numbers are near-identical (8.3/83.3/91.7% vs 9/72/92); inside any frame they now mean the frameVerified at full size and at 1000×620: the palette holds its proportions inside the window and scrolls internally. Second lesson from the same family: a layer is only as embeddable as its least container-relative unit — the orb's JS viewport read and the spotlight's vh were the same assumption in two languages
2026-08-27Appear animations across the overview — and Reveal gets promoted by its own trigger (user direction: subtle appear from the text to the demo shells; the demo starts once the layer is in view). The wordmark and the demo window now enter on the shared Reveal (one-time whileInView, surface spring + micro fade), sequenced so the entrance leads and the film follows: the shell reveals as the reader scrolls to it, and the existing 40%-visibility observer starts the film once the frame is properly in view. The governance part: Reveal's §13 watchlist entry said "promote if a second page wants scroll-staged sections" — the overview is that page, so Reveal moved to apps/site/src/components/reveal.tsx as an app-shared composition (not vocabulary: it is a page-authoring device; a third consumer outside this app is the next trigger), and the playbook now imports itThe watchlist did its job exactly as written: the pattern waited as a local, its promotion condition was named in advance, and the second consumer tripped it — no debate needed, the decision had already been made
2026-08-27The ViewMenu's state switch becomes one continuous move (user direction: give it a transition between states). The pill and every segment now carry framer layout on the surface spring: when the active view changes, the old label folds out, the new one grows in, and the neighbors slide to make room — previously the label animated but the segment sizes and positions cut. Registry behavior line updatedThe label animation alone was half a transition: the eye tracks the row, not the label, and a row whose boxes jump reads as a cut no matter how gracefully one span fades
2026-08-27The overview gains its statement section (user direction, with Mercury's overview as the reference: a large muted statement, then a 2×2 grid of one-word principles over hairlines). Translated through the system: the statement is the muted-foreground role at the 3xl/4xl steps, centered and balanced; the four cards are border-t hairlines with 2xl heads — and the four WORDS are the paper's own four shifts wearing single-name form: Ambient (attention, not screens) · Grounded (contracts, not integrations) · Composed (components, not chat) · Inherited (inheritance, not branding). One card carries the reference-style link, into the architecture. Every block enters on the shared RevealThe content did not need writing — the philosophy section had already condensed itself to four moves, and the statement section is those moves at poster scale. When copy falls out of the paper instead of being invented for the page, the two cannot disagree later
2026-08-27The statement section takes the enterprise frame (user direction: the philosophy is a decluttering of system-of-record SaaS — the layer stays on top of the data record and works alongside the SaaS; infrastructure-first, enterprise-first). The statement rewritten: "Ambient UI is a decluttering of enterprise software — the system of record keeps every pixel of its screen, and the AI works above the data, alongside your SaaS, not inside it." The four principles keep their words but their bodies now speak that language: Ambient (no tab, no panel, no corner of the record), Grounded (acts on the row you are looking at, never on a paste of it), Composed (working UI against the record, in place), Inherited (infrastructure-first — deploys without adding a brand or a drift surface)The earlier copy said what the layer IS; this says what it spares — screen space, re-explaining, chat walls, a foreign brand. For an enterprise buyer, what a tool does not consume is the pitch
2026-08-27The principle heads go plain (user direction: use the direct word, like Context — no one-word abstractions; make the lines simply explainable). Ambient/Grounded/Composed/Inherited become Takes no space · Knows your context · Answers with real UI · Wears your design system, each body cut to two or three plain sentences a reader parses in one pass ("Ask about \u2018this invoice\u2019 and it knows which one — you never paste, describe, or re-explain your screen")The one-worders read as brand vocabulary that needed the paragraph to decode; the plain heads ARE the claim. A heading that requires its body is a label; one that survives alone is a promise
2026-08-27The ground becomes the viewport, not the document (user report: some issue with the scroll — the field's glow ramp smeared across the page's lower half). The field had been absolutely positioned against the ROOT, so as the page grew to four viewports the shader stretched with it: its inner gradient became a giant ramp over the empty stretch and read as broken scrolling. The ground layer is now fixed inset-0 — sized to the screen, steady while content scrolls over it, unmounting with the view — with the horizontal -inset-x-1/4 oversize keeping the side glow end to end (the previous fix's -inset-1/4 had also been scaling with document HEIGHT, compounding the smear)A background is a property of the screen, not of the document. The field behaved perfectly on a one-viewport page and the bug only existed at four — backgrounds sized to content are a scroll-length bomb with a delay
2026-08-27The forms section: one presence, many shapes, shown by the real layer changing shape (user direction: showcase the AI's forms in the same style as the top demo). New section after the statement — "One presence, many forms" — with a second embedded layer in the shared presentation window (DemoWindow, extracted so both demos wear one shell). A FormsDriver seeds one real exchange on activation (so panel, dock, and history have a transcript to show) and then holds the layer in whichever of its five real modes the section presents: Orb · Spotlight · Panel · Dock · History, each described in the paper's own §2 words. Auto-advances every 6.5s while watched; the chips switch the actual mode — a visitor clicking Dock watches the real panel anchor and the page reflow. Also fixed in passing (user report): the ViewMenu's layout animation was FLIP-scaling the buttons mid-transition — segments now animate position only, with the width change carried solely by the label spanThe section does not depict the shapes, it PUTS the layer through them — the same setMode any product calls. Five forms, one component, zero mockups; the demo is the §2 table executing
2026-08-27The forms break into sections, and both loops learn to hand over (user directions, two in a row). (1) The top demo's film now tours the layer after the ask — panel, dock, history, back to rest — then starts from the beginning; the moment the visitor touches the window (pointer or key, captured on the frame) the script stops and the layer is theirs. The forms demo's auto-advance stops the same way. (2) The single cycling forms window then split into FIVE SECTIONS in the established style — each form gets its name, its line from the paper, and its own presentation window with a real layer held in that form, seeded through the spotlight first so even History opens onto a transcript. Every window sits at the wordmark's measured width, same as the firstAn autoplaying demo and a visitor's hand on the same control is two drivers at one wheel — the film must yield the moment a human reaches for it. And five pinned sections beat one cycling window for reading: a form you can scroll back to is documentation; one that rotates away is a carousel
2026-08-27The layer's drag geometry learns the frame, and the page settles on one measure (user report: the panel walked out of the demo shell; user direction: keep the content width like the statement section across the page). (1) The orb's containing-block lesson, applied to the Assistant: panel drag and the dock/spotlight hot zones computed from window.innerWidth/Height and raw client coordinates — correct at page level, wrong inside a frame, where the panel positioned itself off the whole screen and left the shell. A zero-size fixed probe finds the containing block (offsetParent), pointer coordinates convert to frame space, and every clamp and zone tests against the frame. Third member of the family: orb JS, spotlight CSS, drag math — all the same assumption. (2) The glyph-width machinery retired: every content block (statement, demo, forms header, five form sections) now shares max-w-5xl — one measure, no ruler, the wordmark alone staying display-scaleThe embedding demo keeps earning its keep as a test rig: every "render anywhere" assumption the layer still carried has been found by actually rendering it somewhere. And on width: a measured alignment was clever, a shared token is calmer — the page stopped needing the ruler once everything agreed on the measure
2026-08-29Heat surfaces survive context loss, and demo windows learn to let go (user report: the wordmark's shader animation dies after a while). The browser caps live WebGL contexts per page and evicts the OLDEST when a new one is created — which is precisely the long-lived identity surfaces: the wordmark and the page ground froze while freshly-scrolled demo layers kept compiling shaders. Two-sided fix: every heat surface (OrbCharacter, OrbHeat, OrbField) now listens for webglcontextlost and remounts its shader (useShaderEpoch), and each demo window mounts its embedded layer only within 300px of the viewport — an off-screen window holds zero contexts. Leaving also resets interacted, so the film re-arms for the next visit (closing the gap where a documented re-arm did not exist in code)The identity's permanence was resting on a resource the browser reclaims silently. Recovery makes death impossible; the mount gate makes it rare — a page full of live layers needs a context budget the way it needs a z-index discipline
2026-08-29The film gets a hand (user direction: a cursor that mimics the clicks, per Figma 163:230). A glass puck (DemoCursorLayer, app-side presentation choreography like the schematic kit — not vocabulary) glides to the layer's REAL controls and presses them before each film step: the orb button to open the spotlight, "Open in chat window" for panel, the drag handle PULLED to the edge for dock (the drag-as-mode-switch gesture, drawn), "History", "Close history". Repeat cycles press the real "Back to search" — actually dispatched, since clearing has no public-context API — so every loop starts from the clean palette. The puck is pointer-events-none throughout and a dispatched click never counts as visitor interaction (the stop listens for pointerdown). Sizing per the token rule: the design's 22px disc rides size-5, the 14px core is size-3.5A mode change with no visible cause reads as a slideshow; the same change caused by a drawn press on the control that really does it is a lesson in the interface. The film now TEACHES the clicks it performs
2026-08-29An embedded layer must never scroll its host (found while verifying the film: the page yanked itself mid-demo). Two culprits, both in the layer: every focus call now passes preventScroll (a layer surface is in view by construction — the browser scrolling to it can only mean scrolling someone else's page), and PaletteList scoped its keyboard-follow to its own list element — it had used document.getElementById on an index-numbered id, so with several layers mounted (the demo page) it could grab a SIBLING instance's row and scrollIntoView walked the host scroller across the page to it. The list now scrolls only itself, by arithmetic on its own scrollTopFourth member of the containing-block family, and the first found by observation rather than layout: window dims, vh/vw, client coords, and now document-wide id lookups — every global the layer touches is a way to escape the frame. Duplicate DOM ids were the misfire's fuse; data attributes scoped to the instance defuse it
2026-08-29The film declares itself a recording (user direction: show that this is playing, how long it is, and that it loops). A transport pill under the demo window (DemoPlayback), story-bar style: one segment per chapter (Ask · Panel · Dock · History · Orb), played chapters as lit dots, the playing one a bar filling in real time, plus pause/play. The film restructured into budgeted beats (FILM_BEATS) whose durations the pill and the choreography share, and its sleep learned to pause — while held, time simply does not pass for typing, holds, and budgets alike. The fill's linear tween is a progress METER whose duration is the chapter's length — data, not motion styling, so the motion roles don't apply. The pill vanishes the moment the visitor takes overAn unlabeled autoplay reads as a broken app or a trapped user; a transport strip reads as a demo. Naming the chapters also names the tour — the pill is the film's table of contents
2026-08-29The film's gestures become real events (user directions: the orb click should show the blob's opening state, show the zones, drag the component). The hand stopped miming: its orb press is a dispatched pointer tap, so the QUICK-ASK pill grows out of the character first (the orb's true opening state, per the tap's own behavior) before the palette; its dock move is a REAL drag — pointerdown on the panel's handle, pointermove streamed along the glide, pointerup in the zone — so the panel travels with the hand, the hot zones appear, and the DROP docks it through the layer's own machinery. The interaction stop now honors only TRUSTED events, which is what lets the film operate real controls without shooting itself. The cursor layer moved to a whole-window overlay slot on DemoWindow (a body-clipped host hid the hand behind full-screen surfaces)The film had two vocabularies — drawn gestures and API calls — and every place they diverged was a small lie about how the product works. Dispatched events collapse them: what the hand does IS what happens, zones and all
2026-08-29The Ask chapter ends when the answer does, and the film says goodbye in words (user reports: the settled answer holds far too long; on interruption, show a shimmered handover with better copy). (1) Beat 0 stopped padding to a clock: the film watches the layer's own settling signal (orbState returning to "still" — the same settleResponse the product runs), holds a 2.4s reading beat, and moves on; the pill's first-segment duration is now an estimate, honestly commented as one. A waitUntil with a ceiling keeps a wedged pipeline from wedging the film. (2) When a trusted press ends the film, the transport pill is replaced by a handover line in the layer's own ambient-shimmer: "All yours — ask a follow-up, drag the panel, dock it. And this whole page runs the same layer: press ⌘K anywhere." — the film's last subtitle, pointing at both the demo and the page's real layerA demo that outstays its own answer teaches patience, not the product. And the interruption is the conversion moment: the one line shown then should hand over the wheel and mention the page itself is drivable
2026-08-29The form sections learn the film's manners (user direction: bring the same behaviour to the other demos on the page). Each of the five form windows now stages itself the way the top film does: its own hand (the same DemoCursorLayer, whole-window overlay) draws the REAL gesture that produces its form after the seeded exchange — Panel presses "Open in chat window", Dock presses it and then performs the actual drag (panel travelling, zones live, the drop docking), History presses "History"; Orb and Spotlight need no gesture, one being rest and the other the seed's own surface. A trusted press ends any section's script and swaps in the shared HandoverLine (extracted from the top demo): "All yours — this is the live component, not a recording…". Scrolling away re-arms a section, same as the filmOne demo teaching the gestures while five others teleport is a page arguing with itself. The staging code is one shared vocabulary now — hand, gesture, trusted-stop, handover — and every window speaks it
2026-08-29The demo window rises into the wordmark (user annotation: bring the panel up, the text behind). The hero's bottom padding went to zero and the demo section pulls up by −8.5% of width, so the window's top edge crosses the name's lower glyphs and "Ambient UI" passes BEHIND the product. The pull is a PERCENTAGE, not a spacing step, because the wordmark's height is width-proportional (viewBox 640×150): −8.5%w lands ~2.9%w above the baseline (y=114/150) at every viewport, where a fixed step would swallow the whole name on small screens — the same license as the measured glyph widthThe overlap is the thesis drawn: the layer sits above the record, so the product sits above its own name. And any dimension coupled to the wordmark must scale like the wordmark — proportions, not steps
2026-08-29Every demo wears the transport (user report: the playback pill was missing from the form sections). DemoPlayback generalised — it takes its chapter list, and a one-shot staging that finishes shows every dot lit with the button turned to REPLAY (the replay icon, already in the vocabulary). Each form section now carries a two-chapter transport (Ask · its own form name), its staging made pause-aware like the film's, and replay remounts the section's layer by key — fresh transcript, fresh refs, the staging from the top. Leaving the viewport still resets everythingA looping film gets pause; a finished staging gets replay — the same pill, telling the truth about which kind of demo sits above it
2026-08-29Demos start only when their window is properly on screen (user direction: a demo begins when the visitor has scrolled to it and the UI is visible). The start observers for the film and all five stagings moved from 0.4/0.3 to a shared 0.65 visibility threshold — a window peeking a sliver above the fold holds at rest (verified: 29% visible, no staging for seconds; scrolled to center, it begins). The 300px MOUNT gate is untouched: mount early so the layer is warm, start late so it plays to someoneA demo that plays below the fold spends its one scripted performance on nobody — mounting and starting are different thresholds, and only the second is about attention
2026-08-29The Orb demo shows the orb, immediately (user direction). Its staging dropped the seeded exchange: the section's whole exhibit is the resting state, so the window opens on the orb the moment its start gate passes — no spotlight flash, no transcript it would never show. Its transport is one chapterA demo's script should contain only what its subject needs. The Ask chapter existed for forms that display a conversation; rest is not one of them
2026-08-29The Orb demo performs the orb's own gesture — and a StrictMode lesson (user report, twice: the staging wasn't running). The Orb section now stages what its subject IS: rest, then the hand taps the character with a real pointer pair and the QUICK-ASK grows out of it (data-orb-state listening, the suggestion ghost, Tab) — the opened state stays as the exhibit. The reason the first attempt showed nothing: staged/seeded flags were marked at the START of the async run, and StrictMode's mount-abort-remount left the aborted run's flags behind, so the real run short-circuited to "done" without performing anything. Flags now mark only ON COMPLETION — an aborted run leaves no footprintOptimistic flags and aborted async runs are a standing conspiracy: any effect that marks work done before doing it will someday be cleaned up in between. Completion is the only honest place to write the flag
2026-08-29The Spotlight demo tells its gesture in order: ⌘K, type, ↩ (user direction). The cursor layer grew a keystroke card — screencast-style keycaps over the window's lower third — for the one gesture a pointer can't draw. The Spotlight section's staging replaced the instant seed with the real sequence: the ⌘K chord on screen, the palette opening, the long-tail question typing itself character by character, the ↩ card, the send, and the pipeline answering — settle-detected (the same orbState signal as the film), then the answered surface holds as the exhibitThe palette's whole argument is a keystroke; a demo that skips the keystroke skips the argument. The keycap card completes the hand's vocabulary: clicks drawn, drags performed, chords shown
2026-08-29The later forms start where the Spotlight demo ended (user direction: the Panel should just take the command palette's state). Panel, Dock, and History no longer stage a fresh-looking ask: their first chapter — now labelled "Spotlight", naming the state it inherits — seeds the exchange immediately (no typing; that story is told one section up) and WAITS FOR THE ANSWER TO SETTLE before the hand carries it into the section's form, so what travels is the finished palette, never a mid-stream flickerThe five sections read as one continuing story now: the Spotlight demo earns the answer, and every later form is that same answer changing shape — which is literally the §2 claim
2026-08-29Prep runs off-screen; the gesture opens the show (user direction: keep the palette loaded and click the panel icon the moment the layer is in view; the Dock starts from the panel and drags). The section stagings split in two: the 300px mount gate's head start now does the LOADING — the exchange seeds and settles (and the Dock's starting panel opens) while nobody is watching — and the visible show, gated on the 0.65 start threshold plus a prepped flag, is the one gesture: Panel's click carrying the loaded palette over, Dock's drag from panel to dock with the zones live, History's press. Their transports shrank to one chapter each — the loading was never a scene. (Chased a phantom mid-verify: the page appeared to scroll itself, but a hands-off watch showed zero drift — the motion was the test harness's own queued scrolls)The mount gate stopped being only a resource budget and became the STAGE DOOR: everything that is preparation happens behind it, everything that is performance happens in front. A demo's first visible frame should already be the interesting one
2026-08-29History opens from the panel, like a person would (user direction). The History section's prep now ends at the panel — same starting tableau as the Dock — and its visible gesture is the hand pressing the panel header's own History button. All three inherited forms now depart from states a visitor could actually be inThe gestures form a chain now: palette → panel (click) → dock (drag) / history (press) — each section demonstrates one real edge of the layer's state graph, from the node the previous section reached
2026-08-29Replay replays the show, not the loading (user report: the transport's seek bar dead on replay). The prep split had left replay remounting the whole layer, which meant a silent ~8s re-prep with a motionless bar. Pre-loaded forms (Panel · Dock · History) now replay by NONCE: the living layer rewinds to the gesture's starting state — transcript intact — and the gesture performs again with the bar moving at once (measured: bar at ~3s, panel landed at ~5s). Orb and Spotlight, whose whole story is on screen, still rebuild from scratch by key. The nonce is consumed inside the run, mindful of the completion-flag lessonWhat replay means depends on what the audience saw: they saw a gesture, so replay owes them that gesture — never the backstage work it took to be able to perform it
2026-08-29The architecture reads at the page's shared measure (user direction: give the overview's content width to the architecture body). The article column moved from max-w-3xl to max-w-5xl — the same measure as the overview's written sections — with the lead paragraphs keeping their own line-length caps; the width goes to diagrams, tables, and cardsTwo pages of the same site with two content measures read as two sites. The measure is a token of the page family now, not a per-page taste
2026-08-29The view menu holds one place, and the dev tool names itself a demo (user directions). The switcher no longer docks top-right over the editor — it centres at the top on every view, so the reader never re-finds it; and the "Dev tool" segment became "Dev tool demo", saying what the view IS to a strangerA control that relocates per view spends the reader's memory on navigation chrome. And on a public page every label is read by someone with no context: "demo" is one word of honesty
2026-08-29One site menu, every page (user directions: bring the menu to /ds, make it part of the top menu, don't hide the Design system segment on the main page). SiteMenu unifies the switcher: the home page's four views plus the design system are ONE destination set in one centred pill, rendered identically on / and /ds. It owns no state — navigation goes through pushState + a popstate dispatch, which the App router and the home page's view state already listen to. The /ds canvas pane gained top clearance so the floating pill never covers a headingA page's switcher and a site's switcher were two objects one click apart, and the difference was invisible to the visitor. Making the menu stateless made it placeless — it can sit on any page because it belongs to none
2026-08-29The /ds rail becomes a tree (user reference: the Cloudflare-style sidebar; and: remove the brand row). The brand header row went — the site menu names where you are, and the rail spends its top on search. Kits, documents, and the shadcn set fold as PARENT ROWS (RailFold: icon, name, count, disclosure chevron) whose children indent onto the sidebar's own sub-menu (SidebarMenuSub, the vertical guide line) — the sanctioned anatomy the vocabulary already shipped, finally used by its own reference page. Sections: Foundations · Ambient vocabulary (core + kits) · Documents · Product vocabularyA rail of labelled flat lists reads as a pile; the same rows with a guide line read as a place. And the /ds page eating its own sub-menu cooking is the point of the page
2026-08-29The registry never shipped the orb's shapes (found while packaging for launch). The five SVGs the heat shader wraps lived only in apps/web/public/, so npx shadcn add @ambientui/ambient-layer installed a layer whose orb rendered a cold, empty canvas — no error, no warning, the product's own identity silently dead in every consumer's app. The shapes moved to packages/ambient/assets/ (the layer owns them), ship as a new ambient-assets item targeting public/, and are a registry dependency of the layer block and of every per-component item whose closure reaches the shader. The site copies them in via a Vite plugin, so there is one copy under version control and the site cannot drift from what it publishesThe gate proved the registry was internally consistent for months while door 1 was broken. Consistency is not correctness: nothing had ever installed it
2026-08-29The layer learns where its assets live (same root cause, second half). Root-absolute asset URLs resolve against the DOMAIN, not the app — so under /ambientui/ the shader requested /orb-circle.svg and got the Pages 404 page. assetBase joins the runtime contract (default "/", correct for an app at the root), useAmbientAsset resolves names against it, and FoundationProvider takes it as a prop because only the HOST knows its base. The satisfies AmbientRuntime on the Foundation's bridge caught the new required field at compile time, exactly as its comment promised it would. Verified by serving the real build under a simulated /ambientui/: the old URL 404s, every new one is 200A component that hardcodes a root-absolute asset path has quietly assumed it will only ever be mounted at a domain root. The runtime already existed to hold what only the host can know; the assets simply had not been recognised as one of those things
2026-08-29The page ends on a command, and the doors become two tracks (launch packaging). InstallSection — 129 finished lines whose own comment called it "the section the whole site exists to reach" — was dead code that nothing imported; the landing page ended on two spacer divs, offering the reader nothing to do with the argument it had just made. Now rendered, followed by a real footer. Its four peer tiles became two named tracks — Take the components (things that render) and Adopt the architecture (the thing that decides what rendering looks like) — so a visitor is told which half is theirs instead of classifying themselves. The registry host and the installable count now come from a generated registry-facts.ts, checked by registry:checkThe doors were four items in a list; they are actually two ideas. And a landing page that renders perfectly while printing a hand-typed host is one rename away from handing out a dead URL
2026-08-29The catalog splits from its stories (launch prerequisite). ds-docs.tsx welded 50 components' serializable prose to live JSX inside the same object literals, so anything wanting to render what a component IS had to import framer-motion, the shader, sonner and the whole ambient kit to do it. Now: catalog.ts (1,088 lines, strings only, zero React — readable by a Node script or a static page), stories.tsx (the demo kit, keyed by the same ids), entries.ts (the join, for /ds). The split was performed as an AST codemod moving text by SOURCE SLICE, never re-serialising — and the proof is external and mechanical: registry.json is byte-identical afterwards, which is 50 components' prose surviving intact without anyone reading it. extract-catalog.mjs now reads catalog.ts, and its parse gets simpler because the file can no longer contain JSX. The command palette was the first beneficiary: it lists components by name and had been dragging the entire demo kit in to do itProse and demos are two different kinds of thing that had been one file because they were authored together. The registry already treated them separately in memory; the file system just caught up. And a refactor of this size is only safe because something downstream is byte-comparable
2026-08-29The second bijection: documented ↔ demonstrated (scripts/check-catalog.mjs, in the gate). Splitting the halves opened a drift the compiler cannot see — prose with no demo, or a demo for a component nobody documented. Neither breaks a build; both surface as an empty panel someone eventually notices. So the registry's own contract applies again: anything documented must have stories or a named reason in NO_STORIES, and no story may exist for an unknown id. It reads the keys with the TypeScript parser rather than importing the module, because a gate that boots React to check a list of strings is a gate people turn offEvery split this repo makes gets a check that the halves still agree. The first bijection stopped a component falling out of the registry; this one stops it falling out of the reference
2026-08-29The prerender hazards, and a gate that keeps them fixed (launch prerequisite). Six browser-global reads sat where a static build would execute them — theme-provider (localStorage), view-menu (matchMedia, unguarded), translucency-page (getComputedStyle INLINE IN A RENDER BODY), and route state in App/home-page/ds-page. Each fixed to the shape the repo already argued for: the colour scheme became a useSyncExternalStore read with a server snapshot (the useIsMobile pattern — mirroring an external store into state costs a cascading render and the compiler rules forbid it), storage and route reads became GUARDED initializers rather than effects for the same reason, and the live blur read moved to the next frame because the Foundation writes its style tag from an effect too. scripts/check-prerender.mjs now fails the gate on any new oneThe four that were flagged first were the easy half. The checker's second pass — resolving useState(readView) to the function one hop away — found the two that were actually hiding, which is the argument for the gate rather than the audit
2026-08-29The theme is stamped before first paint, from one pair of values (a bug I introduced and the gate caught). Moving the stored-theme read off first render meant the document would paint in the default and swap — a flash on every load. A blocking script in the head fixes that, but a script in HTML cannot import, so it restated the storage key: I wrote "theme" while the app stores under "ambientui-theme", and the flash was still there while the page looked fine. Now THEME_STORAGE_KEY and THEME_DEFAULT are named in theme-provider.tsx, main.tsx stops restating them, and scripts/check-theme-boot.mjs fails the gate if the HTML and the TypeScript disagree — verified by breaking it on purposeThe copy is unavoidable: only a blocking head script runs before paint. When a copy cannot be removed it gets a checker, which is the same answer this repo already gave for the vendored stylesheet and the registry
2026-08-29The governing documents split from their bytes (last Phase 0 prerequisite). system-docs.ts mixed eleven ?raw imports with the metadata describing them, so listing the documents cost ~9,000 lines of markdown in the bundle and could only happen inside Vite. Now system-docs.meta.ts is plain data any Node script or static page can import — and gains a slug per document, since these become real /docs/<slug> URLs and a URL that changes when an internal id is renamed is a link someone already shared — while system-docs.source.ts holds the ?raw imports and is deliberately the only Vite-specific module left in the docs pipeline: it is the single file the build-time-file-read move replaces, and everything importing the metadata carries over untouched. system-docs.ts still exports SYSTEM_DOCS, so every rendering surface is unchangedSame shape as the catalog split, for the same reason: description and payload have different reach, and the surface that only needs the description should not pay for the payload
2026-08-29The third bijection: described ↔ loaded ↔ on disk (scripts/check-system-docs.mjs). The docs split can drift three ways and only one of them is a compile error. The valuable one is the third: ?raw currently makes a missing governing file fail the BUILD for free, and that guarantee evaporates the moment those imports become readFileSync — so it is asserted here, where it outlives the mechanism that used to provide it. Slugs are checked for collisions too, because they become public URLs and a duplicate would silently shadow. Verified by breaking each of the three directions on purpose and watching it fail with the right messageA guarantee that comes free from a bundler feature is one you lose silently when you change bundlers. Write it down before the move, not after
2026-08-29The static-export app stands up beside the SPA (Phase 1 of the launch migration). apps/site is a Next App Router workspace with output: "export" — every route prerendered to real HTML, no server, which is the whole point: the Vite SPA served one identical document for every URL, so nothing but / could carry a title or its own content to a crawler. Four things had to be proven and were: the export emits real HTML with a real <title>; the packages resolve through their exports maps from dist (transpilePackages, and the tsconfig maps ONLY @/* — the SPA aliased two of three straight to source, so it could build green against a stale dist and never exercise the artifact it publishes); all four stylesheets import from one file in the root layout, in order, because Next guarantees CSS order within a file and not across them; and the Tailwind @source globs gained apps/*/app/** for Next's route layer. Both apps build; the deploy still ships the SPAThe migration's first commit adds an app and removes nothing. Everything before Phase 6 is additive on purpose — two apps running at once is what makes a comparison possible at all
2026-08-29The CSS surface gets an alarm (scripts/check-css-surface.mjs). Tailwind finds classes by scanning path-shaped globs, so moving code can stop the scanner seeing it — and nothing errors: the build succeeds, the bundle shrinks, the site renders unstyled. The check asserts eleven RARE sentinel classes survive (an arbitrary value like text-[11px] only exists if the file declaring it was scanned; common utilities survive partial detection and would hide the regression) plus a floor taken from a committed snapshot rather than a number someone guessed. Proven by breaking the packages glob on purpose — .peer/menu-button vanished and the check caught it. Also learned: breaking the APPS glob changes nothing, because Vite auto-detects its own root, which is precisely the crutch that disappears under NextThe Next build emits 789 classes against the SPA's 789, all sentinels present — that number is the evidence the glob fix worked, and it only means anything because the alarm was tested first
2026-08-29The seek bar is promoted, and the demos wear it (user direction: use this component on the home page). The OrbCharacter playground's lifecycle bar — segments plus a control at the head — became components/seek-bar.tsx when the home page's transports asked for it, which is the watchlist's own promotion trigger. Both consumers now render one implementation. onSeek is OPTIONAL and that is the design: the orb's lifecycle is a TIMELINE and passes it, so its track scrubs and wears a pointer cursor; the film is a SCRIPT that cannot be rewound to an arbitrary point, so it omits it and renders no affordance suggesting otherwise. Also fixed while adjacent: ambient.css moved to src/styles/ so the file layout matches the specifier the package has always exported (ambientui/styles/ambient.css), which resolved only through the SPA's alias beforeA control that looks scrubbable and is not is a worse lie than no control. One component, two honest affordances, decided by what the host can actually do
2026-08-29The demos move first, and become noindex routes (Phase 2). /demo/canvas and /demo/devtool are real prerendered pages under a layout carrying robots: index:false, follow:false — so a new demo is excluded by BEING a demo rather than by someone remembering. That meta tag is not belt-and-braces: a GitHub project Pages site cannot serve an effective robots.txt (crawlers read the org root, which is not ours), so it is the only exclusion that works, and it is verified in the emitted HTML. They were ported first on purpose: nobody indexes them, so they are the cheap place to establish the two patterns every later surface reuses — the "use client" boundary and dynamic({ ssr: false }) for what genuinely begins at mount (the canvas backdrop seeds itself with Math.random). Their components moved to apps/site/src, their final home, with the SPA borrowing them through a new @site/* alias: one copy, in its destinationThe demo surfaces were the right first port precisely because getting them wrong costs nothing. Every pattern they establish is now paid for
2026-08-29The export build found what the checker could not see (and then the checker learned to). The first /demo/* build died on window.innerWidth in orb.tsx's render body — a hazard inside the PACKAGE, which check-prerender.mjs was not scanning because it only knew about app code. A static build renders the layer's own components too, so the checker now covers packages/ambient, packages/foundation and packages/ui (87 files, up from 37) and immediately flagged a second: the Foundation's readSaved relied on its malformed-data try/catch to swallow the missing-storage case. Both now guard explicitly and say why. Also confirmed live: the site resolves packages through dist, so a stale dist builds the OLD code — the first fixed build still failed until the packages were rebuiltThe layer's whole claim is that it renders anywhere. "Anywhere" now includes a build with no DOM at all, and the package was never asked that question before
2026-08-29The prose becomes HTML (Phase 3, and the first real SEO payload). ?raw became a build-time readFileSync (read-doc.ts, import "server-only" so a client import is a BUILD error, and the repo root resolved from the module rather than process.cwd() — cwd differs under turbo and bare npm, which is a build that works locally and fails in CI). The markdown renderer split into a pure parseMarkdown and a hookless renderer, which is what makes the documents SERVER components: 18 static pages now, including /architecture and eleven /docs/[slug]. Measured in the emitted HTML: 41,649 characters of the paper and 245,264 of DESIGN.md, each with its own title and description — all of it previously invisible behind a ?view= param on a single-document SPA. The paper's title and lede are DERIVED from the file so they cannot drift; the eleven doc descriptions are the summary lines that had been sitting unused in the metadata all along. The renderer also emits real heading levels, where the SPA flattened every heading to h2 and threw the outline awayThe parse was a useMemo, so it could only happen where React was running. That one fact was the whole reason 1,400 lines of argument could not be read by anything but a browser
2026-08-29The check the migration exists for (scripts/check-static-html.mjs, in the gate). A static export can complete, look perfect in a browser, and still ship empty documents to a crawler — every page wearing the layout's title, its content promised to hydration. Nobody would notice, because a browser is the only place anyone looks. So each indexed page must carry its OWN title, its own description, and ≥400 characters of real prose; demo and 404 pages are asserted to be the opposite (noindex). Proven by stripping a built page's body and watching it fail. index.html is listed in NOT_YET with the phase that ports it — the same contract as NOT_DISTRIBUTABLE: an exemption names its reason and the list must emptyEvery other check in this repo guards something a compiler could almost see. This one guards the difference between a site that exists and a site that is findable, which nothing else in the toolchain has an opinion about
2026-08-29The component reference becomes 51 real URLs (Phase 4). Every documented component now has its own prerendered page — the whole long tail that was previously unreachable behind a ?c= query parameter on one document, with no link, no URL, and no way in except the rail's JavaScript. /ds is the CRAWL HUB that never existed: static inbound links to all fifty. The catalog split pays off exactly as intended — the pages are SERVER components reading catalog.ts alone, so the indexable prose is not hostage to fifty playgrounds each being SSR-safe; a regressed playground costs a demo, never a page. Indexed pages went from 13 to 64, each with its own title and description drawn from the descriptions that were already writtenThe reference has always been the most useful thing here and the least findable. Being documented is now the same act as being discoverable
2026-08-29The install command is read from the registry, never composed (Phase 4). Printing npx shadcn add @ambientui/<id> for each component would have been one line and confidently wrong: command-palette is documented and deliberately NOT installable, and the eighteen product entries are shadcn's own primitives. registry-facts.ts asks the built registry which names exist and prints a truthful note otherwise — verified per page: a real command for reasoning-panel, "ships inside the ambient layer" for command-palette, "install it from shadcn" for badgeThe registry already knew the answer. Every place the site restates something the build knows is a place the site can lie, and a wrong install command is the most expensive lie a docs page can tell
2026-08-29The landing page becomes static HTML — and the exemption list empties (Phase 5). The overview, the install section and their leaves moved to apps/site, and / now prerenders: 7,262 characters of real prose in the emitted file, containing the wordmark, the statement, all four principles, the forms section, the actual install command and the footer. This is the property the whole migration rests on — output: "export" prerenders CLIENT components too, so a fully interactive tree still ships its text, as long as nothing is gated behind mount. Verified in the browser against the built export: 6 demo windows, the film running, five live shader canvases, no console errors. NOT_YET in check-static-html.mjs is now empty, which was the point of writing it as a list that must empty rather than a threshold that could be loweredThe page that argues for the whole project was the last one a crawler could not read. 65 indexed pages now, from one
2026-08-29The overlap ends for the home surface, deliberately (Phase 5 consequence). Sharing files between two apps worked while every shared file imported only PACKAGES — the demos and the doc metadata moved that way without incident. The landing-page cluster broke it: those modules use the @/ alias, which resolves to a different directory in each app, so one file cannot be compiled by both. Rather than teach the SPA to resolve two roots, the shared LEAVES (seek-bar, command-line) dropped their app alias for the package specifier and stayed shareable, and the SPA's overview tab now points at the static page instead of maintaining a second copy of itThe alias was the boundary all along. A file is portable exactly as far as its imports are, and @/ is a promise about a directory that only one app can keep
2026-08-29The cutover: the static site IS the site, and apps/web is gone (Phase 6). The deploy builds apps/site with AMBIENTUI_BASE_PATH (no trailing slash — Next's shape, unlike Vite's), publishes out/, and asserts the export is indexable BEFORE uploading. Two traps handled explicitly: .nojekyll stays, because Next emits everything into _next/ and Jekyll eats underscore directories; and cp index.html 404.html was DELETED, because it would overwrite Next's own 404 and silently restore the SPA-shell fallback — every unknown path booting the whole app instead of saying not found, which is precisely what this migration undid. Verified against the real artifact served under /ambientui/: every route 200, the registry at /r/registry.json, no doubled slashes, and 404.html present and different from index.htmlThe riskiest step was the one with the least to write. Everything that made it safe was built in the five phases before it
2026-08-29What deletion forced the checkers to admit (Phase 6). Removing the SPA broke five gate scripts, and each break was a mechanism claim that had quietly expired. check-system-docs was verifying described ↔ LOADED ↔ on disk, but the ?raw half no longer exists — it now checks described ↔ on disk, which is the guarantee that actually needed rescuing from ?raw in the first place. check-theme-boot compared a key restated in index.html; a TypeScript module can IMPORT it, so the copy was deleted outright and the check now asserts the copy stays gone — removing a class of bug rather than detecting it. The generated registry-facts.ts went too: the site reads registry.json directly. The bare-mount harness moved to packages/ambient/dev/, where a route cannot silently wrap it in the very providers it exists to do withoutA checker that survives a refactor unchanged is usually checking the wrong thing. Deleting the app was the audit
2026-08-29The SEO finishing pass (Phase 7). metadataBase plus a per-route canonical on all 72 pages — a WRONG canonical is worse than a missing one, since it tells a crawler the real page is elsewhere, so every page names itself and the gate verifies it. sitemap.ts is DERIVED from the same catalog and document metadata the pages and the registry read, so a component enters the sitemap because it was documented (rule 10, one step further); 72 URLs, matching the 72 indexed pages exactly. robots.ts ships with an honest comment that it is decorative on a project Pages site — crawlers read the org root — and that the noindex meta tag remains the only working exclusion. JSON-LD per page type: WebSite + SoftwareSourceCode on the home, TechArticle on the paper and documents, BreadcrumbList three levels deep on a component. Both sitemap and robots needed dynamic = "force-static", which is output: export stating plainly that it has no serverStructured data is unusually easy to lie in and unusually hard to notice, which is why SearchAction is deliberately absent: there is no search endpoint, and declaring one would be a fabricated capability in a machine-readable format
2026-08-29The rail comes back, on real links (Phase 7). The first cut of the static /ds traded the SPA's navigation for a list on one page — a genuine regression, named at the time rather than left to be discovered. It returns as a client component over next/link: real hrefs a crawler follows and a reader can middle-click, usePathname marking position, folding kit groups with counts, and a search that filters the SAME catalog the pages are generated from — so it can never offer a component that has no page. The gate grew with it: check-static-html now also verifies every canonical points at its own page and that the sitemap lists exactly what was built, both proven by breaking them on purposeThe static rebuild was allowed to lose this for one phase because it was written down as lost. An unrecorded regression is the one that ships
2026-08-29The orb does not exist until it is mounted (found by running the dev server and reading the one warning it showed). Its position is a function of the viewport, which a prerender does not have — so the guessed fallback coordinates I added in Phase 2 produced markup the client immediately disagreed with: a hydration mismatch about the one element whose placement is meaningless without a window. It now renders nothing until mounted, via useSyncExternalStore with a server snapshot (the useIsMobile pattern — no state, no effect, no cascading render). Nothing indexable is lost: the orb is a control, not contentThe guard that stopped the build crashing was not the same thing as being correct. "It builds" and "it hydrates" are two different claims, and only the second is visible by opening the page
2026-08-29Typecheck and lint now depend on the packages being built (turbo.json). The site consumes @ambientui/* through their exports maps, from dist — so typechecking it against a stale dist typechecks yesterday's package. That produced two rounds of phantom errors (ContextChip "not exported", useFoundation returning never) that were nothing but staleness, and would have produced them for any contributor after any package edit. dependsOn: ["^build"] on both tasks, and the build's outputs now include Next's out/ and .next/ so the cache knows what it producedThis is the cost of the Phase 1 decision to resolve packages the way a real installer does, and it is worth paying: the alternative is a site that typechecks against source it does not ship
2026-08-29The site menu went missing in the cutover, and comes back in the layout (user report: the whole nav is gone — correct, and my error). view-menu.tsx was moved to the site; site-menu.tsx was not, so deleting apps/web deleted the switcher, and no check noticed because the HTML gate asserts titles, descriptions and prose — none of which a missing nav affects. It is restored as a client component deriving its active segment FROM THE URL rather than tracking it, which is what a static site requires: a visitor arrives at any page directly rather than navigating to it. It now lives in the ROOT LAYOUT (SiteChrome), so a new route cannot forget to render it — which is exactly how it disappearedThe gate checked what a crawler reads and nothing about what a person uses. A page can carry a perfect title, description and canonical while having no way out of it
2026-08-29The providers move to the root, because the site's own chrome needs them (consequence of the above). Mounting the nav in the layout made it throw: ViewMenu reads motion roles, and the Foundation was mounted per-surface. Theme, tooltips and the Foundation are now SiteProviders in the root layout — the Foundation IS the site's theme, not a demo's — and DemoProviders shrank to just the assistant context, for the surfaces that actually mount the layerThe provider tree had been shaped by the order things were ported, not by what they are. The first component that belonged to the site rather than to a page exposed it
2026-08-29The document scrolls again (user report: the scroll across the whole site is not working — correct). theme.css still carried html, body { overflow: hidden }, a desktop-app shell rule that froze every page at one viewport, including a 40,000-word article. Phase 0 named this rule as the thing to delete and it survived the move. The surfaces that genuinely want a locked viewport lock a WRAPPER instead, in the demo layout — a global rule cannot know which kind of page it is on, and a layout canThe rule was written for an app whose panes scroll internally, and it outlived that app by one migration. A stylesheet moved as a file keeps every assumption it was written under
2026-08-29The pre-paint theme script disappeared from the build, twice, silently (found while chasing dev-mode hydration warnings). First: next/script with beforeInteractive — the tidier-looking answer — simply declines to emit into a static export. Second, after reverting: the boot string is built by the SERVER layout, and it imported its key from theme-provider.tsx, a "use client" module, so the constant resolved across a client boundary and rendered an empty string. Both times every page looked perfect and every load flashed. The constants moved to theme-constants.ts with no directive, and TWO checks now hold it: check-theme-boot fails if that file ever becomes a client module, and check-static-html asserts the script is in the emitted <head>The thing that keeps breaking is the same thing each time — a value crossing a boundary it was not built to cross. Checking the SOURCE was not enough; the check that matters reads the artifact
2026-08-29The dev overlay's warnings were dev-only (worth recording, because they cost an hour). React 19 warns about <script> tags rendered in components and reported a hydration failure on every page. The shipped export has ZERO console errors — verified by loading the built artifact rather than the dev server. The one real bug the chase surfaced was the missing boot script above, which the warnings did not mentionA dev overlay reports what the framework dislikes; the artifact reports what a visitor gets. When they disagree, the artifact is the one with users on it
2026-08-29Door 4 opens: the Foundation splits, and the dependency inverts (launch). foundation-context.tsx was 821 lines in which ~640 lines of pure DATA sat behind a "use client" directive and a React import they never needed. Now: tokens.ts (the configuration space — every dimension, plus compileFoundationCss, no React and no imports at all), foundation-context.tsx (the live provider over it), ambient-bridge.tsx (the binding to the assistant), index.ts. The bridge moving OUT is the substance: mounting it inside the provider meant taking a radius scale also took an entire ambient layer. ambientui and @ambientui/ui became OPTIONAL peers, so npm i @ambientui/foundation now pulls React and nothing else — which is what door 4 always claimed and was not trueThe dependency did not point the wrong way by accident; it pointed that way because both things lived in one file, and a file is a dependency you cannot see
2026-08-29Four registry items, and two classes of publish bug made impossible (door 4). foundation-tokens (registry:lib), foundation-theme (the token bridge as CSS + cssVars), foundation (the provider block) and foundation-ambient-bridge (for hosts that took both doors). Two guards were added because both bugs were live: (1) stage() now REFUSES to publish a file still carrying a workspace specifier — the missing ambientui/ rewrite would have shipped an unresolvable import, and that failure only happens in a repo that is not this one; (2) the Foundation's three files sit together here and land in three DIFFERENT directories there, so their relative imports are rewritten to the declared targets. stageCss strips the repo-shaped @source globs and the vendored shadcn base, which would double-declare :root against a consumer's own shadcn init. Its assertion first failed on this file's OWN comment explaining why @source is absent — checking for a mention rather than a directiveThe registry has been publishing for weeks with a rule missing that nothing could detect, because the only symptom appears in someone else's project. A byte scan turns the whole class into a build failure
2026-08-29A command is not printed until something has run it (verify:install). The gate has always proved registry.json is internally CONSISTENT; it never proved an install WORKS, and the two bugs that reached production last week — the missing ambientui/ rewrite and the orb's shader shapes never shipping — were both invisible to every check here because both only fail in someone else's repo. So a committed fixture (fixtures/consumer, a plain Vite + React 19 + Tailwind v4 project) is copied to a temp dir, the registry is rebuilt against a localhost host, a separate process serves it, and the real npx shadcn add runs for all eight doors. Then: every declared target landed, no workspace specifier survived, cssVars reached the consumer's stylesheet, tsc clean, vite build clean. Four failures found on the way were all mine and not the doors' — an in-process server that could never answer because execFileSync blocks the loop, a HOST/r/ path mismatch, and two things a real shadcn init writes that the fixture lacked. Deliberately NOT in npm run gate: a two-minute pre-commit hook gets disabled, and then the gate is advice. It runs on demand, in CI on package changes, nightly (shadcn, Tailwind and Radix move underneath us), and in deploy.yml before the site is published. Only after it went green did the landing page's fourth tile stop saying "still being built" and start printing npx shadcn add @ambientui/foundationinstall-section.tsx has carried the comment "ONLY COMMANDS THAT RUN" since it was written, and it was a promise a human had to keep. It is now a script. The difference between a rule and a check is who notices when it lapses
2026-08-29npm i ambientui worked; the line after it did not (npm packaging). Every package's exports map began at ./* with no "." entry, so import { Assistant } from "ambientui" failed with ERR_PACKAGE_PATH_NOT_EXPORTED — while the README had been printing npm i ambientui for months. Nothing here could catch it: the site and the packages import deep specifiers exclusively, so the one form a stranger writes first was the one form never exercised. Each package now has a src/index.ts barrel and a "." export, and check-package-roots BUNDLES those three imports on every gate run. It bundles rather than running them under Node deliberately: bundle: false emits extensionless relative imports, which Node's resolver rejects and every bundler accepts, so these are bundler-targeted ESM and bare Node would fail for an unrelated reason. Separately, the five icon libraries left dependencies for optional peers — a consumer picks ONE library in the Foundation and was installing all fiveThe export map described how this repo imports the packages, not how anyone else would. Every convenience the authors do not use is a convenience nobody tests
2026-08-29The $comment asking humans not to let two files diverge had already failed (tokens.json). It said the JSON and the code must not disagree; the code shipped 18 accents and the JSON listed 17, and meta.implementedBy still pointed at apps/web/, deleted three commits earlier. check-tokens-json now compares the VALUES on every gate run — accents, grays, fonts, icon libraries, the radius ramp, scaling, spacing units, motion roles, characters, paces, and the defaults — by importing the built dist/tokens.js, which it can do only because that module is pure data with no imports at all. NOT a generator, which the plan called for and I did not build: most of this file is prose that exists nowhere in the code (why the ramp is Tailwind's, which steps were retired and why, what each motion role is for), and generating it would delete the reason it is worth reading. Writing the checker also found three bugs in the CHECKER — RADIUS_NAMES is the sliding window, not the ramp; MOTION_ROLES is an array of objects; the config stores a radius INDEX where the JSON records pxA comment is not a mechanism. This one was specific, correct, prominently placed, and wrong within a month. The version that holds is the one that fails a build
2026-08-29The harness ran a command nobody publishes (found by a question: is npx shadcn add @ambientui/ambient-layer npm or shadcn?). It is shadcn, and the @ambientui/ is a REGISTRY NAMESPACE — a local alias in components.json mapping to a URL template, written by shadcn registry add. verify-install was calling the URL form (${HOST}/r/<name>.json), which proves an item is fetchable and proves nothing about the line printed on the site. The fixture now commits the namespace mapping with a {REGISTRY_HOST} placeholder the harness rewrites, and the harness runs @ambientui/<door>. Verified by deleting the mapping and watching it fail, because a check that cannot fail is not a check. NOTE THE COLLISION this leaves: @ambientui/foundation is simultaneously a valid npm package and a registry reference, so npm i @ambientui/foundation and npx shadcn add @ambientui/foundation are both real commands that do entirely different thingsI verified the mechanism and not the artifact — the URL the site RESOLVES to rather than the string it PRINTS. The same class as every other bug this week: the thing that was true here was not the thing a stranger runs
2026-08-29The /docs route is removed; the documents live on GitHub (user decision). Twelve indexed pages go with it — the hub and eleven governing documents — taking the sitemap from 72 to 60, which is the largest deliberate reduction in reach this migration has made and was made with that number stated. Everything that pointed there now opens the file on GitHub through one docUrl() helper: the /ds rail (rows marked external, because a rail that navigates off-site without saying so is lying about where it goes), the /ds index, and the paper's inline document links. read-doc.ts and markdown-parse.ts stay — /architecture still renders PAPER.md at build time — so nothing was orphaned by the deletionRendering a document the repo already publishes means keeping two copies in step forever. The blob view carries history and blame, which for a governing document is most of what makes it worth opening
2026-08-29The /ds rail could not scroll, and had never been able to (user report, correct). It carried overflow-y-auto while its parent was min-h-svh, so it stretched to the height of the whole document and there was never any overflow to scroll — the search box simply left with the page. h-svh plus sticky top-0 gives it something to overflow and somewhere to stay. This is the second bug from the same root as the site-wide scroll fix: the old shell got a locked viewport free from overflow: hidden on body, and every layout written under that assumption broke quietly when it was removed, in a way that looks like nothing at allAn overflow property with no height to work against is decoration. The rule had been there the whole time and had never once done anything
2026-08-29One shape for every list in the rail (user direction, with a reference). Component lists came in two forms: loose rows under a section label (Core, the shadcn primitives) and collapsible groups with counts (Messages, Tool use, Knowledge) — the same kind of thing wearing two shapes, so the rail read as if the loose rows belonged to the heading and the groups were something else. Now every list is a Category: chevron, count, children on an indent rail; section labels dropped uppercase mono for sentence case, since at that size the tracking made the signposts harder to scan than the rows they organise. Categorising immediately hid the current component behind a closed summary, so a category opens itself when the page you are on is inside it — a rail that cannot answer "where am I" has lost its jobTwo shapes for one kind of thing is a claim that they differ. Nothing in the catalog justified the claim; it was only the order the groups were added in
2026-08-29The view pill separates its kinds, and names its one shortcut (user direction). It held three sorts of control in one unbroken row of circles: the project's own pages, demos of it, and appearance, which is not a destination at all. A thin rule is now drawn wherever the item's group changes — supplied by the app, so the component renders a seam rather than hardcoding an index. The appearance tooltip states its key, read from THEME_TOGGLE_KEY, the same constant the handler binds: a tooltip that advertises a shortcut nobody wired is worse than no tooltip. Verified by pressing it. (The binding already existed and had never been shown — plain D, ignored while typing.) Also removed: the dev-tool demo's "Review with ambientui" and "Submit review" buttonsThe pill's own doc comment already said appearance "is the one control here that is not a destination". It said so in a comment while rendering it identically to the destinations
2026-08-29The home page says what it is built on, before it says what to type (user direction). A new section between the forms and the commands: design architecture named as the framework the Lumenridge team develops here, the Foundation as a configuration space rather than a stylesheet, and the Figma engine as a one-way projection. It carried the accent, gray, radius and motion counts read live from tokens.ts — honest, and removed anyway (2026-08-30, user direction): they answered a question nobody arrives with, since a stranger meeting the phrase "design architecture" needs the idea and the exact size of the space is what /ds is for. The live-read was the right way to show a number; the section did not need one. The sync claim is drawn with the schematic kit rather than written, because the whole point is direction and a picture with no arrow coming back is harder to disbelieve than a sentence saying soThe paper made this argument at length and the site repeated none of it where a stranger would see it. A visitor could install an assistant without once learning why it matches their product
2026-08-29/ds IS the Foundation (user direction). The index page went: fifty component links, seven tools, the documents. It existed because on the SPA nothing in static HTML linked to a component, and the rail has since taken that job — CHECKED in the emitted HTML, where every /ds page already carries all fifty inbound links, rather than assumed. What remained was a page whose only content was a duplicate of the navigation beside it, standing between the visitor and the thing they came for. /ds/foundation was deleted rather than kept alongside, since two URLs for one page is the duplicate content this migration exists to remove; canonical is /ds, and the sitemap drops the tool entryThe paper already said it: "/ds with no selection IS the Foundation." A comment in the code said so too. The routing was the last place still disagreeing
2026-08-29The demo stops staging a code review it cannot finish (user direction). Removed the dev tool's "Review with ambientui" and "Submit review" buttons and the inline ReviewComment thread. Removing the thread orphaned more than itself: resolving was only ever set by its reply handler, so the handover-highlight path became permanently dead, and the review fixture on every file became data nothing read. All of it went, rather than leaving a state variable that can no longer change and a highlight that can no longer appear. The ReviewComment component itself is untouched — it is documented vocabulary with its own storiesDeleting the visible thing is the easy half. What it was feeding is the half that quietly stays and reads as working code
2026-08-29A context-contract section was built for the home page, then removed (user direction, both ways). It ran the layer's central claim as a loop: a product moving through four beats — page, page, file, a selection inside that file — with the chip following each one, placed before the six forms. It came out on the user's call after seeing it in place. Recorded rather than quietly dropped, because the useful part outlived it: writing it forced the audit in the row below, and it surfaced two of my own violations (a comment claiming it called setPageChip when it never did, and two arbitrary values in a file arguing for token discipline). The home page still asserts the contract in one sentence under a screenshot; whether that is enough is now an open question rather than an unexamined oneA section can be correct, compile, and still be one section too many for a page that already asks a lot of a first-time reader
2026-08-30The context audit, corrected: the site has ONE real violation, not fifty-nine. The first pass counted every page that never calls setPageChip and reported 59 of 65 breaking DESIGN.md §8. That number was misleading, and I gave it to the user before checking the thing that decides it: the ambient layer is mounted on /, /demo/devtool and /demo/canvas and NOWHERE ELSE. /architecture and all 57 /ds routes host no assistant, so there is nothing there for a page to declare itself to — and /architecture's existing setPageIntel call is inert for the same reason. The single genuine violation was /demo/canvas, which mounts the layer and tells it nothing; it now declares. The fix arrived as one component, DeclareContext, rather than a hook-effect-cleanup trio each page re-implements — the clearing on unmount is the part a page hand-rolling this forgets, and a chip that outlives its page has the assistant answering confidently about a surface the user already left. Whether /ds should mount the layer at all is a product question, left openI audited for a missing CALL and reported a number, when the question was whether the call had anywhere to land. A count is not a finding until you know what it counted
2026-08-29The anatomy becomes a list, and the CTAs become Buttons (user direction). Twelve files meant to be read in order were rendered as a two-column card grid, which turns an inventory into a gallery: the eye picks a card instead of reading down. Rows on a rule keep the reading order the source table had. The "View on GitHub" affordance and the overview's "Read the architecture" were hand-rolled <button>/<a> elements with their own hover classes, and are now the sanctioned Button — including one I had written in the design-architecture section an hour earlier, which is how quickly a near-miss appears when the composing surface is newRule 4 forbids re-implementing a near-miss of an existing component. The violation was not old code; it was code from this same session
2026-08-30The pill offers the overview and nothing else on mobile (user direction). The other four destinations are built for a width a phone does not have: a 40,000-word essay with a contents column, a three-pane component reference, and two demos whose entire subject is a layer moving around a desktop workspace. HIDDEN BY CSS, NEVER PRUNED FROM THE DOM — hidden sm:block on the wrapper, so all six hrefs stay in the emitted HTML and mobile-first crawling still follows every one of them. Removing them in JavaScript would have been the obvious move and would have quietly cut the site's internal linking for the crawler that matters most, five days after the migration existed to build that linking. The rule that opens a fully hidden group hides with it, or the pill shows a divider with nothing on the far sideThe accessible answer and the indexable answer were the same answer here, and both differ from the one a useIsMobile reflex reaches for
2026-08-30The five form sections show the component, not a product around it (user direction). Each rendered a browser window — traffic lights, title bar — with the fake dashboard at 60% behind the surface. That framing belongs to the film at the top, whose claim is "this is your product and the layer is living in it". These five answer a narrower question (what IS a dock), and a dashboard under the answer is the loudest thing on screen while being the one thing the section is not about. THE BOX ITSELF STAYS, and is not decoration: the layer's surfaces are position: fixed, and a transformed ancestor is what makes them fixed to the frame rather than to the viewport — delete the box and the dock docks to the browser. It is now a dashed hairline with no ground of its own, since a card behind the glass would be the product shell again under another nameThe chrome was carrying a claim the section had stopped making. It took removing it to notice the frame was doing two jobs, only one of which was visual
2026-08-30The overview's demos were unusable on a phone, and the frame was the reason (user report, with Linear's mobile site as the bar). At 375px the demo frame measured 292 × 165 — 16:9 at that width — and no surface with a header, a message list, a context row and a composer fits in 165px. The layer had sized itself to the frame correctly; the frame was wrong. It is now full-width and 640px tall below sm (h-160), which is not a guess: the panel is 560px and sits 16px off the bottom edge, so any frame under 576px cuts it however the type is tuned, returning to the wordmark measure and 16:9 above it, and the measured width rides a custom property because an inline style cannot carry a breakpoint. The page's rhythm came with it: a 30px statement on a 375px screen, and vertical gaps tuned against a 1200px column. NOT SCALED DOWN — Linear's mobile hero keeps the product at real size and lets it bleed off the edge, and a transform-scaled miniature would have traded readable text for a faithful thumbnail nobody can read. The CSS sentinel check then failed on .aspect-video, correctly: the class had become sm:aspect-video, and a VARIANT CANNOT BE A SENTINEL because the parser stops at the colon — it would have failed forever for a reason unrelated to scanning. Its replacement was the frame's mobile height, which broke again on the very next adjustment; the sentinel is now transform-gpu, structural rather than a number under active tuningThe check earned its place twice in one change: it caught a real rename, and trying to fix it the obvious way would have wedged it permanently
2026-08-30The quick-ask row assumed a desktop, in the layer itself (user report: cut off on mobile). QUICK_W was a flat 420, and the open row is the pill PLUS the record button 8px beside it — 480px — so on any host narrower than that the button was sliced by the edge. Reported against the overview's 327px demo frame, but the same arithmetic fails on a real phone: this was a bug in packages/ambient, not in the page embedding it. The width now takes the room it has (w is already the frame's width when embedded and the viewport's when not, so one expression serves both). FIXING THE WIDTH WAS HALF OF IT — the position clamp still only reserved space for the pill, so the pill sat inside the bounds and pushed the button out, and the measurement after the first fix showed the row still overflowing by 18px. The clamp now reserves the row, on whichever side the button takes. Verified at 375px: pill 12→303, button 311→363, 12px clear on both edgesThe first fix made the reported symptom smaller and left the bug in place. Measuring the result rather than re-screenshotting it is the only reason that was caught
2026-08-30vw describes the viewport, and an embedded layer does not own the viewport (user report: the panel still cut on mobile; they proposed scaling the frame's contents). The panel capped itself with max-w-[92vw], which inside a 327px demo frame on a 375px phone resolves to 345px — 18px over the edge, and no amount of frame height fixes a width. The same family as the quick-ask's flat 420: a number that was true on the author's screen and false in the frame it was handed. A ResizeObserver on the probe's offsetParent now gives the surfaces their FRAME's box, and the panel and dock cap against it; when the layer is not embedded that offsetParent is null, the state stays null, and the viewport units remain correct with nothing overriding them. SCALING WAS THE OTHER OPTION AND WAS DECLINED: transform: scale() on the frame's contents would have fixed every clipping bug at once, and traded 14px text for 9px to do it — the surfaces are the subject of these sections, and an unreadable faithful miniature is worse than a readable real one. Verified: panel 298px inside a 327px frame, 15px clear on both edgesTwo clipping bugs, one cause, and the cause was a unit. The layer measures its containing block for POSITION already; it was still sizing itself against the screen
2026-08-30The wordmark's heat runs on its own clock, disconnected from the assistant (user direction: the orb should loop across all states, without the connection to the original orb). The §12 row of 2026-08-27 made the wordmark ride the layer's real orbState, on the argument that the name and the assistant are one organism. In practice the page's own assistant is not the one the demos drive, so for almost every visitor the identity at identity scale sat in still for the entire visit — a mark that only animates when nobody is looking at it. useLoopingOrbState now cycles the four states forever with a per-state dwell (thinking needs length to read as churn, answer is a release and reads short), chained timeouts rather than one interval so the timer cannot drift out of step with the state on screen. THE GROUND FIELD STILL TRACKS THE REAL STATE, so nothing that was actually reporting has stopped reporting — the wordmark is now a mark, and the field is still a readout. Verified by reading OrbHeat's committed props over 17s: all four states observedThe earlier decision was right about what the coupling MEANT and wrong about what it would do. A status display whose status never changes is indistinguishable from a broken animation, and the visitor cannot tell which they are looking at
2026-08-30"Make it yours" becomes its own section, ahead of the argument it used to close (user direction, with the design-architecture opening as the reference). It was a closing note under the design-architecture body — smaller type, behind a rule, the shape a page uses for a footnote — and it is not a footnote: everything around it argues about configuration, and it is the live configuration. It now stands alone in the same composition as the design-architecture opening (eyebrow, display title, one centred line, one Button) so the two read as siblings, and it stands BEFORE that argument, because the case for configuration is more convincing to someone who has already watched the site re-theme itself. The copy shortened in the move: a paragraph that was fine at body size is a wall at display sizePosition was carrying an argument the prose was not. Ordering the invitation before the explanation costs nothing and spends the moment when a visitor has just watched the layer work
2026-08-30Two third-party brand marks enter the codebase as hand-drawn SVG — the only exception to rule 6 (user direction: add the official Tailwind and shadcn logos with a line about building on the stack you already use). Rule 6 says icons come from <Icon name> and nothing else, and that rule is about the ICON VOCABULARY — semantic names the Foundation's configured library draws. A third party's logo is not a semantic name: Tailwind's wave and shadcn's slash are fixed artwork owned by somebody else, and mapping them across all five icon libraries would be claiming they are ours to redraw. They live in make-it-yours-section.tsx rather than in the icon set for exactly that reason, and any further brand mark is a governance event rather than a precedent. MONOCHROME, IN currentColor AT THE MUTED ROLE: Tailwind's mark is officially cyan, but a hex in component code is the thing this repo exists to prevent, and one muted weight reads as provenance rather than as two badges competing with the display title above themThe exception is narrow on purpose. "It is an SVG in a component file" is the exact shape of the violation rule 6 forbids, so the thing that makes this legal has to be written down where the next person will look for it
2026-08-30/ds/form-factors shows the five forms instead of claiming to switch them (user report: none of the buttons are working). Each row carried a Switch button calling the page's setMode, under a caption saying it drove the real assistant. The buttons were NOT broken — the state changed and the label flipped to Current — but /ds mounts DemoProviders, which supplies the assistant CONTEXT without mounting the Assistant, so the mode was read by nothing. Every part worked except the one that would have been visible, which is why it survived a review: a control whose only failure is that nothing happens looks identical to a control nobody pressed. Each form now carries its own framed layer instead — a real assistant, held in that form, that a visitor can type into. ONE LAYER PER SECTION, NOT ONE PER PAGE: a global assistant can only be in one form at a time, so a page whose subject is five coexisting forms would make the reader flip back and forth to compare two of them. DemoWindow moved out of overview-view.tsx to components/demos/ to serve both pages, with the fictional product's caption becoming a prop, and the frames carry the overview's 300px mount gate for the same WebGL-context reason. Quick ask gets no frame: it and Orb are both line, and the pill is reached by clicking the orb the Orb frame already showsThe caption was the actual bug. Code that does nothing visible is a bug you find by using the page; prose promising it does something is a bug that survives until someone believes it
2026-08-30The share cards described the site, not the page (user direction: make sure the OG text explains every component). Two faults, both invisible to the existing checks because the fields they check were all correct. (1) SEVEN PAGES WORE THE LAYOUT'S CARD: Next replaces openGraph rather than deep-merging it, so /ds and the six Foundation pages — each with a correct <title> and a correct meta description — shared a card reading "ambientui, an AI layer that inherits your design system" and the site's pitch. A link to Motion advertised the homepage. Fixed structurally with pageMetadata() in lib/site.ts: describe a page once and the OG and Twitter fields derive from it, so the card is not something a page can forget. (2) ELEVEN COMPONENT PAGES HAD CARD TEXT UNDER 55 CHARACTERS. description is the VOCABULARY line, read by the AI composing from this system, and its terseness is correct beside the component — "Single-line text entry." explains nothing standing alone in a search result. The card now COMPOSES rather than the vocabulary being padded: the first whenToUse line is appended when the description is short, so Input reads "Single-line text entry. Free-form single-line values: names, emails, search queries." Already written, already reviewed, nothing invented, and the AI-facing text untouched. Descriptions that already explain themselves are left as their author wrote them rather than truncated to a number. check-static-html gained three assertions — a page may not wear the layout's og:title, two pages may not share an og:description, and a card description has a 60-character floor — each induced and watched to fail. Result: 59 of 59 indexed pages carry a unique, page-specific card, none under 82 charactersThe vocabulary line and the meta description look like the same sentence and are answerable to different readers: one to a model composing UI beside the component, one to a stranger who has only that sentence. Reusing one for the other was the bug, and padding the vocabulary to fix it would have been a worse one
2026-08-3063 pages declared a share card and none carried an image. twitter: { card: "summary_large_image" } shipped in the root metadata from the day it was written, and no page ever emitted an og:image — which renders WORSE than declaring no card, because the platform reserves the large slot and fills it with nothing. It stayed invisible because nothing in the repo renders a share card: the only way to see it was to post a link somewhere. The card is now the REAL hero, captured with Playwright at 1200×630 (the plan's own call — the shader is the brand, and a Satori lookalike would be a picture of something that does not exist), and THE FRAME WAS CHOSEN: the wordmark's heat cycles four states now, still is quiet by design, and a shot at an arbitrary moment catches the name flat grey. Two traps, both mechanical. (1) metadataBase carries the project-Pages base path, and a leading-slash image path resolves against the ORIGIN — /og-home.png becomes lumenridge.github.io/og-home.png, dropping /ambientui and pointing every card at a 404 that looks perfect in the HTML; the URL is written absolute. (2) Next REPLACES openGraph rather than deep-merging it, so the two routes that set their own title and description silently dropped the parent's image — that alone was 51 of the 63 pages, and the constant is now exported and spread at each site. check-static-html gained the assertion: a page declaring a card must carry both images, every image URL must sit under the site URL, and the file must exist in the export. All three failure modes were induced and watched to fail before the check was keptThe metadata had been "correct" for as long as it existed, in the sense that every field it declared was spelled properly. What it never had was a check that the fields agreed with each other. A card promising an image is a claim, and this repo's rule is that claims get executed
2026-08-30The skills stop being listed as governing documents (user direction: we do not need to show these, or give anyone access to them for open source). .claude/skills/* had four rows in the /ds rail and a row in the architecture page's anatomy table, both linking to GitHub. They are how WE brief an assistant working in this repo — a working process, not a contract anyone installing ambientui needs or is owed — and listing them invited strangers into the room where the work is planned, with nothing in that room they could act on. SYSTEM_DOC_META drops from 10 documents to 6 and the Skills group leaves the type; the files stay in the repo, unlisted. ONE EXPOSURE IS DELIBERATE AND WAS LEFT ALONE: the governance registry item still ships all four skill files to a consumer's ~/.claude/skills/, because that IS door 4 — "the rules your AI reads before it writes anything, and three reviewer roles it can take on" is the door's whole proposition, and quietly emptying it would leave the README and the install section advertising a command that installs one file. Closing that is a product decision, flagged rather than takenThe distinction that matters is between BROWSING and INSTALLING. Nobody needs to read our working process on a website; someone adopting the architecture may well want the roles in their own repo. The same four files, and the answer differs by which of those two things is happening
2026-08-30The orb was put back on the dot of the ı and removed again, same day (user proposal, then user direction on seeing it). The 2026-08-27 removal gave a specific reason — "two rendered characters on one page read as two assistants" — and the second character, the page-level resting orb, had itself been removed earlier today. So the stated objection had lapsed, and the dot went back with the looping heat state driving it. It came off again within minutes of being looked at. THE RECORDED REASON WAS NOT THE WHOLE REASON: with the competing character gone the composition still did not want a second body in it, which means the August entry captured a symptom and the actual judgement is about the wordmark itself — the name wears the identity's FIELD, and that is now settled by two independent viewings rather than by one argument. Treat the dot as closed. One thing was worth keeping from the attempt and is recorded here instead of in code: the August literal x=257 is dead, because in the font configured today the ı stem centres at 289 — anyone restoring this would park the dot 12px left of its own stem unless they measure getExtentOfChar the way the glyph width already isThe first removal's stated reason was falsifiable and got falsified, and the decision survived anyway. A log entry that records the argument is less durable than one that records the look — worth remembering when writing the next one
2026-08-30The shells scale their contents on narrow hosts (user direction, after I argued against it). DemoWindow lays its contents out at a laptop measure (1000px) and transforms the whole frame down to fit; the wrapper's transform becomes the containing block, so the layer's fixed surfaces scale with everything else and nothing inside needs to know. Above 1000px the factor clamps to 1 and NO wrapper is inserted at all, so desktop renders exactly as before — verified at 1400px. The frame also stopped being portrait: it had gone to 640px tall trying to physically contain a desktop surface, and scaling takes that job away, so it is one 16:9 window at every width again. THE COST IS REAL AND WAS THE REASON I RESISTED: at a 359px frame the factor is 0.357, so 14px body text lands near 5px. What changed my mind was seeing it — History's whole composition (sidebar, conversation list, answer, references, composer) is legible AS A COMPOSITION at that size, and it was simply impossible before; the words are in the prose above the frame anyway. The demo cursor stays OUTSIDE the scale: it reads real element rects and draws in real pixelsI was confident and I was half wrong. Cropping is better where the words are the point, and scaling is better where the arrangement is
2026-08-30The product shell returns behind the form surfaces, one change after it was removed (user direction both times, and both were right at the time). It was removed because a full-size dashboard crowded the surface it sat behind. With the frame scaling, that stops being true — and the version without it read as a large empty box with a small pill at the bottom, which says less about what a dock IS than a workspace does. Back at 60% opacity, the same recession as the film'sRemoving it was correct against a frame that could not scale. The fix to the frame changed what the right answer was, which is the sort of thing only re-looking catches
2026-08-30The design-architecture section loses its diagram (user direction). The schematic drew one store projecting into the app and into Figma, which was the right picture while the section was arguing about DIRECTION. The rewrite moved the argument to the data layer underneath both environments, and a picture of two arrows is a weaker version of a sentence that already says it. Removed with it: SyncSchematic and this file's imports from the schematic kit — the kit itself stays, since the paper still draws with itThe diagram outlived the claim it illustrated by exactly one rewrite
2026-08-30The overview page rewritten for someone who has never heard of any of this (user direction, via the product-copy skill). The page was written by people who already agreed with it: "a decluttering of enterprise software", "the system of record keeps every pixel", "a bounded configuration space, not a stylesheet", "one presence, many forms". Every one of those is accurate and none of them land on a first read. Now: the statement names the thing it is arguing against before the thing it proposes ("most products add AI by finding a spot for it"); the four principles lead with a verb about the reader's experience ("It knows where you are"); the five forms describe what you see rather than what they are for ("a small character parked at the edge of the page"); and the design-architecture section explains configuration through the moment it exists for — someone saying "make it feel like that". TERMINOLOGY HELD where the register requires it: orb, panel, dock, spotlight, history, Foundation. Simplifying is not licence to rename the things the AI composes fromThe copy was impact-first for a reader who already had the context. A stranger needs the situation first and the impact second
2026-08-30site-voice — the writing rules for a stranger, as a skill (user direction: make it repeatable). Distinct from product-copy, which writes strings for someone already inside the product, and from PAPER.md, which argues at length to a reader who chose to read. It records the five moves this session actually used — name the situation before the solution, lead with what the reader gets, describe what you see rather than what it is for, explain an idea through the moment it exists for, mechanism only after the problem — and, more usefully, the three things simplicity does NOT license: the terminology register stays (orb, panel, dock, spotlight, history, Foundation), no claim you cannot defend, and every number read from code. All three are failures I made or nearly made this session; the skill exists so the next pass does not repeat themThe rules were in my head and in a dozen commit messages. A skill is the same rules where the next session actually reads them
2026-08-30Six /ds pages shared one meta description word for word. "A Foundation dimension of the ambientui design system: what it decides, and every value it can take" was on colors, spacing, shadows, motion, translucency and form-factors — invisible on the page, and the single most-read sentence each of those URLs has, since it is what a search result shows. Each now says what its own page holds, in plain words, read off the page's own lede rather than invented. The site description and the OG line went with them: the OG line was "a design system has to become a bounded configuration space before an AI can safely build inside it", which is the paper's thesis stated to someone who has read the paperDuplicate descriptions are the copy equivalent of a hardcoded constant: one sentence doing six jobs, correct nowhere in particular
2026-08-30PAPER.md is deleted; /architecture is written instead (user direction). The page rendered the paper's real bytes, which had one excellent property — no second copy could drift — and one that had quietly become fatal: it spoke to a reader who had already committed half an hour, while the front door had just been rewritten for a stranger with eight seconds. Two halves of one site that no longer sounded like the same project. The argument is now architecture-content.ts: 15 sections in the site-voice register, 12 minutes instead of 32, serialisable and React-free so the renderer decides presentation and the file only knows what it says — the same split as catalog.ts. Not shipped in any registry item, so no consumer install changed. Recoverable from git if the shorter version turns out to be the wrong tradeThe unrenderable virtue of rendering real bytes was that the page could not drift from the file. What it could drift from was the rest of the site
2026-08-30Deleting the paper nearly deleted 1,250 lines of working UI with it. playbook-view was keyed to the paper's chunks by regex — four hand-drawn diagrams, two live demos, the anatomy list, the contents nav, all matched on /^## 9\. / and the like. The obvious git rm would have taken every one. Rewired instead: sections name a SLOT and the renderer fills it, so a diagram is attached to a section by name rather than by a pattern matching its markdown. Two components that parsed markdown structures were rewritten to read written data (AnatomyList from an ANATOMY array), and PipelineSchematic — orphaned when its markdown wrapper went — became the pipeline slot, since it is the schematic-kit redraw of the diagram it replacedThe demos looked like decoration on top of the prose. They were bound to its bytes, and only reading the file showed which half was load-bearing
2026-08-30The switcher moved on every navigation (user asked whether it jumps; it did). Measured: 343px wide on /, 378px on /ds, 363px on /architecture — the active segment expands to carry its label and the labels are different lengths. Because the pill is CENTRED, a width change moves both edges, so every icon in the chrome slid sideways on each route change, ~18px each way between the overview and the design system. The label box is now a one-cell grid holding every label stacked, with only the live one visible: the cell is as wide as the longest name whichever page you are on, so the pill is 378px everywhere and the expand animation on click is untouched. Verified identical width AND identical left edge on all three pagesThe animation was deliberate and the jump was not: growing a label inside a centred container quietly moves everything else in it
2026-08-30The home page stops wearing the layer (user direction). It mounted its own resting assistant, so the site demonstrated the thing by running it — a good argument, and also a character floating over the prose at every scroll position, in a page now dense with framed demos that each run their own. The orb belongs in the frames, where it is the subject; over the copy it was furniture. TWO LINES OF COPY WENT WITH IT: both handover lines promised "the page itself runs the same layer: press ⌘K anywhere", which stops being true the moment the mount is removed. Also reverted the switcher's constant-width label — sizing the box to the longest name stopped the pill drifting between routes and left "Overview" visibly loose in a box cut for "Design system"; hugging won, and the per-route width change came back with itRemoving a mount is one line. Finding the sentence that promised it is the work, and nothing in the build would have caught that one
2026-09-06The npm scope becomes @ambient-ui — the four scoped packages (@ambient-ui/ui, @ambient-ui/foundation, @ambient-ui/patterns, @ambient-ui/docs) rename from @ambientui/*; the unscoped ambientui package and the shadcn registry namespace @ambientui are unchanged. The npm org name ambientui was already held by an inactive account, and a scope is the one name a package cannot quietly change after its first publish — better renamed once, before any publish, than disputed after. The registry namespace stays @ambientui deliberately: it is our own alias resolved by a URL in the consumer's components.json, it matches the brand and the live domain, and commands were already printed and copiedA scope and a brand are different names with different landlords. The brand lives at ambientui.ai; the scope lives wherever npm's namespace has room

13. Pattern watchlist

(Patterns spotted but not yet ruled on — triage through governance.)

  • Foundation swatch picker (accent/gray circles with ring selection) — used twice on the Foundation page; if a third use appears, promote to a component.
  • Scale ruler / type ramp preview (Foundation → Scale) — candidate for the vocabulary once the Spacing page and Foundation stop being its only consumers.
  • Reveal scroll-entrance wrapper — PROMOTED 2026-08-27: the overview became the second page wanting scroll-staged sections, tripping exactly the condition named here. Now site:src/components/reveal.tsx (app-shared; vocabulary promotion is the next trigger, on a consumer outside this app).
  • DocDownload row (Playbook) — a Button handing over a governing file as a Blob of the same ?raw bytes /ds renders. Kept local; promote if the /ds doc reader grows a download affordance too.
  • WireframeShell frame (Playbook) — a dashed border-role frame with corner ticks and a mono uppercase tag; the article's device for framing a live demo as a blueprint. Kept local; promote if docs pages or /ds stories want the same "this part is a specimen" framing.
  • Schematic kit (site:src/components/home/schematic-kit.tsx) — eight SVG line-work primitives (nodes, fan curves, waypoints, leader annotations) for presentation diagrams, all strokes semantic roles. PRESENTATION-ONLY by explicit charter: not vocabulary, no /ds entry, and any product-surface use is a governance event. Interaction layer planned.
  • PlaybookContents / PlaybookNav (Playbook) — a grouped TOC of Collapsible entries whose open state follows the scroll position; fixed left column from lg up, in flow above the article below. Kept local; if /ds or a docs page wants a grouped scroll-tracking TOC, promote it (and note it overlaps SectionRail's job — the promotion should decide which of the two survives as the system's in-page nav).