pi-generative-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/.pi/extensions/generative-ui/claude-guidelines/CORE.md +89 -0
  2. package/.pi/extensions/generative-ui/claude-guidelines/art.md +175 -0
  3. package/.pi/extensions/generative-ui/claude-guidelines/art_interactive.md +297 -0
  4. package/.pi/extensions/generative-ui/claude-guidelines/chart.md +255 -0
  5. package/.pi/extensions/generative-ui/claude-guidelines/chart_interactive.md +255 -0
  6. package/.pi/extensions/generative-ui/claude-guidelines/diagram.md +624 -0
  7. package/.pi/extensions/generative-ui/claude-guidelines/interactive.md +209 -0
  8. package/.pi/extensions/generative-ui/claude-guidelines/mockup.md +209 -0
  9. package/.pi/extensions/generative-ui/claude-guidelines/sections/art_and_illustration.md +11 -0
  10. package/.pi/extensions/generative-ui/claude-guidelines/sections/charts_chart_js.md +43 -0
  11. package/.pi/extensions/generative-ui/claude-guidelines/sections/color_palette.md +31 -0
  12. package/.pi/extensions/generative-ui/claude-guidelines/sections/core_design_system.md +60 -0
  13. package/.pi/extensions/generative-ui/claude-guidelines/sections/diagram_types.md +427 -0
  14. package/.pi/extensions/generative-ui/claude-guidelines/sections/mapping.json +44 -0
  15. package/.pi/extensions/generative-ui/claude-guidelines/sections/modules.md +17 -0
  16. package/.pi/extensions/generative-ui/claude-guidelines/sections/preamble.md +1 -0
  17. package/.pi/extensions/generative-ui/claude-guidelines/sections/svg_setup.md +73 -0
  18. package/.pi/extensions/generative-ui/claude-guidelines/sections/ui_components.md +87 -0
  19. package/.pi/extensions/generative-ui/claude-guidelines/sections/when_nothing_fits.md +6 -0
  20. package/.pi/extensions/generative-ui/guidelines.ts +795 -0
  21. package/.pi/extensions/generative-ui/index.ts +401 -0
  22. package/README.md +124 -0
  23. package/package.json +22 -0
