eklavya 1.26.0 → 1.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +48 -42
  2. package/dist/artifacts.js +214 -0
  3. package/dist/artifacts.js.map +1 -0
  4. package/dist/assets/artifact-template.html +144 -0
  5. package/dist/assets/dashboard.html +131 -11
  6. package/dist/assets/tokens.css +6 -4
  7. package/dist/assets/tutor/SKILL.md +7 -0
  8. package/dist/cli-memory.js +6 -3
  9. package/dist/cli-memory.js.map +1 -1
  10. package/dist/cli.js +85 -2
  11. package/dist/cli.js.map +1 -1
  12. package/dist/config.js +3 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/dashboard.js +41 -1
  15. package/dist/dashboard.js.map +1 -1
  16. package/dist/hooks/memory-lib.js +1 -1
  17. package/dist/hooks/memory-lib.js.map +1 -1
  18. package/dist/hooks/session-start.js +10 -2
  19. package/dist/hooks/session-start.js.map +1 -1
  20. package/dist/install-lock.js +155 -0
  21. package/dist/install-lock.js.map +1 -0
  22. package/dist/install.js +64 -45
  23. package/dist/install.js.map +1 -1
  24. package/dist/memory/reservation.js +15 -5
  25. package/dist/memory/reservation.js.map +1 -1
  26. package/dist/memory/worker.js +72 -31
  27. package/dist/memory/worker.js.map +1 -1
  28. package/dist/migrations/015_event_link_indexes.sql +24 -0
  29. package/dist/paths.js +8 -0
  30. package/dist/paths.js.map +1 -1
  31. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  32. package/dist/plugin/agents/explainer.md +57 -0
  33. package/dist/plugin/agents/tutor.md +7 -0
  34. package/dist/plugin/cli/CLAUDE.md +60 -108
  35. package/dist/plugin/hooks/CLAUDE.md +118 -357
  36. package/dist/plugin/hooks/run.mjs +104 -21
  37. package/dist/plugin/skills/CLAUDE.md +106 -291
  38. package/dist/plugin/skills/tutor/SKILL.md +7 -0
  39. package/dist/tools/config_tools.js +4 -0
  40. package/dist/tools/config_tools.js.map +1 -1
  41. package/dist/tools/record_attempt.js +25 -1
  42. package/dist/tools/record_attempt.js.map +1 -1
  43. package/dist/update.js +7 -83
  44. package/dist/update.js.map +1 -1
  45. package/dist/user-skill/eklavya/SKILL.md +8 -0
  46. package/dist/user-skill/eklavya-artifacts/SKILL.md +135 -0
  47. package/package.json +1 -1
package/README.md CHANGED
@@ -1,27 +1,29 @@
1
- # eklavya
1
+ # Eklavya
2
2
 
