# CSS Patterns for Diagrams Reusable patterns for layout, connectors, theming, and visual effects in self-contained HTML diagrams. ## Theme Setup Always define both light and dark palettes via custom properties. Start with whichever fits the chosen aesthetic, ensure both work. ```css :root { --font-body: 'Outfit', system-ui, sans-serif; --font-mono: 'Space Mono', 'SF Mono', Consolas, monospace; --bg: #f8f9fa; --surface: #ffffff; --surface-elevated: #ffffff; --border: rgba(0, 0, 0, 0.08); --border-bright: rgba(0, 0, 0, 0.15); --text: #1a1a2e; --text-dim: #6b7280; --accent: #0891b2; --accent-dim: rgba(8, 145, 178, 0.1); /* Semantic accents for diagram elements */ --node-a: #0891b2; --node-a-dim: rgba(8, 145, 178, 0.1); --node-b: #059669; --node-b-dim: rgba(5, 150, 105, 0.1); --node-c: #d97706; --node-c-dim: rgba(217, 119, 6, 0.1); } @media (prefers-color-scheme: dark) { :root { --bg: #0d1117; --surface: #161b22; --surface-elevated: #1c2333; --border: rgba(255, 255, 255, 0.06); --border-bright: rgba(255, 255, 255, 0.12); --text: #e6edf3; --text-dim: #8b949e; --accent: #22d3ee; --accent-dim: rgba(34, 211, 238, 0.12); --node-a: #22d3ee; --node-a-dim: rgba(34, 211, 238, 0.12); --node-b: #34d399; --node-b-dim: rgba(52, 211, 153, 0.12); --node-c: #fbbf24; --node-c-dim: rgba(251, 191, 36, 0.12); } } ``` ## Background Atmosphere Flat backgrounds feel dead. Use subtle gradients or patterns. ```css /* Radial glow behind focal area */ body { background: var(--bg); background-image: radial-gradient(ellipse at 50% 0%, var(--accent-dim) 0%, transparent 60%); } /* Faint dot grid */ body { background-color: var(--bg); background-image: radial-gradient(circle, var(--border) 1px, transparent 1px); background-size: 24px 24px; } /* Diagonal subtle lines */ body { background-color: var(--bg); background-image: repeating-linear-gradient( -45deg, transparent, transparent 40px, var(--border) 40px, var(--border) 41px ); } /* Gradient mesh (pick 2-3 positioned radials) */ body { background: var(--bg); background-image: radial-gradient(at 20% 20%, var(--node-a-dim) 0%, transparent 50%), radial-gradient(at 80% 60%, var(--node-b-dim) 0%, transparent 50%); } ``` ## Link Styling **Never rely on browser default link colors.** The default blue (`#0000EE`) has poor contrast on dark backgrounds. Style links with `color: var(--accent)` and keep underlines for discoverability. On dark backgrounds, use bright accents (`#22d3ee`, `#34d399`, `#fbbf24`). On light backgrounds, use deeper tones (`#0891b2`, `#059669`, `#d97706`). ## Section / Card Components The fundamental building block. A colored card representing a system component, pipeline step, or data entity. **IMPORTANT: Never use `.node` as a CSS class name.** Mermaid.js internally uses `.node` on its SVG `` elements with `transform: translate(x, y)` for positioning. Any page-level `.node` styles (hover transforms, box-shadows, transitions) will leak into Mermaid diagrams and break their layout. Use `.ve-card` instead (namespaced to avoid collisions with CSS frameworks like Bootstrap/Tailwind that also use `.card`). ```css .ve-card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 16px 20px; position: relative; } /* Colored accent border (left or top) */ .ve-card--accent-a { border-left: 3px solid var(--node-a); } /* --- Depth tiers: vary card depth to signal importance --- */ /* Elevated: KPIs, key sections, anything that should pop */ .ve-card--elevated { background: var(--surface-elevated); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04); } /* Recessed: code blocks, secondary content, detail panels */ .ve-card--recessed { background: color-mix(in srgb, var(--bg) 70%, var(--surface) 30%); box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.06); border-color: var(--border); } /* Hero: executive summaries, focal elements — demands attention */ .ve-card--hero { background: color-mix(in srgb, var(--surface) 92%, var(--accent) 8%); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04); border-color: color-mix(in srgb, var(--border) 50%, var(--accent) 50%); } /* Glass: special-occasion overlay effect (use sparingly) */ .ve-card--glass { background: color-mix(in srgb, var(--surface) 60%, transparent 40%); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-color: rgba(255, 255, 255, 0.1); } /* Section label (monospace, uppercase, small) */ .ve-card__label { font-family: var(--font-mono); font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.5px; color: var(--node-a); margin-bottom: 10px; display: flex; align-items: center; gap: 8px; } /* Colored dot indicator */ .ve-card__label::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: currentColor; } ``` ## Code Blocks Code blocks need explicit whitespace preservation and a max-height constraint. Without these, code runs together and long files overwhelm the page. ### Basic Pattern ```css .code-block { font-family: var(--font-mono); font-size: 13px; line-height: 1.5; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 16px; overflow-x: auto; /* CRITICAL: preserve line breaks and indentation */ white-space: pre-wrap; word-break: break-word; } /* Constrain height for long code */ .code-block--scroll { max-height: 400px; overflow-y: auto; } ``` ```html
// Your code here
function example() {
  return true;
}
``` ### With File Header ```css .code-file { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; } .code-file__header { display: flex; align-items: center; gap: 8px; padding: 10px 16px; background: var(--surface); border-bottom: 1px solid var(--border); font-family: var(--font-mono); font-size: 12px; color: var(--text-dim); } .code-file__body { font-family: var(--font-mono); font-size: 13px; line-height: 1.5; padding: 16px; background: var(--surface-elevated); white-space: pre-wrap; word-break: break-word; max-height: 500px; overflow: auto; } ``` ```html
src/extension.ts
export function activate() {
  // ...
}
``` ### Implementation Plans: Don't Dump Full Files For implementation plans and architecture docs, **don't display entire source files inline**. Instead: 1. **Show structure, not code:** ```html
src/extension.ts
``` 2. **Use collapsible sections for full code:** ```html
Full implementation (87 lines)
...
``` 3. **Show key snippets only:** ```html

