egregore-artifacts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,134 @@
1
+ // Parse quest markdown (YAML frontmatter + body) into structured data
2
+ import fs from 'node:fs';
3
+
4
+ export function parseQuest(input) {
5
+ let content;
6
+ if (typeof input === 'string' && (input.endsWith('.md') || input.includes('/'))) {
7
+ if (!fs.existsSync(input)) throw new Error(`File not found: ${input}`);
8
+ content = fs.readFileSync(input, 'utf-8');
9
+ } else if (typeof input === 'string') {
10
+ content = input;
11
+ } else {
12
+ throw new Error('parseQuest expects a file path or markdown string');
13
+ }
14
+
15
+ const frontmatter = parseFrontmatter(content);
16
+ const body = content.replace(/^---\n[\s\S]*?\n---\n*/, '');
17
+
18
+ return {
19
+ ...frontmatter,
20
+ question: extractSection(body, 'The Question'),
21
+ context: extractSection(body, 'Context') || extractSection(body, 'Why This Matters'),
22
+ vision: extractBullets(body, 'Vision'),
23
+ threads: extractThreads(body),
24
+ artifacts: extractArtifactLines(body),
25
+ contributors: extractContributors(body, frontmatter),
26
+ entryPoints: extractBullets(body, 'Entry Points'),
27
+ technicalNotes: extractBullets(body, 'Technical Notes'),
28
+ };
29
+ }
30
+
31
+ function parseFrontmatter(content) {
32
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
33
+ if (!match) return {};
34
+
35
+ const result = {};
36
+ for (const line of match[1].split('\n')) {
37
+ const kv = line.match(/^(\w+):\s*(.+)$/);
38
+ if (!kv) continue;
39
+ const [, key, raw] = kv;
40
+ let value = raw.trim();
41
+
42
+ // Parse arrays: [a, b, c]
43
+ if (value.startsWith('[') && value.endsWith(']')) {
44
+ value = value.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
45
+ }
46
+ // Parse null
47
+ else if (value === 'null') value = null;
48
+ // Parse numbers
49
+ else if (/^\d+$/.test(value)) value = parseInt(value, 10);
50
+
51
+ result[key] = value;
52
+ }
53
+ return result;
54
+ }
55
+
56
+ function extractSection(body, heading) {
57
+ const regex = new RegExp(`## ${escapeRegex(heading)}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`);
58
+ const match = body.match(regex);
59
+ if (!match) return null;
60
+
61
+ // Return text content, stripping bullet lists and code blocks
62
+ const text = match[1].trim();
63
+ const lines = text.split('\n')
64
+ .filter(l => !l.startsWith('- [') && !l.startsWith('→') && !l.startsWith('```'))
65
+ .filter(l => !l.match(/^\|.*\|$/)) // skip tables
66
+ .join('\n')
67
+ .trim();
68
+
69
+ return lines || null;
70
+ }
71
+
72
+ function extractBullets(body, heading) {
73
+ const regex = new RegExp(`## ${escapeRegex(heading)}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`);
74
+ const match = body.match(regex);
75
+ if (!match) return [];
76
+
77
+ return match[1].split('\n')
78
+ .filter(l => l.match(/^- /))
79
+ .map(l => l.replace(/^- /, '').trim())
80
+ .filter(Boolean);
81
+ }
82
+
83
+ function extractThreads(body) {
84
+ const regex = /## Threads\s*\n([\s\S]*?)(?=\n## |$)/;
85
+ const match = body.match(regex);
86
+ if (!match) return [];
87
+
88
+ return match[1].split('\n')
89
+ .filter(l => l.match(/^- \[[ x]\]/))
90
+ .map(l => {
91
+ const done = l.includes('[x]');
92
+ const text = l.replace(/^- \[[ x]\]\s*/, '').trim();
93
+ return { text, done };
94
+ });
95
+ }
96
+
97
+ function extractArtifactLines(body) {
98
+ const regex = /## Artifacts\s*\n([\s\S]*?)(?=\n## |$)/;
99
+ const match = body.match(regex);
100
+ if (!match) return [];
101
+
102
+ return match[1].split('\n')
103
+ .filter(l => l.startsWith('→'))
104
+ .map(l => {
105
+ // Format: → 2026-04-03 [type] Title (author)
106
+ const m = l.match(/^→\s+(\S+)\s+\[(\w+)\]\s+(.+?)(?:\s+\((\w+)\))?$/);
107
+ if (!m) return { date: '', type: '', title: l.replace(/^→\s*/, ''), author: null };
108
+ return { date: m[1], type: m[2], title: m[3].trim(), author: m[4] || null };
109
+ });
110
+ }
111
+
112
+ function extractContributors(body, frontmatter) {
113
+ // From contributions table
114
+ const regex = /## Contributions\s*\n([\s\S]*?)(?=\n## |$)/;
115
+ const match = body.match(regex);
116
+ const names = new Set();
117
+
118
+ if (frontmatter.started_by) names.add(frontmatter.started_by);
119
+
120
+ if (match) {
121
+ for (const line of match[1].split('\n')) {
122
+ const row = line.match(/^\|\s*\S+\s*\|\s*(\S+)\s*\|/);
123
+ if (row && row[1] !== 'Who' && row[1] !== '—') {
124
+ names.add(row[1]);
125
+ }
126
+ }
127
+ }
128
+
129
+ return [...names].map(name => ({ name, avatar: null, role: null }));
130
+ }
131
+
132
+ function escapeRegex(str) {
133
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
134
+ }
@@ -0,0 +1,157 @@
1
+ // Egregore component registry — maps catalog types to branded React components
2
+ import React from 'react';
3
+ import { defineRegistry } from '@json-render/react';
4
+ import { catalog } from './catalog.js';
5
+ import { colors, fonts } from './tokens.js';
6
+ import { renderMarkdown } from './markdown.js';
7
+
8
+ const h = React.createElement;
9
+
10
+ export const { registry, handlers, executeAction } = defineRegistry(catalog, {
11
+ components: {
12
+ Page: ({ props, children }) => {
13
+ const statusClass = `eg-badge eg-badge-${props.status || 'active'}`;
14
+ const priorityLabels = { 1: 'low', 2: 'medium', 3: 'high' };
15
+
16
+ return h('div', null,
17
+ h('header', { className: 'eg-header' },
18
+ h('div', { className: 'eg-header-top' },
19
+ h('span', { className: 'eg-sigil' }, '✦'),
20
+ h('span', { className: 'eg-org-label' }, `egregore · ${props.type}`),
21
+ ),
22
+ h('h1', { className: 'eg-title' }, props.title),
23
+ h('div', { className: 'eg-meta-row' },
24
+ props.status && h('span', { className: statusClass }, props.status),
25
+ props.priority > 0 && h('span', { className: 'eg-badge eg-badge-priority' },
26
+ `↑ ${priorityLabels[props.priority] || 'p' + props.priority}`
27
+ ),
28
+ ...(props.projects || []).map((p, i) =>
29
+ h('span', { key: `p-${i}`, className: 'eg-badge eg-badge-outline' }, p)
30
+ ),
31
+ props.author && h('span', null, `by ${props.author}`),
32
+ props.date && h('span', null, props.date),
33
+ ),
34
+ ),
35
+ children,
36
+ h('footer', { className: 'eg-footer' },
37
+ h('div', { className: 'eg-footer-brand' },
38
+ h('span', { className: 'eg-footer-sigil' }, '✦'),
39
+ h('span', null, 'egregore'),
40
+ ),
41
+ h('div', null, new Date().toLocaleDateString('en-US', {
42
+ year: 'numeric', month: 'short', day: 'numeric',
43
+ hour: '2-digit', minute: '2-digit',
44
+ })),
45
+ ),
46
+ );
47
+ },
48
+
49
+ Section: ({ props, children }) =>
50
+ h('div', { className: 'eg-card' },
51
+ h('div', { className: 'eg-card-title' }, props.label),
52
+ children,
53
+ ),
54
+
55
+ Prose: ({ props }) =>
56
+ h('div', { className: props.variant === 'lead' ? 'eg-lead' : 'eg-body' },
57
+ renderMarkdown(props.text),
58
+ ),
59
+
60
+ MetricGrid: ({ props }) =>
61
+ h('div', {
62
+ style: {
63
+ display: 'grid',
64
+ gridTemplateColumns: `repeat(${Math.min(props.metrics.length, 4)}, 1fr)`,
65
+ gap: '12px',
66
+ marginBottom: '1.5rem',
67
+ },
68
+ },
69
+ ...props.metrics.map((m, i) =>
70
+ h('div', {
71
+ key: i,
72
+ style: {
73
+ background: 'white', border: `1px solid ${colors.border}`,
74
+ borderRadius: '12px', padding: '1.25rem', textAlign: 'center',
75
+ },
76
+ },
77
+ h('div', { style: { fontSize: '28px', fontFamily: fonts.serif, fontWeight: 400, color: colors.black, lineHeight: 1.2 } }, m.value),
78
+ h('div', { style: { fontSize: '12px', fontFamily: fonts.mono, color: colors.muted, marginTop: '4px', textTransform: 'uppercase', letterSpacing: '0.04em' } }, m.label),
79
+ )
80
+ ),
81
+ ),
82
+
83
+ ThreadList: ({ props }) =>
84
+ h('ul', { className: 'eg-thread-list' },
85
+ ...props.threads.map((t, i) =>
86
+ h('li', { key: i, className: 'eg-thread-item' },
87
+ h('span', { className: t.done ? 'eg-checkbox eg-checkbox-done' : 'eg-checkbox' }),
88
+ h('span', { className: t.done ? 'eg-thread-done' : undefined }, t.text),
89
+ )
90
+ ),
91
+ ),
92
+
93
+ ArtifactList: ({ props }) =>
94
+ h('div', null,
95
+ ...props.artifacts.map((a, i) =>
96
+ h('div', { key: i, className: 'eg-artifact-item' },
97
+ h('span', { className: 'eg-artifact-date' }, a.date),
98
+ h('span', { className: 'eg-artifact-type' }, a.type),
99
+ h('span', { className: 'eg-artifact-title' }, a.title),
100
+ a.author && h('span', { style: { color: colors.muted, fontSize: '13px', fontFamily: fonts.mono } }, `(${a.author})`),
101
+ )
102
+ ),
103
+ ),
104
+
105
+ PersonRow: ({ props }) =>
106
+ h('div', { className: 'eg-contributors' },
107
+ ...props.people.map((p, i) =>
108
+ h('div', { key: i, className: 'eg-contributor' },
109
+ h('span', { className: 'eg-contributor-avatar' }, (p.name || '?')[0].toUpperCase()),
110
+ h('span', null, p.name),
111
+ p.role && h('span', { style: { color: colors.muted, fontSize: '12px' } }, p.role),
112
+ )
113
+ ),
114
+ ),
115
+
116
+ BulletList: ({ props }) =>
117
+ h('ul', { className: 'eg-bullet-list' },
118
+ ...props.items.map((item, i) => h('li', { key: i }, item)),
119
+ ),
120
+
121
+ NumberedSteps: ({ props }) =>
122
+ h('ol', { style: { listStyle: 'none', padding: 0 } },
123
+ ...props.steps.map((step, i) =>
124
+ h('li', { key: i, style: { display: 'flex', gap: '12px', padding: '8px 0', borderBottom: i < props.steps.length - 1 ? '1px solid rgba(224, 216, 204, 0.5)' : 'none', fontSize: '15px', lineHeight: 1.5 } },
125
+ h('span', { style: { flexShrink: 0, width: '24px', height: '24px', borderRadius: '50%', background: colors.terracotta, color: colors.cream, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '12px', fontFamily: fonts.mono, fontWeight: 600 } }, String(i + 1)),
126
+ h('span', null, step),
127
+ )
128
+ ),
129
+ ),
130
+
131
+ SessionList: ({ props }) =>
132
+ h('div', null,
133
+ ...props.sessions.map((s, i) =>
134
+ h('div', { key: i, style: { display: 'flex', alignItems: 'baseline', gap: '12px', padding: '6px 0', borderBottom: '1px solid rgba(224, 216, 204, 0.5)', fontSize: '14px' } },
135
+ h('span', { style: { fontFamily: fonts.mono, fontSize: '12px', color: colors.muted, minWidth: '72px', flexShrink: 0 } }, s.date),
136
+ s.by && h('span', { style: { fontFamily: fonts.mono, fontSize: '11px', color: colors.blueMuted, background: 'rgba(123, 157, 183, 0.1)', padding: '1px 6px', borderRadius: '3px', flexShrink: 0 } }, s.by),
137
+ h('span', { style: { color: colors.black, flex: 1 } }, s.topic),
138
+ s.handedTo && h('span', { style: { fontFamily: fonts.mono, fontSize: '11px', color: colors.terracotta } }, `→ ${s.handedTo}`),
139
+ )
140
+ ),
141
+ ),
142
+
143
+ StatusBadge: ({ props }) =>
144
+ h('span', { className: `eg-badge eg-badge-${props.status}` }, props.status),
145
+
146
+ Callout: ({ props }) =>
147
+ h('div', {
148
+ style: {
149
+ background: 'rgba(212, 135, 90, 0.06)', border: '1px solid rgba(212, 135, 90, 0.15)',
150
+ borderRadius: '12px', padding: '1.25rem 1.5rem', marginBottom: '1.5rem',
151
+ fontSize: '15px', lineHeight: 1.6, color: colors.dark,
152
+ },
153
+ }, renderMarkdown(props.text)),
154
+
155
+ Divider: () => h('div', { className: 'eg-section-divider' }),
156
+ },
157
+ });
package/lib/render.js ADDED
@@ -0,0 +1,37 @@
1
+ // React SSR: component tree → HTML string wrapped in shell
2
+ import React from 'react';
3
+ import { renderToStaticMarkup } from 'react-dom/server';
4
+ import { Renderer, JSONUIProvider, createStateStore } from '@json-render/react';
5
+ import { registry } from './registry.js';
6
+ import { htmlShell } from './shell.js';
7
+
8
+ const h = React.createElement;
9
+
10
+ // Render a React element tree (from direct templates)
11
+ export function renderToHtml(element, options = {}) {
12
+ const bodyHtml = renderToStaticMarkup(element);
13
+ return htmlShell(bodyHtml, {
14
+ title: options.title || 'Egregore Artifact',
15
+ type: options.type || 'artifact',
16
+ });
17
+ }
18
+
19
+ // Render a json-render flat spec (from Claude-generated specs)
20
+ export function renderSpecToHtml(spec, options = {}) {
21
+ const store = createStateStore({});
22
+
23
+ const element = h(JSONUIProvider, {
24
+ spec,
25
+ registry,
26
+ onAction: () => {},
27
+ store,
28
+ },
29
+ h(Renderer, { spec, registry })
30
+ );
31
+
32
+ const bodyHtml = renderToStaticMarkup(element);
33
+ return htmlShell(bodyHtml, {
34
+ title: options.title || 'Egregore Artifact',
35
+ type: options.type || 'artifact',
36
+ });
37
+ }
package/lib/shell.js ADDED
@@ -0,0 +1,341 @@
1
+ // Self-contained HTML document wrapper with Egregore branding
2
+ import { colors, fonts } from './tokens.js';
3
+
4
+ function escapeHtml(str) {
5
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
6
+ }
7
+
8
+ export function htmlShell(bodyHtml, { title = 'Egregore Artifact', type = 'artifact' } = {}) {
9
+ return `<!DOCTYPE html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="UTF-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
14
+ <title>${escapeHtml(title)} — Egregore</title>
15
+
16
+ <!-- Inter + IBM Plex Mono from Google Fonts -->
17
+ <link rel="preconnect" href="https://fonts.googleapis.com">
18
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
19
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
20
+
21
+ <style>
22
+ :root {
23
+ --cream: ${colors.cream};
24
+ --black: ${colors.black};
25
+ --terracotta: ${colors.terracotta};
26
+ --blue-muted: ${colors.blueMuted};
27
+ --dark: ${colors.dark};
28
+ --border: ${colors.border};
29
+ --muted: ${colors.muted};
30
+ --warm-gray: ${colors.warmGray};
31
+ --terminal-bg: ${colors.terminalBg};
32
+ --font-serif: ${fonts.serif};
33
+ --font-sans: ${fonts.sans};
34
+ --font-mono: ${fonts.mono};
35
+ }
36
+
37
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
38
+
39
+ body {
40
+ background: var(--cream);
41
+ color: var(--black);
42
+ font-family: var(--font-sans);
43
+ font-size: 16px;
44
+ line-height: 1.65;
45
+ -webkit-font-smoothing: antialiased;
46
+ -moz-osx-font-smoothing: grayscale;
47
+ }
48
+
49
+ ::selection { background: var(--terracotta); color: var(--cream); }
50
+
51
+ /* Layout */
52
+ .eg-artifact {
53
+ max-width: 780px;
54
+ margin: 0 auto;
55
+ padding: 3.5rem 2rem 4rem;
56
+ }
57
+
58
+ /* Header */
59
+ .eg-header {
60
+ border-bottom: 2px solid var(--border);
61
+ padding-bottom: 2rem;
62
+ margin-bottom: 2.5rem;
63
+ }
64
+ .eg-header-top {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 12px;
68
+ margin-bottom: 1.25rem;
69
+ }
70
+ .eg-sigil {
71
+ font-size: 20px;
72
+ color: var(--terracotta);
73
+ line-height: 1;
74
+ }
75
+ .eg-org-label {
76
+ font-family: var(--font-mono);
77
+ font-size: 13px;
78
+ color: var(--muted);
79
+ letter-spacing: 0.02em;
80
+ }
81
+ .eg-title {
82
+ font-family: var(--font-serif);
83
+ font-size: 36px;
84
+ font-weight: 400;
85
+ line-height: 1.2;
86
+ letter-spacing: -0.02em;
87
+ color: var(--black);
88
+ margin-bottom: 0.75rem;
89
+ }
90
+ .eg-meta-row {
91
+ display: flex;
92
+ flex-wrap: wrap;
93
+ align-items: center;
94
+ gap: 16px;
95
+ font-family: var(--font-mono);
96
+ font-size: 13px;
97
+ color: var(--muted);
98
+ }
99
+ .eg-meta-row span { white-space: nowrap; }
100
+
101
+ /* Badges */
102
+ .eg-badge {
103
+ display: inline-block;
104
+ padding: 3px 10px;
105
+ border-radius: 50px;
106
+ font-family: var(--font-mono);
107
+ font-size: 12px;
108
+ font-weight: 500;
109
+ letter-spacing: 0.01em;
110
+ }
111
+ .eg-badge-active { background: var(--terracotta); color: var(--cream); }
112
+ .eg-badge-paused { background: var(--blue-muted); color: var(--cream); }
113
+ .eg-badge-completed { background: var(--black); color: var(--cream); }
114
+ .eg-badge-blocked { background: var(--dark); color: var(--cream); }
115
+ .eg-badge-outline {
116
+ background: transparent;
117
+ border: 1px solid var(--border);
118
+ color: var(--muted);
119
+ }
120
+ .eg-badge-priority {
121
+ background: rgba(212, 135, 90, 0.12);
122
+ color: var(--terracotta);
123
+ }
124
+
125
+ /* Sections */
126
+ .eg-section {
127
+ margin-bottom: 2.5rem;
128
+ }
129
+ .eg-section-title {
130
+ font-family: var(--font-serif);
131
+ font-size: 24px;
132
+ font-weight: 400;
133
+ line-height: 1.3;
134
+ letter-spacing: -0.02em;
135
+ color: var(--black);
136
+ margin-bottom: 1rem;
137
+ }
138
+ .eg-section-divider {
139
+ height: 1px;
140
+ background: var(--border);
141
+ margin: 2.5rem 0;
142
+ }
143
+
144
+ /* Card */
145
+ .eg-card {
146
+ background: white;
147
+ border: 1px solid var(--border);
148
+ border-radius: 12px;
149
+ padding: 1.5rem;
150
+ margin-bottom: 1.25rem;
151
+ }
152
+ .eg-card-title {
153
+ font-family: var(--font-mono);
154
+ font-size: 12px;
155
+ font-weight: 500;
156
+ text-transform: uppercase;
157
+ letter-spacing: 0.06em;
158
+ color: var(--muted);
159
+ margin-bottom: 0.75rem;
160
+ }
161
+
162
+ /* Text */
163
+ .eg-lead {
164
+ font-size: 18px;
165
+ line-height: 1.6;
166
+ color: var(--black);
167
+ }
168
+ .eg-body {
169
+ font-size: 16px;
170
+ line-height: 1.65;
171
+ color: var(--dark);
172
+ }
173
+ .eg-body p { margin-bottom: 0.75rem; }
174
+ .eg-body p:last-child { margin-bottom: 0; }
175
+
176
+ /* Threads (checklist) */
177
+ .eg-thread-list {
178
+ list-style: none;
179
+ padding: 0;
180
+ }
181
+ .eg-thread-item {
182
+ display: flex;
183
+ align-items: flex-start;
184
+ gap: 10px;
185
+ padding: 8px 0;
186
+ border-bottom: 1px solid rgba(224, 216, 204, 0.5);
187
+ font-size: 15px;
188
+ line-height: 1.5;
189
+ }
190
+ .eg-thread-item:last-child { border-bottom: none; }
191
+ .eg-checkbox {
192
+ flex-shrink: 0;
193
+ width: 18px;
194
+ height: 18px;
195
+ border: 2px solid var(--border);
196
+ border-radius: 4px;
197
+ margin-top: 2px;
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ }
202
+ .eg-checkbox-done {
203
+ background: var(--terracotta);
204
+ border-color: var(--terracotta);
205
+ color: white;
206
+ }
207
+ .eg-checkbox-done::after { content: "✓"; font-size: 12px; font-weight: 700; }
208
+ .eg-thread-done { color: var(--muted); text-decoration: line-through; }
209
+
210
+ /* Artifact list */
211
+ .eg-artifact-item {
212
+ display: flex;
213
+ gap: 12px;
214
+ padding: 8px 0;
215
+ border-bottom: 1px solid rgba(224, 216, 204, 0.5);
216
+ font-size: 14px;
217
+ }
218
+ .eg-artifact-item:last-child { border-bottom: none; }
219
+ .eg-artifact-date {
220
+ font-family: var(--font-mono);
221
+ font-size: 12px;
222
+ color: var(--muted);
223
+ white-space: nowrap;
224
+ min-width: 80px;
225
+ padding-top: 1px;
226
+ }
227
+ .eg-artifact-type {
228
+ font-family: var(--font-mono);
229
+ font-size: 11px;
230
+ color: var(--blue-muted);
231
+ background: rgba(123, 157, 183, 0.1);
232
+ padding: 1px 6px;
233
+ border-radius: 3px;
234
+ white-space: nowrap;
235
+ }
236
+ .eg-artifact-title { color: var(--black); }
237
+
238
+ /* Contributors */
239
+ .eg-contributors {
240
+ display: flex;
241
+ flex-wrap: wrap;
242
+ gap: 12px;
243
+ }
244
+ .eg-contributor {
245
+ display: flex;
246
+ align-items: center;
247
+ gap: 8px;
248
+ padding: 6px 12px;
249
+ background: rgba(59, 45, 33, 0.04);
250
+ border-radius: 50px;
251
+ font-size: 14px;
252
+ }
253
+ .eg-contributor-avatar {
254
+ width: 28px;
255
+ height: 28px;
256
+ border-radius: 50%;
257
+ background: var(--terracotta);
258
+ color: var(--cream);
259
+ display: flex;
260
+ align-items: center;
261
+ justify-content: center;
262
+ font-family: var(--font-mono);
263
+ font-size: 12px;
264
+ font-weight: 500;
265
+ }
266
+
267
+ /* Bullet list (vision, entry points) */
268
+ .eg-bullet-list {
269
+ list-style: none;
270
+ padding: 0;
271
+ }
272
+ .eg-bullet-list li {
273
+ position: relative;
274
+ padding-left: 1.25rem;
275
+ margin-bottom: 0.5rem;
276
+ font-size: 15px;
277
+ line-height: 1.55;
278
+ color: var(--dark);
279
+ }
280
+ .eg-bullet-list li::before {
281
+ content: "";
282
+ position: absolute;
283
+ left: 0;
284
+ top: 9px;
285
+ width: 6px;
286
+ height: 6px;
287
+ border-radius: 50%;
288
+ background: var(--terracotta);
289
+ }
290
+
291
+ /* Code blocks */
292
+ .eg-code {
293
+ font-family: var(--font-mono);
294
+ font-size: 13px;
295
+ background: rgba(59, 45, 33, 0.06);
296
+ padding: 2px 6px;
297
+ border-radius: 4px;
298
+ }
299
+
300
+ /* Footer */
301
+ .eg-footer {
302
+ border-top: 1px solid var(--border);
303
+ padding-top: 1.5rem;
304
+ margin-top: 3rem;
305
+ display: flex;
306
+ justify-content: space-between;
307
+ align-items: center;
308
+ font-family: var(--font-mono);
309
+ font-size: 12px;
310
+ color: var(--muted);
311
+ }
312
+ .eg-footer-brand {
313
+ display: flex;
314
+ align-items: center;
315
+ gap: 6px;
316
+ }
317
+ .eg-footer-sigil { color: var(--terracotta); }
318
+
319
+ /* Print */
320
+ @media print {
321
+ body { background: white; }
322
+ .eg-artifact { padding: 1rem; max-width: none; }
323
+ .eg-card { break-inside: avoid; }
324
+ }
325
+
326
+ /* Mobile */
327
+ @media (max-width: 640px) {
328
+ .eg-artifact { padding: 2rem 1.25rem 3rem; }
329
+ .eg-title { font-size: 28px; }
330
+ .eg-section-title { font-size: 20px; }
331
+ .eg-meta-row { gap: 10px; }
332
+ }
333
+ </style>
334
+ </head>
335
+ <body>
336
+ <div class="eg-artifact" data-type="${escapeHtml(type)}">
337
+ ${bodyHtml}
338
+ </div>
339
+ </body>
340
+ </html>`;
341
+ }