hlassiege/panopliepublic Fork
⑂ 3a8a782
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/panoplie.git
git clone ssh://git@rickub.com/hlassiege/panoplie.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat: add showcase site

hlassiege committed 2026-11-07T15:53:21+01:00 Browse files
3a8a782 parent: 3273cfe
modified package.json +2 -1
@@ -9,7 +9,8 @@
99 "build": "pnpm --filter @skilly/ui build",
1010 "start": "pnpm --filter skilly start",
1111 "test": "vitest run",
12- "typecheck": "tsc -p packages/core && tsc -p packages/server && tsc -p packages/cli"
12+ "typecheck": "tsc -p packages/core && tsc -p packages/server && tsc -p packages/cli",
13+ "site:build": "node site/build.mjs"
1314 },
1415 "devDependencies": {
1516 "@types/node": "^22.10.0",
@@ -9,7 +9,8 @@
9 "build": "pnpm --filter @skilly/ui build",9 "build": "pnpm --filter @skilly/ui build",
10 "start": "pnpm --filter skilly start",10 "start": "pnpm --filter skilly start",
11 "test": "vitest run",11 "test": "vitest run",
12- "typecheck": "tsc -p packages/core && tsc -p packages/server && tsc -p packages/cli"12+ "typecheck": "tsc -p packages/core && tsc -p packages/server && tsc -p packages/cli",
13+ "site:build": "node site/build.mjs"
13 },14 },
14 "devDependencies": {15 "devDependencies": {
15 "@types/node": "^22.10.0",16 "@types/node": "^22.10.0",
added site/build.mjs +15 -0
new file mode 100644
@@ -0,0 +1,15 @@
1+// Builds the showcase site into site/dist: the static files of site/public, plus the screenshots and
2+// the favicon, taken from the repository so they have a single source.
3+// No dependencies: `node site/build.mjs`, then open site/dist/index.html or serve the folder.
4+import { cpSync, rmSync } from 'node:fs'
5+import { fileURLToPath } from 'node:url'
6+
7+const root = (path) => fileURLToPath(new URL(`../${path}`, import.meta.url))
8+const dist = root('site/dist')
9+
10+rmSync(dist, { recursive: true, force: true })
11+cpSync(root('site/public'), dist, { recursive: true })
12+cpSync(root('media/screenshots'), `${dist}/screenshots`, { recursive: true })
13+cpSync(root('packages/ui/public/favicon.svg'), `${dist}/favicon.svg`)
14+
15+console.log(`Site built in ${dist}`)
new file mode 100644
@@ -0,0 +1,15 @@
1+// Builds the showcase site into site/dist: the static files of site/public, plus the screenshots and
2+// the favicon, taken from the repository so they have a single source.
3+// No dependencies: `node site/build.mjs`, then open site/dist/index.html or serve the folder.
4+import { cpSync, rmSync } from 'node:fs'
5+import { fileURLToPath } from 'node:url'
6+
7+const root = (path) => fileURLToPath(new URL(`../${path}`, import.meta.url))
8+const dist = root('site/dist')
9+
10+rmSync(dist, { recursive: true, force: true })
11+cpSync(root('site/public'), dist, { recursive: true })
12+cpSync(root('media/screenshots'), `${dist}/screenshots`, { recursive: true })
13+cpSync(root('packages/ui/public/favicon.svg'), `${dist}/favicon.svg`)
14+
15+console.log(`Site built in ${dist}`)
added site/public/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
new file mode 100644
Binary files /dev/null and b/site/public/fonts/ibm-plex-mono-latin-400-normal.woff2 differ
new file mode 100644
Binary files /dev/null and b/site/public/fonts/ibm-plex-mono-latin-400-normal.woff2 differBinary files /dev/null and b/site/public/fonts/ibm-plex-mono-latin-400-normal.woff2 differ
added site/public/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
new file mode 100644
Binary files /dev/null and b/site/public/fonts/ibm-plex-mono-latin-500-normal.woff2 differ
new file mode 100644
Binary files /dev/null and b/site/public/fonts/ibm-plex-mono-latin-500-normal.woff2 differBinary files /dev/null and b/site/public/fonts/ibm-plex-mono-latin-500-normal.woff2 differ
added site/public/fonts/instrument-sans-latin-400-normal.woff2 +0 -0
new file mode 100644
Binary files /dev/null and b/site/public/fonts/instrument-sans-latin-400-normal.woff2 differ
new file mode 100644
Binary files /dev/null and b/site/public/fonts/instrument-sans-latin-400-normal.woff2 differBinary files /dev/null and b/site/public/fonts/instrument-sans-latin-400-normal.woff2 differ
added site/public/fonts/instrument-sans-latin-500-normal.woff2 +0 -0
new file mode 100644
Binary files /dev/null and b/site/public/fonts/instrument-sans-latin-500-normal.woff2 differ
new file mode 100644
Binary files /dev/null and b/site/public/fonts/instrument-sans-latin-500-normal.woff2 differBinary files /dev/null and b/site/public/fonts/instrument-sans-latin-500-normal.woff2 differ
added site/public/fonts/instrument-sans-latin-600-normal.woff2 +0 -0
new file mode 100644
Binary files /dev/null and b/site/public/fonts/instrument-sans-latin-600-normal.woff2 differ
new file mode 100644
Binary files /dev/null and b/site/public/fonts/instrument-sans-latin-600-normal.woff2 differBinary files /dev/null and b/site/public/fonts/instrument-sans-latin-600-normal.woff2 differ
added site/public/index.html +193 -0
new file mode 100644
@@ -0,0 +1,193 @@
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>skilly: stop copying agent skills between projects</title>
7+ <meta name="description" content="One catalog for your Claude Code and Codex skills, rules and MCP servers. Each project gets only what it needs. Open source.">
8+ <link rel="canonical" href="https://skilly.eventuallycoding.com/">
9+ <meta name="color-scheme" content="light dark">
10+ <meta name="theme-color" content="#f5f4f1" media="(prefers-color-scheme: light)">
11+ <meta name="theme-color" content="#121110" media="(prefers-color-scheme: dark)">
12+
13+ <meta property="og:type" content="website">
14+ <meta property="og:url" content="https://skilly.eventuallycoding.com/">
15+ <meta property="og:title" content="skilly: stop copying agent skills between projects">
16+ <meta property="og:description" content="One catalog for your Claude Code and Codex skills, rules and MCP servers. Each project gets only what it needs.">
17+ <meta property="og:image" content="https://skilly.eventuallycoding.com/screenshots/project.png">
18+ <meta name="twitter:card" content="summary_large_image">
19+
20+ <link rel="icon" type="image/svg+xml" href="favicon.svg">
21+ <link rel="preload" href="fonts/ibm-plex-mono-latin-400-normal.woff2" as="font" type="font/woff2" crossorigin>
22+ <link rel="preload" href="fonts/instrument-sans-latin-400-normal.woff2" as="font" type="font/woff2" crossorigin>
23+ <link rel="stylesheet" href="styles.css">
24+ <script>document.documentElement.classList.add('js')</script>
25+ <script src="main.js" defer></script>
26+</head>
27+<body>
28+ <a class="skip" href="#main">Skip to content</a>
29+
30+ <header class="top">
31+ <a class="brand" href="./" aria-label="skilly, home">
32+ <span class="mark" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
33+ <span>skilly</span>
34+ </a>
35+ <nav class="top-nav" aria-label="Sections">
36+ <a href="#tour">Tour</a>
37+ <a href="#how">How it works</a>
38+ <a href="#try">Try it</a>
39+ </nav>
40+ <a class="btn btn-ink btn-sm" href="https://rickub.com/hlassiege/skilly">Source code</a>
41+ </header>
42+
43+ <main id="main">
44+ <section class="hero">
45+ <h1>Stop copying skills from project to&nbsp;project.</h1>
46+ <p class="lead">One catalog for your skills, rules and MCP servers.</p>
47+ <div class="actions">
48+ <a class="btn btn-ink" href="#try">Try it</a>
49+ <a class="btn btn-line" href="https://rickub.com/hlassiege/skilly">Source code</a>
50+ </div>
51+ </section>
52+
53+ <section class="tour" id="tour" aria-labelledby="tour-title">
54+ <h2 class="sr-only" id="tour-title">A tour of skilly</h2>
55+ <div class="tour-tabs" role="tablist" aria-label="Screens" aria-orientation="vertical">
56+ <button role="tab" id="tab-project" aria-controls="shot-project" aria-selected="true">
57+ <span class="tab-title">Pick what each project uses</span>
58+ <span class="tab-desc">Add a preset or a single skill. skilly writes the files.</span>
59+ </button>
60+ <button role="tab" id="tab-review" aria-controls="shot-review" aria-selected="false" tabindex="-1">
61+ <span class="tab-title">See every change first</span>
62+ <span class="tab-desc">Nothing is written until you apply. Edited a file by hand? skilly asks first.</span>
63+ </button>
64+ <button role="tab" id="tab-global" aria-controls="shot-global" aria-selected="false" tabindex="-1">
65+ <span class="tab-title">Slim down your global config</span>
66+ <span class="tab-desc">Move the skills you rarely use out of <code>~/.claude</code>.</span>
67+ </button>
68+ <button role="tab" id="tab-catalog" aria-controls="shot-catalog" aria-selected="false" tabindex="-1">
69+ <span class="tab-title">All your items in one place</span>
70+ <span class="tab-desc">Import what you already have, or write new ones.</span>
71+ </button>
72+ <button role="tab" id="tab-presets" aria-controls="shot-presets" aria-selected="false" tabindex="-1">
73+ <span class="tab-title">Bundle what goes together</span>
74+ <span class="tab-desc">A Nuxt preset, an SEO preset. One click per project.</span>
75+ </button>
76+ <button role="tab" id="tab-marketplaces" aria-controls="shot-marketplaces" aria-selected="false" tabindex="-1">
77+ <span class="tab-title">Grab skills from others</span>
78+ <span class="tab-desc">Anthropic, Vercel, or any git repo.</span>
79+ </button>
80+ </div>
81+ <div class="tour-shots">
82+ <div class="frame" id="shot-project" role="tabpanel" aria-labelledby="tab-project"><img src="screenshots/project.png" width="2880" height="1800" alt="A project in skilly with its presets and items" fetchpriority="high"></div>
83+ <div class="frame" id="shot-review" role="tabpanel" aria-labelledby="tab-review"><img src="screenshots/review.png" width="2880" height="1800" alt="The review screen listing the files that will change" loading="lazy"></div>
84+ <div class="frame" id="shot-global" role="tabpanel" aria-labelledby="tab-global"><img src="screenshots/global.png" width="2880" height="1800" alt="Global items with suggestions to move them to projects" loading="lazy"></div>
85+ <div class="frame" id="shot-catalog" role="tabpanel" aria-labelledby="tab-catalog"><img src="screenshots/catalog.png" width="2880" height="1800" alt="The catalog with a skill opened in the editor" loading="lazy"></div>
86+ <div class="frame" id="shot-presets" role="tabpanel" aria-labelledby="tab-presets"><img src="screenshots/presets.png" width="2880" height="1120" alt="Presets and the projects that use them" loading="lazy"></div>
87+ <div class="frame" id="shot-marketplaces" role="tabpanel" aria-labelledby="tab-marketplaces"><img src="screenshots/marketplaces.png" width="2880" height="1800" alt="Browsing the skills of a marketplace" loading="lazy"></div>
88+ </div>
89+ </section>
90+
91+ <section class="why" aria-labelledby="why-title">
92+ <h2 id="why-title">Agent setups get messy fast</h2>
93+ <div class="why-list">
94+ <div>
95+ <h3>Copies everywhere</h3>
96+ <p>You copy a skill to a new project. Now you have two versions.</p>
97+ </div>
98+ <div>
99+ <h3>Everything in global</h3>
100+ <p>Every skill loads in every session, even where it is useless.</p>
101+ </div>
102+ <div>
103+ <h3>No overview</h3>
104+ <p>Which skills do you have? Which project uses them? No idea.</p>
105+ </div>
106+ </div>
107+ </section>
108+
109+ <section class="how" id="how" aria-labelledby="how-title">
110+ <h2 id="how-title">One catalog, plain files in each project</h2>
111+ <p class="section-lead">Your projects keep working without skilly.</p>
112+
113+ <div class="flow" data-flow>
114+ <div class="panel cat">
115+ <div class="panel-head">
116+ <span class="name">catalog</span>
117+ <span class="path">git repo</span>
118+ </div>
119+ <ul class="items">
120+ <li><span class="tag">skill</span>nuxt</li>
121+ <li><span class="tag">skill</span>code-review</li>
122+ <li><span class="tag">skill</span>seo-audit</li>
123+ <li><span class="tag">rule</span>typescript</li>
124+ <li><span class="tag">rule</span>writing</li>
125+ <li><span class="tag">mcp</span>nuxt-docs</li>
126+ <li><span class="tag">preset</span>nuxt-saas</li>
127+ </ul>
128+ </div>
129+
130+ <div class="apply" aria-hidden="true"><span>apply</span></div>
131+
132+ <div class="projects">
133+ <div class="panel proj">
134+ <div class="panel-head"><span class="name">acme-shop</span><span class="agents">claude, codex</span></div>
135+ <ul class="files">
136+ <li style="--i:0">.claude/skills/nuxt/SKILL.md</li>
137+ <li style="--i:1">.agents/skills/nuxt/SKILL.md</li>
138+ <li style="--i:2">.claude/rules/typescript.md</li>
139+ <li style="--i:3">.mcp.json</li>
140+ </ul>
141+ </div>
142+ <div class="panel proj">
143+ <div class="panel-head"><span class="name">billing-api</span><span class="agents">codex</span></div>
144+ <ul class="files">
145+ <li style="--i:4">.agents/skills/code-review/SKILL.md</li>
146+ <li style="--i:5">AGENTS.md</li>
147+ <li style="--i:6">.codex/config.toml</li>
148+ </ul>
149+ </div>
150+ <div class="panel proj">
151+ <div class="panel-head"><span class="name">blog</span><span class="agents">claude</span></div>
152+ <ul class="files">
153+ <li style="--i:7">.claude/skills/seo-audit/SKILL.md</li>
154+ <li style="--i:8">.claude/rules/writing.md</li>
155+ </ul>
156+ </div>
157+ </div>
158+ </div>
159+
160+ <dl class="facts">
161+ <div>
162+ <dt>Skills on demand</dt>
163+ <dd>Installed, but invisible until you call them. No context wasted.</dd>
164+ </div>
165+ <div>
166+ <dt>Synced with git</dt>
167+ <dd>New machine? Clone your catalog and you are back.</dd>
168+ </div>
169+ <div>
170+ <dt>Your team needs nothing</dt>
171+ <dd>The files are committed with the project.</dd>
172+ </div>
173+ </dl>
174+ </section>
175+
176+ <section class="try" id="try" aria-labelledby="try-title">
177+ <h2 id="try-title">Try it</h2>
178+ <div class="code">
179+ <pre><code id="install-cmd">git clone https://rickub.com/hlassiege/skilly.git
180+cd skilly
181+pnpm install &amp;&amp; pnpm build &amp;&amp; pnpm start</code></pre>
182+ <button class="btn btn-line btn-sm copy" type="button" data-copy="install-cmd">Copy</button>
183+ </div>
184+ <p class="note"><a href="https://rickub.com/hlassiege/skilly">Read the README</a> for everything else.</p>
185+ </section>
186+ </main>
187+
188+ <footer class="foot">
189+ <p>Open source, MIT. Made by <a href="https://eventuallycoding.com">Hugo Lassiège</a>.</p>
190+ <a href="https://rickub.com/hlassiege/skilly">Source code</a>
191+ </footer>
192+</body>
193+</html>
new file mode 100644
@@ -0,0 +1,193 @@
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>skilly: stop copying agent skills between projects</title>
7+ <meta name="description" content="One catalog for your Claude Code and Codex skills, rules and MCP servers. Each project gets only what it needs. Open source.">
8+ <link rel="canonical" href="https://skilly.eventuallycoding.com/">
9+ <meta name="color-scheme" content="light dark">
10+ <meta name="theme-color" content="#f5f4f1" media="(prefers-color-scheme: light)">
11+ <meta name="theme-color" content="#121110" media="(prefers-color-scheme: dark)">
12+
13+ <meta property="og:type" content="website">
14+ <meta property="og:url" content="https://skilly.eventuallycoding.com/">
15+ <meta property="og:title" content="skilly: stop copying agent skills between projects">
16+ <meta property="og:description" content="One catalog for your Claude Code and Codex skills, rules and MCP servers. Each project gets only what it needs.">
17+ <meta property="og:image" content="https://skilly.eventuallycoding.com/screenshots/project.png">
18+ <meta name="twitter:card" content="summary_large_image">
19+
20+ <link rel="icon" type="image/svg+xml" href="favicon.svg">
21+ <link rel="preload" href="fonts/ibm-plex-mono-latin-400-normal.woff2" as="font" type="font/woff2" crossorigin>
22+ <link rel="preload" href="fonts/instrument-sans-latin-400-normal.woff2" as="font" type="font/woff2" crossorigin>
23+ <link rel="stylesheet" href="styles.css">
24+ <script>document.documentElement.classList.add('js')</script>
25+ <script src="main.js" defer></script>
26+</head>
27+<body>
28+ <a class="skip" href="#main">Skip to content</a>
29+
30+ <header class="top">
31+ <a class="brand" href="./" aria-label="skilly, home">
32+ <span class="mark" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
33+ <span>skilly</span>
34+ </a>
35+ <nav class="top-nav" aria-label="Sections">
36+ <a href="#tour">Tour</a>
37+ <a href="#how">How it works</a>
38+ <a href="#try">Try it</a>
39+ </nav>
40+ <a class="btn btn-ink btn-sm" href="https://rickub.com/hlassiege/skilly">Source code</a>
41+ </header>
42+
43+ <main id="main">
44+ <section class="hero">
45+ <h1>Stop copying skills from project to&nbsp;project.</h1>
46+ <p class="lead">One catalog for your skills, rules and MCP servers.</p>
47+ <div class="actions">
48+ <a class="btn btn-ink" href="#try">Try it</a>
49+ <a class="btn btn-line" href="https://rickub.com/hlassiege/skilly">Source code</a>
50+ </div>
51+ </section>
52+
53+ <section class="tour" id="tour" aria-labelledby="tour-title">
54+ <h2 class="sr-only" id="tour-title">A tour of skilly</h2>
55+ <div class="tour-tabs" role="tablist" aria-label="Screens" aria-orientation="vertical">
56+ <button role="tab" id="tab-project" aria-controls="shot-project" aria-selected="true">
57+ <span class="tab-title">Pick what each project uses</span>
58+ <span class="tab-desc">Add a preset or a single skill. skilly writes the files.</span>
59+ </button>
60+ <button role="tab" id="tab-review" aria-controls="shot-review" aria-selected="false" tabindex="-1">
61+ <span class="tab-title">See every change first</span>
62+ <span class="tab-desc">Nothing is written until you apply. Edited a file by hand? skilly asks first.</span>
63+ </button>
64+ <button role="tab" id="tab-global" aria-controls="shot-global" aria-selected="false" tabindex="-1">
65+ <span class="tab-title">Slim down your global config</span>
66+ <span class="tab-desc">Move the skills you rarely use out of <code>~/.claude</code>.</span>
67+ </button>
68+ <button role="tab" id="tab-catalog" aria-controls="shot-catalog" aria-selected="false" tabindex="-1">
69+ <span class="tab-title">All your items in one place</span>
70+ <span class="tab-desc">Import what you already have, or write new ones.</span>
71+ </button>
72+ <button role="tab" id="tab-presets" aria-controls="shot-presets" aria-selected="false" tabindex="-1">
73+ <span class="tab-title">Bundle what goes together</span>
74+ <span class="tab-desc">A Nuxt preset, an SEO preset. One click per project.</span>
75+ </button>
76+ <button role="tab" id="tab-marketplaces" aria-controls="shot-marketplaces" aria-selected="false" tabindex="-1">
77+ <span class="tab-title">Grab skills from others</span>
78+ <span class="tab-desc">Anthropic, Vercel, or any git repo.</span>
79+ </button>
80+ </div>
81+ <div class="tour-shots">
82+ <div class="frame" id="shot-project" role="tabpanel" aria-labelledby="tab-project"><img src="screenshots/project.png" width="2880" height="1800" alt="A project in skilly with its presets and items" fetchpriority="high"></div>
83+ <div class="frame" id="shot-review" role="tabpanel" aria-labelledby="tab-review"><img src="screenshots/review.png" width="2880" height="1800" alt="The review screen listing the files that will change" loading="lazy"></div>
84+ <div class="frame" id="shot-global" role="tabpanel" aria-labelledby="tab-global"><img src="screenshots/global.png" width="2880" height="1800" alt="Global items with suggestions to move them to projects" loading="lazy"></div>
85+ <div class="frame" id="shot-catalog" role="tabpanel" aria-labelledby="tab-catalog"><img src="screenshots/catalog.png" width="2880" height="1800" alt="The catalog with a skill opened in the editor" loading="lazy"></div>
86+ <div class="frame" id="shot-presets" role="tabpanel" aria-labelledby="tab-presets"><img src="screenshots/presets.png" width="2880" height="1120" alt="Presets and the projects that use them" loading="lazy"></div>
87+ <div class="frame" id="shot-marketplaces" role="tabpanel" aria-labelledby="tab-marketplaces"><img src="screenshots/marketplaces.png" width="2880" height="1800" alt="Browsing the skills of a marketplace" loading="lazy"></div>
88+ </div>
89+ </section>
90+
91+ <section class="why" aria-labelledby="why-title">
92+ <h2 id="why-title">Agent setups get messy fast</h2>
93+ <div class="why-list">
94+ <div>
95+ <h3>Copies everywhere</h3>
96+ <p>You copy a skill to a new project. Now you have two versions.</p>
97+ </div>
98+ <div>
99+ <h3>Everything in global</h3>
100+ <p>Every skill loads in every session, even where it is useless.</p>
101+ </div>
102+ <div>
103+ <h3>No overview</h3>
104+ <p>Which skills do you have? Which project uses them? No idea.</p>
105+ </div>
106+ </div>
107+ </section>
108+
109+ <section class="how" id="how" aria-labelledby="how-title">
110+ <h2 id="how-title">One catalog, plain files in each project</h2>
111+ <p class="section-lead">Your projects keep working without skilly.</p>
112+
113+ <div class="flow" data-flow>
114+ <div class="panel cat">
115+ <div class="panel-head">
116+ <span class="name">catalog</span>
117+ <span class="path">git repo</span>
118+ </div>
119+ <ul class="items">
120+ <li><span class="tag">skill</span>nuxt</li>
121+ <li><span class="tag">skill</span>code-review</li>
122+ <li><span class="tag">skill</span>seo-audit</li>
123+ <li><span class="tag">rule</span>typescript</li>
124+ <li><span class="tag">rule</span>writing</li>
125+ <li><span class="tag">mcp</span>nuxt-docs</li>
126+ <li><span class="tag">preset</span>nuxt-saas</li>
127+ </ul>
128+ </div>
129+
130+ <div class="apply" aria-hidden="true"><span>apply</span></div>
131+
132+ <div class="projects">
133+ <div class="panel proj">
134+ <div class="panel-head"><span class="name">acme-shop</span><span class="agents">claude, codex</span></div>
135+ <ul class="files">
136+ <li style="--i:0">.claude/skills/nuxt/SKILL.md</li>
137+ <li style="--i:1">.agents/skills/nuxt/SKILL.md</li>
138+ <li style="--i:2">.claude/rules/typescript.md</li>
139+ <li style="--i:3">.mcp.json</li>
140+ </ul>
141+ </div>
142+ <div class="panel proj">
143+ <div class="panel-head"><span class="name">billing-api</span><span class="agents">codex</span></div>
144+ <ul class="files">
145+ <li style="--i:4">.agents/skills/code-review/SKILL.md</li>
146+ <li style="--i:5">AGENTS.md</li>
147+ <li style="--i:6">.codex/config.toml</li>
148+ </ul>
149+ </div>
150+ <div class="panel proj">
151+ <div class="panel-head"><span class="name">blog</span><span class="agents">claude</span></div>
152+ <ul class="files">
153+ <li style="--i:7">.claude/skills/seo-audit/SKILL.md</li>
154+ <li style="--i:8">.claude/rules/writing.md</li>
155+ </ul>
156+ </div>
157+ </div>
158+ </div>
159+
160+ <dl class="facts">
161+ <div>
162+ <dt>Skills on demand</dt>
163+ <dd>Installed, but invisible until you call them. No context wasted.</dd>
164+ </div>
165+ <div>
166+ <dt>Synced with git</dt>
167+ <dd>New machine? Clone your catalog and you are back.</dd>
168+ </div>
169+ <div>
170+ <dt>Your team needs nothing</dt>
171+ <dd>The files are committed with the project.</dd>
172+ </div>
173+ </dl>
174+ </section>
175+
176+ <section class="try" id="try" aria-labelledby="try-title">
177+ <h2 id="try-title">Try it</h2>
178+ <div class="code">
179+ <pre><code id="install-cmd">git clone https://rickub.com/hlassiege/skilly.git
180+cd skilly
181+pnpm install &amp;&amp; pnpm build &amp;&amp; pnpm start</code></pre>
182+ <button class="btn btn-line btn-sm copy" type="button" data-copy="install-cmd">Copy</button>
183+ </div>
184+ <p class="note"><a href="https://rickub.com/hlassiege/skilly">Read the README</a> for everything else.</p>
185+ </section>
186+ </main>
187+
188+ <footer class="foot">
189+ <p>Open source, MIT. Made by <a href="https://eventuallycoding.com">Hugo Lassiège</a>.</p>
190+ <a href="https://rickub.com/hlassiege/skilly">Source code</a>
191+ </footer>
192+</body>
193+</html>
added site/public/main.js +59 -0
new file mode 100644
@@ -0,0 +1,59 @@
1+// Progressive enhancement only: without this file every screenshot is listed and the diagram shows its final state.
2+
3+// Tour: screenshots as tabs (WAI-ARIA tabs pattern; up/down on the vertical list, left/right on mobile)
4+const tabs = [...document.querySelectorAll('.tour-tabs [role="tab"]')]
5+
6+function select(tab, focus) {
7+ for (const t of tabs) {
8+ const on = t === tab
9+ t.setAttribute('aria-selected', String(on))
10+ t.tabIndex = on ? 0 : -1
11+ document.getElementById(t.getAttribute('aria-controls')).hidden = !on
12+ }
13+ if (focus) {
14+ tab.focus()
15+ tab.scrollIntoView({ block: 'nearest', inline: 'nearest' })
16+ }
17+}
18+
19+for (const tab of tabs) {
20+ tab.addEventListener('click', () => select(tab, false))
21+ tab.addEventListener('keydown', (e) => {
22+ const i = tabs.indexOf(tab)
23+ const next = { ArrowDown: i + 1, ArrowRight: i + 1, ArrowUp: i - 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 }[e.key]
24+ if (next === undefined) return
25+ e.preventDefault()
26+ select(tabs[(next + tabs.length) % tabs.length], true)
27+ })
28+}
29+if (tabs.length) select(tabs[0], false)
30+
31+// Copy buttons
32+for (const button of document.querySelectorAll('[data-copy]')) {
33+ button.addEventListener('click', async () => {
34+ const text = document.getElementById(button.dataset.copy).textContent
35+ try {
36+ await navigator.clipboard.writeText(text)
37+ button.textContent = 'Copied'
38+ } catch {
39+ button.textContent = 'Copy failed'
40+ }
41+ setTimeout(() => { button.textContent = 'Copy' }, 1600)
42+ })
43+}
44+
45+// Diagram: files get "written" once, the first time it comes into view
46+const flow = document.querySelector('[data-flow]')
47+if (flow) {
48+ if (!('IntersectionObserver' in window)) {
49+ flow.classList.add('done')
50+ } else {
51+ const observer = new IntersectionObserver((entries) => {
52+ if (entries.some((e) => e.isIntersecting)) {
53+ flow.classList.add('done')
54+ observer.disconnect()
55+ }
56+ }, { threshold: 0.4 })
57+ observer.observe(flow)
58+ }
59+}
new file mode 100644
@@ -0,0 +1,59 @@
1+// Progressive enhancement only: without this file every screenshot is listed and the diagram shows its final state.
2+
3+// Tour: screenshots as tabs (WAI-ARIA tabs pattern; up/down on the vertical list, left/right on mobile)
4+const tabs = [...document.querySelectorAll('.tour-tabs [role="tab"]')]
5+
6+function select(tab, focus) {
7+ for (const t of tabs) {
8+ const on = t === tab
9+ t.setAttribute('aria-selected', String(on))
10+ t.tabIndex = on ? 0 : -1
11+ document.getElementById(t.getAttribute('aria-controls')).hidden = !on
12+ }
13+ if (focus) {
14+ tab.focus()
15+ tab.scrollIntoView({ block: 'nearest', inline: 'nearest' })
16+ }
17+}
18+
19+for (const tab of tabs) {
20+ tab.addEventListener('click', () => select(tab, false))
21+ tab.addEventListener('keydown', (e) => {
22+ const i = tabs.indexOf(tab)
23+ const next = { ArrowDown: i + 1, ArrowRight: i + 1, ArrowUp: i - 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 }[e.key]
24+ if (next === undefined) return
25+ e.preventDefault()
26+ select(tabs[(next + tabs.length) % tabs.length], true)
27+ })
28+}
29+if (tabs.length) select(tabs[0], false)
30+
31+// Copy buttons
32+for (const button of document.querySelectorAll('[data-copy]')) {
33+ button.addEventListener('click', async () => {
34+ const text = document.getElementById(button.dataset.copy).textContent
35+ try {
36+ await navigator.clipboard.writeText(text)
37+ button.textContent = 'Copied'
38+ } catch {
39+ button.textContent = 'Copy failed'
40+ }
41+ setTimeout(() => { button.textContent = 'Copy' }, 1600)
42+ })
43+}
44+
45+// Diagram: files get "written" once, the first time it comes into view
46+const flow = document.querySelector('[data-flow]')
47+if (flow) {
48+ if (!('IntersectionObserver' in window)) {
49+ flow.classList.add('done')
50+ } else {
51+ const observer = new IntersectionObserver((entries) => {
52+ if (entries.some((e) => e.isIntersecting)) {
53+ flow.classList.add('done')
54+ observer.disconnect()
55+ }
56+ }, { threshold: 0.4 })
57+ observer.observe(flow)
58+ }
59+}
added site/public/styles.css +382 -0
new file mode 100644
@@ -0,0 +1,382 @@
1+/* skilly site. Tokens mirror the app (packages/ui/app/assets/css/main.css) so screenshots sit at home. */
2+
3+@font-face { font-family: 'Instrument Sans'; font-weight: 400; font-display: swap; src: url(fonts/instrument-sans-latin-400-normal.woff2) format('woff2'); }
4+@font-face { font-family: 'Instrument Sans'; font-weight: 500; font-display: swap; src: url(fonts/instrument-sans-latin-500-normal.woff2) format('woff2'); }
5+@font-face { font-family: 'Instrument Sans'; font-weight: 600; font-display: swap; src: url(fonts/instrument-sans-latin-600-normal.woff2) format('woff2'); }
6+@font-face { font-family: 'IBM Plex Mono'; font-weight: 400; font-display: swap; src: url(fonts/ibm-plex-mono-latin-400-normal.woff2) format('woff2'); }
7+@font-face { font-family: 'IBM Plex Mono'; font-weight: 500; font-display: swap; src: url(fonts/ibm-plex-mono-latin-500-normal.woff2) format('woff2'); }
8+
9+:root {
10+ --bg: #f5f4f1;
11+ --dot: #dcd8d0;
12+ --panel: #ffffff;
13+ --line: #e3e0da;
14+ --line2: #eeebe6;
15+ --hover: #f6f5f2;
16+ --text: #1c1b19;
17+ --muted: #6b6862;
18+ --faint: #8a867e;
19+ --ink: #1c1b19;
20+ --onink: #ffffff;
21+ --accent: #e2774c;
22+ --accent-text: #b4471d;
23+ --ok: oklch(0.6 0.13 155);
24+ --info: oklch(0.58 0.13 250);
25+ --ring: #ebe8e2;
26+
27+ --sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif;
28+ --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
29+ --page: 1120px;
30+ --gutter: clamp(20px, 4vw, 32px);
31+}
32+
33+@media (prefers-color-scheme: dark) {
34+ :root {
35+ --bg: #121110;
36+ --dot: #272521;
37+ --panel: #1a1917;
38+ --line: #2e2c28;
39+ --line2: #24221f;
40+ --hover: #22201d;
41+ --text: #eeece8;
42+ --muted: #a7a39b;
43+ --faint: #7d7970;
44+ --ink: #eeece8;
45+ --onink: #121110;
46+ --accent: #ec8a5f;
47+ --accent-text: #f1a07b;
48+ --ok: oklch(0.72 0.13 155);
49+ --info: oklch(0.72 0.12 250);
50+ --ring: #2e2c28;
51+ }
52+}
53+
54+/* ------------------------------------------------------------------ base */
55+*, *::before, *::after { box-sizing: border-box; }
56+
57+html { scroll-behavior: smooth; scroll-padding-top: 24px; }
58+
59+body {
60+ margin: 0;
61+ font-family: var(--sans);
62+ font-size: 1rem;
63+ line-height: 1.55;
64+ color: var(--text);
65+ background-color: var(--bg);
66+ background-image: radial-gradient(var(--dot) 1px, transparent 1.2px);
67+ background-size: 12px 12px;
68+ -webkit-font-smoothing: antialiased;
69+ text-rendering: optimizeLegibility;
70+}
71+
72+::selection { background: color-mix(in oklch, var(--accent) 28%, transparent); }
73+
74+h1, h2, h3, p, dl, dd, ul, figure { margin: 0; }
75+ul { padding: 0; list-style: none; }
76+
77+a { color: inherit; text-underline-offset: 3px; text-decoration-thickness: 1px; }
78+a:hover { color: var(--accent-text); }
79+
80+code {
81+ font-family: var(--mono);
82+ font-size: 0.875em;
83+ padding: 0.05em 0.35em;
84+ border-radius: 5px;
85+ background: var(--line2);
86+}
87+
88+:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
89+
90+.sr-only {
91+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
92+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
93+}
94+
95+.skip {
96+ position: absolute; left: 12px; top: -48px; z-index: 10;
97+ padding: 8px 14px; border-radius: 7px; background: var(--ink); color: var(--onink);
98+}
99+.skip:focus { top: 12px; }
100+
101+main > section,
102+.top,
103+.foot {
104+ width: min(100% - 2 * var(--gutter), var(--page));
105+ margin-inline: auto;
106+}
107+
108+main > section + section { margin-top: clamp(88px, 12vw, 144px); }
109+
110+h2 {
111+ font-family: var(--mono);
112+ font-weight: 400;
113+ font-size: clamp(1.5rem, 3vw, 2.125rem);
114+ line-height: 1.2;
115+ letter-spacing: -0.02em;
116+ max-width: 28ch;
117+ text-wrap: balance;
118+}
119+
120+h3 { font-size: 1.0625rem; font-weight: 600; line-height: 1.35; }
121+
122+.section-lead { margin-top: 16px; max-width: 62ch; color: var(--muted); font-size: 1.0625rem; }
123+
124+.note { margin-top: 16px; color: var(--muted); font-size: 0.9375rem; max-width: 70ch; }
125+
126+/* ------------------------------------------------------------------ buttons */
127+.btn {
128+ display: inline-flex; align-items: center; justify-content: center;
129+ height: 44px; padding: 0 20px;
130+ border-radius: 999px;
131+ font: 500 0.9375rem/1 var(--sans);
132+ text-decoration: none;
133+ white-space: nowrap;
134+ cursor: pointer;
135+ border: 0;
136+}
137+.btn-sm { height: 34px; padding: 0 14px; font-size: 0.875rem; }
138+.btn-ink { background: var(--ink); color: var(--onink); }
139+.btn-ink:hover { color: var(--onink); background: color-mix(in oklch, var(--ink) 86%, var(--bg)); }
140+.btn-line { background: var(--panel); color: var(--text); box-shadow: 0 0 0 1px var(--line); }
141+.btn-line:hover { color: var(--text); box-shadow: 0 0 0 1px var(--faint); }
142+
143+/* ------------------------------------------------------------------ header */
144+.top {
145+ display: flex; align-items: center; gap: 24px;
146+ padding-block: 20px;
147+}
148+
149+.brand {
150+ display: inline-flex; align-items: center; gap: 10px;
151+ font-weight: 600; font-size: 1.125rem; text-decoration: none;
152+}
153+.brand:hover { color: inherit; }
154+
155+/* The in-app logo mark: a 2x2 grid, one clay square */
156+.mark {
157+ display: grid; grid-template-columns: 1fr 1fr; gap: 3px;
158+ width: 28px; height: 28px; padding: 6px;
159+ border-radius: 8px; background: var(--ink);
160+}
161+.mark i { border-radius: 1px; }
162+.mark i:nth-child(1), .mark i:nth-child(4) { background: var(--onink); }
163+.mark i:nth-child(3) { background: var(--accent); }
164+
165+.top-nav { display: flex; gap: 4px; margin-left: auto; }
166+.top-nav a {
167+ padding: 6px 12px; border-radius: 999px;
168+ color: var(--muted); text-decoration: none; font-size: 0.9375rem;
169+}
170+.top-nav a:hover { color: var(--text); background: var(--line2); }
171+
172+@media (max-width: 720px) {
173+ .top-nav { display: none; }
174+ .top .btn { margin-left: auto; }
175+}
176+
177+/* ------------------------------------------------------------------ hero */
178+.hero { padding-top: clamp(56px, 10vw, 120px); }
179+
180+.hero h1 {
181+ font-family: var(--mono);
182+ font-weight: 400;
183+ font-size: clamp(2.125rem, 5.6vw, 4rem);
184+ line-height: 1.06;
185+ letter-spacing: -0.035em;
186+ max-width: 19ch;
187+ text-wrap: balance;
188+}
189+
190+.lead {
191+ margin-top: 28px;
192+ max-width: 58ch;
193+ font-size: clamp(1.0625rem, 1.6vw, 1.1875rem);
194+ color: var(--muted);
195+}
196+.lead strong { color: var(--text); font-weight: 500; }
197+
198+.actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 36px; }
199+
200+/* ------------------------------------------------------------------ tour: tabs on the left, screenshot on the right */
201+.hero + .tour { margin-top: clamp(56px, 8vw, 88px); }
202+
203+[hidden] { display: none !important; }
204+
205+.tour {
206+ display: grid;
207+ grid-template-columns: minmax(0, 300px) minmax(0, 1fr);
208+ gap: 40px;
209+ align-items: start;
210+}
211+
212+.tour-tabs { display: flex; flex-direction: column; gap: 4px; }
213+
214+.tour-tabs button {
215+ display: block;
216+ width: 100%;
217+ padding: 14px 16px;
218+ border: 0; border-radius: 14px;
219+ background: transparent;
220+ color: var(--text);
221+ font: inherit;
222+ text-align: left;
223+ cursor: pointer;
224+}
225+.tab-title { display: block; font-weight: 600; line-height: 1.35; color: var(--muted); }
226+.tab-desc { display: block; margin-top: 4px; font-size: 0.9375rem; line-height: 1.45; color: var(--faint); }
227+
228+.tour-tabs button:hover .tab-title { color: var(--text); }
229+.tour-tabs button[aria-selected="true"] { background: var(--panel); box-shadow: 0 0 0 1px var(--ring); }
230+.tour-tabs button[aria-selected="true"] .tab-title { color: var(--text); }
231+.tour-tabs button[aria-selected="true"] .tab-desc { color: var(--muted); }
232+
233+.tour-shots { display: grid; gap: 24px; }
234+
235+.frame {
236+ aspect-ratio: 2880 / 1800;
237+ border-radius: 20px;
238+ overflow: hidden;
239+ background: #f5f4f1; /* screenshots are light: keep the frame light in dark mode too */
240+ box-shadow: 0 0 0 1px var(--ring), 0 24px 60px -24px rgba(28, 27, 25, 0.25);
241+}
242+.frame img { display: block; width: 100%; height: 100%; object-fit: contain; object-position: top; }
243+
244+/* Small screens: titles become a scrolling row above the screenshot */
245+@media (max-width: 900px) {
246+ .tour { grid-template-columns: 1fr; gap: 16px; }
247+ .tour-tabs { flex-direction: row; overflow-x: auto; scrollbar-width: none; margin-inline: calc(-1 * var(--gutter)); padding-inline: var(--gutter); }
248+ .tour-tabs::-webkit-scrollbar { display: none; }
249+ .tour-tabs button { flex: none; width: auto; padding: 9px 14px; border-radius: 999px; }
250+ .tab-desc { display: none; }
251+ .frame { border-radius: 12px; }
252+}
253+
254+/* ------------------------------------------------------------------ why */
255+.why-list {
256+ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px;
257+ margin-top: 40px;
258+}
259+.why-list > div { padding-top: 20px; border-top: 1px solid var(--line); }
260+.why-list h3 { display: flex; align-items: center; gap: 10px; }
261+.why-list h3::before {
262+ content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
263+}
264+.why-list p { margin-top: 8px; color: var(--muted); }
265+
266+@media (max-width: 820px) { .why-list { grid-template-columns: 1fr; gap: 24px; } }
267+
268+/* ------------------------------------------------------------------ how: the flow diagram */
269+.flow {
270+ display: grid;
271+ grid-template-columns: minmax(0, 0.9fr) 72px minmax(0, 1.4fr);
272+ align-items: center;
273+ margin-top: 48px;
274+}
275+
276+.panel {
277+ background: var(--panel);
278+ border-radius: 20px;
279+ box-shadow: 0 0 0 1px var(--ring);
280+ padding: 20px 22px;
281+}
282+
283+.panel-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
284+.panel-head .name { font-family: var(--mono); font-size: 1.125rem; letter-spacing: -0.01em; }
285+.panel-head .path,
286+.panel-head .agents { font-family: var(--mono); font-size: 0.8125rem; color: var(--faint); }
287+
288+.items { margin-top: 16px; display: grid; gap: 2px; }
289+.items li {
290+ display: flex; align-items: baseline; gap: 10px;
291+ padding: 7px 10px; border-radius: 9px;
292+ font-size: 0.9375rem;
293+}
294+.items li:nth-child(odd) { background: var(--hover); }
295+
296+.tag {
297+ flex: none; min-width: 52px;
298+ font-family: var(--mono); font-size: 0.75rem; color: var(--faint);
299+}
300+
301+.apply { position: relative; align-self: stretch; display: grid; place-items: center; }
302+.apply::before {
303+ content: ""; position: absolute; top: 12%; bottom: 12%; left: 50%;
304+ border-left: 1px dashed var(--line);
305+}
306+.apply span {
307+ position: relative;
308+ padding: 4px 10px; border-radius: 999px;
309+ font-family: var(--mono); font-size: 0.75rem;
310+ background: var(--ink); color: var(--onink);
311+}
312+
313+.projects { display: grid; gap: 14px; }
314+.proj { padding-block: 16px; }
315+
316+.files { margin-top: 10px; display: grid; gap: 4px; }
317+.files li {
318+ display: flex; align-items: center; gap: 10px;
319+ font-family: var(--mono); font-size: 0.8125rem;
320+ overflow-wrap: anywhere;
321+}
322+.files li::before {
323+ content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--ok);
324+}
325+
326+/* The one orchestrated moment: files go from "will add" (blue, faded) to written (green), in order */
327+.js .flow .files li,
328+.js .flow .files li::before {
329+ transition-property: opacity, background-color;
330+ transition-duration: 320ms;
331+ transition-timing-function: ease-out;
332+ transition-delay: calc(500ms + var(--i) * 140ms);
333+}
334+.js .flow:not(.done) .files li { opacity: 0.4; }
335+.js .flow:not(.done) .files li::before { background: var(--info); }
336+
337+@media (max-width: 820px) {
338+ .flow { grid-template-columns: 1fr; }
339+ .apply { height: 64px; }
340+ .apply::before { top: 0; bottom: 0; }
341+}
342+
343+.facts {
344+ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px;
345+ margin-top: 64px;
346+}
347+.facts > div { padding-top: 20px; border-top: 1px solid var(--line); }
348+.facts dt { font-weight: 600; font-size: 1.0625rem; }
349+.facts dd { margin-top: 6px; color: var(--muted); }
350+
351+@media (max-width: 820px) { .facts { grid-template-columns: 1fr; gap: 24px; } }
352+
353+/* ------------------------------------------------------------------ try it */
354+.code {
355+ position: relative;
356+ margin-top: 28px;
357+ max-width: 720px;
358+ border-radius: 16px;
359+ background: var(--ink);
360+ color: var(--onink);
361+}
362+.code pre { margin: 0; padding: 22px 24px; overflow-x: auto; }
363+.code code {
364+ background: none; padding: 0;
365+ font-size: 0.9375rem; line-height: 1.7;
366+}
367+.code .copy { position: absolute; top: 14px; right: 14px; }
368+
369+/* ------------------------------------------------------------------ footer */
370+.foot {
371+ display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px 32px;
372+ margin-top: clamp(88px, 12vw, 144px);
373+ padding-block: 28px 40px;
374+ border-top: 1px solid var(--line);
375+ color: var(--muted); font-size: 0.9375rem;
376+}
377+
378+/* ------------------------------------------------------------------ motion */
379+@media (prefers-reduced-motion: reduce) {
380+ html { scroll-behavior: auto; }
381+ *, *::before, *::after { transition: none !important; }
382+}
new file mode 100644
@@ -0,0 +1,382 @@
1+/* skilly site. Tokens mirror the app (packages/ui/app/assets/css/main.css) so screenshots sit at home. */
2+
3+@font-face { font-family: 'Instrument Sans'; font-weight: 400; font-display: swap; src: url(fonts/instrument-sans-latin-400-normal.woff2) format('woff2'); }
4+@font-face { font-family: 'Instrument Sans'; font-weight: 500; font-display: swap; src: url(fonts/instrument-sans-latin-500-normal.woff2) format('woff2'); }
5+@font-face { font-family: 'Instrument Sans'; font-weight: 600; font-display: swap; src: url(fonts/instrument-sans-latin-600-normal.woff2) format('woff2'); }
6+@font-face { font-family: 'IBM Plex Mono'; font-weight: 400; font-display: swap; src: url(fonts/ibm-plex-mono-latin-400-normal.woff2) format('woff2'); }
7+@font-face { font-family: 'IBM Plex Mono'; font-weight: 500; font-display: swap; src: url(fonts/ibm-plex-mono-latin-500-normal.woff2) format('woff2'); }
8+
9+:root {
10+ --bg: #f5f4f1;
11+ --dot: #dcd8d0;
12+ --panel: #ffffff;
13+ --line: #e3e0da;
14+ --line2: #eeebe6;
15+ --hover: #f6f5f2;
16+ --text: #1c1b19;
17+ --muted: #6b6862;
18+ --faint: #8a867e;
19+ --ink: #1c1b19;
20+ --onink: #ffffff;
21+ --accent: #e2774c;
22+ --accent-text: #b4471d;
23+ --ok: oklch(0.6 0.13 155);
24+ --info: oklch(0.58 0.13 250);
25+ --ring: #ebe8e2;
26+
27+ --sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif;
28+ --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
29+ --page: 1120px;
30+ --gutter: clamp(20px, 4vw, 32px);
31+}
32+
33+@media (prefers-color-scheme: dark) {
34+ :root {
35+ --bg: #121110;
36+ --dot: #272521;
37+ --panel: #1a1917;
38+ --line: #2e2c28;
39+ --line2: #24221f;
40+ --hover: #22201d;
41+ --text: #eeece8;
42+ --muted: #a7a39b;
43+ --faint: #7d7970;
44+ --ink: #eeece8;
45+ --onink: #121110;
46+ --accent: #ec8a5f;
47+ --accent-text: #f1a07b;
48+ --ok: oklch(0.72 0.13 155);
49+ --info: oklch(0.72 0.12 250);
50+ --ring: #2e2c28;
51+ }
52+}
53+
54+/* ------------------------------------------------------------------ base */
55+*, *::before, *::after { box-sizing: border-box; }
56+
57+html { scroll-behavior: smooth; scroll-padding-top: 24px; }
58+
59+body {
60+ margin: 0;
61+ font-family: var(--sans);
62+ font-size: 1rem;
63+ line-height: 1.55;
64+ color: var(--text);
65+ background-color: var(--bg);
66+ background-image: radial-gradient(var(--dot) 1px, transparent 1.2px);
67+ background-size: 12px 12px;
68+ -webkit-font-smoothing: antialiased;
69+ text-rendering: optimizeLegibility;
70+}
71+
72+::selection { background: color-mix(in oklch, var(--accent) 28%, transparent); }
73+
74+h1, h2, h3, p, dl, dd, ul, figure { margin: 0; }
75+ul { padding: 0; list-style: none; }
76+
77+a { color: inherit; text-underline-offset: 3px; text-decoration-thickness: 1px; }
78+a:hover { color: var(--accent-text); }
79+
80+code {
81+ font-family: var(--mono);
82+ font-size: 0.875em;
83+ padding: 0.05em 0.35em;
84+ border-radius: 5px;
85+ background: var(--line2);
86+}
87+
88+:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
89+
90+.sr-only {
91+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
92+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
93+}
94+
95+.skip {
96+ position: absolute; left: 12px; top: -48px; z-index: 10;
97+ padding: 8px 14px; border-radius: 7px; background: var(--ink); color: var(--onink);
98+}
99+.skip:focus { top: 12px; }
100+
101+main > section,
102+.top,
103+.foot {
104+ width: min(100% - 2 * var(--gutter), var(--page));
105+ margin-inline: auto;
106+}
107+
108+main > section + section { margin-top: clamp(88px, 12vw, 144px); }
109+
110+h2 {
111+ font-family: var(--mono);
112+ font-weight: 400;
113+ font-size: clamp(1.5rem, 3vw, 2.125rem);
114+ line-height: 1.2;
115+ letter-spacing: -0.02em;
116+ max-width: 28ch;
117+ text-wrap: balance;
118+}
119+
120+h3 { font-size: 1.0625rem; font-weight: 600; line-height: 1.35; }
121+
122+.section-lead { margin-top: 16px; max-width: 62ch; color: var(--muted); font-size: 1.0625rem; }
123+
124+.note { margin-top: 16px; color: var(--muted); font-size: 0.9375rem; max-width: 70ch; }
125+
126+/* ------------------------------------------------------------------ buttons */
127+.btn {
128+ display: inline-flex; align-items: center; justify-content: center;
129+ height: 44px; padding: 0 20px;
130+ border-radius: 999px;
131+ font: 500 0.9375rem/1 var(--sans);
132+ text-decoration: none;
133+ white-space: nowrap;
134+ cursor: pointer;
135+ border: 0;
136+}
137+.btn-sm { height: 34px; padding: 0 14px; font-size: 0.875rem; }
138+.btn-ink { background: var(--ink); color: var(--onink); }
139+.btn-ink:hover { color: var(--onink); background: color-mix(in oklch, var(--ink) 86%, var(--bg)); }
140+.btn-line { background: var(--panel); color: var(--text); box-shadow: 0 0 0 1px var(--line); }
141+.btn-line:hover { color: var(--text); box-shadow: 0 0 0 1px var(--faint); }
142+
143+/* ------------------------------------------------------------------ header */
144+.top {
145+ display: flex; align-items: center; gap: 24px;
146+ padding-block: 20px;
147+}
148+
149+.brand {
150+ display: inline-flex; align-items: center; gap: 10px;
151+ font-weight: 600; font-size: 1.125rem; text-decoration: none;
152+}
153+.brand:hover { color: inherit; }
154+
155+/* The in-app logo mark: a 2x2 grid, one clay square */
156+.mark {
157+ display: grid; grid-template-columns: 1fr 1fr; gap: 3px;
158+ width: 28px; height: 28px; padding: 6px;
159+ border-radius: 8px; background: var(--ink);
160+}
161+.mark i { border-radius: 1px; }
162+.mark i:nth-child(1), .mark i:nth-child(4) { background: var(--onink); }
163+.mark i:nth-child(3) { background: var(--accent); }
164+
165+.top-nav { display: flex; gap: 4px; margin-left: auto; }
166+.top-nav a {
167+ padding: 6px 12px; border-radius: 999px;
168+ color: var(--muted); text-decoration: none; font-size: 0.9375rem;
169+}
170+.top-nav a:hover { color: var(--text); background: var(--line2); }
171+
172+@media (max-width: 720px) {
173+ .top-nav { display: none; }
174+ .top .btn { margin-left: auto; }
175+}
176+
177+/* ------------------------------------------------------------------ hero */
178+.hero { padding-top: clamp(56px, 10vw, 120px); }
179+
180+.hero h1 {
181+ font-family: var(--mono);
182+ font-weight: 400;
183+ font-size: clamp(2.125rem, 5.6vw, 4rem);
184+ line-height: 1.06;
185+ letter-spacing: -0.035em;
186+ max-width: 19ch;
187+ text-wrap: balance;
188+}
189+
190+.lead {
191+ margin-top: 28px;
192+ max-width: 58ch;
193+ font-size: clamp(1.0625rem, 1.6vw, 1.1875rem);
194+ color: var(--muted);
195+}
196+.lead strong { color: var(--text); font-weight: 500; }
197+
198+.actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 36px; }
199+
200+/* ------------------------------------------------------------------ tour: tabs on the left, screenshot on the right */
201+.hero + .tour { margin-top: clamp(56px, 8vw, 88px); }
202+
203+[hidden] { display: none !important; }
204+
205+.tour {
206+ display: grid;
207+ grid-template-columns: minmax(0, 300px) minmax(0, 1fr);
208+ gap: 40px;
209+ align-items: start;
210+}
211+
212+.tour-tabs { display: flex; flex-direction: column; gap: 4px; }
213+
214+.tour-tabs button {
215+ display: block;
216+ width: 100%;
217+ padding: 14px 16px;
218+ border: 0; border-radius: 14px;
219+ background: transparent;
220+ color: var(--text);
221+ font: inherit;
222+ text-align: left;
223+ cursor: pointer;
224+}
225+.tab-title { display: block; font-weight: 600; line-height: 1.35; color: var(--muted); }
226+.tab-desc { display: block; margin-top: 4px; font-size: 0.9375rem; line-height: 1.45; color: var(--faint); }
227+
228+.tour-tabs button:hover .tab-title { color: var(--text); }
229+.tour-tabs button[aria-selected="true"] { background: var(--panel); box-shadow: 0 0 0 1px var(--ring); }
230+.tour-tabs button[aria-selected="true"] .tab-title { color: var(--text); }
231+.tour-tabs button[aria-selected="true"] .tab-desc { color: var(--muted); }
232+
233+.tour-shots { display: grid; gap: 24px; }
234+
235+.frame {
236+ aspect-ratio: 2880 / 1800;
237+ border-radius: 20px;
238+ overflow: hidden;
239+ background: #f5f4f1; /* screenshots are light: keep the frame light in dark mode too */
240+ box-shadow: 0 0 0 1px var(--ring), 0 24px 60px -24px rgba(28, 27, 25, 0.25);
241+}
242+.frame img { display: block; width: 100%; height: 100%; object-fit: contain; object-position: top; }
243+
244+/* Small screens: titles become a scrolling row above the screenshot */
245+@media (max-width: 900px) {
246+ .tour { grid-template-columns: 1fr; gap: 16px; }
247+ .tour-tabs { flex-direction: row; overflow-x: auto; scrollbar-width: none; margin-inline: calc(-1 * var(--gutter)); padding-inline: var(--gutter); }
248+ .tour-tabs::-webkit-scrollbar { display: none; }
249+ .tour-tabs button { flex: none; width: auto; padding: 9px 14px; border-radius: 999px; }
250+ .tab-desc { display: none; }
251+ .frame { border-radius: 12px; }
252+}
253+
254+/* ------------------------------------------------------------------ why */
255+.why-list {
256+ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px;
257+ margin-top: 40px;
258+}
259+.why-list > div { padding-top: 20px; border-top: 1px solid var(--line); }
260+.why-list h3 { display: flex; align-items: center; gap: 10px; }
261+.why-list h3::before {
262+ content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
263+}
264+.why-list p { margin-top: 8px; color: var(--muted); }
265+
266+@media (max-width: 820px) { .why-list { grid-template-columns: 1fr; gap: 24px; } }
267+
268+/* ------------------------------------------------------------------ how: the flow diagram */
269+.flow {
270+ display: grid;
271+ grid-template-columns: minmax(0, 0.9fr) 72px minmax(0, 1.4fr);
272+ align-items: center;
273+ margin-top: 48px;
274+}
275+
276+.panel {
277+ background: var(--panel);
278+ border-radius: 20px;
279+ box-shadow: 0 0 0 1px var(--ring);
280+ padding: 20px 22px;
281+}
282+
283+.panel-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
284+.panel-head .name { font-family: var(--mono); font-size: 1.125rem; letter-spacing: -0.01em; }
285+.panel-head .path,
286+.panel-head .agents { font-family: var(--mono); font-size: 0.8125rem; color: var(--faint); }
287+
288+.items { margin-top: 16px; display: grid; gap: 2px; }
289+.items li {
290+ display: flex; align-items: baseline; gap: 10px;
291+ padding: 7px 10px; border-radius: 9px;
292+ font-size: 0.9375rem;
293+}
294+.items li:nth-child(odd) { background: var(--hover); }
295+
296+.tag {
297+ flex: none; min-width: 52px;
298+ font-family: var(--mono); font-size: 0.75rem; color: var(--faint);
299+}
300+
301+.apply { position: relative; align-self: stretch; display: grid; place-items: center; }
302+.apply::before {
303+ content: ""; position: absolute; top: 12%; bottom: 12%; left: 50%;
304+ border-left: 1px dashed var(--line);
305+}
306+.apply span {
307+ position: relative;
308+ padding: 4px 10px; border-radius: 999px;
309+ font-family: var(--mono); font-size: 0.75rem;
310+ background: var(--ink); color: var(--onink);
311+}
312+
313+.projects { display: grid; gap: 14px; }
314+.proj { padding-block: 16px; }
315+
316+.files { margin-top: 10px; display: grid; gap: 4px; }
317+.files li {
318+ display: flex; align-items: center; gap: 10px;
319+ font-family: var(--mono); font-size: 0.8125rem;
320+ overflow-wrap: anywhere;
321+}
322+.files li::before {
323+ content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--ok);
324+}
325+
326+/* The one orchestrated moment: files go from "will add" (blue, faded) to written (green), in order */
327+.js .flow .files li,
328+.js .flow .files li::before {
329+ transition-property: opacity, background-color;
330+ transition-duration: 320ms;
331+ transition-timing-function: ease-out;
332+ transition-delay: calc(500ms + var(--i) * 140ms);
333+}
334+.js .flow:not(.done) .files li { opacity: 0.4; }
335+.js .flow:not(.done) .files li::before { background: var(--info); }
336+
337+@media (max-width: 820px) {
338+ .flow { grid-template-columns: 1fr; }
339+ .apply { height: 64px; }
340+ .apply::before { top: 0; bottom: 0; }
341+}
342+
343+.facts {
344+ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px;
345+ margin-top: 64px;
346+}
347+.facts > div { padding-top: 20px; border-top: 1px solid var(--line); }
348+.facts dt { font-weight: 600; font-size: 1.0625rem; }
349+.facts dd { margin-top: 6px; color: var(--muted); }
350+
351+@media (max-width: 820px) { .facts { grid-template-columns: 1fr; gap: 24px; } }
352+
353+/* ------------------------------------------------------------------ try it */
354+.code {
355+ position: relative;
356+ margin-top: 28px;
357+ max-width: 720px;
358+ border-radius: 16px;
359+ background: var(--ink);
360+ color: var(--onink);
361+}
362+.code pre { margin: 0; padding: 22px 24px; overflow-x: auto; }
363+.code code {
364+ background: none; padding: 0;
365+ font-size: 0.9375rem; line-height: 1.7;
366+}
367+.code .copy { position: absolute; top: 14px; right: 14px; }
368+
369+/* ------------------------------------------------------------------ footer */
370+.foot {
371+ display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px 32px;
372+ margin-top: clamp(88px, 12vw, 144px);
373+ padding-block: 28px 40px;
374+ border-top: 1px solid var(--line);
375+ color: var(--muted); font-size: 0.9375rem;
376+}
377+
378+/* ------------------------------------------------------------------ motion */
379+@media (prefers-reduced-motion: reduce) {
380+ html { scroll-behavior: auto; }
381+ *, *::before, *::after { transition: none !important; }
382+}