diff --git a/.env b/.env
new file mode 100644
index 0000000..f7d1d47
--- /dev/null
+++ b/.env
@@ -0,0 +1,6 @@
+# Default env vars for the Vite build. Checked in.
+# Trainees who want to override locally should create `.env.local` (gitignored).
+
+# Canonical "Acme Widgets — Catalog" Google Sheet, published to web as CSV.
+# A fresh clone runs against this URL out of the box.
+VITE_WIDGETS_SHEET_URL=https://docs.google.com/spreadsheets/d/e/2PACX-1vRDcRnvmze_Pj2tdivNrUuJRJtTT1Hugiv2OUiRZYYEz1n5WHcFdBP45FucjUGwfRV0JwYijSIxm4gQ/pub?gid=0&single=true&output=csv
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..a3fc3ec
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,74 @@
+name: CI
+
+on:
+ pull_request:
+ branches: [main]
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ env:
+ # Hardcoded; bump in lockstep with @playwright/test in package.json.
+ # Cache is invalidated on version change.
+ PLAYWRIGHT_VERSION: 1.59.1
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: 1.3.11
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
+
+ - name: Typecheck
+ run: bun run typecheck
+
+ - name: Build
+ run: bun run build
+
+ - name: Vitest
+ run: bun run test
+
+ - name: Cache Playwright browsers
+ id: playwright-cache
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-${{ runner.os }}-${{ env.PLAYWRIGHT_VERSION }}
+
+ - name: Install Playwright browsers
+ run: bunx playwright install --with-deps chromium
+
+ - name: Playwright
+ run: bun run test:e2e
+
+ - name: Upload Playwright report on failure
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 7
+
+ - name: Upload Playwright test-results on failure
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: playwright-test-results
+ path: test-results/
+ retention-days: 7
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..87a7bf0
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,51 @@
+name: Deploy to GitHub Pages
+
+on:
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: 1.3.11
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
+
+ - name: Build with Pages base path
+ env:
+ VITE_BASE: /acme-widgets/
+ run: bun run build
+
+ - name: Upload Pages artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: dist
+
+ deploy:
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7925542
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,32 @@
+
+# Beads / Dolt files (added by bd init)
+.dolt/
+*.db
+.beads-credential-key
+
+# Manual backups of dolt db for evolving versions of workshop
+.backups
+
+# Node / Bun
+node_modules/
+
+# Foreign package-manager lockfiles (this project uses bun.lock)
+package-lock.json
+yarn.lock
+pnpm-lock.yaml
+
+# Build output
+dist/
+.vite/
+*.tsbuildinfo
+
+# Local env overrides (Vite convention; `.env` itself is committed)
+*.local
+
+# Test artifacts
+playwright-report/
+test-results/
+.playwright-mcp/
+
+# OS
+.DS_Store
diff --git a/README.md b/README.md
index 36b06a7..05e43e4 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,92 @@
# Acme Widgets
-This repo is used to create an example website for Software Factory Intensive training.
+Example website for Software Factory Intensive training. Live at
+****.
+
+## Quick Start
+
+### Prerequisites
+
+- **[Bun](https://bun.sh) `>=1.2`** — runtime + package manager + script runner.
+ Vite (the dev server / bundler) is installed for you when you run `bun install`,
+ so you don't need to install it globally.
+
+#### Install Bun
+
+See the official install guide: .
+
+```bash
+# macOS / Linux / WSL
+curl -fsSL https://bun.sh/install | bash
+
+# macOS via Homebrew
+brew install oven-sh/bun/bun
+
+# Windows (PowerShell)
+powershell -c "irm bun.sh/install.ps1 | iex"
+```
+
+Verify:
+
+```bash
+bun --version # should print 1.2.x or newer
+```
+
+#### About Vite
+
+You don't install Vite globally for this project — it ships as a dev dependency
+(see `package.json`) and is run via the `bun run` scripts below. If you want to
+read up on it: .
+
+### Clone and run
+
+```bash
+git clone git@github.com:bryanhirsch/acme-widgets-private.git
+cd acme-widgets-private
+bun install # installs Vite and all other deps
+bun run dev # http://localhost:5173
+```
+
+Other useful scripts:
+
+```bash
+bun run typecheck # tsc --noEmit
+bun run build # production build to dist/ (also writes 404.html)
+bun run preview # serve the built output on http://localhost:4173
+bun run test # Vitest, one shot
+bun run test:watch # Vitest, watch mode
+bun run test:e2e # Playwright (chromium); spins up `bun run preview`
+```
+
+## Stack
+
+TypeScript + React 19 + Vite 6 + Tailwind v4 + React Router v7,
+hosted on GitHub Pages, CI on GitHub Actions. See
+[`docs/current/STACK.md`](docs/current/STACK.md) for as-built versions
+and conventions, and
+[`docs/decision-records/0001.ADR.TECH_STACK.md`](docs/decision-records/0001.ADR.TECH_STACK.md)
+for the decision record.
+
+## Repo layout
+
+- [`docs/current/`](docs/current) — what is true on `main` right now.
+- [`docs/decision-records/`](docs/decision-records) — frozen ADRs.
+- [`docs/future/`](docs/future) — proposed changes drafted as ADRs.
+- `src/` — application source. `pages/` contains the three routes;
+ `components/` contains shared chrome and the 15 widget presets;
+ `lib/widgets.ts` is the CSV parser + cache.
+- `e2e/` — Playwright specs.
+- `public/data/widgets.csv` — last-resort fallback for the catalog if the
+ Google Sheet can't be reached. Editing this file does **not** change
+ the live site; see below.
+
+## Editing the catalog
+
+Widget data (prices, stock counts, descriptions) lives in a Google Sheet,
+not in this repo. Producers and employees edit the sheet directly; changes
+appear on the live site within a few minutes — no PR, no deploy.
+
+See [`docs/current/EDITING_THE_CATALOG.md`](docs/current/EDITING_THE_CATALOG.md)
+for the sheet location and editing rules. The architecture choice is
+captured in
+[`docs/decision-records/0002.ADR.DYNAMIC_WIDGET_CATALOG.md`](docs/decision-records/0002.ADR.DYNAMIC_WIDGET_CATALOG.md).
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..f885144
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,543 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "acme-widgets",
+ "dependencies": {
+ "@heroicons/react": "^2.2.0",
+ "papaparse": "^5.5.3",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router": "^7.14.2",
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.59.1",
+ "@tailwindcss/vite": "^4.2.4",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/papaparse": "^5.5.2",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "@vitejs/plugin-react": "^4.3.4",
+ "jsdom": "^29.1.0",
+ "tailwindcss": "^4.2.4",
+ "typescript": "^5.7.0",
+ "vite": "^6.0.0",
+ "vitest": "^4.1.5",
+ },
+ },
+ },
+ "packages": {
+ "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
+
+ "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="],
+
+ "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="],
+
+ "@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="],
+
+ "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
+
+ "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
+
+ "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
+
+ "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
+
+ "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
+
+ "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
+
+ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
+
+ "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
+
+ "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
+
+ "@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="],
+
+ "@csstools/css-calc": ["@csstools/css-calc@3.2.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w=="],
+
+ "@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.0", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.2.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ=="],
+
+ "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
+
+ "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.3", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg=="],
+
+ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
+
+ "@exodus/bytes": ["@exodus/bytes@1.15.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="],
+
+ "@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.2", "", { "os": "android", "cpu": "arm64" }, "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.2", "", { "os": "linux", "cpu": "arm" }, "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.2", "", { "os": "linux", "cpu": "arm" }, "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA=="],
+
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A=="],
+
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw=="],
+
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.2", "", { "os": "linux", "cpu": "x64" }, "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.2", "", { "os": "linux", "cpu": "x64" }, "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw=="],
+
+ "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.2", "", { "os": "none", "cpu": "arm64" }, "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg=="],
+
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA=="],
+
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
+ "@tailwindcss/node": ["@tailwindcss/node@4.2.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.4" } }, "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA=="],
+
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.4", "@tailwindcss/oxide-darwin-arm64": "4.2.4", "@tailwindcss/oxide-darwin-x64": "4.2.4", "@tailwindcss/oxide-freebsd-x64": "4.2.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", "@tailwindcss/oxide-linux-x64-musl": "4.2.4", "@tailwindcss/oxide-wasm32-wasi": "4.2.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q=="],
+
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g=="],
+
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg=="],
+
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg=="],
+
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw=="],
+
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA=="],
+
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw=="],
+
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g=="],
+
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA=="],
+
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA=="],
+
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.4", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw=="],
+
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ=="],
+
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw=="],
+
+ "@tailwindcss/vite": ["@tailwindcss/vite@4.2.4", "", { "dependencies": { "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "tailwindcss": "4.2.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw=="],
+
+ "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
+
+ "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="],
+
+ "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
+
+ "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="],
+
+ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
+
+ "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
+
+ "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
+
+ "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
+
+ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
+
+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
+
+ "@types/papaparse": ["@types/papaparse@5.5.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA=="],
+
+ "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
+
+ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+
+ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
+
+ "@vitest/expect": ["@vitest/expect@4.1.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw=="],
+
+ "@vitest/mocker": ["@vitest/mocker@4.1.5", "", { "dependencies": { "@vitest/spy": "4.1.5", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw=="],
+
+ "@vitest/pretty-format": ["@vitest/pretty-format@4.1.5", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g=="],
+
+ "@vitest/runner": ["@vitest/runner@4.1.5", "", { "dependencies": { "@vitest/utils": "4.1.5", "pathe": "^2.0.3" } }, "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ=="],
+
+ "@vitest/snapshot": ["@vitest/snapshot@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "@vitest/utils": "4.1.5", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ=="],
+
+ "@vitest/spy": ["@vitest/spy@4.1.5", "", {}, "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ=="],
+
+ "@vitest/utils": ["@vitest/utils@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug=="],
+
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
+
+ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
+
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.24", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA=="],
+
+ "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
+
+ "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
+
+ "caniuse-lite": ["caniuse-lite@1.0.30001791", "", {}, "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ=="],
+
+ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
+
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
+ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
+
+ "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
+
+ "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
+
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
+ "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
+
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
+ "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
+
+ "electron-to-chromium": ["electron-to-chromium@1.5.345", "", {}, "sha512-F9JXQGiMrz6yVNPI2qOVPvB9HzjH5cGzhs8oJ6A28V5L/YnzN/0KsuiibqF+F1Fd9qxFzD1BUnYSd8JfULxTwg=="],
+
+ "enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="],
+
+ "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
+
+ "es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="],
+
+ "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+ "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+ "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
+
+ "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
+
+ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
+
+ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "jsdom": ["jsdom@29.1.0", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.3", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.3.5", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-YNUc7fB9QuvSSQWfrH0xF+TyABkxUwx8sswgIDaCrw4Hol8BghdZDkITtZheRJeMtzWlnTfsM3bBBusRvpO1wg=="],
+
+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+
+ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+
+ "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "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" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
+
+ "lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="],
+
+ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
+
+ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="],
+
+ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
+
+ "papaparse": ["papaparse@5.5.3", "", {}, "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A=="],
+
+ "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="],
+
+ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
+ "playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="],
+
+ "playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
+
+ "postcss": ["postcss@8.5.12", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA=="],
+
+ "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
+
+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+
+ "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
+
+ "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="],
+
+ "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
+
+ "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
+
+ "react-router": ["react-router@7.14.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw=="],
+
+ "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
+
+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
+ "rollup": ["rollup@4.60.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", "@rollup/rollup-darwin-arm64": "4.60.2", "@rollup/rollup-darwin-x64": "4.60.2", "@rollup/rollup-freebsd-arm64": "4.60.2", "@rollup/rollup-freebsd-x64": "4.60.2", "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", "@rollup/rollup-linux-arm-musleabihf": "4.60.2", "@rollup/rollup-linux-arm64-gnu": "4.60.2", "@rollup/rollup-linux-arm64-musl": "4.60.2", "@rollup/rollup-linux-loong64-gnu": "4.60.2", "@rollup/rollup-linux-loong64-musl": "4.60.2", "@rollup/rollup-linux-ppc64-gnu": "4.60.2", "@rollup/rollup-linux-ppc64-musl": "4.60.2", "@rollup/rollup-linux-riscv64-gnu": "4.60.2", "@rollup/rollup-linux-riscv64-musl": "4.60.2", "@rollup/rollup-linux-s390x-gnu": "4.60.2", "@rollup/rollup-linux-x64-gnu": "4.60.2", "@rollup/rollup-linux-x64-musl": "4.60.2", "@rollup/rollup-openbsd-x64": "4.60.2", "@rollup/rollup-openharmony-arm64": "4.60.2", "@rollup/rollup-win32-arm64-msvc": "4.60.2", "@rollup/rollup-win32-ia32-msvc": "4.60.2", "@rollup/rollup-win32-x64-gnu": "4.60.2", "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ=="],
+
+ "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
+
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
+
+ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
+ "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="],
+
+ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
+
+ "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
+
+ "tailwindcss": ["tailwindcss@4.2.4", "", {}, "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA=="],
+
+ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
+
+ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
+
+ "tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="],
+
+ "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+
+ "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
+
+ "tldts": ["tldts@7.0.29", "", { "dependencies": { "tldts-core": "^7.0.29" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg=="],
+
+ "tldts-core": ["tldts-core@7.0.29", "", {}, "sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q=="],
+
+ "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="],
+
+ "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="],
+
+ "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
+
+ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
+
+ "vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="],
+
+ "vitest": ["vitest@4.1.5", "", { "dependencies": { "@vitest/expect": "4.1.5", "@vitest/mocker": "4.1.5", "@vitest/pretty-format": "4.1.5", "@vitest/runner": "4.1.5", "@vitest/snapshot": "4.1.5", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.5", "@vitest/browser-preview": "4.1.5", "@vitest/browser-webdriverio": "4.1.5", "@vitest/coverage-istanbul": "4.1.5", "@vitest/coverage-v8": "4.1.5", "@vitest/ui": "4.1.5", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg=="],
+
+ "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
+
+ "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
+
+ "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
+
+ "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
+
+ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
+
+ "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
+
+ "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
+
+ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
+ "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
+
+ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
+
+ "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
+ }
+}
diff --git a/docs/current/DOCUMENTATION.md b/docs/current/DOCUMENTATION.md
new file mode 100644
index 0000000..fb90728
--- /dev/null
+++ b/docs/current/DOCUMENTATION.md
@@ -0,0 +1,68 @@
+# Documentation Standard
+
+In a mature software factory, agents read and write the code; humans read and
+write plain English. Agents do code review; humans review functionality and
+the plain-English descriptions in `docs/`. This document defines how those
+plain-English documents are organized in this repository.
+
+## Directory structure
+
+```
+docs/
+ current/ # Authoritative docs for current state of code on the default branch (main)
+ future/ # Proposed changes, drafted as ADRs awaiting approval
+ roadmap → future # Symlink for anyone looking for "the roadmap"
+ decision-records/ # ADRs in flight or landed, moved here on a branch where the ADR is being implemented
+ human-review-logs/ # Per-PR checklists for human functional review
+```
+
+## Lifecycle of an architecture decision
+
+1. **Draft.** A proposed change is written as an ADR in `docs/future/`,
+ numbered sequentially (e.g. `0001.ADR.TECH_STACK.md`).
+2. **Approve & branch.** After approval if/when the work gets prioritized
+ the ADR is moved from `docs/future/`
+ to `docs/decision-records/` on the branch created to implement it.
+3. **Evolve during implementation.** If the architecture evolves before
+ merge, update the ADR in place in `docs/decision-records/` so the PR
+ reflects what was actually built.
+4. **Merge.** After merge, the ADR in `docs/decision-records/` is a frozen
+ historical record — a decision made at a point in time, in a particular
+ context. It does not change again. New decisions get new ADRs.
+5. **Reflect current state.** The same PR updates `docs/current/` to describe
+ the new state of the codebase.
+
+## What goes where
+
+| Directory | Stable? | Purpose |
+|--------------------------|---------|-----------------------------------------------------------|
+| `docs/current/` | Living | What is true on the default branch right now. |
+| `docs/future/` | Living | What we are considering. |
+| `docs/decision-records/` | Frozen upon merge to main | ADRs get moved here as part of implementation. |
+| `docs/human-review-logs/`| Append | One file per PR that warrants human functional review. |
+
+## When to update `docs/current/`
+
+Any pull request that makes a meaningful change to technical architecture,
+user experience, or information architecture must include corresponding
+updates to `docs/current/`. If the docs and the code disagree, that is a bug
+in either the code or the docs — never an acceptable steady state.
+
+## Human review logs
+
+Some changes deserve a human walkthrough before merge. For those, add a file
+at `docs/human-review-logs/--.md` containing a checklist of
+suggested functional reviews. Where a review item corresponds to an
+automated test, reference the test so the reviewer has context. The goal is
+to let a human verify, as efficiently as possible, that the change does what
+it was intended to do.
+
+A review log is appropriate when:
+- The change is user-visible and an automated test cannot fully cover the
+ intent (e.g. copy, layout, motion).
+- The change crosses subsystem boundaries in a way a single PR diff is hard
+ to evaluate.
+- The change is a milestone the team wants captured for posterity.
+
+Skip the review log for routine refactors, dependency bumps, and other
+changes whose intent is fully captured by tests and the diff itself.
diff --git a/docs/current/EDITING_THE_CATALOG.md b/docs/current/EDITING_THE_CATALOG.md
new file mode 100644
index 0000000..1f53e48
--- /dev/null
+++ b/docs/current/EDITING_THE_CATALOG.md
@@ -0,0 +1,93 @@
+# Editing the Catalog
+
+Widget data on the live site comes from a Google Sheet. Editing the
+sheet updates the live site within a few minutes — no PR, no deploy.
+
+## Where the sheet lives
+
+**Sheet:** "Acme Widgets — Catalog."
+
+If you need edit access, ask Bryan. Editor access is intentionally
+limited to the same group who can merge code to `main`, because changes
+to the sheet go live with no review.
+
+> ⚠ The published version of this sheet is publicly readable on the
+> internet. Put **only catalog data** in it — no internal notes, no
+> private prices, no draft copy, no customer info.
+
+## Editing rules
+
+The sheet has one row per widget. Edit cells in place, add new rows at
+the bottom, or delete rows you no longer want on the site.
+
+| Column | What goes here |
+| ----------------- | ------------------------------------------------------------------ |
+| `type` | The widget's display name. Required. |
+| `description` | One-sentence description shown on the catalog card. |
+| `number_in_stock` | A whole number. The card shows "N in stock" (or "Only 9 left"). |
+| `price` | A number in dollars. Decimals OK (`19.99`). No `$` sign. |
+| `icon` | Preset key for the card's visual (e.g. `brass-widget.svg`). Required. See "How `icon` picks a visual" below. |
+| `categories` | Tags separated by `;` (e.g. `everyday;artisanal;gift`). |
+
+**Do not rename the column headers.** The site reads columns by name;
+renaming `price` to `Price ($)` will make the catalog page show an
+error to visitors instead of silently breaking every price. If you
+need a label change, ask an engineer.
+
+A row missing `type` or `icon` is skipped on the site (other rows
+still appear) — useful if you want to draft a row in place before
+filling it out.
+
+## How `icon` picks a visual
+
+Each widget card has a "hero" panel — the colored background, label,
+and cog at the top of the card. The `icon` cell decides which one a
+row gets. The match is by filename:
+
+- `icon` is `brass-widget.svg` → the site looks for a preset file at
+ [`src/components/presets/brass-widget.tsx`](../../src/components/presets)
+ and renders that.
+- `icon` is anything the site doesn't recognize → the card falls back
+ to the generic "Standard Issue" preset. The row still appears; only
+ the visual is generic.
+
+The full list of presets a producer can pick from is whatever lives in
+[`src/components/presets/`](../../src/components/presets). The
+filenames in that directory (minus the `.tsx` extension, plus `.svg`)
+are the valid `icon` values. Files starting with `_` (like
+`_standard.tsx`) are internal helpers and are not pickable from the
+sheet.
+
+> **Adding a new widget design is a code change, not a sheet change.**
+> A producer with a new visual idea works with an engineer to add a
+> file under `src/components/presets/`. Once that file is merged, the
+> producer can reference its filename in the sheet's `icon` column and
+> the new design appears on the site. The decision to organize presets
+> this way is captured in
+> [`../decision-records/0003.ADR.SELF_REGISTERING_PRESETS.md`](../decision-records/0003.ADR.SELF_REGISTERING_PRESETS.md).
+
+## How fast does an edit go live?
+
+Google's published-CSV URL is edge-cached. In practice, edits show up
+on the live site within ~5 minutes. There is no manual "publish" step
+after the initial setup.
+
+If the sheet itself becomes unreachable, the site falls back to a
+snapshot bundled with the code (`public/data/widgets.csv`) so visitors
+still see something. The snapshot may be out of date — that's expected.
+
+## When something looks wrong
+
+- **Prices are all $0 or fields look empty across all widgets:** a
+ required column was probably renamed in the header. Restore the
+ original column name.
+- **The catalog page shows "The Catalog is temporarily unavailable":**
+ the sheet is unreachable AND the bundled fallback also failed. Ping
+ an engineer.
+- **The site shows old data after I edited the sheet:** wait ~5
+ minutes for Google's edge cache, then hard-refresh.
+
+## Why a Google Sheet and not a CMS?
+
+Captured in
+[`../decision-records/0002.ADR.DYNAMIC_WIDGET_CATALOG.md`](../decision-records/0002.ADR.DYNAMIC_WIDGET_CATALOG.md).
diff --git a/docs/current/GITHUB_ACTIONS.md b/docs/current/GITHUB_ACTIONS.md
new file mode 100644
index 0000000..f805627
--- /dev/null
+++ b/docs/current/GITHUB_ACTIONS.md
@@ -0,0 +1,38 @@
+# GitHub Actions on forks
+
+The repo ships with two workflows under `.github/workflows/`:
+
+- **`ci.yml`** — typecheck, build, Vitest, Playwright. Runs on PRs and pushes to `main`.
+- **`deploy.yml`** — builds and publishes the site to GitHub Pages. See [GITHUB_PAGES.md](./GITHUB_PAGES.md).
+
+Actions are free for public repos. Keep the fork public.
+
+## One-time setup per fork
+
+### 1. Enable Actions on the fork
+
+Forks have Actions **disabled by default**. Open the **Actions** tab and click "I understand my workflows, go ahead and enable them."
+
+### 2. Allow the actions used by the workflows
+
+Settings → Actions → General → Actions permissions. The simplest option is "Allow all actions and reusable workflows." If you prefer a stricter allowlist, the workflows use:
+
+- `actions/checkout`
+- `actions/cache`
+- `actions/upload-artifact`
+- `actions/upload-pages-artifact`
+- `actions/deploy-pages`
+- `oven-sh/setup-bun`
+
+### 3. Workflow permissions
+
+Settings → Actions → General → Workflow permissions. The shipped workflows declare the permissions they need (`contents: read`, plus `pages: write` and `id-token: write` for `deploy.yml`), so the default "Read repository contents and packages permissions" is fine. If a run fails with "Resource not accessible by integration," switch to "Read and write permissions."
+
+## Verifying CI runs
+
+Push a commit to `main` or open a PR against `main`. The Actions tab should show a green "CI" run. The Playwright report and `test-results/` artifacts upload only on failure (7-day retention).
+
+## Related
+
+- [GITHUB_PAGES.md](./GITHUB_PAGES.md) — Pages-specific setup (the deploy workflow)
+- [HOSTING.md](./HOSTING.md) — overall remote / Dolt / Pages architecture
diff --git a/docs/current/GITHUB_PAGES.md b/docs/current/GITHUB_PAGES.md
new file mode 100644
index 0000000..c4d98ba
--- /dev/null
+++ b/docs/current/GITHUB_PAGES.md
@@ -0,0 +1,43 @@
+# Hosting acme-widgets on GitHub Pages (forks)
+
+This project ships with a Pages deploy workflow (`.github/workflows/deploy.yml`) preconfigured for the default repo name `acme-widgets`. On a fork, you only need to flip two switches in the GitHub UI.
+
+Pages is free only for **public** repos — keep the fork public.
+
+## Prerequisite
+
+Actions must be enabled on the fork — see [GITHUB_ACTIONS.md](./GITHUB_ACTIONS.md).
+
+## One-time setup
+
+1. **Set Pages source to "GitHub Actions".** Settings → Pages → Build and deployment → Source: **GitHub Actions**. Do not pick "Deploy from a branch."
+2. **Trigger a deploy.** Push to `main`, or run the workflow manually: Actions tab → "Deploy to GitHub Pages" → "Run workflow".
+
+## Your site URL
+
+```
+https://.github.io/acme-widgets/
+```
+
+(Same form on the canonical repo: .)
+
+The URL also appears in Settings → Pages and on the workflow run summary once the deploy succeeds.
+
+## Already wired up (no action needed)
+
+- **SPA routing.** `package.json`'s `postbuild` copies `dist/index.html` to `dist/404.html` so React Router handles deep-link refreshes.
+- **Base path.** `VITE_BASE: /acme-widgets/` is set in `deploy.yml` so assets resolve under `/acme-widgets/...`.
+- **Pages permissions** (`pages: write`, `id-token: write`) and **concurrency** (`pages` group) are set in `deploy.yml`.
+
+## Troubleshooting
+
+| Symptom | Likely cause | Fix |
+| --- | --- | --- |
+| Workflow doesn't run on the fork | Actions disabled | See [GITHUB_ACTIONS.md](./GITHUB_ACTIONS.md) |
+| `actions/deploy-pages` fails with "Pages site not found" | Source not set to "GitHub Actions" | Step 1 |
+| Deep-link refresh returns 404 | `dist/404.html` missing | Re-run the workflow; verify `postbuild` ran |
+
+## Related
+
+- [GITHUB_ACTIONS.md](./GITHUB_ACTIONS.md) — enabling and configuring Actions on a fork
+- [HOSTING.md](./HOSTING.md) — overall remote / Dolt / Pages architecture
diff --git a/docs/current/HOSTING.md b/docs/current/HOSTING.md
new file mode 100644
index 0000000..b0aad24
--- /dev/null
+++ b/docs/current/HOSTING.md
@@ -0,0 +1,58 @@
+# Hosting & Repo Architecture
+
+The acme-widgets training project is one local working tree synced to **two GitHub remotes** plus **one Dolt remote**. The Git source — all branches — is fully shared with the public remote; only the Dolt issue-tracker database stays private.
+
+## Remotes
+
+| Name | URL | Carries | Why |
+| -------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------- |
+| `origin` | `git@github.com:bryanhirsch/acme-widgets-private.git` | All Git branches + `.beads/` config + Dolt branches (issue data) | Private repo holds the Dolt issue data we don't want public |
+| `public` | `git@github.com:bryanhirsch/acme-widgets.git` | All Git branches (no Dolt branches) | Public repo unlocks **free** GitHub Pages hosting + GitHub Actions CI |
+| Dolt | `git+ssh://git@github.com/bryanhirsch/acme-widgets-private.git` (via `bd dolt remote`) | Dolt issue data on orphan branches inside the private repo | Issue tracker (beads) data is sensitive; never goes to the public repo |
+
+## Branches
+
+| Branch | Purpose |
+| ---------------- | ------------------------------------------------------------------------------------ |
+| `main` | Production branch — powers GitHub Pages; workshop participants merge their finished work here |
+| `development` | Main development branch — where the demo website for the workshop gets built before merging to `main` |
+| `dev/start-here` | Workshop starting point for participants |
+| `dev/` | Targeted starting points for individual modules and lessons (WIP scaffolds) |
+
+Every Git branch above is pushed to **both** remotes. Only the Dolt orphan branches (`build1.0.0/nyc1`, `workshop1.0.0/nyc1`, etc.) stay on the private remote; they're managed by `bd dolt push` / `bd dolt pull`.
+
+The Pages deploy workflow (`.github/workflows/deploy.yml`) triggers on push to `main`, so a deploy fires when finished work lands there.
+
+## Pushing
+
+```bash
+git push origin # private remote (also receives Dolt branches via bd dolt push)
+git push public # public remote (Pages + CI run from here)
+bd dolt push # push Dolt issue data to the private remote
+```
+
+For convenience, configure a single `both` remote whose push URLs cover origin and public, so `git push both ` updates both repos in one shot.
+
+## What ends up where
+
+- **Public remote (`public`):** every Git branch (`main`, `development`, `dev/start-here`, `dev/`, …). No Dolt branches. The `.beads/` config files are present in tree but contain no issue content — only project bootstrap metadata.
+- **Private remote (`origin`):** every Git branch **plus** the Dolt branches. Source of truth for both code and issue tracking.
+- **GitHub Pages:** deploys from `public/main`. Free tier requires a public repo.
+- **GitHub Actions CI:** runs on the **public** remote. Free tier for public repos.
+
+## Working with this setup
+
+- For **issue tracking** (`bd ready`, `bd close`, etc.): operate normally; `bd dolt push` syncs to the private Dolt remote.
+- For **shipping code**: push the relevant branch to both remotes (or to a configured `both` remote). The deploy/CI workflows run on the public remote.
+- For **fresh clones**: clone the **private** repo first (`git@github.com:bryanhirsch/acme-widgets-private.git`) to get the beads bootstrap config, run `bd bootstrap` to clone the Dolt issue data, then add the `public` remote so you can push code there too.
+
+## Why not one repo?
+
+- A purely **public** repo would expose the Dolt issue database (issues, decisions, planning notes) to anyone.
+- A purely **private** repo would require a paid GitHub plan for Pages and Actions on private repos.
+- Splitting Git history (public, all branches) from issue-tracker data (private, in Dolt branches on the private remote) keeps everything free while keeping the issue tracker private.
+
+## Hosting the public repo (or a fork)
+
+- [GITHUB_ACTIONS.md](./GITHUB_ACTIONS.md) — enabling and configuring Actions on a fork.
+- [GITHUB_PAGES.md](./GITHUB_PAGES.md) — Pages setup, base-path config, and SPA routing.
diff --git a/docs/current/INFORMATION_ARCHITECTURE.md b/docs/current/INFORMATION_ARCHITECTURE.md
new file mode 100644
index 0000000..8c47ccf
--- /dev/null
+++ b/docs/current/INFORMATION_ARCHITECTURE.md
@@ -0,0 +1,27 @@
+# Information Architecture
+
+The Acme Widgets site is intentionally small. There are three top-level routes,
+no auth, and no nested navigation. Anyone landing on the home page should be
+able to find a widget or learn how to order one within a single click.
+
+## Route map
+
+| Path | Purpose |
+|------------|--------------------------------------------------------------------------------------------------|
+| `/` | Home page advertising Acme Widgets — widgets for all purposes and occasions. |
+| `/widgets` | Listing of every widget for sale, sourced live from a Google Sheet (see [EDITING_THE_CATALOG](EDITING_THE_CATALOG.md)). |
+| `/orders` | Placeholder explaining that online orders are coming soon; meanwhile, email `sales@acmewidgets.com`. |
+
+## Navigation
+
+A single top-level navigation surfaces all three routes from every page. There
+is no breadcrumb, no sidebar, and no per-widget detail page. If a future
+feature needs deeper nesting (e.g. `/widgets/:id`), capture the change in a
+new ADR before adding it.
+
+## Routing implementation
+
+See [`0001.ADR.TECH_STACK.md`](../decision-records/0001.ADR.TECH_STACK.md)
+for the choice of React Router v7 (data router). Routes are declared in a
+single `routes.tsx` module so trainees can see the entire information
+architecture in one file.
diff --git a/docs/current/PERSONAS.md b/docs/current/PERSONAS.md
new file mode 100644
index 0000000..0667cf3
--- /dev/null
+++ b/docs/current/PERSONAS.md
@@ -0,0 +1,41 @@
+# Personas
+
+Acme Widgets serves three personas. None of them have accounts in the current
+system — the site is anonymous and read-only — but the personas still drive
+how the catalog is organized and how copy is written.
+
+## Customers
+
+Everyone needs widgets. Customers range from kids buying a first widget to
+elderly buyers replacing a long-loved one, and they shop both for personal
+use and for business. The catalog must be browseable without a tutorial and
+should communicate purpose and occasion clearly.
+
+**What customers do on the site:** browse `/widgets`, decide they want one,
+follow the instructions on `/orders` to email sales.
+
+## Producers
+
+The people who make widgets and sell them through Acme Widgets. This includes
+handcrafted artisanal makers, manufacturers of high-precision widgets, and
+specialty producers for particular occasions and purposes.
+
+**What producers do on the site:** producers do not interact with the live
+site directly. They submit catalog entries by editing `public/data/widgets.csv`
+in a pull request. An employee reviews and merges.
+
+## Employees
+
+People who work for Acme Widgets, buying widgets from producers and selling to
+customers.
+
+**What employees do on the site:** employees own the catalog. They review
+producer PRs against `widgets.csv`, merge them, and respond to orders sent to
+`sales@acmewidgets.com`.
+
+## Why this matters for the codebase
+
+Because there is no auth and no per-persona UI, persona differences show up in
+**copy and catalog structure**, not in conditional rendering. When adding new
+content, ask which persona it serves and whether the existing IA
+(`/`, `/widgets`, `/orders`) is the right home for it.
diff --git a/docs/current/STACK.md b/docs/current/STACK.md
new file mode 100644
index 0000000..208c339
--- /dev/null
+++ b/docs/current/STACK.md
@@ -0,0 +1,122 @@
+# As-Built Stack
+
+This file is the ground truth for what the default branch actually ships.
+It is the companion to [`../decision-records/0001.ADR.TECH_STACK.md`](../decision-records/0001.ADR.TECH_STACK.md):
+the ADR captures the decision and its rationale; this file captures the
+versions and conventions that won during implementation.
+
+If the code and this file disagree, that is a bug in the file.
+
+## Pinned versions
+
+| Layer | Choice | Notes |
+| ---------------- | --------------------------------- | ---------------------------------------------------------- |
+| Runtime / pm | Bun `>=1.2`, currently `1.3.11` | Pinned in CI to `1.3.11` via `oven-sh/setup-bun@v2`. |
+| Bundler | Vite `^6` | `@vitejs/plugin-react`. |
+| Language | TypeScript `^5.7` | Strict + `noUncheckedIndexedAccess`. |
+| UI | React `^19` | |
+| Routing | React Router `^7` (data router) | `createBrowserRouter` + ``. |
+| Styling | **Tailwind CSS v4** (CSS-first) | `@tailwindcss/vite` plugin; no `tailwind.config.js`. |
+| Icons | `@heroicons/react/24/{solid,outline}` | |
+| CSV parsing | `papaparse` | Used for RFC 4180 quote handling. |
+| Unit / component | Vitest `^4`, jsdom, testing-library | Config in `vite.config.ts`, setup in `src/test/setup.ts`. |
+| E2E | Playwright `^1.59`, chromium-only | Config at repo root. webServer wraps `bun run preview`. |
+
+The `engines.bun` floor lives in `package.json`. The CI bun version is
+pinned in `.github/workflows/ci.yml` (and `deploy.yml`) — bump both at
+the same time when you upgrade the runtime.
+
+## Tailwind variant — v4, not v3
+
+Tailwind v4 was chosen during implementation of aw-l0r.2 over v3, per
+the bead's own NOTES which named v4 as the recommended default. The
+practical implications:
+
+- **No `tailwind.config.js` and no `postcss.config.js`.** Both files
+ intentionally do not exist. v4 reads its theme from a `@theme` block
+ in CSS.
+- **Single source of truth for design tokens** is `src/index.css`. The
+ brass / paper / jewel-tone palette and the Fraunces + Inter font
+ stacks live in that one `@theme` block. Reach into that block to add
+ or change tokens; do not introduce a JS config.
+- **Plugin pipeline** is the `@tailwindcss/vite` plugin registered in
+ `vite.config.ts`. There is no PostCSS config in the repo.
+- **`@apply` still works** inside the `@layer base` block in
+ `src/index.css`; we use it for the small set of base styles
+ (`html { @apply bg-paper text-ink … }`).
+
+If you ever switch back to v3, write a new ADR. Don't silently mix the
+two — every preset in `src/components/widgetPresets.tsx` assumes v4
+arbitrary-value syntax (`text-[clamp(40px,6vw,72px)]`,
+`bg-brass-600`, etc.).
+
+## Catalog data source
+
+The widget catalog is fetched live from a Google Sheet, not bundled
+into the build. This is the as-built side of
+[`../decision-records/0002.ADR.DYNAMIC_WIDGET_CATALOG.md`](../decision-records/0002.ADR.DYNAMIC_WIDGET_CATALOG.md).
+
+- **Live source:** the published-CSV URL in `VITE_WIDGETS_SHEET_URL`
+ (defined in `.env`, typed in `src/vite-env.d.ts`). A fresh clone runs
+ against the canonical Acme Widgets sheet with no setup. Override
+ locally by creating `.env.local` (gitignored) with a different value.
+- **Fallback:** `public/data/widgets.csv`. Used only when the live fetch
+ fails (network error or non-OK HTTP). When the fallback fires, the
+ browser console gets a `console.warn`. The fallback CSV is a snapshot
+ and may drift from the live sheet — that's accepted for the demo.
+- **Schema policy:** a row missing `type` or `icon` is skipped with a
+ warning (recoverable — the rest of the catalog renders). A header
+ with a renamed required column (`type`, `description`,
+ `number_in_stock`, `price`, `icon`, `categories`) **throws**. The
+ `/widgets` route renders `WidgetsRouteError` in that case rather than
+ silently corrupting prices.
+- **Producer workflow:** see
+ [EDITING_THE_CATALOG](EDITING_THE_CATALOG.md). Producers edit the
+ sheet; engineers do not ship PRs to update prices or stock counts.
+
+## `BASE_URL` convention
+
+The site has to work in two places: at `/` in dev and at
+`/acme-widgets/` on GitHub Pages (the public remote — see
+[`HOSTING.md`](HOSTING.md)). One vite build serves both, controlled
+by `import.meta.env.BASE_URL`:
+
+- **Vite config:** `base: process.env.VITE_BASE ?? '/'` in
+ `vite.config.ts`. CI sets `VITE_BASE=/acme-widgets/` for the deploy
+ job; local dev/preview leaves it unset.
+- **Router basename:** `createBrowserRouter(routes, { basename: import.meta.env.BASE_URL })`
+ in `src/routes.tsx`. **Do not** double-prefix in `` calls —
+ pass app-relative paths only (e.g. `to="/widgets"`).
+- **Asset fetches:** prefix with `import.meta.env.BASE_URL` for any
+ asset under `public/`. The fallback widgets CSV in `src/lib/widgets.ts`
+ uses this pattern (`${import.meta.env.BASE_URL}data/widgets.csv`).
+ `BASE_URL` already has a trailing slash; do not double up. The live
+ catalog URL (`VITE_WIDGETS_SHEET_URL`) is absolute and does **not**
+ use `BASE_URL`.
+
+If you add a new fetch of a `public/` asset, follow the same pattern.
+
+## GitHub Pages SPA fallback (`404.html`)
+
+GitHub Pages serves `404.html` for any path it can't find on disk.
+For an SPA, that means a hard refresh on `/widgets` would 404 unless
+we hand it the SPA shell. The fix is one line in `package.json`:
+
+```json
+"postbuild": "cp -f dist/index.html dist/404.html"
+```
+
+Bun runs lifecycle scripts, so `bun run build` produces both files
+without any extra wiring. **Do not strip this.** If you change the
+build pipeline, make sure `dist/404.html` continues to exist in
+production.
+
+## Test runner split
+
+- **Vitest** (`bun run test`) — pure functions, hooks, components in
+ isolation. Excludes the `e2e/` directory.
+- **Playwright** (`bun run test:e2e`) — flows that depend on routing,
+ real network fetches (yes, the live `widgets.csv`), or browser APIs.
+
+See [`TESTING.md`](TESTING.md) for the policy on when to reach for
+each.
diff --git a/docs/current/TESTING.md b/docs/current/TESTING.md
new file mode 100644
index 0000000..18066fe
--- /dev/null
+++ b/docs/current/TESTING.md
@@ -0,0 +1,47 @@
+# Testing
+
+Acme Widgets uses two test runners, each scoped to a different layer of the
+application. Both run on every PR via GitHub Actions and must be green before
+merge.
+
+## Unit & component tests — Vitest
+
+Vitest runs unit tests for pure functions and component tests for React
+components in isolation. It uses the same Vite transform pipeline as the app
+itself, so tests see the code exactly as the bundler sees it.
+
+- **Location:** co-located with source as `*.test.ts` / `*.test.tsx`.
+- **Run locally:** `bun run test`
+- **Watch mode:** `bun run test:watch`
+
+Use Vitest when:
+- The unit under test is a pure function, hook, or component.
+- The test does not need a real browser DOM, network, or routing.
+
+## End-to-end tests — Playwright
+
+Playwright drives a real headless browser against a built copy of the site.
+E2E tests cover the user-visible flows defined in
+[`INFORMATION_ARCHITECTURE.md`](INFORMATION_ARCHITECTURE.md): landing on `/`,
+browsing `/widgets`, reading `/orders`.
+
+- **Location:** `e2e/` at the repo root.
+- **Run locally:** `bun run test:e2e`
+- **Headed (debug):** `bun run test:e2e:headed`
+
+Use Playwright when:
+- The behavior depends on routing, real network fetches, or browser APIs.
+- The test verifies a user-visible flow end to end.
+
+## Choosing between the two
+
+Default to Vitest. Reach for Playwright only when the value of the test
+depends on real browser behavior. Every Playwright test costs CI time, so
+each one should justify itself with a flow a unit test cannot cover.
+
+## What we do not test
+
+- Visual regression. Out of scope for the training example.
+- Performance / load. The site is static and the catalog is small.
+- Accessibility audits beyond what Playwright + axe-core can assert in a
+ regular E2E run.
diff --git a/docs/decision-records/0001.ADR.TECH_STACK.md b/docs/decision-records/0001.ADR.TECH_STACK.md
new file mode 100644
index 0000000..ef84410
--- /dev/null
+++ b/docs/decision-records/0001.ADR.TECH_STACK.md
@@ -0,0 +1,125 @@
+# ADR 0001: Tech Stack & Hosting for Acme Widgets
+
+- **Status:** Accepted
+- **Date:** 2026-04-29
+- **Deciders:** Bryan
+
+## Context
+
+Acme Widgets is the example website used in the Software Factories Intensive
+training. It exists to give trainees a small, realistic codebase to practice
+factory workflows on. That purpose drives three constraints:
+
+1. **Fast to set up.** A trainee should be able to clone, install, and run
+ locally in minutes, on a clean laptop, without account provisioning.
+2. **Free to operate.** No paid services on the critical path. The repo should
+ stay green on a personal GitHub account at zero cost.
+3. **Recognizable to working engineers.** The stack should reflect choices a
+ real product team might make in 2026, so lessons transfer.
+
+The site itself is small: a marketing home page, a static widget catalog, and
+a placeholder orders page. There is no auth, no database, no payments — those
+are intentionally out of scope so the example stays focused on factory
+mechanics rather than product complexity.
+
+## Decision
+
+### Application stack
+
+- **Language:** TypeScript
+- **UI framework:** React
+- **Runtime / package manager:** Bun
+- **Bundler:** Vite (with Bun as the package manager and script runner)
+- **Routing:** React Router (data router, v7)
+- **Styling:** Tailwind CSS
+
+### Data
+
+> **Superseded in part by [ADR 0002](./0002.ADR.DYNAMIC_WIDGET_CATALOG.md):**
+> the widget catalog moved from a static CSV to a Google Sheet published as
+> CSV, fetched live by the browser. The bullet below describes the original
+> decision and is preserved as historical context.
+
+- **Widget catalog:** static CSV checked into the repo at
+ `public/data/widgets.csv`, fetched at runtime by the catalog page and parsed
+ client-side. Producers and employees update the catalog by editing the CSV
+ and opening a PR.
+- **No backend, no database.** Orders are handled out-of-band by emailing
+ `sales@acmewidgets.com`.
+
+### Hosting & CI
+
+- **Hosting:** GitHub Pages (free for public repos), serving the static build
+ output.
+- **CI:** GitHub Actions (free tier for public repos) for build, test, and
+ deploy-to-Pages on merge to the default branch.
+
+### Testing
+
+- **Unit / component tests:** Vitest (Jest-compatible API, runs natively under
+ Bun/Vite without the Node-compat overhead Jest requires).
+- **End-to-end tests:** Playwright, headless in CI.
+
+> Note: the rough notes proposed Jest. Vitest is preferred because it shares
+> Vite's transform pipeline and works with Bun without extra configuration.
+> If a future ADR moves the project off Vite, revisit this choice.
+
+## Consequences
+
+**Positive**
+
+- Zero hosting cost; trainees can fork and deploy on their own GitHub account.
+- Bun + Vite gives sub-second cold starts on local dev, keeping the inner loop
+ tight during exercises.
+- Static CSV catalog means no database, no migrations, no seeding scripts —
+ exercises can focus on factory workflow rather than infrastructure.
+- TypeScript + React + Tailwind matches what most working engineers use today,
+ so trainees recognize patterns immediately.
+
+**Negative / accepted trade-offs**
+
+- GitHub Pages serves static assets only. Any future feature requiring a
+ server (real orders, search, auth) will require a new ADR and likely a
+ hosting change.
+- CSV-as-database does not scale past a few hundred rows or any concurrent
+ writers. That is acceptable for the training scope.
+- Bun is younger than Node; some npm packages have rough edges under it.
+ Mitigated by keeping dependencies few and mainstream.
+
+## Alternatives considered
+
+- **Next.js on Vercel.** Rejected: requires a Vercel account, and SSR/ISR
+ features add concepts the training doesn't need.
+- **Plain HTML + a static site generator (Astro, 11ty).** Rejected: trainees
+ expect React; SSGs add a meta-framework concept that distracts from factory
+ exercises.
+- **Firebase Hosting + Firestore.** Rejected: introduces account setup,
+ billing risk, and a real database — all out of scope for the example.
+- **Jest for unit tests.** Rejected in favor of Vitest, see Testing above.
+
+### Default branch
+
+`main`. Acme Widgets has no need for parallel major-version branches and
+follows the GitHub default.
+
+## Open questions
+
+- ~~Should the CSV be parsed at build time (typed import) or fetched at
+ runtime (smaller bundle, easier to hot-swap)?~~ **Resolved during
+ implementation:** runtime fetch via `papaparse`, with the parsed list
+ cached in module scope so the home featured trio and the catalog page
+ share one network request. See `src/lib/widgets.ts`.
+
+## Related
+
+- As-built stack (versions, conventions): see
+ [`../current/STACK.md`](../current/STACK.md).
+- Hosting & repo architecture (two GitHub remotes + one Dolt remote): see
+ [`../current/HOSTING.md`](../current/HOSTING.md).
+- Information architecture (routes, page purposes): see
+ [`../current/INFORMATION_ARCHITECTURE.md`](../current/INFORMATION_ARCHITECTURE.md).
+- Personas (customers, producers, employees): see
+ [`../current/PERSONAS.md`](../current/PERSONAS.md).
+- Testing strategy: see [`../current/TESTING.md`](../current/TESTING.md).
+- Documentation convention: see
+ [`../current/DOCUMENTATION.md`](../current/DOCUMENTATION.md).
diff --git a/docs/decision-records/0002.ADR.DYNAMIC_WIDGET_CATALOG.md b/docs/decision-records/0002.ADR.DYNAMIC_WIDGET_CATALOG.md
new file mode 100644
index 0000000..3ec7ace
--- /dev/null
+++ b/docs/decision-records/0002.ADR.DYNAMIC_WIDGET_CATALOG.md
@@ -0,0 +1,178 @@
+# ADR 0002: Dynamic Widget Catalog Backed by Google Sheets
+
+- **Status:** Accepted
+- **Date:** 2026-05-01
+- **Deciders:** Bryan
+- **Supersedes (in part):** [ADR 0001 — "Data" section](./0001.ADR.TECH_STACK.md#data)
+
+## Context
+
+ADR 0001 chose a static CSV at `public/data/widgets.csv` as the widget
+catalog. Producers and employees edit the catalog by opening a PR against
+that file. That worked while the catalog was small and edits were rare,
+but two friction points have surfaced:
+
+1. **Edits require a deploy.** A widget price change or stock correction
+ waits on a PR review, a CI build, and a Pages deploy. That is the right
+ shape for code, not for data the business expects to update casually.
+2. **Non-engineer producers cannot self-serve.** Editing CSV in GitHub is
+ a poor experience for the producer/employee personas described in
+ [`PERSONAS.md`](../current/PERSONAS.md), and we have no good way to
+ teach trainees factory workflows around catalog updates without
+ inventing one.
+
+We want catalog edits to be live (visible to site visitors within minutes,
+not on the next deploy) and to happen in a tool a non-engineer already
+knows. We do not want to walk back any of ADR 0001's hard constraints:
+
+- Fast trainee setup, no account provisioning required to run locally.
+- Zero hosting cost; GitHub Pages stays as the host.
+- No backend, no database, no paid services on the critical path.
+
+## Decision
+
+### Source of truth
+
+The widget catalog moves to a single Google Sheet, "Acme Widgets — Catalog,"
+with one row per widget and a header row whose column names match the
+existing CSV (`type`, `description`, `number_in_stock`, `price`, `icon`,
+`categories`). Producers and employees edit the sheet directly; engineers
+no longer ship PRs to update prices or stock counts.
+
+### Delivery to the browser
+
+The sheet is **published to web as CSV** via Google Sheets'
+`File → Share → Publish to web → CSV` feature. The published URL is a
+stable, public, CORS-permissive endpoint of the form:
+
+```
+https://docs.google.com/spreadsheets/d/e/{publish-id}/pub?output=csv
+```
+
+The site fetches that URL at runtime from the browser, parses it with the
+existing `papaparse` pipeline in `src/lib/widgets.ts`, and renders the
+catalog. No API key, no OAuth, no backend, and no GCP project are
+required. Google's edge serves the CSV; we treat it as a CDN we don't pay
+for.
+
+### Configuration
+
+The published-CSV URL is provided to the build via a Vite env var
+(`VITE_WIDGETS_SHEET_URL`) and read in `src/lib/widgets.ts`. The default
+value points at the canonical Acme Widgets sheet so a fresh clone runs
+out of the box without any setup. Trainees who want to point the example
+at their own sheet override the env var locally.
+
+### Fallback
+
+`public/data/widgets.csv` stays in the repo as a **last-resort fallback**
+only. If the Sheets fetch fails (network error, sheet unpublished,
+Google outage), `fetchWidgets()` retries against the local CSV and logs
+a warning to the browser console. The fallback is whatever is checked
+into git at the time — Acme Widgets is a demonstration site, not a
+production storefront, so the fallback existing matters more than it
+being current. There is no scheduled refresh; if the snapshot drifts
+from the live sheet, that is acceptable for the demo.
+
+### Schema validation
+
+Bad data shows up in two shapes, and they are handled asymmetrically:
+
+- **Bad row** — a single widget is missing a required field (`type` or
+ `icon`). The current behavior is preserved: skip the row and log a
+ warning to the browser console. The rest of the catalog renders.
+- **Bad header** — a column has been renamed in the sheet (e.g.
+ `price` → `Price ($)`), which would silently corrupt that column for
+ every widget. This fails loud: `parseWidgetsCsv` throws, and the
+ catalog page renders a visible error state instead of garbage.
+
+Validation runs at runtime in the browser. There is no scheduled CI
+check against the live sheet — it would reintroduce the same kind of
+scheduled ops work we declined for the fallback refresh.
+
+### Local development
+
+`bun dev` hits the live sheet by default, matching production behavior.
+If the live fetch fails, the same fallback path applies and the warning
+appears in the browser console — trainees working offline will see the
+catalog from the local CSV with a clear signal in the console that
+they're not on the live data.
+
+### Caching and freshness
+
+Google's published-CSV endpoint is edge-cached; in practice edits appear
+within ~5 minutes. The browser-side module cache in `widgets.ts` is kept
+as-is (one fetch per page load shared across views), so within a single
+session a visitor sees a consistent snapshot.
+
+## Consequences
+
+**Positive**
+
+- Producers edit prices and stock in a familiar spreadsheet UI; changes
+ reach visitors in minutes without a PR or a deploy.
+- The web app stays a static bundle on GitHub Pages. Hosting cost stays
+ zero. No backend is introduced.
+- The change is small and localized: a single fetch URL flips, the parse
+ pipeline is unchanged, and the existing tests for `parseWidgetsCsv`
+ remain valid.
+- Trainees see a recognizable real-world pattern (Sheets-as-CMS) without
+ the overhead of a real headless CMS.
+
+**Negative / accepted trade-offs**
+
+- The catalog is now publicly readable via the published-CSV URL. That
+ is acceptable — the site already publishes the same data on `/widgets`
+ — but it means the sheet must contain only catalog data, never internal
+ notes.
+- Anyone with edit access to the sheet can change live site content with
+ no review. Mitigated by limiting sheet editors to the same people who
+ could already merge PRs to `main`.
+- A Google Sheets outage degrades the site. Mitigated by the local-CSV
+ fallback.
+- The "publish to web" URL format is undocumented as a stable contract.
+ It has been stable for the better part of a decade, so we accept the
+ risk; if Google changes the format, ADR 0003 revisits the choice.
+
+## Alternatives considered
+
+- **Google Sheets API with an API key.** Rejected: requires a GCP
+ project, billing-enabled or not, and an API key in env. Conflicts with
+ ADR 0001's "fast to set up, no account provisioning" constraint.
+- **Build-time pull from the sheet via GitHub Actions, commit the CSV.**
+ Rejected: edits still wait on a CI run, which defeats the "live" goal.
+ Also rejected as a *fallback refresh* mechanism — Acme Widgets is a
+ demonstration site, and a static fallback that drifts from the live
+ sheet is acceptable for the demo.
+- **Headless CMS (Sanity, Contentful, Strapi).** Rejected: each
+ introduces an account, a free tier with a billing risk, and a
+ modeling concept (schemas, references) that is overkill for a flat
+ list of widgets and noisy for the training scope.
+- **Airtable.** Rejected: similar shape to Sheets but with a more
+ restrictive free tier and an editor experience trainees are less
+ likely to recognize.
+- **Cloudflare Worker (or similar) proxying the Sheets API.** Rejected:
+ introduces a backend service on the critical path, which ADR 0001
+ rules out.
+- **Firebase / Firestore.** Rejected for the same reasons given in ADR
+ 0001 — account setup, billing risk, real database out of scope.
+
+## Open questions
+
+- **Test strategy.** Current tests parse a fixture CSV string. The
+ fetch path needs a test against a stubbed Sheets response and a test
+ for the fallback when the fetch fails. Out of scope for this ADR;
+ capture in tasks under the implementation plan.
+
+## Related
+
+- ADR 0001 — Tech Stack & Hosting:
+ [`./0001.ADR.TECH_STACK.md`](./0001.ADR.TECH_STACK.md).
+ This ADR amends the "Data" section of 0001.
+- Information architecture:
+ [`../current/INFORMATION_ARCHITECTURE.md`](../current/INFORMATION_ARCHITECTURE.md).
+- Personas (producers/employees):
+ [`../current/PERSONAS.md`](../current/PERSONAS.md).
+- Hosting model:
+ [`../current/HOSTING.md`](../current/HOSTING.md).
+- Implementation plan: tracked in beads epic `aw-dvn`.
diff --git a/docs/decision-records/0003.ADR.SELF_REGISTERING_PRESETS.md b/docs/decision-records/0003.ADR.SELF_REGISTERING_PRESETS.md
new file mode 100644
index 0000000..c0c626d
--- /dev/null
+++ b/docs/decision-records/0003.ADR.SELF_REGISTERING_PRESETS.md
@@ -0,0 +1,228 @@
+# ADR 0003: Self-Registering Widget Presets
+
+- **Status:** Accepted
+- **Date:** 2026-05-26
+- **Deciders:** Bryan
+- **Related:** [ADR 0002 — Dynamic Widget Catalog](./0002.ADR.DYNAMIC_WIDGET_CATALOG.md)
+
+## Context
+
+A "widget" on the site is two things stitched together at render time:
+
+1. **Catalog data** — a row in the Google Sheet (or its CSV fallback at
+ `public/data/widgets.csv`): `type`, `description`, `number_in_stock`,
+ `price`, `icon`, `categories`.
+2. **Visual preset** — a small React component (a "hero" panel: gradient,
+ label, cog icon) that gives the card its character. Today, all fifteen
+ presets live in `src/components/widgetPresets.tsx` and are looked up
+ via a single `widgetPresets: Record` object keyed by
+ the `icon` filename (e.g. `brass-widget.svg` → `Brass`).
+
+The two halves connect through one column. `WidgetCard` does:
+
+```ts
+const preset = widgetPresets[widget.icon] ?? standardPreset;
+```
+
+`widget.icon` is the value of the `icon` cell in the sheet. If the sheet
+says `brass-widget.svg`, the registry's `'brass-widget.svg'` entry is
+used. If the sheet says something the registry doesn't know about, the
+card falls back to `Standard`. The `.svg` suffix is historical — there
+is no real `brass-widget.svg` file on disk (only
+`public/icons/placeholder.svg` exists). The string is just a registry
+key shaped like a filename.
+
+That contract works fine at 15 widgets. It does not scale to the
+workflow the rest of the system is designed for.
+
+ADR 0002 deliberately makes adding catalog **data** a no-code,
+no-PR operation: producers append rows to the sheet and the live site
+picks them up within minutes. But adding a **new visual style** still
+means editing `widgetPresets.tsx` — a single file with one append-only
+registry object at the bottom. Two consequences:
+
+1. **The registry is a serialization point.** N agents (or humans) each
+ adding a preset all edit the same object literal in the same file.
+ Every parallel branch conflicts on the registry block. The work is
+ embarrassingly parallel in nature but artificially serial in
+ practice.
+2. **Visual work and catalog work drift.** A new row can be added to
+ the sheet immediately; the matching visual lands days later in a
+ PR. The fallback to `Standard` makes this safe but invisible —
+ nothing on the site or in the repo tells you that
+ `meteor-widget.svg` is referenced by the sheet but has no preset
+ defined.
+
+## Decision
+
+### Per-preset files
+
+Move each preset to its own file under `src/components/presets/`,
+one file per preset, named after the registry key it owns:
+
+```
+src/components/presets/
+ brass-widget.tsx
+ steel-widget.tsx
+ pocket-widget.tsx
+ ...
+ _standard.tsx # the fallback; leading underscore keeps it
+ # out of the auto-discovery glob
+ _shared.tsx # CogEl, cogClass, shared types
+```
+
+Each preset module has a single default export of type `Preset`. No
+registry edit is required to add one — dropping a new file into
+`src/components/presets/` is the entire change.
+
+### Auto-discovery via `import.meta.glob`
+
+The registry is built at module-load time by Vite's
+[`import.meta.glob`](https://vitejs.dev/guide/features.html#glob-import),
+which expands at build into a static map of `path → module`. Roughly:
+
+```ts
+// src/components/widgetPresets.ts
+import type { Preset } from './presets/_shared';
+import standardPreset from './presets/_standard';
+
+const modules = import.meta.glob<{ default: Preset }>(
+ './presets/[a-z]*.tsx',
+ { eager: true },
+);
+
+export const widgetPresets: Record = Object.fromEntries(
+ Object.entries(modules).map(([path, mod]) => {
+ const key = path.replace(/^\.\/presets\//, '').replace(/\.tsx$/, '');
+ return [`${key}.svg`, mod.default];
+ }),
+);
+
+export { standardPreset };
+```
+
+`WidgetCard` is unchanged: `widgetPresets[widget.icon] ?? standardPreset`
+still works because the generated registry has the same shape.
+
+### How sheet data matches a preset
+
+The contract between the catalog and the visual layer is **one column:
+`icon`**. The mapping is:
+
+| Sheet cell (`icon`) | Preset file path |
+| --------------------- | ----------------------------------------- |
+| `brass-widget.svg` | `src/components/presets/brass-widget.tsx` |
+| `steel-widget.svg` | `src/components/presets/steel-widget.tsx` |
+| `meteor-widget.svg` | *(no file)* → falls back to `_standard` |
+| *(empty)* | row is skipped by `parseWidgetsCsv` |
+
+The rule:
+
+1. Strip the `.svg` suffix from the `icon` cell.
+2. Look for `src/components/presets/.tsx`.
+3. If present, its default export is the preset.
+4. If absent, render `_standard.tsx`.
+
+Producers editing the sheet do not need to know this mapping exists —
+they pick from a documented list of available `icon` values, the same
+way they do today. Engineers adding a new visual style add a file at
+the path that matches the `icon` value the sheet will use.
+
+### Documenting available icons
+
+Today the list of valid `icon` values is implicit — you discover them
+by reading `widgetPresets.tsx`. With per-file presets, the list of
+valid values is the contents of `src/components/presets/` (minus the
+leading-underscore files). Generate a short reference table in
+`docs/current/EDITING_THE_CATALOG.md` from that directory at build time,
+or maintain it by hand — either way, the source of truth is the
+filesystem, not a registry object.
+
+### Naming and validation
+
+- **Filenames are lowercase, hyphenated**, matching the `icon` column
+ convention (`brass-widget.tsx`, not `BrassWidget.tsx`).
+- **Files starting with `_`** (`_standard.tsx`, `_shared.tsx`) are
+ excluded from the glob. The leading-underscore convention is what
+ makes the discovery rule unambiguous.
+- **A unit test** asserts that every preset file's default export
+ satisfies the `Preset` type and that the generated registry has no
+ duplicate keys. This catches a malformed preset at CI time rather
+ than at render time.
+- **No runtime check** that every `icon` value in the sheet has a
+ matching file. That would re-introduce the kind of build-time
+ coupling between catalog data and code that ADR 0002 explicitly
+ rejected. The `_standard` fallback remains the contract for
+ unknown icons.
+
+## Consequences
+
+**Positive**
+
+- Adding a new visual preset is a single new file. N engineers or
+ agents can add N presets on N branches with zero merge conflicts on
+ shared registry code.
+- The pairing between an `icon` cell value and a preset is now visible
+ on the filesystem — `ls src/components/presets/` is the list of
+ supported icons.
+- `widgetPresets.tsx` shrinks from ~350 lines of mixed registry-and-
+ styling to a ~15-line generator. Visual code lives next to the
+ preset it belongs to.
+- The `WidgetCard` consumer is unchanged; the public API of
+ `widgetPresets` and `standardPreset` is preserved, so this is a
+ pure refactor at the call site.
+
+**Negative / accepted trade-offs**
+
+- `import.meta.glob` is a Vite-specific feature. We are already
+ committed to Vite by ADR 0001, so this does not add a new lock-in,
+ but it does deepen the existing one. A move off Vite would need to
+ replace the glob with an explicit `index.ts` re-export.
+- The mapping rule (strip `.svg`, look up filename) is one indirection
+ more than the current explicit object literal. The trade-off is
+ fewer touch points per change in exchange for one documented rule.
+- Eager glob loading bundles every preset into the initial chunk, the
+ same as today. If the preset count grows large enough that this
+ matters (it does not at 15 or even 100), switching to a lazy glob
+ and `React.lazy` is a follow-up — out of scope here.
+
+## Alternatives considered
+
+- **Status quo.** Rejected: the registry-as-bottleneck problem is the
+ whole reason this ADR exists.
+- **One file per preset, manual `index.ts` re-export.** Splits the
+ per-file work cleanly but reintroduces a single shared file
+ (`index.ts`) that every new preset has to edit. Solves half the
+ problem.
+- **Runtime registration via side-effectful `register()` calls inside
+ each preset module.** Works, but depends on every module being
+ imported for its side effect, which is fragile under tree-shaking
+ and requires a barrel file anyway. The `import.meta.glob` approach
+ gets the same effect declaratively and statically.
+- **Move presets into the catalog data itself** (e.g. a `preset_json`
+ column in the sheet describing the hero panel). Rejected: presets
+ are React components with Tailwind classes and Heroicons imports,
+ not data. Embedding them in the sheet would either restrict them to
+ a tiny declarative subset or invite arbitrary JSX-in-a-cell, both
+ worse than the current split.
+
+## Open questions
+
+- **Naming the fallback.** `_standard.tsx` keeps the fallback out of
+ the glob and signals "not user-pickable." If we ever want
+ `standard-widget.svg` in the sheet to render the same visual,
+ re-export `_standard` from a regular `standard-widget.tsx`.
+- **Generated icon reference in `EDITING_THE_CATALOG.md`.** Worth a
+ short script in `package.json` (`bun run docs:icons`) or kept
+ manual? Defer until the preset count grows past ~30.
+
+## Related
+
+- ADR 0001 — Tech Stack & Hosting:
+ [`./0001.ADR.TECH_STACK.md`](./0001.ADR.TECH_STACK.md).
+- ADR 0002 — Dynamic Widget Catalog:
+ [`./0002.ADR.DYNAMIC_WIDGET_CATALOG.md`](./0002.ADR.DYNAMIC_WIDGET_CATALOG.md).
+- Current registry: `src/components/widgetPresets.tsx`.
+- Consumer: `src/components/WidgetCard.tsx`.
+- Catalog editing guide:
+ [`../current/EDITING_THE_CATALOG.md`](../current/EDITING_THE_CATALOG.md).
diff --git a/docs/human-review-logs/aw-dvn--dynamic-catalog.md b/docs/human-review-logs/aw-dvn--dynamic-catalog.md
new file mode 100644
index 0000000..5251289
--- /dev/null
+++ b/docs/human-review-logs/aw-dvn--dynamic-catalog.md
@@ -0,0 +1,93 @@
+# Human review — aw-dvn: dynamic widget catalog (Google Sheets)
+
+**Bead:** [`aw-dvn`](../../README.md) (epic) — see also `aw-dvn.1` through `aw-dvn.11`.
+**ADR:** [`0002.ADR.DYNAMIC_WIDGET_CATALOG.md`](../decision-records/0002.ADR.DYNAMIC_WIDGET_CATALOG.md).
+**Branch:** `aw-dvn/dynamic-catalog`.
+
+The diff is small per file but spans data, routing, UI, config, and docs.
+Most of the runtime behavior is exercised by unit tests (referenced inline
+below) — this log focuses on what the tests *cannot* verify: copy quality,
+end-to-end UX of each failure mode, and the human-only setup items.
+
+## Editorial — copy and writing
+
+The new producer-facing doc and the new error UI are user-visible to
+non-engineers. Tests assert they exist; they cannot tell us whether the
+writing is clear, accurate, or appropriately scoped.
+
+- [ ] [`docs/current/EDITING_THE_CATALOG.md`](../current/EDITING_THE_CATALOG.md)
+ reads cleanly to a non-engineer producer/employee. The column table is
+ accurate. The "do not rename headers" warning is unmistakable. The
+ "what to do when something looks wrong" section matches the actual
+ failure modes.
+- [ ] `WidgetsRouteError` copy in
+ [`src/pages/Widgets.tsx`](../../src/pages/Widgets.tsx) is the right
+ tone for the brand and points users somewhere useful. Both link
+ destinations (Home, How to order) make sense as fallbacks for a
+ catalog outage.
+- [ ] [`README.md`](../../README.md) "Editing the catalog" section is
+ accurate. The reframing of `widgets.csv` as "fallback only, editing it
+ does not change the live site" lands clearly.
+- [ ] [`docs/current/STACK.md`](../current/STACK.md) "Catalog data source"
+ section accurately describes what shipped (not what was originally
+ planned).
+
+## Failure-mode walkthrough
+
+The fetch path has six discrete branches. Each is unit-tested in isolation,
+but the *user-facing experience* of each is worth a one-time manual pass.
+Test names are referenced so you don't redo what's already covered — this
+is about confirming the test contract feels right when seen in a real
+browser, not about catching regressions.
+
+- [ ] **Live sheet, 200, valid CSV** — visit `/widgets` against the live
+ site (or `bun run dev`). All 15 widgets render. No `console.warn`
+ from the fetch path. *Covered by:* `fetches the sheet, parses it,
+ and caches the result` in `src/lib/widgets.test.ts`.
+- [ ] **Sheet network down → fallback succeeds.** In `.env.local`, set
+ `VITE_WIDGETS_SHEET_URL=https://invalid.example.invalid/`. Restart
+ `bun run dev`. `/widgets` should render the bundled snapshot, and the
+ browser console should show the "Sheet catalog unreachable; falling
+ back to bundled widgets.csv" warning. *Covered by:* `falls back to
+ the bundled CSV with a console.warn on a network error`.
+- [ ] **Sheet 404 → fallback succeeds.** Set
+ `VITE_WIDGETS_SHEET_URL=http://localhost:5173/no-such-file.csv`.
+ Same expected outcome. *Covered by:* `falls back when the sheet
+ returns a non-OK HTTP response`.
+- [ ] **Sheet OK, malformed CSV → fail loud.** Temporarily rename the
+ `price` column header in the live sheet (e.g. to `Price ($)`).
+ `/widgets` should show the `WidgetsRouteError` page within ~5 minutes
+ (Google's edge cache window) — *not* the bundled snapshot, *not*
+ every row showing $0. **Restore the header immediately after.**
+ *Covered by:* `does NOT fall back when the sheet returns 200 but
+ the CSV is malformed` and `throws when a required column is missing
+ or renamed in the header`.
+- [ ] **Both sources fail.** With the bad `.env.local` URL above, also
+ delete or rename `public/data/widgets.csv` locally. `/widgets`
+ should render `WidgetsRouteError`. (Restore the file when done.)
+- [ ] **Cached → no re-fetch within a session.** Open `/widgets`, then
+ navigate Home → Widgets again. The sheet should not be fetched a
+ second time within the same page session. *Covered by:* the cache
+ assertion in `fetches the sheet, parses it, and caches the result`.
+
+## Setup / governance items
+
+These cannot be verified from the diff and should be confirmed by someone
+with access to the canonical Google account.
+
+- [ ] The URL in `.env`'s `VITE_WIDGETS_SHEET_URL` points at the canonical
+ "Acme Widgets — Catalog" sheet — not a personal scratch sheet, not a
+ copy.
+- [ ] Editor access on that sheet is restricted to people who already have
+ merge rights on `main`, per ADR 0002. Anyone outside that group is
+ "viewer" only.
+- [ ] The sheet contains catalog data only — no internal notes, no draft
+ copy, no customer info. (The published-CSV URL is publicly readable.)
+
+## Out of scope for this review
+
+- Whether the *architecture* is right: that's ADR 0002, which was
+ reviewed separately before this branch was cut.
+- Visual design of `WidgetCard`, the category chips, or the catalog page
+ layout: unchanged in this PR.
+- Changes to the test runner setup or CI: none.
diff --git a/docs/roadmap b/docs/roadmap
new file mode 120000
index 0000000..5a1521c
--- /dev/null
+++ b/docs/roadmap
@@ -0,0 +1 @@
+future
\ No newline at end of file
diff --git a/e2e/home.spec.ts b/e2e/home.spec.ts
new file mode 100644
index 0000000..ab7e604
--- /dev/null
+++ b/e2e/home.spec.ts
@@ -0,0 +1,17 @@
+import { expect, test } from '@playwright/test';
+
+test('home page renders the hero h1 and links to the catalog', async ({
+ page,
+}) => {
+ await page.goto('/');
+
+ await expect(
+ page.getByRole('heading', { level: 1, name: /widgets for all purposes/i }),
+ ).toBeVisible();
+
+ // Primary CTA links to /widgets. Use the link role + name; the wordmark
+ // logo also links to /, but it doesn't match /catalog/i.
+ await expect(
+ page.getByRole('link', { name: /catalog/i }),
+ ).toHaveAttribute('href', '/widgets');
+});
diff --git a/e2e/orders.spec.ts b/e2e/orders.spec.ts
new file mode 100644
index 0000000..776e386
--- /dev/null
+++ b/e2e/orders.spec.ts
@@ -0,0 +1,17 @@
+import { expect, test } from '@playwright/test';
+
+test('orders page renders an h1 and the sales mailto link', async ({ page }) => {
+ await page.goto('/orders');
+
+ // Asserting an h1 exists so removing it in source produces a useful
+ // failure ("expected to be visible" + the matched query).
+ await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
+
+ // The literal email string must be visible somewhere in the DOM.
+ await expect(page.getByText('sales@acmewidgets.com')).toBeVisible();
+
+ // The mailto link points exactly at the bare address (no query params).
+ await expect(
+ page.getByRole('link', { name: /sales@acmewidgets\.com/ }),
+ ).toHaveAttribute('href', 'mailto:sales@acmewidgets.com');
+});
diff --git a/e2e/widgets.spec.ts b/e2e/widgets.spec.ts
new file mode 100644
index 0000000..b0b4d78
--- /dev/null
+++ b/e2e/widgets.spec.ts
@@ -0,0 +1,20 @@
+import { expect, test } from '@playwright/test';
+
+test('widgets page renders cards from the live CSV with quoted-comma intact', async ({
+ page,
+}) => {
+ await page.goto('/widgets');
+
+ // Wait for at least one card; we look up a stable name string from the
+ // CSV rather than a row index so reordering doesn't break the spec.
+ const classicBrass = page.getByRole('article', { name: 'Classic Brass Widget' });
+ await expect(classicBrass).toBeVisible();
+
+ // The CSV row's description has an embedded comma — verify it survived
+ // the parse intact (would truncate if a naive split-on-comma were used).
+ await expect(
+ classicBrass.getByText(
+ 'A timeless hand-polished brass widget, suitable for everyday use and treasured heirloom display.',
+ ),
+ ).toBeVisible();
+});
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..5b90c10
--- /dev/null
+++ b/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+ Acme Widgets
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..b29f668
--- /dev/null
+++ b/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "acme-widgets",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "engines": {
+ "bun": ">=1.2"
+ },
+ "scripts": {
+ "dev": "bun --bun vite",
+ "build": "bun --bun tsc -b && bun --bun vite build",
+ "postbuild": "cp -f dist/index.html dist/404.html",
+ "preview": "bun --bun vite preview",
+ "typecheck": "bun --bun tsc --noEmit",
+ "test": "bun --bun vitest run",
+ "test:watch": "bun --bun vitest --watch",
+ "test:e2e": "playwright test",
+ "test:e2e:headed": "playwright test --headed"
+ },
+ "dependencies": {
+ "@heroicons/react": "^2.2.0",
+ "papaparse": "^5.5.3",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router": "^7.14.2"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.59.1",
+ "@tailwindcss/vite": "^4.2.4",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/papaparse": "^5.5.2",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "@vitejs/plugin-react": "^4.3.4",
+ "jsdom": "^29.1.0",
+ "tailwindcss": "^4.2.4",
+ "typescript": "^5.7.0",
+ "vite": "^6.0.0",
+ "vitest": "^4.1.5"
+ }
+}
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 0000000..267670c
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './e2e',
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: 1,
+ reporter: process.env.CI
+ ? [['github'], ['html', { open: 'never' }]]
+ : 'list',
+ use: {
+ baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:4173',
+ },
+ webServer: {
+ command: 'bun run preview',
+ url: 'http://localhost:4173',
+ reuseExistingServer: !process.env.CI,
+ timeout: 120_000,
+ },
+ projects: [{ name: 'chromium', use: devices['Desktop Chrome'] }],
+});
diff --git a/public/data/widgets.csv b/public/data/widgets.csv
new file mode 100644
index 0000000..8965793
--- /dev/null
+++ b/public/data/widgets.csv
@@ -0,0 +1,16 @@
+type,description,number_in_stock,price,icon,categories
+Classic Brass Widget,"A timeless hand-polished brass widget, suitable for everyday use and treasured heirloom display.",42,19.99,brass-widget.svg,everyday;artisanal;gift
+Precision Steel Widget,"Machined to 0.001mm tolerance for industrial assemblies and high-precision applications.",128,89.50,steel-widget.svg,industrial;precision;business
+Pocket Widget Mini,"A travel-sized widget that fits in any pocket. Perfect first widget for kids and curious newcomers.",260,4.99,pocket-widget.svg,everyday;kids;starter
+Birthday Sparkle Widget,"Festive widget with confetti finish, designed to make any birthday celebration sparkle.",75,12.50,sparkle-widget.svg,occasion;birthday;gift
+Wedding Heirloom Widget,"Hand-engraved silver-plated widget presented in a velvet box, a keepsake for the happy couple.",18,149.00,heirloom-widget.svg,occasion;wedding;artisanal;gift
+Holiday Evergreen Widget,"Seasonal widget finished in evergreen lacquer with gold accents for the winter holidays.",94,24.99,evergreen-widget.svg,occasion;holiday;gift
+Heavy-Duty Industrial Widget,"Reinforced cast-iron widget rated for continuous operation in demanding factory environments.",36,219.00,industrial-widget.svg,industrial;business;heavy-duty
+Artisan Walnut Widget,"Hand-turned walnut widget from a small Vermont workshop. Each piece is unique.",22,64.00,walnut-widget.svg,artisanal;gift;everyday
+Replacement Standard Widget,"Direct drop-in replacement for the most common widget assembly. Keep one in the drawer.",410,7.99,standard-widget.svg,everyday;replacement;starter
+Anniversary Gold Widget,"24k gold-leafed widget mounted on a polished marble base. Marks the milestones that matter.",9,389.00,gold-widget.svg,occasion;anniversary;artisanal;gift
+Lab-Grade Calibration Widget,"NIST-traceable calibration widget for laboratory and metrology use, shipped with certificate.",54,142.75,lab-widget.svg,precision;business;laboratory
+Kids' Rainbow Widget,"Brightly colored, BPA-free widget sized for small hands. A great first widget.",185,6.50,rainbow-widget.svg,kids;starter;gift
+Graduation Commemorative Widget,"Engravable widget with mortarboard motif, a thoughtful gift for the new graduate.",47,34.99,graduation-widget.svg,occasion;graduation;gift
+Workshop Bulk-Pack Widget,"Box of 25 general-purpose widgets at a working price for shops, makers, and tinkerers.",62,79.00,bulk-widget.svg,business;everyday;bulk
+Vintage-Style Copper Widget,"Antiqued copper widget styled after the original 1952 Acme catalog design.",28,42.00,copper-widget.svg,artisanal;vintage;everyday
diff --git a/public/icons/placeholder.svg b/public/icons/placeholder.svg
new file mode 100644
index 0000000..279cca4
--- /dev/null
+++ b/public/icons/placeholder.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
new file mode 100644
index 0000000..2e8b735
--- /dev/null
+++ b/src/components/Footer.tsx
@@ -0,0 +1,30 @@
+import { Link } from 'react-router';
+
+const links = [
+ { to: '/orders', label: 'Contact Us' },
+ { to: '/pricing-page', label: 'Pricing' },
+ { to: '/feedback-form', label: 'Feedback' },
+];
+
+export default function Footer() {
+ return (
+
+ );
+}
diff --git a/src/components/TopNav.test.tsx b/src/components/TopNav.test.tsx
new file mode 100644
index 0000000..32adac8
--- /dev/null
+++ b/src/components/TopNav.test.tsx
@@ -0,0 +1,67 @@
+import { render, screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { createMemoryRouter, Outlet, RouterProvider } from 'react-router';
+import { describe, expect, it } from 'vitest';
+import TopNav from './TopNav';
+
+function renderTopNavAtPath(initialPath: string) {
+ const router = createMemoryRouter(
+ [
+ {
+ path: '/',
+ element: (
+ <>
+
+
+ >
+ ),
+ children: [
+ { index: true, element:
+ Every widget in the catalog, with current list prices. Bulk and
+ institutional discounts available — see{' '}
+
+ ordering
+
+ .
+
+
+
+ {widgets.length === 0 ? (
+
+ No widgets available.
+
+ ) : (
+
+
+
+
+
+ Widget
+
+
+ Categories
+
+
+ Price
+
+
+
+
+ {widgets.map((w, i) => (
+
+
+
+ {w.type}
+
+
+ {w.description}
+
+
+
+
+ {w.categories.join(' · ')}
+
+
+
+ {currency.format(w.price)}
+
+
+ ))}
+
+
+
+ )}
+
+ );
+}
diff --git a/src/pages/Widgets.tsx b/src/pages/Widgets.tsx
new file mode 100644
index 0000000..864a2f4
--- /dev/null
+++ b/src/pages/Widgets.tsx
@@ -0,0 +1,205 @@
+import { useMemo } from 'react';
+import { Link, useLoaderData, useRouteError, useSearchParams } from 'react-router';
+import WidgetCard from '../components/WidgetCard';
+import WidgetCardSkeleton from '../components/WidgetCardSkeleton';
+import { useDocumentTitle } from '../hooks/useDocumentTitle';
+import type { Widget } from '../lib/widgets';
+import { fetchWidgets } from '../lib/widgets';
+
+export async function widgetsLoader(): Promise {
+ return fetchWidgets();
+}
+
+export function WidgetsRouteError() {
+ const error = useRouteError();
+ const isDev = import.meta.env.DEV;
+ const detail =
+ isDev && error instanceof Error ? error.message : null;
+
+ return (
+
+
+ The Catalog is temporarily unavailable
+
+
+ We couldn’t load the widget catalog right now. This is usually a
+ transient issue — try refreshing in a few minutes. If it persists,
+ please get in touch.
+