The core logic intercepts task completion:

pi.on("agent_end", async () => {
     const summary = generateSummary(workEntries);
     boomerangComplete = true;
   });
``` **Anti-patterns:** - Displaying full source files inline (100+ lines overwhelming the page) - Code blocks without `white-space: pre-wrap` (code runs together into unreadable wall) - No height constraint on long code (page becomes endless scroll) If someone needs the full file, put it in a collapsible section or link to it. ## Directory Tree For file structures, use `
` with monospace + `white-space: pre`. Tree connectors (`├──`, `└──`, `│`) only work when vertically aligned — they become noise if text wraps.

```css
.dir-tree {
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.7;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px 20px;
  overflow-x: auto;
  white-space: pre;
}

.dir-tree .ann { color: var(--text-dim); font-size: 11px; font-style: italic; }
.dir-tree .hl  { color: var(--accent); font-weight: 600; }
```

```html
my-project/
├── src/
│   ├── index.ts       — entry point
│   ├── services/
│   │   └── api.py     (142 lines)
│   └── utils/
├── tests/            (14 test files)
└── README.md
``` For labeled trees, wrap in a card. For side-by-side comparisons, put two cards in a grid: ```css .dir-tree-card { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; } .dir-tree-card__header { display: flex; align-items: center; gap: 8px; padding: 10px 16px; background: var(--surface); border-bottom: 1px solid var(--border); font-family: var(--font-mono); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.5px; } .dir-tree-card .dir-tree { border: none; border-radius: 0; } /* Side-by-side: two .dir-tree-card in a grid */ .dir-compare { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; } @media (max-width: 900px) { .dir-compare { grid-template-columns: 1fr; } } ``` **Never** render tree connectors inside wrapping text (`white-space: normal`), flex children, or grid items — the vertical pipes lose alignment and the hierarchy becomes unreadable. ## Overflow Protection Grid and flex children default to `min-width: auto`, which prevents them from shrinking below their content width. Long text, inline code badges, and non-wrapping elements will blow out containers. ### Global rules ```css /* Every grid/flex child must be able to shrink */ .grid > *, .flex > *, [style*="display: grid"] > *, [style*="display: flex"] > * { min-width: 0; } /* Long text wraps instead of overflowing */ body { overflow-wrap: break-word; } ``` ### Side-by-side comparison panels ```css .comparison { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } .comparison > * { min-width: 0; overflow-wrap: break-word; } @media (max-width: 768px) { .comparison { grid-template-columns: 1fr; } } ``` ### Never use `display: flex` on `
  • ` for marker characters Using `display: flex` on a list item to position a `::before` marker creates an anonymous flex item for the remaining text content. That anonymous flex item gets `min-width: auto` and you **cannot** set `min-width: 0` on anonymous boxes. Lines with many inline `` badges will overflow their container with no CSS fix possible. Use absolute positioning for markers instead: ```css /* WRONG — causes overflow with inline code badges */ li { display: flex; align-items: baseline; gap: 6px; } li::before { content: '›'; flex-shrink: 0; } /* RIGHT — text wraps normally */ li { padding-left: 14px; position: relative; } li::before { content: '›'; position: absolute; left: 0; } ``` ### List markers overlapping container borders By default, `list-style-position: outside` places list markers (bullets, numbers) outside the content box. When lists are inside bordered containers (cards, callout boxes), the markers can overlap or extend beyond the border. ```css /* WRONG — markers overlap container border */ .card ol, .card ul { padding-left: 20px; /* Not enough for outside markers */ } /* RIGHT — use inside positioning */ .card ol, .card ul { list-style-position: inside; } /* OR — adequate padding for outside markers */ .card ol, .card ul { padding-left: 2em; /* ~32px gives room for markers */ } /* OR — custom markers with absolute positioning (most control) */ .card ol { list-style: none; padding-left: 0; counter-reset: item; } .card ol li { counter-increment: item; padding-left: 2em; position: relative; } .card ol li::before { content: counter(item) "."; position: absolute; left: 0; color: var(--accent); font-weight: 600; } ``` **Rule of thumb:** Any `
      ` or `
        ` inside a bordered container needs either `list-style-position: inside` or `padding-left: 2em` minimum. The default 20px padding is not enough for outside-positioned markers. ## Mermaid Containers Mermaid diagrams have two common layout issues: they render too small to read, and they left-align in their container leaving awkward dead space (especially for narrow vertical flowcharts). ### Centering (Required) Mermaid SVGs render at a fixed size based on content. Without explicit centering, they default to top-left alignment. **Always center Mermaid diagrams** — narrow vertical flowcharts look particularly bad when left-aligned in a wide container. ```css /* WRONG — diagram hugs left edge */ .mermaid-container { padding: 24px; border: 1px solid var(--border); } /* RIGHT — diagram centers in container */ .mermaid-wrap { display: flex; justify-content: center; align-items: flex-start; /* or center for shorter diagrams */ padding: 24px; border: 1px solid var(--border); } ``` ### Scaling Small Diagrams Mermaid sizes diagrams based on content, not container. Complex diagrams with many nodes render small to fit everything, leaving the text nearly unreadable. Three fixes: **1. Increase fontSize in themeVariables** (most effective): ```javascript mermaid.initialize({ theme: 'base', themeVariables: { fontSize: '18px', // default is 16px, bump to 18-20px for complex diagrams } }); ``` **2. CSS zoom** for diagrams that still render too small: ```css .mermaid-wrap--scaled .mermaid { zoom: 1.3; } ``` **3. Constrain container width** so the diagram doesn't float in dead space: ```css .mermaid-wrap--constrained { max-width: 800px; margin: 0 auto; } ``` **Rule of thumb:** If the diagram has 10+ nodes or the text is smaller than 12px rendered, increase fontSize to 18-20px or apply CSS zoom. ### Zoom Controls Add zoom controls to every `.mermaid-wrap` container for complex diagrams. **Small diagrams in slides.** If a diagram has fewer than ~7 nodes with no branching, it will render tiny in a full-viewport slide container. For simple linear flows (A → B → C → D), use CSS pipeline cards instead of Mermaid — see `slide-patterns.md` "CSS Pipeline Slide." Reserve Mermaid for complex graphs where automatic edge routing is actually needed. ### Full Pattern ```css .mermaid-wrap { position: relative; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 32px 24px; overflow: auto; /* CRITICAL: center the diagram both horizontally and vertically */ display: flex; justify-content: center; align-items: center; /* Prevent vertical flowcharts from compressing into unreadable thumbnails */ min-height: 400px; scrollbar-width: thin; scrollbar-color: var(--border) transparent; } .mermaid-wrap::-webkit-scrollbar { width: 6px; height: 6px; } .mermaid-wrap::-webkit-scrollbar-track { background: transparent; } .mermaid-wrap::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } .mermaid-wrap::-webkit-scrollbar-thumb:hover { background: var(--text-dim); } /* For shorter diagrams that don't need the full height */ .mermaid-wrap--compact { min-height: 200px; } /* For very tall vertical flowcharts */ .mermaid-wrap--tall { min-height: 600px; } .mermaid-wrap .mermaid { /* Use CSS zoom instead of transform: scale(). Zoom changes actual layout size, so overflow scrolls normally in all directions. Transform only changes visual appearance — content expanding upward/leftward goes into negative space which can't be scrolled to. Supported in all browsers (Firefox added support in v126, June 2024). Note: zoom is not animatable, so no transition. */ /* Optional: start at >1 for complex diagrams that render too small. The diagram stays centered, renders larger, and zoom controls still work. */ zoom: 1.4; } .zoom-controls { position: absolute; top: 8px; right: 8px; display: flex; gap: 2px; z-index: 10; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 2px; } .zoom-controls button { width: 28px; height: 28px; border: none; background: transparent; color: var(--text-dim); font-family: var(--font-mono); font-size: 14px; cursor: pointer; border-radius: 4px; display: flex; align-items: center; justify-content: center; transition: background 0.15s ease, color 0.15s ease; } .zoom-controls button:hover { background: var(--border); color: var(--text); } .mermaid-wrap { cursor: grab; } .mermaid-wrap.is-panning { cursor: grabbing; user-select: none; } ``` **Why zoom instead of transform?** CSS `transform: scale()` only changes visual appearance — the element's layout box stays the same size. When you scale from `center center`, content expands upward and leftward into negative coordinate space. Scroll containers can't scroll to negative positions, so the top and left of the zoomed content get clipped. CSS `zoom` actually changes the element's layout size. The content grows downward and rightward like any other growing element, staying fully scrollable. ### HTML ```html
            graph TD
              A --> B
          
        ``` **Click to expand.** Clicking anywhere on the diagram (without dragging) opens it full-size in a new tab. The expand button (⛶) in the zoom controls does the same thing. ### JavaScript Add once at the end of the page. Handles button clicks and scroll-to-zoom on all `.mermaid-wrap` containers: ```javascript // Match this to the CSS zoom value (or 1 if not set) var INITIAL_ZOOM = 1.4; function zoomDiagram(btn, factor) { var wrap = btn.closest('.mermaid-wrap'); var target = wrap.querySelector('.mermaid'); var current = parseFloat(target.dataset.zoom || INITIAL_ZOOM); var next = Math.min(Math.max(current * factor, 0.5), 5); target.dataset.zoom = next; target.style.zoom = next; } function resetZoom(btn) { var wrap = btn.closest('.mermaid-wrap'); var target = wrap.querySelector('.mermaid'); target.dataset.zoom = INITIAL_ZOOM; target.style.zoom = INITIAL_ZOOM; } function openDiagramFullscreen(btn) { var wrap = btn.closest('.mermaid-wrap'); openMermaidInNewTab(wrap); } function openMermaidInNewTab(wrap) { var svg = wrap.querySelector('.mermaid svg'); if (!svg) return; // Clone the SVG and remove any inline transforms from zoom var clone = svg.cloneNode(true); clone.style.zoom = ''; clone.style.transform = ''; // Get computed styles for theming var styles = getComputedStyle(document.documentElement); var bg = styles.getPropertyValue('--bg').trim() || '#ffffff'; // Build standalone HTML page var html = '' + '' + '' + 'Diagram' + '' + clone.outerHTML + ''; var blob = new Blob([html], { type: 'text/html' }); var url = URL.createObjectURL(blob); window.open(url, '_blank'); } document.querySelectorAll('.mermaid-wrap').forEach(function(wrap) { // Ctrl/Cmd + scroll to zoom wrap.addEventListener('wheel', function(e) { if (!e.ctrlKey && !e.metaKey) return; e.preventDefault(); var target = wrap.querySelector('.mermaid'); var current = parseFloat(target.dataset.zoom || INITIAL_ZOOM); var factor = e.deltaY < 0 ? 1.1 : 0.9; var next = Math.min(Math.max(current * factor, 0.5), 5); target.dataset.zoom = next; target.style.zoom = next; }, { passive: false }); // Click-and-drag to pan, click (without drag) to open full-size var startX, startY, scrollL, scrollT, startTime, didPan; wrap.addEventListener('mousedown', function(e) { if (e.target.closest('.zoom-controls')) return; wrap.classList.add('is-panning'); startX = e.clientX; startY = e.clientY; scrollL = wrap.scrollLeft; scrollT = wrap.scrollTop; startTime = Date.now(); didPan = false; }); window.addEventListener('mousemove', function(e) { if (!wrap.classList.contains('is-panning')) return; var dx = e.clientX - startX; var dy = e.clientY - startY; if (Math.abs(dx) > 5 || Math.abs(dy) > 5) didPan = true; wrap.scrollLeft = scrollL - dx; wrap.scrollTop = scrollT - dy; }); window.addEventListener('mouseup', function() { if (!wrap.classList.contains('is-panning')) return; wrap.classList.remove('is-panning'); // If click was quick and didn't move much, open full-size var elapsed = Date.now() - startTime; if (!didPan && elapsed < 300) { openMermaidInNewTab(wrap); } }); }); ``` Scroll-to-zoom requires Ctrl/Cmd+scroll to avoid hijacking normal page scroll. Cursor changes to `grab`/`grabbing` to signal pan mode. The zoom range is capped at 0.5x–5x. **Clicking without dragging opens the diagram full-size in a new browser tab.** ## Grid Layouts ### Architecture Diagram (2-column with sidebar) ```css .arch-grid { display: grid; grid-template-columns: 260px 1fr; grid-template-rows: auto; gap: 20px; max-width: 1100px; margin: 0 auto; } .arch-grid__sidebar { grid-column: 1; } .arch-grid__main { grid-column: 2; } .arch-grid__full { grid-column: 1 / -1; } ``` ### Pipeline (horizontal steps) ```css .pipeline { display: flex; align-items: stretch; gap: 0; overflow-x: auto; padding-bottom: 8px; } .pipeline__step { min-width: 130px; flex-shrink: 0; } .pipeline__arrow { display: flex; align-items: center; padding: 0 4px; color: var(--border-bright); font-size: 18px; flex-shrink: 0; } /* Parallel branch within a pipeline */ .pipeline__parallel { display: flex; flex-direction: column; gap: 6px; } ``` ### Card Grid (dashboard / metrics) ```css .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; } ``` ### Data Tables Use real `` elements for tabular data. Wrap in a scrollable container for wide tables. ```css /* Scrollable wrapper for wide tables */ .table-wrap { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; } .table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; } /* Base table */ .data-table { width: 100%; border-collapse: collapse; font-size: 13px; line-height: 1.5; } /* Header */ .data-table thead { position: sticky; top: 0; z-index: 2; } .data-table th { background: var(--surface-elevated, var(--surface2, var(--surface))); font-family: var(--font-mono); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--text-dim); text-align: left; padding: 12px 16px; border-bottom: 2px solid var(--border-bright); white-space: nowrap; } /* Cells */ .data-table td { padding: 12px 16px; border-bottom: 1px solid var(--border); vertical-align: top; color: var(--text); } /* Let text-heavy columns wrap naturally */ .data-table .wide { min-width: 200px; max-width: 500px; } /* Right-align numeric columns */ .data-table td.num, .data-table th.num { text-align: right; font-variant-numeric: tabular-nums; font-family: var(--font-mono); } /* Alternating rows */ .data-table tbody tr:nth-child(even) { background: var(--accent-dim); } /* Row hover */ .data-table tbody tr { transition: background 0.15s ease; } .data-table tbody tr:hover { background: var(--border); } /* Last row: no bottom border (container handles it) */ .data-table tbody tr:last-child td { border-bottom: none; } /* Code inside cells */ .data-table code { font-family: var(--font-mono); font-size: 11px; background: var(--accent-dim); color: var(--accent); padding: 1px 5px; border-radius: 3px; } /* Secondary detail text */ .data-table small { display: block; color: var(--text-dim); font-size: 11px; margin-top: 2px; } ``` #### Status Indicators Styled spans for match/gap/warning states. Never use emoji. ```css .status { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: 11px; font-weight: 500; padding: 3px 10px; border-radius: 6px; white-space: nowrap; } .status--match { background: var(--green-dim, rgba(5, 150, 105, 0.1)); color: var(--green, #059669); } .status--gap { background: var(--red-dim, rgba(239, 68, 68, 0.1)); color: var(--red, #ef4444); } .status--warn { background: var(--orange-dim, rgba(217, 119, 6, 0.1)); color: var(--orange, #d97706); } .status--info { background: var(--accent-dim); color: var(--accent); } /* Dot variant (compact, no text) */ .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } .status-dot--match { background: var(--green, #059669); } .status-dot--gap { background: var(--red, #ef4444); } .status-dot--warn { background: var(--orange, #d97706); } ``` Usage in table cells: ```html ``` #### Table Summary Row For totals, counts, or aggregate status at the bottom: ```css .data-table tfoot td { background: var(--surface-elevated, var(--surface2, var(--surface))); font-weight: 600; font-size: 12px; border-top: 2px solid var(--border-bright); border-bottom: none; padding: 12px 16px; } ``` #### Sticky First Column (for very wide tables) ```css .data-table th:first-child, .data-table td:first-child { position: sticky; left: 0; z-index: 1; background: var(--surface); } .data-table tbody tr:nth-child(even) td:first-child { background: color-mix(in srgb, var(--surface) 95%, var(--accent) 5%); } ``` ## Connectors ### CSS Arrow (vertical, between stacked sections) ```css .flow-arrow { display: flex; justify-content: center; align-items: center; gap: 8px; color: var(--text-dim); font-family: var(--font-mono); font-size: 12px; padding: 6px 0; } /* Down arrow via SVG icon */ .flow-arrow svg { width: 20px; height: 20px; fill: none; stroke: var(--border-bright); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } ``` Down arrow SVG (reuse inline): ```html ``` ### CSS Arrow (horizontal, between inline steps) Use `::after` or a literal arrow character: ```css .h-arrow::after { content: '→'; color: var(--border-bright); font-size: 18px; padding: 0 4px; } ``` ### SVG Curved Connector (between arbitrary nodes) For connections that aren't simple vertical/horizontal, use an absolutely positioned SVG overlay: ```html ``` Position the parent container as `position: relative` to scope the SVG overlay. ## Animations ### Staggered Fade-In on Load Define the keyframe once, then stagger via a `--i` CSS variable set per element. This approach works regardless of DOM nesting or interleaved non-animated elements (unlike `nth-child` which breaks when siblings aren't all the same type). ```css @keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } } .ve-card { animation: fadeUp 0.4s ease-out both; animation-delay: calc(var(--i, 0) * 0.05s); } ``` Set `--i` per element in the HTML to control stagger order: ```html
        First
        ...
        Second
        ...
        Third
        ``` ### Hover Lift ```css .ve-card { transition: transform 0.2s ease, box-shadow 0.2s ease; } .ve-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } ``` ### Scale-Fade (for KPI cards, badges, status indicators) ```css @keyframes fadeScale { from { opacity: 0; transform: scale(0.92); } to { opacity: 1; transform: scale(1); } } .kpi-card { animation: fadeScale 0.35s ease-out both; animation-delay: calc(var(--i, 0) * 0.06s); } ``` ### SVG Draw-In (for connectors, progress rings, path elements) ```css @keyframes drawIn { from { stroke-dashoffset: var(--path-length); } to { stroke-dashoffset: 0; } } /* Set --path-length to the path's getTotalLength() value */ .connector path { stroke-dasharray: var(--path-length); animation: drawIn 0.8s ease-in-out both; animation-delay: calc(var(--i, 0) * 0.1s); } ``` ### CSS Counter (for hero numbers without JS) Uses `@property` to animate a custom property as an integer, then display it via `counter()`. No JS required. Falls back to showing the final value immediately in browsers without `@property` support. ```css @property --count { syntax: ''; initial-value: 0; inherits: false; } @keyframes countUp { to { --count: var(--target); } } .kpi-card__value--animated { --target: 247; counter-reset: val var(--count); animation: countUp 1.2s ease-out forwards; } .kpi-card__value--animated::after { content: counter(val); } ``` ### Choreography Don't use the same animation for everything. Mix types by element role, with easing stagger (fast-then-slow, not linear): - **Cards**: `fadeUp` — the default entrance, reliable and subtle - **KPI / badges**: `fadeScale` — scale draws the eye to important numbers - **SVG connectors**: `drawIn` — reveals flow direction, pairs with card stagger - **Hero numbers**: `countUp` — counting motion signals "this number matters" - **Stagger timing**: `calc(var(--i) * 0.06s)` with lower `--i` values on important elements so they appear first ### Respect Reduced Motion ```css @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } ``` ## Sparklines and Simple Charts (Pure SVG) For simple inline visualizations without a library: ```html
        ``` ## Responsive Breakpoint Include a single breakpoint for narrow viewports: ```css @media (max-width: 768px) { .arch-grid { grid-template-columns: 1fr; } .pipeline { flex-wrap: wrap; gap: 8px; } .pipeline__arrow { display: none; } body { padding: 16px; } } ``` ## Badges and Tags Small inline labels for categorizing elements: ```css .tag { font-family: var(--font-mono); font-size: 10px; font-weight: 500; padding: 2px 7px; border-radius: 4px; background: var(--node-a-dim); color: var(--node-a); } ``` ## Lists Inside Nodes For tool listings, feature lists, table columns: ```css .node-list { list-style: none; padding: 0; margin: 0; font-size: 12px; line-height: 1.8; } .node-list li { padding-left: 14px; position: relative; } .node-list li::before { content: '›'; color: var(--text-dim); font-weight: 600; position: absolute; left: 0; } .node-list code { font-family: var(--font-mono); font-size: 11px; background: var(--accent-dim); color: var(--accent); padding: 1px 5px; border-radius: 3px; } ``` ## KPI / Metric Cards Large hero number with trend indicator and label. For dashboards, review summaries, and impact sections. ```css .kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 16px; } .kpi-card { background: var(--surface-elevated); border: 1px solid var(--border); border-radius: 10px; padding: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); } .kpi-card__value { font-size: 36px; font-weight: 700; letter-spacing: -1px; line-height: 1.1; font-variant-numeric: tabular-nums; } .kpi-card__label { font-family: var(--font-mono); font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.5px; color: var(--text-dim); margin-top: 6px; } .kpi-card__trend { font-family: var(--font-mono); font-size: 12px; margin-top: 4px; } .kpi-card__trend--up { color: var(--node-b, #059669); } .kpi-card__trend--down { color: var(--red, #ef4444); } ``` ```html
        247
        Lines Added
        +34%
        ``` ## Before / After Panels Two-column comparison with diff-colored headers. For review pages, migration docs, and feature comparisons. ```css .diff-panels { display: grid; grid-template-columns: 1fr 1fr; gap: 0; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; } .diff-panels > * { min-width: 0; overflow-wrap: break-word; } .diff-panel__header { font-family: var(--font-mono); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; padding: 10px 16px; } .diff-panel__header--before { background: var(--red-dim, rgba(239, 68, 68, 0.08)); color: var(--red, #ef4444); border-bottom: 2px solid var(--red, #ef4444); } .diff-panel__header--after { background: var(--green-dim, rgba(5, 150, 105, 0.08)); color: var(--green, #059669); border-bottom: 2px solid var(--green, #059669); } .diff-panel__body { padding: 16px; background: var(--surface); font-size: 13px; line-height: 1.6; } /* Highlight changed items within a panel */ .diff-changed { background: var(--accent-dim); border-radius: 3px; padding: 0 3px; } @media (max-width: 768px) { .diff-panels { grid-template-columns: 1fr; } } ``` ```html
        Before
        After
        Previous implementation...
        New implementation...
        ``` ## Collapsible Sections Native `
        /` with styled disclosure. Zero JS, accessible. For lower-priority content: file maps, decision logs, reference sections. ```css details.collapsible { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; } details.collapsible summary { padding: 14px 20px; background: var(--surface); font-family: var(--font-mono); font-size: 12px; font-weight: 600; cursor: pointer; list-style: none; display: flex; align-items: center; gap: 8px; color: var(--text); transition: background 0.15s ease; } details.collapsible summary:hover { background: var(--surface-elevated, var(--surface)); } details.collapsible summary::-webkit-details-marker { display: none; } /* Chevron indicator */ details.collapsible summary::before { content: '▸'; font-size: 11px; color: var(--text-dim); transition: transform 0.15s ease; } details.collapsible[open] summary::before { transform: rotate(90deg); } details.collapsible .collapsible__body { padding: 16px 20px; border-top: 1px solid var(--border); font-size: 13px; line-height: 1.6; } ``` ```html
        File Map (14 files changed)
        ``` ## Prose Page Elements Patterns for documentation, articles, blog posts, and other reading-first content. The key difference from visual explanations: optimize for sustained reading, not scanning. ### Body Text Settings ```css /* Comfortable reading baseline */ .prose { font-size: clamp(17px, 1.1vw + 14px, 19px); line-height: 1.7; max-width: 65ch; /* ~600-680px */ text-wrap: pretty; } .prose p { margin-bottom: 1.5em; } /* Narrow column for essays/literary content */ .prose--narrow { max-width: 60ch; line-height: 1.8; } /* Wide column for technical content with code */ .prose--wide { max-width: 75ch; line-height: 1.6; } ``` ### Lead Paragraph Opening paragraph styled distinctly from body text. ```css /* Larger size */ .lead { font-size: 20px; line-height: 1.6; color: var(--text-bright); margin-bottom: 32px; } /* With drop cap */ .lead--dropcap::first-letter { float: left; font-family: var(--font-display); font-size: 64px; font-weight: 600; line-height: 0.85; padding-right: 12px; padding-top: 6px; color: var(--accent); } ``` ### Pull Quotes Key insights pulled out for emphasis. Use sparingly — one or two per article maximum. ```css /* Border left — most versatile */ .pullquote { margin: 48px 0; padding-left: 24px; border-left: 3px solid var(--accent); } .pullquote p { font-size: 22px; font-style: italic; line-height: 1.4; color: var(--text-bright); margin: 0; } /* Centered with quotation mark */ .pullquote--centered { margin: 56px 0; padding: 32px 40px; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); text-align: center; position: relative; } .pullquote--centered::before { content: '"'; position: absolute; top: -12px; left: 50%; transform: translateX(-50%); background: var(--bg); padding: 0 16px; font-family: var(--font-display); font-size: 48px; color: var(--accent); line-height: 1; } ``` ### Section Dividers ```css /* Horizontal rule */ hr { border: none; height: 1px; background: var(--border); margin: 48px 0; } /* Ornamental divider — use:
        ✦ ✦ ✦
        */ .divider { text-align: center; margin: 48px 0; color: var(--text-dim); font-size: 18px; letter-spacing: 12px; } ``` ### Article Hero Patterns ```css /* Centered minimal — essays, personal posts */ .hero--centered { text-align: center; padding: 80px 24px 64px; max-width: 800px; margin: 0 auto; } .hero__category { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 2px; color: var(--accent); margin-bottom: 16px; } .hero__title { font-size: clamp(32px, 5vw, 48px); font-weight: 600; line-height: 1.15; margin-bottom: 16px; } .hero__subtitle { font-size: 20px; font-style: italic; color: var(--text-dim); max-width: 600px; margin: 0 auto 24px; } .hero__meta { font-size: 13px; color: var(--text-dim); } /* Left-aligned editorial — features, documentation */ .hero--editorial { padding: 100px 40px 60px; max-width: 1000px; margin: 0 auto; } .hero--editorial .hero__title { font-size: clamp(40px, 7vw, 72px); font-weight: 800; line-height: 1.0; letter-spacing: -2px; } ``` ### Author Byline ```css .byline { display: flex; align-items: center; gap: 12px; margin-top: 24px; } .byline__avatar { width: 40px; height: 40px; border-radius: 50%; } .byline__name { font-weight: 600; color: var(--text-bright); display: block; } .byline__meta { font-size: 13px; color: var(--text-dim); } ``` ### Callout Boxes For warnings, tips, notes, and key takeaways. ```css .callout { padding: 16px 20px; border-radius: 8px; border-left: 4px solid var(--callout-border); background: var(--callout-bg); margin: 24px 0; } .callout--info { --callout-border: var(--accent); --callout-bg: color-mix(in srgb, var(--accent) 10%, transparent); } .callout--warning { --callout-border: var(--amber); --callout-bg: color-mix(in srgb, var(--amber) 10%, transparent); } .callout--success { --callout-border: var(--green); --callout-bg: color-mix(in srgb, var(--green) 10%, transparent); } .callout__title { font-weight: 600; margin-bottom: 8px; color: var(--callout-border); } /* Lists inside callouts need padding fix */ .callout ul, .callout ol { padding-left: 1.5em; margin: 8px 0 0 0; } ``` ### Theme Toggle Use `data-theme` attribute for user-controllable light/dark modes. Random initial theme adds variety. ```css :root, [data-theme="light"] { --bg: #fafaf9; --surface: #ffffff; --text: #1c1917; --text-dim: #78716c; --border: #e7e5e4; --accent: #0d9488; } [data-theme="dark"] { --bg: #0c0a09; --surface: #1c1917; --text: #fafaf9; --text-dim: #a8a29e; --border: #292524; --accent: #14b8a6; } ``` ```javascript // Random initial theme const themes = ['light', 'dark']; document.documentElement.setAttribute('data-theme', themes[Math.floor(Math.random() * 2)]); // Toggle function function toggleTheme() { const current = document.documentElement.getAttribute('data-theme'); document.documentElement.setAttribute('data-theme', current === 'light' ? 'dark' : 'light'); } ``` ```html ``` ```css .theme-toggle { position: fixed; top: 20px; right: 20px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 8px; cursor: pointer; z-index: 100; } [data-theme="light"] .theme-toggle__moon { display: none; } [data-theme="dark"] .theme-toggle__sun { display: none; } ``` ### Prose Anti-Patterns Avoid these in reading-first content: - Body text smaller than 16px - Line-height below 1.5 - Measure wider than 75ch (text spanning full viewport) - Pull quotes every other paragraph - Drop caps on every section - Busy background patterns behind text ## Generated Images For AI-generated illustrations embedded as base64 data URIs via `surf gemini --generate-image`. Use sparingly — hero banners, conceptual illustrations, educational diagrams, decorative accents. ### Hero Banner Full-width image cropped to a fixed height with a gradient fade into the page background. Place at the top of the page before the title, or between the title and the first content section. ```css .hero-img-wrap { position: relative; border-radius: 12px; overflow: hidden; margin-bottom: 24px; } .hero-img-wrap img { width: 100%; height: 240px; object-fit: cover; display: block; } /* Gradient fade into page background */ .hero-img-wrap::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 50%; background: linear-gradient(to top, var(--bg), transparent); pointer-events: none; } ``` ```html
        Descriptive alt text
        ``` Generate with `--aspect-ratio 16:9` for hero banners. ### Inline Illustration Centered image with border, shadow, and optional caption. Use within content sections for conceptual or educational illustrations. ```css .illus { text-align: center; margin: 24px 0; } .illus img { max-width: 480px; width: 100%; border-radius: 10px; border: 1px solid var(--border); box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); } .illus figcaption { font-family: var(--font-mono); font-size: 11px; color: var(--text-dim); margin-top: 8px; } ``` ```html
        Descriptive alt text
        How the message queue routes events between services
        ``` Generate with `--aspect-ratio 1:1` or `--aspect-ratio 4:3` for inline illustrations. ### Side Accent Small image floated beside a section. Use when the illustration supports but doesn't dominate the content. ```css .accent-img { float: right; max-width: 200px; margin: 0 0 16px 24px; border-radius: 10px; border: 1px solid var(--border); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); } @media (max-width: 768px) { .accent-img { float: none; max-width: 100%; margin: 0 0 16px 0; } } ``` ```html Descriptive alt text ```
        Match Gap Partial