diff --git a/apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx b/apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx
index c7956f4a7..193863efb 100644
--- a/apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx
+++ b/apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx
@@ -59,9 +59,9 @@ The whole protocol is seventeen events; you can hold it in your head.
The seams come first, before any code.
-
+
-Three boxes. Two seams.
+Two seams: the wire, and the contract.
**The backend.** Whatever you want, as long as it can emit AG-UI events.
LangGraph, CrewAI, Mastra, Microsoft Agent Framework, Pydantic AI, AG2, AWS Strands, Agno. They all have first-party or partnership adapters.
diff --git a/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx b/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx
index da7473909..2189bc5f6 100644
--- a/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx
+++ b/apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx
@@ -114,7 +114,7 @@ They describe different topologies.
LangGraph addresses a platform API and names a graph on it.
AG-UI addresses one HTTP endpoint that streams Server-Sent Events, and the endpoint is the whole address space.
-
+
One SSE route to allowlist, proxy, and monitor is a smaller operational surface than a platform API with a run lifecycle behind it.
Whether that simplicity is a feature depends entirely on the next section.
diff --git a/apps/website/content/docs/a2ui/getting-started/introduction.mdx b/apps/website/content/docs/a2ui/getting-started/introduction.mdx
index 0071a07a9..4f03d1fee 100644
--- a/apps/website/content/docs/a2ui/getting-started/introduction.mdx
+++ b/apps/website/content/docs/a2ui/getting-started/introduction.mdx
@@ -4,6 +4,10 @@
It does not render Angular components. It does not register handler functions. It does not decide how an agent should respond to a button click. Those jobs sit in `@threadplane/chat` and `@threadplane/render`.
+## How it fits
+
+
+
## What the package owns
The public entry point exports four groups of tools:
diff --git a/apps/website/content/docs/ag-ui/concepts/architecture.mdx b/apps/website/content/docs/ag-ui/concepts/architecture.mdx
index 25b961f85..b4778c981 100644
--- a/apps/website/content/docs/ag-ui/concepts/architecture.mdx
+++ b/apps/website/content/docs/ag-ui/concepts/architecture.mdx
@@ -4,23 +4,7 @@
The package takes an AG-UI `AbstractAgent`, listens to its protocol events, and exposes the runtime-neutral `Agent` contract that the chat components already understand.
-```text
-Angular component
- |
- v
-@threadplane/chat Agent contract
- | messages, status, isLoading, error, toolCalls, state, events$
- |
- v
-@threadplane/ag-ui toAgent()
- | reduces AG-UI events into Angular signals
- |
- v
-@ag-ui/client AbstractAgent
- |
- v
-AG-UI backend or in-process fake agent
-```
+
## The boundary
diff --git a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx
index 1504eb95f..c4b4bc279 100644
--- a/apps/website/content/docs/ag-ui/getting-started/introduction.mdx
+++ b/apps/website/content/docs/ag-ui/getting-started/introduction.mdx
@@ -14,19 +14,10 @@ The [AG-UI demo](https://ag-ui.threadplane.ai) runs this exact chat surface agai
## How it fits
-```text
-@threadplane/chat
- , ,
- @threadplane/chat/debug ->
- |
- | Agent contract (signals + events$)
- |
- +--> @threadplane/langgraph
- | LangGraphAgent -> LangGraph Platform
- |
- +--> @threadplane/ag-ui
- toAgent(AbstractAgent) -> AG-UI backend
-```
+
## What you get
@@ -57,3 +48,4 @@ Out of scope for now (use `@threadplane/langgraph` if you need LangGraph Platfor
- [Fake Agent](/docs/ag-ui/guides/fake-agent) - build UI without a backend.
- [Citations](/docs/ag-ui/guides/citations) - attach sources through `state.citations`.
- [Troubleshooting](/docs/ag-ui/guides/troubleshooting) - debug provider, stream, state, and citation issues.
+- [Chat Debug](/docs/chat/components/chat-debug) - inspect the raw event stream with ``.
diff --git a/apps/website/content/docs/chat/a2ui/overview.mdx b/apps/website/content/docs/chat/a2ui/overview.mdx
index e45df50b8..674518865 100644
--- a/apps/website/content/docs/chat/a2ui/overview.mdx
+++ b/apps/website/content/docs/chat/a2ui/overview.mdx
@@ -8,14 +8,7 @@ When an assistant message starts with `---a2ui_JSON---`, the chat streaming pipe
## Runtime Flow
-```text
-assistant text starts with ---a2ui_JSON---
- -> content classifier switches to A2UI mode
- -> createA2uiMessageParser() parses JSONL messages
- -> createA2uiSurfaceStore() applies those messages by surface id
- -> ChatComponent passes surface + state into A2uiSurfaceComponent
- -> A2uiSurfaceComponent renders progressive state through your catalog
-```
+
This is why A2UI sits between chat and render. Chat owns message streaming. A2UI owns the protocol shapes. `A2uiSurfaceComponent` turns the accumulated surface state into a render spec and delegates to `@threadplane/render`, so handlers, render events, and json-render state bindings use the same path for both the preferred `state` input and the legacy `surface` input.
diff --git a/apps/website/content/docs/chat/getting-started/introduction.mdx b/apps/website/content/docs/chat/getting-started/introduction.mdx
index 283daa5fe..d0a51debf 100644
--- a/apps/website/content/docs/chat/getting-started/introduction.mdx
+++ b/apps/website/content/docs/chat/getting-started/introduction.mdx
@@ -6,6 +6,13 @@
This guide explains the library's two-tier architecture, how it relates to the rest of the Threadplane stack, and when to reach for the all-in-one composition versus assembling primitives yourself.
+## How it fits
+
+
+
## Two-Tier Architecture
The library is organized into two layers: **primitives** and **compositions**.
@@ -42,21 +49,7 @@ Compositions are opinionated, styled components that combine primitives into rea
## How the Stack Fits Together
-`@threadplane/chat` sits between your application and two other Threadplane libraries:
-
-```
-Your App
- |
- v
-@threadplane/chat <-- UI components (this library)
- | |
- v v
-@threadplane/langgraph @threadplane/render
- (streaming state) (generative UI)
- |
- v
-LangGraph Platform
-```
+`@threadplane/chat` sits between your application and the adapters below the Agent-contract seam shown above; alongside it, `@threadplane/render` turns specs in assistant messages into your components.
- **`@threadplane/langgraph`** provides `provideAgent()` and `injectAgent()` returning a `LangGraphAgent`, which satisfies the `Agent` contract consumed by `@threadplane/chat`. The base `Agent` contract exposes reactive Signals for `messages()`, `status()`, `isLoading()`, `error()`, `toolCalls()`, and `state()`, plus optional `interrupt()` and `subagents()`. Adapters can layer on more: LangGraph implements `AgentWithHistory`, adding `history()` for checkpoint time-travel.
diff --git a/apps/website/content/docs/langgraph/concepts/agent-contract.mdx b/apps/website/content/docs/langgraph/concepts/agent-contract.mdx
index 787ba9c10..a7f1c6781 100644
--- a/apps/website/content/docs/langgraph/concepts/agent-contract.mdx
+++ b/apps/website/content/docs/langgraph/concepts/agent-contract.mdx
@@ -6,13 +6,10 @@ The `Agent` contract is the spine between runtime adapters and chat UI.
The UI shouldn't care whether a response came from LangGraph Platform, AG-UI, a local mock, or a custom HTTP service. That's the point. The boundary is explicit: adapters translate runtime events into Angular signals and a small action surface.
-```text
-LangGraph Platform -- @threadplane/langgraph --+
- +-- Agent -- @threadplane/chat
-AG-UI backend ------ @threadplane/ag-ui -------+
-
-custom backend ------ your adapter -----+
-```
+
## The Contract Surface
diff --git a/apps/website/content/docs/langgraph/getting-started/introduction.mdx b/apps/website/content/docs/langgraph/getting-started/introduction.mdx
index 767c72292..19a5b7e61 100644
--- a/apps/website/content/docs/langgraph/getting-started/introduction.mdx
+++ b/apps/website/content/docs/langgraph/getting-started/introduction.mdx
@@ -8,6 +8,13 @@
This guide walks you through the complete workflow: build a LangGraph agent in Python, run it locally, connect it to an Angular app with `injectAgent()`, and deploy to production.
+## How it fits
+
+
+
## What is `injectAgent()`?
`injectAgent()` is an Angular function that creates a reactive, streaming connection to a LangGraph agent. Configured globally via `provideAgent({...})` at bootstrap, it returns an object whose properties are Angular Signals — meaning your templates update automatically as the agent streams responses, token by token.
diff --git a/apps/website/content/docs/middleware/getting-started/introduction.mdx b/apps/website/content/docs/middleware/getting-started/introduction.mdx
index 80b601060..a93519c09 100644
--- a/apps/website/content/docs/middleware/getting-started/introduction.mdx
+++ b/apps/website/content/docs/middleware/getting-started/introduction.mdx
@@ -21,6 +21,10 @@ import {
There is no root `@threadplane/middleware` JavaScript entry point. Import from `@threadplane/middleware/langgraph`.
+## How it fits
+
+
+
## What it does
The LangGraph entry points read a client tool catalog from graph state, convert it into OpenAI function-tool objects, bind those tool stubs onto your chat model, and route client-tool calls to `END` so the browser can execute them.
diff --git a/apps/website/content/docs/render/concepts/json-render-vs-a2ui.mdx b/apps/website/content/docs/render/concepts/json-render-vs-a2ui.mdx
index 23d4f2a4c..74789b659 100644
--- a/apps/website/content/docs/render/concepts/json-render-vs-a2ui.mdx
+++ b/apps/website/content/docs/render/concepts/json-render-vs-a2ui.mdx
@@ -14,16 +14,7 @@ This matters because the tradeoff is not "which renderer is better." The tradeof
## The Layers
-```text
-@threadplane/render
- renders a Spec using an Angular registry, state store, functions, and handlers
-
-@threadplane/a2ui
- defines A2UI v0.9 message and component types
-
-@threadplane/chat
- detects assistant content, manages streaming state, and mounts render or A2UI surfaces
-```
+
Use `@threadplane/render` directly when your application already has a spec.
diff --git a/apps/website/content/docs/render/getting-started/introduction.mdx b/apps/website/content/docs/render/getting-started/introduction.mdx
index 6b723fcd5..2255d1d23 100644
--- a/apps/website/content/docs/render/getting-started/introduction.mdx
+++ b/apps/website/content/docs/render/getting-started/introduction.mdx
@@ -2,6 +2,10 @@
`@threadplane/render` is the Angular rendering engine for [json-render](https://github.com/nicholasgriffintn/json-render) specs. It takes a declarative JSON specification and renders it into a live Angular component tree -- with reactive state, event handling, and conditional rendering built in.
+## How it fits
+
+
+
## Why @threadplane/render?
Building dynamic UIs from server-driven specs shows up everywhere -- AI apps, form builders, CMS-powered frontends. `@threadplane/render` bridges `@json-render/core` (a framework-agnostic spec evaluation engine) and Angular's component model.
diff --git a/apps/website/content/docs/runtimes/getting-started/introduction.mdx b/apps/website/content/docs/runtimes/getting-started/introduction.mdx
index 91e98d18a..3a43f201a 100644
--- a/apps/website/content/docs/runtimes/getting-started/introduction.mdx
+++ b/apps/website/content/docs/runtimes/getting-started/introduction.mdx
@@ -13,6 +13,13 @@ This section documents what happened when it was tested. On 2026-08-31 the adapt
These pages document runtimes as *backends measured against the adapter*. They are not a substitute for each vendor's own documentation, and Threadplane does not maintain any of the upstream AG-UI bridges described here.
+## How it fits
+
+
+
## The runtimes
diff --git a/apps/website/content/docs/telemetry/getting-started/introduction.mdx b/apps/website/content/docs/telemetry/getting-started/introduction.mdx
index 1f2803f2d..55c357c8c 100644
--- a/apps/website/content/docs/telemetry/getting-started/introduction.mdx
+++ b/apps/website/content/docs/telemetry/getting-started/introduction.mdx
@@ -10,6 +10,10 @@ The important boundary is simple:
Don't treat the browser and Node entry points as interchangeable. They have different runtime assumptions and different privacy defaults.
+## How it fits
+
+
+
## Entry points
```ts
diff --git a/apps/website/e2e/docs.spec.ts b/apps/website/e2e/docs.spec.ts
index 594edc15c..03801fd28 100644
--- a/apps/website/e2e/docs.spec.ts
+++ b/apps/website/e2e/docs.spec.ts
@@ -117,6 +117,15 @@ test.describe('Docs slug page', () => {
test('heading permalinks carry no glyph in the text, only a CSS ::before', async ({ page }) => {
await page.goto(route);
+
+ // The workspace shell re-renders the article on hydration; before that,
+ // computed styles on its subtree can read as empty on a cold CI server.
+ // Same gate the control-plane tests above use.
+ await expect(page.locator('[data-workspace-shell]')).toHaveAttribute(
+ 'data-hydrated',
+ 'true',
+ );
+
const h2 = page.locator('article h2').first();
await expect(h2).toBeVisible();
@@ -127,10 +136,14 @@ test.describe('Docs slug page', () => {
// ...which means the visible affordance hangs entirely on one CSS rule
// (`.docs-prose h2 .heading-anchor::before` in global.css). jsdom cannot
// resolve pseudo-element content, so this is the only place it is guarded.
+ // Poll: stylesheet application can trail hydration on a cold dev server.
const anchor = h2.locator('a.heading-anchor');
await expect(anchor).toHaveCount(1);
- const glyph = await anchor.evaluate((el) => getComputedStyle(el, '::before').content);
- expect(glyph).toBe('"#"');
+ await expect
+ .poll(() =>
+ anchor.evaluate((el) => getComputedStyle(el, '::before').content)
+ )
+ .toBe('"#"');
});
test('breadcrumb renders exactly once', async ({ page }) => {
diff --git a/apps/website/src/app/ag-ui/page.tsx b/apps/website/src/app/ag-ui/page.tsx
index 43501bd5d..74aad1c1d 100644
--- a/apps/website/src/app/ag-ui/page.tsx
+++ b/apps/website/src/app/ag-ui/page.tsx
@@ -10,6 +10,7 @@ import { WhitePaperBlock } from '../../components/landing/WhitePaperBlock';
import { FinalCTA } from '../../components/landing/FinalCTA';
import { MediumSwitcher } from '../../components/landing/MediumSwitcher';
import { BackendsGrid } from '../../components/landing/ag-ui/BackendsGrid';
+import { StackDiagramSection } from '../../components/landing/StackDiagramSection';
import { createPageMetadata, SHORT_POSITIONING_DESCRIPTION } from '../../lib/site-metadata';
import { SECTION_MEDIA } from '../../lib/section-media';
import { buildPanes } from '../../lib/build-panes';
@@ -63,6 +64,15 @@ export default async function AgUiPage() {
+
+
+
+
+
+
{/* Interactive demo showcase */}
diff --git a/apps/website/src/components/docs/AgUiArchDiagram.tsx b/apps/website/src/components/docs/AgUiArchDiagram.tsx
deleted file mode 100644
index 2b8c1fdf2..000000000
--- a/apps/website/src/components/docs/AgUiArchDiagram.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-interface BoxProps {
- eyebrow: string;
- title: string;
- meta: string;
- tone?: 'neutral' | 'accent';
-}
-
-function Box({ eyebrow, title, meta, tone = 'neutral' }: BoxProps) {
- return (
-
diff --git a/apps/website/src/components/landing/StackDiagramSection.spec.tsx b/apps/website/src/components/landing/StackDiagramSection.spec.tsx
new file mode 100644
index 000000000..f817a9242
--- /dev/null
+++ b/apps/website/src/components/landing/StackDiagramSection.spec.tsx
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: MIT
+// @vitest-environment jsdom
+import React from 'react';
+import { describe, expect, it } from 'vitest';
+import { render } from '@testing-library/react';
+import { StackDiagramSection } from './StackDiagramSection';
+
+describe('StackDiagramSection', () => {
+ it('renders heading, body, and a marketing-scaled stack diagram', () => {
+ const { container, getByText } = render(
+
+ );
+ expect(getByText('One contract between your UI and any runtime').tagName).toBe('H2');
+ expect(container.querySelector('section')?.getAttribute('aria-labelledby')).toBe('architecture-heading');
+ expect(container.querySelector('figure.tp-diagram-figure')?.getAttribute('data-scale')).toBe('marketing');
+ });
+
+ it('accents the LangGraph node when highlight="langgraph"', () => {
+ const { container } = render(
+
+ );
+ const titles = Array.from(container.querySelectorAll('.tp-diagram-title'));
+ const langgraphTitle = titles.find((el) => el.textContent === '@threadplane/langgraph');
+ expect(langgraphTitle?.closest('g.tp-diagram-node')?.getAttribute('data-tone')).toBe('accent');
+ });
+
+ it('accents the AG-UI node when highlight="ag-ui"', () => {
+ const { container } = render(
+
+ );
+ const titles = Array.from(container.querySelectorAll('.tp-diagram-title'));
+ const agUiTitle = titles.find((el) => el.textContent === '@threadplane/ag-ui');
+ expect(agUiTitle?.closest('g.tp-diagram-node')?.getAttribute('data-tone')).toBe('accent');
+ });
+});
diff --git a/apps/website/src/components/landing/StackDiagramSection.tsx b/apps/website/src/components/landing/StackDiagramSection.tsx
new file mode 100644
index 000000000..d8ec4f98f
--- /dev/null
+++ b/apps/website/src/components/landing/StackDiagramSection.tsx
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: MIT
+import type { ReactNode } from 'react';
+import { Section } from '../ui/Section';
+import { Container } from '../ui/Container';
+import { SectionHeader } from '../ui/SectionHeader';
+import { StackDiagram, type StackHighlight } from '../docs/diagrams';
+
+interface StackDiagramSectionProps {
+ id: string;
+ eyebrow: string;
+ headline: string;
+ body: ReactNode;
+ highlight?: StackHighlight;
+ caption?: string;
+}
+
+/**
+ * Homepage architecture section: the canonical stack diagram, widened to the
+ * marketing scale, framed by a headline + body in the site's landing idiom.
+ */
+export function StackDiagramSection({
+ id,
+ eyebrow,
+ headline,
+ body,
+ highlight = 'none',
+ caption,
+}: StackDiagramSectionProps) {
+ return (
+
+
+
+
+
{body}
+
+
+
+
+ );
+}
diff --git a/apps/website/src/styles/docs.css b/apps/website/src/styles/docs.css
index b9d171ac9..65e132962 100644
--- a/apps/website/src/styles/docs.css
+++ b/apps/website/src/styles/docs.css
@@ -190,7 +190,7 @@ body:has([data-website-workspace-host]) {
* rather than an inline asset. Tinted backdrop + soft shadow + clean caption.
* Scoped to image figures via :has(> img) so it doesn't collide with the
* code-block figures from rehype-pretty-code or component figures (e.g.
- * AgUiArchDiagram). */
+ * StackDiagram). */
.docs-prose figure:has(> img) {
margin: 2.5rem 0;
padding: 0.75rem 0.75rem 0;
@@ -262,103 +262,6 @@ body:has([data-website-workspace-host]) {
.docs-prose td { padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--docs-accent-tint-soft); color: var(--color-text-secondary); }
.docs-prose td code { font-size: 0.8em; }
-/* AG-UI architecture diagram */
-.ag-ui-arch-grid {
- display: grid;
- grid-template-columns: 1fr auto 1fr auto 1fr;
- align-items: stretch;
- gap: 0;
-}
-/* Reunited with the .ag-ui-arch-arrow mobile override below — this base
- * rule was migrated from AgUiArchDiagram.tsx's inline style, the media
- * query pre-existed it. */
-.ag-ui-arch-arrow {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 4px;
- color: var(--color-text-muted);
- padding: 0 4px;
-}
-@media (max-width: 720px) {
- .ag-ui-arch-grid {
- grid-template-columns: 1fr;
- }
- .ag-ui-arch-arrow {
- transform: rotate(90deg);
- padding: 12px 0;
- margin: 4px auto;
- }
-}
-.ag-ui-arch-box {
- background: var(--color-surface);
- border: 1px solid var(--color-border);
- border-radius: var(--radius-lg);
- padding: 20px 22px;
- display: flex;
- flex-direction: column;
- gap: 6px;
- min-height: 116px;
-}
-.ag-ui-arch-box[data-tone="accent"] {
- background: var(--color-accent-surface);
- border-color: #00409033;
-}
-.ag-ui-arch-box-eyebrow {
- font-family: var(--font-mono);
- font-size: 0.7rem;
- font-weight: 600;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- color: var(--color-text-muted);
-}
-.ag-ui-arch-box[data-tone="accent"] .ag-ui-arch-box-eyebrow {
- color: var(--color-accent);
-}
-.ag-ui-arch-box-title {
- font-family: var(--font-mono);
- font-size: 0.95rem;
- font-weight: 600;
- color: var(--color-text-primary);
-}
-.ag-ui-arch-box-meta {
- font-size: 0.85rem;
- line-height: 1.5;
- color: var(--color-text-secondary);
-}
-.ag-ui-arch-arrow-label {
- font-family: var(--font-mono);
- font-size: 0.65rem;
- font-weight: 600;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- color: var(--color-accent);
- white-space: nowrap;
-}
-.ag-ui-arch-arrow-svg {
- display: block;
-}
-.ag-ui-arch-arrow-sub {
- font-size: 0.72rem;
- color: var(--color-text-muted);
- white-space: nowrap;
-}
-.ag-ui-arch-figure {
- margin: 2rem 0;
- padding: 28px 24px;
- background: var(--color-canvas);
- border: 1px solid var(--color-border);
- border-radius: var(--radius-lg);
-}
-.ag-ui-arch-caption {
- margin-top: 16px;
- font-size: 0.8rem;
- color: var(--color-text-muted);
- text-align: center;
- font-style: italic;
-}
-
/* Docs — readable column max-width */
.docs-prose {
max-width: 70ch;
@@ -425,74 +328,58 @@ body:has([data-website-workspace-host]) {
* Fonts: unified onto the next/font vars — see the font note atop ui.css.
*/
-/* mdx — Callout */
+/* mdx — Callout (header band; tone via custom properties) */
[data-mdx="callout"] {
- position: relative;
- background: var(--color-surface-tinted);
+ --callout-tone-text: var(--color-accent);
+ --callout-tone-surface: rgba(0, 64, 144, 0.06);
+ background: var(--color-surface);
border: 1px solid var(--color-border);
- border-left-width: 4px;
border-radius: var(--radius-md);
- padding: 14px 18px 14px 22px;
+ overflow: hidden;
margin: 20px 0;
}
[data-mdx="callout"][data-tone="tip"] {
- border-left-color: #1a7a40;
+ --callout-tone-text: #1a7a40;
+ --callout-tone-surface: rgba(26, 122, 64, 0.07);
}
[data-mdx="callout"][data-tone="warning"] {
- border-left-color: #D4850F;
-}
-[data-mdx="callout"][data-tone="info"] {
- border-left-color: var(--color-accent);
+ /* Band text darkened from #D4850F for WCAG AA contrast on the tint. */
+ --callout-tone-text: #8a5a00;
+ --callout-tone-surface: rgba(212, 133, 15, 0.08);
}
[data-mdx="callout"][data-tone="danger"] {
- border-left-color: var(--color-angular-red);
+ --callout-tone-text: var(--color-angular-red);
+ --callout-tone-surface: rgba(221, 0, 49, 0.06);
}
-.mdx-callout-header {
+.mdx-callout-band {
display: flex;
- align-items: baseline;
- gap: 10px;
- margin-bottom: 0;
-}
-.mdx-callout-header[data-has-title] {
- margin-bottom: 6px;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 14px;
+ background: var(--callout-tone-surface);
+ border-bottom: 1px solid var(--color-border);
+ color: var(--callout-tone-text);
}
.mdx-callout-icon {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 20px;
- height: 20px;
- border-radius: var(--radius-full);
- color: var(--color-text-inverted);
- font-family: var(--font-mono);
- font-size: 11px;
- font-weight: 700;
+ width: 14px;
+ height: 14px;
flex-shrink: 0;
}
-[data-mdx="callout"][data-tone="tip"] .mdx-callout-icon {
- background: #1a7a40;
-}
-[data-mdx="callout"][data-tone="warning"] .mdx-callout-icon {
- background: #D4850F;
-}
-[data-mdx="callout"][data-tone="info"] .mdx-callout-icon {
- background: var(--color-accent);
-}
-[data-mdx="callout"][data-tone="danger"] .mdx-callout-icon {
- background: var(--color-angular-red);
-}
.mdx-callout-title {
font-family: var(--font-inter);
- font-size: 15px;
- color: var(--color-text-primary);
+ font-size: 13px;
font-weight: 600;
+ color: var(--callout-tone-text);
}
.mdx-callout-body {
+ padding: 12px 14px;
font-family: var(--font-inter);
font-size: 15px;
line-height: 1.6;
color: var(--color-text-secondary);
}
+.mdx-callout-body > :first-child { margin-top: 0; }
+.mdx-callout-body > :last-child { margin-bottom: 0; }
/* mdx — Steps / Step */
.mdx-steps-list {
@@ -765,6 +652,84 @@ body:has([data-website-workspace-host]) {
font-size: 10px;
color: var(--color-accent);
}
+/* mdx — diagram kit (components/docs/diagrams/*) */
+.tp-diagram-figure {
+ margin: 24px 0;
+ overflow-x: auto;
+}
+.tp-diagram-svg {
+ display: block;
+ width: 100%;
+ min-width: 480px;
+ max-width: 680px;
+ margin: 0 auto;
+}
+.tp-diagram-figure[data-scale="marketing"] .tp-diagram-svg {
+ max-width: 860px;
+}
+.tp-diagram-dot { fill: var(--color-border); }
+.tp-diagram-caption {
+ font-family: var(--font-inter);
+ font-size: 13px;
+ color: var(--color-text-muted);
+ text-align: center;
+ margin-top: 10px;
+}
+.tp-diagram-node rect {
+ fill: var(--color-surface);
+ stroke: var(--color-border);
+}
+.tp-diagram-node[data-tone="accent"] rect {
+ fill: var(--color-accent-surface);
+ stroke: var(--color-accent-border);
+}
+.tp-diagram-node[data-tone="dim"] rect {
+ fill: var(--color-surface-dim);
+ stroke: var(--color-border);
+}
+.tp-diagram-eyebrow {
+ font-family: var(--font-mono);
+ font-size: 9px;
+ font-weight: 600;
+ letter-spacing: 0.1em;
+ fill: var(--color-text-muted);
+}
+.tp-diagram-node[data-tone="accent"] .tp-diagram-eyebrow { fill: var(--color-accent); }
+.tp-diagram-title {
+ font-family: var(--font-mono);
+ font-size: 12.5px;
+ font-weight: 700;
+ fill: var(--color-text-primary);
+}
+.tp-diagram-node[data-title="sans"] .tp-diagram-title {
+ font-family: var(--font-inter);
+ font-size: 11px;
+ font-weight: 600;
+ fill: var(--color-text-secondary);
+}
+.tp-diagram-meta {
+ font-family: var(--font-inter);
+ font-size: 10.5px;
+ fill: var(--color-text-muted);
+}
+.tp-diagram-edge {
+ fill: none;
+ stroke: var(--color-text-muted);
+ stroke-width: 1.2;
+}
+.tp-diagram-arrowhead {
+ fill: none;
+ stroke: var(--color-text-muted);
+}
+.tp-diagram-pill rect {
+ fill: var(--color-accent-surface);
+ stroke: var(--color-accent-border);
+}
+.tp-diagram-pill text {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ fill: var(--color-accent);
+}
/*
* DocsSidebar
diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css
index a2e6a475b..86dbc2d4b 100644
--- a/apps/website/src/styles/landing.css
+++ b/apps/website/src/styles/landing.css
@@ -1487,3 +1487,26 @@
height: 1px;
background: var(--color-border);
}
+
+/* StackDiagramSection — components/landing/StackDiagramSection.tsx */
+.stack-diagram-section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ gap: 12px;
+}
+.stack-diagram-body {
+ font-family: var(--font-inter);
+ font-size: var(--text-body-lg);
+ line-height: var(--text-body-lg--line-height);
+ color: var(--color-text-secondary);
+ max-width: 620px;
+ margin: 0 0 12px;
+}
+/* Guards against flex-item shrink-to-content: without an explicit width the
+ figure would collapse to its intrinsic SVG size inside the centered
+ flex column, instead of filling the container like the surrounding text. */
+.stack-diagram-section .tp-diagram-figure {
+ width: 100%;
+}
diff --git a/apps/website/src/styles/style-contracts.spec.ts b/apps/website/src/styles/style-contracts.spec.ts
index afaac58ce..4d4269cec 100644
--- a/apps/website/src/styles/style-contracts.spec.ts
+++ b/apps/website/src/styles/style-contracts.spec.ts
@@ -120,6 +120,22 @@ const CONTRACTS: StyleContract[] = [
'overscroll-behavior': /overscroll-behavior:\s*none/,
},
},
+ {
+ file: 'docs.css',
+ selector: '.tp-diagram-figure',
+ why: 'Diagram SVGs are wider than the article column on narrow viewports. Without horizontal scrolling the figure either overflows the page or gets silently clipped.',
+ requires: {
+ 'overflow-x': /overflow-x:\s*auto/,
+ },
+ },
+ {
+ file: 'docs.css',
+ selector: '.tp-diagram-svg',
+ why: 'The docs-scale cap keeps diagrams from ballooning past a readable width in the article column; losing it lets the SVG stretch to the full (scrollable) figure width instead.',
+ requires: {
+ 'max-width': /max-width:\s*680px/,
+ },
+ },
];
describe('style contracts', () => {
diff --git a/docs/superpowers/plans/2026-09-01-docs-visual-design.md b/docs/superpowers/plans/2026-09-01-docs-visual-design.md
new file mode 100644
index 000000000..7d91152a3
--- /dev/null
+++ b/docs/superpowers/plans/2026-09-01-docs-visual-design.md
@@ -0,0 +1,1557 @@
+# Docs Visual Design Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Replace the left-border docs callout with a header-band design, build an SVG diagram kit, replace all 5 ascii diagrams, add a "How it fits" diagram to every library intro, and add the master stack diagram to the homepage + adapter landing pages.
+
+**Architecture:** A four-primitive SVG kit (`DiagramFrame`/`DiagramNode`/`DiagramEdge`/`DiagramPill`) in `apps/website/src/components/docs/diagrams/`, styled entirely by classes in `docs.css` (token vars only — an ESLint guard bans new static inline `style` props). Hand-placed compositions per diagram; one parametrized `StackDiagram` covers the canonical chat→contract→adapters→backends picture with a `highlight` prop, reused by four docs pages, two blog posts, and three marketing pages.
+
+**Tech Stack:** Next.js (apps/website), MDX via next-mdx-remote (`MdxRenderer` components map), vitest + @testing-library/react, `npx nx test website` / `npx nx lint website`.
+
+**Spec:** `docs/superpowers/specs/2026-09-01-docs-visual-design-design.md`
+
+**Conventions for every task:**
+- Working dir: repo root. Website content: `apps/website/content/docs/`, components: `apps/website/src/components/`, styles: `apps/website/src/styles/docs.css`.
+- Specs are vitest + jsdom: start files with `// @vitest-environment jsdom` (see `apps/website/src/components/docs/LibraryMark.spec.tsx` for the idiom).
+- Run tests with `npx nx test website` (full suite, fast, ~350 tests). Lint with `npx nx lint website`.
+- NO inline `style` props anywhere (lint error). SVG geometry (x/y/width/d/viewBox) as attributes is fine; all colors/fonts go through CSS classes using `var(--color-*)` / `var(--font-*)`.
+
+---
+
+### Task 1: Callout header-band redesign
+
+**Files:**
+- Modify: `apps/website/src/components/docs/mdx/Callout.tsx` (full rewrite)
+- Modify: `apps/website/src/styles/docs.css:372-439` (replace the whole `mdx — Callout` block)
+- Create: `apps/website/src/components/docs/mdx/Callout.spec.tsx`
+
+- [ ] **Step 1: Write the failing spec**
+
+```tsx
+// SPDX-License-Identifier: MIT
+// @vitest-environment jsdom
+import React from 'react';
+import { describe, expect, it } from 'vitest';
+import { render } from '@testing-library/react';
+import { Callout } from './Callout';
+
+describe('Callout', () => {
+ it('renders a header band with the given title and tone', () => {
+ const { container } = render(
+ body text
+ );
+ const root = container.querySelector('[data-mdx="callout"]');
+ expect(root?.getAttribute('data-tone')).toBe('warning');
+ const band = container.querySelector('.mdx-callout-band');
+ expect(band?.textContent).toContain('Heads up');
+ expect(band?.querySelector('svg')).not.toBeNull();
+ });
+
+ it.each([
+ ['info', 'Note'],
+ ['tip', 'Tip'],
+ ['warning', 'Warning'],
+ ['danger', 'Danger'],
+ ] as const)('falls back to the kind label for %s when title is omitted', (type, label) => {
+ const { container } = render(body);
+ expect(container.querySelector('.mdx-callout-title')?.textContent).toBe(label);
+ });
+
+ it('defaults to info and renders children in the body', () => {
+ const { container } = render(the body);
+ expect(container.querySelector('[data-mdx="callout"]')?.getAttribute('data-tone')).toBe('info');
+ expect(container.querySelector('.mdx-callout-body')?.textContent).toBe('the body');
+ });
+});
+```
+
+- [ ] **Step 2: Run to verify it fails**
+
+Run: `npx nx test website`
+Expected: the three new Callout tests FAIL (no `.mdx-callout-band` in the current markup); all pre-existing tests still pass.
+
+- [ ] **Step 3: Rewrite `Callout.tsx`**
+
+```tsx
+import type { ReactNode } from 'react';
+
+type CalloutType = 'tip' | 'warning' | 'info' | 'danger';
+
+interface Props {
+ type?: CalloutType;
+ title?: string;
+ children: ReactNode;
+}
+
+/** Band label when the author gives no title — the band never renders empty. */
+const KIND_LABEL: Record = {
+ tip: 'Tip',
+ warning: 'Warning',
+ info: 'Note',
+ danger: 'Danger',
+};
+
+const ICON_PATHS: Record = {
+ info: (
+ <>
+
+
+ >
+ ),
+ tip: (
+ <>
+
+
+ >
+ ),
+ warning: (
+ <>
+
+
+ >
+ ),
+ danger: (
+ <>
+
+
+ >
+ ),
+};
+
+export function Callout({ type = 'info', title, children }: Props) {
+ return (
+
+
+
+ {title ?? KIND_LABEL[type]}
+
+
{children}
+
+ );
+}
+```
+
+- [ ] **Step 4: Replace the callout CSS block**
+
+In `apps/website/src/styles/docs.css`, replace everything from `/* mdx — Callout */` (line 372) through the end of `.mdx-callout-body { ... }` (line 439) with:
+
+```css
+/* mdx — Callout (header band; tone via custom properties) */
+[data-mdx="callout"] {
+ --callout-tone-text: var(--color-accent);
+ --callout-tone-surface: rgba(0, 64, 144, 0.06);
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ margin: 20px 0;
+}
+[data-mdx="callout"][data-tone="tip"] {
+ --callout-tone-text: #1a7a40;
+ --callout-tone-surface: rgba(26, 122, 64, 0.07);
+}
+[data-mdx="callout"][data-tone="warning"] {
+ /* Band text darkened from #D4850F for contrast on the tint. */
+ --callout-tone-text: #b26d06;
+ --callout-tone-surface: rgba(212, 133, 15, 0.08);
+}
+[data-mdx="callout"][data-tone="danger"] {
+ --callout-tone-text: var(--color-angular-red);
+ --callout-tone-surface: rgba(221, 0, 49, 0.06);
+}
+.mdx-callout-band {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 14px;
+ background: var(--callout-tone-surface);
+ border-bottom: 1px solid var(--color-border);
+ color: var(--callout-tone-text);
+}
+.mdx-callout-icon {
+ width: 14px;
+ height: 14px;
+ flex-shrink: 0;
+}
+.mdx-callout-title {
+ font-family: Inter, system-ui, sans-serif;
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--callout-tone-text);
+}
+.mdx-callout-body {
+ padding: 12px 14px;
+ font-family: var(--font-inter);
+ font-size: 15px;
+ line-height: 1.6;
+ color: var(--color-text-secondary);
+}
+.mdx-callout-body > :first-child { margin-top: 0; }
+.mdx-callout-body > :last-child { margin-bottom: 0; }
+```
+
+Note: `.mdx-callout-header` / `.mdx-callout-header[data-has-title]` and the four `.mdx-callout-icon` background rules are gone. Grep for stale references: `grep -rn "mdx-callout-header" apps/website/src` must return nothing.
+
+- [ ] **Step 5: Run tests + lint**
+
+Run: `npx nx test website && npx nx lint website`
+Expected: all tests PASS, lint clean.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add apps/website/src/components/docs/mdx/Callout.tsx apps/website/src/components/docs/mdx/Callout.spec.tsx apps/website/src/styles/docs.css
+git commit -m "feat(website): header-band docs callouts, retiring the left border"
+```
+
+---
+
+### Task 2: Diagram kit primitives
+
+**Files:**
+- Create: `apps/website/src/components/docs/diagrams/DiagramFrame.tsx`
+- Create: `apps/website/src/components/docs/diagrams/DiagramNode.tsx`
+- Create: `apps/website/src/components/docs/diagrams/DiagramEdge.tsx`
+- Create: `apps/website/src/components/docs/diagrams/DiagramPill.tsx`
+- Create: `apps/website/src/components/docs/diagrams/primitives.spec.tsx`
+- Modify: `apps/website/src/styles/docs.css` (append kit classes at end of the mdx components region, right before the `DocsSidebar` comment block at ~line 713)
+
+- [ ] **Step 1: Write the failing spec**
+
+```tsx
+// SPDX-License-Identifier: MIT
+// @vitest-environment jsdom
+import React from 'react';
+import { describe, expect, it } from 'vitest';
+import { render } from '@testing-library/react';
+import { DiagramFrame } from './DiagramFrame';
+import { DiagramNode } from './DiagramNode';
+import { DiagramEdge } from './DiagramEdge';
+import { DiagramPill } from './DiagramPill';
+
+describe('diagram kit primitives', () => {
+ it('DiagramFrame renders a labeled svg with dot ground, arrow marker, and caption', () => {
+ const { container, getByText } = render(
+
+
+
+ );
+ const svg = container.querySelector('svg.tp-diagram-svg');
+ expect(svg?.getAttribute('viewBox')).toBe('0 0 640 200');
+ expect(svg?.getAttribute('aria-label')).toBe('test diagram');
+ expect(container.querySelector('pattern#t-dots')).not.toBeNull();
+ expect(container.querySelector('marker#t-arrow')).not.toBeNull();
+ expect(container.querySelector('path.tp-diagram-edge')?.getAttribute('marker-end')).toBe('url(#t-arrow)');
+ expect(getByText('a caption').tagName).toBe('FIGCAPTION');
+ });
+
+ it('DiagramFrame passes the marketing scale through as a data attribute', () => {
+ const { container } = render(
+
+
+
+ );
+ expect(container.querySelector('figure')?.getAttribute('data-scale')).toBe('marketing');
+ });
+
+ it('DiagramNode renders eyebrow, title, meta and tone', () => {
+ const { container } = render(
+
+ );
+ const g = container.querySelector('g.tp-diagram-node');
+ expect(g?.getAttribute('data-tone')).toBe('accent');
+ expect(g?.querySelector('.tp-diagram-eyebrow')?.textContent).toBe('ADAPTER');
+ expect(g?.querySelector('.tp-diagram-title')?.textContent).toBe('@threadplane/ag-ui');
+ expect(g?.querySelector('.tp-diagram-meta')?.textContent).toBe('toAgent()');
+ });
+
+ it('DiagramNode centers a title-only node when align is middle', () => {
+ const { container } = render(
+
+ );
+ const title = container.querySelector('.tp-diagram-title');
+ expect(title?.getAttribute('text-anchor')).toBe('middle');
+ expect(container.querySelector('g.tp-diagram-node')?.getAttribute('data-title')).toBe('sans');
+ });
+
+ it('DiagramPill renders a centered label', () => {
+ const { container } = render(
+
+ );
+ const text = container.querySelector('.tp-diagram-pill text');
+ expect(text?.textContent).toBe('SSE');
+ expect(text?.getAttribute('text-anchor')).toBe('middle');
+ });
+});
+```
+
+- [ ] **Step 2: Run to verify it fails**
+
+Run: `npx nx test website`
+Expected: FAIL — modules not found.
+
+- [ ] **Step 3: Implement the four primitives**
+
+`DiagramFrame.tsx`:
+
+```tsx
+import type { ReactNode } from 'react';
+
+interface DiagramFrameProps {
+ /** Unique per rendered diagram; namespaces the SVG defs ids (`{slug}-dots`, `{slug}-arrow`). */
+ slug: string;
+ viewWidth: number;
+ viewHeight: number;
+ /** Accessible one-sentence description of what the diagram shows. */
+ label: string;
+ caption?: string;
+ /** Marketing pages render the same SVG larger. */
+ scale?: 'docs' | 'marketing';
+ children: ReactNode;
+}
+
+export function DiagramFrame({
+ slug,
+ viewWidth,
+ viewHeight,
+ label,
+ caption,
+ scale = 'docs',
+ children,
+}: DiagramFrameProps) {
+ return (
+
+
+ {caption ? {caption} : null}
+
+ );
+}
+```
+
+`DiagramNode.tsx`:
+
+```tsx
+interface DiagramNodeProps {
+ x: number;
+ y: number;
+ w: number;
+ h: number;
+ title: string;
+ eyebrow?: string;
+ meta?: string;
+ tone?: 'neutral' | 'accent' | 'dim';
+ /** 'middle' centers text horizontally (title-only summary nodes). */
+ align?: 'start' | 'middle';
+ /** 'sans' for prose-y titles (backend lists); default mono for package names. */
+ titleStyle?: 'mono' | 'sans';
+}
+
+const PAD = 16;
+
+export function DiagramNode({
+ x,
+ y,
+ w,
+ h,
+ title,
+ eyebrow,
+ meta,
+ tone = 'neutral',
+ align = 'start',
+ titleStyle = 'mono',
+}: DiagramNodeProps) {
+ const tx = align === 'middle' ? x + w / 2 : x + PAD;
+ const anchor = align === 'middle' ? 'middle' : undefined;
+ // Baselines: with an eyebrow the stack is eyebrow/title/meta; without it the
+ // title floats up; a title-only node vertically centers.
+ const titleY = eyebrow ? y + 38 : meta ? y + 26 : y + h / 2 + 4;
+ const metaY = eyebrow ? y + 54 : y + 42;
+ return (
+
+
+ {eyebrow ? (
+
+ {eyebrow.toUpperCase()}
+
+ ) : null}
+
+ {title}
+
+ {meta ? (
+
+ {meta}
+
+ ) : null}
+
+ );
+}
+```
+
+`DiagramEdge.tsx`:
+
+```tsx
+interface DiagramEdgeProps {
+ /** SVG path data; orthogonal segments (H/V) preferred. */
+ d: string;
+ /** DiagramFrame slug — required when arrow is true, to reference `{slug}-arrow`. */
+ slug?: string;
+ arrow?: boolean;
+}
+
+export function DiagramEdge({ d, slug, arrow = false }: DiagramEdgeProps) {
+ return (
+
+ );
+}
+```
+
+`DiagramPill.tsx`:
+
+```tsx
+interface DiagramPillProps {
+ /** Center of the pill. */
+ cx: number;
+ cy: number;
+ w: number;
+ label: string;
+}
+
+const PILL_H = 24;
+
+export function DiagramPill({ cx, cy, w, label }: DiagramPillProps) {
+ return (
+
+
+
+ {label}
+
+
+ );
+}
+```
+
+- [ ] **Step 4: Append kit CSS to `docs.css`**
+
+Insert immediately before the `/*\n * DocsSidebar` comment block:
+
+```css
+/* mdx — diagram kit (components/docs/diagrams/*) */
+.tp-diagram-figure {
+ margin: 24px 0;
+ overflow-x: auto;
+}
+.tp-diagram-svg {
+ display: block;
+ width: 100%;
+ min-width: 480px;
+ max-width: 680px;
+ margin: 0 auto;
+}
+.tp-diagram-figure[data-scale="marketing"] .tp-diagram-svg {
+ max-width: 860px;
+}
+.tp-diagram-dot { fill: var(--color-border); }
+.tp-diagram-caption {
+ font-family: var(--font-inter);
+ font-size: 13px;
+ color: var(--color-text-muted);
+ text-align: center;
+ margin-top: 10px;
+}
+.tp-diagram-node rect {
+ fill: var(--color-surface);
+ stroke: var(--color-border);
+}
+.tp-diagram-node[data-tone="accent"] rect {
+ fill: var(--color-accent-surface);
+ stroke: var(--color-accent-border);
+}
+.tp-diagram-node[data-tone="dim"] rect {
+ fill: var(--color-surface-dim);
+ stroke: var(--color-border);
+}
+.tp-diagram-eyebrow {
+ font-family: var(--font-mono);
+ font-size: 9px;
+ font-weight: 600;
+ letter-spacing: 0.1em;
+ fill: var(--color-text-muted);
+}
+.tp-diagram-node[data-tone="accent"] .tp-diagram-eyebrow { fill: var(--color-accent); }
+.tp-diagram-title {
+ font-family: var(--font-mono);
+ font-size: 12.5px;
+ font-weight: 700;
+ fill: var(--color-text-primary);
+}
+.tp-diagram-node[data-title="sans"] .tp-diagram-title {
+ font-family: var(--font-inter);
+ font-size: 11px;
+ font-weight: 600;
+ fill: var(--color-text-secondary);
+}
+.tp-diagram-meta {
+ font-family: var(--font-inter);
+ font-size: 10.5px;
+ fill: var(--color-text-muted);
+}
+.tp-diagram-edge {
+ fill: none;
+ stroke: var(--color-text-muted);
+ stroke-width: 1.2;
+}
+.tp-diagram-arrowhead {
+ fill: none;
+ stroke: var(--color-text-muted);
+}
+.tp-diagram-pill rect {
+ fill: var(--color-accent-surface);
+ stroke: var(--color-accent-border);
+}
+.tp-diagram-pill text {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ fill: var(--color-accent);
+ text-anchor: middle;
+}
+```
+
+- [ ] **Step 5: Run tests + lint**
+
+Run: `npx nx test website && npx nx lint website`
+Expected: PASS.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add apps/website/src/components/docs/diagrams apps/website/src/styles/docs.css
+git commit -m "feat(website): SVG diagram kit primitives on a dot grid"
+```
+
+---
+
+### Task 3: StackDiagram composition + MDX registration
+
+**Files:**
+- Create: `apps/website/src/components/docs/diagrams/StackDiagram.tsx`
+- Create: `apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx`
+- Modify: `apps/website/src/components/docs/MdxRenderer.tsx`
+
+- [ ] **Step 1: Write the failing spec**
+
+```tsx
+// SPDX-License-Identifier: MIT
+// @vitest-environment jsdom
+import React from 'react';
+import { describe, expect, it } from 'vitest';
+import { render } from '@testing-library/react';
+import { StackDiagram } from './StackDiagram';
+
+function toneOf(container: HTMLElement, title: string): string | null {
+ const titles = Array.from(container.querySelectorAll('.tp-diagram-title'));
+ const t = titles.find((el) => el.textContent === title);
+ return t?.closest('g.tp-diagram-node')?.getAttribute('data-tone') ?? null;
+}
+
+describe('StackDiagram', () => {
+ it('renders the canonical five-node stack', () => {
+ const { container } = render();
+ const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent);
+ expect(titles).toContain('@threadplane/chat');
+ expect(titles).toContain('@threadplane/langgraph');
+ expect(titles).toContain('@threadplane/ag-ui');
+ expect(container.querySelector('.tp-diagram-pill text')?.textContent).toBe('Agent contract · signals + events$');
+ });
+
+ it.each([
+ ['ag-ui', '@threadplane/ag-ui'],
+ ['langgraph', '@threadplane/langgraph'],
+ ['chat', '@threadplane/chat'],
+ ] as const)('highlight=%s accents that node', (highlight, title) => {
+ const { container } = render();
+ expect(toneOf(container, title)).toBe('accent');
+ });
+
+ it('highlight=runtimes accents the backend row', () => {
+ const { container } = render();
+ expect(toneOf(container, 'LangGraph Platform')).toBe('accent');
+ expect(toneOf(container, 'CrewAI · Mastra · MS Agent Fwk · Strands · …')).toBe('accent');
+ });
+
+ it('renders a caption when given', () => {
+ const { getByText } = render();
+ expect(getByText('the caption')).not.toBeNull();
+ });
+});
+```
+
+- [ ] **Step 2: Run to verify it fails**
+
+Run: `npx nx test website`
+Expected: FAIL — module not found.
+
+- [ ] **Step 3: Implement `StackDiagram.tsx`**
+
+```tsx
+import { DiagramFrame } from './DiagramFrame';
+import { DiagramNode } from './DiagramNode';
+import { DiagramEdge } from './DiagramEdge';
+import { DiagramPill } from './DiagramPill';
+
+export type StackHighlight = 'none' | 'chat' | 'langgraph' | 'ag-ui' | 'runtimes' | 'contract';
+
+interface StackDiagramProps {
+ highlight?: StackHighlight;
+ caption?: string;
+ scale?: 'docs' | 'marketing';
+}
+
+/**
+ * The canonical Threadplane stack: chat UI on top, the Agent contract as a
+ * labeled seam, the two runtime adapters, and their backends. `highlight`
+ * accents the node(s) a given page is about; `contract` accents both adapters.
+ */
+export function StackDiagram({ highlight = 'none', caption, scale = 'docs' }: StackDiagramProps) {
+ const slug = `stack-${highlight}`;
+ const adapters = highlight === 'contract';
+ const backends = highlight === 'runtimes';
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+- [ ] **Step 4: Register in `MdxRenderer.tsx`**
+
+Add import `import { StackDiagram } from './diagrams/StackDiagram';` and add `StackDiagram,` to the `mdxComponents` map (keep `AgUiArchDiagram` for now — it is removed in Task 4).
+
+- [ ] **Step 5: Run tests, then commit**
+
+Run: `npx nx test website && npx nx lint website`
+Expected: PASS.
+
+```bash
+git add apps/website/src/components/docs/diagrams/StackDiagram.tsx apps/website/src/components/docs/diagrams/StackDiagram.spec.tsx apps/website/src/components/docs/MdxRenderer.tsx
+git commit -m "feat(website): StackDiagram — the canonical stack schematic"
+```
+
+---
+
+### Task 4: Replace ascii + retire AgUiArchDiagram
+
+**Files:**
+- Modify: `apps/website/content/docs/ag-ui/getting-started/introduction.mdx:17-29`
+- Modify: `apps/website/content/docs/langgraph/concepts/agent-contract.mdx` (the ```` ```text ```` fan-in fence)
+- Modify: `apps/website/content/blog/2026-05-21-build-fullstack-agentic-angular-apps-using-ag-ui.mdx:62`
+- Modify: `apps/website/content/blog/2026-08-31-what-changes-when-the-runtime-changes.mdx:117`
+- Delete: `apps/website/src/components/docs/AgUiArchDiagram.tsx`
+- Modify: `apps/website/src/components/docs/MdxRenderer.tsx` (drop AgUiArchDiagram import + registration)
+- Modify: `apps/website/src/styles/docs.css` (delete the `.ag-ui-arch-*` rule block, ~lines 195–260 — find with `grep -n "ag-ui-arch" apps/website/src/styles/docs.css`)
+
+- [ ] **Step 1: Replace the ag-ui intro ascii diagram**
+
+In `ag-ui/getting-started/introduction.mdx`, replace the whole ```` ```text ```` fence under `## How it fits` (the `@threadplane/chat … +--> @threadplane/ag-ui` tree) with:
+
+```mdx
+
+```
+
+- [ ] **Step 2: Replace the agent-contract fan-in ascii**
+
+In `langgraph/concepts/agent-contract.mdx`, replace the ```` ```text ```` fence (the `LangGraph Platform -- @threadplane/langgraph --+ …` fan-in) with:
+
+```mdx
+
+```
+
+- [ ] **Step 3: Swap the two blog usages**
+
+Replace `` with `` in both blog posts listed above.
+
+- [ ] **Step 4: Retire the old component**
+
+Delete `AgUiArchDiagram.tsx`; remove its import and map entry from `MdxRenderer.tsx`; delete the `.ag-ui-arch-*` CSS block (including its mobile `@media` override — grep to catch all of it). Verify: `grep -rn "AgUiArchDiagram\|ag-ui-arch" apps/website` returns nothing.
+
+- [ ] **Step 5: Run tests + lint, verify the pages render**
+
+Run: `npx nx test website && npx nx lint website`
+Expected: PASS.
+Then start the dev server and load `/docs/ag-ui/getting-started/introduction`, `/docs/langgraph/concepts/agent-contract`, and one of the two blog posts; the kit diagram must render with no console errors.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add -A apps/website
+git commit -m "refactor(website): replace ascii + HTML arch diagrams with StackDiagram"
+```
+
+---
+
+### Task 5: "How it fits" for langgraph, chat, runtimes intros
+
+**Files:**
+- Modify: `apps/website/content/docs/langgraph/getting-started/introduction.mdx`
+- Modify: `apps/website/content/docs/chat/getting-started/introduction.mdx`
+- Modify: `apps/website/content/docs/runtimes/getting-started/introduction.mdx`
+
+- [ ] **Step 1: Add the sections**
+
+In each file, insert a `## How it fits` section directly after the intro paragraph(s) and before the first existing `##` heading (langgraph: before `## What is \`injectAgent()\`?`; chat: before `## Two-Tier Architecture`; runtimes: before `## The runtimes`). Content per file:
+
+langgraph:
+
+```mdx
+## How it fits
+
+
+```
+
+chat:
+
+```mdx
+## How it fits
+
+
+```
+
+runtimes:
+
+```mdx
+## How it fits
+
+
+```
+
+- [ ] **Step 2: Verify rendering, run tests, commit**
+
+Run: `npx nx test website`
+Expected: PASS. Load each of the three intro pages on the dev server; diagrams render.
+
+```bash
+git add apps/website/content/docs
+git commit -m "docs(website): How-it-fits stack diagrams for langgraph, chat, runtimes intros"
+```
+
+---
+
+### Task 6: AgUiArchitecturePipeline (ag-ui concepts/architecture)
+
+**Files:**
+- Create: `apps/website/src/components/docs/diagrams/AgUiArchitecturePipeline.tsx`
+- Modify: `apps/website/src/components/docs/MdxRenderer.tsx` (register)
+- Modify: `apps/website/content/docs/ag-ui/concepts/architecture.mdx` (replace the ```` ```text ```` vertical pipeline fence)
+- Modify: `apps/website/src/components/docs/diagrams/compositions.spec.tsx` (create; grows in later tasks)
+
+- [ ] **Step 1: Start the compositions spec (failing)**
+
+Create `compositions.spec.tsx`:
+
+```tsx
+// SPDX-License-Identifier: MIT
+// @vitest-environment jsdom
+import React from 'react';
+import { describe, expect, it } from 'vitest';
+import { render } from '@testing-library/react';
+import { AgUiArchitecturePipeline } from './AgUiArchitecturePipeline';
+
+/**
+ * Compositions are hand-placed layouts; the spec guards that each mounts,
+ * is labeled for screen readers, and names its load-bearing packages.
+ * Later tasks append one describe block per composition.
+ */
+describe('AgUiArchitecturePipeline', () => {
+ it('mounts with an accessible label and the pipeline stages', () => {
+ const { container } = render();
+ expect(container.querySelector('svg[role="img"]')?.getAttribute('aria-label')).toBeTruthy();
+ const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent);
+ expect(titles).toContain('@threadplane/ag-ui');
+ expect(titles).toContain('AbstractAgent');
+ });
+});
+```
+
+Run: `npx nx test website` — expected FAIL (module not found).
+
+- [ ] **Step 2: Implement the composition**
+
+```tsx
+import { DiagramFrame } from './DiagramFrame';
+import { DiagramNode } from './DiagramNode';
+import { DiagramEdge } from './DiagramEdge';
+
+const SLUG = 'agui-pipeline';
+
+/** Vertical pipeline from an Angular component down to the AG-UI backend. */
+export function AgUiArchitecturePipeline() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+Register in `MdxRenderer.tsx` (import + map entry `AgUiArchitecturePipeline,`).
+
+- [ ] **Step 3: Replace the mdx fence**
+
+In `ag-ui/concepts/architecture.mdx`, replace the ```` ```text ```` fence (the `Angular component | v @threadplane/chat …` chain) with ``.
+
+- [ ] **Step 4: Test + verify + commit**
+
+Run: `npx nx test website && npx nx lint website` — PASS. Load `/docs/ag-ui/concepts/architecture` on the dev server.
+
+```bash
+git add -A apps/website
+git commit -m "docs(website): kit pipeline diagram for AG-UI architecture page"
+```
+
+---
+
+### Task 7: A2uiMessageFlow (chat/a2ui overview + a2ui intro)
+
+**Files:**
+- Create: `apps/website/src/components/docs/diagrams/A2uiMessageFlow.tsx`
+- Modify: `apps/website/src/components/docs/MdxRenderer.tsx` (register)
+- Modify: `apps/website/content/docs/chat/a2ui/overview.mdx` (replace the FIRST ```` ```text ```` fence — the `assistant text starts with ---a2ui_JSON--- …` pipeline; the second fence, JSONL payload examples, stays)
+- Modify: `apps/website/content/docs/a2ui/getting-started/introduction.mdx` (add `## How it fits` before `## What the package owns`, reusing the same diagram)
+- Modify: `apps/website/src/components/docs/diagrams/compositions.spec.tsx`
+
+- [ ] **Step 1: Append the failing spec block**
+
+```tsx
+import { A2uiMessageFlow } from './A2uiMessageFlow';
+
+describe('A2uiMessageFlow', () => {
+ it('mounts and names the parser and surface store stages', () => {
+ const { container } = render();
+ expect(container.querySelector('svg[role="img"]')).not.toBeNull();
+ const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent);
+ expect(titles).toContain('createA2uiMessageParser()');
+ expect(titles).toContain('createA2uiSurfaceStore()');
+ });
+});
+```
+
+Run: `npx nx test website` — FAIL.
+
+- [ ] **Step 2: Implement**
+
+```tsx
+import { DiagramFrame } from './DiagramFrame';
+import { DiagramNode } from './DiagramNode';
+import { DiagramEdge } from './DiagramEdge';
+
+const SLUG = 'a2ui-flow';
+
+/** How an assistant message becomes a live A2UI surface. */
+export function A2uiMessageFlow() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+Register in `MdxRenderer.tsx`.
+
+- [ ] **Step 3: Wire into content**
+
+- `chat/a2ui/overview.mdx`: replace the first ```` ```text ```` fence with ``.
+- `a2ui/getting-started/introduction.mdx`: insert before `## What the package owns`:
+
+```mdx
+## How it fits
+
+
+```
+
+- [ ] **Step 4: Test + verify + commit**
+
+Run: `npx nx test website` — PASS. Load `/docs/chat/a2ui/overview` and `/docs/a2ui/getting-started/introduction`.
+
+```bash
+git add -A apps/website
+git commit -m "docs(website): A2UI message-flow diagram for overview and intro"
+```
+
+---
+
+### Task 8: Render diagrams (intro + json-render-vs-a2ui)
+
+**Files:**
+- Create: `apps/website/src/components/docs/diagrams/RenderHowItFits.tsx`
+- Create: `apps/website/src/components/docs/diagrams/RenderVsA2ui.tsx`
+- Modify: `apps/website/src/components/docs/MdxRenderer.tsx` (register both)
+- Modify: `apps/website/content/docs/render/getting-started/introduction.mdx` (add `## How it fits` before `## Why @threadplane/render?`)
+- Modify: `apps/website/content/docs/render/concepts/json-render-vs-a2ui.mdx` (replace the ```` ```text ```` three-package fence)
+- Modify: `apps/website/src/components/docs/diagrams/compositions.spec.tsx`
+
+- [ ] **Step 1: Append failing spec blocks**
+
+```tsx
+import { RenderHowItFits } from './RenderHowItFits';
+import { RenderVsA2ui } from './RenderVsA2ui';
+
+describe('RenderHowItFits', () => {
+ it('mounts and shows the spec-to-components pipeline', () => {
+ const { container } = render();
+ const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent);
+ expect(titles).toContain('@threadplane/render');
+ });
+});
+
+describe('RenderVsA2ui', () => {
+ it('mounts and shows both packages under chat', () => {
+ const { container } = render();
+ const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent);
+ expect(titles).toContain('@threadplane/render');
+ expect(titles).toContain('@threadplane/a2ui');
+ expect(titles).toContain('@threadplane/chat');
+ });
+});
+```
+
+Run: `npx nx test website` — FAIL.
+
+- [ ] **Step 2: Implement `RenderHowItFits.tsx`**
+
+```tsx
+import { DiagramFrame } from './DiagramFrame';
+import { DiagramNode } from './DiagramNode';
+import { DiagramEdge } from './DiagramEdge';
+import { DiagramPill } from './DiagramPill';
+
+const SLUG = 'render-fits';
+
+/** JSON spec in, your Angular components out. */
+export function RenderHowItFits() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+Note the pill overlaps its edge midpoint deliberately — the pill rect masks the line beneath the label.
+
+- [ ] **Step 3: Implement `RenderVsA2ui.tsx`**
+
+```tsx
+import { DiagramFrame } from './DiagramFrame';
+import { DiagramNode } from './DiagramNode';
+import { DiagramEdge } from './DiagramEdge';
+
+const SLUG = 'render-vs-a2ui';
+
+/** The three-package split behind generative UI in chat. */
+export function RenderVsA2ui() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+```
+
+Register both in `MdxRenderer.tsx`.
+
+- [ ] **Step 4: Wire into content**
+
+- `render/getting-started/introduction.mdx`: insert before `## Why @threadplane/render?`:
+
+```mdx
+## How it fits
+
+
+```
+
+- `render/concepts/json-render-vs-a2ui.mdx`: replace the ```` ```text ```` fence with ``.
+
+- [ ] **Step 5: Test + verify + commit**
+
+Run: `npx nx test website` — PASS. Load both pages on the dev server.
+
+```bash
+git add -A apps/website
+git commit -m "docs(website): render pipeline + render-vs-a2ui kit diagrams"
+```
+
+---
+
+### Task 9: MiddlewareHowItFits + TelemetryHowItFits
+
+**Files:**
+- Create: `apps/website/src/components/docs/diagrams/MiddlewareHowItFits.tsx`
+- Create: `apps/website/src/components/docs/diagrams/TelemetryHowItFits.tsx`
+- Modify: `apps/website/src/components/docs/MdxRenderer.tsx` (register both)
+- Modify: `apps/website/content/docs/middleware/getting-started/introduction.mdx` (add `## How it fits` before `## What it does`)
+- Modify: `apps/website/content/docs/telemetry/getting-started/introduction.mdx` (add `## How it fits` before `## Entry points`)
+- Modify: `apps/website/src/components/docs/diagrams/compositions.spec.tsx`
+
+- [ ] **Step 1: Append failing spec blocks**
+
+```tsx
+import { MiddlewareHowItFits } from './MiddlewareHowItFits';
+import { TelemetryHowItFits } from './TelemetryHowItFits';
+
+describe('MiddlewareHowItFits', () => {
+ it('mounts and places the middleware between frontend and graph', () => {
+ const { container } = render();
+ const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent);
+ expect(titles).toContain('threadplane-middleware');
+ });
+});
+
+describe('TelemetryHowItFits', () => {
+ it('mounts and shows both entry points feeding ingest', () => {
+ const { container } = render();
+ const titles = Array.from(container.querySelectorAll('.tp-diagram-title')).map((t) => t.textContent);
+ expect(titles).toContain('@threadplane/telemetry');
+ });
+});
+```
+
+Run: `npx nx test website` — FAIL.
+
+- [ ] **Step 2: Implement `MiddlewareHowItFits.tsx`** (horizontal: frontend → middleware → graph)
+
+```tsx
+import { DiagramFrame } from './DiagramFrame';
+import { DiagramNode } from './DiagramNode';
+import { DiagramEdge } from './DiagramEdge';
+import { DiagramPill } from './DiagramPill';
+
+const SLUG = 'middleware-fits';
+
+/** The Python middleware sits between the Angular frontend and your graph. */
+export function MiddlewareHowItFits() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+- [ ] **Step 3: Implement `TelemetryHowItFits.tsx`** (fan-in: two entry points → package → ingest)
+
+```tsx
+import { DiagramFrame } from './DiagramFrame';
+import { DiagramNode } from './DiagramNode';
+import { DiagramEdge } from './DiagramEdge';
+import { DiagramPill } from './DiagramPill';
+
+const SLUG = 'telemetry-fits';
+
+/** Browser and Node entry points feed sampled events to the ingest endpoint. */
+export function TelemetryHowItFits() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+Register both in `MdxRenderer.tsx`.
+
+Before wiring content, read both intros and adjust the eyebrow/meta strings to match what those pages actually claim (e.g. exact entry-point names in `telemetry/getting-started/introduction.mdx` under `## Entry points`) — the topology stays as coded.
+
+- [ ] **Step 4: Wire into content**
+
+Insert into each intro at the position listed in **Files**:
+
+```mdx
+## How it fits
+
+
+```
+
+```mdx
+## How it fits
+
+
+```
+
+- [ ] **Step 5: Test + verify + commit**
+
+Run: `npx nx test website && npx nx lint website` — PASS. Load both intro pages.
+
+```bash
+git add -A apps/website
+git commit -m "docs(website): middleware + telemetry How-it-fits diagrams"
+```
+
+---
+
+### Task 10: Marketing — StackDiagramSection + homepage
+
+**Files:**
+- Create: `apps/website/src/components/landing/StackDiagramSection.tsx`
+- Create: `apps/website/src/components/landing/StackDiagramSection.spec.tsx`
+- Modify: `apps/website/src/styles/landing.css` (append section classes)
+- Modify: `apps/website/src/app/page.tsx` (insert after ``)
+
+- [ ] **Step 1: Write the failing spec**
+
+```tsx
+// SPDX-License-Identifier: MIT
+// @vitest-environment jsdom
+import React from 'react';
+import { describe, expect, it } from 'vitest';
+import { render } from '@testing-library/react';
+import { StackDiagramSection } from './StackDiagramSection';
+
+describe('StackDiagramSection', () => {
+ it('renders heading, body, and a marketing-scaled stack diagram', () => {
+ const { container, getByText } = render(
+
+ );
+ expect(getByText('One contract between your UI and any runtime').tagName).toBe('H2');
+ expect(container.querySelector('section')?.getAttribute('aria-labelledby')).toBe('architecture-heading');
+ expect(container.querySelector('figure.tp-diagram-figure')?.getAttribute('data-scale')).toBe('marketing');
+ });
+});
+```
+
+Run: `npx nx test website` — FAIL.
+
+- [ ] **Step 2: Implement the section component**
+
+```tsx
+import type { ReactNode } from 'react';
+import { Section } from '../ui/Section';
+import { Container } from '../ui/Container';
+import { Eyebrow } from '../ui/Eyebrow';
+import { StackDiagram, type StackHighlight } from '../docs/diagrams/StackDiagram';
+
+interface StackDiagramSectionProps {
+ id: string;
+ eyebrow: string;
+ headline: string;
+ body: ReactNode;
+ highlight?: StackHighlight;
+ caption?: string;
+}
+
+export function StackDiagramSection({
+ id,
+ eyebrow,
+ headline,
+ body,
+ highlight = 'none',
+ caption,
+}: StackDiagramSectionProps) {
+ return (
+
+
+
+ {eyebrow}
+
+ {headline}
+
+
{body}
+
+
+
+
+ );
+}
+```
+
+Append to `landing.css` (match the file's existing class style — check its heading classes for the exact Garamond pattern before writing):
+
+```css
+/* StackDiagramSection */
+.stack-diagram-section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ gap: 12px;
+}
+.stack-diagram-headline {
+ font-family: var(--font-garamond);
+ font-size: clamp(1.75rem, 3.5vw, 2.5rem);
+ color: var(--color-text-primary);
+ margin: 0;
+}
+.stack-diagram-body {
+ font-family: var(--font-inter);
+ font-size: 1.05rem;
+ line-height: 1.6;
+ color: var(--color-text-secondary);
+ max-width: 620px;
+ margin: 0 0 12px;
+}
+```
+
+- [ ] **Step 3: Insert on the homepage**
+
+In `apps/website/src/app/page.tsx`, import `StackDiagramSection` and insert between `` and the DemoShowcase ``:
+
+```tsx
+
+```
+
+- [ ] **Step 4: Test + verify + commit**
+
+Run: `npx nx test website && npx nx lint website` — PASS. Load `/` on the dev server; check the section renders between the Yes wall and the demo showcase, at both desktop and 375px widths (the figure scrolls horizontally, the page must not).
+
+```bash
+git add -A apps/website
+git commit -m "feat(website): homepage architecture section with the master stack diagram"
+```
+
+---
+
+### Task 11: Marketing — adapter pages (/langgraph, /ag-ui)
+
+**Files:**
+- Modify: `apps/website/src/app/langgraph/page.tsx`
+- Modify: `apps/website/src/app/ag-ui/page.tsx`
+
+- [ ] **Step 1: Insert the sections**
+
+In each page, import `StackDiagramSection` from `../../components/landing/StackDiagramSection` and insert directly after the hero `` (the first Section in the returned JSX):
+
+`/langgraph`:
+
+```tsx
+
+```
+
+`/ag-ui`:
+
+```tsx
+
+```
+
+- [ ] **Step 2: Test + verify + commit**
+
+Run: `npx nx test website` — PASS (both pages have `page.spec.tsx` files; if a spec asserts on section order/count, update it to include the new section). Load `/langgraph` and `/ag-ui` on the dev server.
+
+```bash
+git add apps/website/src/app/langgraph/page.tsx apps/website/src/app/ag-ui/page.tsx
+git commit -m "feat(website): stack diagram sections on the adapter landing pages"
+```
+
+---
+
+### Task 12: Full verification pass
+
+- [ ] **Step 1: Suite + lint**
+
+Run: `npx nx test website && npx nx lint website`
+Expected: PASS, no lint errors (warnings tolerated; errors not — pipe through `sed -e 's/\x1b\[[0-9;]*m//g'` before grepping if counting).
+
+- [ ] **Step 2: Production build**
+
+Run: `npx nx build website --configuration=production`
+Expected: builds clean.
+
+- [ ] **Step 3: Visual sweep**
+
+On the dev server, load and eyeball every touched page: the 4 docs pages with replaced diagrams, the 7 intros, one blog post, `/`, `/langgraph`, `/ag-ui`. Check: no horizontal page scroll at 375px (diagrams scroll inside their figure), callouts render the band on pages using `` (e.g. `/docs/ag-ui/getting-started/introduction` has two), no console errors.
+
+- [ ] **Step 4: Commit any fixes**
+
+```bash
+git add -A apps/website
+git commit -m "fix(website): visual-sweep fixes for callouts and diagrams"
+```
+
+(Skip the commit if the sweep found nothing.)
diff --git a/docs/superpowers/specs/2026-09-01-docs-visual-design-design.md b/docs/superpowers/specs/2026-09-01-docs-visual-design-design.md
new file mode 100644
index 000000000..5f32dd7be
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-01-docs-visual-design-design.md
@@ -0,0 +1,155 @@
+# Docs visual design: callouts, diagram kit, and the diagram audit
+
+**Date:** 2026-09-01
+**Status:** Approved for planning
+**Scope:** apps/website — docs MDX components, docs content, and a marketing follow-up
+
+## Motivation
+
+Two visual problems on the docs side, one gap on the marketing side:
+
+1. The MDX callout is a left-border box with a solid mono-letter icon circle — it
+ does not speak the visual language the docs sidebar landed on (panels,
+ tinted chips, mono uppercase labels, hairline borders).
+2. Architecture explanations ("How it fits") are ascii diagrams inside
+ ```` ```text ```` fences. They are load-bearing content rendered in the
+ least polished form on the page.
+3. No marketing page uses a single architecture graphic; the two existing
+ diagram components are docs-only.
+
+Decisions below were made interactively (visual companion session,
+`.superpowers/brainstorm/76640-1788300865`): callout direction **B — header
+band**; diagram style **B — SVG schematic on dot grid**; production approach
+**A — diagram kit**; docs scope **B — replace ascii + every intro gets a
+diagram**; marketing scope **1+2 — homepage master diagram + adapter pages**.
+
+## 1. Callout redesign (header band)
+
+`components/docs/mdx/Callout.tsx` keeps its public API unchanged:
+`type?: 'tip' | 'warning' | 'info' | 'danger'`, `title?: string`, `children`.
+
+Visual spec (all presentation in `src/styles/docs.css`):
+
+- **Card:** 1px solid `--color-border`, `--radius-md`, `--color-surface`
+ background, `overflow: hidden`. No left accent border.
+- **Header band:** a full-width strip, tone-tinted background at ~6% opacity,
+ hairline bottom border, containing an outline SVG icon (stroke style, ~14px)
+ and the title, both in the tone's text color. Padding ~8px 14px.
+- **Body:** plain surface below the band, Inter 15px / 1.6,
+ `--color-text-secondary`, padding ~12px 14px.
+- **Title fallback:** when `title` is omitted, the band renders the kind name
+ ("Note", "Tip", "Warning", "Danger") so it is never empty. `info` renders as
+ "Note".
+- **Icons:** outline SVGs replacing the solid letter circles — info: circled i,
+ tip: check-circle, warning: triangle-alert, danger: octagon/circle-x. Inline
+ `