3
- [Eklavya](https://github.com/ProjectAJ14/eklavya) — learn while your agent works. This package is
4
- the whole thing: the installer, the MCP server, the CLI, and the Claude Code plugin it installs.
3
+ [Eklavya](https://github.com/ProjectAJ14/eklavya) teaches the concepts behind
4
+ agent-written code and keeps local project memory. This package includes the
5
+ Claude Code plugin, installer, CLI and MCP server.
5
6
 
6
- ## Install
7
+ ## Install in Claude Code
8
+
9
+ With Node 22 or newer:
7
10
 
8
11
  ```bash
9
12
  npx eklavya install
10
13
  ```
11
14
 
12
- That checks your Node version, installs the runtime and its SQLite driver, registers the plugin
13
- with Claude Code and enables it, installs the chat skill into `~/.claude/skills/`, and creates the
14
- database. Restart Claude Code afterwards, then run `/eklavya:setup` to pick a mode. Re-running it
15
- is how you upgrade; `npx eklavya uninstall` removes it and keeps your history unless you pass
16
- `--purge`.
15
+ Follow the settings prompts, then restart Claude Code. The installer registers
16
+ the plugin, installs the managed runtime and chat/artifact skills, and prepares
17
+ local storage. `/eklavya:setup` lets you revisit settings.
17
18
 
18
- Requires **Node 22+** — below that the SQLite driver has no prebuilt binary and would need a C++
19
- toolchain to compile.
19
+ Read the [installation guide](https://eklavya-run.web.app/docs/installing/) for
20
+ verification and [updates and removal](https://eklavya-run.web.app/docs/updates/)
21
+ for maintenance. Uninstall preserves learner data unless you explicitly use
22
+ `--purge`.
20
23
 
21
- ## As a standalone MCP server
24
+ ## Standalone MCP connection
22
25
 
23
- The server runs on its own, so any MCP client can use it — this is what makes Eklavya
24
- editor-agnostic.
26
+ An MCP client can connect directly:
25
27
 
26
28
  ```json
27
29
  {
@@ -31,41 +33,45 @@ editor-agnostic.
31
33
  }
32
34
  ```
33
35
 
34
- > Renamed from `eklavya-mcp` after 1.7.0, and the separate `eklavya-mcp` binary folded into
35
- > `eklavya serve` at the same time — one package, one command. The old package still exists on
36
- > npm so older installs keep resolving, but it is no longer updated.
36
+ This exposes tools. Automatic capture, checkpoints and session instructions
37
+ depend on host integration; an MCP connection alone does not provide Claude
38
+ Code's learning loop.
37
39
 
38
- State lives in `~/.eklavya/knowledge.db` (SQLite, WAL). Override with `EKLAVYA_HOME` or `EKLAVYA_DB`.
40
+ ## Available tools
39
41
 
40
- ## Tools
41
-
42
- | Tool | Purpose |
42
+ | Purpose | Tools |
43
43
  |---|---|
44
- | `get_learner_profile` | what the developer already knows; call before teaching |
45
- | `log_session_concepts` | record what the current task exercises, with code context |
46
- | `get_session_quiz_plan` | what to ask, and at which difficulty tier |
47
- | `record_attempt` | grade 0–5, update mastery and the review schedule |
48
- | `get_gate_status` | whether this session's quiz gate has passed |
49
- | `upsert_concepts` | grow the graph, with slug normalization and dedupe |
50
- | `get_concept_graph` | a domain in prerequisite order |
51
- | `get_config` / `set_config` | global and per-repo settings |
44
+ | Learner state and concepts | `get_learner_profile`, `log_session_concepts`, `upsert_concepts`, `get_concept_graph` |
45
+ | Questions and assessment | `get_session_quiz_plan`, `record_attempt`, `get_gate_status` |
46
+ | Settings | `get_config`, `set_config` |
47
+ | Find and read memory | `memory_search`, `memory_get`, `memory_timeline`, `memory_file_history`, `memory_status` |
48
+ | Maintain memory | `memory_write`, `memory_correct`, `memory_delete`, `memory_collections` |
49
+ | Inspect code | `code_outline`, `code_find_symbol` |
52
50
 
53
- `session_id` is optional on every tool — the server resolves the current session itself.
51
+ Search first, choose relevant entries, then use `memory_get` for their full
52
+ content. Session-aware learning and configuration tools resolve the current session
53
+ when `session_id` is omitted. In `memory_timeline`, it instead filters results
54
+ to one session; omission shows the project timeline. Other tools may not accept
55
+ that argument.
54
56
 
55
- ## CLI
57
+ ## CLI and data
56
58
 
57
59
  ```bash
58
- eklavya install # install into Claude Code, runtime included
59
- eklavya uninstall [--purge] # remove it; --purge also deletes your history
60
- eklavya doctor # check the install
61
- eklavya config get
62
- eklavya config set quiz.enforced true [--project] # gate commits on one codebase
63
- eklavya config set focus concept # what it teaches
64
- eklavya config set focus learn --topic caching
65
- eklavya export-rules --out rules.md # the tutor pedagogy as Markdown
66
- eklavya dashboard [--port <n>] [--no-open] # your learning history, served locally and opened
67
- # in your browser (--no-open just prints the URL)
68
- eklavya db-path
60
+ npx eklavya doctor
61
+ npx eklavya memory status
62
+ npx eklavya config get
63
+ npx eklavya dashboard
64
+ npx eklavya db-path
69
65
  ```
70
66
 
67
+ The [CLI reference](https://eklavya-run.web.app/docs/cli/) lists all commands and
68
+ flags. [Configuration](https://eklavya-run.web.app/docs/configuration/) lists
69
+ defaults and scopes. The [memory guide](https://eklavya-run.web.app/docs/memory/)
70
+ covers retrieval, processing, providers, privacy and sync.
71
+
72
+ State is local under `~/.eklavya/`, including `knowledge.db` (SQLite, WAL).
73
+ `EKLAVYA_HOME` and `EKLAVYA_DB` override those paths. Questions and memory have
74
+ independent switches. External processing is optional and explicitly configured;
75
+ see [your data](https://eklavya-run.web.app/docs/your-data/).
76
+
71
77
  MIT.
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Artifacts: self-contained HTML pages an agent writes to explain something —
3
+ * a concept the developer got wrong, a design, a report — kept under
4
+ * `~/.eklavya/artifacts/<project slug>/`.
5
+ *
6
+ * Three rules shape this file.
7
+ *
8
+ * **The files are the index.** There is no table. `listArtifacts` reads each
9
+ * page's `<head>` for the metadata `createArtifact` stamped into it, so a page
10
+ * deleted, renamed or copied in by hand is reflected on the next read, and
11
+ * there is nothing to fall out of step with the disk. The ceiling is a scan of
12
+ * every file per dashboard load; `HEAD_BYTES` keeps each read small, and a few
13
+ * thousand pages is still milliseconds.
14
+ *
15
+ * **The design tokens are inlined at creation, not copied into the template.**
16
+ * `tokens.css` is the one palette the site and the dashboard share; a second
17
+ * hand-kept copy inside a template is the copy nobody updates. Inlining makes
18
+ * the page self-contained — its "download HTML" button hands over a file that
19
+ * still themes and prints — without a second source.
20
+ *
21
+ * **Every path in is untrusted.** `resolveArtifact` is what the dashboard
22
+ * serves through, and it refuses anything that does not land on an `.html`
23
+ * file directly inside one project folder.
24
+ */
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
28
+ import { findRepoConfig, mainRepoRoot } from './config.js';
29
+ import { artifactsDir, projectSlug } from './paths.js';
30
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
31
+ /** Enough of a file to hold its `<head>`: the metadata is written first. */
32
+ const HEAD_BYTES = 16 * 1024;
33
+ /** What a page is filed under: the main checkout behind a worktree, else the directory itself. */
34
+ export function artifactProject(cwd = process.cwd()) {
35
+ const { repoRoot } = findRepoConfig(cwd);
36
+ if (repoRoot)
37
+ return mainRepoRoot(repoRoot);
38
+ try {
39
+ return fs.realpathSync(path.resolve(cwd));
40
+ }
41
+ catch {
42
+ return path.resolve(cwd);
43
+ }
44
+ }
45
+ export function kebab(title) {
46
+ const s = title
47
+ .normalize('NFKD')
48
+ .replace(/[̀-ͯ]/g, '')
49
+ .toLowerCase()
50
+ .replace(/[^a-z0-9]+/g, '-')
51
+ .replace(/^-+|-+$/g, '')
52
+ .slice(0, 60)
53
+ .replace(/-+$/, '');
54
+ return s || 'artifact';
55
+ }
56
+ const escHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
57
+ function assetPath(name) {
58
+ return path.join(moduleDir, 'assets', name);
59
+ }
60
+ /** The shared tokens, minus the remote font import (the template links the fonts itself). */
61
+ function inlineTokens() {
62
+ try {
63
+ const css = fs.readFileSync(assetPath('tokens.css'), 'utf8');
64
+ return css.replace(/@import\s+url\([^)]*\)[^;]*;\s*/gi, '');
65
+ }
66
+ catch {
67
+ // A build without the tokens still writes the page; it renders unstyled
68
+ // until the tokens are restored, rather than failing the command.
69
+ return '';
70
+ }
71
+ }
72
+ /**
73
+ * Writes the template, filled in, to a fresh file and returns its path. Never
74
+ * overwrites: a second page with the same title on the same day gets `-2`.
75
+ */
76
+ export function createArtifact(opts) {
77
+ const title = opts.title.trim();
78
+ if (!title)
79
+ throw new Error('an artifact needs a title');
80
+ const now = opts.now ?? new Date();
81
+ const project = artifactProject(opts.cwd);
82
+ const folder = projectSlug(project);
83
+ const dir = path.join(artifactsDir(), folder);
84
+ fs.mkdirSync(dir, { recursive: true });
85
+ // The local date, not UTC: the filename is for the person reading their folder.
86
+ const pad = (n) => String(n).padStart(2, '0');
87
+ const day = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
88
+ const stem = `${day}-${kebab(title)}`;
89
+ let file = `${stem}.html`;
90
+ for (let n = 2; fs.existsSync(path.join(dir, file)); n++)
91
+ file = `${stem}-${n}.html`;
92
+ const description = (opts.description ?? '').trim();
93
+ const kind = opts.kind ?? 'artifact';
94
+ const fill = {
95
+ '{{TITLE}}': escHtml(title),
96
+ '{{DESCRIPTION}}': escHtml(description),
97
+ '{{PROJECT}}': escHtml(project),
98
+ '{{PROJECT_NAME}}': escHtml(path.basename(project) || project),
99
+ '{{KIND}}': kind,
100
+ '{{CONCEPT}}': escHtml(opts.concept ?? ''),
101
+ '{{CREATED}}': now.toISOString(),
102
+ '{{EYEBROW}}': kind === 'explainer' ? 'Explainer' : 'Artifact',
103
+ };
104
+ let html = fs.readFileSync(assetPath('artifact-template.html'), 'utf8');
105
+ // Tokens first: the CSS contains no placeholders, but user text could.
106
+ html = html.replace('/*{{TOKENS}}*/', () => inlineTokens());
107
+ html = html.replace(/\{\{[A-Z_]+\}\}/g, (m) => fill[m] ?? m);
108
+ const target = path.join(dir, file);
109
+ fs.writeFileSync(target, html, { flag: 'wx' });
110
+ return { path: target, id: `${folder}/${file}`, project };
111
+ }
112
+ function meta(head, name) {
113
+ const re = new RegExp(`<meta\\s+name="${name}"\\s+content="([^"]*)"`, 'i');
114
+ const m = re.exec(head);
115
+ return m ? unescHtml(m[1]) : null;
116
+ }
117
+ function unescHtml(s) {
118
+ return s.replace(/&(amp|lt|gt|quot|#39);/g, (_, e) => ({ amp: '&', lt: '<', gt: '>', quot: '"', '#39': "'" })[e]);
119
+ }
120
+ function readHead(file) {
121
+ const fd = fs.openSync(file, 'r');
122
+ try {
123
+ const buf = Buffer.alloc(HEAD_BYTES);
124
+ const n = fs.readSync(fd, buf, 0, HEAD_BYTES, 0);
125
+ return buf.subarray(0, n).toString('utf8');
126
+ }
127
+ finally {
128
+ fs.closeSync(fd);
129
+ }
130
+ }
131
+ /**
132
+ * Every artifact on disk, newest first. A page with no Eklavya metadata — one
133
+ * copied in by hand — is still listed, titled from its `<title>` or filename.
134
+ * Never throws: an unreadable file costs that one row.
135
+ */
136
+ export function listArtifacts(root = artifactsDir()) {
137
+ const rows = [];
138
+ let folders;
139
+ try {
140
+ folders = fs.readdirSync(root, { withFileTypes: true });
141
+ }
142
+ catch {
143
+ return rows;
144
+ }
145
+ for (const folder of folders) {
146
+ // The same rules `resolveArtifact` serves by, so nothing is listed that
147
+ // the dashboard would then refuse: no dot-names, no symlinks — a link's
148
+ // `<head>` would otherwise be read from wherever it points.
149
+ if (!folder.isDirectory() || folder.name.startsWith('.'))
150
+ continue;
151
+ let files;
152
+ try {
153
+ files = fs.readdirSync(path.join(root, folder.name));
154
+ }
155
+ catch {
156
+ continue;
157
+ }
158
+ for (const name of files) {
159
+ if (!/\.html?$/i.test(name) || name.startsWith('.'))
160
+ continue;
161
+ const file = path.join(root, folder.name, name);
162
+ try {
163
+ const st = fs.lstatSync(file);
164
+ if (!st.isFile())
165
+ continue;
166
+ const head = readHead(file);
167
+ const title = /<title>([^<]*)<\/title>/i.exec(head)?.[1]?.trim();
168
+ const kind = meta(head, 'eklavya:kind');
169
+ rows.push({
170
+ id: `${folder.name}/${name}`,
171
+ title: title ? unescHtml(title) : name.replace(/\.html?$/i, ''),
172
+ description: meta(head, 'description') ?? '',
173
+ project: meta(head, 'eklavya:project') || null,
174
+ kind: kind === 'explainer' ? 'explainer' : 'artifact',
175
+ concept: meta(head, 'eklavya:concept') || null,
176
+ created: meta(head, 'eklavya:created') || st.mtime.toISOString(),
177
+ bytes: st.size,
178
+ });
179
+ }
180
+ catch {
181
+ continue;
182
+ }
183
+ }
184
+ }
185
+ return rows.sort((a, b) => (a.created < b.created ? 1 : a.created > b.created ? -1 : 0));
186
+ }
187
+ /**
188
+ * The file behind an id, or null. Exactly `<folder>/<file>.html`, both plain
189
+ * names — no separators, no `..`, no leading dot — and the resolved path must
190
+ * still sit inside the root after symlinks, so a link planted in a project
191
+ * folder cannot serve a file from elsewhere on the machine.
192
+ */
193
+ export function resolveArtifact(id, root = artifactsDir()) {
194
+ const parts = id.split('/');
195
+ if (parts.length !== 2)
196
+ return null;
197
+ const plain = /^[^/\\\0]+$/;
198
+ for (const p of parts)
199
+ if (!plain.test(p) || p.startsWith('.'))
200
+ return null;
201
+ if (!/\.html?$/i.test(parts[1]))
202
+ return null;
203
+ try {
204
+ const realRoot = fs.realpathSync(root);
205
+ const real = fs.realpathSync(path.join(root, parts[0], parts[1]));
206
+ if (path.dirname(path.dirname(real)) !== realRoot)
207
+ return null;
208
+ return fs.statSync(real).isFile() ? real : null;
209
+ }
210
+ catch {
211
+ return null;
212
+ }
213
+ }
214
+ //# sourceMappingURL=artifacts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,144 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>{{TITLE}}</title>
7
+ <meta name="description" content="{{DESCRIPTION}}">
8
+ <meta name="eklavya:project" content="{{PROJECT}}">
9
+ <meta name="eklavya:kind" content="{{KIND}}">
10
+ <meta name="eklavya:concept" content="{{CONCEPT}}">
11
+ <meta name="eklavya:created" content="{{CREATED}}">
12
+ <!-- Eklavya type. Offline, the stacks fall back to system faces; nothing breaks. -->
13
+ <link rel="preconnect" href="https://fonts.googleapis.com">
14
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;800;900&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap">
16
+ <style>
17
+ /* Eklavya design tokens, inlined from web/public/tokens.css when this page was
18
+ created. Ink (dark) is the default on screen and in the PDF; data-mode="paper"
19
+ on <html> switches to the paper ground. Components below name ROLES only. */
20
+ /*{{TOKENS}}*/
21
+ </style>
22
+ <style>
23
+ :root{ --maxw:880px; }
24
+
25
+ *{box-sizing:border-box}
26
+ body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.65 var(--font-body)}
27
+ ::selection{background:var(--spot);color:var(--spot-ink)}
28
+ main{max-width:var(--maxw);margin:0 auto;padding:64px 16px 96px}
29
+
30
+ .eyebrow{margin:0 0 14px;font:400 11px/1 var(--font-mono);letter-spacing:var(--tracking-caps-wide);
31
+ text-transform:uppercase;color:var(--spot)}
32
+ h1{font:900 clamp(34px,5.4vw,64px)/.95 var(--font-disp);letter-spacing:var(--tracking-display);margin:0 0 .35em}
33
+ h2{font:800 clamp(24px,3vw,34px)/1 var(--font-disp);letter-spacing:var(--tracking-tight);
34
+ margin:2.4em 0 .7em;padding-top:1em;border-top:1px solid var(--line-2)}
35
+ h3{font:800 18px/1.3 var(--font-disp);letter-spacing:var(--tracking-snug);margin:1.8em 0 .4em}
36
+ p,li{color:var(--ink)}
37
+ .lede{color:var(--dim);font-size:17px;max-width:620px;margin:0 0 2.4em}
38
+ .muted{color:var(--dim)}
39
+ a{color:var(--spot);text-decoration:none;border-bottom:1px solid var(--spot)}
40
+ strong{font-weight:600}
41
+
42
+ code{font-family:var(--font-mono);font-size:.88em;background:var(--mass);padding:.1em .38em}
43
+ pre{background:var(--panel);border:1px solid var(--line-2);padding:14px 16px;overflow:auto;font-size:13px}
44
+ pre code{background:none;padding:0}
45
+
46
+ table{width:100%;border-collapse:collapse;margin:1.2em 0;font-size:14px}
47
+ th,td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--line-2);vertical-align:top}
48
+ th{font:400 11px var(--font-mono);letter-spacing:var(--tracking-caps);text-transform:uppercase;color:var(--faint)}
49
+ td.num,th.num{text-align:right;font-family:var(--font-mono);font-variant-numeric:tabular-nums}
50
+ blockquote{margin:1.2em 0;padding:.2em 0 .2em 16px;border-left:2px solid var(--spot);color:var(--dim)}
51
+
52
+ .card{background:var(--panel);border:1px solid var(--line);padding:24px}
53
+ .card h3{margin-top:0}
54
+ .card p{color:var(--dim);font-size:14.5px;margin:0}
55
+ .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin:1.4em 0}
56
+ .chip{display:inline-block;font:12px var(--font-mono);color:var(--spot);background:var(--spot-soft);
57
+ border:1px solid var(--spot);padding:2px 9px}
58
+ .chip.warn{color:var(--warning);border-color:var(--warning);background:color-mix(in srgb,var(--warning) 12%,transparent)}
59
+ .chip.bad{color:var(--error);border-color:var(--error);background:color-mix(in srgb,var(--error) 12%,transparent)}
60
+ .stat{border:1px solid var(--line-2);padding:18px 20px}
61
+ .stat .k{font:400 11px var(--font-mono);letter-spacing:var(--tracking-caps);text-transform:uppercase;color:var(--faint)}
62
+ .stat .v{font:900 40px/1.1 var(--font-disp);letter-spacing:var(--tracking-tight);margin-top:6px}
63
+ .stat .d{color:var(--dim);font-size:13px}
64
+ .callout{border:1px solid var(--spot);background:var(--spot-soft);padding:14px 18px;margin:1.4em 0}
65
+
66
+ figure{margin:1.8em 0;background:var(--panel);border:1px solid var(--line-2);
67
+ padding:20px;text-align:center;break-inside:avoid}
68
+ figure svg{max-width:100%;height:auto}
69
+ figcaption{margin-top:12px;color:var(--dim);font-size:13px;text-align:center}
70
+ .svg-ink{fill:var(--ink)} .svg-muted{fill:var(--dim)}
71
+ .svg-box{fill:var(--mass);stroke:var(--line-2);stroke-width:1}
72
+ .svg-box-spot{fill:var(--spot-soft);stroke:var(--spot);stroke-width:1.5}
73
+ .svg-line{stroke:var(--faint);stroke-width:1.5;fill:none}
74
+ .svg-line-spot{stroke:var(--spot);stroke-width:1.5;fill:none}
75
+ .svg-label{font:500 13px var(--font-body);fill:var(--ink)}
76
+ .svg-small{font:11px var(--font-mono);fill:var(--dim)}
77
+
78
+ .actions{position:fixed;top:16px;right:16px;z-index:50;display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end}
79
+ .actions button{display:inline-flex;gap:7px;align-items:center;padding:8px 14px;
80
+ border:1px solid var(--line-2);border-radius:0;background:var(--panel);color:var(--ink);
81
+ font:500 12px var(--font-mono);letter-spacing:.06em;cursor:pointer}
82
+ .actions button:hover{border-color:var(--spot);color:var(--spot)}
83
+ .actions button:focus-visible{outline:none;box-shadow:var(--ring)}
84
+
85
+ @media screen{
86
+ body{background-image:linear-gradient(var(--grid) 1px,transparent 1px),
87
+ linear-gradient(90deg,var(--grid) 1px,transparent 1px);background-size:72px 72px}
88
+ main{background:var(--bg);border-left:1px solid var(--line-2);border-right:1px solid var(--line-2);
89
+ padding-left:40px;padding-right:40px;min-height:100vh}
90
+ }
91
+ @media (max-width:640px){ main{padding:76px 16px 64px!important} .actions{top:12px;right:12px} }
92
+
93
+ @media print{
94
+ /* Dark in the PDF too. The @page margin is canvas and prints white, so the
95
+ page has none, a fixed layer covers the sheet, and the gutter lives on
96
+ <main>, repeated on every page fragment by box-decoration-break. */
97
+ *{-webkit-print-color-adjust:exact;print-color-adjust:exact}
98
+ html,body{background:var(--bg);color:var(--ink)}
99
+ @page{margin:0}
100
+ body::before{content:"";position:fixed;inset:0;background:var(--bg);z-index:-1}
101
+ .actions{display:none}
102
+ body{font-size:11pt}
103
+ main{padding:12mm 16mm;max-width:none;-webkit-box-decoration-break:clone;box-decoration-break:clone}
104
+ h1,h2,h3{break-after:avoid;break-inside:avoid}
105
+ p,li{orphans:3;widows:3}
106
+ /* Anything that can outgrow a page must be allowed to split. */
107
+ table,pre,section,div{break-inside:auto}
108
+ thead{display:table-header-group}
109
+ tfoot{display:table-footer-group}
110
+ tr,td,th,li,.card,.stat{break-inside:avoid}
111
+ figure,blockquote{break-inside:avoid}
112
+ figure svg,figure img{max-height:170mm}
113
+ a{color:inherit;border:0}
114
+ }
115
+ </style>
116
+ </head>
117
+ <body>
118
+ <div class="actions">
119
+ <button type="button" onclick="window.print()" aria-label="Download this page as PDF">PDF</button>
120
+ <button type="button" onclick="saveHtml()" aria-label="Download this page as an HTML file">HTML</button>
121
+ </div>
122
+ <script>
123
+ function saveHtml(){
124
+ var name=(location.pathname.split('/').pop()||'').replace(/\.html?$/,'')||'artifact';
125
+ var blob=new Blob(['<!DOCTYPE html>\n'+document.documentElement.outerHTML],{type:'text/html'});
126
+ var a=document.createElement('a');
127
+ a.href=URL.createObjectURL(blob); a.download=name+'.html';
128
+ document.body.appendChild(a); a.click(); a.remove();
129
+ setTimeout(function(){URL.revokeObjectURL(a.href)},1000);
130
+ }
131
+ </script>
132
+
133
+ <main>
134
+ <p class="eyebrow">{{EYEBROW}} · {{PROJECT_NAME}}</p>
135
+ <h1>{{TITLE}}</h1>
136
+ <p class="lede">{{DESCRIPTION}}</p>
137
+
138
+ <!-- CONTENT: replace this comment with the page. Sections under <h2>, a
139
+ <figure> with an inline SVG (the .svg-* classes) for every sequence,
140
+ branch or before/after, a <figcaption> that states the takeaway. -->
141
+
142
+ </main>
143
+ </body>
144
+ </html>