Merge pull request #11 from cathrynlavery/loop-type

Add the Loop type (flywheel) + palette linter — 27 types
This commit is contained in:
Cathryn Lavery
2026-07-14 19:52:07 -07:00
committed by GitHub
25 changed files with 915 additions and 50 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "diagram-design",
"metadata": {
"description": "Editorial-quality technical and product diagrams — 26 types rendered as standalone HTML with inline SVG, skinnable to match your brand"
"description": "Editorial-quality technical and product diagrams — 27 types rendered as standalone HTML with inline SVG, skinnable to match your brand"
},
"owner": {
"name": "Cathryn Lavery"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "diagram-design",
"description": "Editorial-quality technical and product diagrams — 26 types rendered as standalone HTML with inline SVG, skinnable to match your brand",
"description": "Editorial-quality technical and product diagrams — 27 types rendered as standalone HTML with inline SVG, skinnable to match your brand",
"version": "2.0.0",
"homepage": "https://github.com/cathrynlavery/diagram-design",
"repository": "https://github.com/cathrynlavery/diagram-design",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "diagram-design",
"description": "Editorial-quality technical and product diagrams - 26 types rendered as standalone HTML with inline SVG, skinnable to match your brand",
"description": "Editorial-quality technical and product diagrams - 27 types rendered as standalone HTML with inline SVG, skinnable to match your brand",
"version": "2.0.0",
"author": {
"name": "Cathryn Lavery",
+27 -13
View File
@@ -4,7 +4,11 @@
![Content site architecture](docs/screenshots/architecture.png)
26 types. One Claude Code skill. Your brand in 60 seconds — the skill reads your website and maps colors + fonts to every diagram.
![The self-improving loop](docs/screenshots/loop.png)
*New in 2.0 — the Loop: flywheels with a shared-memory hub. The dashed lines are the write-backs.*
27 types. One Claude Code skill. Your brand in 60 seconds — the skill reads your website and maps colors + fonts to every diagram.
No Figma. No generic rounded boxes. No 30-minute color-picking sessions.
@@ -14,7 +18,7 @@ No Figma. No generic rounded boxes. No 30-minute color-picking sessions.
I write at [littlemight.com](https://littlemight.com?utm_source=diagram-design&utm_medium=readme&utm_campaign=github&utm_content=intro) (and run [BestSelf.co](https://bestself.co?utm_source=diagram-design&utm_medium=readme&utm_campaign=github&utm_content=intro) on the side). Every time I needed a diagram — an architecture sketch, a flowchart, a pyramid of what matters most — I'd ask Claude and get back a generic rounded-box thing that looked nothing like the rest of the site. I'd either fight with Figma for 30 minutes or just skip the diagram.
So I built a Claude Code skill for it. Twenty-six types, editorial quality, matches your brand in 60 seconds by reading your website.
So I built a Claude Code skill for it. Twenty-seven types, editorial quality, matches your brand in 60 seconds by reading your website.
> *The highest-quality move is usually deletion.* Every node earns its place. The accent color is reserved for the 1–2 things the reader should look at first. Target density: 4/10.
@@ -22,7 +26,7 @@ So I built a Claude Code skill for it. Twenty-six types, editorial quality, matc
## What it makes
All 26 diagrams ship in three variants: minimal light, minimal dark, and full-editorial. Open any of them directly in a browser — no build step, no JS, no external images.
All 27 diagrams ship in three variants: minimal light, minimal dark, and full-editorial. Open any of them directly in a browser — no build step, no JS, no external images.
<table>
<tr>
@@ -52,27 +56,32 @@ All 26 diagrams ship in three variants: minimal light, minimal dark, and full-ed
</tr>
<tr>
<td align="center"><img src="docs/screenshots/radar.png" alt="Radar"><br><b>Radar / Spider</b><br><sub>Multi-axis comparison</sub></td>
<td align="center"><img src="docs/screenshots/loop.png" alt="Loop"><br><b>Loop</b><br><sub>Flywheel · stations around a hub</sub></td>
<td align="center"><img src="docs/screenshots/it-state.png" alt="IT current-state"><br><b>IT current-state</b><br><sub>Legacy landscape · modernization</sub></td>
<td align="center"><img src="docs/screenshots/high-level.png" alt="High-Level"><br><b>High-Level</b><br><sub>End-to-end stack on a cluster</sub></td>
</tr>
<tr>
<td align="center"><img src="docs/screenshots/high-level.png" alt="High-Level"><br><b>High-Level</b><br><sub>End-to-end stack on a cluster</sub></td>
<td align="center"><img src="docs/screenshots/bar.png" alt="Bar chart"><br><b>Bar chart</b><br><sub>Categorical comparison</sub></td>
<td align="center"><img src="docs/screenshots/line.png" alt="Line chart"><br><b>Line chart</b><br><sub>Trends over time</sub></td>
<td align="center"><img src="docs/screenshots/gantt.png" alt="Gantt"><br><b>Gantt</b><br><sub>Tasks and phases on a timeline</sub></td>
</tr>
<tr>
<td align="center"><img src="docs/screenshots/gantt.png" alt="Gantt"><br><b>Gantt</b><br><sub>Tasks and phases on a timeline</sub></td>
<td align="center"><img src="docs/screenshots/scatter.png" alt="Scatter plot"><br><b>Scatter plot</b><br><sub>Distribution and correlation</sub></td>
<td align="center"><img src="docs/screenshots/process.png" alt="Process"><br><b>Process</b><br><sub>Multi-actor sequential workflow</sub></td>
<td align="center"><img src="docs/screenshots/medallion.png" alt="Medallion"><br><b>Medallion</b><br><sub>Multi-tier data storage</sub></td>
</tr>
<tr>
<td align="center"><img src="docs/screenshots/medallion.png" alt="Medallion"><br><b>Medallion</b><br><sub>Multi-tier data storage</sub></td>
<td align="center"><img src="docs/screenshots/data-flow.png" alt="Data flow"><br><b>Data flow</b><br><sub>Role-scoped pipeline steps</sub></td>
<td align="center"><img src="docs/screenshots/dp-integration.png" alt="DP integration"><br><b>DP integration</b><br><sub>Sources → core → consumers</sub></td>
</tr>
<tr>
<td align="center"><img src="docs/screenshots/dp-security-matrix.png" alt="DP security matrix"><br><b>DP security matrix</b><br><sub>Per-role access permissions</sub></td>
<td></td>
<td></td>
</tr>
</table>
**Browse the live gallery:** open [`skills/diagram-design/assets/index.html`](skills/diagram-design/assets/index.html) in your browser to flip through all 26 diagrams with light / dark / full-editorial tabs.
**Browse the live gallery:** open [`skills/diagram-design/assets/index.html`](skills/diagram-design/assets/index.html) in your browser to flip through all 27 diagrams with light / dark / full-editorial tabs.
---
@@ -149,7 +158,7 @@ Before writing tokens, the skill verifies WCAG AA contrast on `ink` over `paper`
### Manual override
Prefer to set tokens by hand? Open [`skills/diagram-design/references/style-guide.md`](skills/diagram-design/references/style-guide.md) and edit the table. Everything downstream reads from there — all 26 diagrams, the annotation primitive, and the gallery all inherit semantic role names (`accent`, not `#eb6c36`).
Prefer to set tokens by hand? Open [`skills/diagram-design/references/style-guide.md`](skills/diagram-design/references/style-guide.md) and edit the table. Everything downstream reads from there — all 27 diagrams, the annotation primitive, and the gallery all inherit semantic role names (`accent`, not `#eb6c36`).
### First-run gate
@@ -164,7 +173,7 @@ See [`skills/diagram-design/references/onboarding.md`](skills/diagram-design/ref
## Quickstart
```bash
# Open the gallery to see all 26 diagrams
# Open the gallery to see all 27 diagrams
open ~/.claude/skills/diagram-design/assets/index.html
# In Claude Code, just ask:
@@ -235,16 +244,21 @@ diagram-design/
├── assets/
│ ├── index.html — live gallery, tabbed
│ ├── template*.html — scaffolds for new diagrams
│ ├── example-<type>.html — 3 variants × 26 types
│ ├── example-<type>.html — 3 variants × 27 types
│ └── example-quadrant-consultant.html — consultant-special 2×2 scenario matrix
└── docs/screenshots/ — the images in this README
```
This keeps Claude's working context tight (only load what you need) and makes the skill easy to extend — drop a new `type-<name>.md` and wire it into the selection guide. The skill ships with 32 reference files covering every diagram type, primitive, and utility.
This keeps Claude's working context tight (only load what you need) and makes the skill easy to extend — drop a new `type-<name>.md` and wire it into the selection guide. The skill ships with 33 reference files covering every diagram type, primitive, and utility.
### Contributing / skin lint
Before submitting a new example, run `python3 scripts/lint-skin.py <your-new-example.html>`.
The repository-wide check `python3 scripts/lint-skin.py --all --baseline` must stay green.
### What loads when
The top-level `SKILL.md` is always in context. Everything else is pulled in only when relevant — this is what keeps the skill fast even with 32 reference files.
The top-level `SKILL.md` is always in context. Everything else is pulled in only when relevant — this is what keeps the skill fast even with 33 reference files.
| You ask for… | Claude loads |
|---|---|
@@ -253,7 +267,7 @@ The top-level `SKILL.md` is always in context. Everything else is pulled in only
| "Onboard this skill to my site" | `SKILL.md` + `references/onboarding.md` + `references/style-guide.md` |
| "Add an editorial callout to this diagram" | `SKILL.md` + `references/primitive-annotation.md` |
| "Give me a hand-drawn version" | `SKILL.md` + `references/primitive-sketchy.md` |
| Routine diagram-making (any of the 26 diagrams) | Only `SKILL.md` + that one type's reference |
| Routine diagram-making (any of the 27 diagrams) | Only `SKILL.md` + that one type's reference |
No matter how many types exist, Claude only reads the one you need. Add a new type tomorrow and nothing else changes.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

+20
View File
@@ -0,0 +1,20 @@
example-architecture-dark.html
example-data-flow-dark.html
example-data-flow-full.html
example-data-flow.html
example-dp-integration-dark.html
example-dp-integration-full.html
example-dp-integration.html
example-high-level-dark.html
example-high-level-full.html
example-high-level-vertical-dark.html
example-high-level-vertical-full.html
example-high-level-vertical.html
example-high-level.html
example-it-state-dark.html
example-it-state-full.html
example-it-state.html
example-layers-dark.html
example-sequence-dark.html
example-timeline-dark.html
example-venn-dark.html
+267
View File
@@ -0,0 +1,267 @@
#!/usr/bin/env python3
"""Lint NEW and CHANGED diagram examples against the current visual skin.
Pre-2.0 examples may legitimately fail because they were built against an older
skin. Use ``--all --baseline`` to skip those documented legacy files.
"""
import argparse
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
STYLE_GUIDE = ROOT / "skills/diagram-design/references/style-guide.md"
ASSET_DIR = ROOT / "skills/diagram-design/assets"
BASELINE = ROOT / "scripts/lint-skin-baseline.txt"
HEX_RE = re.compile(
r"(?<![\w-])#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})(?![0-9a-fA-F])"
)
RGBA_RE = re.compile(
r"rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([^)]+)\)",
re.IGNORECASE,
)
BLACK_RGB_RE = re.compile(r"rgb\(\s*0\s*,\s*0\s*,\s*0\s*\)", re.IGNORECASE)
SCRIPT_RE = re.compile(r"<script\b", re.IGNORECASE)
SRC_HTTP_RE = re.compile(r"\bsrc\s*=\s*(['\"])\s*https?://", re.IGNORECASE)
IMPORT_HTTP_RE = re.compile(
r"@import\s+(?:url\(\s*)?(?:['\"]\s*)?https?://", re.IGNORECASE
)
URL_HTTP_RE = re.compile(r"url\(\s*(?:['\"]\s*)?https?://", re.IGNORECASE)
LINK_RE = re.compile(r"<link\b[^>]*>", re.IGNORECASE | re.DOTALL)
HREF_RE = re.compile(r"\bhref\s*=\s*(['\"])(.*?)\1", re.IGNORECASE | re.DOTALL)
FONT_CSS_RE = re.compile(r"font-family\s*:\s*([^;}]+)", re.IGNORECASE)
FONT_ATTR_RE = re.compile(
r"\bfont-family\s*=\s*(['\"])(.*?)\1", re.IGNORECASE | re.DOTALL
)
ALLOWED_FONTS = {
"instrument serif",
"geist",
"geist mono",
"system-ui",
"sans-serif",
"serif",
"monospace",
"ui-monospace",
}
CSS_FONT_KEYWORDS = {"inherit", "initial", "revert", "revert-layer", "unset"}
def normalize_hex(value):
value = value.lower()
if len(value) == 4:
return "#" + "".join(character * 2 for character in value[1:])
return value
def table_hexes(markdown, heading):
"""Return hex colors from the first Markdown table after *heading*."""
start = markdown.find(heading)
if start < 0:
raise ValueError(f"missing {heading!r} in {STYLE_GUIDE}")
table_started = False
colors = set()
for line in markdown[start:].splitlines()[1:]:
if line.startswith("|"):
table_started = True
colors.update(normalize_hex(match.group()) for match in HEX_RE.finditer(line))
elif table_started:
break
return colors
def allowed_colors():
markdown = STYLE_GUIDE.read_text(encoding="utf-8")
colors = table_hexes(markdown, "### Semantic roles")
colors.update(table_hexes(markdown, "### Series palette"))
colors.update({"#fff", "#ffffff"})
rgb_triplets = set()
for color in colors:
expanded = normalize_hex(color)
if len(expanded) == 7:
rgb_triplets.add(
tuple(int(expanded[offset : offset + 2], 16) for offset in (1, 3, 5))
)
return colors, rgb_triplets
def line_number(text, offset):
return text.count("\n", 0, offset) + 1
def display_path(path):
try:
return path.resolve().relative_to(ROOT).as_posix()
except ValueError:
return str(path)
def named_families(value):
families = []
for raw_family in value.split(","):
family = raw_family.strip().strip("'\"").strip()
lowered = family.casefold()
if not family or lowered in CSS_FONT_KEYWORDS or lowered.startswith("var("):
continue
if lowered not in ALLOWED_FONTS:
families.append(family)
return families
def lint_text(text, colors, rgb_triplets):
findings = []
def add(offset, category, message):
findings.append((line_number(text, offset), offset, category, message))
for match in HEX_RE.finditer(text):
value = match.group()
normalized = normalize_hex(value)
if normalized == "#000000":
add(match.start(), "pure-black", f"pure black {value} is not allowed")
elif normalized not in colors:
add(match.start(), "color", f"{value} is not in the style-guide palette")
for match in RGBA_RE.finditer(text):
rgb = tuple(int(match.group(index)) for index in (1, 2, 3))
if rgb not in rgb_triplets:
add(
match.start(),
"color",
f"{match.group()} is not derived from an allowed palette color",
)
for match in BLACK_RGB_RE.finditer(text):
add(match.start(), "pure-black", "pure black rgb(0,0,0) is not allowed")
for match in SCRIPT_RE.finditer(text):
add(match.start(), "script", "<script> tags are not allowed")
for match in SRC_HTTP_RE.finditer(text):
add(match.start(), "external-asset", "external HTTP(S) src is not allowed")
import_spans = []
for match in IMPORT_HTTP_RE.finditer(text):
import_spans.append(match.span())
add(match.start(), "external-asset", "external HTTP(S) @import is not allowed")
for match in URL_HTTP_RE.finditer(text):
if any(start <= match.start() < end for start, end in import_spans):
continue
add(match.start(), "external-asset", "external HTTP(S) url() is not allowed")
for link_match in LINK_RE.finditer(text):
href_match = HREF_RE.search(link_match.group())
if not href_match:
continue
href = href_match.group(2).strip()
if href.lower().startswith("https://fonts.googleapis.com"):
continue
if href.lower().startswith(("http://", "https://")):
href_offset = link_match.start() + href_match.start(2)
add(href_offset, "external-asset", "external HTTP(S) <link> is not allowed")
for match in FONT_CSS_RE.finditer(text):
unsupported = named_families(match.group(1))
if unsupported:
add(
match.start(),
"font-family",
"unsupported font family: " + ", ".join(unsupported),
)
for match in FONT_ATTR_RE.finditer(text):
unsupported = named_families(match.group(2))
if unsupported:
add(
match.start(),
"font-family",
"unsupported font family: " + ", ".join(unsupported),
)
findings.sort(key=lambda finding: (finding[0], finding[1], finding[2]))
return findings
def load_baseline():
if not BASELINE.exists():
return set()
return {
line.strip()
for line in BASELINE.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")
}
def parse_args():
parser = argparse.ArgumentParser(
description="Lint diagram examples against the colors and fonts in style-guide.md."
)
parser.add_argument("files", nargs="*", type=Path, help="HTML examples to lint")
parser.add_argument(
"--all",
action="store_true",
help="lint every skills/diagram-design/assets/example-*.html file",
)
parser.add_argument(
"--baseline",
action="store_true",
help="under --all, skip files listed in scripts/lint-skin-baseline.txt",
)
parser.add_argument(
"--quiet", action="store_true", help="print only the summary line"
)
args = parser.parse_args()
if args.all == bool(args.files):
parser.error("provide either file arguments or --all")
return args
def main():
args = parse_args()
colors, rgb_triplets = allowed_colors()
skipped = 0
if args.all:
paths = sorted(ASSET_DIR.glob("example-*.html"))
if args.baseline:
baseline = load_baseline()
selected = []
for path in paths:
relative = display_path(path)
if path.name in baseline or relative in baseline:
skipped += 1
else:
selected.append(path)
paths = selected
else:
paths = args.files
total_findings = 0
for path in paths:
try:
text = path.read_text(encoding="utf-8")
findings = lint_text(text, colors, rgb_triplets)
except OSError as error:
findings = [(0, 0, "read-error", str(error))]
total_findings += len(findings)
if not args.quiet:
shown_path = display_path(path)
for line, _offset, category, message in findings:
print(f"{shown_path}:{line}: {category}: {message}")
print(
f"Summary: {len(paths)} file(s) checked, {skipped} skipped, "
f"{total_findings} finding(s)."
)
return 1 if total_findings else 0
if __name__ == "__main__":
sys.exit(main())
+4 -3
View File
@@ -1,6 +1,6 @@
---
name: diagram-design
description: Create technical and product diagrams — architecture, IT current-state, flowchart, sequence, state machine, ER / data model, timeline, swimlane, quadrant, radar / spider, nested, tree, org chart, layer stack, venn, pyramid / funnel, bar chart, line chart, Gantt, scatter plot, high-level, process, medallion, data flow, DP integration, DP security matrix — as standalone HTML files with inline SVG. Ships with a neutral editorial skin and a first-run gate that prompts users to customize the style guide (colors, fonts) from their own website before generating. Includes annotation-callout primitive and optional sketchy variant.
description: Create technical and product diagrams — architecture, IT current-state, flowchart, sequence, state machine, ER / data model, timeline, swimlane, quadrant, radar / spider, loop / flywheel, nested, tree, org chart, layer stack, venn, pyramid / funnel, bar chart, line chart, Gantt, scatter plot, high-level, process, medallion, data flow, DP integration, DP security matrix — as standalone HTML files with inline SVG. Ships with a neutral editorial skin and a first-run gate that prompts users to customize the style guide (colors, fonts) from their own website before generating. Includes annotation-callout primitive and optional sketchy variant.
license: MIT
metadata:
version: "2.0"
@@ -10,7 +10,7 @@ metadata:
Create visual diagrams as self-contained HTML files with inline SVG and CSS, following an opinionated editorial design system.
Twenty-six diagram types. One shared design system, complexity budget, and taste gate. Type-specific conventions live in `references/` and are loaded only when you pick a type.
Twenty-seven diagram types. One shared design system, complexity budget, and taste gate. Type-specific conventions live in `references/` and are loaded only when you pick a type.
---
@@ -55,7 +55,7 @@ Applied to schematics:
## 2. When to Use
Use for any of the 26 diagram types (§3) when a reader will learn more from a visual than from prose, a table, or a bulleted list.
Use for any of the 27 diagram types (§3) when a reader will learn more from a visual than from prose, a table, or a bulleted list.
**Don't use for:**
@@ -84,6 +84,7 @@ Before drawing, ask: *Would the reader learn more from this than from a well-wri
| Cross-functional process with handoffs | **Swimlane** | [type-swimlane.md](references/type-swimlane.md) |
| Two-axis positioning / prioritization | **Quadrant** | [type-quadrant.md](references/type-quadrant.md) |
| Multiple entities scored across 3–5 quantitative criteria | **Radar / Spider** | [type-radar.md](references/type-radar.md) |
| Reinforcing cycle / flywheel where the last step feeds the first and a shared hub accumulates state | **Loop** | [type-loop.md](references/type-loop.md) |
| Hierarchy through containment / scope | **Nested** | [type-nested.md](references/type-nested.md) |
| Parent → children relationships | **Tree** | [type-tree.md](references/type-tree.md) |
| Human/agent/team ownership, reporting, routing, escalation | **Org chart** | [type-org-chart.md](references/type-org-chart.md) |
@@ -85,7 +85,7 @@
<rect x="146" y="44" width="100" height="64" rx="6" fill="rgba(245,245,245,0.04)" stroke="rgba(245,245,245,0.20)"/>
<rect x="150" y="48" width="18" height="10" rx="3" fill="rgba(245,245,245,0.12)"/><text x="159" y="53" class="role-text">ENG</text>
<text x="196" y="67" class="node-title">Capture Events</text><text x="196" y="79" class="node-sub">shop events → batch</text><text x="196" y="91" class="node-tool">NiFi ingest</text>
<rect x="150" y="98" width="16" height="8" rx="3" fill="#4a7c59"/><text x="158" y="104" class="chip-text">LS</text>
<rect x="150" y="98" width="16" height="8" rx="3" fill="#7c8f6f"/><text x="158" y="104" class="chip-text">LS</text>
<rect x="226" y="98" width="16" height="8" rx="3" fill="#5e7a9b"/><text x="234" y="104" class="chip-text">DB</text>
<!-- Data Engineer: Land Records -->
@@ -126,7 +126,7 @@
<!-- Legend: data types -->
<text x="164" y="314" class="legend-label">DATA TYPE</text>
<rect x="180" y="306" width="16" height="8" rx="3" fill="#4a7c59"/><text x="188" y="312" class="chip-text">LS</text><text x="202" y="314" class="legend-text">Stream</text>
<rect x="180" y="306" width="16" height="8" rx="3" fill="#7c8f6f"/><text x="188" y="312" class="chip-text">LS</text><text x="202" y="314" class="legend-text">Stream</text>
<rect x="252" y="306" width="16" height="8" rx="3" fill="#5e7a9b"/><text x="260" y="312" class="chip-text">DB</text><text x="274" y="314" class="legend-text">Dataset</text>
<rect x="332" y="306" width="16" height="8" rx="3" fill="#b8915a"/><text x="340" y="312" class="chip-text">TB</text><text x="354" y="314" class="legend-text">Table</text>
<rect x="404" y="306" width="16" height="8" rx="3" fill="#9c6b50"/><text x="412" y="312" class="chip-text">FL</text><text x="426" y="314" class="legend-text">Dashboard</text>
@@ -105,7 +105,7 @@
<rect x="146" y="44" width="100" height="64" rx="6" fill="#f5f5f5" stroke="rgba(45,49,66,0.25)"/>
<rect x="150" y="48" width="18" height="10" rx="3" fill="rgba(45,49,66,0.12)"/><text x="159" y="53" class="role-text">ENG</text>
<text x="196" y="67" class="node-title">Capture Events</text><text x="196" y="79" class="node-sub">shop events → batch</text><text x="196" y="91" class="node-tool">NiFi ingest</text>
<rect x="150" y="98" width="16" height="8" rx="3" fill="#4a7c59"/><text x="158" y="104" class="chip-text">LS</text>
<rect x="150" y="98" width="16" height="8" rx="3" fill="#7c8f6f"/><text x="158" y="104" class="chip-text">LS</text>
<rect x="226" y="98" width="16" height="8" rx="3" fill="#5e7a9b"/><text x="234" y="104" class="chip-text">DB</text>
<!-- Data Engineer: Land Records -->
@@ -146,7 +146,7 @@
<!-- Legend: data types -->
<text x="164" y="314" class="legend-label">DATA TYPE</text>
<rect x="180" y="306" width="16" height="8" rx="3" fill="#4a7c59"/><text x="188" y="312" class="chip-text">LS</text><text x="202" y="314" class="legend-text">Stream</text>
<rect x="180" y="306" width="16" height="8" rx="3" fill="#7c8f6f"/><text x="188" y="312" class="chip-text">LS</text><text x="202" y="314" class="legend-text">Stream</text>
<rect x="252" y="306" width="16" height="8" rx="3" fill="#5e7a9b"/><text x="260" y="312" class="chip-text">DB</text><text x="274" y="314" class="legend-text">Dataset</text>
<rect x="332" y="306" width="16" height="8" rx="3" fill="#b8915a"/><text x="340" y="312" class="chip-text">TB</text><text x="354" y="314" class="legend-text">Table</text>
<rect x="404" y="306" width="16" height="8" rx="3" fill="#9c6b50"/><text x="412" y="312" class="chip-text">FL</text><text x="426" y="314" class="legend-text">Dashboard</text>
@@ -85,7 +85,7 @@
<rect x="146" y="44" width="100" height="64" rx="6" fill="#f5f5f5" stroke="rgba(45,49,66,0.25)"/>
<rect x="150" y="48" width="18" height="10" rx="3" fill="rgba(45,49,66,0.12)"/><text x="159" y="53" class="role-text">ENG</text>
<text x="196" y="67" class="node-title">Capture Events</text><text x="196" y="79" class="node-sub">shop events → batch</text><text x="196" y="91" class="node-tool">NiFi ingest</text>
<rect x="150" y="98" width="16" height="8" rx="3" fill="#4a7c59"/><text x="158" y="104" class="chip-text">LS</text>
<rect x="150" y="98" width="16" height="8" rx="3" fill="#7c8f6f"/><text x="158" y="104" class="chip-text">LS</text>
<rect x="226" y="98" width="16" height="8" rx="3" fill="#5e7a9b"/><text x="234" y="104" class="chip-text">DB</text>
<!-- Data Engineer: Land Records -->
@@ -126,7 +126,7 @@
<!-- Legend: data types -->
<text x="164" y="314" class="legend-label">DATA TYPE</text>
<rect x="180" y="306" width="16" height="8" rx="3" fill="#4a7c59"/><text x="188" y="312" class="chip-text">LS</text><text x="202" y="314" class="legend-text">Stream</text>
<rect x="180" y="306" width="16" height="8" rx="3" fill="#7c8f6f"/><text x="188" y="312" class="chip-text">LS</text><text x="202" y="314" class="legend-text">Stream</text>
<rect x="252" y="306" width="16" height="8" rx="3" fill="#5e7a9b"/><text x="260" y="312" class="chip-text">DB</text><text x="274" y="314" class="legend-text">Dataset</text>
<rect x="332" y="306" width="16" height="8" rx="3" fill="#b8915a"/><text x="340" y="312" class="chip-text">TB</text><text x="354" y="314" class="legend-text">Table</text>
<rect x="404" y="306" width="16" height="8" rx="3" fill="#9c6b50"/><text x="412" y="312" class="chip-text">FL</text><text x="426" y="314" class="legend-text">Dashboard</text>
@@ -7,7 +7,7 @@
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Geist:wght@400;500;600&amp;family=Geist+Mono:wght@400;500;600&amp;display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--paper:#f5f5f5;--paper-2:#ececec;--ink:#2d3142;--muted:#4f5d75;--soft:#7a8399;--accent:#eb6c36;--accent-tint:rgba(235,108,54,.08);--link:#2e5aa8;--rule:rgba(45,49,66,.12);--dot:rgba(45,49,66,.10);--side-fill:rgba(79,93,117,.06);--side-stroke:#7a8399;--zone-fill:rgba(45,49,66,.025);--zone-stroke:rgba(45,49,66,.32);--bar-fill:rgba(45,49,66,.05);--node-fill:#fff;--custom-red:#b85450;--custom-red-fill:rgba(184,84,80,.06);--custom-red-stroke:rgba(184,84,80,.35);--custom-blue:#5a7d9a;--custom-blue-fill:rgba(90,125,154,.06);--custom-blue-stroke:rgba(90,125,154,.35);--sans:'Geist',system-ui,sans-serif;--serif:'Instrument Serif',serif;--mono:'Geist Mono',ui-monospace,monospace}
:root{--paper:#f5f5f5;--paper-2:#ececec;--ink:#2d3142;--muted:#4f5d75;--soft:#7a8399;--accent:#eb6c36;--accent-tint:rgba(235,108,54,.08);--link:#2e5aa8;--rule:rgba(45,49,66,.12);--dot:rgba(45,49,66,.10);--side-fill:rgba(79,93,117,.06);--side-stroke:#7a8399;--zone-fill:rgba(45,49,66,.025);--zone-stroke:rgba(45,49,66,.32);--bar-fill:rgba(45,49,66,.05);--node-fill:#fff;--custom-red:#b88670;--custom-red-fill:rgba(184,134,112,.06);--custom-red-stroke:rgba(184,134,112,.35);--custom-blue:#82a0c0;--custom-blue-fill:rgba(130,160,192,.06);--custom-blue-stroke:rgba(130,160,192,.35);--sans:'Geist',system-ui,sans-serif;--serif:'Instrument Serif',serif;--mono:'Geist Mono',ui-monospace,monospace}
body{font-family:var(--sans);background:var(--paper);color:var(--ink);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:3rem 2rem}.frame{max-width:1200px;width:100%}.eyebrow{font:500 .66rem var(--mono);letter-spacing:.18em;text-transform:uppercase;color:var(--muted);margin-bottom:.5rem}h1{font:400 clamp(1.5rem,2.4vw + .75rem,2rem)/1.15 var(--serif);letter-spacing:-.02em;margin-bottom:1.5rem}svg{width:100%;min-width:1100px;display:block}svg text{font-family:var(--sans)}.mono{font-family:var(--mono)}.connector{fill:none;stroke:var(--muted);stroke-width:1.2}.connector.primary{stroke:var(--accent);stroke-width:1.4}.connector.trigger{stroke:var(--muted);stroke-width:1;stroke-dasharray:4 3}.connector.auth{stroke:var(--accent);stroke-width:1.2;stroke-dasharray:5 4}.label-mask{fill:var(--paper)}.edge-label{fill:var(--accent);font:8px var(--mono);letter-spacing:.06em}.zone{fill:var(--zone-fill);stroke:var(--zone-stroke)}.zone-mask,.node-mask{fill:var(--paper)}.zone-label{fill:var(--soft);font:500 8px var(--mono);letter-spacing:.18em}.side-node{fill:var(--side-fill);stroke:var(--side-stroke)}.core-node{fill:var(--accent-tint);stroke:var(--accent)}.bar{fill:var(--bar-fill);stroke:var(--zone-stroke)}.role-box{fill:transparent;stroke:var(--side-stroke);stroke-opacity:.5}.role-box.focal{stroke:var(--accent)}.role-text{fill:var(--side-stroke);font:500 7px var(--mono);letter-spacing:.08em}.role-text.focal{fill:var(--accent)}.node-name{fill:var(--ink);font:600 12px var(--sans)}.node-name.focal{fill:var(--ink)}.node-sub{fill:var(--muted);font:9px var(--mono)}.footer-red{fill:var(--custom-red-fill);stroke:var(--custom-red-stroke)}.footer-blue{fill:var(--custom-blue-fill);stroke:var(--custom-blue-stroke)}.footer-red-text{fill:var(--custom-red)}.footer-blue-text{fill:var(--custom-blue)}.legend-key{fill:var(--muted);font:500 8px var(--mono);letter-spacing:.14em}.legend-text{fill:var(--muted);font:8px var(--sans)}
</style>
</head>
@@ -7,7 +7,7 @@
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Geist:wght@400;500;600&amp;family=Geist+Mono:wght@400;500;600&amp;display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--paper:#f5f5f5;--paper-2:#ececec;--ink:#2d3142;--muted:#4f5d75;--soft:#7a8399;--accent:#eb6c36;--accent-tint:rgba(235,108,54,.08);--link:#2e5aa8;--rule:rgba(45,49,66,.12);--dot:rgba(45,49,66,.10);--side-fill:rgba(79,93,117,.06);--side-stroke:#7a8399;--zone-fill:rgba(45,49,66,.025);--zone-stroke:rgba(45,49,66,.32);--bar-fill:rgba(45,49,66,.05);--node-fill:#fff;--custom-red:#b85450;--custom-red-fill:rgba(184,84,80,.06);--custom-red-stroke:rgba(184,84,80,.35);--custom-blue:#5a7d9a;--custom-blue-fill:rgba(90,125,154,.06);--custom-blue-stroke:rgba(90,125,154,.35);--sans:'Geist',system-ui,sans-serif;--serif:'Instrument Serif',serif;--mono:'Geist Mono',ui-monospace,monospace}
:root{--paper:#f5f5f5;--paper-2:#ececec;--ink:#2d3142;--muted:#4f5d75;--soft:#7a8399;--accent:#eb6c36;--accent-tint:rgba(235,108,54,.08);--link:#2e5aa8;--rule:rgba(45,49,66,.12);--dot:rgba(45,49,66,.10);--side-fill:rgba(79,93,117,.06);--side-stroke:#7a8399;--zone-fill:rgba(45,49,66,.025);--zone-stroke:rgba(45,49,66,.32);--bar-fill:rgba(45,49,66,.05);--node-fill:#fff;--custom-red:#9c6b50;--custom-red-fill:rgba(156,107,80,.06);--custom-red-stroke:rgba(156,107,80,.35);--custom-blue:#5e7a9b;--custom-blue-fill:rgba(94,122,155,.06);--custom-blue-stroke:rgba(94,122,155,.35);--sans:'Geist',system-ui,sans-serif;--serif:'Instrument Serif',serif;--mono:'Geist Mono',ui-monospace,monospace}
body{font-family:var(--sans);background:var(--paper);color:var(--ink);min-height:100vh;padding:3rem 2rem}.frame{max-width:1200px;width:100%;margin:0 auto}.eyebrow{font:500 .66rem var(--mono);letter-spacing:.18em;text-transform:uppercase;color:var(--muted);margin-bottom:.5rem}h1{font:400 clamp(1.5rem,2.4vw + .75rem,2rem)/1.15 var(--serif);letter-spacing:-.02em;margin-bottom:1.5rem}svg{width:100%;min-width:1100px;display:block}svg text{font-family:var(--sans)}.mono{font-family:var(--mono)}.connector{fill:none;stroke:var(--muted);stroke-width:1.2}.connector.primary{stroke:var(--accent);stroke-width:1.4}.connector.trigger{stroke:var(--muted);stroke-width:1;stroke-dasharray:4 3}.connector.auth{stroke:var(--accent);stroke-width:1.2;stroke-dasharray:5 4}.label-mask{fill:var(--paper)}.edge-label{fill:var(--accent);font:8px var(--mono);letter-spacing:.06em}.zone{fill:var(--zone-fill);stroke:var(--zone-stroke)}.zone-mask,.node-mask{fill:var(--paper)}.zone-label{fill:var(--soft);font:500 8px var(--mono);letter-spacing:.18em}.side-node{fill:var(--side-fill);stroke:var(--side-stroke)}.core-node{fill:var(--accent-tint);stroke:var(--accent)}.bar{fill:var(--bar-fill);stroke:var(--zone-stroke)}.role-box{fill:transparent;stroke:var(--side-stroke);stroke-opacity:.5}.role-box.focal{stroke:var(--accent)}.role-text{fill:var(--side-stroke);font:500 7px var(--mono);letter-spacing:.08em}.role-text.focal{fill:var(--accent)}.node-name{fill:var(--ink);font:600 12px var(--sans)}.node-name.focal{fill:var(--ink)}.node-sub{fill:var(--muted);font:9px var(--mono)}.footer-red{fill:var(--custom-red-fill);stroke:var(--custom-red-stroke)}.footer-blue{fill:var(--custom-blue-fill);stroke:var(--custom-blue-stroke)}.footer-red-text{fill:var(--custom-red)}.footer-blue-text{fill:var(--custom-blue)}.legend-key{fill:var(--muted);font:500 8px var(--mono);letter-spacing:.14em}.legend-text{fill:var(--muted);font:8px var(--sans)}
.subtitle{max-width:62ch;color:var(--muted);font-size:1rem;line-height:1.55;margin:-.75rem 0 2rem}.diagram-container{background:var(--paper-2);border:1px solid var(--rule);border-radius:8px;padding:1.5rem;overflow-x:auto}.cards{display:grid;grid-template-columns:1.1fr 1fr .9fr;gap:1rem;margin-top:1.5rem}.card{background:#fff;border:1px solid var(--rule);border-radius:6px;padding:1.25rem}.card .card-label{font:500 .5rem var(--mono);letter-spacing:.18em;text-transform:uppercase;color:var(--muted);margin-bottom:.5rem}.card-header{display:flex;align-items:center;gap:.6rem;margin-bottom:.875rem;padding-bottom:.875rem;border-bottom:1px solid rgba(45,49,66,.08)}.card-dot{width:7px;height:7px;border-radius:50%;background:var(--muted)}.card-dot.coral{background:var(--accent)}.card-dot.ink{background:var(--ink)}.card h3{font-size:.875rem;font-weight:600}.card p,.card li{color:var(--muted);font-size:.8125rem;line-height:1.55}.card ul{list-style:none}.card li{padding-left:.875rem;position:relative}.card li::before{content:'—';position:absolute;left:0;color:rgba(45,49,66,.25)}.footer{display:flex;justify-content:space-between;gap:.5rem;flex-wrap:wrap;margin-top:2rem;padding-top:1.5rem;border-top:1px solid var(--rule);color:var(--soft);font:.72rem var(--mono);letter-spacing:.06em}@media(max-width:820px){.cards{grid-template-columns:1fr}}
</style>
@@ -7,7 +7,7 @@
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Geist:wght@400;500;600&amp;family=Geist+Mono:wght@400;500;600&amp;display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--paper:#f5f5f5;--paper-2:#ececec;--ink:#2d3142;--muted:#4f5d75;--soft:#7a8399;--accent:#eb6c36;--accent-tint:rgba(235,108,54,.08);--link:#2e5aa8;--rule:rgba(45,49,66,.12);--dot:rgba(45,49,66,.10);--side-fill:rgba(79,93,117,.06);--side-stroke:#7a8399;--zone-fill:rgba(45,49,66,.025);--zone-stroke:rgba(45,49,66,.32);--bar-fill:rgba(45,49,66,.05);--node-fill:#fff;--custom-red:#b85450;--custom-red-fill:rgba(184,84,80,.06);--custom-red-stroke:rgba(184,84,80,.35);--custom-blue:#5a7d9a;--custom-blue-fill:rgba(90,125,154,.06);--custom-blue-stroke:rgba(90,125,154,.35);--sans:'Geist',system-ui,sans-serif;--serif:'Instrument Serif',serif;--mono:'Geist Mono',ui-monospace,monospace}
:root{--paper:#f5f5f5;--paper-2:#ececec;--ink:#2d3142;--muted:#4f5d75;--soft:#7a8399;--accent:#eb6c36;--accent-tint:rgba(235,108,54,.08);--link:#2e5aa8;--rule:rgba(45,49,66,.12);--dot:rgba(45,49,66,.10);--side-fill:rgba(79,93,117,.06);--side-stroke:#7a8399;--zone-fill:rgba(45,49,66,.025);--zone-stroke:rgba(45,49,66,.32);--bar-fill:rgba(45,49,66,.05);--node-fill:#fff;--custom-red:#9c6b50;--custom-red-fill:rgba(156,107,80,.06);--custom-red-stroke:rgba(156,107,80,.35);--custom-blue:#5e7a9b;--custom-blue-fill:rgba(94,122,155,.06);--custom-blue-stroke:rgba(94,122,155,.35);--sans:'Geist',system-ui,sans-serif;--serif:'Instrument Serif',serif;--mono:'Geist Mono',ui-monospace,monospace}
body{font-family:var(--sans);background:var(--paper);color:var(--ink);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:3rem 2rem}.frame{max-width:1200px;width:100%}.eyebrow{font:500 .66rem var(--mono);letter-spacing:.18em;text-transform:uppercase;color:var(--muted);margin-bottom:.5rem}h1{font:400 clamp(1.5rem,2.4vw + .75rem,2rem)/1.15 var(--serif);letter-spacing:-.02em;margin-bottom:1.5rem}svg{width:100%;min-width:1100px;display:block}svg text{font-family:var(--sans)}.mono{font-family:var(--mono)}.connector{fill:none;stroke:var(--muted);stroke-width:1.2}.connector.primary{stroke:var(--accent);stroke-width:1.4}.connector.trigger{stroke:var(--muted);stroke-width:1;stroke-dasharray:4 3}.connector.auth{stroke:var(--accent);stroke-width:1.2;stroke-dasharray:5 4}.label-mask{fill:var(--paper)}.edge-label{fill:var(--accent);font:8px var(--mono);letter-spacing:.06em}.zone{fill:var(--zone-fill);stroke:var(--zone-stroke)}.zone-mask,.node-mask{fill:var(--paper)}.zone-label{fill:var(--soft);font:500 8px var(--mono);letter-spacing:.18em}.side-node{fill:var(--side-fill);stroke:var(--side-stroke)}.core-node{fill:var(--accent-tint);stroke:var(--accent)}.bar{fill:var(--bar-fill);stroke:var(--zone-stroke)}.role-box{fill:transparent;stroke:var(--side-stroke);stroke-opacity:.5}.role-box.focal{stroke:var(--accent)}.role-text{fill:var(--side-stroke);font:500 7px var(--mono);letter-spacing:.08em}.role-text.focal{fill:var(--accent)}.node-name{fill:var(--ink);font:600 12px var(--sans)}.node-name.focal{fill:var(--ink)}.node-sub{fill:var(--muted);font:9px var(--mono)}.footer-red{fill:var(--custom-red-fill);stroke:var(--custom-red-stroke)}.footer-blue{fill:var(--custom-blue-fill);stroke:var(--custom-blue-stroke)}.footer-red-text{fill:var(--custom-red)}.footer-blue-text{fill:var(--custom-blue)}.legend-key{fill:var(--muted);font:500 8px var(--mono);letter-spacing:.14em}.legend-text{fill:var(--muted);font:8px var(--sans)}
</style>
</head>
@@ -21,9 +21,9 @@
svg .node { fill:rgba(245,245,245,.04); stroke:rgba(245,245,245,.32); stroke-width:1; }
svg .node.external { stroke:#bfc0c0; stroke-dasharray:4 3; }
svg .node.focal { fill:rgba(240,138,89,.12); stroke:#f08a59; stroke-width:1.4; }
svg .node.survivor { fill:rgba(154,172,103,.10); stroke:rgba(154,172,103,.55); }
svg .node.survivor { fill:rgba(156,175,143,.10); stroke:rgba(156,175,143,.55); }
svg .node-name { fill:#f5f5f5; font:600 12px 'Geist',sans-serif; }
svg .node-name.survivor { fill:#9aac67; }
svg .node-name.survivor { fill:#9caf8f; }
svg .sub { fill:#bfc0c0; font:400 10px 'Geist Mono',monospace; } svg .sub.focal { fill:#f08a59; }
svg .arrow-label { font-size:8px; font-weight:600; letter-spacing:.08em; } svg .legend-label { fill:#bfc0c0; font-size:8px; }
</style>
@@ -64,7 +64,7 @@
<rect class="node-mask" x="36" y="256" width="216" height="56" rx="6"/><rect class="node external" x="36" y="256" width="216" height="56" rx="6"/><use href="#icon-file" x="48" y="272" width="24" height="24" color="#bfc0c0"/><text class="node-name" x="80" y="282">Supplier Price Lists</text><text class="sub" x="80" y="298">external · XLSX</text>
<rect class="node-mask" x="340" y="80" width="264" height="68" rx="6"/><rect class="node focal" x="340" y="80" width="264" height="68" rx="6"/><use href="#icon-file" x="352" y="100" width="24" height="24" color="#f08a59"/><text class="node-name" x="384" y="108">Shared Drive</text><text class="sub focal" x="384" y="124">no version control</text><text class="sub" x="384" y="140">department folders</text>
<rect class="node-mask" x="340" y="232" width="264" height="72" rx="6"/><rect class="node" x="340" y="232" width="264" height="72" rx="6"/><use href="#icon-screen" x="352" y="256" width="24" height="24" color="#f5f5f5"/><text class="node-name" x="384" y="262">Spreadsheet Handoffs</text><text class="sub" x="384" y="278">local workbooks · macros</text><text class="sub" x="384" y="292">manual reconciliation</text>
<rect class="node-mask" x="340" y="344" width="264" height="56" rx="6"/><rect class="node survivor" x="340" y="344" width="264" height="56" rx="6"/><use href="#icon-db" x="352" y="360" width="24" height="24" color="#9aac67"/><text class="node-name survivor" x="384" y="370">On-prem RDBMS</text><text class="sub" x="384" y="386">inventory · finance</text>
<rect class="node-mask" x="340" y="344" width="264" height="56" rx="6"/><rect class="node survivor" x="340" y="344" width="264" height="56" rx="6"/><use href="#icon-db" x="352" y="360" width="24" height="24" color="#9caf8f"/><text class="node-name survivor" x="384" y="370">On-prem RDBMS</text><text class="sub" x="384" y="386">inventory · finance</text>
<rect class="node-mask" x="704" y="80" width="208" height="60" rx="6"/><rect class="node focal" x="704" y="80" width="208" height="60" rx="6"/><use href="#icon-screen" x="716" y="100" width="24" height="24" color="#f08a59"/><text class="node-name" x="748" y="108">Reporting Portal</text><text class="sub focal" x="748" y="124">manual refresh</text>
<rect class="node-mask" x="704" y="216" width="208" height="56" rx="6"/><rect class="node" x="704" y="216" width="208" height="56" rx="6"/><use href="#icon-file" x="716" y="232" width="24" height="24" color="#f5f5f5"/><text class="node-name" x="748" y="242">Email Report Packs</text><text class="sub" x="748" y="258">weekly · PDF</text>
<rect class="node-mask" x="704" y="344" width="208" height="56" rx="6"/><rect class="node external" x="704" y="344" width="208" height="56" rx="6"/><use href="#icon-users" x="716" y="360" width="24" height="24" color="#bfc0c0"/><text class="node-name" x="748" y="370">Regional Managers</text><text class="sub" x="748" y="386">12 store regions</text>
@@ -18,8 +18,8 @@
svg { display:block; width:100%; min-width:920px; }
svg .zone { fill:rgba(45,49,66,.02); stroke:rgba(45,49,66,.10); stroke-width:.8; } svg .zone-mask,svg .node-mask,svg .label-mask { fill:#f5f5f5; }
svg .zone-label,svg .arrow-label,svg .legend-label { font-family:'Geist Mono',monospace; } svg .zone-label { fill:rgba(45,49,66,.40); font-size:8px; letter-spacing:.14em; }
svg .node { fill:#fff; stroke:#2d3142; stroke-width:1; } svg .node.external { stroke:#4f5d75; stroke-dasharray:4 3; } svg .node.focal { fill:rgba(235,108,54,.07); stroke:#eb6c36; stroke-width:1.4; } svg .node.survivor { fill:rgba(122,140,71,.06); stroke:rgba(122,140,71,.45); }
svg .node-name { fill:#2d3142; font:600 12px 'Geist',sans-serif; } svg .node-name.survivor { fill:#7a8c47; } svg .sub { fill:#4f5d75; font:400 10px 'Geist Mono',monospace; } svg .sub.focal { fill:#eb6c36; }
svg .node { fill:#fff; stroke:#2d3142; stroke-width:1; } svg .node.external { stroke:#4f5d75; stroke-dasharray:4 3; } svg .node.focal { fill:rgba(235,108,54,.07); stroke:#eb6c36; stroke-width:1.4; } svg .node.survivor { fill:rgba(124,143,111,.06); stroke:rgba(124,143,111,.45); }
svg .node-name { fill:#2d3142; font:600 12px 'Geist',sans-serif; } svg .node-name.survivor { fill:#7c8f6f; } svg .sub { fill:#4f5d75; font:400 10px 'Geist Mono',monospace; } svg .sub.focal { fill:#eb6c36; }
svg .arrow-label { font-size:8px; font-weight:600; letter-spacing:.08em; } svg .legend-label { fill:#4f5d75; font-size:8px; }
.cards { display:grid; grid-template-columns:1.1fr 1fr .9fr; gap:1rem; margin-top:1.5rem; } @media(max-width:820px){.cards{grid-template-columns:1fr;}}
.card { padding:1.25rem; background:#fff; border:1px solid var(--rule); border-radius:6px; } .card .eyebrow { margin-bottom:.5rem; font-size:.5rem; }
@@ -49,7 +49,7 @@
<rect class="node-mask" x="36" y="256" width="216" height="56" rx="6"/><rect class="node external" x="36" y="256" width="216" height="56" rx="6"/><use href="#icon-file" x="48" y="272" width="24" height="24" color="#4f5d75"/><text class="node-name" x="80" y="282">Supplier Price Lists</text><text class="sub" x="80" y="298">external · XLSX</text>
<rect class="node-mask" x="340" y="80" width="264" height="68" rx="6"/><rect class="node focal" x="340" y="80" width="264" height="68" rx="6"/><use href="#icon-file" x="352" y="100" width="24" height="24" color="#eb6c36"/><text class="node-name" x="384" y="108">Shared Drive</text><text class="sub focal" x="384" y="124">no version control</text><text class="sub" x="384" y="140">department folders</text>
<rect class="node-mask" x="340" y="232" width="264" height="72" rx="6"/><rect class="node" x="340" y="232" width="264" height="72" rx="6"/><use href="#icon-screen" x="352" y="256" width="24" height="24" color="#2d3142"/><text class="node-name" x="384" y="262">Spreadsheet Handoffs</text><text class="sub" x="384" y="278">local workbooks · macros</text><text class="sub" x="384" y="292">manual reconciliation</text>
<rect class="node-mask" x="340" y="344" width="264" height="56" rx="6"/><rect class="node survivor" x="340" y="344" width="264" height="56" rx="6"/><use href="#icon-db" x="352" y="360" width="24" height="24" color="#7a8c47"/><text class="node-name survivor" x="384" y="370">On-prem RDBMS</text><text class="sub" x="384" y="386">inventory · finance</text>
<rect class="node-mask" x="340" y="344" width="264" height="56" rx="6"/><rect class="node survivor" x="340" y="344" width="264" height="56" rx="6"/><use href="#icon-db" x="352" y="360" width="24" height="24" color="#7c8f6f"/><text class="node-name survivor" x="384" y="370">On-prem RDBMS</text><text class="sub" x="384" y="386">inventory · finance</text>
<rect class="node-mask" x="704" y="80" width="208" height="60" rx="6"/><rect class="node focal" x="704" y="80" width="208" height="60" rx="6"/><use href="#icon-screen" x="716" y="100" width="24" height="24" color="#eb6c36"/><text class="node-name" x="748" y="108">Reporting Portal</text><text class="sub focal" x="748" y="124">manual refresh</text>
<rect class="node-mask" x="704" y="216" width="208" height="56" rx="6"/><rect class="node" x="704" y="216" width="208" height="56" rx="6"/><use href="#icon-file" x="716" y="232" width="24" height="24" color="#2d3142"/><text class="node-name" x="748" y="242">Email Report Packs</text><text class="sub" x="748" y="258">weekly · PDF</text>
<rect class="node-mask" x="704" y="344" width="208" height="56" rx="6"/><rect class="node external" x="704" y="344" width="208" height="56" rx="6"/><use href="#icon-users" x="716" y="360" width="24" height="24" color="#4f5d75"/><text class="node-name" x="748" y="370">Regional Managers</text><text class="sub" x="748" y="386">12 store regions</text>
@@ -29,9 +29,9 @@
svg .node { fill: #fff; stroke: #2d3142; stroke-width: 1; }
svg .node.external { stroke: #4f5d75; stroke-dasharray: 4 3; }
svg .node.focal { fill: rgba(235,108,54,0.07); stroke: #eb6c36; stroke-width: 1.4; }
svg .node.survivor { fill: rgba(122,140,71,0.06); stroke: rgba(122,140,71,0.45); }
svg .node.survivor { fill: rgba(124,143,111,0.06); stroke: rgba(124,143,111,0.45); }
svg .node-name { fill: #2d3142; font: 600 12px 'Geist', sans-serif; }
svg .node-name.survivor { fill: #7a8c47; }
svg .node-name.survivor { fill: #7c8f6f; }
svg .sub { fill: #4f5d75; font: 400 10px 'Geist Mono', monospace; }
svg .sub.focal { fill: #eb6c36; }
svg .arrow-label { font-size: 8px; font-weight: 600; letter-spacing: 0.08em; }
@@ -94,7 +94,7 @@
<rect class="node-mask" x="340" y="232" width="264" height="72" rx="6"/><rect class="node" x="340" y="232" width="264" height="72" rx="6"/>
<use href="#icon-screen" x="352" y="256" width="24" height="24" color="#2d3142"/><text class="node-name" x="384" y="262">Spreadsheet Handoffs</text><text class="sub" x="384" y="278">local workbooks · macros</text><text class="sub" x="384" y="292">manual reconciliation</text>
<rect class="node-mask" x="340" y="344" width="264" height="56" rx="6"/><rect class="node survivor" x="340" y="344" width="264" height="56" rx="6"/>
<use href="#icon-db" x="352" y="360" width="24" height="24" color="#7a8c47"/><text class="node-name survivor" x="384" y="370">On-prem RDBMS</text><text class="sub" x="384" y="386">inventory · finance</text>
<use href="#icon-db" x="352" y="360" width="24" height="24" color="#7c8f6f"/><text class="node-name survivor" x="384" y="370">On-prem RDBMS</text><text class="sub" x="384" y="386">inventory · finance</text>
<!-- Dissemination -->
<rect class="node-mask" x="704" y="80" width="208" height="60" rx="6"/><rect class="node focal" x="704" y="80" width="208" height="60" rx="6"/>
@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>The self-improving loop · Dark</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Geist:wght@400;500;600&amp;family=Geist+Mono:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--paper: #2d3142; --ink: #f5f5f5; --muted: #bfc0c0; --soft: #8e98ac;
--accent: #f08a59; --accent-tint: rgba(240,138,89,0.10); --link: #6a95d8;
--sans: 'Geist', system-ui, sans-serif; --serif: 'Instrument Serif', serif;
--mono: 'Geist Mono', ui-monospace, monospace;
}
body { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 3rem 2rem; background: var(--paper); color: var(--ink); font-family: var(--sans); }
.frame { width: 100%; max-width: 1200px; }
.eyebrow { margin-bottom: 0.5rem; color: var(--muted); font: 500 0.66rem var(--mono); letter-spacing: 0.18em; text-transform: uppercase; }
h1 { margin-bottom: 1.5rem; color: var(--ink); font: 400 clamp(1.5rem, 2.4vw + 0.75rem, 2rem)/1.15 var(--serif); letter-spacing: -0.02em; }
svg { display: block; width: 100%; min-width: 900px; }
.ring { fill: none; stroke: var(--muted); stroke-width: 1.2; }
.spoke { fill: none; stroke: var(--soft); stroke-width: 1; stroke-dasharray: 5 4; }
.station { fill: var(--paper); stroke: var(--ink); stroke-width: 1; }
.station.focal { fill: var(--accent-tint); stroke: var(--accent); stroke-width: 1.2; }
.hub { fill: var(--ink); }
.node-name { fill: var(--ink); font: 600 12px var(--sans); text-anchor: middle; }
.focal-name { fill: var(--accent); }
.sublabel { fill: var(--soft); font: 400 8px var(--mono); text-anchor: middle; }
.hub-name { fill: var(--paper); font: 600 16px var(--sans); text-anchor: middle; }
.hub-sub { fill: var(--paper); opacity: 0.72; font: 400 8px var(--mono); text-anchor: middle; }
.arrow-label { fill: var(--soft); font: 400 8px var(--mono); letter-spacing: 0.06em; text-anchor: middle; }
</style>
</head>
<body>
<main class="frame">
<p class="eyebrow">Loop · Diagram Design</p>
<h1>The self-improving loop</h1>
<svg viewBox="0 0 1040 680" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="loop-title loop-desc">
<title id="loop-title">The self-improving loop</title>
<desc id="loop-desc">Six stations flow clockwise from Capture through Learn and back to Capture. Each station writes shared state into one central memory hub, with Decide highlighted as the human approval gate.</desc>
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse"><circle cx="1" cy="1" r="0.9" fill="rgba(245,245,245,0.10)"/></pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#bfc0c0"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f08a59"/></marker>
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#6a95d8"/></marker>
<marker id="arrow-soft" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#8e98ac"/></marker>
</defs>
<rect width="1040" height="680" fill="#2d3142"/>
<rect width="1040" height="680" fill="url(#dots)" opacity="0.55"/>
<!-- Solid clockwise ring: six arcs on the same r=240 station circle. -->
<path class="ring" d="M600 113.726 A240 240 0 0 1 704.969 187.073" marker-end="url(#arrow)"/>
<path class="ring" d="M743.285 252 A240 240 0 0 1 743.722 426.882" marker-end="url(#arrow)"/>
<path class="ring" d="M705.731 492 A240 240 0 0 1 601.130 565.871" marker-end="url(#arrow)"/>
<path class="ring" d="M440 566.274 A240 240 0 0 1 335.031 492.927" marker-end="url(#arrow)"/>
<path class="ring" d="M296.715 428 A240 240 0 0 1 296.278 253.118" marker-end="url(#arrow)"/>
<path class="ring" d="M334.269 188 A240 240 0 0 1 438.870 114.129" marker-end="url(#arrow)"/>
<!-- Dashed write-backs: station inner edge to 6px before the hub stroke. -->
<path class="spoke" d="M520 132 V280" marker-end="url(#arrow-soft)"/>
<path class="spoke" d="M672 252 L616 284" marker-end="url(#arrow-soft)"/>
<path class="spoke" d="M672 428 L616 396" marker-end="url(#arrow-soft)"/>
<path class="spoke" d="M520 548 V400" marker-end="url(#arrow-soft)"/>
<path class="spoke" d="M368 428 L424 396" marker-end="url(#arrow-soft)"/>
<path class="spoke" d="M368 252 L424 284" marker-end="url(#arrow-soft)"/>
<!-- Curated spoke labels sit beside the line with an 8px visible gap. -->
<rect x="532" y="200" width="48" height="16" rx="4" fill="#2d3142"/>
<text x="556" y="212" class="arrow-label">SIGNALS</text>
<rect x="460" y="464" width="52" height="16" rx="4" fill="#2d3142"/>
<text x="486" y="476" class="arrow-label">OUTCOMES</text>
<!-- Stations -->
<rect class="station" x="440" y="68" width="160" height="64" rx="6"/>
<text x="520" y="96" class="node-name">Capture</text><text x="520" y="116" class="sublabel">signals in / intake</text>
<rect class="station" x="648" y="188" width="160" height="64" rx="6"/>
<text x="728" y="216" class="node-name">Research</text><text x="728" y="236" class="sublabel">evidence pulled</text>
<rect class="station focal" x="648" y="428" width="160" height="64" rx="6"/>
<text x="728" y="456" class="node-name focal-name">Decide</text><text x="728" y="476" class="sublabel">human approves</text>
<rect class="station" x="440" y="548" width="160" height="64" rx="6"/>
<text x="520" y="576" class="node-name">Act</text><text x="520" y="596" class="sublabel">work ships</text>
<rect class="station" x="232" y="428" width="160" height="64" rx="6"/>
<text x="312" y="456" class="node-name">Measure</text><text x="312" y="476" class="sublabel">outcomes logged</text>
<rect class="station" x="232" y="188" width="160" height="64" rx="6"/>
<text x="312" y="216" class="node-name">Learn</text><text x="312" y="236" class="sublabel">playbook updated</text>
<!-- The one shared-state hub. -->
<rect class="hub" x="420" y="288" width="200" height="104" rx="8"/>
<text x="520" y="336" class="hub-name">Shared memory</text>
<text x="520" y="360" class="hub-sub">one record, every loop</text>
</svg>
</main>
</body>
</html>
@@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>The self-improving loop · Full</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Geist:wght@400;500;600&amp;family=Geist+Mono:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--paper: #f5f5f5; --paper-2: #ececec; --ink: #2d3142; --muted: #4f5d75;
--soft: #7a8399; --rule: rgba(45,49,66,0.12); --accent: #eb6c36;
--accent-tint: rgba(235,108,54,0.08); --link: #2e5aa8;
--sans: 'Geist', system-ui, sans-serif; --serif: 'Instrument Serif', serif;
--mono: 'Geist Mono', ui-monospace, monospace;
}
body { min-height: 100vh; padding: 3rem 2rem; background: var(--paper); color: var(--ink); font-family: var(--sans); }
.container { width: 100%; max-width: 1200px; margin: 0 auto; }
.header { margin-bottom: 2.5rem; }
.eyebrow { margin-bottom: 0.5rem; color: var(--muted); font: 500 0.66rem var(--mono); letter-spacing: 0.18em; text-transform: uppercase; }
h1 { margin-bottom: 0.5rem; color: var(--ink); font: 400 clamp(1.5rem, 2.4vw + 0.75rem, 2rem)/1.15 var(--serif); letter-spacing: -0.02em; }
.subtitle { max-width: 62ch; color: var(--muted); font-size: 1rem; line-height: 1.55; }
.diagram-container { overflow-x: auto; padding: 1.5rem; background: var(--paper-2); border: 1px solid var(--rule); border-radius: 8px; }
svg { display: block; width: 100%; min-width: 1040px; }
.ring { fill: none; stroke: var(--muted); stroke-width: 1.2; }
.spoke { fill: none; stroke: var(--soft); stroke-width: 1; stroke-dasharray: 5 4; }
.station { fill: var(--paper); stroke: var(--ink); stroke-width: 1; }
.station.focal { fill: var(--accent-tint); stroke: var(--accent); stroke-width: 1.2; }
.hub { fill: var(--ink); }
.node-name { fill: var(--ink); font: 600 12px var(--sans); text-anchor: middle; }
.focal-name { fill: var(--accent); }
.sublabel { fill: var(--soft); font: 400 8px var(--mono); text-anchor: middle; }
.hub-name { fill: var(--paper); font: 600 16px var(--sans); text-anchor: middle; }
.hub-sub { fill: var(--paper); opacity: 0.72; font: 400 8px var(--mono); text-anchor: middle; }
.arrow-label { fill: var(--soft); font: 400 8px var(--mono); letter-spacing: 0.06em; text-anchor: middle; }
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
.card { padding: 1.25rem; background: #fff; border: 1px solid var(--rule); border-radius: 6px; }
.card-eyebrow { margin-bottom: 0.5rem; color: var(--muted); font: 500 0.5rem var(--mono); letter-spacing: 0.18em; text-transform: uppercase; }
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(45,49,66,0.08); }
.card-dot { width: 7px; height: 7px; flex: 0 0 auto; border-radius: 50%; }
.card-dot.coral { background: var(--accent); } .card-dot.ink { background: var(--ink); } .card-dot.muted { background: var(--muted); }
.card h2 { font-size: 0.875rem; font-weight: 600; }
.card p, .card ul { color: var(--muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
.card li { position: relative; margin-bottom: 0.3rem; padding-left: 0.875rem; }
.card li::before { position: absolute; left: 0; color: rgba(45,49,66,0.25); content: '—'; }
.footer { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.5rem; margin-top: 2rem; padding-top: 1.5rem; color: var(--soft); border-top: 1px solid rgba(45,49,66,0.10); font: 400 0.72rem var(--mono); letter-spacing: 0.06em; }
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<main class="container">
<header class="header">
<p class="eyebrow">Loop · Diagram Design</p>
<h1>The self-improving loop</h1>
<p class="subtitle">A company operating rhythm where work moves clockwise, every pass writes back to one shared record, and a human stays accountable for the decision.</p>
</header>
<div class="diagram-container">
<svg viewBox="0 0 1040 680" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="loop-title loop-desc">
<title id="loop-title">The self-improving loop</title>
<desc id="loop-desc">Six stations flow clockwise from Capture through Learn and back to Capture. Each station writes shared state into one central memory hub, with Decide highlighted as the human approval gate.</desc>
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse"><circle cx="1" cy="1" r="0.9" fill="rgba(45,49,66,0.10)"/></pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#4f5d75"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#eb6c36"/></marker>
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#2e5aa8"/></marker>
<marker id="arrow-soft" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#7a8399"/></marker>
</defs>
<rect width="1040" height="680" fill="#f5f5f5"/>
<rect width="1040" height="680" fill="url(#dots)" opacity="0.55"/>
<!-- Solid clockwise ring: six arcs on the same r=240 station circle. -->
<path class="ring" d="M600 113.726 A240 240 0 0 1 704.969 187.073" marker-end="url(#arrow)"/>
<path class="ring" d="M743.285 252 A240 240 0 0 1 743.722 426.882" marker-end="url(#arrow)"/>
<path class="ring" d="M705.731 492 A240 240 0 0 1 601.130 565.871" marker-end="url(#arrow)"/>
<path class="ring" d="M440 566.274 A240 240 0 0 1 335.031 492.927" marker-end="url(#arrow)"/>
<path class="ring" d="M296.715 428 A240 240 0 0 1 296.278 253.118" marker-end="url(#arrow)"/>
<path class="ring" d="M334.269 188 A240 240 0 0 1 438.870 114.129" marker-end="url(#arrow)"/>
<!-- Dashed write-backs: station inner edge to 6px before the hub stroke. -->
<path class="spoke" d="M520 132 V280" marker-end="url(#arrow-soft)"/>
<path class="spoke" d="M672 252 L616 284" marker-end="url(#arrow-soft)"/>
<path class="spoke" d="M672 428 L616 396" marker-end="url(#arrow-soft)"/>
<path class="spoke" d="M520 548 V400" marker-end="url(#arrow-soft)"/>
<path class="spoke" d="M368 428 L424 396" marker-end="url(#arrow-soft)"/>
<path class="spoke" d="M368 252 L424 284" marker-end="url(#arrow-soft)"/>
<!-- Curated spoke labels sit beside the line with an 8px visible gap. -->
<rect x="532" y="200" width="48" height="16" rx="4" fill="#f5f5f5"/>
<text x="556" y="212" class="arrow-label">SIGNALS</text>
<rect x="460" y="464" width="52" height="16" rx="4" fill="#f5f5f5"/>
<text x="486" y="476" class="arrow-label">OUTCOMES</text>
<!-- Stations -->
<rect class="station" x="440" y="68" width="160" height="64" rx="6"/>
<text x="520" y="96" class="node-name">Capture</text><text x="520" y="116" class="sublabel">signals in / intake</text>
<rect class="station" x="648" y="188" width="160" height="64" rx="6"/>
<text x="728" y="216" class="node-name">Research</text><text x="728" y="236" class="sublabel">evidence pulled</text>
<rect class="station focal" x="648" y="428" width="160" height="64" rx="6"/>
<text x="728" y="456" class="node-name focal-name">Decide</text><text x="728" y="476" class="sublabel">human approves</text>
<rect class="station" x="440" y="548" width="160" height="64" rx="6"/>
<text x="520" y="576" class="node-name">Act</text><text x="520" y="596" class="sublabel">work ships</text>
<rect class="station" x="232" y="428" width="160" height="64" rx="6"/>
<text x="312" y="456" class="node-name">Measure</text><text x="312" y="476" class="sublabel">outcomes logged</text>
<rect class="station" x="232" y="188" width="160" height="64" rx="6"/>
<text x="312" y="216" class="node-name">Learn</text><text x="312" y="236" class="sublabel">playbook updated</text>
<!-- The one shared-state hub. -->
<rect class="hub" x="420" y="288" width="200" height="104" rx="8"/>
<text x="520" y="336" class="hub-name">Shared memory</text>
<text x="520" y="360" class="hub-sub">one record, every loop</text>
</svg>
</div>
<section class="cards" aria-label="Loop summary">
<article class="card">
<p class="card-eyebrow">THE LOOP SIGNAL</p>
<div class="card-header"><span class="card-dot coral"></span><h2>The dashed lines are the product</h2></div>
<p>Shipping work matters once. Writing the result back makes the next pass faster, safer, and better informed.</p>
</article>
<article class="card">
<div class="card-header"><span class="card-dot ink"></span><h2>Memory compounds</h2></div>
<ul><li>Evidence survives the handoff</li><li>Outcomes update the playbook</li><li>One record stays shared</li></ul>
</article>
<article class="card">
<div class="card-header"><span class="card-dot muted"></span><h2>One human gate</h2></div>
<p>Decide is the only focal station because accountability should remain visible inside an improving system.</p>
</article>
</section>
<footer class="footer">
<span>company operations · reinforcing loop</span>
<span>example · schematic skill</span>
</footer>
</main>
</body>
</html>
@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>The self-improving loop</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Geist:wght@400;500;600&amp;family=Geist+Mono:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--paper: #f5f5f5; --ink: #2d3142; --muted: #4f5d75; --soft: #7a8399;
--accent: #eb6c36; --accent-tint: rgba(235,108,54,0.08); --link: #2e5aa8;
--sans: 'Geist', system-ui, sans-serif; --serif: 'Instrument Serif', serif;
--mono: 'Geist Mono', ui-monospace, monospace;
}
body { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 3rem 2rem; background: var(--paper); color: var(--ink); font-family: var(--sans); }
.frame { width: 100%; max-width: 1200px; }
.eyebrow { margin-bottom: 0.5rem; color: var(--muted); font: 500 0.66rem var(--mono); letter-spacing: 0.18em; text-transform: uppercase; }
h1 { margin-bottom: 1.5rem; color: var(--ink); font: 400 clamp(1.5rem, 2.4vw + 0.75rem, 2rem)/1.15 var(--serif); letter-spacing: -0.02em; }
svg { display: block; width: 100%; min-width: 900px; }
.ring { fill: none; stroke: var(--muted); stroke-width: 1.2; }
.spoke { fill: none; stroke: var(--soft); stroke-width: 1; stroke-dasharray: 5 4; }
.station { fill: var(--paper); stroke: var(--ink); stroke-width: 1; }
.station.focal { fill: var(--accent-tint); stroke: var(--accent); stroke-width: 1.2; }
.hub { fill: var(--ink); }
.node-name { fill: var(--ink); font: 600 12px var(--sans); text-anchor: middle; }
.focal-name { fill: var(--accent); }
.sublabel { fill: var(--soft); font: 400 8px var(--mono); text-anchor: middle; }
.hub-name { fill: var(--paper); font: 600 16px var(--sans); text-anchor: middle; }
.hub-sub { fill: var(--paper); opacity: 0.72; font: 400 8px var(--mono); text-anchor: middle; }
.arrow-label { fill: var(--soft); font: 400 8px var(--mono); letter-spacing: 0.06em; text-anchor: middle; }
</style>
</head>
<body>
<main class="frame">
<p class="eyebrow">Loop · Diagram Design</p>
<h1>The self-improving loop</h1>
<svg viewBox="0 0 1040 680" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="loop-title loop-desc">
<title id="loop-title">The self-improving loop</title>
<desc id="loop-desc">Six stations flow clockwise from Capture through Learn and back to Capture. Each station writes shared state into one central memory hub, with Decide highlighted as the human approval gate.</desc>
<defs>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse"><circle cx="1" cy="1" r="0.9" fill="rgba(45,49,66,0.10)"/></pattern>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#4f5d75"/></marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#eb6c36"/></marker>
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#2e5aa8"/></marker>
<marker id="arrow-soft" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#7a8399"/></marker>
</defs>
<rect width="1040" height="680" fill="#f5f5f5"/>
<rect width="1040" height="680" fill="url(#dots)" opacity="0.55"/>
<!-- Solid clockwise ring: six arcs on the same r=240 station circle. -->
<path class="ring" d="M600 113.726 A240 240 0 0 1 704.969 187.073" marker-end="url(#arrow)"/>
<path class="ring" d="M743.285 252 A240 240 0 0 1 743.722 426.882" marker-end="url(#arrow)"/>
<path class="ring" d="M705.731 492 A240 240 0 0 1 601.130 565.871" marker-end="url(#arrow)"/>
<path class="ring" d="M440 566.274 A240 240 0 0 1 335.031 492.927" marker-end="url(#arrow)"/>
<path class="ring" d="M296.715 428 A240 240 0 0 1 296.278 253.118" marker-end="url(#arrow)"/>
<path class="ring" d="M334.269 188 A240 240 0 0 1 438.870 114.129" marker-end="url(#arrow)"/>
<!-- Dashed write-backs: station inner edge to 6px before the hub stroke. -->
<path class="spoke" d="M520 132 V280" marker-end="url(#arrow-soft)"/>
<path class="spoke" d="M672 252 L616 284" marker-end="url(#arrow-soft)"/>
<path class="spoke" d="M672 428 L616 396" marker-end="url(#arrow-soft)"/>
<path class="spoke" d="M520 548 V400" marker-end="url(#arrow-soft)"/>
<path class="spoke" d="M368 428 L424 396" marker-end="url(#arrow-soft)"/>
<path class="spoke" d="M368 252 L424 284" marker-end="url(#arrow-soft)"/>
<!-- Curated spoke labels sit beside the line with an 8px visible gap. -->
<rect x="532" y="200" width="48" height="16" rx="4" fill="#f5f5f5"/>
<text x="556" y="212" class="arrow-label">SIGNALS</text>
<rect x="460" y="464" width="52" height="16" rx="4" fill="#f5f5f5"/>
<text x="486" y="476" class="arrow-label">OUTCOMES</text>
<!-- Stations -->
<rect class="station" x="440" y="68" width="160" height="64" rx="6"/>
<text x="520" y="96" class="node-name">Capture</text><text x="520" y="116" class="sublabel">signals in / intake</text>
<rect class="station" x="648" y="188" width="160" height="64" rx="6"/>
<text x="728" y="216" class="node-name">Research</text><text x="728" y="236" class="sublabel">evidence pulled</text>
<rect class="station focal" x="648" y="428" width="160" height="64" rx="6"/>
<text x="728" y="456" class="node-name focal-name">Decide</text><text x="728" y="476" class="sublabel">human approves</text>
<rect class="station" x="440" y="548" width="160" height="64" rx="6"/>
<text x="520" y="576" class="node-name">Act</text><text x="520" y="596" class="sublabel">work ships</text>
<rect class="station" x="232" y="428" width="160" height="64" rx="6"/>
<text x="312" y="456" class="node-name">Measure</text><text x="312" y="476" class="sublabel">outcomes logged</text>
<rect class="station" x="232" y="188" width="160" height="64" rx="6"/>
<text x="312" y="216" class="node-name">Learn</text><text x="312" y="236" class="sublabel">playbook updated</text>
<!-- The one shared-state hub. -->
<rect class="hub" x="420" y="288" width="200" height="104" rx="8"/>
<text x="520" y="336" class="hub-name">Shared memory</text>
<text x="520" y="360" class="hub-sub">one record, every loop</text>
</svg>
</main>
</body>
</html>
+16 -13
View File
@@ -236,44 +236,47 @@
<button class="tab new" data-type="radar">
<span class="eyebrow">15</span>Radar
</button>
<button class="tab new" data-type="loop">
<span class="eyebrow">16</span>Loop
</button>
<button class="tab new" data-type="datalake">
<span class="eyebrow">16</span>Data lake
<span class="eyebrow">17</span>Data lake
</button>
<button class="tab new" data-type="bar">
<span class="eyebrow">17</span>Bar
<span class="eyebrow">18</span>Bar
</button>
<button class="tab new" data-type="line">
<span class="eyebrow">18</span>Line
<span class="eyebrow">19</span>Line
</button>
<button class="tab new" data-type="gantt">
<span class="eyebrow">19</span>Gantt
<span class="eyebrow">20</span>Gantt
</button>
<button class="tab new" data-type="scatter">
<span class="eyebrow">20</span>Scatter
<span class="eyebrow">21</span>Scatter
</button>
<button class="tab new" data-type="medallion">
<span class="eyebrow">21</span>Medallion
<span class="eyebrow">22</span>Medallion
</button>
<button class="tab new" data-type="high-level">
<span class="eyebrow">22</span>High-Level
<span class="eyebrow">23</span>High-Level
</button>
<button class="tab new" data-type="process">
<span class="eyebrow">23</span>Process
<span class="eyebrow">24</span>Process
</button>
<button class="tab new" data-type="data-flow">
<span class="eyebrow">25</span>Data flow
<span class="eyebrow">26</span>Data flow
</button>
<button class="tab new" data-type="dp-integration">
<span class="eyebrow">26</span>DP integration
<span class="eyebrow">27</span>DP integration
</button>
<button class="tab new" data-type="high-level-vertical">
<span class="eyebrow">27</span>High-Level / Parametric
<span class="eyebrow">28</span>High-Level / Parametric
</button>
<button class="tab new" data-type="dp-security-matrix">
<span class="eyebrow">31</span>DP security matrix
<span class="eyebrow">32</span>DP security matrix
</button>
<button class="tab new" data-type="it-state">
<span class="eyebrow">32</span>IT current-state
<span class="eyebrow">33</span>IT current-state
</button>
</div>
@@ -0,0 +1,223 @@
# Loop
**Best for:** reinforcing cycles, flywheels, feedback loops, and operating loops — anything where the last step feeds the first and a shared hub accumulates state. Use Loop when the reader must see both motions at once: work advances clockwise around the ring, while each pass writes durable state back to one common center.
Prefer **Flowchart** when the path ends, branches toward an outcome, or never truly returns to its first step. Prefer **Cycle** when the center does not accumulate shared state. The dashed write-back spokes are the defining signal here: remove them and the figure is only a circular process.
This type is **parametric**. The inputs in §1 determine station count, angles, edge intersections, connector paths, and viewBox bounds. Identical inputs should produce identical geometry.
---
## 1. Inputs — the parameter contract
```yaml
title: "The self-improving loop"
subtitle: "Every pass improves the shared operating record"
hub: # exactly one
name: "Shared memory"
sublabel: "one record, every loop"
stations: # 5..8, clockwise from top
- { name: "Capture", sublabel: "signals in", spoke_label: "SIGNALS" }
- { name: "Research", sublabel: "evidence pulled" }
- { name: "Decide", sublabel: "human approves", focal: true }
- { name: "Act", sublabel: "work ships", spoke_label: "OUTCOMES" }
- { name: "Measure", sublabel: "outcomes logged" }
- { name: "Learn", sublabel: "playbook updated" }
station_w: 160
station_h: 64
hub_w: 200
hub_h: 104
radius: 240
margin: 64
dark: false
```
**Budget (hard):** **5–8 stations plus exactly one hub.** Above 8 stations, split the subject into an overview Loop and one or more detail diagrams. Exactly one hub — a loop with two hubs is two diagrams. At most one station may set `focal: true`; zero is allowed when no editorial gate deserves emphasis.
Station order is semantic. `stations[0]` is the top station, then entries proceed clockwise. The last station always connects back to station 0; if that return would be false, use a Flowchart instead.
---
## 2. Layout math — deterministic geometry
Use SVG coordinates, where positive y points downward. Let the hub center be `C = (cx, cy)`, station count be `N`, station ring radius be `R`, station half-size be `a = station_w/2`, `b = station_h/2`, and hub half-size be `A = hub_w/2`, `B = hub_h/2`.
### 2.1 Station centers
For zero-indexed station `k`:
```text
theta_k = -90deg + k * (360deg / N)
u_k = (cos(theta_k), sin(theta_k))
P_k = C + R * u_k
station_center_x(k) = cx + R * cos(theta_k)
station_center_y(k) = cy + R * sin(theta_k)
station_x(k) = station_center_x(k) - station_w/2
station_y(k) = station_center_y(k) - station_h/2
```
Thus station 1 sits at the top, and increasing `k` moves clockwise. Round station rectangles to the nearest 4px grid point after computing the ideal geometry; preserve symmetry when rounding paired stations. Keep ring-circle intersection points to three decimal places so every arc remains on the same circle.
### 2.2 Solid ring-flow endpoints
Ring connectors travel from station `k` to station `j = (k + 1) mod N` as circular SVG arcs on the station circle itself. Every segment uses the same center `C`, radius `R`, and clockwise sweep. The station boxes interrupt the circle; connectors begin at the circle's clockwise exit from the source box and end just before its counterclockwise entry into the destination box, so the marker tip lands on the destination stroke.
Find the circle/rectangle intersections against all four edges of station `k`. For vertical edge `x = x_e`:
```text
y = cy +/- sqrt(R^2 - (x_e - cx)^2)
```
Keep only candidates whose `y` lies within the edge. For horizontal edge `y = y_e`:
```text
x = cx +/- sqrt(R^2 - (y_e - cy)^2)
```
Keep only candidates whose `x` lies within the edge. The two surviving points are classified by their normalized polar angles around `C`:
```text
q_entry(k) = circle/box intersection immediately before theta_k clockwise
q_exit(k) = circle/box intersection immediately after theta_k clockwise
```
Compensate for the marker tip before emitting the destination endpoint. With the canonical marker (`refX=7`, polygon tip at `x=8`) and ring stroke width `1.2`, `marker_overhang = 1.2`:
```text
phi_entry = atan2(q_entry(j).y - cy, q_entry(j).x - cx)
phi_end = phi_entry - marker_overhang / R
q_end = C + R * (cos(phi_end), sin(phi_end))
M q_exit(k).x q_exit(k).y
A R R 0 0 1 q_end.x q_end.y
```
The large-arc flag is `0` because adjacent-station gaps are less than 180 degrees; the sweep flag is always `1` for clockwise motion in SVG coordinates. The arrowhead overhang completes the final 1.2px to `q_entry(j)`, landing on the box edge without crossing its stroke. The closing connector from station `N-1` to station 0 uses the identical formula.
Loop's circular ring arcs are a documented type-specific exception to SKILL.md §6 rule 1, following the same precedent as Medallion's promotion arcs. A Loop never mixes cubic, straight, or rounded-orthogonal segments into its ring: the six visible gaps must read as pieces of one continuous circle.
### 2.3 Dashed write-back spoke endpoints
Each spoke runs inward from the station edge toward the hub. Use the same ray/box intersection formula, now on the radial vector `u_k`:
```text
box_distance(v, half_w, half_h) = min(half_w / abs(v.x), half_h / abs(v.y))
# ignore a term whose denominator is zero
d_station = box_distance(u_k, a, b)
d_hub = box_distance(u_k, A, B)
marker_gap = 6 # 4..8px; 6px canonical
spoke_start(k) = P_k - d_station * u_k
hub_edge(k) = C + d_hub * u_k
spoke_end(k) = C + (d_hub + marker_gap) * u_k
```
Because the arrow travels from the station toward `C`, adding `marker_gap` leaves the endpoint just outside the hub boundary. The lighter arrowhead stops before the hub stroke instead of colliding with it. Radial spokes are the type-specific exception to the general ban on slanted straight connectors; they must remain true radii, must not cross one another, and may touch only their source station and the hub.
Labels are optional when the station sublabel already names the write-back. When used, they follow the `arrow-label` role, stay to one side of the spoke, and receive an opaque `paper` mask with a visible 6–10px gap from the stroke. Label a curated subset rather than forcing six labels into the hub halo.
### 2.4 ViewBox sizing
The viewBox must include the full station rectangles, outer ring curves, arrowheads, and at least `margin` breathing room:
```text
left <= cx - R - station_w/2 - margin
right >= cx + R + station_w/2 + margin
top <= cy - R - station_h/2 - margin
bottom >= cy + R + station_h/2 + margin
viewBox_w = right - left
viewBox_h = bottom - top
```
Include the full circle extrema `cx +/- R`, `cy +/- R` plus station bounds and marker clearance when checking these limits. Never shrink the canvas until a station stroke, marker, or ring arc clips. For the six-station canonical example, `viewBox="0 0 1040 680"`, `C=(520,340)`, `R=240`, station size `160×64`, and hub size `200×104` leave generous outer clearance.
---
## 3. Visual grammar
| Element | Treatment |
|---|---|
| Station | Standard node: `paper` fill, `ink` stroke, `radius-md`; name in `node-name`, sublabel in `sublabel` |
| Hub | The one dark element: `ink` fill, `paper` text; slightly larger than a station |
| Focal station | At most one: `accent-tint` fill, `accent` stroke; station name may use `accent` |
| Ring flow | Circular `A R R 0 0 1` arcs on the station circle, solid `muted` stroke, default arrowhead at the destination; clockwise only |
| Write-back spoke | Dashed `soft` stroke at reduced emphasis, `stroke-dasharray="5,4"`, with a `soft` arrowhead |
| Spoke label | `arrow-label` role, `soft`, uppercase, paper mask, 6–10px clear of the connector |
Draw in this order: paper or optional dot grid → ring arrows → dashed spokes → spoke-label masks and labels → station boxes → hub → text. The nodes mask microscopic connector overshoot, while every intended endpoint still lands on an edge.
The hub is not a seventh process step. It is accumulated state: memory, standards, evidence, policy, or a shared operating record. Keep its copy to one name plus one short sublabel.
---
## 4. Connector rules (mandatory)
SKILL.md §6 applies in full except for the two Loop-specific connector primitives: circular ring arcs (§2.2) and straight radial spokes (§2.3). Like Medallion's promotion arcs, these replace §6 rule 1 for this diagram type:
- Ring arrows are same-radius circular arcs, solid, and clockwise. Every path uses `A R R 0 0 1`; destination markers land on station edges and no connector ends at a center point.
- Spokes are dashed and point inward. A solid spoke destroys the visual distinction between operating flow and write-back.
- Labels use opaque masks and maintain a visible 6–10px connector gap. Never place text on the stroke.
- No ring connector or spoke may overlap another connector. Ring paths remain outside the hub; spokes occupy distinct radial routes.
- When two spokes must leave the same station edge, fan their attach points by the §6 formula with at least 12px separation. The normal Loop has one spoke per station; use a second only when the semantics cannot be merged.
- If a ring route would cross the hub, increase `R` or split the diagram. Do not thread flow through shared state or substitute an orthogonal route.
---
## 5. Dark variant — token swap
Apply the style-guide inversion rule; do not invent a second palette.
| Role | Light | Dark |
|---|---|---|
| Canvas and station fill | `paper` | dark `paper` |
| Primary text and station stroke | `ink` | inverted `ink` |
| Hub fill / hub text | `ink` / `paper` | inverted `ink` / dark `paper` |
| Ring flow | `muted` | dark `muted` |
| Write-back spokes and labels | `soft` | dark `soft` |
| Focal fill / stroke | `accent-tint` / `accent` | dark `accent-tint` / brighter dark `accent` |
| Rule and dot grid | `rule` | inverted `rule` at the same opacity |
The semantic relationship stays unchanged in dark mode: one `ink`-filled hub, one optional `accent` station, neutral solid ring arrows, and lighter dashed write-backs.
---
## 6. Reproducibility checklist
1. Station count is 5–8 and hub count is exactly one.
2. Station 0 is at `-90deg`; all others use equal `360/N` steps clockwise.
3. Every solid ring arrow connects adjacent stations with `A R R 0 0 1`, using the same `R`; the last returns to the first.
4. Every ring marker lands on a station edge, not its center.
5. Every dashed spoke begins on a station's inner edge and stops `marker_gap` before the hub stroke.
6. Ring connectors stay outside the hub; spokes do not cross or overlap.
7. At most one station uses `accent-tint` + `accent`; the hub alone uses the dark `ink` fill.
8. Spoke labels, if present, use `arrow-label`, an opaque mask, and a 6–10px gap.
9. The viewBox includes station boxes, strokes, curves, markers, and margins without clipping.
---
## 7. Anti-patterns
| Anti-pattern | Why it fails / correction |
|---|---|
| Two hubs | Two accumulated states create two systems. Draw two diagrams. |
| Solid spokes | They look like primary flow and kill the dashed return signal. Use dashed `soft` write-backs. |
| Stations at uneven angles without reason | The ring stops reading as one operating cadence. Use equal `360/N` spacing unless a documented phase grouping requires a deliberate gap. |
| Mixed arc + orthogonal ring segments | The ring becomes a rounded rectangle. Every segment must be a circular arc of the same radius so the ring reads as one continuous circle. |
| Connectors crossing the hub | Flow becomes confused with state. Route the ring outside or enlarge the radius. |
| Accent on multiple stations | The editorial gate disappears. Keep one focal station at most. |
| More than 8 stations | Labels and spokes crowd the hub. Split into overview + detail. |
| A cycle that never actually returns | That is a Flowchart arranged in a circle. Use Flowchart and show the real endpoint. |
---
## 8. Examples
- `assets/example-loop.html` — minimal light: six-station self-improving operating loop.
- `assets/example-loop-dark.html` — the same geometry under the dark token inversion.
- `assets/example-loop-full.html` — editorial page with the flagship loop, three summary cards, and colophon.