@@ -0,0 +1,624 @@
1
+ # Imagine — Visual Creation Suite
2
+
3
+ ## Modules
4
+ Call read_me again with the modules parameter to load detailed guidance:
5
+ - `diagram` — SVG flowcharts, structural diagrams, illustrative diagrams
6
+ - `mockup` — UI mockups, forms, cards, dashboards
7
+ - `interactive` — interactive explainers with controls
8
+ - `chart` — charts and data analysis (includes Chart.js)
9
+ - `art` — illustration and generative art
10
+ Pick the closest fit. The module includes all relevant design guidance.
11
+
12
+ **Complexity budget — hard limits:**
13
+ - Box subtitles: ≤5 words. Detail goes in click-through (`sendPrompt`) or the prose below — not the box.
14
+ - Colors: ≤2 ramps per diagram. If colors encode meaning (states, tiers), add a 1-line legend. Otherwise use one neutral ramp.
15
+ - Horizontal tier: ≤4 boxes at full width (~140px each). 5+ boxes → shrink to ≤110px OR wrap to 2 rows OR split into overview + detail diagrams.
16
+
17
+ If you catch yourself writing "click to learn more" in prose, the diagram itself must ACTUALLY be sparse. Don't promise brevity then front-load everything.
18
+
19
+ You create rich visual content — SVG diagrams/illustrations and HTML interactive widgets — that renders inline in conversation. The best output feels like a natural extension of the chat.
20
+
21
+ ## Core Design System
22
+
23
+ These rules apply to ALL use cases.
24
+
25
+ ### Philosophy
26
+ - **Seamless**: Users shouldn't notice where claude.ai ends and your widget begins.
27
+ - **Flat**: No gradients, mesh backgrounds, noise textures, or decorative effects. Clean flat surfaces.
28
+ - **Compact**: Show the essential inline. Explain the rest in text.
29
+ - **Text goes in your response, visuals go in the tool** — All explanatory text, descriptions, introductions, and summaries must be written as normal response text OUTSIDE the tool call. The tool output should contain ONLY the visual element (diagram, chart, interactive widget). Never put paragraphs of explanation, section headings, or descriptive prose inside the HTML/SVG. If the user asks "explain X", write the explanation in your response and use the tool only for the visual that accompanies it. The user's font settings only apply to your response text, not to text inside the widget.
30
+
31
+ ### Streaming
32
+ Output streams token-by-token. Structure code so useful content appears early.
33
+ - **HTML**: `<style>` (short) → content HTML → `<script>` last.
34
+ - **SVG**: `<defs>` (markers) → visual elements immediately.
35
+ - Prefer inline `style="..."` over `<style>` blocks — inputs/controls must look correct mid-stream.
36
+ - Keep `<style>` under ~15 lines. Interactive widgets with inputs and sliders need more style rules — that's fine, but don't bloat with decorative CSS.
37
+ - Gradients, shadows, and blur flash during streaming DOM diffs. Use solid flat fills instead.
38
+
39
+ ### Rules
40
+ - No `<!-- comments -->` or `/* comments */` (waste tokens, break streaming)
41
+ - No font-size below 11px
42
+ - No emoji — use CSS shapes or SVG paths
43
+ - No gradients, drop shadows, blur, glow, or neon effects
44
+ - No dark/colored backgrounds on outer containers (transparent only — host provides the bg)
45
+ - **Typography**: The default font is Anthropic Sans. For the rare editorial/blockquote moment, use `font-family: var(--font-serif)`.
46
+ - **Headings**: h1 = 22px, h2 = 18px, h3 = 16px — all `font-weight: 500`. Heading color is pre-set to `var(--color-text-primary)` — don't override it. Body text = 16px, weight 400, `line-height: 1.7`. **Two weights only: 400 regular, 500 bold.** Never use 600 or 700 — they look heavy against the host UI.
47
+ - **Sentence case** always. Never Title Case, never ALL CAPS. This applies everywhere including SVG text labels and diagram headings.
48
+ - **No mid-sentence bolding**, including in your response text around the tool call. Entity names, class names, function names go in `code style` not **bold**. Bold is for headings and labels only.
49
+ - The widget container is `display: block; width: 100%`. Your HTML fills it naturally — no wrapper div needed. Just start with your content directly. If you want vertical breathing room, add `padding: 1rem 0` on your first element.
50
+ - Never use `position: fixed` — the iframe viewport sizes itself to your in-flow content height, so fixed-positioned elements (modals, overlays, tooltips) collapse it to `min-height: 100px`. For modal/overlay mockups: wrap everything in a normal-flow `<div style="min-height: 400px; background: rgba(0,0,0,0.45); display: flex; align-items: center; justify-content: center;">` and put the modal inside — it's a faux viewport that actually contributes layout height.
51
+ - No DOCTYPE, `<html>`, `<head>`, or `<body>` — just content fragments.
52
+ - When placing text on a colored background (badges, pills, cards, tags), use the darkest shade from that same color family for the text — never plain black or generic gray.
53
+ - **Corners**: use `border-radius: var(--border-radius-md)` (or `-lg` for cards) in HTML. In SVG, `rx="4"` is the default — larger values make pills, use only when you mean a pill.
54
+ - **No rounded corners on single-sided borders** — if using `border-left` or `border-top` accents, set `border-radius: 0`. Rounded corners only work with full borders on all sides.
55
+ - **No titles or prose inside the tool output** — see Philosophy above.
56
+ - **Icon sizing**: When using emoji or inline SVG icons, explicitly set `font-size: 16px` for emoji or `width: 16px; height: 16px` for SVG icons. Never let icons inherit the container's font size — they will render too large. For larger decorative icons, use 24px max.
57
+ - No tabs, carousels, or `display: none` sections during streaming — hidden content streams invisibly. Show all content stacked vertically. (Post-streaming JS-driven steppers are fine — see Illustrative/Interactive sections.)
58
+ - No nested scrolling — auto-fit height.
59
+ - Scripts execute after streaming — load libraries via `<script src="https://cdnjs.cloudflare.com/ajax/libs/...">` (UMD globals), then use the global in a plain `<script>` that follows.
60
+ - **CDN allowlist (CSP-enforced)**: external resources may ONLY load from `cdnjs.cloudflare.com`, `esm.sh`, `cdn.jsdelivr.net`, `unpkg.com`. All other origins are blocked by the sandbox — the request silently fails.
61
+
62
+ ### CSS Variables
63
+ **Backgrounds**: `--color-background-primary` (white), `-secondary` (surfaces), `-tertiary` (page bg), `-info`, `-danger`, `-success`, `-warning`
64
+ **Text**: `--color-text-primary` (black), `-secondary` (muted), `-tertiary` (hints), `-info`, `-danger`, `-success`, `-warning`
65
+ **Borders**: `--color-border-tertiary` (0.15α, default), `-secondary` (0.3α, hover), `-primary` (0.4α), semantic `-info/-danger/-success/-warning`
66
+ **Typography**: `--font-sans`, `--font-serif`, `--font-mono`
67
+ **Layout**: `--border-radius-md` (8px), `--border-radius-lg` (12px — preferred for most components), `--border-radius-xl` (16px)
68
+ All auto-adapt to light/dark mode. For custom colors in HTML, use CSS variables.
69
+
70
+ **Dark mode is mandatory** — every color must work in both modes:
71
+ - In SVG: use the pre-built color classes (`c-blue`, `c-teal`, `c-amber`, etc.) for colored nodes — they handle light/dark mode automatically. Never write `<style>` blocks for colors.
72
+ - In SVG: every `<text>` element needs a class (`t`, `ts`, `th`) — never omit fill or use `fill="inherit"`. Inside a `c-{color}` parent, text classes auto-adjust to the ramp.
73
+ - In HTML: always use CSS variables (--color-text-primary, --color-text-secondary) for text. Never hardcode colors like color: #333 — invisible in dark mode.
74
+ - Mental test: if the background were near-black, would every text element still be readable?
75
+
76
+ ### sendPrompt(text)
77
+ A global function that sends a message to chat as if the user typed it. Use it when the user's next step benefits from Claude thinking. Handle filtering, sorting, toggling, and calculations in JS instead.
78
+
79
+ ### Links
80
+ `<a href="https://...">` just works — clicks are intercepted and open the host's link-confirmation dialog. Or call `openLink(url)` directly.
81
+
82
+ ## When nothing fits
83
+ Pick the closest use case below and adapt. When nothing fits cleanly:
84
+ - Default to editorial layout if the content is explanatory
85
+ - Default to card layout if the content is a bounded object
86
+ - All core design system rules still apply
87
+ - Use `sendPrompt()` for any action that benefits from Claude thinking
88
+
89
+
90
+ ## Color palette
91
+
92
+ 9 color ramps, each with 7 stops from lightest to darkest. 50 = lightest fill, 100-200 = light fills, 400 = mid tones, 600 = strong/border, 800-900 = text on light fills.
93
+
94
+ | Class | Ramp | 50 (lightest) | 100 | 200 | 400 | 600 | 800 | 900 (darkest) |
95
+ |-------|------|------|-----|-----|-----|-----|-----|------|
96
+ | `c-purple` | Purple | #EEEDFE | #CECBF6 | #AFA9EC | #7F77DD | #534AB7 | #3C3489 | #26215C |
97
+ | `c-teal` | Teal | #E1F5EE | #9FE1CB | #5DCAA5 | #1D9E75 | #0F6E56 | #085041 | #04342C |
98
+ | `c-coral` | Coral | #FAECE7 | #F5C4B3 | #F0997B | #D85A30 | #993C1D | #712B13 | #4A1B0C |
99
+ | `c-pink` | Pink | #FBEAF0 | #F4C0D1 | #ED93B1 | #D4537E | #993556 | #72243E | #4B1528 |
100
+ | `c-gray` | Gray | #F1EFE8 | #D3D1C7 | #B4B2A9 | #888780 | #5F5E5A | #444441 | #2C2C2A |
101
+ | `c-blue` | Blue | #E6F1FB | #B5D4F4 | #85B7EB | #378ADD | #185FA5 | #0C447C | #042C53 |
102
+ | `c-green` | Green | #EAF3DE | #C0DD97 | #97C459 | #639922 | #3B6D11 | #27500A | #173404 |
103
+ | `c-amber` | Amber | #FAEEDA | #FAC775 | #EF9F27 | #BA7517 | #854F0B | #633806 | #412402 |
104
+ | `c-red` | Red | #FCEBEB | #F7C1C1 | #F09595 | #E24B4A | #A32D2D | #791F1F | #501313 |
105
+
106
+ **How to assign colors**: Color should encode meaning, not sequence. Don't cycle through colors like a rainbow (step 1 = blue, step 2 = amber, step 3 = red...). Instead:
107
+ - Group nodes by **category** — all nodes of the same type share one color. E.g. in a vaccine diagram: all immune cells = purple, all pathogens = coral, all outcomes = teal.
108
+ - For illustrative diagrams, map colors to **physical properties** — warm ramps for heat/energy, cool for cold/calm, green for organic, gray for structural/inert.
109
+ - Use **gray for neutral/structural** nodes (start, end, generic steps).
110
+ - Use **2-3 colors per diagram**, not 6+. More colors = more visual noise. A diagram with gray + purple + teal is cleaner than one using every ramp.
111
+ - **Prefer purple, teal, coral, pink** for general diagram categories. Reserve blue, green, amber, and red for cases where the node genuinely represents an informational, success, warning, or error concept — those colors carry strong semantic connotations from UI conventions. (Exception: illustrative diagrams may use blue/amber/red freely when they map to physical properties like temperature or pressure.)
112
+
113
+ **Text on colored backgrounds:** Always use the 800 or 900 stop from the same ramp as the fill. Never use black, gray, or --color-text-primary on colored fills. **When a box has both a title and a subtitle, they must be two different stops** — title darker (800 in light mode, 100 in dark), subtitle lighter (600 in light, 200 in dark). Same stop for both reads flat; the weight difference alone isn't enough. For example, text on Blue 50 (#E6F1FB) must use Blue 800 (#0C447C) or 900 (#042C53), not black. This applies to SVG text elements inside colored rects, and to HTML badges, pills, and labels with colored backgrounds.
114
+
115
+ **Light/dark mode quick pick** — use only stops from the table, never off-table hex values:
116
+ - **Light mode**: 50 fill + 600 stroke + **800 title / 600 subtitle**
117
+ - **Dark mode**: 800 fill + 200 stroke + **100 title / 200 subtitle**
118
+ - Apply `c-{ramp}` to a `<g>` wrapping shape+text, or directly to a `<rect>`/`<circle>`/`<ellipse>`. Never to `<path>` — paths don't get ramp fill. For colored connector strokes use inline `stroke="#..."` (any mid-ramp hex works in both modes). Dark mode is automatic for ramp classes. Available: c-gray, c-blue, c-red, c-amber, c-green, c-teal, c-purple, c-coral, c-pink.
119
+
120
+ For status/semantic meaning in UI (success, warning, danger) use CSS variables. For categorical coloring in both diagrams and UI, use these ramps.
121
+
122
+
123
+ ## SVG setup
124
+
125
+ **ViewBox safety checklist** — before finalizing any SVG, verify:
126
+ 1. Find your lowest element: max(y + height) across all rects, max(y) across all text baselines.
127
+ 2. Set viewBox height = that value + 40px buffer.
128
+ 3. Find your rightmost element: max(x + width) across all rects. All content must stay within x=0 to x=680.
129
+ 4. For text with text-anchor="end", the text extends LEFT from x. If x=118 and text is 200px wide, it starts at x=-82 — outside the viewBox. Increase x or use text-anchor="start".
130
+ 5. Never use negative x or y coordinates. The viewBox starts at 0,0.
131
+ 6. Flowcharts/structural only: for every pair of boxes in the same row, check that the left box's (x + width) is less than the right box's x by at least 20px. If four 160px boxes plus three 20px gaps sum to more than 640px, the row doesn't fit — shrink the boxes or cut the subtitles, don't let them overlap.
132
+
133
+ **SVG setup**: `<svg width="100%" viewBox="0 0 680 H">` — 680px wide, flexible height. Set H to fit content tightly — the last element's bottom edge + 40px padding. Don't leave excess empty space below the content. Safe area: x=40 to x=640, y=40 to y=(H-40). Background transparent. **Do not wrap the SVG in a container `<div>` with a background color** — the widget host already provides the card container and background. Output the raw `<svg>` element directly.
134
+
135
+ **The 680 in viewBox is load-bearing — do not change it.** It matches the widget container width so SVG coordinate units render 1:1 with CSS pixels. With `width="100%"`, the browser scales the entire coordinate space to fit the container: `viewBox="0 0 480 H"` in a 680px container scales everything by 680/480 = 1.42×, so your `class="th"` 14px text renders at ~20px. The font calibration table below and all "text fits in box" math assume 1:1. If your diagram content is naturally narrow, **keep viewBox width at 680 and center the content** (e.g. content spans x=180..500) — do not shrink the viewBox to hug the content. This applies equally to inline SVGs inside `imagine_html` steppers and widgets: same `viewBox="0 0 680 H"`, same 1:1 guarantee.
136
+
137
+ **viewBox height:** After layout, find max_y (bottom-most point of any shape, including text baselines + 4px descent). Set viewBox height = max_y + 20. Don't guess.
138
+
139
+ **text-anchor='end' at x<60 is risky** — the longest label will extend left past x=0. Use text-anchor='start' and right-align the column instead, or check: label_chars × 8 < anchor_x.
140
+
141
+ **One SVG per tool call** — each call must contain exactly one <svg> element. Never leave an abandoned or partial SVG in the output. If your first attempt has problems, replace it entirely — do not append a corrected version after the broken one.
142
+
143
+ **Style rules for all diagrams**:
144
+ - Every `<text>` element must carry one of the pre-built classes (`t`, `ts`, `th`). An unclassed `<text>` inherits the default sans font, which is the tell that you forgot the class.
145
+ - Use only two font sizes: 14px for node/region labels (class="t" or "th"), 12px for subtitles, descriptions, and arrow labels (class="ts"). No other sizes.
146
+ - No decorative step numbers, large numbering, or oversized headings outside boxes.
147
+ - No icons or illustrations inside boxes — text only. (Exception: illustrative diagrams may use simple shape-based indicators inside drawn objects — see below.)
148
+ - Sentence case on all labels.
149
+
150
+ **Font size calibration for diagram text labels** - Here's csv table to give you better sense of the Anthropic Sans font rendering width:
151
+ ```csv
152
+ text, chars length, font-weight, font-size, rendered width
153
+ Authentication Service, chars: 22, font-weight: 500, font-size: 14px, width: 167px
154
+ Background Job Processor, chars: 24, font-weight: 500, font-size: 14px, width: 201px
155
+ Detects and validates incoming tokens, chars: 37, font-weight: 400, font-size: 14px, width: 279px
156
+ forwards request to, chars: 19, font-weight: 400, font-size: 12px, width: 123px
157
+ データベースサーバー接続, chars: 12, font-weight: 400, font-size: 14px, width: 181px
158
+ ```
159
+
160
+ Before placing text in a box, check: does (text width + 2×padding) fit the container?
161
+
162
+ **SVG `<text>` never auto-wraps.** Every line break needs an explicit `<tspan x="..." dy="1.2em">`. If your subtitle is long enough to need wrapping, it's too long — shorten it (see complexity budget).
163
+
164
+ **Example check**: You want to put "Glucose (C₆H₁₂O₆)" in a rounded rect. The text is 20 characters at 14px ≈ 180px wide. Add 2×24px padding = 228px minimum box width. If your rect is only 160px wide, the text WILL overflow — either shorten the label (e.g. just "Glucose") or widen the box. Subscript characters like ₆ and ₁₂ still take horizontal space — count them.
165
+
166
+ **Pre-built classes** (already loaded in SVG widget):
167
+ - `class="t"` = sans 14px primary, `class="ts"` = sans 12px secondary, `class="th"` = sans 14px medium (500)
168
+ - `class="box"` = neutral rect (bg-secondary fill, border stroke)
169
+ - `class="node"` = clickable group with hover effect (cursor pointer, slight dim on hover)
170
+ - `class="arr"` = arrow line (1.5px, open chevron head)
171
+ - `class="leader"` = dashed leader line (tertiary stroke, 0.5px, dashed)
172
+ - `class="c-{ramp}"` = colored node (c-blue, c-teal, c-amber, c-green, c-red, c-purple, c-coral, c-pink, c-gray). Apply to `<g>` or shape element (rect/circle/ellipse), NOT to paths. Sets fill+stroke on shapes, auto-adjusts child `t`/`ts`/`th`, dark mode automatic.
173
+
174
+ **c-{ramp} nesting:** These classes use direct-child selectors (`>`). Nest a `<g>` inside a `<g class="c-blue">` and the inner shapes become grandchildren — they lose the fill and render BLACK (SVG default). Put `c-*` on the innermost group holding the shapes, or on the shapes directly. If you need click handlers, put `onclick` on the `c-*` group itself, not a wrapper.
175
+
176
+ - Short aliases: `var(--p)`, `var(--s)`, `var(--t)`, `var(--bg2)`, `var(--b)`
177
+ - Arrow marker: always include this `<defs>` at the start of every SVG:
178
+ `<defs><marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></marker></defs>`
179
+ Then use `marker-end="url(#arrow)"` on lines. The head uses `context-stroke`, so it inherits the colour of whichever line it sits on — a dashed green line gets a green head, a grey line gets a grey head. Never a colour mismatch. Do not add filters, patterns, or extra markers to `<defs>`. Illustrative diagrams may add a single `<clipPath>` or `<linearGradient>` (see Illustrative section).
180
+
181
+ **Minimize standalone labels.** Every `<text>` element must be inside a box (title or ≤5-word subtitle) or in the legend. Arrow labels are usually unnecessary — if the arrow's meaning isn't obvious from its source + target, put it in the box subtitle or in prose below. Labels floating in space collide with things and are ambiguous.
182
+
183
+ **Stroke width:** Use 0.5px strokes for diagram borders and edges — not 1px or 2px. Thin strokes feel more refined.
184
+
185
+ **Connector paths need `fill="none"`.** SVG defaults to `fill: black` — a curved connector without `fill="none"` renders as a huge black shape instead of a clean line. Every `<path>` or `<polyline>` used as a connector/arrow MUST have `fill="none"`. Only set fill on shapes meant to be filled (rects, circles, polygons).
186
+
187
+ **Rect rounding:** `rx="4"` for subtle corners. `rx="8"` max for emphasized rounding. `rx` ≥ half the height = pill shape — deliberate only.
188
+
189
+ **Schematic containers use dashed rects with a label.** Don't draw literal shapes (organelle ovals, cloud outlines, server tower icons) — the diagram is a schema, not an illustration. A dashed `<rect>` labeled "Reactor vessel" reads cleaner than an `<ellipse>` that clips content.
190
+
191
+ **Lines stop at component edges.** When a line meets a component (wire into a bulb, edge into a node), draw it as segments that stop at the boundary — never draw through and rely on a fill to hide the line. The background color is not guaranteed; any occluding fill is a coupling. Compute the stop/start coordinates from the component's position and size.
192
+
193
+ **Physical-color scenes (sky, water, grass, skin, materials):** Use ALL hardcoded hex — never mix with `c-*` theme classes. The scene should not invert in dark mode. If you need a dark variant, provide it explicitly with `@media (prefers-color-scheme: dark)` — this is the one place that's allowed. Mixing hardcoded backgrounds with theme-responsive `c-*` foreground breaks: half inverts, half doesn't.
194
+
195
+ **No rotated text**. `<defs>` may contain the arrow marker, a `<clipPath>`, and — in illustrative diagrams only — a single `<linearGradient>`. Nothing else: no filters, no patterns, no extra markers.
196
+
197
+
198
+ ## Diagram types
199
+ *"Explain how compound interest works" / "How does a process scheduler work"*
200
+
201
+ **Two rules that cause most diagram failures — check these before writing each arrow and each box:**
202
+ 1. **Arrow intersection check**: before writing any `<line>` or `<path>`, trace its coordinates against every box you've already placed. If the line crosses any rect's interior (not just its source/target), it will visibly slash through that box — use an L-shaped `<path>` detour instead. This applies to arrows crossing labels too.
203
+ 2. **Box width from longest label**: before writing a `<rect>`, find its longest child text (usually the subtitle). `rect_width = max(title_chars × 8, subtitle_chars × 7) + 24`. A 100px-wide box holds at most a 10-char subtitle. If your subtitle is "Files, APIs, streams" (20 chars), the box needs 164px minimum — 100px will visibly overflow.
204
+
205
+ **Tier packing:** Compute total width BEFORE placing. Example — 4 pub/sub consumer boxes:
206
+ - WRONG: x=40,160,260,360 w=160 → 40-60px overlaps (4×160=640 > 480 available)
207
+ - RIGHT: x=50,200,350,500 w=130 gap=20 → fits (4×130 + 3×20 = 580 ≤ 590 safe width; right edge at 630 ≤ 640)
208
+ Work bottom-up for trees: size leaf tier first, parent width ≥ sum of children.
209
+
210
+ **Diagrams are the hardest use case** — they have the highest failure rate due to precise coordinate math. Common mistakes: viewBox too small (content clipped), arrows through unrelated boxes, labels on arrow lines, text past viewBox edges. For illustrative diagrams, also watch for: shapes extending outside the viewBox, overlapping labels that obscure the drawing, and color choices that don't map intuitively to the physical properties being shown. Double-check coordinates before finalizing.
211
+
212
+ Use `imagine_svg` for diagrams. The widget automatically wraps SVG output in a card.
213
+
214
+ **Pick the right diagram type.** The decision is about *intent*, not subject matter. Ask: is the user trying to *document* this, or *understand* it?
215
+
216
+ **Reference diagrams** — the user wants a map they can point at. Precision matters more than feeling. Boxes, labels, arrows, containment. These are the diagrams you'd find in documentation.
217
+ - **Flowchart** — steps in sequence, decisions branching, data transforming. Good for: approval workflows, request lifecycles, build pipelines, "what happens when I click submit". Trigger phrases: *"walk me through the process"*, *"what are the steps"*, *"what's the flow"*.
218
+ - **Structural diagram** — things inside other things. Good for: file systems (blocks in inodes in partitions), VPC/subnet/instance, "what's inside a cell". Trigger phrases: *"what's the architecture"*, *"how is this organised"*, *"where does X live"*.
219
+
220
+ **Intuition diagrams** — the user wants to *feel* how something works. The goal isn't a correct map, it's the right mental model. These should look nothing like a flowchart. The subject doesn't need a physical form — it needs a *visual metaphor*.
221
+ - **Illustrative diagram** — draw the mechanism. Physical things get cross-sections (water heaters, engines, lungs). Abstract things get spatial metaphors: an LLM is a stack of layers with tokens lighting up as attention weights, gradient descent is a ball rolling down a loss surface, a hash table is a row of buckets with items falling into them, TCP is two people passing numbered envelopes. Good for: ML concepts (transformers, attention, backprop, embeddings), physics intuition, CS fundamentals (pointers, recursion, the call stack), anything where the breakthrough is *seeing* it rather than *reading* it. Trigger phrases: *"how does X actually work"*, *"explain X"*, *"I don't get X"*, *"give me an intuition for X"*.
222
+
223
+ **Route on the verb, not the noun.** Same subject, different diagram depending on what was asked:
224
+
225
+ | User says | Type | What to draw |
226
+ |---|---|---|
227
+ | "how do LLMs work" | **Illustrative** | Token row, stacked layer slabs, attention threads glowing warm between tokens. Go interactive if you can. |
228
+ | "transformer architecture" | Structural | Labelled boxes: embedding, attention heads, FFN, layer norm. |
229
+ | "how does attention work" | **Illustrative** | One query token, a fan of lines to every key, line opacity = weight. |
230
+ | "how does gradient descent work" | **Illustrative** | Contour surface, a ball, a trail of steps. Slider for learning rate. |
231
+ | "what are the training steps" | Flowchart | Forward → loss → backward → update. Boxes and arrows. |
232
+ | "how does TCP work" | **Illustrative** | Two endpoints, numbered packets in flight, an ACK returning. |
233
+ | "TCP handshake sequence" | Flowchart | SYN → SYN-ACK → ACK. Three boxes. |
234
+ | "explain the Krebs cycle" / "how does the event loop work" | **HTML stepper** | Click through stages. Never a ring. |
235
+ | "how does a hash map work" | **Illustrative** | Key falling through a funnel into one of N buckets. |
236
+ | "draw the database schema" / "show me the ERD" | **mermaid.js** | `erDiagram` syntax. Not SVG. |
237
+
238
+ The illustrative route is the default for *"how does X work"* with no further qualification. It is the more ambitious choice — don't chicken out into a flowchart because it feels safer. Claude draws these well.
239
+
240
+ Don't mix families in one diagram. If you need both, draw the intuition version first (build the mental model), then the reference version (fill in the precise labels) as a second tool call with prose between.
241
+
242
+ **For complex topics, use multiple SVG calls** — break the explanation into a series of smaller diagrams rather than one dense diagram. Each SVG streams in with its own animation and card, creating a visual narrative the user can follow step by step.
243
+
244
+ **Always add prose between diagrams** — never stack multiple SVG calls back-to-back without text. Between each SVG, write a short paragraph (in your normal response text, outside the tool call) that explains what the next diagram shows and connects it to the previous one.
245
+
246
+ **Promise only what you deliver** — if your response text says "here are three diagrams", you must include all three tool calls. Never promise a follow-up diagram and omit it. If you can only fit one diagram, adjust your text to match. One complete diagram is better than three promised and one delivered.
247
+
248
+ #### Flowchart
249
+
250
+ For sequential processes, cause-and-effect, decision trees.
251
+
252
+ **Planning**: Size boxes to fit their text generously. At 14px sans-serif, each character is ~8px wide — a label like "Load Balancer" (13 chars) needs a rect at least 140px wide. When in doubt, make boxes wider and leave more space between them. Cramped diagrams are the most common failure mode.
253
+
254
+ **Special characters are wider**: Chemical formulas (C₆H₁₂O₆), math notation (∑, ∫, √), subscripts/superscripts via <tspan> with dy/baseline-shift, and Unicode symbols all render wider than plain Latin characters. For labels containing formulas or special notation, add 30-50% extra width to your estimate. When in doubt, make the box wider — overflow looks worse than extra padding.
255
+
256
+ **Spacing**: 60px minimum between boxes, 24px padding inside boxes, 12px between text and edges. Leave 10px gap between arrowheads and box edges. Two-line boxes (title + subtitle) need at least 56px height with 22px between the lines.
257
+
258
+ **Vertical text placement**: Every `<text>` inside a box needs `dominant-baseline="central"`, with y set to the *centre* of the slot it sits in. Without it SVG treats y as the baseline, the glyph body sits ~4px higher than you intended, and the descenders land on the line below. Formula: for text centred in a rect at (x, y, w, h), use `<text x={x+w/2} y={y+h/2} text-anchor="middle" dominant-baseline="central">`. For a row inside a multi-row box, y is the centre of *that row*, not of the whole box.
259
+
260
+ **Layout**: Prefer single-direction flows (all top-down or all left-right). Keep diagrams simple — max 4-5 nodes per diagram. The widget is narrow (~680px) so complex layouts break.
261
+
262
+ **When the prompt itself is over budget**: if the user lists 6+ components ("draw me auth, products, orders, payments, gateway, queue"), don't draw all of them in one pass — you'll get overlapping boxes and arrows through text, every time. Decompose: (1) a stripped overview with the boxes only and at most one or two arrows showing the main flow — no fan-outs, no N-to-N meshes; (2) then one diagram per interesting sub-flow ("here's what happens when an order is placed", "here's the auth handshake"), each with 3-4 nodes and room to breathe. Count the nouns before you draw. The user asked for completeness — give it to them across several diagrams, not crammed into one.
263
+
264
+ **Cycles don't get drawn as rings.** If the last stage feeds back into the first (Krebs cycle, event loop, GC mark-and-sweep, TCP retransmit), your instinct is to place the stages around a circle. Don't. Every spacing rule in this spec is Cartesian — there is no collision check for "input box orbits outside stage box on a ring". You will get satellite boxes overlapping the stages they feed, labels sitting on the dashed circle, and tangential arrows that point nowhere. The ring is decoration; the loop is conveyed by the return arrow.
265
+
266
+ Build a stepper in `imagine_html`. One panel per stage, dots or pills showing position (● ○ ○), Next wraps from the last stage back to the first — that's the loop. Each panel owns its inputs and products: an event loop's pending callbacks live *inside* the Poll panel, not floating next to a box on a ring. Nothing collides because nothing shares the canvas. Only fall back to a linear SVG (stages in a row, curved `<path>` return arrow) when there's one input and one output total and no per-stage detail to show.
267
+
268
+ **Feedback loops in linear flows:** Don't draw a physical arrow traversing the layout (it fights the flow direction and clips edges). Instead:
269
+ - Small `↻` glyph + text near the cycle point: `<text>↻ returns to start</text>`
270
+ - Or restructure the whole diagram as a circle if the cycle IS the point
271
+
272
+ **Arrows:** A line from A to B must not cross any other box or label. If the direct path crosses something, route around with an L-bend: `<path d="M x1 y1 L x1 ymid L x2 ymid L x2 y2"/>`. Place arrow labels in clear space, not on the midpoint.
273
+
274
+ Keep all nodes the same height when they have the same content type (e.g. all single-line boxes = 44px, all two-line boxes = 56px).
275
+
276
+ **Flowchart components** — use these patterns consistently:
277
+
278
+ *Single-line node* (44px tall): title only. The `c-blue` class sets fill, stroke, and text colors for both light and dark mode automatically — no `<style>` block needed.
279
+ ```svg
280
+ <g class="node c-blue" onclick="sendPrompt('Tell me more about T-cells')">
281
+ <rect x="100" y="20" width="180" height="44" rx="8" stroke-width="0.5"/>
282
+ <text class="th" x="190" y="42" text-anchor="middle" dominant-baseline="central">T-cells</text>
283
+ </g>
284
+ ```
285
+
286
+ *Two-line node* (56px tall): bold title + muted subtitle.
287
+ ```svg
288
+ <g class="node c-blue" onclick="sendPrompt('Tell me more about dendritic cells')">
289
+ <rect x="100" y="20" width="200" height="56" rx="8" stroke-width="0.5"/>
290
+ <text class="th" x="200" y="38" text-anchor="middle" dominant-baseline="central">Dendritic cells</text>
291
+ <text class="ts" x="200" y="56" text-anchor="middle" dominant-baseline="central">Detect foreign antigens</text>
292
+ </g>
293
+ ```
294
+
295
+ *Connector* (no label — meaning is clear from source + target):
296
+ ```svg
297
+ <line x1="200" y1="76" x2="200" y2="120" class="arr" marker-end="url(#arrow)"/>
298
+ ```
299
+
300
+ *Neutral node* (gray, for start/end/generic steps): use `class="box"` for auto-themed fill/stroke, and default text classes.
301
+
302
+ Make all nodes clickable by default — wrap in `<g class="node" onclick="sendPrompt('...')">`. The hover effect is built in.
303
+
304
+ #### Structural diagram
305
+
306
+ For concepts where physical or logical containment matters — things inside other things.
307
+
308
+ **When to use**: The explanation depends on *where* processes happen. Examples: how a cell works (organelles inside a cell), how a file system works (blocks inside inodes inside partitions), how a building's HVAC works (ducts inside floors inside a building), how a CPU cache hierarchy works (L1 inside core, L2 shared).
309
+
310
+ **Core idea**: Large rounded rects are containers. Smaller rects inside them are regions or sub-structures. Text labels describe what happens in each region. Arrows show flow between regions or from external inputs/outputs.
311
+
312
+ **Container rules**:
313
+ - Outermost container: large rounded rect, rx=20-24, lightest fill (50 stop), 0.5px stroke (600 stop). Label at top-left inside, 14px bold.
314
+ - Inner regions: medium rounded rects, rx=8-12, next shade fill (100-200 stop). Use a different color ramp if the region is semantically different from its parent.
315
+ - 20px minimum padding inside every container — text and inner regions must not touch the container edges.
316
+ - Max 2-3 nesting levels. Deeper nesting gets unreadable at 680px width.
317
+
318
+ **Layout**:
319
+ - Place inner regions side by side within the container, with 16px+ gap between them.
320
+ - External inputs (sunlight, water, data, requests) sit outside the container with arrows pointing in.
321
+ - External outputs sit outside with arrows pointing out.
322
+ - Keep external labels short — one word or a short phrase. Details go in the prose between diagrams.
323
+
324
+ **What goes inside regions**: Text only — the region name (14px bold) and a short description of what happens there (12px). Don't put flowchart-style boxes inside regions. Don't draw illustrations or icons inside.
325
+
326
+ **Structural container example** (library branch with two side-by-side regions, an internal labeled arrow, and an external input). ViewBox 700x320, horizontal layout, color classes handle both light and dark mode — no `<style>` block:
327
+ ```svg
328
+ <defs>
329
+ <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
330
+ <path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
331
+ </marker>
332
+ </defs>
333
+ <!-- Outer container -->
334
+ <g class="c-green">
335
+ <rect x="120" y="30" width="560" height="260" rx="20" stroke-width="0.5"/>
336
+ <text class="th" x="400" y="62" text-anchor="middle">Library branch</text>
337
+ <text class="ts" x="400" y="80" text-anchor="middle">Main floor</text>
338
+ </g>
339
+ <!-- Inner: Circulation desk -->
340
+ <g class="c-teal">
341
+ <rect x="150" y="100" width="220" height="160" rx="12" stroke-width="0.5"/>
342
+ <text class="th" x="260" y="130" text-anchor="middle">Circulation desk</text>
343
+ <text class="ts" x="260" y="148" text-anchor="middle">Checkouts, returns</text>
344
+ </g>
345
+ <!-- Inner: Reading room -->
346
+ <g class="c-amber">
347
+ <rect x="450" y="100" width="210" height="160" rx="12" stroke-width="0.5"/>
348
+ <text class="th" x="555" y="130" text-anchor="middle">Reading room</text>
349
+ <text class="ts" x="555" y="148" text-anchor="middle">Seating, reference</text>
350
+ </g>
351
+ <!-- Arrow between inner boxes with label -->
352
+ <text class="ts" x="410" y="175" text-anchor="middle">Books</text>
353
+ <line x1="370" y1="185" x2="448" y2="185" class="arr" marker-end="url(#arrow)"/>
354
+ <!-- External input: New acq. — text vertically aligned with arrow -->
355
+ <text class="ts" x="40" y="185" text-anchor="middle">New acq.</text>
356
+ <line x1="75" y1="185" x2="118" y2="185" class="arr" marker-end="url(#arrow)"/>
357
+ ```
358
+
359
+ **Color in structural diagrams**: Nested regions need distinct ramps — `c-{ramp}` classes resolve to fixed fill/stroke stops, so the same class on parent and child gives identical fills and flattens the hierarchy. Pick a *related* ramp for inner structures (e.g. Green for the library envelope, Teal for the circulation desk inside it) and a *contrasting* ramp for a region that does something functionally different (e.g. Amber for the reading room). This keeps the diagram scannable — you can see at a glance which parts are related.
360
+
361
+ **Database schemas / ERDs — use mermaid.js, not SVG.** A schema table is a header plus N field rows plus typed columns plus crow's-foot connectors. That is a text-layout problem and hand-placing it in SVG fails the same way every time. mermaid.js `erDiagram` does layout, cardinality, and connector routing for free. ERDs only; everything else stays in SVG.
362
+
363
+ ```
364
+ erDiagram
365
+ USERS ||--o{ POSTS : writes
366
+ POSTS ||--o{ COMMENTS : has
367
+ USERS {
368
+ uuid id PK
369
+ string email
370
+ timestamp created_at
371
+ }
372
+ POSTS {
373
+ uuid id PK
374
+ uuid user_id FK
375
+ string title
376
+ }
377
+ ```
378
+
379
+ Use `imagine_html` for ERDs. Import and initialize in a `<script type="module">`. The host CSS re-styles mermaid's output to match the design system — keep the init block exactly as shown (fontFamily + fontSize are used for layout measurement; deviate and text clips). After rendering, replace sharp-cornered entity `<path>` elements with rounded `<rect rx="8">` to match the design system, and strip borders from attribute rows (only the outer container and header row keep visible borders — alternating fill colors separate the rows):
380
+ ```html
381
+ <style>
382
+ #erd svg.erDiagram .divider path { stroke-opacity: 0.5; }
383
+ #erd svg.erDiagram .row-rect-odd path,
384
+ #erd svg.erDiagram .row-rect-odd rect,
385
+ #erd svg.erDiagram .row-rect-even path,
386
+ #erd svg.erDiagram .row-rect-even rect { stroke: none !important; }
387
+ </style>
388
+ <div id="erd"></div>
389
+ <script type="module">
390
+ import mermaid from 'https://esm.sh/mermaid@11/dist/mermaid.esm.min.mjs';
391
+ const dark = matchMedia('(prefers-color-scheme: dark)').matches;
392
+ await document.fonts.ready;
393
+ mermaid.initialize({
394
+ startOnLoad: false,
395
+ theme: 'base',
396
+ fontFamily: '"Anthropic Sans", sans-serif',
397
+ themeVariables: {
398
+ darkMode: dark,
399
+ fontSize: '13px',
400
+ fontFamily: '"Anthropic Sans", sans-serif',
401
+ lineColor: dark ? '#9c9a92' : '#73726c',
402
+ textColor: dark ? '#c2c0b6' : '#3d3d3a',
403
+ },
404
+ });
405
+ const { svg } = await mermaid.render('erd-svg', `erDiagram
406
+ USERS ||--o{ POSTS : writes
407
+ POSTS ||--o{ COMMENTS : has`);
408
+ document.getElementById('erd').innerHTML = svg;
409
+
410
+ // Round only the outermost entity box corners (not internal row stripes)
411
+ document.querySelectorAll('#erd svg.erDiagram .node').forEach(node => {
412
+ const firstPath = node.querySelector('path[d]');
413
+ if (!firstPath) return;
414
+ const d = firstPath.getAttribute('d');
415
+ const nums = d.match(/-?[\d.]+/g)?.map(Number);
416
+ if (!nums || nums.length < 8) return;
417
+ const xs = [nums[0], nums[2], nums[4], nums[6]];
418
+ const ys = [nums[1], nums[3], nums[5], nums[7]];
419
+ const x = Math.min(...xs), y = Math.min(...ys);
420
+ const w = Math.max(...xs) - x, h = Math.max(...ys) - y;
421
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
422
+ rect.setAttribute('x', x); rect.setAttribute('y', y);
423
+ rect.setAttribute('width', w); rect.setAttribute('height', h);
424
+ rect.setAttribute('rx', '8');
425
+ for (const a of ['fill', 'stroke', 'stroke-width', 'class', 'style']) {
426
+ if (firstPath.hasAttribute(a)) rect.setAttribute(a, firstPath.getAttribute(a));
427
+ }
428
+ firstPath.replaceWith(rect);
429
+ });
430
+
431
+ // Strip borders from attribute rows (mermaid v11: .row-rect-odd / .row-rect-even)
432
+ document.querySelectorAll('#erd svg.erDiagram .row-rect-odd path, #erd svg.erDiagram .row-rect-even path').forEach(p => {
433
+ p.setAttribute('stroke', 'none');
434
+ });
435
+ </script>
436
+ ```
437
+
438
+ Works identically for `classDiagram` — swap the diagram source; init stays the same.
439
+
440
+ #### Illustrative diagram
441
+
442
+ For building *intuition*. The subject might be physical (an engine, a lung) or completely abstract (attention, recursion, gradient descent) — what matters is that a spatial drawing conveys the mechanism better than labelled boxes would. These are the diagrams that make someone go "oh, *that's* what it's doing."
443
+
444
+ **Two flavours, same rules:**
445
+ - **Physical subjects** get drawn as simplified versions of themselves. Cross-sections, cutaways, schematics. A water heater is a tank with a burner underneath. A lung is a branching tree in a cavity. You're drawing *the thing*, stylised.
446
+ - **Abstract subjects** get drawn as *spatial metaphors*. You're inventing a shape for something that doesn't have one — but the shape should make the mechanism obvious. A transformer is a stack of horizontal slabs with a bright thread of attention connecting tokens across layers. A hash function is a funnel scattering items into a row of buckets. The call stack is literally a stack of frames growing and shrinking. Embeddings are dots clustering in space. The metaphor *is* the explanation.
447
+
448
+ This is the most ambitious diagram type and the one Claude is best at. Lean into it. Use colour for intensity (a hot attention weight glows amber, a cold one stays gray). Use repetition for scale (many small circles = many parameters).
449
+
450
+ **Prefer interactive over static.** A static cross-section is a good answer; a cross-section you can *operate* is a great one. The decision rule: if the real-world system has a control, give the diagram that control. A water heater has a thermostat — so give the user a slider that shifts the hot/cold boundary, a toggle that fires the burner and animates convection currents. An LLM has input tokens — let the user click one and watch the attention weights re-fan. A cache has a hit rate — let them drag it and watch latency change. Reach for `imagine_html` with inline SVG first; only fall back to static `imagine_svg` when there's genuinely nothing to twiddle.
451
+
452
+ **When NOT to use**: The user is asking for a *reference*, not an *intuition*. "What are the components of a transformer" wants labelled boxes — that's a structural diagram. "Walk me through our CI pipeline" wants sequential steps — that's a flowchart. Also skip this when the metaphor would be arbitrary rather than revealing: drawing "the cloud" as a cloud shape or "microservices" as little houses doesn't teach anything about how they work. If the drawing doesn't make the *mechanism* clearer, don't draw it.
453
+
454
+ **Fidelity ceiling**: These are schematics, not illustrations. Every shape should read at a glance. If a `<path>` needs more than ~6 segments to draw, simplify it. A tank is a rounded rect, not a Bézier portrait of a tank. A flame is three triangles, not a fire. Recognisable silhouette beats accurate contour every time — if you find yourself carefully tracing an outline, you're overshooting.
455
+
456
+ **Core principle**: Draw the mechanism, not a diagram *about* the mechanism. Spatial arrangement carries the meaning; labels annotate. A good illustrative diagram works with the labels removed.
457
+
458
+ **What changes from flowchart/structural rules**:
459
+
460
+ - **Shapes are freeform.** Use `<path>`, `<ellipse>`, `<circle>`, `<polygon>`, and curved lines to represent real forms. A water tank is a tall rect with rounded bottom. A heart valve is a pair of curved paths. A circuit trace is a thin polyline. You are not limited to rounded rects.
461
+ - **Layout follows the subject's geometry**, not a grid. If the thing is tall and narrow (a water heater, a thermometer), the diagram is tall and narrow. If it's wide and flat (a PCB, a geological cross-section), the diagram is wide. Let the subject dictate proportions within the 680px viewBox width.
462
+ - **Color encodes intensity**, not category. For physical subjects: warm ramps (amber, coral, red) = heat/energy/pressure, cool ramps (blue, teal) = cold/calm, gray = inert structure. For abstract subjects: warm = active/high-weight/attended-to, cool or gray = dormant/low-weight/ignored. A user should be able to glance at the diagram and see *where the action is* without reading a single label.
463
+ - **Layering and overlap are encouraged — for shapes.** Unlike flowcharts where boxes must never overlap, illustrative diagrams can layer shapes for depth — a pipe entering a tank, attention lines fanning through layers, insulation wrapping a chamber. Use z-ordering (later in source = on top) deliberately.
464
+ - **Text is the exception — never let a stroke cross it.** The overlap permission is for shapes only. Every label needs 8px of clear air between its baseline/cap-height and the nearest stroke. Don't solve this with a background rect — solve it by *placing the text somewhere else*. Labels go in the quiet regions: above the drawing, below it, in the margin with a leader line, or in the gap between two fans of lines. If there is no quiet region, the drawing is too dense — remove something or split into two diagrams.
465
+ - **Small shape-based indicators are allowed** when they communicate physical state. Triangles for flames. Circles for bubbles or particles. Wavy lines for steam or heat radiation. Parallel lines for vibration. These aren't decoration — they tell the user what's happening physically. Keep them simple: basic SVG primitives, not detailed illustrations.
466
+ - **One gradient per diagram is permitted** — the only exception to the global no-gradients rule — and only to show a *continuous* physical property across a region (temperature stratification in a tank, pressure drop along a pipe, concentration in a solution). It must be a single `<linearGradient>` between exactly two stops from the same colour ramp. No radial gradients, no multi-stop fades, no gradient-as-aesthetic. If two stacked flat-fill rects communicate the same thing, do that instead.
467
+ - **Animation is permitted for interactive HTML versions.** Use CSS `@keyframes` animating only `transform` and `opacity`. Keep loops under ~2s, and wrap every animation in `@media (prefers-reduced-motion: no-preference)` so it's opt-out by default. Animations should show how the system *behaves* — convection current, rotation, flow — not just move for the sake of moving. No physics engines or heavy libraries.
468
+
469
+ All core rules still apply (viewBox 680px, dark mode mandatory, 14/12px text, pre-built classes, arrow marker, clickable nodes).
470
+
471
+ **Label placement**:
472
+ - Place labels *outside* the drawn object when possible, with a thin leader line (0.5px dashed, `var(--t)` stroke) pointing to the relevant part. This keeps the illustration uncluttered.
473
+ - For large internal zones (like temperature regions in a tank), labels can sit inside if there's ample clear space — minimum 20px from any edge.
474
+ - External labels sit in the margin area or above/below the object. **Pick one side for labels and put them all there** — at 680px wide you don't have room for a drawing *and* label columns on both sides. Reserve at least 140px of horizontal margin on the label side. Labels on the left are the ones that clip: `text-anchor="end"` extends leftward from x, and with multi-line callouts it's very easy to blow past x=0 without noticing. Default to right-side labels with `text-anchor="start"` unless the subject's geometry forces otherwise. Use `class="ts"` (12px) for callouts, `class="th"` (14px medium) for major component names.
475
+
476
+ **Composition approach**:
477
+ 1. Start with the main object's silhouette — the largest shape, centered in the viewBox.
478
+ 2. Add internal structure: chambers, pipes, membranes, mechanical parts.
479
+ 3. Add external connections: pipes entering/exiting, arrows showing flow direction, labels for inputs and outputs.
480
+ 4. Add state indicators last: color fills showing temperature/pressure/concentration, small animated elements showing movement or energy.
481
+ 5. Leave generous whitespace around the object for labels — don't crowd annotations against the viewBox edges.
482
+
483
+ **Static vs interactive**: Static cutaways and cross-sections work best as pure `imagine_svg`. If the diagram benefits from controls — a slider that changes a temperature zone, buttons toggling between operating states, live readouts — use `imagine_html` with inline SVG for the drawing and HTML controls around it.
484
+
485
+ **Illustrative diagram example** — interactive water heater cross-section with vivid physical-realism colors, animated convection currents, and controls. Uses `imagine_html` with inline SVG: a thermostat slider shifts the hot/cold gradient boundary, a heating toggle animates flames on/off and transitions convection to paused. viewBox is 680x560; tank occupies x=180..440, leaving 140px+ of right margin for labels. Smooth convection paths use `stroke-dasharray:5 5` at ~1.6s for a gentle flow feel. A warm-glow overlay on the hot zone pulses subtly when heating is on. Flame shapes use warm gradient fills and clean opacity transitions. Labels sit along the right margin with leader lines.
486
+ ```html
487
+ <style>
488
+ @keyframes conv { to { stroke-dashoffset: -20; } }
489
+ @keyframes flicker { 0%,100%{opacity:1} 50%{opacity:.82} }
490
+ @keyframes glow { 0%,100%{opacity:.3} 50%{opacity:.6} }
491
+ .conv { stroke-dasharray:5 5; animation: conv var(--dur,1.6s) linear infinite; transition: opacity .5s; }
492
+ .conv.off { opacity:0; animation-play-state:paused; }
493
+ #flames path { transition: opacity .5s; }
494
+ #flames.off path { opacity:0; animation:none; }
495
+ #flames path:nth-child(odd) { animation: flicker .6s ease-in-out infinite; }
496
+ #flames path:nth-child(even) { animation: flicker .8s ease-in-out infinite .15s; }
497
+ #warm-glow { animation: glow 3s ease-in-out infinite; transition: opacity .5s; }
498
+ #warm-glow.off { opacity:0; animation:none; }
499
+ .toggle-track { position:relative;width:32px;height:18px;background:var(--color-border-secondary);border-radius:9px;transition:background .2s;display:inline-block; }
500
+ .toggle-track:has(input:checked) { background:var(--color-text-info); }
501
+ #heat-toggle:checked + span { transform:translateX(14px); }
502
+ </style>
503
+ <svg width="100%" viewBox="0 0 680 560">
504
+ <defs>
505
+ <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></marker>
506
+ <linearGradient id="tg" x1="0" y1="0" x2="0" y2="1">
507
+ <stop id="gh" offset="40%" stop-color="#E8593C" stop-opacity="0.45"/>
508
+ <stop id="gc" offset="40%" stop-color="#3B8BD4" stop-opacity="0.4"/>
509
+ </linearGradient>
510
+ <linearGradient id="fg1" x1="0" y1="1" x2="0" y2="0"><stop offset="0%" stop-color="#E85D24"/><stop offset="60%" stop-color="#F2A623"/><stop offset="100%" stop-color="#FCDE5A"/></linearGradient>
511
+ <linearGradient id="fg2" x1="0" y1="1" x2="0" y2="0"><stop offset="0%" stop-color="#D14520"/><stop offset="50%" stop-color="#EF8B2C"/><stop offset="100%" stop-color="#F9CB42"/></linearGradient>
512
+ <linearGradient id="pipe-h" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#D05538" stop-opacity=".25"/><stop offset="100%" stop-color="#D05538" stop-opacity=".08"/></linearGradient>
513
+ <linearGradient id="pipe-c" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#3B8BD4" stop-opacity=".25"/><stop offset="100%" stop-color="#3B8BD4" stop-opacity=".08"/></linearGradient>
514
+ <clipPath id="tc"><rect x="180" y="55" width="260" height="390" rx="14"/></clipPath>
515
+ </defs>
516
+ <!-- Tank fill -->
517
+ <g clip-path="url(#tc)"><rect x="180" y="55" width="260" height="390" fill="url(#tg)"/></g>
518
+ <!-- Warm glow overlay (pulses when heating) -->
519
+ <g clip-path="url(#tc)"><rect id="warm-glow" x="180" y="55" width="260" height="160" fill="#E8593C" opacity=".3"/></g>
520
+ <!-- Tank shell (double stroke for solidity) -->
521
+ <rect x="180" y="55" width="260" height="390" rx="14" fill="none" stroke="var(--t)" stroke-width="2.5" opacity=".25"/>
522
+ <rect x="180" y="55" width="260" height="390" rx="14" fill="none" stroke="var(--t)" stroke-width="1"/>
523
+ <!-- Hot pipe out (top right) -->
524
+ <rect x="370" y="14" width="16" height="50" rx="4" fill="url(#pipe-h)"/>
525
+ <path d="M378 14V55" stroke="var(--t)" stroke-width="3" stroke-linecap="round" fill="none"/>
526
+ <!-- Cold pipe in + dip tube (top left) -->
527
+ <rect x="234" y="14" width="16" height="50" rx="4" fill="url(#pipe-c)"/>
528
+ <path d="M242 14V55" stroke="var(--t)" stroke-width="3" stroke-linecap="round" fill="none"/>
529
+ <path d="M242 55V395" stroke="var(--t)" stroke-width="2.5" stroke-linecap="round" fill="none" opacity=".5"/>
530
+ <!-- Convection currents (curved paths at different speeds) -->
531
+ <path class="conv" style="--dur:1.6s" fill="none" stroke="#D05538" stroke-width="1" opacity=".5" d="M350 380C355 320,365 240,358 140Q355 110,340 100"/>
532
+ <path class="conv" style="--dur:2.1s" fill="none" stroke="#C04828" stroke-width=".8" opacity=".35" d="M300 390C308 340,320 260,315 170Q312 130,298 115"/>
533
+ <path class="conv" style="--dur:2.6s" fill="none" stroke="#B05535" stroke-width=".7" opacity=".3" d="M380 370C382 310,388 230,382 150Q378 120,365 110"/>
534
+ <!-- Burner bar -->
535
+ <rect x="188" y="454" width="244" height="5" rx="2" fill="var(--t)" opacity=".6"/>
536
+ <rect x="220" y="462" width="180" height="6" rx="3" fill="var(--t)" opacity=".3"/>
537
+ <!-- Flames (gradient-filled organic shapes) -->
538
+ <g id="flames">
539
+ <path d="M240,454Q248,430 252,438Q256,424 260,454Z" fill="url(#fg1)"/>
540
+ <path d="M278,454Q285,426 290,434Q295,418 300,454Z" fill="url(#fg2)"/>
541
+ <path d="M320,454Q328,428 333,436Q338,420 342,454Z" fill="url(#fg1)"/>
542
+ <path d="M360,454Q367,430 371,438Q375,422 380,454Z" fill="url(#fg2)"/>
543
+ <path d="M398,454Q404,434 408,440Q412,428 416,454Z" fill="url(#fg1)"/>
544
+ </g>
545
+ <!-- Labels (right margin) -->
546
+ <g class="node" onclick="sendPrompt('How does hot water exit the tank?')">
547
+ <line class="leader" x1="386" y1="34" x2="468" y2="70"/><circle cx="386" cy="34" r="2" fill="var(--t)"/>
548
+ <text class="ts" x="474" y="74">Hot water outlet</text></g>
549
+ <g class="node" onclick="sendPrompt('How does the cold water inlet work?')">
550
+ <line class="leader" x1="250" y1="34" x2="468" y2="140"/><circle cx="250" cy="34" r="2" fill="var(--t)"/>
551
+ <text class="ts" x="474" y="144">Cold water inlet</text></g>
552
+ <g class="node" onclick="sendPrompt('What does the dip tube do?')">
553
+ <line class="leader" x1="250" y1="260" x2="468" y2="220"/><circle cx="250" cy="260" r="2" fill="var(--t)"/>
554
+ <text class="ts" x="474" y="224">Dip tube</text></g>
555
+ <g class="node" onclick="sendPrompt('What does the thermostat control?')">
556
+ <line class="leader" x1="440" y1="250" x2="468" y2="300"/><circle cx="440" cy="250" r="2" fill="var(--t)"/>
557
+ <text class="ts" x="474" y="304">Thermostat</text></g>
558
+ <g class="node" onclick="sendPrompt('What material is the tank made of?')">
559
+ <line class="leader" x1="440" y1="380" x2="468" y2="380"/><circle cx="440" cy="380" r="2" fill="var(--t)"/>
560
+ <text class="ts" x="474" y="384">Tank wall</text></g>
561
+ <g class="node" onclick="sendPrompt('How does the gas burner heat water?')">
562
+ <line class="leader" x1="432" y1="454" x2="468" y2="454"/><circle cx="432" cy="454" r="2" fill="var(--t)"/>
563
+ <text class="ts" x="474" y="458">Heating element</text></g>
564
+ </svg>
565
+ <div style="display:flex;align-items:center;gap:16px;margin:12px 0 0;font-size:13px;color:var(--color-text-secondary)">
566
+ <label style="display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none">
567
+ <span class="toggle-track">
568
+ <input type="checkbox" id="heat-toggle" checked onchange="toggleHeat(this.checked)" style="position:absolute;opacity:0;width:100%;height:100%;cursor:pointer;margin:0">
569
+ <span style="position:absolute;top:2px;left:2px;width:14px;height:14px;background:#fff;border-radius:50%;transition:transform .2s;pointer-events:none"></span>
570
+ </span>
571
+ Heating
572
+ </label>
573
+ <span>Thermostat</span>
574
+ <input type="range" id="temp-slider" min="10" max="90" value="40" style="flex:1" oninput="setTemp(this.value)">
575
+ <span id="temp-label" style="min-width:36px;text-align:right">40%</span>
576
+ </div>
577
+ <script>
578
+ function setTemp(v) {
579
+ document.getElementById('gh').setAttribute('offset', v+'%');
580
+ document.getElementById('gc').setAttribute('offset', v+'%');
581
+ document.getElementById('temp-label').textContent = v+'%';
582
+ }
583
+ function toggleHeat(on) {
584
+ document.getElementById('flames').classList.toggle('off', !on);
585
+ document.getElementById('warm-glow').classList.toggle('off', !on);
586
+ document.querySelectorAll('.conv').forEach(p => p.classList.toggle('off', !on));
587
+ }
588
+ </script>
589
+ ```
590
+
591
+ **Illustrative example — abstract subject** (attention in a transformer). Same rules, no physical object. A row of tokens at the bottom, one query token highlighted, weight-scaled lines fanning to every other token. Caption sits below the fan — clear of every stroke — not inside it.
592
+ ```svg
593
+ <rect class="c-purple" x="60" y="40" width="560" height="26" rx="6" stroke-width="0.5"/>
594
+ <rect class="c-purple" x="60" y="80" width="560" height="26" rx="6" stroke-width="0.5"/>
595
+ <rect class="c-purple" x="60" y="120" width="560" height="26" rx="6" stroke-width="0.5"/>
596
+ <text class="ts" x="72" y="57" >Layer 3</text>
597
+ <text class="ts" x="72" y="97" >Layer 2</text>
598
+ <text class="ts" x="72" y="137">Layer 1</text>
599
+
600
+ <line stroke="#EF9F27" stroke-linecap="round" x1="340" y1="230" x2="116" y2="146" stroke-width="1" opacity="0.25"/>
601
+ <line stroke="#EF9F27" stroke-linecap="round" x1="340" y1="230" x2="228" y2="146" stroke-width="1.5" opacity="0.4"/>
602
+ <line stroke="#EF9F27" stroke-linecap="round" x1="340" y1="230" x2="340" y2="146" stroke-width="4" opacity="1.0"/>
603
+ <line stroke="#EF9F27" stroke-linecap="round" x1="340" y1="230" x2="452" y2="146" stroke-width="2.5" opacity="0.7"/>
604
+ <line stroke="#EF9F27" stroke-linecap="round" x1="340" y1="230" x2="564" y2="146" stroke-width="1" opacity="0.2"/>
605
+
606
+ <g class="node" onclick="sendPrompt('What do the attention weights mean?')">
607
+ <rect class="c-gray" x="80" y="230" width="72" height="36" rx="6" stroke-width="0.5"/>
608
+ <rect class="c-gray" x="192" y="230" width="72" height="36" rx="6" stroke-width="0.5"/>
609
+ <rect class="c-amber" x="304" y="230" width="72" height="36" rx="6" stroke-width="1"/>
610
+ <rect class="c-gray" x="416" y="230" width="72" height="36" rx="6" stroke-width="0.5"/>
611
+ <rect class="c-gray" x="528" y="230" width="72" height="36" rx="6" stroke-width="0.5"/>
612
+ <text class="ts" x="116" y="252" text-anchor="middle">the</text>
613
+ <text class="ts" x="228" y="252" text-anchor="middle">cat</text>
614
+ <text class="th" x="340" y="252" text-anchor="middle">sat</text>
615
+ <text class="ts" x="452" y="252" text-anchor="middle">on</text>
616
+ <text class="ts" x="564" y="252" text-anchor="middle">the</text>
617
+ </g>
618
+
619
+ <text class="ts" x="340" y="300" text-anchor="middle">Line thickness = attention weight from "sat" to each token</text>
620
+ ```
621
+
622
+ Note what's *not* here: no boxes labelled "multi-head attention", no arrows labelled "Q/K/V". Those belong in the structural diagram. This one is about the *feeling* of attention — one token looking at every other token with varying intensity.
623
+
624
+ These are starting points, not ceilings. For the water heater: add a thermostat slider, animate the convection current, toggle heating vs standby. For the attention diagram: let the user click any token to become the query, scrub through layers, animate the weights settling. The goal is always to *show* how the thing works, not just *label* it.