diff --git a/.gitignore b/.gitignore
index 2e7b3b0..b14cbc4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.DS_Store
+.claude/settings.local.json
.superpowers
.brainstorming
.worktrees
diff --git a/README.md b/README.md
index 86a15f7..717b04b 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,38 @@
+
+
+
+
# Synthpunk
High-energy synthwave-inspired color themes for VS Code, Zed, Neovim, WezTerm, and Starship. Four variants: Pastel Dark/Light and Neon Dark/Light.
-
+
+
+## Preview
+
+Syntax highlighting shown in the **Neon Dark** variant:
+
+### React
+
+
+
+### Python
+
+
+
+### Rust
+
+
+
+### C#
+
+
+
+### Markdown
+
+
+
+Full palette swatches for every variant: [Pastel Dark](assets/palette-preview-dark.png) · [Pastel Light](assets/palette-preview-light.png) · [Neon Dark](assets/palette-preview-neon-dark.png) · [Neon Light](assets/palette-preview-neon-light.png). Preview images are generated by [`assets/generate/`](assets/generate/).
## Installation
diff --git a/assets/generate/README.md b/assets/generate/README.md
new file mode 100644
index 0000000..b222c17
--- /dev/null
+++ b/assets/generate/README.md
@@ -0,0 +1,40 @@
+# Preview image generator
+
+Generates the marketing images used by the top-level [`README.md`](../../README.md):
+
+| Output (`assets/`) | What it is |
+| -------------------- | ----------------------------------------------------------------- |
+| `icon.png` | Angular-synthwave neon icon (1024×1024) |
+| `hero.png` | The same snippet rendered across all four variants (2×2 grid) |
+| `sample-react.png` | React/TSX preview, Neon Dark |
+| `sample-python.png` | Python preview, Neon Dark |
+| `sample-rust.png` | Rust preview, Neon Dark |
+| `sample-csharp.png` | C# preview, Neon Dark |
+| `sample-markdown.png`| Markdown preview, Neon Dark |
+
+Highlighting is done with [shiki](https://shiki.style) loaded with the repo's own
+generated VS Code themes (`themes/vscode/themes/*.json`), so the colors match the
+editor exactly. The highlighted HTML is screenshotted with the system Chrome via
+`puppeteer-core`.
+
+## Regenerate
+
+Run after changing palette colors (and rebuilding the VS Code themes with
+`bun run build` from the repo root):
+
+```sh
+cd assets/generate
+bun install
+bun run generate # or: bun run generate.ts
+```
+
+Requires a Chromium-based browser. The script auto-detects Google Chrome, Chromium,
+and Edge in the usual macOS/Linux locations; set `CHROME_PATH` to override:
+
+```sh
+CHROME_PATH="/path/to/chrome" bun run generate
+```
+
+The code shown in each image lives in [`snippets/`](snippets/) — edit those files to
+change the sample code. This directory is excluded from the repo's `tsc`/biome checks
+(see `tsconfig.json` and `biome.json`); its `node_modules/` is git-ignored.
diff --git a/assets/generate/bun.lock b/assets/generate/bun.lock
new file mode 100644
index 0000000..0e2adea
--- /dev/null
+++ b/assets/generate/bun.lock
@@ -0,0 +1,156 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "synthpunk-preview-generator",
+ "dependencies": {
+ "puppeteer-core": "^25.1.0",
+ "shiki": "^4.2.0",
+ },
+ },
+ },
+ "packages": {
+ "@puppeteer/browsers": ["@puppeteer/browsers@3.0.4", "", { "dependencies": { "modern-tar": "^0.7.6", "yargs": "^17.7.2" }, "peerDependencies": { "proxy-agent": ">=8.0.1" }, "optionalPeers": ["proxy-agent"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-HGM8iAmGTf+Y7t0373szVbTmt3d7vPkYL/1bpOkOFO0YUYLgSeuYBCzESklogNPvOBnZ/MRD5f07OkpqH1trtA=="],
+
+ "@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="],
+
+ "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og=="],
+
+ "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g=="],
+
+ "@shikijs/langs": ["@shikijs/langs@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0" } }, "sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ=="],
+
+ "@shikijs/primitive": ["@shikijs/primitive@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA=="],
+
+ "@shikijs/themes": ["@shikijs/themes@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0" } }, "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w=="],
+
+ "@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="],
+
+ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
+
+ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
+
+ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
+
+ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+
+ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="],
+
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
+
+ "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
+
+ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
+
+ "chromium-bidi": ["chromium-bidi@16.0.1", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA=="],
+
+ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
+
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
+
+ "devtools-protocol": ["devtools-protocol@0.0.1624250", "", {}, "sha512-YFAat/lOiIk0ARmBweG+ygrEcbZrq5B9urRyUoeQKp53MlidHXE2TmTbxKcaXoQj7u/aX+jebDO4BW55rs0WwA=="],
+
+ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
+
+ "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
+
+ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
+
+ "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
+
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+ "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
+
+ "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
+
+ "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
+
+ "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
+
+ "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
+
+ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
+
+ "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
+
+ "modern-tar": ["modern-tar@0.7.6", "", {}, "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg=="],
+
+ "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
+
+ "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
+
+ "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
+
+ "puppeteer-core": ["puppeteer-core@25.1.0", "", { "dependencies": { "@puppeteer/browsers": "3.0.4", "chromium-bidi": "16.0.1", "devtools-protocol": "0.0.1624250", "typed-query-selector": "^2.12.2", "webdriver-bidi-protocol": "0.4.2", "ws": "^8.21.0" } }, "sha512-jKzy5y4WG6uNuFbTWgW1D7mqoT9o0nllc/6a1DGF775T1mPmgw3scdFEtEq67yVFikavQmbYq6NLfbTfxHSlqQ=="],
+
+ "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
+
+ "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
+
+ "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
+
+ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
+
+ "shiki": ["shiki@4.2.0", "", { "dependencies": { "@shikijs/core": "4.2.0", "@shikijs/engine-javascript": "4.2.0", "@shikijs/engine-oniguruma": "4.2.0", "@shikijs/langs": "4.2.0", "@shikijs/themes": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ=="],
+
+ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
+
+ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
+
+ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
+
+ "typed-query-selector": ["typed-query-selector@2.12.2", "", {}, "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ=="],
+
+ "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
+
+ "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
+
+ "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
+
+ "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
+
+ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
+
+ "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
+
+ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
+
+ "webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.2", "", {}, "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA=="],
+
+ "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+ "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
+
+ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
+
+ "yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="],
+
+ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
+
+ "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+
+ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+ }
+}
diff --git a/assets/generate/generate.ts b/assets/generate/generate.ts
new file mode 100644
index 0000000..91b8bd4
--- /dev/null
+++ b/assets/generate/generate.ts
@@ -0,0 +1,208 @@
+/**
+ * Generates the README marketing images from the Synthpunk VS Code themes:
+ * - assets/icon.png angular-synthwave neon icon
+ * - assets/sample-*.png Neon Dark syntax-highlight previews (react/python/rust/csharp/markdown)
+ * - assets/hero.png 2x2 grid of one snippet across all four variants
+ *
+ * Highlighting uses shiki loaded with the repo's own VS Code theme JSON, so the
+ * colors are exactly what the editor renders. Rendering is done by screenshotting
+ * HTML with the system Chrome via puppeteer-core.
+ *
+ * Run: bun install && bun run generate.ts (set CHROME_PATH to override the browser)
+ */
+import { existsSync, readFileSync } from "node:fs";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+import puppeteer from "puppeteer-core";
+import { createHighlighter, type ThemeRegistrationRaw } from "shiki";
+
+const HERE = dirname(fileURLToPath(import.meta.url));
+const REPO = resolve(HERE, "..", "..");
+const THEMES_DIR = resolve(REPO, "themes/vscode/themes");
+const SNIPPETS = resolve(HERE, "snippets");
+const OUT = resolve(REPO, "assets");
+
+// --- theme loading ---------------------------------------------------------
+
+const VARIANT_FILES = {
+ "pastel-dark": "synthpunk-pastel-dark-color-theme.json",
+ "pastel-light": "synthpunk-pastel-light-color-theme.json",
+ "neon-dark": "synthpunk-neon-dark-color-theme.json",
+ "neon-light": "synthpunk-neon-light-color-theme.json",
+} as const;
+type VariantKey = keyof typeof VARIANT_FILES;
+
+type Theme = {
+ name: string;
+ type: string;
+ colors: Record;
+ tokenColors: unknown[];
+};
+
+const themes = Object.fromEntries(
+ (Object.keys(VARIANT_FILES) as VariantKey[]).map((key) => [
+ key,
+ JSON.parse(readFileSync(resolve(THEMES_DIR, VARIANT_FILES[key]), "utf8")) as Theme,
+ ]),
+) as Record;
+
+// --- browser / chrome ------------------------------------------------------
+
+function chromePath(): string {
+ const candidates = [
+ process.env.CHROME_PATH,
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
+ "/usr/bin/google-chrome",
+ "/usr/bin/chromium",
+ ].filter((p): p is string => Boolean(p));
+ const found = candidates.find((p) => existsSync(p));
+ if (!found) {
+ throw new Error("No Chrome/Chromium found. Set CHROME_PATH to a browser executable.");
+ }
+ return found;
+}
+
+// --- html templating -------------------------------------------------------
+
+const FONT_STACK = "'Cascadia Code', 'Cascadia Mono', Menlo, Consolas, monospace";
+const UI_FONT = "-apple-system, 'Segoe UI', system-ui, sans-serif";
+
+const PAGE_CSS = `
+ * { box-sizing: border-box; }
+ html, body { margin: 0; padding: 0; background: transparent; }
+ #shot { display: inline-block; padding: 56px; background: transparent; }
+ .window {
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 30px 70px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.05);
+ }
+ .titlebar { display: flex; align-items: center; gap: 9px; padding: 13px 16px; }
+ .dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
+ .filename { margin-left: 10px; font: 500 13px ${UI_FONT}; opacity: 0.85; }
+ .code { padding: 22px 26px; overflow: hidden; }
+ .code pre.shiki { margin: 0; background: transparent !important; }
+ .code pre.shiki code { font: 15px/1.65 ${FONT_STACK}; }
+ .code .line { white-space: pre; }
+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 36px; }
+ .cell .label { margin: 0 0 11px 3px; font: 600 14px ${UI_FONT}; color: #9a8fb0; letter-spacing: 0.4px; }
+`;
+
+function page(inner: string, pad = 56): string {
+ return `${inner}
`;
+}
+
+function windowHtml(codeHtml: string, theme: Theme, filename: string): string {
+ const bg = theme.colors["editor.background"];
+ const titleBg = theme.colors["titleBar.activeBackground"] ?? bg;
+ const nameColor = theme.colors["tab.activeForeground"] ?? theme.colors.foreground ?? "#888";
+ return `
+
+
+
+
+ ${filename}
+
+
${codeHtml}
+
`;
+}
+
+const ICON_HTML = ``;
+
+// --- rendering -------------------------------------------------------------
+
+async function main() {
+ const highlighter = await createHighlighter({
+ themes: Object.values(themes) as ThemeRegistrationRaw[],
+ langs: ["tsx", "python", "rust", "csharp", "markdown", "typescript"],
+ });
+
+ const browser = await puppeteer.launch({
+ executablePath: chromePath(),
+ headless: true,
+ args: ["--force-color-profile=srgb", "--font-render-hinting=none", "--hide-scrollbars"],
+ });
+
+ async function shoot(html: string, outFile: string, selector = "#shot", scale = 2) {
+ const p = await browser.newPage();
+ await p.setViewport({ width: 1700, height: 1100, deviceScaleFactor: scale });
+ await p.setContent(html, { waitUntil: "load" });
+ await p.evaluate(async () => {
+ await document.fonts.ready;
+ });
+ const el = await p.$(selector);
+ if (!el) throw new Error(`selector ${selector} not found`);
+ await el.screenshot({ path: outFile, omitBackground: true });
+ await p.close();
+ console.log("wrote", outFile.replace(`${REPO}/`, ""));
+ }
+
+ function read(name: string): string {
+ return readFileSync(resolve(SNIPPETS, name), "utf8").replace(/\n+$/, "");
+ }
+
+ // Per-language samples in Neon Dark.
+ const neon = themes["neon-dark"];
+ const samples = [
+ { out: "sample-react.png", file: "App.tsx", lang: "tsx", snippet: "App.tsx" },
+ { out: "sample-python.png", file: "pipeline.py", lang: "python", snippet: "pipeline.py" },
+ { out: "sample-rust.png", file: "lib.rs", lang: "rust", snippet: "lib.rs" },
+ { out: "sample-csharp.png", file: "Service.cs", lang: "csharp", snippet: "Service.cs" },
+ { out: "sample-markdown.png", file: "README.md", lang: "markdown", snippet: "sample.md" },
+ ];
+ for (const s of samples) {
+ const codeHtml = highlighter.codeToHtml(read(s.snippet), { lang: s.lang, theme: neon.name });
+ await shoot(page(windowHtml(codeHtml, neon, s.file)), resolve(OUT, s.out));
+ }
+
+ // Four-variant hero from one shared snippet.
+ const heroCode = read("hero.ts");
+ const order: Array<[VariantKey, string]> = [
+ ["pastel-dark", "Pastel Dark"],
+ ["pastel-light", "Pastel Light"],
+ ["neon-dark", "Neon Dark"],
+ ["neon-light", "Neon Light"],
+ ];
+ const cells = order
+ .map(([key, label]) => {
+ const codeHtml = highlighter.codeToHtml(heroCode, { lang: "typescript", theme: themes[key].name });
+ return `${label}
${windowHtml(codeHtml, themes[key], "theme.ts")}
`;
+ })
+ .join("");
+ await shoot(page(`${cells}
`, 56), resolve(OUT, "hero.png"));
+
+ // Icon.
+ await shoot(ICON_HTML, resolve(OUT, "icon.png"), ".icon");
+
+ await browser.close();
+ console.log("done.");
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/assets/generate/package.json b/assets/generate/package.json
new file mode 100644
index 0000000..0c8a1b0
--- /dev/null
+++ b/assets/generate/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "synthpunk-preview-generator",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "description": "Generates README marketing images (icon + syntax-highlight previews) from the Synthpunk VS Code themes.",
+ "scripts": {
+ "generate": "bun run generate.ts"
+ },
+ "dependencies": {
+ "puppeteer-core": "^25.1.0",
+ "shiki": "^4.2.0"
+ }
+}
diff --git a/assets/generate/snippets/App.tsx b/assets/generate/snippets/App.tsx
new file mode 100644
index 0000000..20da713
--- /dev/null
+++ b/assets/generate/snippets/App.tsx
@@ -0,0 +1,26 @@
+import { useState, useEffect } from "react";
+
+interface CounterProps {
+ label: string;
+ initial?: number;
+}
+
+// A tiny counter that shifts hue as it climbs.
+export function Counter({ label, initial = 0 }: CounterProps) {
+ const [count, setCount] = useState(initial);
+ const hue = (count * 24) % 360;
+
+ useEffect(() => {
+ document.title = `${label}: ${count}`;
+ }, [label, count]);
+
+ return (
+
+ );
+}
diff --git a/assets/generate/snippets/Service.cs b/assets/generate/snippets/Service.cs
new file mode 100644
index 0000000..3f6ecb1
--- /dev/null
+++ b/assets/generate/snippets/Service.cs
@@ -0,0 +1,22 @@
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Synthpunk.Audio;
+
+/// Streams and ranks tracks by tempo.
+public sealed class TrackService(IClock clock)
+{
+ public required string Channel { get; init; }
+
+ public async Task CountUptempoAsync(Track[] tracks)
+ {
+ await Task.Delay(10);
+ return tracks.Count(t => t.Bpm >= 128);
+ }
+
+ // Format a track for the now-playing line.
+ public string Tag(Track t) =>
+ $"[{clock.Now:HH:mm}] {t.Title} — {t.Bpm} bpm";
+}
+
+public record Track(string Title, int Bpm);
diff --git a/assets/generate/snippets/hero.ts b/assets/generate/snippets/hero.ts
new file mode 100644
index 0000000..d536200
--- /dev/null
+++ b/assets/generate/snippets/hero.ts
@@ -0,0 +1,10 @@
+type Variant = "pastel" | "neon";
+
+// Build a Synthpunk theme id from its parts.
+function themeId(variant: Variant, dark: boolean): string {
+ const mode = dark ? "dark" : "light";
+ return `synthpunk-${variant}-${mode}`;
+}
+
+const current = themeId("neon", true);
+console.log(`active → ${current}`);
diff --git a/assets/generate/snippets/lib.rs b/assets/generate/snippets/lib.rs
new file mode 100644
index 0000000..0ce45b1
--- /dev/null
+++ b/assets/generate/snippets/lib.rs
@@ -0,0 +1,29 @@
+use std::collections::HashMap;
+
+/// A neon palette keyed by semantic role.
+#[derive(Debug, Clone, Default)]
+pub struct Palette {
+ colors: HashMap,
+}
+
+impl Palette {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Insert a color and return `self` for chaining.
+ pub fn set(&mut self, role: &str, hex: u32) -> &mut Self {
+ self.colors.insert(role.to_string(), hex);
+ self
+ }
+
+ pub fn get(&self, role: &str) -> Option {
+ self.colors.get(role).copied()
+ }
+}
+
+fn main() {
+ let mut p = Palette::new();
+ p.set("keyword", 0xFF5CA8).set("string", 0x40FF80);
+ println!("keyword = {:#08X}", p.get("keyword").unwrap_or(0));
+}
diff --git a/assets/generate/snippets/pipeline.py b/assets/generate/snippets/pipeline.py
new file mode 100644
index 0000000..a432ee9
--- /dev/null
+++ b/assets/generate/snippets/pipeline.py
@@ -0,0 +1,25 @@
+from dataclasses import dataclass
+from functools import cache
+
+
+@dataclass(frozen=True)
+class Track:
+ title: str
+ bpm: int = 120
+
+ @property
+ def is_uptempo(self) -> bool:
+ return self.bpm >= 128
+
+
+@cache
+def neon_label(track: Track) -> str:
+ """Return a short, human label for a track."""
+ mood = "drive" if track.is_uptempo else "chill"
+ return f"{track.title} ({track.bpm} bpm, {mood})"
+
+
+if __name__ == "__main__":
+ deck = [Track("Outrun", 140), Track("Midnight", 96)]
+ for t in deck:
+ print(neon_label(t))
diff --git a/assets/generate/snippets/sample.md b/assets/generate/snippets/sample.md
new file mode 100644
index 0000000..a600e22
--- /dev/null
+++ b/assets/generate/snippets/sample.md
@@ -0,0 +1,17 @@
+# Synthpunk
+
+High-energy **synthwave** color themes for your editor.
+
+> Four variants — *Pastel* and *Neon*, each in light and dark.
+
+## Install
+
+1. Open the **Extensions** panel
+2. Search for `Synthpunk`
+3. Pick a theme via `Color Theme`
+
+```ts
+const theme = "Synthpunk Neon Dark";
+```
+
+See the [documentation](https://example.com) for more.
diff --git a/assets/hero.png b/assets/hero.png
new file mode 100644
index 0000000..4f837e5
Binary files /dev/null and b/assets/hero.png differ
diff --git a/assets/icon.png b/assets/icon.png
new file mode 100644
index 0000000..223a678
Binary files /dev/null and b/assets/icon.png differ
diff --git a/assets/palette-preview-dark.png b/assets/palette-preview-dark.png
deleted file mode 100644
index 57fbb9f..0000000
Binary files a/assets/palette-preview-dark.png and /dev/null differ
diff --git a/assets/palette-preview-light.png b/assets/palette-preview-light.png
deleted file mode 100644
index 7841b28..0000000
Binary files a/assets/palette-preview-light.png and /dev/null differ
diff --git a/assets/palette-preview-neon-dark.png b/assets/palette-preview-neon-dark.png
deleted file mode 100644
index 62f07d1..0000000
Binary files a/assets/palette-preview-neon-dark.png and /dev/null differ
diff --git a/assets/palette-preview-neon-light.png b/assets/palette-preview-neon-light.png
deleted file mode 100644
index 4ce5a11..0000000
Binary files a/assets/palette-preview-neon-light.png and /dev/null differ
diff --git a/assets/sample-csharp.png b/assets/sample-csharp.png
new file mode 100644
index 0000000..7ab699e
Binary files /dev/null and b/assets/sample-csharp.png differ
diff --git a/assets/sample-markdown.png b/assets/sample-markdown.png
new file mode 100644
index 0000000..58121cd
Binary files /dev/null and b/assets/sample-markdown.png differ
diff --git a/assets/sample-python.png b/assets/sample-python.png
new file mode 100644
index 0000000..d323a16
Binary files /dev/null and b/assets/sample-python.png differ
diff --git a/assets/sample-react.png b/assets/sample-react.png
new file mode 100644
index 0000000..f7b1c7c
Binary files /dev/null and b/assets/sample-react.png differ
diff --git a/assets/sample-rust.png b/assets/sample-rust.png
new file mode 100644
index 0000000..1175f40
Binary files /dev/null and b/assets/sample-rust.png differ
diff --git a/biome.json b/biome.json
index f6e1f23..32765c9 100644
--- a/biome.json
+++ b/biome.json
@@ -6,7 +6,8 @@
"useIgnoreFile": true
},
"files": {
- "ignoreUnknown": false
+ "ignoreUnknown": false,
+ "includes": ["**", "!assets/generate"]
},
"formatter": {
"enabled": true,
diff --git a/tsconfig.json b/tsconfig.json
index 21eef14..f413a23 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -12,5 +12,5 @@
"types": ["bun"]
},
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
- "exclude": ["node_modules"]
+ "exclude": ["node_modules", "assets/generate"]
}