diff --git a/.gitattributes b/.gitattributes
index ad81aff..bb09798 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -15,6 +15,12 @@
*.cs text eol=lf
*.csproj text eol=lf
*.js text eol=lf
+*.mjs text eol=lf
+*.ts text eol=lf
+*.tsx text eol=lf
+*.css text eol=lf
+*.html text eol=lf
+*.svg text eol=lf
# The sandbox exists to be read and experimented with, not to describe what
# this repository is written in.
diff --git a/.gitignore b/.gitignore
index b4e458c..08ae3a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ dist/
# Personal, machine-local agent settings
.claude/settings.local.json
+.claude/launch.json
# Sandbox build output. The projects are committed; what building them
# produces is not.
diff --git a/CLAUDE.md b/CLAUDE.md
index 0d66e50..a52ce25 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -14,6 +14,7 @@ crate, ports and adapters.
| [`BACKLOG.md`](BACKLOG.md) | What is not built yet, and what was decided against. |
| [`README.md`](README.md) | The user-facing manual. |
| [`sandbox/`](sandbox) | Working projects per ecosystem, to try by hand and to read as examples. |
+| [`website/`](website) | The landing page, and what its deployment still needs. Not part of the crate, and not versioned with it. |
Before changing behavior, read the relevant `DESIGN.md` section. Before
building something new, check `BACKLOG.md` — it may already be decided against,
diff --git a/website/.env b/website/.env
new file mode 100644
index 0000000..dca6dee
--- /dev/null
+++ b/website/.env
@@ -0,0 +1,7 @@
+# Where the site will be served from. Both values are read at build time.
+#
+# VITE_BASE_PATH is the path every asset URL is prefixed with: "/" for a custom
+# domain at the root, "/vump/" for a GitHub project site at okcodes.github.io.
+# VITE_SITE_URL is the absolute origin used for canonical and social-card URLs.
+VITE_BASE_PATH=/
+VITE_SITE_URL=https://okcodes.github.io/vump
diff --git a/website/.gitignore b/website/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/website/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/website/.oxlintrc.json b/website/.oxlintrc.json
new file mode 100644
index 0000000..8e4a9df
--- /dev/null
+++ b/website/.oxlintrc.json
@@ -0,0 +1,34 @@
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": ["react", "typescript", "oxc", "jsx-a11y", "import", "promise"],
+ "categories": {
+ "correctness": "error",
+ "suspicious": "error",
+ "perf": "error",
+ "pedantic": "warn"
+ },
+ "rules": {
+ "react/rules-of-hooks": "error",
+ "react/only-export-components": [
+ "warn",
+ {
+ "allowConstantExport": true
+ }
+ ],
+ "typescript/consistent-type-imports": "error",
+ "typescript/no-explicit-any": "error",
+ "eslint/no-console": [
+ "error",
+ {
+ "allow": ["warn", "error"]
+ }
+ ],
+ "react/react-in-jsx-scope": "off",
+ "import/no-unassigned-import": "off",
+ "require-unicode-regexp": "off",
+ "max-lines-per-function": "off",
+ "react/no-array-index-key": "off",
+ "import/max-dependencies": "off"
+ },
+ "ignorePatterns": ["dist", "node_modules"]
+}
diff --git a/website/.prettierignore b/website/.prettierignore
new file mode 100644
index 0000000..91a3983
--- /dev/null
+++ b/website/.prettierignore
@@ -0,0 +1,3 @@
+dist
+node_modules
+package-lock.json
diff --git a/website/.prettierrc.json b/website/.prettierrc.json
new file mode 100644
index 0000000..a01d677
--- /dev/null
+++ b/website/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "semi": true,
+ "singleQuote": true,
+ "printWidth": 100,
+ "trailingComma": "all",
+ "plugins": ["prettier-plugin-tailwindcss"]
+}
diff --git a/website/README.md b/website/README.md
new file mode 100644
index 0000000..02cffb1
--- /dev/null
+++ b/website/README.md
@@ -0,0 +1,143 @@
+# vump website
+
+The landing page for [vump](https://github.com/okcodes/vump). One page, built
+to static files and destined for GitHub Pages.
+
+```bash
+npm install
+npm run dev # http://localhost:5173
+npm run check # typecheck, lint, format, build — what CI will run
+```
+
+| Script | Does |
+| ------------------------- | ------------------------------------------------------------- |
+| `dev` | Vite dev server with hot reload |
+| `build` | Type-checks, then writes `dist/` |
+| `preview` | Serves `dist/` as a deploy would |
+| `typecheck` | `tsc -b`, no emit |
+| `lint` | oxlint; any warning fails |
+| `format` / `format:check` | Prettier over everything |
+| `check` | All four, in order |
+| `og` | Regenerates `public/og.png` and `public/apple-touch-icon.png` |
+
+## What is where
+
+| Path | Holds |
+| -------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
+| `src/content/` | Every fact the page states: terminal runs, tracked files, version rules, exit codes, links. Nothing here renders. |
+| `src/components/sections/` | One file per numbered section, in page order. |
+| `src/components/ui/` | The primitives: terminal pane, code block, section frame, reveal, buttons. |
+| `src/lib/` | Hooks and the two small engines — a segment model for command output, and a highlighter for the four snippet languages. |
+| `src/index.css` | The whole design system: palette, type, motion. |
+| `scripts/og.mjs` | Renders the social card from the same values, offline. |
+
+**Content is separate from layout on purpose.** Every terminal pane on the page
+reproduces what the binary actually prints — the marks, the column alignment,
+the wording of the errors. When vump's output changes, `src/content/runs.ts` is
+the only file to revisit.
+
+**The page states no version number.** The release chip fetches the newest
+release from the GitHub API as the page loads, and renders nothing when that
+fails. A version written into a marketing site is a version nobody bumps, which
+is the defect vump exists to prevent.
+
+## Design system
+
+Colours are semantic roles — `ground`, `ink`, `muted`, `line`, `signal` — defined
+once in `src/index.css` and flipped by a `data-theme` attribute on the document
+element. No component names a literal colour, so the two themes cannot drift
+apart. A script in `index.html` resolves the theme before first paint; an
+explicit choice outranks the system preference.
+
+Terminal surfaces are held dark in both themes. Rendered command output should
+read as output, not as a styled quotation.
+
+The accent is spent on one thing: what changed. A new version number, a passing
+mark, a highlighted line in a snippet. Amber marks a refusal, red a failure, and
+nothing else is coloured at all.
+
+## Lint configuration
+
+Four rules are off in `.oxlintrc.json`, each because it does not fit this
+codebase rather than because it was inconvenient:
+
+| Rule | Why |
+| -------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
+| `react/no-array-index-key` | Rendered lines and tokens are static arrays that never reorder; the index is the correct identity. |
+| `max-lines-per-function` | Counts JSX as logic. A section component is markup, not complexity. |
+| `import/max-dependencies` | `App.tsx` composes every section; that is its whole job. |
+| `require-unicode-regexp` | The highlighter's patterns run over ASCII snippets written in this repository, and `u` mode outlaws the escapes they need. |
+
+Everything else — correctness, suspicious, perf and pedantic — is on, and a
+warning fails the run.
+
+## Deploying to GitHub Pages
+
+Not wired up yet, deliberately: the domain is not settled, and the base path is
+baked into every asset URL at build time.
+
+Two values decide it, both in `.env`:
+
+| Variable | For a custom domain | For `okcodes.github.io/vump` |
+| ---------------- | --------------------- | -------------------------------- |
+| `VITE_BASE_PATH` | `/` | `/vump/` |
+| `VITE_SITE_URL` | `https://your.domain` | `https://okcodes.github.io/vump` |
+
+A custom domain also needs `public/CNAME` holding the bare hostname, which ships
+to `dist/` untouched. `public/.nojekyll` is already there, so Pages serves the
+build as-is instead of running it through Jekyll.
+
+When the domain is decided, add `.github/workflows/website.yml`:
+
+```yaml
+name: website
+
+on:
+ push:
+ branches: [main]
+ paths: ['website/**', '.github/workflows/website.yml']
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: true
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: website
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: npm
+ cache-dependency-path: website/package-lock.json
+ - run: npm ci
+ - run: npm run check
+ - uses: actions/upload-pages-artifact@v3
+ with:
+ path: website/dist
+
+ deploy:
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deploy.outputs.page_url }}
+ steps:
+ - id: deploy
+ uses: actions/deploy-pages@v4
+```
+
+Then set Pages → Source → GitHub Actions in the repository settings.
+
+The site is not versioned and vump does not track its `package.json`, which
+declares no version at all. It ships when `main` moves, not when a tag does.
diff --git a/website/index.html b/website/index.html
new file mode 100644
index 0000000..f683933
--- /dev/null
+++ b/website/index.html
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+ vump — one version, every file, proven at the tag
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/website/package-lock.json b/website/package-lock.json
new file mode 100644
index 0000000..b033c92
--- /dev/null
+++ b/website/package-lock.json
@@ -0,0 +1,2616 @@
+{
+ "name": "vump-website",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "vump-website",
+ "dependencies": {
+ "@fontsource-variable/instrument-sans": "^5.3.0",
+ "@fontsource-variable/jetbrains-mono": "^5.3.0",
+ "@fontsource/instrument-serif": "^5.3.0",
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8"
+ },
+ "devDependencies": {
+ "@resvg/resvg-js": "^2.6.2",
+ "@tailwindcss/vite": "^4.3.3",
+ "@types/node": "^24.13.3",
+ "@types/react": "^19.2.18",
+ "@types/react-dom": "^19.2.4",
+ "@vitejs/plugin-react": "^6.1.0",
+ "npm-run-all2": "^9.0.3",
+ "oxlint": "^1.79.0",
+ "prettier": "^3.9.6",
+ "prettier-plugin-tailwindcss": "^0.8.1",
+ "tailwindcss": "^4.3.3",
+ "typescript": "~6.0.2",
+ "vite": "^8.2.2",
+ "wawoff2": "^2.0.1"
+ }
+ },
+ "node_modules/@fontsource-variable/instrument-sans": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource-variable/instrument-sans/-/instrument-sans-5.3.0.tgz",
+ "integrity": "sha512-u4gKbDBTNFGkg997tfQn3eHOhHuquWUFTRT/rwzuKtrxX5P2ekfs2x+LgBPP4P32+cC+vUwF1Cr+IdRoPQbrGw==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
+ "node_modules/@fontsource-variable/jetbrains-mono": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz",
+ "integrity": "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
+ "node_modules/@fontsource/instrument-serif": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource/instrument-serif/-/instrument-serif-5.3.0.tgz",
+ "integrity": "sha512-mDiaIg0u67sYV59fie92Wz4sM8UiVlbL7fLxnFPCKkX15DMASEnQTREbaP5S5/3DCcsoAOcQ0sV9E4AeY4QqYQ==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.149.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz",
+ "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/oxc-project"
+ }
+ },
+ "node_modules/@oxlint/binding-android-arm-eabi": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.82.0.tgz",
+ "integrity": "sha512-a3LB+C5Dsj5b/qtmG/mv5WrzuiXEpg1KF5nXWcEvaoN5TYAqkIvxPOwTPp3Jy/FoGpRo8zsTFhMElMXfeoOEzA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-android-arm64": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.82.0.tgz",
+ "integrity": "sha512-OBlhRgNqFblGpGenno/aqOfJLOkQ2B8Ig3iDAalfn0H8hJGZKXPeexCRTDm6uwv6YUjSA9Xnwt1y/Bgj5ZH8uw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-darwin-arm64": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.82.0.tgz",
+ "integrity": "sha512-dsopxqtY5ZdyT9uLHyGt1SyiLop6hi7hWI3PKpePodkRQOkLaCm+OE4fR9CAz9qdfjiFO8531tX/QDyP/psjFg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-darwin-x64": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.82.0.tgz",
+ "integrity": "sha512-94Lu0SgTClKColU66g1VDuigV3HkcbkJBnTtZjGYfE8UPugaWDgKrm2icjC6HJVUYler2OXaHP/X0TBy8+CowQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-freebsd-x64": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.82.0.tgz",
+ "integrity": "sha512-hne/V06ewhh1i0w8+l7GDNROAGCGPmyFuOwiP7YTRu0JycyStJ4785dmF8xU5p0uUwt2emvIF9vc7Xjis+cJ0g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm-gnueabihf": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.82.0.tgz",
+ "integrity": "sha512-aWY2xtbZf1LneW9Qsv/n2Sp8gOu74JrlQzEtj4coHX2SHFrCfhmAumaU+sI/A5nr+yoTRTSmI/pL2s6ADlNSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm-musleabihf": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.82.0.tgz",
+ "integrity": "sha512-Fe+TtXCXMh/5f7kWlZ2VAwsMumZWtraFlKVk1NJlL52/beGwfDE7ov+/8gVirHzWokzGu7X65hSPq0ucPDskWQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm64-gnu": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.82.0.tgz",
+ "integrity": "sha512-6azCZ6OJudlvipNttXCCQcyeFfcJ/NvUZdSN1z8elo73kCHtyQC7WTiUcSjWYvJ1jaq9KDUyMAoAS/vNzhBomA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm64-musl": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.82.0.tgz",
+ "integrity": "sha512-PLEaSD8IAIIlwW4dwOd9YaxuxeOpwiXL4J24rcnE4iNtyM5j9Q9/3+gti08oXpx0u2ygNjRDx9xjWWpQonuJEw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-ppc64-gnu": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.82.0.tgz",
+ "integrity": "sha512-D94em/BwknNTn4vqxjHh5wb2oL566eFhArabqKIr0cNZMHOJuiraFp1A8tXpH05bbE5tqwEfLXTI0MWEGtn3Dw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-riscv64-gnu": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.82.0.tgz",
+ "integrity": "sha512-MOprxBaoYU2D4VgxXCl3ghydThWtx7Um1lL51kGYNeQ5Al7WzsH7/tqGdNtbLrIWnjq3bsm13+nz/gRIxjrOXw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-riscv64-musl": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.82.0.tgz",
+ "integrity": "sha512-5h55QsfJ/luDXZzC20k6SNOY1Az+dCP9WvntKtcUWh2JhckAdwApY2ZusaBTwLENnReXU+A2fJtSrYvZJNKNPg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-s390x-gnu": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.82.0.tgz",
+ "integrity": "sha512-IE8NJNLlHr0CaXyGJPGVn0eTkUyoj1I2UfA8x7I4PSOYKsQ/6btVC7Pywrj5onk0cMH25r6Z38SoN3AvE5Zuog==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-x64-gnu": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.82.0.tgz",
+ "integrity": "sha512-XUUUxaBo9XKl+J1B9EmP1cTGQPddzeURvoGkfwh/94PGnbW+hBprDljneoI2M1jzC1bzrIV3ihc7iM9UXl8+tg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-x64-musl": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.82.0.tgz",
+ "integrity": "sha512-SWLSFulX9TDuH6yvbPYp4+VNn6jkkIvvI+KiujDM5rWBRHEfkesCC/pCneIIUr6ovkxZ5fRtpi2v5Cz5FrMJZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-openharmony-arm64": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.82.0.tgz",
+ "integrity": "sha512-BQy35f6ZUdNr9a6c7B7orxQTcLjByGT2z3WAgmRovpRwmPYAaJ+NTplmMzhdjdJ4qSchfMNZy/Ukg+qRg6zseQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-arm64-msvc": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.82.0.tgz",
+ "integrity": "sha512-V4QhSTg5gctZue8RJjsGi7NpQPThr/p1/HfmiMC5kfe1KFEup9SQRVub4A6kijQjdHfxj7bLL1KO3QO7/5bwMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-ia32-msvc": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.82.0.tgz",
+ "integrity": "sha512-TUSCLaKB2yktpFAJ/r3HAUYsaV/3DT7JS4iNKyoh3a9YNwD0UG7Ezh4D8m23654vQcU6P/RQrCAjRPKe4peP/A==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-x64-msvc": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.82.0.tgz",
+ "integrity": "sha512-VTVoRIWJTb+wvUX8EYoPArfFH02whuR10goFXE/LHRRX33ajRrFgqbcONXZMiF4C5rnattfkm87HqYn8jb8hmQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@resvg/resvg-js": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz",
+ "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@resvg/resvg-js-android-arm-eabi": "2.6.2",
+ "@resvg/resvg-js-android-arm64": "2.6.2",
+ "@resvg/resvg-js-darwin-arm64": "2.6.2",
+ "@resvg/resvg-js-darwin-x64": "2.6.2",
+ "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2",
+ "@resvg/resvg-js-linux-arm64-gnu": "2.6.2",
+ "@resvg/resvg-js-linux-arm64-musl": "2.6.2",
+ "@resvg/resvg-js-linux-x64-gnu": "2.6.2",
+ "@resvg/resvg-js-linux-x64-musl": "2.6.2",
+ "@resvg/resvg-js-win32-arm64-msvc": "2.6.2",
+ "@resvg/resvg-js-win32-ia32-msvc": "2.6.2",
+ "@resvg/resvg-js-win32-x64-msvc": "2.6.2"
+ }
+ },
+ "node_modules/@resvg/resvg-js-android-arm-eabi": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz",
+ "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-android-arm64": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz",
+ "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-darwin-arm64": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz",
+ "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-darwin-x64": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz",
+ "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz",
+ "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-linux-arm64-gnu": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz",
+ "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-linux-arm64-musl": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz",
+ "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-linux-x64-gnu": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz",
+ "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-linux-x64-musl": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz",
+ "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-win32-arm64-msvc": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz",
+ "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-win32-ia32-msvc": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz",
+ "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@resvg/resvg-js-win32-x64-msvc": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz",
+ "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm-eabi": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz",
+ "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz",
+ "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz",
+ "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz",
+ "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz",
+ "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz",
+ "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz",
+ "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz",
+ "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz",
+ "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz",
+ "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz",
+ "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz",
+ "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz",
+ "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz",
+ "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz",
+ "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
+ "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.24.1",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.3"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz",
+ "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.3",
+ "@tailwindcss/oxide-darwin-x64": "4.3.3",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.3",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.3",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.3",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.3",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.3"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz",
+ "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz",
+ "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz",
+ "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz",
+ "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz",
+ "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz",
+ "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz",
+ "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz",
+ "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz",
+ "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz",
+ "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.11.1",
+ "@emnapi/runtime": "^1.11.1",
+ "@emnapi/wasi-threads": "^1.2.2",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
+ "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz",
+ "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz",
+ "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.3.3",
+ "@tailwindcss/oxide": "4.3.3",
+ "tailwindcss": "4.3.3"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "24.13.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz",
+ "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
+ "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
+ "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.3.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz",
+ "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "oxc-transform-react": "^0.145.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ },
+ "oxc-transform-react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-7.0.0.tgz",
+ "integrity": "sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=22"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cross-spawn/node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/cross-spawn/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.24.5",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz",
+ "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/isexe": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
+ "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-6.0.0.tgz",
+ "integrity": "sha512-2/8adwnK1/+Fdjyts4r6wSpfANWw8zdNhU9U/Llk59c6O+DjSisPWPykwoL8gZmocP9Dy64S7oie2g+Mia123A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/memorystream": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
+ "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/npm-normalize-package-bin": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-6.0.0.tgz",
+ "integrity": "sha512-tdt4aFn9QamlhdN3HV2D2ccpBwO5/fyjjbXUxYA6uBjyekMZcZvDq0aSj9t5Jo+tih6AYFnt/cuIRn9013e0Uw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
+ }
+ },
+ "node_modules/npm-run-all2": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-9.0.3.tgz",
+ "integrity": "sha512-BQAEdU1PtYc48qYRdghW2BVTQT3VqWCoFQmO87NlM1h1PYwMCKQpUWaNyB20V26caNzFsXQDfyOdznLlHCih6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^7.0.0",
+ "cross-spawn": "^7.0.6",
+ "memorystream": "^0.3.1",
+ "picomatch": "^4.0.2",
+ "pidtree": "^1.0.0",
+ "read-package-json-fast": "^6.0.0",
+ "shell-quote": "^1.8.4",
+ "which": "^7.0.0"
+ },
+ "bin": {
+ "npm-run-all": "bin/npm-run-all/index.js",
+ "npm-run-all2": "bin/npm-run-all/index.js",
+ "run-p": "bin/run-p/index.js",
+ "run-s": "bin/run-s/index.js"
+ },
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0",
+ "npm": ">= 10"
+ }
+ },
+ "node_modules/oxlint": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.82.0.tgz",
+ "integrity": "sha512-+iFM1BGw1ntYJt3QngbJmjbrGxPaKMUADOXOijpWGnYcBPq8YZnQftSS1C+pVcDYy9YxqDVJKQqQkTazTQMboQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "oxlint": "bin/oxlint"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/oxc-project"
+ },
+ "optionalDependencies": {
+ "@oxlint/binding-android-arm-eabi": "1.82.0",
+ "@oxlint/binding-android-arm64": "1.82.0",
+ "@oxlint/binding-darwin-arm64": "1.82.0",
+ "@oxlint/binding-darwin-x64": "1.82.0",
+ "@oxlint/binding-freebsd-x64": "1.82.0",
+ "@oxlint/binding-linux-arm-gnueabihf": "1.82.0",
+ "@oxlint/binding-linux-arm-musleabihf": "1.82.0",
+ "@oxlint/binding-linux-arm64-gnu": "1.82.0",
+ "@oxlint/binding-linux-arm64-musl": "1.82.0",
+ "@oxlint/binding-linux-ppc64-gnu": "1.82.0",
+ "@oxlint/binding-linux-riscv64-gnu": "1.82.0",
+ "@oxlint/binding-linux-riscv64-musl": "1.82.0",
+ "@oxlint/binding-linux-s390x-gnu": "1.82.0",
+ "@oxlint/binding-linux-x64-gnu": "1.82.0",
+ "@oxlint/binding-linux-x64-musl": "1.82.0",
+ "@oxlint/binding-openharmony-arm64": "1.82.0",
+ "@oxlint/binding-win32-arm64-msvc": "1.82.0",
+ "@oxlint/binding-win32-ia32-msvc": "1.82.0",
+ "@oxlint/binding-win32-x64-msvc": "1.82.0"
+ },
+ "peerDependencies": {
+ "oxlint-tsgolint": ">=7.0.2001",
+ "vite-plus": "*"
+ },
+ "peerDependenciesMeta": {
+ "oxlint-tsgolint": {
+ "optional": true
+ },
+ "vite-plus": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pidtree": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-1.0.0.tgz",
+ "integrity": "sha512-avfAvjB9Dd0wdj3rjJX//yS+G79OO0KrS5pJHFJENjYGX6N4SMgEDBBI/yFy0lloOYSaC6XQxzpOAMPfSYFV/Q==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.28",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
+ "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.18",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.9.6",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
+ "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-plugin-tailwindcss": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.8.1.tgz",
+ "integrity": "sha512-iaFMYqDsE4ffdDkn5qup0j5f2aCEBFZrdrZnvu9QKTlWx/iGPeQ4HHu7b7fCPMxeo9nwQBiOAh2nSypdFYWJkw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19"
+ },
+ "peerDependencies": {
+ "@ianvs/prettier-plugin-sort-imports": "*",
+ "@prettier/plugin-hermes": "*",
+ "@prettier/plugin-oxc": "*",
+ "@prettier/plugin-pug": "*",
+ "@shopify/prettier-plugin-liquid": "*",
+ "@trivago/prettier-plugin-sort-imports": "*",
+ "@zackad/prettier-plugin-twig": "*",
+ "prettier": "^3.0",
+ "prettier-plugin-astro": "*",
+ "prettier-plugin-css-order": "*",
+ "prettier-plugin-jsdoc": "*",
+ "prettier-plugin-marko": "*",
+ "prettier-plugin-multiline-arrays": "*",
+ "prettier-plugin-organize-attributes": "*",
+ "prettier-plugin-organize-imports": "*",
+ "prettier-plugin-sort-imports": "*",
+ "prettier-plugin-svelte": "*"
+ },
+ "peerDependenciesMeta": {
+ "@ianvs/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "@prettier/plugin-hermes": {
+ "optional": true
+ },
+ "@prettier/plugin-oxc": {
+ "optional": true
+ },
+ "@prettier/plugin-pug": {
+ "optional": true
+ },
+ "@shopify/prettier-plugin-liquid": {
+ "optional": true
+ },
+ "@trivago/prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "@zackad/prettier-plugin-twig": {
+ "optional": true
+ },
+ "prettier-plugin-astro": {
+ "optional": true
+ },
+ "prettier-plugin-css-order": {
+ "optional": true
+ },
+ "prettier-plugin-jsdoc": {
+ "optional": true
+ },
+ "prettier-plugin-marko": {
+ "optional": true
+ },
+ "prettier-plugin-multiline-arrays": {
+ "optional": true
+ },
+ "prettier-plugin-organize-attributes": {
+ "optional": true
+ },
+ "prettier-plugin-organize-imports": {
+ "optional": true
+ },
+ "prettier-plugin-sort-imports": {
+ "optional": true
+ },
+ "prettier-plugin-svelte": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react": {
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
+ "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
+ "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.28.0"
+ },
+ "peerDependencies": {
+ "react": "^19.3.0"
+ }
+ },
+ "node_modules/read-package-json-fast": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-6.0.0.tgz",
+ "integrity": "sha512-PNaGjoCnw9DBA2Kl8D+8po957z778q/HOPuY2u3Bkw/JO3eC8MDx7jn/PgMtSgpcBbs+6UOjDbwReGpXmRvs0g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "json-parse-even-better-errors": "^6.0.0",
+ "npm-normalize-package-bin": "^6.0.0"
+ },
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz",
+ "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.149.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm-eabi": "1.2.8",
+ "@rolldown/binding-android-arm64": "1.2.8",
+ "@rolldown/binding-darwin-arm64": "1.2.8",
+ "@rolldown/binding-darwin-x64": "1.2.8",
+ "@rolldown/binding-freebsd-x64": "1.2.8",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.8",
+ "@rolldown/binding-linux-arm64-gnu": "1.2.8",
+ "@rolldown/binding-linux-arm64-musl": "1.2.8",
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.8",
+ "@rolldown/binding-linux-s390x-gnu": "1.2.8",
+ "@rolldown/binding-linux-x64-gnu": "1.2.8",
+ "@rolldown/binding-linux-x64-musl": "1.2.8",
+ "@rolldown/binding-openharmony-arm64": "1.2.8",
+ "@rolldown/binding-win32-arm64-msvc": "1.2.8",
+ "@rolldown/binding-win32-x64-msvc": "1.2.8"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
+ "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz",
+ "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
+ "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz",
+ "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.33.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.26",
+ "rolldown": "~1.2.4",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.4.0 || ^0.5.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/wawoff2": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/wawoff2/-/wawoff2-2.0.1.tgz",
+ "integrity": "sha512-r0CEmvpH63r4T15ebFqeOjGqU4+EgTx4I510NtK35EMciSdcTxCw3Byy3JnBonz7iyIFZ0AbVo0bbFpEVuhCYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "woff2_compress.js": "bin/woff2_compress.js",
+ "woff2_decompress.js": "bin/woff2_decompress.js"
+ }
+ },
+ "node_modules/which": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-7.0.0.tgz",
+ "integrity": "sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^4.0.0"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
+ }
+ }
+ }
+}
diff --git a/website/package.json b/website/package.json
new file mode 100644
index 0000000..ee0806e
--- /dev/null
+++ b/website/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "vump-website",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview",
+ "typecheck": "tsc -b",
+ "lint": "oxlint --max-warnings=0",
+ "lint:fix": "oxlint --fix --max-warnings=0",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
+ "check": "run-s typecheck lint format:check build",
+ "og": "node scripts/og.mjs"
+ },
+ "dependencies": {
+ "@fontsource-variable/instrument-sans": "^5.3.0",
+ "@fontsource-variable/jetbrains-mono": "^5.3.0",
+ "@fontsource/instrument-serif": "^5.3.0",
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8"
+ },
+ "devDependencies": {
+ "@resvg/resvg-js": "^2.6.2",
+ "@tailwindcss/vite": "^4.3.3",
+ "@types/node": "^24.13.3",
+ "@types/react": "^19.2.18",
+ "@types/react-dom": "^19.2.4",
+ "@vitejs/plugin-react": "^6.1.0",
+ "npm-run-all2": "^9.0.3",
+ "oxlint": "^1.79.0",
+ "prettier": "^3.9.6",
+ "prettier-plugin-tailwindcss": "^0.8.1",
+ "tailwindcss": "^4.3.3",
+ "typescript": "~6.0.2",
+ "vite": "^8.2.2",
+ "wawoff2": "^2.0.1"
+ },
+ "description": "Marketing site for vump"
+}
diff --git a/website/public/.nojekyll b/website/public/.nojekyll
new file mode 100644
index 0000000..e69de29
diff --git a/website/public/404.html b/website/public/404.html
new file mode 100644
index 0000000..646f710
--- /dev/null
+++ b/website/public/404.html
@@ -0,0 +1,65 @@
+
+
+
+
+
+ Not found — vump
+
+
+
+
+ 404
+ There is nothing at this address.
+ Back to vump
+
+
diff --git a/website/public/apple-touch-icon.png b/website/public/apple-touch-icon.png
new file mode 100644
index 0000000..4ce952d
Binary files /dev/null and b/website/public/apple-touch-icon.png differ
diff --git a/website/public/favicon.svg b/website/public/favicon.svg
new file mode 100644
index 0000000..e99c314
--- /dev/null
+++ b/website/public/favicon.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/website/public/og.png b/website/public/og.png
new file mode 100644
index 0000000..1f99f75
Binary files /dev/null and b/website/public/og.png differ
diff --git a/website/scripts/og.mjs b/website/scripts/og.mjs
new file mode 100644
index 0000000..6403fb3
--- /dev/null
+++ b/website/scripts/og.mjs
@@ -0,0 +1,157 @@
+/**
+ * Renders the social card and the touch icon from the same values the site
+ * uses, with no browser and no network.
+ *
+ * The card is generated rather than drawn by hand so it cannot drift from the
+ * page it advertises: change the headline here and both move together. Fonts
+ * come out of node_modules, decompressed from woff2 because the renderer reads
+ * TrueType only.
+ */
+
+import { Resvg } from '@resvg/resvg-js';
+import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import wawoff2 from 'wawoff2';
+
+const root = fileURLToPath(new URL('..', import.meta.url));
+
+const FONTS = [
+ '@fontsource-variable/instrument-sans/files/instrument-sans-latin-wght-normal.woff2',
+ '@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-wght-normal.woff2',
+ '@fontsource/instrument-serif/files/instrument-serif-latin-400-italic.woff2',
+];
+
+const INK = '#f2f4f6';
+const MUTED = '#9aa3ae';
+const FAINT = '#6c7481';
+const SIGNAL = '#5ee0bd';
+const GROUND = '#121419';
+const PANE = '#171a20';
+
+// The check is drawn rather than typed: U+2713 is outside the latin subset the
+// site loads, and a font that does not carry it renders a notdef box.
+const RUN = [
+ { text: '$ vump patch --through tag', fill: INK },
+ { text: '0.2.0 -> 0.2.1', fill: SIGNAL, ok: true },
+ { text: ' Cargo.toml', fill: FAINT, indent: true },
+ { text: ' Cargo.lock', fill: FAINT, indent: true },
+ { text: 'tagged v0.2.1', fill: SIGNAL, ok: true },
+];
+
+function check(x, y) {
+ return ` `;
+}
+
+function grid(width, height, step) {
+ const lines = [];
+ for (let x = step; x < width; x += step) {
+ lines.push(` `);
+ }
+ for (let y = step; y < height; y += step) {
+ lines.push(` `);
+ }
+ return `${lines.join('')} `;
+}
+
+function mark(x, y, size) {
+ const s = size / 32;
+ return `
+
+
+
+ `;
+}
+
+const card = `
+
+
+
+
+
+
+
+
+ ${grid(1200, 630, 60)}
+
+
+ ${mark(72, 64, 40)}
+ vump
+
+ One version.
+ Every file.
+ Proven at the tag.
+
+
+ Keep every version number in a repository in step, and fail
+
+
+ the build before a tag that disagrees with its source.
+
+
+
+ GITHUB.COM/OKCODES/VUMP
+
+
+
+
+ ~/widget
+ ${RUN.map((line, index) => {
+ const y = 94 + index * 34;
+ const x = line.ok || line.indent ? 48 : 24;
+ const glyph = line.ok ? check(25, y - 7) : '';
+ return `${glyph}${line.text} `;
+ }).join('\n ')}
+
+ `;
+
+async function loadFonts() {
+ const dir = await mkdtemp(join(tmpdir(), 'vump-og-'));
+
+ const paths = await Promise.all(
+ FONTS.map(async (font) => {
+ const woff2 = await readFile(join(root, 'node_modules', font));
+ const ttf = Buffer.from(await wawoff2.decompress(woff2));
+ const path = join(dir, `${font.split('/').pop()?.replace('.woff2', '')}.ttf`);
+ await writeFile(path, ttf);
+ return path;
+ }),
+ );
+
+ return { dir, paths };
+}
+
+function render(svg, width, fontFiles) {
+ return new Resvg(svg, {
+ fitTo: { mode: 'width', value: width },
+ font: { fontFiles, loadSystemFonts: false, defaultFontFamily: 'Instrument Sans' },
+ })
+ .render()
+ .asPng();
+}
+
+const { dir, paths } = await loadFonts();
+
+try {
+ await mkdir(join(root, 'public'), { recursive: true });
+
+ await writeFile(join(root, 'public/og.png'), render(card, 1200, paths));
+
+ const favicon = await readFile(join(root, 'public/favicon.svg'), 'utf8');
+ await writeFile(join(root, 'public/apple-touch-icon.png'), render(favicon, 180, paths));
+
+ process.stdout.write('wrote public/og.png and public/apple-touch-icon.png\n');
+} finally {
+ await rm(dir, { recursive: true, force: true });
+}
diff --git a/website/src/App.tsx b/website/src/App.tsx
new file mode 100644
index 0000000..5134659
--- /dev/null
+++ b/website/src/App.tsx
@@ -0,0 +1,37 @@
+import { Footer } from './components/Footer.tsx';
+import { Nav } from './components/Nav.tsx';
+import { Ci } from './components/sections/Ci.tsx';
+import { Defect } from './components/sections/Defect.tsx';
+import { Files } from './components/sections/Files.tsx';
+import { Hero } from './components/sections/Hero.tsx';
+import { Install } from './components/sections/Install.tsx';
+import { Projects } from './components/sections/Projects.tsx';
+import { Rule } from './components/sections/Rule.tsx';
+import { Safety } from './components/sections/Safety.tsx';
+import { Versions } from './components/sections/Versions.tsx';
+
+export function App() {
+ return (
+ <>
+
+ Skip to content
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/website/src/components/Footer.tsx b/website/src/components/Footer.tsx
new file mode 100644
index 0000000..45b0d63
--- /dev/null
+++ b/website/src/components/Footer.tsx
@@ -0,0 +1,69 @@
+import { links } from '../content/links.ts';
+import { Mark } from './ui/icons.tsx';
+
+const COLUMNS = [
+ {
+ title: 'Project',
+ items: [
+ { label: 'Repository', href: links.repo },
+ { label: 'Releases', href: links.releases },
+ { label: 'CI action', href: links.action },
+ { label: 'Sandbox projects', href: links.sandbox },
+ ],
+ },
+ {
+ title: 'Documentation',
+ items: [
+ { label: 'Manual', href: links.readme },
+ { label: 'Design specification', href: links.design },
+ { label: 'Contributing', href: links.contributing },
+ { label: 'Backlog', href: links.backlog },
+ ],
+ },
+];
+
+export function Footer() {
+ return (
+
+
+
+
+
+
+ vump
+
+
+ Keep semver version numbers in sync across the files of a repository, and verify in CI
+ that a released tag matches what is recorded in source.
+
+
+
+ {COLUMNS.map((column) => (
+
+ ))}
+
+
+
+
The documentation lives with the code, where it stays true.
+
This page keeps no copy of vump’s version number.
+
+
+
+ );
+}
diff --git a/website/src/components/Nav.tsx b/website/src/components/Nav.tsx
new file mode 100644
index 0000000..b68ada0
--- /dev/null
+++ b/website/src/components/Nav.tsx
@@ -0,0 +1,81 @@
+import { links } from '../content/links.ts';
+import { cn } from '../lib/cn.ts';
+import { useScrolled } from '../lib/hooks.ts';
+import { useLatestRelease } from '../lib/release.ts';
+import { GitHubIcon, Mark } from './ui/icons.tsx';
+import { ThemeToggle } from './ui/ThemeToggle.tsx';
+
+const SECTIONS = [
+ { href: '#defect', label: 'Why' },
+ { href: '#rule', label: 'The rule' },
+ { href: '#files', label: 'Files' },
+ { href: '#ci', label: 'CI' },
+ { href: '#install', label: 'Install' },
+];
+
+function ReleaseChip() {
+ const release = useLatestRelease();
+ if (!release) return null;
+
+ return (
+
+
+ {release.version}
+ {release.prerelease ? pre : null}
+
+ );
+}
+
+export function Nav() {
+ const scrolled = useScrolled();
+
+ return (
+
+ );
+}
diff --git a/website/src/components/sections/Ci.tsx b/website/src/components/sections/Ci.tsx
new file mode 100644
index 0000000..0d2b7a9
--- /dev/null
+++ b/website/src/components/sections/Ci.tsx
@@ -0,0 +1,85 @@
+import { links } from '../../content/links.ts';
+import { CI_WORKFLOW, EXIT_CODES } from '../../content/rules.ts';
+import { checkPassRun } from '../../content/runs.ts';
+import { Code } from '../ui/Code.tsx';
+import { ArrowIcon } from '../ui/icons.tsx';
+import { Reveal } from '../ui/Reveal.tsx';
+import { Section } from '../ui/Section.tsx';
+import { Terminal } from '../ui/Terminal.tsx';
+
+export function Ci() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ The action resolves the release binary by name and verifies it against the release’s
+ published SHA256SUMS{' '}
+ before running it.
+
+
+ Inputs and defaults
+
+
+
+
+
+
+
+
+
+
+ Exit codes are a stable contract
+
+
+
+ Each distinct failure has its own code, so a caller branches on a number rather than
+ matching strings. --json{' '}
+ renders every command’s result — bumps and errors alike — from the same structured value
+ the human output comes from, so neither can carry information the other lacks.
+
+
+
+
+
+ {EXIT_CODES.map((entry) => (
+
+
+ {entry.code}
+
+ {entry.meaning}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/website/src/components/sections/Defect.tsx b/website/src/components/sections/Defect.tsx
new file mode 100644
index 0000000..f46f423
--- /dev/null
+++ b/website/src/components/sections/Defect.tsx
@@ -0,0 +1,56 @@
+import { checkFailRun } from '../../content/runs.ts';
+import { Reveal } from '../ui/Reveal.tsx';
+import { Section } from '../ui/Section.tsx';
+import { Terminal } from '../ui/Terminal.tsx';
+
+const SPEC = [
+ { key: 'Exit code', value: '4' },
+ { key: 'Reported by', value: 'vump check' },
+ { key: 'Spent so far', value: 'nothing' },
+];
+
+export function Defect() {
+ return (
+
+ A tag that disagrees with its source is a{' '}
+ defect .
+ >
+ }
+ lede="A version number lives in more than one file, and files drift. The build that notices is usually the one that has already compiled, signed and published under a number that was never true — and the repair costs a deleted tag and a redone release."
+ >
+
+
+
+
+
+
+
+
+ Put the check first in the workflow and a bad tag is rejected in seconds, before a
+ runner has spent a minute on it.
+
+
+
+ {SPEC.map((row) => (
+
+
{row.key}
+ {row.value}
+
+ ))}
+
+
+
+ Every failure mode has its own exit code, so a script branches on the number instead
+ of matching strings against an error message.
+
+
+
+
+
+ );
+}
diff --git a/website/src/components/sections/Files.tsx b/website/src/components/sections/Files.tsx
new file mode 100644
index 0000000..febfd46
--- /dev/null
+++ b/website/src/components/sections/Files.tsx
@@ -0,0 +1,144 @@
+import { useRef, useState, type KeyboardEvent } from 'react';
+
+import { TRACKED_FILES } from '../../content/files.ts';
+import { repairRun } from '../../content/runs.ts';
+import { cn } from '../../lib/cn.ts';
+import { Code } from '../ui/Code.tsx';
+import { Reveal } from '../ui/Reveal.tsx';
+import { Section } from '../ui/Section.tsx';
+import { Terminal } from '../ui/Terminal.tsx';
+
+export function Files() {
+ const [selected, setSelected] = useState(0);
+ const tabs = useRef>([]);
+
+ const onKeyDown = (event: KeyboardEvent) => {
+ const step = event.key === 'ArrowDown' || event.key === 'ArrowRight' ? 1 : 0;
+ const back = event.key === 'ArrowUp' || event.key === 'ArrowLeft' ? -1 : 0;
+ if (!step && !back) return;
+
+ event.preventDefault();
+ const next = (selected + step + back + TRACKED_FILES.length) % TRACKED_FILES.length;
+ setSelected(next);
+ tabs.current[next]?.focus();
+ };
+
+ const file = TRACKED_FILES[selected] ?? TRACKED_FILES[0];
+ if (!file) return null;
+
+ return (
+
+
+
+
+ {TRACKED_FILES.map((entry, index) => (
+ {
+ tabs.current[index] = node;
+ }}
+ onClick={() => setSelected(index)}
+ onKeyDown={onKeyDown}
+ className={cn(
+ 'flex shrink-0 items-baseline justify-between gap-4 rounded-lg px-3.5 py-2.5 text-left transition-colors lg:shrink',
+ index === selected
+ ? 'bg-signal-soft text-ink'
+ : 'text-muted hover:bg-ink/[0.04] hover:text-ink',
+ )}
+ >
+ {entry.name}
+
+ {entry.ecosystem}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
Version lives in
+
{file.where}
+
+
+
Never mistaken for it
+
{file.guard}
+
+
+
+
+ .fsproj and{' '}
+ .vbproj are read exactly like{' '}
+ .csproj, and{' '}
+ Directory.Build.targets like{' '}
+ Directory.Build.props — except that a targets file
+ overrides a project’s own version, where a props file only supplies a default.
+
+
+
+
+
+
+
+
+ Lock files move with the manifest
+
+
+ Cargo.lock and{' '}
+ package-lock.json record their
+ project’s own version, and{' '}
+ cargo build --locked and{' '}
+ npm ci both reject a tree where
+ a lock and its manifest disagree. vump writes them in the same run, so nothing is left
+ to finish afterwards. If a lock file records your version but is missing from the
+ configuration, it stops before writing anything and names it.
+
+
+ This is not vump running a package manager, which it never does. The test is whether the
+ result can be computed with no network and no knowledge of the dependency graph.
+
+
+
+
+ And when they disagree
+
+ A bump requires the tracked files to agree, and refuses when they do not — a source of
+ truth contradicting itself is something to look at, not to guess about.
+
+
+
+
+
+ );
+}
diff --git a/website/src/components/sections/Hero.tsx b/website/src/components/sections/Hero.tsx
new file mode 100644
index 0000000..660c021
--- /dev/null
+++ b/website/src/components/sections/Hero.tsx
@@ -0,0 +1,77 @@
+import { TRACKED_FILES } from '../../content/files.ts';
+import { links } from '../../content/links.ts';
+import { bumpRun } from '../../content/runs.ts';
+import { Button } from '../ui/Button.tsx';
+import { ArrowIcon } from '../ui/icons.tsx';
+import { Reveal } from '../ui/Reveal.tsx';
+import { Terminal } from '../ui/Terminal.tsx';
+
+export function Hero() {
+ return (
+
+
+
+
+
+
+
+
+ One binary · macOS, Linux, Windows
+
+
+
+
+ One version.
+
+ Every file.
+
+ Proven at the tag.
+
+
+
+
+
+ vump moves every version number a repository records in lockstep, then fails the
+ build before a tag that disagrees with its source can cost you anything.
+
+
+
+
+
+
Install vump
+
+ Read the manual
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Reads and writes
+
+ {TRACKED_FILES.map((file) => (
+
+ {file.name}
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/website/src/components/sections/Install.tsx b/website/src/components/sections/Install.tsx
new file mode 100644
index 0000000..f879a00
--- /dev/null
+++ b/website/src/components/sections/Install.tsx
@@ -0,0 +1,90 @@
+import { links } from '../../content/links.ts';
+import { initRun, selfRun } from '../../content/runs.ts';
+import { Button } from '../ui/Button.tsx';
+import { Code } from '../ui/Code.tsx';
+import { ArrowIcon } from '../ui/icons.tsx';
+import { Reveal } from '../ui/Reveal.tsx';
+import { Terminal } from '../ui/Terminal.tsx';
+
+const SOURCE = `git clone https://github.com/okcodes/vump
+cd vump
+cargo install --path .`;
+
+export function Install() {
+ return (
+
+
+
+
+
+
+
+ 08
+ Install
+
+
+ Two commands from here.
+
+
+ vump init writes a configuration
+ tracking the version files it finds. After that, a bump is one word.
+
+
+
+
+
+
+
Download a binary
+
+
+ A signed universal binary for macOS, static musl builds for Linux, and Windows
+ including arm64. Every release publishes checksums, and both the installer and the
+ CI action verify against them.
+
+
+ Releases
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ vump self update keeps the
+ binary current, and --channel{' '}
+ names the least mature release you will accept — a floor rather than an exact match,
+ so tracking release candidates never silently moves you onto the next minor’s first
+ alpha.
+
+
+
+
+
+ );
+}
diff --git a/website/src/components/sections/Projects.tsx b/website/src/components/sections/Projects.tsx
new file mode 100644
index 0000000..add9cbd
--- /dev/null
+++ b/website/src/components/sections/Projects.tsx
@@ -0,0 +1,54 @@
+import { MULTI_PROJECT_CONFIG } from '../../content/rules.ts';
+import { statusRun } from '../../content/runs.ts';
+import { Code } from '../ui/Code.tsx';
+import { Reveal } from '../ui/Reveal.tsx';
+import { Section } from '../ui/Section.tsx';
+import { Terminal } from '../ui/Terminal.tsx';
+
+export function Projects() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tags that name their project
+
+ Projects moving independently need distinguishable tags, or they collide the moment two
+ reach the same version and nothing can say which project a pushed tag describes. One{' '}
+ tag_pattern covers every
+ project, or each overrides its own. If two would produce the same tag, vump says so
+ rather than guessing — and CI can then pass a pushed tag straight through without
+ knowing which project it names.
+
+
+
+
+ One vump.toml per repository
+
+ A second configuration deeper in the tree is what{' '}
+ [[project]] exists to prevent:
+ scattered configurations cannot be addressed by name, git operations still run against
+ the enclosing repository, and a pushed tag can no longer be traced back to what produced
+ it. vump refuses that rather than warning about it — a warning arrives too late to help
+ once a tag exists.
+
+
+
+
+ );
+}
diff --git a/website/src/components/sections/Rule.tsx b/website/src/components/sections/Rule.tsx
new file mode 100644
index 0000000..70aff63
--- /dev/null
+++ b/website/src/components/sections/Rule.tsx
@@ -0,0 +1,47 @@
+import { guidedRun, scriptedRun } from '../../content/runs.ts';
+import { Reveal } from '../ui/Reveal.tsx';
+import { Section } from '../ui/Section.tsx';
+import { Terminal } from '../ui/Terminal.tsx';
+
+export function Rule() {
+ return (
+
+
+
+
+
+ vump
+ asks, then confirms the whole plan
+
+
+
+
+
+
+
+
+ vump patch
+ never asks anything, ever
+
+
+
+
+
+
+
+
+ Anything a subcommand would otherwise have to ask about is a required flag, or an error
+ that names what is missing. That is what makes vump safe to put in a script, a CI job, or
+ an agent’s toolbelt: a command either has everything it needs, or it fails saying what it
+ does not.
+
+
+
+ );
+}
diff --git a/website/src/components/sections/Safety.tsx b/website/src/components/sections/Safety.tsx
new file mode 100644
index 0000000..51252c3
--- /dev/null
+++ b/website/src/components/sections/Safety.tsx
@@ -0,0 +1,36 @@
+import { GUARANTEES } from '../../content/rules.ts';
+import { Reveal } from '../ui/Reveal.tsx';
+import { Section } from '../ui/Section.tsx';
+
+export function Safety() {
+ return (
+
+
+ {GUARANTEES.map((guarantee, index) => (
+
+
+
✓
+
{guarantee.title}
+
{guarantee.detail}
+
+
+ ))}
+
+
+
+
+ Releases carry keyless build provenance, signed with a short-lived certificate from the
+ workflow’s own identity and recorded in a public transparency log. There is no key to
+ store, rotate, or leak.
+
+
+
+ );
+}
diff --git a/website/src/components/sections/Versions.tsx b/website/src/components/sections/Versions.tsx
new file mode 100644
index 0000000..9192637
--- /dev/null
+++ b/website/src/components/sections/Versions.tsx
@@ -0,0 +1,59 @@
+import { REFUSALS, TRANSITIONS } from '../../content/rules.ts';
+import { Reveal } from '../ui/Reveal.tsx';
+import { Section } from '../ui/Section.tsx';
+
+export function Versions() {
+ return (
+
+
+
+
+ Current
+ Command
+ Result
+
+ {TRANSITIONS.map((row) => (
+
+ {row.from}
+
+ vump
+ {row.command}
+
+ {row.result}
+
+ ))}
+
+
+
+
+
+
+ Refused, deliberately
+
+
+
+
+
+ {REFUSALS.map((refusal, index) => (
+
+
+
✗
+
{refusal.title}
+
{refusal.reason}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/website/src/components/ui/Button.tsx b/website/src/components/ui/Button.tsx
new file mode 100644
index 0000000..8ff9bf6
--- /dev/null
+++ b/website/src/components/ui/Button.tsx
@@ -0,0 +1,34 @@
+import type { ReactNode } from 'react';
+
+import { cn } from '../../lib/cn.ts';
+
+interface Props {
+ href: string;
+ children: ReactNode;
+ variant?: 'solid' | 'outline' | 'ghost';
+ external?: boolean;
+ className?: string;
+}
+
+const VARIANTS = {
+ solid:
+ 'bg-signal text-signal-ink hover:brightness-[1.08] shadow-[0_10px_30px_-12px_var(--signal)]',
+ outline: 'border border-line-strong text-ink hover:bg-ink/[0.04]',
+ ghost: 'text-muted hover:text-ink',
+} as const;
+
+export function Button({ href, children, variant = 'solid', external = false, className }: Props) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/website/src/components/ui/Code.tsx b/website/src/components/ui/Code.tsx
new file mode 100644
index 0000000..a237e6e
--- /dev/null
+++ b/website/src/components/ui/Code.tsx
@@ -0,0 +1,69 @@
+import { cn } from '../../lib/cn.ts';
+import { highlight, type Lang, type TokenKind } from '../../lib/highlight.ts';
+import { CopyButton } from './CopyButton.tsx';
+
+const KIND: Record = {
+ text: 'text-ink/90',
+ comment: 'text-faint italic',
+ section: 'text-ink font-medium',
+ key: 'text-ink/90',
+ value: 'text-signal',
+ expr: 'text-warn',
+ punct: 'text-faint',
+};
+
+interface Props {
+ code: string;
+ lang: Lang;
+ /** The file this snippet belongs in. Shown in the header. */
+ title?: string;
+ /** Marks lines (1-based) as the ones that matter. */
+ emphasis?: number[];
+ className?: string;
+ copyable?: boolean;
+}
+
+export function Code({ code, lang, title, emphasis, className, copyable = true }: Props) {
+ const lines = highlight(code, lang);
+ const marked = new Set(emphasis ?? []);
+
+ return (
+
+ {title || copyable ? (
+
+ {title}
+ {copyable ? : null}
+
+ ) : null}
+
+
+
+ {lines.map((tokens, index) => {
+ const highlighted = marked.has(index + 1);
+ return (
+
+ {tokens.length === 0 ? ' ' : null}
+ {tokens.map((token, tokenIndex) => (
+
+ {token.text}
+
+ ))}
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/website/src/components/ui/CopyButton.tsx b/website/src/components/ui/CopyButton.tsx
new file mode 100644
index 0000000..b7222f4
--- /dev/null
+++ b/website/src/components/ui/CopyButton.tsx
@@ -0,0 +1,36 @@
+import { useCopy } from '../../lib/hooks.ts';
+import { cn } from '../../lib/cn.ts';
+import { CheckIcon, CopyIcon } from './icons.tsx';
+
+interface Props {
+ value: string;
+ label?: string;
+ className?: string;
+ tone?: 'terminal' | 'page';
+}
+
+export function CopyButton({ value, label = 'Copy', className, tone = 'page' }: Props) {
+ const { copied, copy } = useCopy(value);
+
+ return (
+
+ {copied ? (
+
+ ) : (
+
+ )}
+ {copied ? 'Copied' : label}
+
+ );
+}
diff --git a/website/src/components/ui/Reveal.tsx b/website/src/components/ui/Reveal.tsx
new file mode 100644
index 0000000..2d41c10
--- /dev/null
+++ b/website/src/components/ui/Reveal.tsx
@@ -0,0 +1,26 @@
+import type { ReactNode } from 'react';
+
+import { cn } from '../../lib/cn.ts';
+import { useInView } from '../../lib/hooks.ts';
+
+interface Props {
+ children: ReactNode;
+ /** Milliseconds to hold before entering, for staggering a group. */
+ delay?: number;
+ className?: string;
+}
+
+export function Reveal({ children, delay = 0, className }: Props) {
+ const { ref, inView } = useInView();
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/website/src/components/ui/Section.tsx b/website/src/components/ui/Section.tsx
new file mode 100644
index 0000000..87ae3d1
--- /dev/null
+++ b/website/src/components/ui/Section.tsx
@@ -0,0 +1,42 @@
+import type { ReactNode } from 'react';
+
+import { cn } from '../../lib/cn.ts';
+import { Reveal } from './Reveal.tsx';
+
+interface Props {
+ id: string;
+ /** The section's number, in the instrument's own voice. */
+ index: string;
+ eyebrow: string;
+ title: ReactNode;
+ lede?: ReactNode;
+ children: ReactNode;
+ className?: string;
+}
+
+export function Section({ id, index, eyebrow, title, lede, children, className }: Props) {
+ return (
+
+
+
+
+ {index}
+ {eyebrow}
+
+
+
+ {title}
+
+
+ {lede ? (
+
+ {lede}
+
+ ) : null}
+
+
+
{children}
+
+
+ );
+}
diff --git a/website/src/components/ui/Terminal.tsx b/website/src/components/ui/Terminal.tsx
new file mode 100644
index 0000000..14570e6
--- /dev/null
+++ b/website/src/components/ui/Terminal.tsx
@@ -0,0 +1,128 @@
+import { useEffect, useState } from 'react';
+
+import { cn } from '../../lib/cn.ts';
+import { useInView, useReducedMotion } from '../../lib/hooks.ts';
+import { plain, type Line, type Tone } from '../../lib/terminal.ts';
+import { CopyButton } from './CopyButton.tsx';
+
+const TONES: Record = {
+ ink: 'text-term-ink',
+ muted: 'text-term-muted',
+ signal: 'text-term-signal',
+ warn: 'text-term-warn',
+ danger: 'text-term-danger',
+};
+
+const CHAR_MS = 24;
+const AFTER_COMMAND_MS = 320;
+const BETWEEN_LINES_MS = 80;
+
+interface Props {
+ lines: Line[];
+ /** Shown in the pane's header. A path, or the shell being used. */
+ title?: string;
+ /** Types the run out on first sight. Off for panes that are pure reference. */
+ animate?: boolean;
+ /** Overrides what the copy button yields. Defaults to the commands only. */
+ copy?: string;
+ className?: string;
+}
+
+/**
+ * A pane of command output.
+ *
+ * Unrevealed text is rendered transparent rather than omitted, so the pane is
+ * its final size from the first frame and the page never reflows around it
+ * while a run plays.
+ */
+export function Terminal({ lines, title, animate = false, copy, className }: Props) {
+ const reduced = useReducedMotion();
+ const { ref, inView } = useInView('0px 0px -15% 0px');
+ const playing = animate && !reduced && inView;
+ const [typed, setTyped] = useState(() => ({ line: 0, char: 0 }));
+ const cursor = playing ? typed : { line: lines.length, char: 0 };
+
+ useEffect(() => {
+ if (!playing) return;
+
+ let timer = 0;
+ let stopped = false;
+
+ const step = (line: number, char: number) => {
+ if (stopped || line >= lines.length) return;
+ const target = lines[line];
+ if (!target) return;
+
+ const width = target.segs.reduce((total, s) => total + s.text.length, 0);
+ const typing = target.prompt && char < width;
+ const delay = typing ? CHAR_MS : target.prompt ? AFTER_COMMAND_MS : BETWEEN_LINES_MS;
+ const next = typing ? { line, char: char + 1 } : { line: line + 1, char: 0 };
+
+ timer = window.setTimeout(() => {
+ setTyped(next);
+ step(next.line, next.char);
+ }, delay);
+ };
+
+ step(0, 0);
+
+ return () => {
+ stopped = true;
+ window.clearTimeout(timer);
+ };
+ }, [lines, playing]);
+
+ const copyValue =
+ copy ??
+ lines
+ .filter((line) => line.prompt)
+ .map((line) => plain(line).replace(/^\$ /, ''))
+ .join('\n');
+
+ return (
+
+
+
+ {title ?? 'zsh'}
+
+ {copyValue ? : null}
+
+
+
+
+ {lines.map((line, index) => {
+ const revealed = index < cursor.line;
+ const active = index === cursor.line;
+ let budget = active ? cursor.char : revealed ? Number.POSITIVE_INFINITY : 0;
+
+ return (
+
+ {line.prompt ? (
+ $
+ ) : null}
+ {line.segs.map((seg, segIndex) => {
+ const shown = Math.max(0, Math.min(seg.text.length, budget));
+ budget -= seg.text.length;
+ return (
+
+ {seg.text.slice(0, shown)}
+ {seg.text.slice(shown)}
+
+ );
+ })}
+ {playing && active && line.prompt ? : null}
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/website/src/components/ui/ThemeToggle.tsx b/website/src/components/ui/ThemeToggle.tsx
new file mode 100644
index 0000000..41b08de
--- /dev/null
+++ b/website/src/components/ui/ThemeToggle.tsx
@@ -0,0 +1,17 @@
+import { useTheme } from '../../lib/theme.ts';
+import { MoonIcon, SunIcon } from './icons.tsx';
+
+export function ThemeToggle() {
+ const { theme, toggle } = useTheme();
+
+ return (
+
+ {theme === 'dark' ? : }
+
+ );
+}
diff --git a/website/src/components/ui/icons.tsx b/website/src/components/ui/icons.tsx
new file mode 100644
index 0000000..448b675
--- /dev/null
+++ b/website/src/components/ui/icons.tsx
@@ -0,0 +1,98 @@
+/** Inline icons, drawn to one grid: 16px box, 1.6 stroke, round caps. */
+
+type Props = { className?: string };
+
+const base = {
+ viewBox: '0 0 16 16',
+ fill: 'none',
+ stroke: 'currentColor',
+ strokeWidth: 1.6,
+ strokeLinecap: 'round' as const,
+ strokeLinejoin: 'round' as const,
+ 'aria-hidden': true,
+};
+
+export function CopyIcon({ className }: Props) {
+ return (
+
+
+
+
+ );
+}
+
+export function CheckIcon({ className }: Props) {
+ return (
+
+
+
+ );
+}
+
+export function ArrowIcon({ className }: Props) {
+ return (
+
+
+
+ );
+}
+
+export function GitHubIcon({ className }: Props) {
+ return (
+
+
+
+ );
+}
+
+export function SunIcon({ className }: Props) {
+ return (
+
+
+
+
+ );
+}
+
+export function MoonIcon({ className }: Props) {
+ return (
+
+
+
+ );
+}
+
+/**
+ * The mark: a bump over a baseline. It is the favicon at 16px too, which is why
+ * it is two strokes and nothing else.
+ */
+export function Mark({ className }: Props) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/website/src/content/files.ts b/website/src/content/files.ts
new file mode 100644
index 0000000..6b6b33a
--- /dev/null
+++ b/website/src/content/files.ts
@@ -0,0 +1,145 @@
+import type { Lang } from '../lib/highlight.ts';
+
+export interface TrackedFile {
+ /** The filename vump recognizes. */
+ name: string;
+ ecosystem: string;
+ /** Where in the file the project's own version lives. */
+ where: string;
+ /** What is deliberately never mistaken for it. */
+ guard: string;
+ lang: Lang;
+ code: string;
+ /** 1-based lines vump writes. */
+ emphasis: number[];
+}
+
+export const TRACKED_FILES: TrackedFile[] = [
+ {
+ name: 'package.json',
+ ecosystem: 'npm',
+ where: 'top-level version',
+ guard: 'A version nested under dependencies is never mistaken for the project’s own.',
+ lang: 'json',
+ emphasis: [3],
+ code: `{
+ "name": "@acme/widget",
+ "version": "1.4.0",
+ "dependencies": {
+ "react": "^19.0.0"
+ }
+}`,
+ },
+ {
+ name: 'package-lock.json',
+ ecosystem: 'npm',
+ where: 'top-level version, and the root packages entry',
+ guard: 'Both move together, because npm ci rejects a tree where they disagree.',
+ lang: 'json',
+ emphasis: [3, 7],
+ code: `{
+ "name": "@acme/widget",
+ "version": "1.4.0",
+ "lockfileVersion": 3,
+ "packages": {
+ "": {
+ "version": "1.4.0"
+ }
+ }
+}`,
+ },
+ {
+ name: 'Cargo.toml',
+ ecosystem: 'Cargo',
+ where: '[package].version',
+ guard: 'Editing the document structurally cannot reach a dependency’s own pin.',
+ lang: 'toml',
+ emphasis: [3],
+ code: `[package]
+name = "widget"
+version = "1.4.0"
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }`,
+ },
+ {
+ name: 'Cargo.lock',
+ ecosystem: 'Cargo',
+ where: 'the [[package]] entry for this crate',
+ guard: 'Matched by the package name the manifest declares — a locked dependency is left alone.',
+ lang: 'toml',
+ emphasis: [3],
+ code: `[[package]]
+name = "widget"
+version = "1.4.0"
+
+[[package]]
+name = "serde"
+version = "1.0.229"`,
+ },
+ {
+ name: 'pyproject.toml',
+ ecosystem: 'Python',
+ where: '[project].version',
+ guard: 'Requirements pinned under dependencies are requirements, not this project’s version.',
+ lang: 'toml',
+ emphasis: [3],
+ code: `[project]
+name = "widget"
+version = "1.4.0"
+dependencies = ["httpx>=0.28"]`,
+ },
+ {
+ name: 'uv.lock',
+ ecosystem: 'Python',
+ where: 'the [[package]] entry for this project',
+ guard: 'Written in the same run as pyproject.toml, so the pair is never half-bumped.',
+ lang: 'toml',
+ emphasis: [3],
+ code: `[[package]]
+name = "widget"
+version = "1.4.0"
+source = { editable = "." }`,
+ },
+ {
+ name: '*.csproj',
+ ecosystem: '.NET',
+ where: ' in a ',
+ guard:
+ 'Not , which has four numeric parts and no pre-release. Not a .',
+ lang: 'xml',
+ emphasis: [3],
+ code: `
+
+ 1.4.0
+ 1.4.0.0
+
+
+
+
+ `,
+ },
+ {
+ name: 'Directory.Build.props',
+ ecosystem: '.NET',
+ where: ' in a ',
+ guard:
+ 'Where projects sharing one version declare it. MSBuild stops at the nearest one rather than merging several.',
+ lang: 'xml',
+ emphasis: [3],
+ code: `
+
+ 1.4.0
+
+ `,
+ },
+ {
+ name: 'VERSION',
+ ecosystem: 'any',
+ where: 'the whole file',
+ guard: 'The escape hatch: any ecosystem, any build system, one line.',
+ lang: 'toml',
+ emphasis: [1],
+ code: `1.4.0`,
+ },
+];
diff --git a/website/src/content/links.ts b/website/src/content/links.ts
new file mode 100644
index 0000000..f12237a
--- /dev/null
+++ b/website/src/content/links.ts
@@ -0,0 +1,12 @@
+const REPO = 'https://github.com/okcodes/vump';
+
+export const links = {
+ repo: REPO,
+ readme: `${REPO}#readme`,
+ releases: `${REPO}/releases`,
+ design: `${REPO}/blob/main/DESIGN.md`,
+ contributing: `${REPO}/blob/main/CONTRIBUTING.md`,
+ backlog: `${REPO}/blob/main/BACKLOG.md`,
+ action: `${REPO}/tree/main/.github/actions/check`,
+ sandbox: `${REPO}/tree/main/sandbox`,
+} as const;
diff --git a/website/src/content/rules.ts b/website/src/content/rules.ts
new file mode 100644
index 0000000..b9531c4
--- /dev/null
+++ b/website/src/content/rules.ts
@@ -0,0 +1,132 @@
+export interface Transition {
+ from: string;
+ command: string;
+ result: string;
+}
+
+/** The version state machine, in the order the manual states it. */
+export const TRANSITIONS: Transition[] = [
+ { from: '1.2.3', command: 'patch', result: '1.2.4' },
+ { from: '1.2.3', command: 'minor', result: '1.3.0' },
+ { from: '1.2.3', command: 'major', result: '2.0.0' },
+ { from: '1.2.3', command: 'alpha --toward minor', result: '1.3.0-alpha.0' },
+ { from: '1.2.3-alpha.0', command: 'alpha', result: '1.2.3-alpha.1' },
+ { from: '1.2.3-alpha.2', command: 'beta', result: '1.2.3-beta.0' },
+ { from: '1.2.3-rc.1', command: 'release', result: '1.2.3' },
+];
+
+export interface Refusal {
+ title: string;
+ reason: string;
+}
+
+export const REFUSALS: Refusal[] = [
+ {
+ title: 'Moving to a less mature channel',
+ reason:
+ 'rc back to beta. There is no flag to force it, because there is no workflow that wants it.',
+ },
+ {
+ title: 'patch, minor or major while on a pre-release',
+ reason:
+ 'It is ambiguous between finalizing and abandoning. Run release first, then bump from there.',
+ },
+ {
+ title: 'A pre-release from a stable version',
+ reason:
+ 'Without --toward, nothing says which release it precedes. The flag is required, not guessed at.',
+ },
+];
+
+export interface Guarantee {
+ title: string;
+ detail: string;
+}
+
+export const GUARANTEES: Guarantee[] = [
+ {
+ title: 'A dirty tree stops the run',
+ detail:
+ 'Checked before anything is written, not after — so a refused release leaves the working tree exactly as it found it.',
+ },
+ {
+ title: 'Only declared files are staged',
+ detail:
+ 'Unrelated work in the tree cannot ride along in a version-bump commit, however it got there.',
+ },
+ {
+ title: 'Nothing is half-applied',
+ detail:
+ 'Everything knowable before writing is checked before writing, so a run that cannot finish cleanly does nothing at all.',
+ },
+ {
+ title: 'Partial success is reported exactly',
+ detail:
+ 'A push that fails after the commit and tag exits 8 and prints the command that finishes the job by hand.',
+ },
+ {
+ title: 'Downloads are verified',
+ detail:
+ 'Every release publishes SHA256SUMS, checked before a binary is written to disk or run. A release publishing none is refused, not warned about.',
+ },
+ {
+ title: 'No package manager is ever run',
+ detail:
+ 'No dependency resolution, no registry, no install scripts. An in-place edit that needs neither a network nor a dependency graph.',
+ },
+];
+
+export interface ExitCode {
+ code: number;
+ meaning: string;
+}
+
+export const EXIT_CODES: ExitCode[] = [
+ { code: 0, meaning: 'Success' },
+ { code: 1, meaning: 'Unexpected failure, a declined prompt, or an update is available' },
+ { code: 2, meaning: 'Usage error' },
+ { code: 3, meaning: 'Configuration missing or invalid' },
+ { code: 4, meaning: 'Version mismatch — check failed' },
+ { code: 5, meaning: 'Tracked files disagree with each other' },
+ { code: 6, meaning: 'Working tree dirty' },
+ { code: 7, meaning: 'Invalid version transition' },
+ { code: 8, meaning: 'Git operation failed' },
+ { code: 9, meaning: 'A release artifact could not be trusted' },
+ { code: 10, meaning: 'A release could not be obtained' },
+];
+
+export const CI_WORKFLOW = `on:
+ push:
+ tags: ['v*']
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ # First, so a tag that lies costs nothing.
+ - uses: okcodes/vump/.github/actions/check@main
+ with:
+ version: \${{ github.ref_name }}
+
+ - run: cargo build --release`;
+
+export const MULTI_PROJECT_CONFIG = `[git]
+tag_pattern = "{project}-v{new_version}"
+
+[[project]]
+name = "api"
+files = ["services/api/Cargo.toml", "Cargo.lock"]
+
+[[project]]
+name = "web"
+files = ["apps/web/package.json"]`;
+
+export const CONFIG = `files = ["VERSION", "ui/package.json"]
+
+[git]
+through = "tag" # none, commit, tag, or push
+commit_message = "chore: bump version to v{new_version}"
+tag_pattern = "v{new_version}"
+release_branches = ["main"]`;
diff --git a/website/src/content/runs.ts b/website/src/content/runs.ts
new file mode 100644
index 0000000..4fca591
--- /dev/null
+++ b/website/src/content/runs.ts
@@ -0,0 +1,114 @@
+/**
+ * Every pane on this page is what the binary prints.
+ *
+ * The marks, the column widths, the wording of the errors and the blank lines
+ * between them are copied from vump's own renderer rather than invented for the
+ * screenshot. A site that shows friendlier output than the tool produces is
+ * advertising something that does not exist.
+ */
+
+import { bad, caution, cmd, dim, gap, out, sig, t, type Line } from '../lib/terminal.ts';
+
+/** A patch bump carried through to a tag. */
+export const bumpRun: Line[] = [
+ cmd('vump patch --through tag'),
+ out(sig('✓ '), t('0.2.0 -> '), sig('0.2.1')),
+ out(dim(' Cargo.toml')),
+ out(dim(' Cargo.lock')),
+ out(sig('✓ '), t('committed '), dim('chore: bump version to v0.2.1')),
+ out(sig('✓ '), t('tagged '), sig('v0.2.1')),
+ gap(),
+ out(dim('To push:')),
+ out(dim(' git push && git push origin v0.2.1')),
+];
+
+/** A tag that does not describe the source it claims to. */
+export const checkFailRun: Line[] = [
+ cmd('vump check v1.4.0'),
+ out(bad('✗ '), t('version mismatch: expected 1.4.0')),
+ gap(),
+ out(bad(' ✗ '), t('Cargo.toml '), bad('1.3.2')),
+ out(sig(' ✓ '), t('package.json '), t('1.4.0')),
+];
+
+/** The same check, on a tag that tells the truth. */
+export const checkPassRun: Line[] = [
+ cmd('vump check v1.4.0'),
+ out(sig('✓ '), t('2 files match 1.4.0')),
+];
+
+/** Omitting a subcommand: vump asks, then shows the whole plan before acting. */
+export const guidedRun: Line[] = [
+ cmd('vump'),
+ out(caution('? '), t('Current version 1.2.3. Bump to: '), sig('patch -> 1.2.4')),
+ out(caution('? '), t('Carry the release through: '), sig('Tag — commit and tag')),
+ gap(),
+ out(dim(' Bumping: '), t('1.2.3 -> '), sig('1.2.4')),
+ out(dim(' File: '), t('Cargo.toml')),
+ out(dim(' File: '), t('Cargo.lock')),
+ out(dim(' Commit: '), t('chore: bump version to v1.2.4')),
+ out(dim(' Tag: '), t('v1.2.4')),
+ out(dim(' Push: '), t('no')),
+ gap(),
+ out(caution('? '), t('Proceed? '), sig('Yes')),
+ out(sig('✓ '), t('1.2.3 -> '), sig('1.2.4')),
+];
+
+/** Naming one: no questions, and a missing decision is an error instead. */
+export const scriptedRun: Line[] = [
+ cmd('vump patch --through tag'),
+ out(sig('✓ '), t('1.2.3 -> '), sig('1.2.4')),
+ out(dim(' Cargo.toml')),
+ out(sig('✓ '), t('committed '), dim('chore: bump version to v1.2.4')),
+ out(sig('✓ '), t('tagged '), sig('v1.2.4')),
+ gap(),
+ cmd('vump alpha'),
+ out(bad('error: '), t('1.2.3 is stable, so `alpha` needs to know which')),
+ out(t('release it leads to; pass --toward patch, --toward minor,')),
+ out(t('or --toward major')),
+];
+
+/** Files that have drifted apart, and the one command that repairs them. */
+export const repairRun: Line[] = [
+ cmd('vump patch'),
+ out(bad('error: '), t('tracked files disagree about the current version:')),
+ out(t(' VERSION '), bad('1.2.3')),
+ out(t(' package.json '), bad('0.9.0')),
+ gap(),
+ cmd('vump set 2.0.0'),
+ out(sig('✓ '), t('set to '), sig('2.0.0')),
+ out(dim(' VERSION')),
+ out(dim(' package.json')),
+];
+
+/** Every project in the repository, at a glance. */
+export const statusRun: Line[] = [
+ cmd('vump status'),
+ out(sig('✓ '), t('api '), sig('1.4.2')),
+ out(sig('✓ '), t('web '), sig('0.9.0')),
+ gap(),
+ cmd('vump minor --project api'),
+ out(sig('✓ '), t('1.4.2 -> '), sig('1.5.0')),
+ out(dim(' services/api/Cargo.toml')),
+ out(dim(' Cargo.lock')),
+];
+
+/** Keeping the binary current. */
+export const selfRun: Line[] = [
+ cmd('vump self status'),
+ out(t('0.8.0 is available; running 0.7.0.')),
+ out(dim('Run `vump self update` to install it.')),
+ gap(),
+ cmd('vump self update'),
+ out(sig('✓ '), t('updated 0.7.0 -> '), sig('0.8.0')),
+];
+
+/** Adopting a repository: init finds the version files and writes them down. */
+export const initRun: Line[] = [
+ cmd('vump init'),
+ out(sig('✓ '), t('wrote vump.toml tracking:')),
+ out(dim(' Cargo.toml')),
+ out(dim(' Cargo.lock')),
+ gap(),
+ out(dim('Review it, then run `vump` to bump.')),
+];
diff --git a/website/src/index.css b/website/src/index.css
new file mode 100644
index 0000000..f830e90
--- /dev/null
+++ b/website/src/index.css
@@ -0,0 +1,234 @@
+@import 'tailwindcss';
+
+/*
+ * The palette is defined once as semantic roles and flipped by `data-theme` on
+ * the document element, so a component never names a literal colour and the two
+ * themes cannot drift apart. Dark is the default: it is the site's own look, and
+ * the light theme is a designed alternative rather than an inversion of it.
+ */
+:root,
+[data-theme='dark'] {
+ --ground: oklch(0.145 0.008 264);
+ --sunken: oklch(0.118 0.007 264);
+ --raised: oklch(0.18 0.0085 264);
+ --raised-2: oklch(0.212 0.009 264);
+ --ink: oklch(0.965 0.004 264);
+ --muted: oklch(0.73 0.008 264);
+ --faint: oklch(0.62 0.01 264);
+ --line: oklch(1 0 0 / 0.08);
+ --line-strong: oklch(1 0 0 / 0.17);
+ --signal: oklch(0.845 0.145 168);
+ --signal-ink: oklch(0.16 0.03 168);
+ --signal-soft: oklch(0.845 0.145 168 / 0.14);
+ --warn: oklch(0.81 0.13 78);
+ --danger: oklch(0.7 0.185 22);
+ --glow: oklch(0.845 0.145 168 / 0.1);
+ --grid: oklch(1 0 0 / 0.05);
+ --grain: 0.05;
+}
+
+[data-theme='light'] {
+ --ground: oklch(0.981 0.003 264);
+ --sunken: oklch(0.955 0.004 264);
+ --raised: oklch(1 0 0);
+ --raised-2: oklch(0.972 0.003 264);
+ --ink: oklch(0.21 0.012 264);
+ --muted: oklch(0.46 0.011 264);
+ --faint: oklch(0.52 0.011 264);
+ --line: oklch(0.21 0.012 264 / 0.12);
+ --line-strong: oklch(0.21 0.012 264 / 0.24);
+ --signal: oklch(0.54 0.125 168);
+ --signal-ink: oklch(0.99 0.01 168);
+ --signal-soft: oklch(0.54 0.125 168 / 0.1);
+ --warn: oklch(0.56 0.125 62);
+ --danger: oklch(0.53 0.195 25);
+ --glow: oklch(0.54 0.125 168 / 0.07);
+ --grid: oklch(0.21 0.012 264 / 0.055);
+ --grain: 0.025;
+}
+
+/*
+ * A terminal is a dark instrument in both themes. Holding it fixed keeps the
+ * site's identity in the light theme and keeps rendered command output looking
+ * like output rather than like a styled quotation.
+ */
+:root {
+ --term-bg: oklch(0.132 0.009 264);
+ --term-ink: oklch(0.92 0.005 264);
+ --term-muted: oklch(0.63 0.01 264);
+ --term-line: oklch(1 0 0 / 0.1);
+ --term-signal: oklch(0.845 0.145 168);
+ --term-warn: oklch(0.83 0.13 78);
+ --term-danger: oklch(0.72 0.17 22);
+}
+
+@theme inline {
+ --color-ground: var(--ground);
+ --color-sunken: var(--sunken);
+ --color-raised: var(--raised);
+ --color-raised-2: var(--raised-2);
+ --color-ink: var(--ink);
+ --color-muted: var(--muted);
+ --color-faint: var(--faint);
+ --color-line: var(--line);
+ --color-line-strong: var(--line-strong);
+ --color-signal: var(--signal);
+ --color-signal-ink: var(--signal-ink);
+ --color-signal-soft: var(--signal-soft);
+ --color-warn: var(--warn);
+ --color-danger: var(--danger);
+
+ --color-term-bg: var(--term-bg);
+ --color-term-ink: var(--term-ink);
+ --color-term-muted: var(--term-muted);
+ --color-term-line: var(--term-line);
+ --color-term-signal: var(--term-signal);
+ --color-term-warn: var(--term-warn);
+ --color-term-danger: var(--term-danger);
+
+ --font-sans:
+ 'Instrument Sans Variable', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
+ --font-mono:
+ 'JetBrains Mono Variable', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
+ --font-display: 'Instrument Serif', ui-serif, Georgia, 'Times New Roman', serif;
+
+ --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
+ --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
+}
+
+@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
+
+@layer base {
+ :root {
+ color-scheme: dark;
+ }
+
+ [data-theme='light'] {
+ color-scheme: light;
+ }
+
+ html {
+ scroll-behavior: smooth;
+ /* Anchored sections must clear the fixed masthead. */
+ scroll-padding-top: 6rem;
+ -webkit-text-size-adjust: 100%;
+ }
+
+ body {
+ background-color: var(--ground);
+ color: var(--ink);
+ font-family: var(--font-sans);
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ overflow-x: hidden;
+ }
+
+ ::selection {
+ background: var(--signal);
+ color: var(--signal-ink);
+ }
+
+ :focus-visible {
+ outline: 2px solid var(--signal);
+ outline-offset: 3px;
+ border-radius: 2px;
+ }
+
+ h1,
+ h2,
+ h3 {
+ text-wrap: balance;
+ }
+
+ p {
+ text-wrap: pretty;
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ html {
+ scroll-behavior: auto;
+ }
+
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+ }
+}
+
+@layer components {
+ /* Section numbers, field labels, table headers: the instrument's own voice. */
+ .label {
+ font-family: var(--font-mono);
+ font-size: 0.6875rem;
+ font-weight: 500;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--faint);
+ }
+
+ .rule {
+ border-color: var(--line);
+ }
+
+ /* Content enters once, on first sight. Nothing moves on the way out. */
+ .reveal {
+ opacity: 0;
+ transform: translateY(14px);
+ transition:
+ opacity 0.7s var(--ease-out-expo),
+ transform 0.7s var(--ease-out-expo);
+ }
+
+ .reveal[data-visible='true'] {
+ opacity: 1;
+ transform: none;
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .reveal {
+ opacity: 1;
+ transform: none;
+ }
+ }
+}
+
+@layer utilities {
+ .text-balance-none {
+ text-wrap: initial;
+ }
+
+ /* Hairline grid, faded out towards the edges so it never reads as a border. */
+ .grid-backdrop {
+ background-image:
+ linear-gradient(to right, var(--grid) 1px, transparent 1px),
+ linear-gradient(to bottom, var(--grid) 1px, transparent 1px);
+ background-size: 64px 64px;
+ mask-image: radial-gradient(ellipse 100% 70% at 50% 0%, black 20%, transparent 78%);
+ }
+
+ .caret::after {
+ content: '';
+ display: inline-block;
+ width: 0.55em;
+ height: 1.05em;
+ margin-left: 0.12em;
+ vertical-align: text-bottom;
+ background: var(--term-signal);
+ animation: blink 1.1s steps(1) infinite;
+ }
+
+ @keyframes blink {
+ 0%,
+ 50% {
+ opacity: 1;
+ }
+ 50.01%,
+ 100% {
+ opacity: 0;
+ }
+ }
+}
diff --git a/website/src/lib/cn.ts b/website/src/lib/cn.ts
new file mode 100644
index 0000000..9da92c2
--- /dev/null
+++ b/website/src/lib/cn.ts
@@ -0,0 +1,4 @@
+/** Joins class names, dropping anything falsy. */
+export function cn(...parts: Array): string {
+ return parts.filter(Boolean).join(' ');
+}
diff --git a/website/src/lib/highlight.ts b/website/src/lib/highlight.ts
new file mode 100644
index 0000000..9502235
--- /dev/null
+++ b/website/src/lib/highlight.ts
@@ -0,0 +1,61 @@
+/**
+ * A small, deliberately incomplete highlighter.
+ *
+ * The page shows a handful of short TOML, YAML, JSON and shell snippets, all
+ * written here. A parser for each language, or a library carrying them, would be
+ * several times the size of everything it ever renders. Colour is restricted to
+ * three roles — structure, value, aside — so a snippet reads the same way the
+ * terminal panes do.
+ */
+
+export type Lang = 'toml' | 'yaml' | 'json' | 'bash' | 'xml';
+
+export type TokenKind = 'text' | 'comment' | 'section' | 'key' | 'value' | 'expr' | 'punct';
+
+export interface Token {
+ text: string;
+ kind: TokenKind;
+}
+
+const PATTERNS: Record = {
+ // Strings come first everywhere, so a `#` inside one is never a comment.
+ toml: /("[^"]*")|(#.*$)|(^\s*\[\[?[^\]\n]+\]\]?)|([A-Za-z_][\w.-]*)(?=\s*=)|(\b\d[\w.+-]*)|([[\]{}=,])/gm,
+ yaml: /("[^"]*"|'[^']*')|(#.*$)|(\$\{\{[^}]*\}\})|(^\s*-?\s*[A-Za-z_][\w.-]*)(?=\s*:)|(\b\d[\w.+-]*)|([[\]{}:,-])/gm,
+ json: /("(?:[^"\\]|\\.)*")(?=\s*:)|("(?:[^"\\]|\\.)*")|(\b\d[\w.+-]*)|([[\]{},:])/gm,
+ bash: /("[^"]*"|'[^']*')|(#.*$)|(\s--?[A-Za-z][\w-]*)|(^\s*[a-z][\w.-]*)|([|&<>$])/gm,
+ xml: /("[^"]*")|()|(<\/?[A-Za-z][\w.:-]*)|([A-Za-z][\w.:-]*)(?==)|(\/?>)|(\b\d[\w.+-]*)/gm,
+};
+
+const KINDS: Record = {
+ toml: ['value', 'comment', 'section', 'key', 'value', 'punct'],
+ yaml: ['value', 'comment', 'expr', 'key', 'value', 'punct'],
+ json: ['key', 'value', 'value', 'punct'],
+ bash: ['value', 'comment', 'punct', 'key', 'punct'],
+ xml: ['value', 'comment', 'section', 'key', 'punct', 'value'],
+};
+
+/** Splits a snippet into lines of tokens. */
+export function highlight(code: string, lang: Lang): Token[][] {
+ const pattern = PATTERNS[lang];
+ const kinds = KINDS[lang];
+
+ return code.split('\n').map((line) => {
+ const tokens: Token[] = [];
+ let last = 0;
+
+ pattern.lastIndex = 0;
+ for (let match = pattern.exec(line); match; match = pattern.exec(line)) {
+ const group = match.findIndex((value, index) => index > 0 && value !== undefined);
+ if (group < 1) continue;
+
+ if (match.index > last) {
+ tokens.push({ text: line.slice(last, match.index), kind: 'text' });
+ }
+ tokens.push({ text: match[group] ?? '', kind: kinds[group - 1] ?? 'text' });
+ last = match.index + (match[group]?.length ?? 0);
+ }
+
+ if (last < line.length) tokens.push({ text: line.slice(last), kind: 'text' });
+ return tokens;
+ });
+}
diff --git a/website/src/lib/hooks.ts b/website/src/lib/hooks.ts
new file mode 100644
index 0000000..b93bd32
--- /dev/null
+++ b/website/src/lib/hooks.ts
@@ -0,0 +1,105 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+/**
+ * Reports when an element has been scrolled into view, and stops watching once
+ * it has. Everything on this page enters once; nothing animates on the way out,
+ * because content that moves when you scroll back up is harder to read, not
+ * more alive.
+ *
+ * Only the bottom edge is inset, so entering is held back until an element is
+ * properly on screen. Insetting the top as well leaves a dead band under the
+ * masthead where anything already in view on load never reveals at all.
+ */
+export function useInView(rootMargin = '0px 0px -10% 0px') {
+ const ref = useRef(null);
+ const [inView, setInView] = useState(false);
+
+ useEffect(() => {
+ const node = ref.current;
+ if (!node || inView) return;
+
+ // Anything already on screen when it mounts is answered from geometry on a
+ // timer rather than from the observer. An observer only reports during a
+ // rendering update, so a tab that is throttled or not yet painted can hold
+ // its first callback indefinitely, and a page whose content is hidden until
+ // that callback arrives would stay blank. The timer also lets the first
+ // frame paint at rest, so what is already in view fades in rather than
+ // appearing finished.
+ const timer = window.setTimeout(() => {
+ if (node.getBoundingClientRect().top < window.innerHeight) setInView(true);
+ }, 60);
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ if (entries.some((entry) => entry.isIntersecting)) {
+ setInView(true);
+ observer.disconnect();
+ }
+ },
+ { rootMargin },
+ );
+
+ observer.observe(node);
+ return () => {
+ window.clearTimeout(timer);
+ observer.disconnect();
+ };
+ }, [inView, rootMargin]);
+
+ return { ref, inView };
+}
+
+/** Whether the visitor has asked for less motion. */
+export function useReducedMotion() {
+ const [reduced, setReduced] = useState(
+ () => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
+ );
+
+ useEffect(() => {
+ const query = window.matchMedia('(prefers-reduced-motion: reduce)');
+ const onChange = () => setReduced(query.matches);
+ query.addEventListener('change', onChange);
+ return () => query.removeEventListener('change', onChange);
+ }, []);
+
+ return reduced;
+}
+
+/** Copies text, and reports for a moment that it did. */
+export function useCopy(value: string) {
+ const [copied, setCopied] = useState(false);
+ const timer = useRef(undefined);
+
+ useEffect(() => () => window.clearTimeout(timer.current), []);
+
+ const copy = useCallback(() => {
+ void (async () => {
+ try {
+ await navigator.clipboard.writeText(value);
+ } catch {
+ // A browser that refuses the clipboard says nothing; the text is on
+ // screen and selectable either way.
+ return;
+ }
+ setCopied(true);
+ window.clearTimeout(timer.current);
+ timer.current = window.setTimeout(() => setCopied(false), 1600);
+ })();
+ }, [value]);
+
+ return { copied, copy };
+}
+
+/** Whether the page has been scrolled past a threshold. */
+export function useScrolled(threshold = 8) {
+ const [scrolled, setScrolled] = useState(false);
+
+ useEffect(() => {
+ const onScroll = () => setScrolled(window.scrollY > threshold);
+ onScroll();
+ window.addEventListener('scroll', onScroll, { passive: true });
+ return () => window.removeEventListener('scroll', onScroll);
+ }, [threshold]);
+
+ return scrolled;
+}
diff --git a/website/src/lib/release.ts b/website/src/lib/release.ts
new file mode 100644
index 0000000..5152968
--- /dev/null
+++ b/website/src/lib/release.ts
@@ -0,0 +1,57 @@
+import { useEffect, useState } from 'react';
+
+export interface Release {
+ version: string;
+ url: string;
+ prerelease: boolean;
+}
+
+interface ApiRelease {
+ tag_name: string;
+ html_url: string;
+ prerelease: boolean;
+ draft: boolean;
+}
+
+/**
+ * The newest published release, read from GitHub when the page loads.
+ *
+ * This page deliberately keeps no copy of vump's version number. A number
+ * written into a marketing site is a number nobody bumps, and a version that
+ * disagrees with its source is the exact defect this tool exists to prevent.
+ * When the request fails, nothing is shown — an unverifiable version is worse
+ * than none.
+ */
+export function useLatestRelease(): Release | null {
+ const [release, setRelease] = useState(null);
+
+ useEffect(() => {
+ const controller = new AbortController();
+
+ void (async () => {
+ try {
+ const response = await fetch(
+ 'https://api.github.com/repos/okcodes/vump/releases?per_page=5',
+ { signal: controller.signal, headers: { Accept: 'application/vnd.github+json' } },
+ );
+ if (!response.ok) return;
+
+ const releases = (await response.json()) as ApiRelease[];
+ const newest = releases.find((entry) => !entry.draft);
+ if (!newest) return;
+
+ setRelease({
+ version: newest.tag_name.replace(/^v/, ''),
+ url: newest.html_url,
+ prerelease: newest.prerelease,
+ });
+ } catch {
+ // Offline, rate-limited, or blocked: the chip simply does not appear.
+ }
+ })();
+
+ return () => controller.abort();
+ }, []);
+
+ return release;
+}
diff --git a/website/src/lib/terminal.ts b/website/src/lib/terminal.ts
new file mode 100644
index 0000000..454ecf1
--- /dev/null
+++ b/website/src/lib/terminal.ts
@@ -0,0 +1,59 @@
+/**
+ * A model for rendered command output.
+ *
+ * Terminal panes on this page reproduce what vump actually prints — the marks,
+ * the column alignment, the wording of its errors. Writing them as segments
+ * rather than as pre-formatted strings is what lets the accent land on the part
+ * that matters (the version that changed) instead of on the whole line.
+ */
+
+export type Tone = 'ink' | 'muted' | 'signal' | 'warn' | 'danger';
+
+export interface Seg {
+ text: string;
+ tone: Tone;
+}
+
+export interface Line {
+ /** Prompt lines are typed out; output lines appear a line at a time. */
+ prompt: boolean;
+ segs: Seg[];
+}
+
+const seg =
+ (tone: Tone) =>
+ (text: string): Seg => ({ text, tone });
+
+/** Ordinary output. */
+export const t = seg('ink');
+/** Secondary output: paths, hints, anything the eye should pass over. */
+export const dim = seg('muted');
+/** The accent. Reserved for what changed, and for a passing mark. */
+export const sig = seg('signal');
+/** A waived guard or a caution. */
+export const caution = seg('warn');
+/** A refusal or a failing mark. */
+export const bad = seg('danger');
+
+/** A line the user types. */
+export function cmd(text: string): Line {
+ return { prompt: true, segs: [t(text)] };
+}
+
+/** A line the program prints. */
+export function out(...segs: Array): Line {
+ return {
+ prompt: false,
+ segs: segs.map((s) => (typeof s === 'string' ? t(s) : s)),
+ };
+}
+
+/** A blank line. Spacing in real output is part of the output. */
+export function gap(): Line {
+ return { prompt: false, segs: [] };
+}
+
+/** The text of a line, with no styling — what a copy button should yield. */
+export function plain(line: Line): string {
+ return (line.prompt ? '$ ' : '') + line.segs.map((s) => s.text).join('');
+}
diff --git a/website/src/lib/theme.ts b/website/src/lib/theme.ts
new file mode 100644
index 0000000..bd5a946
--- /dev/null
+++ b/website/src/lib/theme.ts
@@ -0,0 +1,31 @@
+import { useCallback, useEffect, useState } from 'react';
+
+export type Theme = 'dark' | 'light';
+
+const KEY = 'vump-theme';
+
+function current(): Theme {
+ return document.documentElement.dataset['theme'] === 'light' ? 'light' : 'dark';
+}
+
+/**
+ * Reads the theme the pre-paint script in `index.html` already resolved, and
+ * writes any change back to it. The stored value is a deliberate choice, so it
+ * outranks the system preference from then on.
+ */
+export function useTheme() {
+ const [theme, setTheme] = useState(current);
+
+ useEffect(() => {
+ document.documentElement.dataset['theme'] = theme;
+ try {
+ localStorage.setItem(KEY, theme);
+ } catch {
+ // A browser refusing storage is not a reason to refuse the theme.
+ }
+ }, [theme]);
+
+ const toggle = useCallback(() => setTheme((t) => (t === 'dark' ? 'light' : 'dark')), []);
+
+ return { theme, toggle };
+}
diff --git a/website/src/main.tsx b/website/src/main.tsx
new file mode 100644
index 0000000..94f1b1b
--- /dev/null
+++ b/website/src/main.tsx
@@ -0,0 +1,22 @@
+// Variable faces ship one stylesheet per axis, covering every subset; the
+// browser fetches only the subset it needs, gated by unicode-range. The static
+// serif is imported latin-only, which is all this site is written in.
+import '@fontsource-variable/instrument-sans/wght.css';
+import '@fontsource-variable/jetbrains-mono/wght.css';
+import '@fontsource/instrument-serif/latin-400.css';
+import '@fontsource/instrument-serif/latin-400-italic.css';
+import './index.css';
+
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+
+import { App } from './App.tsx';
+
+const root = document.getElementById('root');
+if (!root) throw new Error('missing #root');
+
+createRoot(root).render(
+
+
+ ,
+);
diff --git a/website/tsconfig.app.json b/website/tsconfig.app.json
new file mode 100644
index 0000000..289c5df
--- /dev/null
+++ b/website/tsconfig.app.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
+ "module": "esnext",
+ "types": ["vite/client"],
+ "allowArbitraryExtensions": true,
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true,
+ "noImplicitOverride": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/website/tsconfig.json b/website/tsconfig.json
new file mode 100644
index 0000000..d32ff68
--- /dev/null
+++ b/website/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "files": [],
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
+}
diff --git a/website/tsconfig.node.json b/website/tsconfig.node.json
new file mode 100644
index 0000000..8455dcb
--- /dev/null
+++ b/website/tsconfig.node.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "module": "nodenext",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/website/vite.config.ts b/website/vite.config.ts
new file mode 100644
index 0000000..f87568e
--- /dev/null
+++ b/website/vite.config.ts
@@ -0,0 +1,21 @@
+import tailwindcss from '@tailwindcss/vite';
+import react from '@vitejs/plugin-react';
+import { defineConfig, loadEnv } from 'vite';
+
+export default defineConfig(({ mode }) => {
+ // The site is destined for GitHub Pages. A project site is served from
+ // `//`, a custom domain from the root, and the difference is baked into
+ // every asset URL at build time — so it is an environment variable rather than
+ // a value edited in this file before each deploy.
+ const env = loadEnv(mode, process.cwd(), '');
+
+ return {
+ base: env.VITE_BASE_PATH || '/',
+ plugins: [react(), tailwindcss()],
+ build: {
+ // Every visitor loads the whole page, so splitting it into chunks buys
+ // nothing and costs a round trip.
+ assetsInlineLimit: 2048,
+ },
+ };
+});