krz/brand-bench

A dynamic brand documentation generator with Ollama integration.

clone: git clone https://gitbay.org/krz/brand-bench.git

14818e090e468685a7240126c671292fbf99dd10

unsigned

author: Christian Cleberg <hello@cleberg.net> · 2026-04-19T03:32:53Z

initial commit
 LICENSE                            |   21 +
 README.md                          |  108 +++
 docs/screenshot.png                |  Bin 0 -> 317271 bytes
 index.html                         |   12 +
 package-lock.json                  | 1768 ++++++++++++++++++++++++++++++++++++
 package.json                       |   22 +
 src/App.tsx                        |  130 +++
 src/components/BrandDoc.tsx        |  518 +++++++++++
 src/components/CopyButton.tsx      |   31 +
 src/components/InputPanel.tsx      |  133 +++
 src/components/PackageSwitcher.tsx |  125 +++
 src/components/PreviewPanel.tsx    |  116 +++
 src/components/SettingsPanel.tsx   |  158 ++++
 src/components/TokenInput.tsx      |   85 ++
 src/engine/aiGenerator.ts          |  138 +++
 src/engine/generator.ts            |  957 +++++++++++++++++++
 src/hooks/usePackages.ts           |  112 +++
 src/hooks/useSettings.ts           |   41 +
 src/hooks/useWorkspace.ts          |  225 +++++
 src/lib/clipboard.ts               |   17 +
 src/lib/export.ts                  |  326 +++++++
 src/lib/sanitize.ts                |  141 +++
 src/main.tsx                       |   10 +
 src/styles/globals.css             | 1594 ++++++++++++++++++++++++++++++++
 src/types.ts                       |  100 ++
 tsconfig.json                      |   20 +
 vite.config.ts                     |    6 +
 27 files changed, 6914 insertions(+)

diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..382ed52
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 ZeroLabs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..82ba080
--- /dev/null
+++ b/README.md
@@ -0,0 +1,108 @@
+# Brand Bench
+
+A client-side brand identity generator. Describe a project and get a complete
+brand package — positioning, tone of voice, color palette, typography system,
+logo concepts, taglines, and usage examples — instantly in the browser.
+
+No backend. No accounts. Everything runs locally and persists in `localStorage`.
+
+![Brandbench screenshot](docs/screenshot.png)
+
+## Features
+
+- **Template generation** — instant, offline-capable brand packages tailored by
+  category (developer tool, creative studio, SaaS product, etc.)
+- **AI generation** — connect your local [Ollama](https://ollama.com) instance
+  for LLM-powered output; cancellable mid-stream
+- **Multiple packages** — create, rename, duplicate, and switch between brand
+  packages in tabs; each package is independently persisted
+- **Inline editing** — click any field in the preview to edit it directly
+- **Section locking** — lock sections before regenerating so they stay unchanged
+  across runs
+- **Undo / redo** — full edit history per package (`⌘Z` / `⌘⇧Z`)
+- **Color palette** — editable swatches with a native color picker
+- **Typography system** — curated font pairings per category with an 8-level
+  type scale
+- **Export** — download as Markdown, JSON, or a self-contained HTML guidelines page
+
+## Getting started
+
+```bash
+npm install
+npm run dev
+```
+
+Open `http://localhost:5173`.
+
+To build for production:
+
+```bash
+npm run build      # outputs to dist/
+npm run preview    # serve the build locally
+```
+
+## AI with Ollama
+
+Brandbench can generate brand packages using a locally running Ollama model.
+
+1. [Install Ollama](https://ollama.com/download) and pull a model:
+
+   ```bash
+   ollama pull llama3.2        # recommended default
+   ollama pull mistral         # good alternative
+   ollama pull gemma3          # another option
+   ```
+
+2. Make sure Ollama is running:
+
+   ```bash
+   ollama serve
+   ```
+
+3. If you're running Ollama on a non-default port or a different host, set the
+   `OLLAMA_ORIGINS` environment variable to allow browser requests:
+
+   ```bash
+   OLLAMA_ORIGINS="*" ollama serve
+   ```
+
+4. Open Settings (⚙) in the app, enable AI, set your base URL
+   (`http://localhost:11434` by default), select a model, and click **Test
+   connection**.
+
+Larger models produce better-structured output. If generation fails with a JSON
+error, try a bigger model. Generation typically takes 15–60 seconds depending on
+hardware.
+
+## Project structure
+
+```
+src/
+  engine/
+    generator.ts       # Template-based brand package generator
+    aiGenerator.ts     # Ollama integration
+  hooks/
+    useWorkspace.ts    # Per-package state, generate, undo/redo
+    usePackages.ts     # Multi-package tabs and localStorage slots
+    useSettings.ts     # Ollama settings persistence
+  components/
+    InputPanel.tsx     # Project details form
+    PreviewPanel.tsx   # Brand package preview with toolbar
+    BrandDoc.tsx       # Full brand document (sections, palette, type scale)
+    PackageSwitcher.tsx  # Tab bar with rename/duplicate/delete
+    SettingsPanel.tsx  # AI settings drawer
+  lib/
+    sanitize.ts        # Defensive coercion for AI output fields
+    export.ts          # Markdown / JSON / HTML export formatters
+  types.ts             # All shared TypeScript interfaces
+```
+
+## Tech stack
+
+- [React 18](https://react.dev) + [TypeScript](https://www.typescriptlang.org)
+- [Vite](https://vitejs.dev)
+- No UI library — plain CSS with custom properties
+
+## License
+
+MIT
diff --git a/docs/screenshot.png b/docs/screenshot.png
new file mode 100644
index 0000000..be70fe9
Binary files /dev/null and b/docs/screenshot.png differ
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..a115e71
--- /dev/null
+++ b/index.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Brand Workbench</title>
+  </head>
+  <body>
+    <div id="root"></div>
+    <script type="module" src="/src/main.tsx"></script>
+  </body>
+</html>
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..bb40e52
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1768 @@
+{
+  "name": "brand-workbench",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "brand-workbench",
+      "version": "0.1.0",
+      "dependencies": {
+        "react": "^18.3.1",
+        "react-dom": "^18.3.1"
+      },
+      "devDependencies": {
+        "@types/react": "^18.3.1",
+        "@types/react-dom": "^18.3.1",
+        "@vitejs/plugin-react": "^4.3.1",
+        "typescript": "^5.5.3",
+        "vite": "^5.4.1"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+      "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+      "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+      "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+      "dev": true,
+      "license": "MIT",
+      "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"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.29.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+      "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+      "dev": true,
+      "license": "MIT",
+      "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"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+      "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+      "dev": true,
+      "license": "MIT",
+      "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"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-globals": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+      "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+      "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+      "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.28.6",
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "@babel/traverse": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+      "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+      "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+      "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.29.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+      "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-self": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+      "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-source": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+      "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+      "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.28.6",
+        "@babel/parser": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+      "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+      "dev": true,
+      "license": "MIT",
+      "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"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-beta.27",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+      "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
+      "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
+      "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
+      "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
+      "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
+      "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
+      "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
+      "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
+      "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
+      "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
+      "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
+      "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
+      "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
+      "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
+      "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
+      "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
+      "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
+      "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
+      "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
+      "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
+      "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
+      "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
+      "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
+      "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
+      "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
+      "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@types/babel__core": {
+      "version": "7.20.5",
+      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+      "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.20.7",
+        "@babel/types": "^7.20.7",
+        "@types/babel__generator": "*",
+        "@types/babel__template": "*",
+        "@types/babel__traverse": "*"
+      }
+    },
+    "node_modules/@types/babel__generator": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+      "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__template": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+      "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__traverse": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+      "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.28.2"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/prop-types": {
+      "version": "15.7.15",
+      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+      "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/react": {
+      "version": "18.3.28",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
+      "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/prop-types": "*",
+        "csstype": "^3.2.2"
+      }
+    },
+    "node_modules/@types/react-dom": {
+      "version": "18.3.7",
+      "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+      "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "^18.0.0"
+      }
+    },
+    "node_modules/@vitejs/plugin-react": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+      "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+      "dev": true,
+      "license": "MIT",
+      "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"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+      }
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.10.19",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz",
+      "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.2",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+      "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "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"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001788",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
+      "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.340",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz",
+      "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/esbuild": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "license": "MIT"
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.37",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
+      "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/postcss": {
+      "version": "8.5.10",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
+      "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/react": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+      "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+      "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0",
+        "scheduler": "^0.23.2"
+      },
+      "peerDependencies": {
+        "react": "^18.3.1"
+      }
+    },
+    "node_modules/react-refresh": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+      "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
+      "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.60.1",
+        "@rollup/rollup-android-arm64": "4.60.1",
+        "@rollup/rollup-darwin-arm64": "4.60.1",
+        "@rollup/rollup-darwin-x64": "4.60.1",
+        "@rollup/rollup-freebsd-arm64": "4.60.1",
+        "@rollup/rollup-freebsd-x64": "4.60.1",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
+        "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
+        "@rollup/rollup-linux-arm64-gnu": "4.60.1",
+        "@rollup/rollup-linux-arm64-musl": "4.60.1",
+        "@rollup/rollup-linux-loong64-gnu": "4.60.1",
+        "@rollup/rollup-linux-loong64-musl": "4.60.1",
+        "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
+        "@rollup/rollup-linux-ppc64-musl": "4.60.1",
+        "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
+        "@rollup/rollup-linux-riscv64-musl": "4.60.1",
+        "@rollup/rollup-linux-s390x-gnu": "4.60.1",
+        "@rollup/rollup-linux-x64-gnu": "4.60.1",
+        "@rollup/rollup-linux-x64-musl": "4.60.1",
+        "@rollup/rollup-openbsd-x64": "4.60.1",
+        "@rollup/rollup-openharmony-arm64": "4.60.1",
+        "@rollup/rollup-win32-arm64-msvc": "4.60.1",
+        "@rollup/rollup-win32-ia32-msvc": "4.60.1",
+        "@rollup/rollup-win32-x64-gnu": "4.60.1",
+        "@rollup/rollup-win32-x64-msvc": "4.60.1",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/scheduler": {
+      "version": "0.23.2",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+      "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      }
+    },
+    "node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+      "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/vite": {
+      "version": "5.4.21",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+      "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.21.3",
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || >=20.0.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "dev": true,
+      "license": "ISC"
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f5a768b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,22 @@
+{
+  "name": "brand-workbench",
+  "version": "0.1.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc && vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "react": "^18.3.1",
+    "react-dom": "^18.3.1"
+  },
+  "devDependencies": {
+    "@types/react": "^18.3.1",
+    "@types/react-dom": "^18.3.1",
+    "@vitejs/plugin-react": "^4.3.1",
+    "typescript": "^5.5.3",
+    "vite": "^5.4.1"
+  }
+}
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..bd43726
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,130 @@
+import { useState, useEffect } from 'react';
+import { useWorkspace } from './hooks/useWorkspace';
+import { usePackages } from './hooks/usePackages';
+import { useSettings } from './hooks/useSettings';
+import type { PackageSlot } from './hooks/usePackages';
+import { InputPanel } from './components/InputPanel';
+import { PreviewPanel } from './components/PreviewPanel';
+import { PackageSwitcher } from './components/PackageSwitcher';
+import { SettingsPanel } from './components/SettingsPanel';
+import type { BrandOutputs } from './types';
+
+interface WorkspaceShellProps {
+  storageKey: string;
+  slots: PackageSlot[];
+  activeId: string;
+  onSwitch: (id: string) => void;
+  onCreate: () => void;
+  onRemove: (id: string) => void;
+  onRename: (id: string, name: string) => void;
+  onDuplicate: (id: string) => void;
+  onOpenSettings: () => void;
+  aiEnabled: boolean;
+}
+
+function WorkspaceShell({
+  storageKey, slots, activeId,
+  onSwitch, onCreate, onRemove, onRename, onDuplicate,
+  onOpenSettings, aiEnabled,
+}: WorkspaceShellProps) {
+  const {
+    inputs, setInputs, outputs, isGenerating, generateMode, generateError,
+    locked, runGenerate, cancelGenerate, updateEdit, toggleLock,
+    undo, redo, canUndo, canRedo,
+  } = useWorkspace(storageKey);
+
+  useEffect(() => {
+    const handler = (e: KeyboardEvent) => {
+      if (!(e.metaKey || e.ctrlKey) || e.key !== 'z') return;
+      e.preventDefault();
+      if (e.shiftKey) redo();
+      else undo();
+    };
+    window.addEventListener('keydown', handler);
+    return () => window.removeEventListener('keydown', handler);
+  }, [undo, redo]);
+
+  const handleEdit = <K extends keyof BrandOutputs>(key: K, value: BrandOutputs[K]) =>
+    updateEdit(key, value);
+
+  const generateLabel = isGenerating
+    ? 'Cancel'
+    : outputs
+      ? (aiEnabled ? `Regenerate` : 'Regenerate')
+      : (aiEnabled ? 'Generate with AI' : 'Generate');
+
+  return (
+    <>
+      <header className="app-header">
+        <div className="app-header-left">
+          <span className="app-wordmark">Brand<span>bench</span></span>
+          <PackageSwitcher
+            slots={slots} activeId={activeId}
+            onSwitch={onSwitch} onCreate={onCreate}
+            onRemove={onRemove} onRename={onRename} onDuplicate={onDuplicate}
+          />
+        </div>
+        <div className="app-header-right">
+          {outputs && (
+            <>
+              <button className="btn btn-ghost" onClick={undo} disabled={!canUndo} title="Undo (⌘Z)">Undo</button>
+              <button className="btn btn-ghost" onClick={redo} disabled={!canRedo} title="Redo (⌘⇧Z)">Redo</button>
+            </>
+          )}
+          {aiEnabled && (
+            <span className="ai-badge" title={`AI mode: ${generateMode}`}>AI</span>
+          )}
+          <button className="btn btn-ghost settings-open-btn" onClick={onOpenSettings} title="Settings">⚙</button>
+        </div>
+      </header>
+
+      <div className="app-shell">
+        <InputPanel
+          inputs={inputs}
+          onChange={setInputs}
+          onGenerate={isGenerating ? cancelGenerate : runGenerate}
+          isGenerating={isGenerating}
+          generateLabel={generateLabel}
+        />
+        <PreviewPanel
+          inputs={inputs}
+          outputs={outputs}
+          locked={locked}
+          onToggleLock={toggleLock}
+          onEdit={handleEdit}
+          isGenerating={isGenerating}
+          generateMode={generateMode}
+          generateError={generateError}
+          onGenerate={runGenerate}
+        />
+      </div>
+    </>
+  );
+}
+
+export default function App() {
+  const { slots, activeId, activeSlot, switchTo, createNew, remove, rename, duplicate } = usePackages();
+  const { settings, setSettings } = useSettings();
+  const [settingsOpen, setSettingsOpen] = useState(false);
+
+  return (
+    <>
+      <WorkspaceShell
+        key={activeId}
+        storageKey={activeSlot.storageKey}
+        slots={slots} activeId={activeId}
+        onSwitch={switchTo} onCreate={createNew}
+        onRemove={remove} onRename={rename} onDuplicate={duplicate}
+        onOpenSettings={() => setSettingsOpen(true)}
+        aiEnabled={settings.enabled}
+      />
+      {settingsOpen && (
+        <SettingsPanel
+          settings={settings}
+          onChange={setSettings}
+          onClose={() => setSettingsOpen(false)}
+        />
+      )}
+    </>
+  );
+}
diff --git a/src/components/BrandDoc.tsx b/src/components/BrandDoc.tsx
new file mode 100644
index 0000000..fffea97
--- /dev/null
+++ b/src/components/BrandDoc.tsx
@@ -0,0 +1,518 @@
+import { useRef, useState } from 'react';
+import type { BrandInputs, BrandOutputs, ColorSwatch, LockedSections, Typography } from '../types';
+import { CopyButton } from './CopyButton';
+
+// ── Editable text ─────────────────────────────────────────────────────────────
+
+interface EditableProps {
+  value: string;
+  onChange: (v: string) => void;
+  multiline?: boolean;
+}
+
+function Editable({ value, onChange, multiline = false }: EditableProps) {
+  const [editing, setEditing] = useState(false);
+  const [draft, setDraft] = useState(value);
+  const ref = useRef<HTMLTextAreaElement | HTMLInputElement>(null);
+
+  const commit = () => {
+    setEditing(false);
+    if (draft !== value) onChange(draft);
+  };
+
+  if (!editing) {
+    return (
+      <div
+        className="editable-text"
+        onClick={() => { setDraft(value); setEditing(true); }}
+        title="Click to edit"
+        style={{ whiteSpace: multiline ? 'pre-wrap' : 'normal', padding: '2px 4px', margin: '-2px -4px' }}
+      >
+        {value}
+      </div>
+    );
+  }
+
+  if (multiline) {
+    return (
+      <textarea
+        ref={ref as React.RefObject<HTMLTextAreaElement>}
+        autoFocus
+        className="editable-text editing"
+        value={draft}
+        onChange={e => setDraft(e.target.value)}
+        onBlur={commit}
+        onKeyDown={e => { if (e.key === 'Escape') { setEditing(false); setDraft(value); } }}
+        style={{
+          width: '100%',
+          resize: 'vertical',
+          minHeight: 80,
+          padding: '4px 6px',
+          fontFamily: 'inherit',
+          fontSize: 'inherit',
+          lineHeight: 1.65,
+          background: 'var(--bg-2)',
+          border: '1px solid var(--border-3)',
+          borderRadius: 3,
+          color: 'var(--text)',
+          outline: 'none',
+        }}
+      />
+    );
+  }
+
+  return (
+    <input
+      ref={ref as React.RefObject<HTMLInputElement>}
+      autoFocus
+      className="editable-text editing"
+      value={draft}
+      onChange={e => setDraft(e.target.value)}
+      onBlur={commit}
+      onKeyDown={e => {
+        if (e.key === 'Enter') commit();
+        if (e.key === 'Escape') { setEditing(false); setDraft(value); }
+      }}
+      style={{
+        width: '100%',
+        padding: '2px 6px',
+        fontFamily: 'inherit',
+        fontSize: 'inherit',
+        background: 'var(--bg-2)',
+        border: '1px solid var(--border-3)',
+        borderRadius: 3,
+        color: 'var(--text)',
+        outline: 'none',
+      }}
+    />
+  );
+}
+
+// ── Section wrapper ───────────────────────────────────────────────────────────
+
+interface SectionProps {
+  title: string;
+  locked: boolean;
+  onToggleLock: () => void;
+  copyText?: string;
+  children: React.ReactNode;
+}
+
+function Section({ title, locked, onToggleLock, copyText, children }: SectionProps) {
+  return (
+    <div className={`doc-section${locked ? ' is-locked' : ''}`}>
+      <div className="doc-section-header">
+        <div className="doc-section-title">{title}</div>
+        <div className="doc-section-actions">
+          {copyText && <CopyButton text={copyText} label="Copy" />}
+          <button
+            type="button"
+            className={`section-lock${locked ? ' locked' : ''}`}
+            onClick={onToggleLock}
+            title={locked ? 'Locked — click to unlock' : 'Lock to preserve on regenerate'}
+          >
+            {locked ? '● locked' : '○ lock'}
+          </button>
+        </div>
+      </div>
+      <div className="doc-section-body">
+        {children}
+      </div>
+    </div>
+  );
+}
+
+// ── Editable list item ────────────────────────────────────────────────────────
+
+interface EditableListProps {
+  items: string[];
+  onChange: (items: string[]) => void;
+  numbered?: boolean;
+}
+
+function EditableList({ items, onChange, numbered = true }: EditableListProps) {
+  const updateAt = (i: number, v: string) => {
+    const next = [...items];
+    next[i] = v;
+    onChange(next);
+  };
+
+  if (numbered) {
+    return (
+      <div className="numbered-list">
+        {items.map((item, i) => (
+          <div key={i} className="numbered-item">
+            <span className="numbered-item-num">{i + 1}.</span>
+            <div
+              className="numbered-item-text"
+              contentEditable
+              suppressContentEditableWarning
+              onBlur={e => updateAt(i, e.currentTarget.textContent ?? '')}
+              onKeyDown={e => { if (e.key === 'Escape') e.currentTarget.blur(); }}
+            >
+              {item}
+            </div>
+          </div>
+        ))}
+      </div>
+    );
+  }
+
+  return (
+    <div className="bullet-list">
+      {items.map((item, i) => (
+        <div key={i} className="bullet-item">
+          {item}
+        </div>
+      ))}
+    </div>
+  );
+}
+
+// ── Typography section ────────────────────────────────────────────────────────
+
+function TypographySection({ typography }: { typography: Typography }) {
+  return (
+    <div className="type-section">
+      <div className="type-fonts">
+        <div className="type-font-row">
+          <span className="type-font-role">Primary</span>
+          <span className="type-font-name">{typography.primary}</span>
+        </div>
+        {typography.secondary !== typography.primary && (
+          <div className="type-font-row">
+            <span className="type-font-role">Secondary</span>
+            <span className="type-font-name">{typography.secondary}</span>
+          </div>
+        )}
+        <div className="type-font-row">
+          <span className="type-font-role">Monospace</span>
+          <span className="type-font-name" style={{ fontFamily: 'var(--font-mono)' }}>{typography.mono}</span>
+        </div>
+        <div className="type-pair-note">{typography.pairNote}</div>
+      </div>
+
+      <div className="type-scale">
+        <div className="type-scale-header">
+          <span>Style</span>
+          <span>Size</span>
+          <span>Weight</span>
+          <span className="type-scale-usage">Usage</span>
+        </div>
+        {typography.scale.map(token => (
+          <div key={token.label} className="type-scale-row">
+            <span className="type-scale-label">{token.label}</span>
+            <span className="type-scale-size">{token.size}</span>
+            <span className="type-scale-weight">{token.weight}</span>
+            <span className="type-scale-usage">{token.usage}</span>
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+}
+
+// ── Color swatch card ─────────────────────────────────────────────────────────
+
+interface ColorSwatchCardProps {
+  swatch: ColorSwatch;
+  onChange: (s: ColorSwatch) => void;
+  onRemove: () => void;
+}
+
+function ColorSwatchCard({ swatch, onChange, onRemove }: ColorSwatchCardProps) {
+  const [nameEditing, setNameEditing] = useState(false);
+  const [nameDraft, setNameDraft] = useState(swatch.name);
+  const [hexEditing, setHexEditing] = useState(false);
+  const [hexDraft, setHexDraft] = useState(swatch.hex);
+
+  const commitName = () => {
+    setNameEditing(false);
+    const v = nameDraft.trim();
+    if (v && v !== swatch.name) onChange({ ...swatch, name: v });
+  };
+
+  const commitHex = () => {
+    setHexEditing(false);
+    const v = hexDraft.trim().toLowerCase();
+    const normalized = v.startsWith('#') ? v : `#${v}`;
+    if (/^#[0-9a-f]{6}$/.test(normalized)) {
+      onChange({ ...swatch, hex: normalized });
+    } else {
+      setHexDraft(swatch.hex);
+    }
+  };
+
+  return (
+    <div className="color-swatch-card">
+      <div className="color-swatch-preview" style={{ background: swatch.hex }}>
+        <input
+          type="color"
+          className="color-swatch-picker"
+          value={swatch.hex}
+          onChange={e => onChange({ ...swatch, hex: e.target.value })}
+          title="Pick color"
+        />
+        <button className="color-swatch-remove" onClick={onRemove} title="Remove">×</button>
+      </div>
+      <div className="color-swatch-info">
+        {nameEditing ? (
+          <input
+            autoFocus
+            className="color-swatch-field-input"
+            value={nameDraft}
+            onChange={e => setNameDraft(e.target.value)}
+            onBlur={commitName}
+            onKeyDown={e => {
+              if (e.key === 'Enter') commitName();
+              if (e.key === 'Escape') { setNameEditing(false); setNameDraft(swatch.name); }
+            }}
+          />
+        ) : (
+          <div className="color-swatch-name" onClick={() => { setNameDraft(swatch.name); setNameEditing(true); }} title="Click to edit">
+            {swatch.name}
+          </div>
+        )}
+        {hexEditing ? (
+          <input
+            autoFocus
+            className="color-swatch-field-input color-swatch-hex-input"
+            value={hexDraft}
+            onChange={e => setHexDraft(e.target.value)}
+            onBlur={commitHex}
+            onKeyDown={e => {
+              if (e.key === 'Enter') commitHex();
+              if (e.key === 'Escape') { setHexEditing(false); setHexDraft(swatch.hex); }
+            }}
+          />
+        ) : (
+          <div className="color-swatch-hex" onClick={() => { setHexDraft(swatch.hex); setHexEditing(true); }} title="Click to edit">
+            {swatch.hex}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
+
+// ── Main BrandDoc ─────────────────────────────────────────────────────────────
+
+interface Props {
+  inputs: BrandInputs;
+  outputs: BrandOutputs;
+  locked: LockedSections;
+  onToggleLock: (section: keyof LockedSections) => void;
+  onEdit: <K extends keyof BrandOutputs>(key: K, value: BrandOutputs[K]) => void;
+}
+
+export function BrandDoc({ inputs, outputs, locked, onToggleLock, onEdit }: Props) {
+  const paletteCopy = outputs.palette.swatches
+    .map(s => `${s.name}: ${s.hex}`)
+    .join('\n');
+
+  const updateSwatch = (index: number, updated: ColorSwatch) => {
+    const swatches = [...outputs.palette.swatches];
+    swatches[index] = updated;
+    onEdit('palette', { swatches });
+  };
+
+  const removeSwatch = (index: number) => {
+    const swatches = outputs.palette.swatches.filter((_, i) => i !== index);
+    onEdit('palette', { swatches });
+  };
+
+  const addSwatch = () => {
+    const id = `s${Date.now()}`;
+    onEdit('palette', {
+      swatches: [...outputs.palette.swatches, { id, name: 'New Color', hex: '#888888', role: 'accent' }],
+    });
+  };
+
+  const messagingCopy = [
+    'Titles:',
+    ...outputs.titles.map((t, i) => `${i + 1}. ${t}`),
+    '',
+    'Taglines:',
+    ...outputs.taglines.map((t, i) => `${i + 1}. ${t}`),
+  ].join('\n');
+
+  return (
+    <div className="brand-doc">
+      <div className="brand-doc-header">
+        <div className="brand-doc-title">{inputs.name || 'Brand Package'}</div>
+        <div className="brand-doc-meta">
+          {inputs.category && (
+            <span className="brand-doc-meta-item">
+              <span className="brand-doc-meta-label">category</span>
+              {inputs.category}
+            </span>
+          )}
+          {inputs.audience && (
+            <span className="brand-doc-meta-item">
+              <span className="brand-doc-meta-label">audience</span>
+              {inputs.audience}
+            </span>
+          )}
+        </div>
+      </div>
+
+      {/* Overview */}
+      <Section title="Overview" locked={locked.overview} onToggleLock={() => onToggleLock('overview')} copyText={outputs.overview}>
+        <Editable value={outputs.overview} onChange={v => onEdit('overview', v)} multiline />
+      </Section>
+
+      {/* Positioning */}
+      <Section title="Positioning" locked={locked.positioning} onToggleLock={() => onToggleLock('positioning')} copyText={outputs.positioning}>
+        <Editable value={outputs.positioning} onChange={v => onEdit('positioning', v)} multiline />
+      </Section>
+
+      {/* Tone */}
+      <Section title="Tone & Voice" locked={locked.tone} onToggleLock={() => onToggleLock('tone')}>
+        <div className="tone-grid">
+          <div className="tone-row">
+            <div className="tone-row-label">Attributes</div>
+            <div className="tone-tags">
+              {outputs.tone.attributes.map(a => (
+                <span key={a} className="tone-tag">{a}</span>
+              ))}
+            </div>
+          </div>
+          <div className="tone-row">
+            <div className="tone-row-label">Voice</div>
+            <div className="tone-row-value">{outputs.tone.voiceNotes}</div>
+          </div>
+          {outputs.tone.avoidList.length > 0 && (
+            <div className="tone-row">
+              <div className="tone-row-label">Avoid</div>
+              <div className="tone-row-value">{outputs.tone.avoidList.join(', ')}</div>
+            </div>
+          )}
+          {outputs.tone.examplePhrases.length > 0 && (
+            <div className="tone-row">
+              <div className="tone-row-label">Example phrases</div>
+              <div className="tone-phrases">
+                {outputs.tone.examplePhrases.map((p, i) => (
+                  <div key={i} className="tone-phrase">{p}</div>
+                ))}
+              </div>
+            </div>
+          )}
+        </div>
+      </Section>
+
+      {/* Messaging */}
+      <Section title="Messaging" locked={locked.messaging} onToggleLock={() => onToggleLock('messaging')} copyText={messagingCopy}>
+        <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
+          <div>
+            <div style={{ fontSize: 11, color: 'var(--text-3)', fontWeight: 500, marginBottom: 10 }}>Titles</div>
+            <EditableList items={outputs.titles} onChange={v => onEdit('titles', v)} />
+          </div>
+          <div>
+            <div style={{ fontSize: 11, color: 'var(--text-3)', fontWeight: 500, marginBottom: 10 }}>Subtitles</div>
+            <EditableList items={outputs.subtitles} onChange={v => onEdit('subtitles', v)} />
+          </div>
+          <div>
+            <div style={{ fontSize: 11, color: 'var(--text-3)', fontWeight: 500, marginBottom: 10 }}>Taglines</div>
+            <EditableList items={outputs.taglines} onChange={v => onEdit('taglines', v)} />
+          </div>
+        </div>
+      </Section>
+
+      {/* Visual Directions */}
+      <Section title="Visual Direction" locked={locked.visual} onToggleLock={() => onToggleLock('visual')}>
+        <div className="directions-grid">
+          {outputs.visualDirections.map(dir => (
+            <div key={dir.id} className="direction-card">
+              <div className="direction-name">{dir.name}</div>
+              <div className="direction-desc">{dir.description}</div>
+              <div className="direction-attrs">
+                <div className="direction-attr">
+                  <span className="direction-attr-label">Palette</span>
+                  <span className="direction-attr-value">{dir.palette}</span>
+                </div>
+                <div className="direction-attr">
+                  <span className="direction-attr-label">Typography</span>
+                  <span className="direction-attr-value">{dir.typography}</span>
+                </div>
+                <div className="direction-attr">
+                  <span className="direction-attr-label">References</span>
+                  <span className="direction-attr-value">{dir.references}</span>
+                </div>
+              </div>
+            </div>
+          ))}
+        </div>
+      </Section>
+
+      {/* Color Palette */}
+      <Section title="Color Palette" locked={locked.palette} onToggleLock={() => onToggleLock('palette')} copyText={paletteCopy}>
+        <div className="color-palette">
+          {outputs.palette.swatches.map((swatch, i) => (
+            <ColorSwatchCard
+              key={swatch.id}
+              swatch={swatch}
+              onChange={updated => updateSwatch(i, updated)}
+              onRemove={() => removeSwatch(i)}
+            />
+          ))}
+          <button className="color-swatch-add" onClick={addSwatch} title="Add color">
+            +
+          </button>
+        </div>
+      </Section>
+
+      {/* Typography */}
+      <Section title="Typography" locked={locked.typography} onToggleLock={() => onToggleLock('typography')}>
+        <TypographySection typography={outputs.typography} />
+      </Section>
+
+      {/* Logo Concepts */}
+      <Section title="Logo Concepts" locked={locked.logo} onToggleLock={() => onToggleLock('logo')}>
+        <div className="logo-grid">
+          {outputs.logoConcepts.map(lc => (
+            <div key={lc.id} className="logo-card">
+              <div className="logo-card-title">{lc.title}</div>
+              <div className="logo-card-concept">{lc.concept}</div>
+              <div className="logo-card-attrs">
+                <div className="logo-card-attr">
+                  <span className="logo-card-attr-label">Mark</span>
+                  <span className="logo-card-attr-value">{lc.mark}</span>
+                </div>
+                <div className="logo-card-attr">
+                  <span className="logo-card-attr-label">Execution</span>
+                  <span className="logo-card-attr-value">{lc.execution}</span>
+                </div>
+              </div>
+            </div>
+          ))}
+        </div>
+      </Section>
+
+      {/* Usage Examples */}
+      <Section title="Usage Examples" locked={locked.usage} onToggleLock={() => onToggleLock('usage')}>
+        <div className="usage-grid">
+          {outputs.usageExamples.map((ex, i) => (
+            <div key={i} className="usage-item">
+              <div className="usage-context">{ex.context}</div>
+              <div className="usage-text" style={{ position: 'relative' }}>
+                {ex.text}
+                <div className="usage-copy">
+                  <CopyButton text={ex.text} label="Copy" />
+                </div>
+              </div>
+            </div>
+          ))}
+        </div>
+      </Section>
+
+      {/* Constraints */}
+      <Section title="Constraints" locked={locked.constraints} onToggleLock={() => onToggleLock('constraints')}>
+        <div className="constraints-list">
+          {outputs.constraints.map((c, i) => (
+            <div key={i} className="constraint-item">{c}</div>
+          ))}
+        </div>
+      </Section>
+    </div>
+  );
+}
diff --git a/src/components/CopyButton.tsx b/src/components/CopyButton.tsx
new file mode 100644
index 0000000..18a7433
--- /dev/null
+++ b/src/components/CopyButton.tsx
@@ -0,0 +1,31 @@
+import { useState } from 'react';
+import { copyToClipboard } from '../lib/clipboard';
+
+interface Props {
+  text: string;
+  label?: string;
+  className?: string;
+}
+
+export function CopyButton({ text, label = 'Copy', className = '' }: Props) {
+  const [copied, setCopied] = useState(false);
+
+  const handle = async () => {
+    const ok = await copyToClipboard(text);
+    if (ok) {
+      setCopied(true);
+      setTimeout(() => setCopied(false), 1500);
+    }
+  };
+
+  return (
+    <button
+      type="button"
+      className={`copy-btn${copied ? ' copied' : ''} ${className}`}
+      onClick={handle}
+      title={label}
+    >
+      {copied ? '✓ Copied' : label}
+    </button>
+  );
+}
diff --git a/src/components/InputPanel.tsx b/src/components/InputPanel.tsx
new file mode 100644
index 0000000..c4ad902
--- /dev/null
+++ b/src/components/InputPanel.tsx
@@ -0,0 +1,133 @@
+import type { BrandInputs } from '../types';
+import { TokenInput } from './TokenInput';
+
+const TONE_SUGGESTIONS = ['minimal', 'technical', 'calm', 'bold', 'warm', 'dry', 'focused', 'sharp'];
+const AVOID_SUGGESTIONS = ['buzzwords', 'hype', 'startup language', 'passive voice', 'corporate tone', 'exclamation points', 'superlatives'];
+
+interface Props {
+  inputs: BrandInputs;
+  onChange: (inputs: BrandInputs) => void;
+  onGenerate: () => void;
+  isGenerating: boolean;
+  generateLabel?: string;
+}
+
+export function InputPanel({ inputs, onChange, onGenerate, isGenerating, generateLabel }: Props) {
+  const set = <K extends keyof BrandInputs>(key: K) =>
+    (value: BrandInputs[K]) => onChange({ ...inputs, [key]: value });
+
+  const handleKey = (e: React.KeyboardEvent) => {
+    if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
+      e.preventDefault();
+      onGenerate();
+    }
+  };
+
+  const canGenerate = inputs.name.trim().length > 0 && inputs.purpose.trim().length > 0;
+
+  return (
+    <div className="input-panel" onKeyDown={handleKey}>
+      <div className="input-section">
+        <div className="input-section-label">Project</div>
+        <div className="field">
+          <label className="field-label" htmlFor="f-name">Name</label>
+          <input
+            id="f-name"
+            className="field-input"
+            placeholder="e.g. Foundry"
+            value={inputs.name}
+            onChange={e => set('name')(e.target.value)}
+          />
+        </div>
+        <div className="field">
+          <label className="field-label" htmlFor="f-category">Category</label>
+          <input
+            id="f-category"
+            className="field-input"
+            placeholder="e.g. developer tool, design studio"
+            value={inputs.category}
+            onChange={e => set('category')(e.target.value)}
+          />
+        </div>
+        <div className="field">
+          <label className="field-label" htmlFor="f-purpose">One-line purpose</label>
+          <input
+            id="f-purpose"
+            className="field-input"
+            placeholder="e.g. deploy microservices without boilerplate"
+            value={inputs.purpose}
+            onChange={e => set('purpose')(e.target.value)}
+          />
+        </div>
+        <div className="field">
+          <label className="field-label" htmlFor="f-audience">Audience</label>
+          <input
+            id="f-audience"
+            className="field-input"
+            placeholder="e.g. backend engineers"
+            value={inputs.audience}
+            onChange={e => set('audience')(e.target.value)}
+          />
+        </div>
+      </div>
+
+      <div className="input-section">
+        <div className="input-section-label">Voice</div>
+        <div className="field">
+          <TokenInput
+            label="Tone attributes"
+            values={inputs.tone}
+            onChange={set('tone')}
+            suggestions={TONE_SUGGESTIONS}
+            placeholder="Add tone..."
+          />
+        </div>
+        <div className="field" style={{ marginTop: 8 }}>
+          <TokenInput
+            label="Avoid"
+            values={inputs.avoid}
+            onChange={set('avoid')}
+            suggestions={AVOID_SUGGESTIONS}
+            placeholder="Add avoid..."
+          />
+        </div>
+      </div>
+
+      <div className="input-section">
+        <div className="input-section-label">Notes</div>
+        <div className="field">
+          <label className="field-label sr-only" htmlFor="f-notes">Notes & constraints</label>
+          <textarea
+            id="f-notes"
+            className="field-textarea"
+            placeholder="Optional: constraints, context, inspirations, or anything the brand package should reflect."
+            value={inputs.notes}
+            onChange={e => set('notes')(e.target.value)}
+            rows={4}
+          />
+        </div>
+      </div>
+
+      <div className="generate-area">
+        <button
+          className={`btn-generate${isGenerating ? ' generating' : ''}`}
+          onClick={onGenerate}
+          disabled={!isGenerating && !canGenerate}
+          title={!canGenerate ? 'Enter a name and purpose to generate' : 'Generate brand package (⌘Enter)'}
+        >
+          {generateLabel ?? (isGenerating ? 'Generating…' : 'Generate brand package')}
+        </button>
+        {!canGenerate && (
+          <div style={{ marginTop: 7, fontSize: 11, color: 'var(--text-4)', textAlign: 'center' }}>
+            Name and purpose required
+          </div>
+        )}
+        {canGenerate && !isGenerating && (
+          <div style={{ marginTop: 7, fontSize: 11, color: 'var(--text-4)', textAlign: 'center' }}>
+            ⌘ Enter to generate
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
diff --git a/src/components/PackageSwitcher.tsx b/src/components/PackageSwitcher.tsx
new file mode 100644
index 0000000..38c1967
--- /dev/null
+++ b/src/components/PackageSwitcher.tsx
@@ -0,0 +1,125 @@
+import { useState, useEffect } from 'react';
+import { createPortal } from 'react-dom';
+import type { PackageSlot } from '../hooks/usePackages';
+
+interface Props {
+  slots: PackageSlot[];
+  activeId: string;
+  onSwitch: (id: string) => void;
+  onCreate: () => void;
+  onRemove: (id: string) => void;
+  onRename: (id: string, name: string) => void;
+  onDuplicate: (id: string) => void;
+}
+
+interface MenuState {
+  id: string;
+  top: number;
+  left: number;
+}
+
+export function PackageSwitcher({ slots, activeId, onSwitch, onCreate, onRemove, onRename, onDuplicate }: Props) {
+  const [editingId, setEditingId] = useState<string | null>(null);
+  const [draft, setDraft] = useState('');
+  const [menu, setMenu] = useState<MenuState | null>(null);
+
+  // Close dropdown on any outside click
+  useEffect(() => {
+    if (!menu) return;
+    const handler = () => setMenu(null);
+    document.addEventListener('click', handler);
+    return () => document.removeEventListener('click', handler);
+  }, [menu]);
+
+  const openMenu = (id: string, e: React.MouseEvent<HTMLButtonElement>) => {
+    e.stopPropagation();
+    if (menu?.id === id) { setMenu(null); return; }
+    const rect = e.currentTarget.getBoundingClientRect();
+    setMenu({ id, top: rect.bottom + 4, left: rect.left });
+  };
+
+  const commitRename = (id: string) => {
+    const name = draft.trim();
+    if (name) onRename(id, name);
+    setEditingId(null);
+  };
+
+  return (
+    <div className="pkg-switcher">
+      {slots.map(slot => (
+        <div
+          key={slot.id}
+          className={`pkg-tab${slot.id === activeId ? ' active' : ''}`}
+          onClick={() => slot.id !== activeId && onSwitch(slot.id)}
+        >
+          {editingId === slot.id ? (
+            <input
+              autoFocus
+              className="pkg-tab-input"
+              value={draft}
+              onChange={e => setDraft(e.target.value)}
+              onBlur={() => commitRename(slot.id)}
+              onKeyDown={e => {
+                if (e.key === 'Enter') commitRename(slot.id);
+                if (e.key === 'Escape') setEditingId(null);
+              }}
+              onClick={e => e.stopPropagation()}
+            />
+          ) : (
+            <span
+              className="pkg-tab-name"
+              onDoubleClick={e => {
+                e.stopPropagation();
+                setDraft(slot.name);
+                setEditingId(slot.id);
+              }}
+            >
+              {slot.name}
+            </span>
+          )}
+
+          <div className="pkg-tab-actions">
+            <button
+              className="pkg-tab-menu-btn"
+              title="Options"
+              onClick={e => openMenu(slot.id, e)}
+            >
+              ···
+            </button>
+            {slots.length > 1 && (
+              <button
+                className="pkg-tab-close"
+                title="Remove"
+                onClick={e => { e.stopPropagation(); onRemove(slot.id); }}
+              >
+                ×
+              </button>
+            )}
+          </div>
+        </div>
+      ))}
+
+      <button className="pkg-new-btn" onClick={onCreate} title="New package">+</button>
+
+      {/* Dropdown rendered in a portal to escape overflow clipping */}
+      {menu && createPortal(
+        <div
+          className="pkg-tab-dropdown"
+          style={{ position: 'fixed', top: menu.top, left: menu.left }}
+          onClick={e => e.stopPropagation()}
+        >
+          <button onClick={() => {
+            const slot = slots.find(s => s.id === menu.id);
+            if (slot) { setDraft(slot.name); setEditingId(slot.id); }
+            setMenu(null);
+          }}>Rename</button>
+          <button onClick={() => { onDuplicate(menu.id); setMenu(null); }}>Duplicate</button>
+          {slots.length > 1 && (
+            <button className="danger" onClick={() => { onRemove(menu.id); setMenu(null); }}>Delete</button>
+          )}
+        </div>,
+        document.body
+      )}
+    </div>
+  );
+}
diff --git a/src/components/PreviewPanel.tsx b/src/components/PreviewPanel.tsx
new file mode 100644
index 0000000..695698a
--- /dev/null
+++ b/src/components/PreviewPanel.tsx
@@ -0,0 +1,116 @@
+import type { BrandInputs, BrandOutputs, LockedSections } from '../types';
+import { BrandDoc } from './BrandDoc';
+import { CopyButton } from './CopyButton';
+import { toMarkdown, toJSON, toHTML, downloadFile } from '../lib/export';
+
+interface Props {
+  inputs: BrandInputs;
+  outputs: BrandOutputs | null;
+  locked: LockedSections;
+  onToggleLock: (section: keyof LockedSections) => void;
+  onEdit: <K extends keyof BrandOutputs>(key: K, value: BrandOutputs[K]) => void;
+  isGenerating: boolean;
+  generateMode?: 'template' | string;
+  generateError?: string | null;
+  onGenerate?: () => void;
+}
+
+export function PreviewPanel({ inputs, outputs, locked, onToggleLock, onEdit, isGenerating, generateMode, generateError, onGenerate }: Props) {
+  const handleExportMd = () => {
+    if (!outputs) return;
+    downloadFile(toMarkdown(inputs, outputs), `${inputs.name || 'brand'}-package.md`, 'text/markdown');
+  };
+
+  const handleExportJson = () => {
+    if (!outputs) return;
+    downloadFile(toJSON(inputs, outputs), `${inputs.name || 'brand'}-package.json`, 'application/json');
+  };
+
+  const handleExportHtml = () => {
+    if (!outputs) return;
+    downloadFile(toHTML(inputs, outputs), `${inputs.name || 'brand'}-guidelines.html`, 'text/html');
+  };
+
+  const fullMarkdown = outputs ? toMarkdown(inputs, outputs) : '';
+
+  return (
+    <div className="preview-panel">
+      <div className="preview-toolbar">
+        <div className="preview-toolbar-left">
+          <span className="preview-doc-label">
+            {outputs ? 'Brand Package' : 'Preview'}
+          </span>
+          {outputs && (
+            <span style={{ fontSize: 11, color: 'var(--text-4)' }}>
+              · {new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
+            </span>
+          )}
+        </div>
+        {outputs && (
+          <div className="preview-toolbar-right">
+            <CopyButton text={fullMarkdown} label="Copy all" />
+          </div>
+        )}
+      </div>
+
+      {generateError && (
+        <div className="generate-error">
+          <span className="generate-error-icon">⚠</span>
+          <span className="generate-error-msg">{generateError}</span>
+          {onGenerate && (
+            <button className="generate-error-retry btn btn-ghost" onClick={onGenerate}>
+              Try template instead
+            </button>
+          )}
+        </div>
+      )}
+
+      <div className="preview-scroll">
+        {isGenerating ? (
+          <div className="empty-state">
+            <div className="empty-state-title" style={{ color: 'var(--text-3)' }}>
+              {generateMode && generateMode !== 'template'
+                ? `Thinking with ${generateMode}…`
+                : 'Generating…'}
+            </div>
+            {generateMode && generateMode !== 'template' && (
+              <div className="empty-state-sub">This may take 15–60 seconds depending on your hardware</div>
+            )}
+          </div>
+        ) : outputs ? (
+          <BrandDoc
+            inputs={inputs}
+            outputs={outputs}
+            locked={locked}
+            onToggleLock={onToggleLock}
+            onEdit={onEdit}
+          />
+        ) : (
+          <div className="empty-state">
+            <div className="empty-state-title">No package generated yet</div>
+            <div className="empty-state-sub">Fill in the project details and click Generate</div>
+          </div>
+        )}
+      </div>
+
+      {outputs && (
+        <div className="export-bar">
+          <span className="export-bar-left">
+            Export
+          </span>
+          <div className="export-bar-right">
+            <button className="btn" onClick={handleExportMd}>
+              Export Markdown
+            </button>
+            <button className="btn" onClick={handleExportJson}>
+              Export JSON
+            </button>
+            <button className="btn" onClick={handleExportHtml}>
+              Export HTML
+            </button>
+          </div>
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx
new file mode 100644
index 0000000..bb44c74
--- /dev/null
+++ b/src/components/SettingsPanel.tsx
@@ -0,0 +1,158 @@
+import { useState, useEffect } from 'react';
+import type { OllamaSettings } from '../hooks/useSettings';
+import { testOllamaConnection } from '../engine/aiGenerator';
+
+interface Props {
+  settings: OllamaSettings;
+  onChange: (next: Partial<OllamaSettings>) => void;
+  onClose: () => void;
+}
+
+type TestState =
+  | { status: 'idle' }
+  | { status: 'testing' }
+  | { status: 'ok'; models: string[] }
+  | { status: 'error'; message: string };
+
+export function SettingsPanel({ settings, onChange, onClose }: Props) {
+  const [url, setUrl] = useState(settings.baseUrl);
+  const [model, setModel] = useState(settings.model);
+  const [test, setTest] = useState<TestState>({ status: 'idle' });
+
+  // Commit field on blur/enter
+  const commitUrl = () => onChange({ baseUrl: url });
+  const commitModel = (m: string) => { setModel(m); onChange({ model: m }); };
+
+  const runTest = async () => {
+    onChange({ baseUrl: url }); // save current URL first
+    setTest({ status: 'testing' });
+    try {
+      const models = await testOllamaConnection(url);
+      setTest({ status: 'ok', models });
+    } catch (err) {
+      setTest({ status: 'error', message: (err as Error).message });
+    }
+  };
+
+  // Close on Escape
+  useEffect(() => {
+    const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
+    window.addEventListener('keydown', handler);
+    return () => window.removeEventListener('keydown', handler);
+  }, [onClose]);
+
+  return (
+    <div className="settings-overlay" onClick={onClose}>
+      <div className="settings-panel" onClick={e => e.stopPropagation()}>
+        <div className="settings-header">
+          <span className="settings-title">Settings</span>
+          <button className="settings-close" onClick={onClose}>×</button>
+        </div>
+
+        <div className="settings-body">
+          {/* AI toggle */}
+          <div className="settings-section">
+            <div className="settings-section-label">AI Generation</div>
+            <label className="settings-toggle-row">
+              <div className="settings-toggle-info">
+                <span className="settings-toggle-name">Use Ollama</span>
+                <span className="settings-toggle-desc">
+                  Replace template generation with a local AI model
+                </span>
+              </div>
+              <button
+                className={`toggle${settings.enabled ? ' on' : ''}`}
+                onClick={() => onChange({ enabled: !settings.enabled })}
+                role="switch"
+                aria-checked={settings.enabled}
+              >
+                <span className="toggle-thumb" />
+              </button>
+            </label>
+          </div>
+
+          {/* Ollama config */}
+          {settings.enabled && (
+            <div className="settings-section">
+              <div className="settings-section-label">Ollama</div>
+
+              <div className="settings-field">
+                <label className="settings-field-label">Server URL</label>
+                <div className="settings-field-row">
+                  <input
+                    className="settings-input"
+                    type="url"
+                    value={url}
+                    onChange={e => setUrl(e.target.value)}
+                    onBlur={commitUrl}
+                    onKeyDown={e => e.key === 'Enter' && commitUrl()}
+                    placeholder="http://localhost:11434"
+                    spellCheck={false}
+                  />
+                  <button
+                    className={`btn settings-test-btn${test.status === 'testing' ? ' testing' : ''}`}
+                    onClick={runTest}
+                    disabled={test.status === 'testing'}
+                  >
+                    {test.status === 'testing' ? 'Testing…' : 'Test'}
+                  </button>
+                </div>
+
+                {test.status === 'ok' && (
+                  <div className="settings-status ok">
+                    Connected · {test.models.length} model{test.models.length !== 1 ? 's' : ''} available
+                  </div>
+                )}
+                {test.status === 'error' && (
+                  <div className="settings-status error">{test.message}</div>
+                )}
+              </div>
+
+              <div className="settings-field">
+                <label className="settings-field-label">Model</label>
+                <input
+                  className="settings-input"
+                  value={model}
+                  onChange={e => setModel(e.target.value)}
+                  onBlur={() => commitModel(model)}
+                  onKeyDown={e => e.key === 'Enter' && commitModel(model)}
+                  placeholder="llama3.2"
+                  spellCheck={false}
+                />
+                {test.status === 'ok' && test.models.length > 0 && (
+                  <div className="settings-model-list">
+                    {test.models.map(m => (
+                      <button
+                        key={m}
+                        className={`settings-model-item${m === model ? ' active' : ''}`}
+                        onClick={() => commitModel(m)}
+                      >
+                        {m}
+                      </button>
+                    ))}
+                  </div>
+                )}
+                <div className="settings-field-hint">
+                  Pull a model first: <code>ollama pull llama3.2</code>
+                </div>
+              </div>
+            </div>
+          )}
+
+          {/* Help */}
+          <div className="settings-section settings-help">
+            <div className="settings-section-label">About Ollama</div>
+            <p>
+              Ollama runs AI models locally on your machine — no API key, no data sent externally.
+              Install from <strong>ollama.com</strong>, then pull any model to get started.
+            </p>
+            <p>
+              If connection fails, ensure Ollama is running and CORS is open:<br />
+              <code>OLLAMA_ORIGINS=* ollama serve</code>
+            </p>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/src/components/TokenInput.tsx b/src/components/TokenInput.tsx
new file mode 100644
index 0000000..c0e23de
--- /dev/null
+++ b/src/components/TokenInput.tsx
@@ -0,0 +1,85 @@
+import { useState, useRef, KeyboardEvent } from 'react';
+
+interface Props {
+  label: string;
+  values: string[];
+  onChange: (values: string[]) => void;
+  suggestions?: string[];
+  placeholder?: string;
+}
+
+export function TokenInput({ label, values, onChange, suggestions = [], placeholder = 'Type and press Enter' }: Props) {
+  const [draft, setDraft] = useState('');
+  const inputRef = useRef<HTMLInputElement>(null);
+
+  const add = (val: string) => {
+    const trimmed = val.trim().toLowerCase();
+    if (trimmed && !values.includes(trimmed)) {
+      onChange([...values, trimmed]);
+    }
+    setDraft('');
+  };
+
+  const remove = (val: string) => {
+    onChange(values.filter(v => v !== val));
+  };
+
+  const handleKey = (e: KeyboardEvent<HTMLInputElement>) => {
+    if ((e.key === 'Enter' || e.key === ',') && draft.trim()) {
+      e.preventDefault();
+      add(draft);
+    } else if (e.key === 'Backspace' && !draft && values.length > 0) {
+      remove(values[values.length - 1]);
+    }
+  };
+
+  const availableSuggestions = suggestions.filter(s => !values.includes(s));
+
+  return (
+    <div className="token-field">
+      <label className="field-label">{label}</label>
+      <div
+        className="token-container"
+        onClick={() => inputRef.current?.focus()}
+      >
+        {values.map(v => (
+          <span key={v} className="token">
+            {v}
+            <button
+              type="button"
+              className="token-remove"
+              onClick={e => { e.stopPropagation(); remove(v); }}
+              aria-label={`Remove ${v}`}
+            >
+              ×
+            </button>
+          </span>
+        ))}
+        <input
+          ref={inputRef}
+          className="token-input"
+          value={draft}
+          onChange={e => setDraft(e.target.value)}
+          onKeyDown={handleKey}
+          onBlur={() => { if (draft.trim()) add(draft); }}
+          placeholder={values.length === 0 ? placeholder : ''}
+          aria-label={label}
+        />
+      </div>
+      {availableSuggestions.length > 0 && (
+        <div className="token-suggestions">
+          {availableSuggestions.map(s => (
+            <button
+              key={s}
+              type="button"
+              className={`token-suggestion${values.includes(s) ? ' active' : ''}`}
+              onClick={() => add(s)}
+            >
+              {s}
+            </button>
+          ))}
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/src/engine/aiGenerator.ts b/src/engine/aiGenerator.ts
new file mode 100644
index 0000000..3cd9d5e
--- /dev/null
+++ b/src/engine/aiGenerator.ts
@@ -0,0 +1,138 @@
+import type { BrandInputs, BrandOutputs } from '../types';
+import type { OllamaSettings } from '../hooks/useSettings';
+import { sanitizeOutputs } from '../lib/sanitize';
+
+const SYSTEM_PROMPT = `You are an expert brand strategist and copywriter. Generate a complete brand identity package as a JSON object. Be specific and creative — every output must feel crafted for the exact brand described, not generic. Use the project details to inform all choices: tone, color palette, typography, and messaging.`;
+
+function buildUserPrompt(inputs: BrandInputs): string {
+  const lines = [
+    'Generate a brand package for this project.',
+    '',
+    'PROJECT DETAILS:',
+    `Name: ${inputs.name || 'Untitled'}`,
+    `Category: ${inputs.category || 'general'}`,
+    `Purpose: ${inputs.purpose || 'not specified'}`,
+    `Target audience: ${inputs.audience || 'not specified'}`,
+    inputs.tone.length ? `Tone words: ${inputs.tone.join(', ')}` : '',
+    inputs.avoid.length ? `Words/phrases to avoid: ${inputs.avoid.join(', ')}` : '',
+    inputs.notes ? `Additional notes: ${inputs.notes}` : '',
+    '',
+    'Return ONLY a valid JSON object with exactly this structure:',
+    '',
+    JSON.stringify({
+      overview: 'One crisp sentence: what this project is and who it helps',
+      positioning: '2–3 sentence strategic positioning statement — what makes this brand distinct',
+      tone: {
+        attributes: ['attribute1', 'attribute2', 'attribute3', 'attribute4'],
+        voiceNotes: '2–3 sentences describing how the brand writes and speaks',
+        avoidList: ['thing to never say or do', 'another thing to avoid'],
+        examplePhrases: ['A phrase that shows the brand voice', 'Another example', 'A third example'],
+      },
+      titles: ['Title option 1', 'Title option 2', 'Title option 3'],
+      subtitles: ['Subtitle option 1', 'Subtitle option 2', 'Subtitle option 3'],
+      taglines: ['Short punchy tagline', 'Alternative tagline', 'Third tagline option'],
+      palette: {
+        swatches: [
+          { id: 's0', name: 'Background', hex: '#hexcode', role: 'background' },
+          { id: 's1', name: 'Surface', hex: '#hexcode', role: 'neutral' },
+          { id: 's2', name: 'Primary', hex: '#hexcode', role: 'primary' },
+          { id: 's3', name: 'Accent', hex: '#hexcode', role: 'accent' },
+          { id: 's4', name: 'Text', hex: '#hexcode', role: 'text' },
+        ],
+      },
+      typography: {
+        primary: 'Primary font name (e.g. Inter, Playfair Display)',
+        secondary: 'Secondary font name or same as primary',
+        mono: 'Monospace font name (e.g. JetBrains Mono, Fira Code)',
+        pairNote: 'One sentence on why this pairing fits the brand',
+      },
+      visualDirections: [
+        { id: 'v1', name: 'Direction name', description: '2–3 sentence visual direction description', palette: 'Color mood description', typography: 'Type style description', references: 'Visual references and inspirations' },
+        { id: 'v2', name: 'Direction name', description: '2–3 sentence description', palette: 'Color mood', typography: 'Type style', references: 'Visual references' },
+        { id: 'v3', name: 'Direction name', description: '2–3 sentence description', palette: 'Color mood', typography: 'Type style', references: 'Visual references' },
+      ],
+      logoConcepts: [
+        { id: 'l1', title: 'Logo concept name', concept: 'Concept description and rationale', mark: 'Mark/symbol description', execution: 'Execution and usage guidelines' },
+        { id: 'l2', title: 'Second concept name', concept: 'Concept description', mark: 'Mark description', execution: 'Execution guidelines' },
+      ],
+      usageExamples: [
+        { context: 'README headline', text: 'Actual example copy here' },
+        { context: 'Landing page hero', text: 'Actual example copy here' },
+        { context: 'Social bio', text: 'Actual example copy here' },
+        { context: 'Email subject line', text: 'Actual example copy here' },
+      ],
+      constraints: [
+        'A specific copy rule',
+        'Another brand constraint',
+        'A third constraint',
+        'A fourth constraint',
+      ],
+    }, null, 2),
+  ];
+
+  return lines.filter(l => l !== null).join('\n');
+}
+
+function validate(raw: unknown): BrandOutputs {
+  const result = sanitizeOutputs(raw);
+  if (!result) throw new Error('Response is not a valid brand output object');
+  return result;
+}
+
+export async function generateWithAI(
+  inputs: BrandInputs,
+  settings: OllamaSettings,
+  signal?: AbortSignal,
+): Promise<BrandOutputs> {
+  const url = `${settings.baseUrl.replace(/\/$/, '')}/api/chat`;
+
+  let res: Response;
+  try {
+    res = await fetch(url, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      signal,
+      body: JSON.stringify({
+        model: settings.model,
+        messages: [
+          { role: 'system', content: SYSTEM_PROMPT },
+          { role: 'user', content: buildUserPrompt(inputs) },
+        ],
+        format: 'json',
+        stream: false,
+      }),
+    });
+  } catch (err) {
+    if ((err as Error).name === 'AbortError') throw err;
+    throw new Error(`Cannot reach Ollama at ${settings.baseUrl}. Is it running?`);
+  }
+
+  if (!res.ok) {
+    const text = await res.text().catch(() => '');
+    if (res.status === 404) throw new Error(`Model "${settings.model}" not found. Pull it first: ollama pull ${settings.model}`);
+    throw new Error(`Ollama error ${res.status}: ${text.slice(0, 120)}`);
+  }
+
+  const data = await res.json() as { message?: { content?: string } };
+  const content = data?.message?.content;
+  if (!content) throw new Error('Empty response from Ollama');
+
+  let parsed: unknown;
+  try {
+    parsed = JSON.parse(content);
+  } catch {
+    throw new Error('Ollama returned invalid JSON. Try a larger model.');
+  }
+
+  // validate() calls sanitizeOutputs(), which injects TYPE_SCALE for fresh AI output
+  return validate(parsed);
+}
+
+// Test connectivity and return available model names
+export async function testOllamaConnection(baseUrl: string): Promise<string[]> {
+  const url = `${baseUrl.replace(/\/$/, '')}/api/tags`;
+  const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
+  if (!res.ok) throw new Error(`Ollama responded with ${res.status}`);
+  const data = await res.json() as { models?: Array<{ name: string }> };
+  return (data.models ?? []).map(m => m.name.replace(/:latest$/, ''));
+}
diff --git a/src/engine/generator.ts b/src/engine/generator.ts
new file mode 100644
index 0000000..46ddaf7
--- /dev/null
+++ b/src/engine/generator.ts
@@ -0,0 +1,957 @@
+import type {
+  BrandInputs,
+  BrandOutputs,
+  ToneGuidance,
+  VisualDirection,
+  LogoConcept,
+  UsageExample,
+  ColorPalette,
+  ColorSwatch,
+  Typography,
+  TypographyToken,
+} from '../types';
+
+type CategoryType = 'developer' | 'creative' | 'product' | 'services' | 'personal' | 'general';
+
+interface GenContext {
+  name: string;
+  category: string;
+  purpose: string;
+  audience: string;
+  tone: string[];
+  avoid: string[];
+  notes: string;
+  catType: CategoryType;
+  hasTone: (t: string) => boolean;
+  hasAvoid: (a: string) => boolean;
+}
+
+// ── Helpers ──────────────────────────────────────────────────────────────────
+
+function detectCategory(raw: string): CategoryType {
+  const c = raw.toLowerCase();
+  if (/\b(dev|developer|tool|cli|sdk|api|library|lib|framework|plugin|compiler|linter|utility)\b/.test(c))
+    return 'developer';
+  if (/\b(design|creative|studio|agency|art|visual|branding|illustration|photography)\b/.test(c))
+    return 'creative';
+  if (/\b(saas|platform|service|app|software|product|startup|dashboard)\b/.test(c))
+    return 'product';
+  if (/\b(consult|advisory|freelance|firm|practice|coach|training)\b/.test(c))
+    return 'services';
+  if (/\b(personal|portfolio|blog|brand|me|writer|designer|maker|creator)\b/.test(c))
+    return 'personal';
+  return 'general';
+}
+
+function buildContext(inputs: BrandInputs): GenContext {
+  const catType = detectCategory(inputs.category);
+  const hasTone = (t: string) =>
+    inputs.tone.some(x => x.toLowerCase().includes(t.toLowerCase()));
+  const hasAvoid = (a: string) =>
+    inputs.avoid.some(x => x.toLowerCase().includes(a.toLowerCase()));
+
+  return {
+    name: inputs.name.trim() || 'Untitled',
+    category: inputs.category.trim() || 'project',
+    purpose: inputs.purpose.trim() || 'solve a specific problem',
+    audience: inputs.audience.trim() || 'its intended users',
+    tone: inputs.tone,
+    avoid: inputs.avoid,
+    notes: inputs.notes.trim(),
+    catType,
+    hasTone,
+    hasAvoid,
+  };
+}
+
+function pick<T>(arr: T[]): T {
+  return arr[Math.floor(Math.random() * arr.length)];
+}
+
+function pickN<T>(arr: T[], n: number): T[] {
+  const copy = [...arr];
+  const result: T[] = [];
+  while (result.length < n && copy.length > 0) {
+    const i = Math.floor(Math.random() * copy.length);
+    result.push(copy.splice(i, 1)[0]);
+  }
+  return result;
+}
+
+function cap(s: string): string {
+  return s.charAt(0).toUpperCase() + s.slice(1);
+}
+
+// ── Overview ─────────────────────────────────────────────────────────────────
+
+function generateOverview(ctx: GenContext): string {
+  const { name, category, purpose, audience, catType } = ctx;
+
+  const base = pick([
+    `${name} is a ${category} for ${audience}. ${cap(purpose)}.`,
+    `A ${category} built for ${audience}. ${name} is built to ${purpose}.`,
+    `${name} helps ${audience} ${purpose}.`,
+    `${name} is a ${category} built to ${purpose}. Made for ${audience}.`,
+  ]);
+
+  let suffix = '';
+  if (ctx.hasTone('minimal')) suffix = ' Nothing more.';
+  else if (ctx.hasTone('technical')) suffix = ' Stays out of the way while doing its job.';
+  else if (ctx.hasTone('calm')) suffix = ' Designed to reduce friction, not add to it.';
+  else if (ctx.hasTone('bold')) suffix = ' No compromises.';
+  else if (ctx.hasTone('warm')) suffix = ' Made with care.';
+  else if (catType === 'developer') suffix = ' Built to stay composable and predictable.';
+  else if (catType === 'creative') suffix = ' Work that speaks before the introduction.';
+
+  return base + suffix;
+}
+
+// ── Positioning ───────────────────────────────────────────────────────────────
+
+function generatePositioning(ctx: GenContext): string {
+  const { name, catType } = ctx;
+
+  const templates: Record<CategoryType, string[]> = {
+    developer: [
+      `${name} sits in the space between "build it yourself" and "platform lock-in." It's a workflow layer, not an abstraction. You keep control; ${name} handles the repetitive parts.`,
+      `Most tools in this space try to do too much. ${name} doesn't. It handles exactly what it promises — then stays out of your way.`,
+      `${name} is not a platform. It's a tool. You own the infrastructure; ${name} owns the workflow. That distinction matters.`,
+    ],
+    creative: [
+      `${name} is defined by its process as much as its output. The work should be recognizable without a logo — that's the goal.`,
+      `${name} occupies a deliberate position: between personal and professional, between output and craft. Not trying to be everything. Trying to be specific.`,
+      `There's no shortage of creative work. ${name} earns attention through quality and consistency, not volume or novelty.`,
+    ],
+    product: [
+      `${name} doesn't try to replace your existing workflow. It fits into it. The goal is to reduce friction in a specific, measurable place.`,
+      `The space ${name} plays in has no shortage of tools. What it offers is focus — one problem, done well, with clear boundaries.`,
+      `${name} is built for people who've tried the alternatives and found them too complicated, too expensive, or too broad.`,
+    ],
+    services: [
+      `${name} is not a generalist practice. It works on a specific type of problem with a specific type of client. That specificity is the positioning.`,
+      `Clients come to ${name} when they need someone who has solved this problem before. The brand should communicate experience, not aspiration.`,
+      `${name} does one thing well and charges accordingly. Clarity of scope is the offer.`,
+    ],
+    personal: [
+      `${name} is a deliberate presence — not a portfolio, not a platform, not a personal brand in the marketing sense. A clear point of view, consistently expressed.`,
+      `${name} doesn't try to appeal to everyone. It's built around a specific set of interests and a specific way of working.`,
+      `There are a lot of personal sites. ${name} is distinct by being specific — not broad, not aspirational, not trying to cover every base.`,
+    ],
+    general: [
+      `${name} occupies a clear position: built for a specific audience with a specific need. Not trying to be everything.`,
+      `${name} is not for everyone. That's intentional. Clarity of purpose is more valuable than breadth of appeal.`,
+    ],
+  };
+
+  return pick(templates[catType] ?? templates.general);
+}
+
+// ── Tone Guidance ─────────────────────────────────────────────────────────────
+
+function generateToneGuidance(ctx: GenContext): ToneGuidance {
+  const { name, purpose, tone, avoid, catType, hasTone } = ctx;
+
+  const voiceMap: Record<string, string> = {
+    minimal: 'Short sentences. No adjectives unless load-bearing. Direct.',
+    technical: 'Precise nouns, specific verbs. Write for experts. Avoid explaining what the reader already knows.',
+    calm: 'Measured pace. Confident statements. No urgency cues. Let the work speak.',
+    bold: 'Strong verbs. Active voice. Make a claim and stand behind it.',
+    warm: 'Approachable but not casual. Human without being informal.',
+    dry: 'Deadpan. Understate. Trust the reader to get it.',
+    focused: 'Stay on topic. One idea per sentence. Cut the rest.',
+  };
+
+  const catVoiceDefaults: Record<CategoryType, string> = {
+    developer: 'Write for engineers. Assume technical literacy. Specificity earns trust.',
+    creative: 'Lead with the work. Copy should serve the visual, not explain it.',
+    product: 'Clear over clever. Features earn their mention by solving something real.',
+    services: "Experience over enthusiasm. What you've done, not what you'll do.",
+    personal: 'First person where appropriate. Honest and considered.',
+    general: 'Clear, direct, grounded. Earn attention with specificity.',
+  };
+
+  const toneVoiceParts = tone.map(t => voiceMap[t.toLowerCase()]).filter(Boolean);
+  const voiceNotes =
+    toneVoiceParts.length > 0
+      ? toneVoiceParts.join(' ')
+      : catVoiceDefaults[catType] ?? catVoiceDefaults.general;
+
+  const avoidDefaults: Record<CategoryType, string[]> = {
+    developer: ['"seamlessly"', '"game-changing"', '"powerful" as an adjective', 'passive voice'],
+    creative: ['"unique"', '"innovative"', '"passion-driven"', 'agency-speak'],
+    product: ['"revolutionary"', '"disrupts"', '"leverage"', '"synergy"'],
+    services: ['"partner"', '"solutions"', '"holistic"', '"best-in-class"'],
+    personal: ['"journey"', '"passionate about"', '"thought leader"', '"excited to announce"'],
+    general: ['"world-class"', '"cutting-edge"', '"innovative"', '"paradigm"'],
+  };
+
+  const avoidList = [
+    ...avoid,
+    ...(avoidDefaults[catType] ?? avoidDefaults.general).filter(
+      a => !avoid.some(ua => ua.toLowerCase().includes(a.toLowerCase().replace(/"/g, '')))
+    ),
+  ];
+
+  const phrasesByTone: string[] = [];
+  if (hasTone('minimal')) phrasesByTone.push(`"${name}. ${cap(purpose)}."`);
+  if (hasTone('technical'))
+    phrasesByTone.push('"Typed, composable, deterministic."');
+  if (hasTone('calm'))
+    phrasesByTone.push('"Reliable tools, clearly documented, quietly maintained."');
+  if (hasTone('bold')) phrasesByTone.push('"Pick it up. It works. Put it down."');
+  if (hasTone('warm'))
+    phrasesByTone.push('"Made for people who care about their tools."');
+
+  const catPhrases: Record<CategoryType, string[]> = {
+    developer: [
+      '"One command. Done."',
+      '"Less ceremony. More output."',
+      '"Configure once. Forget about it."',
+    ],
+    creative: [
+      '"The work is the argument."',
+      '"No portfolio lorem ipsum."',
+      '"Good work, clearly presented."',
+    ],
+    product: [
+      '"It fits where you already work."',
+      '"No setup tax."',
+      '"Does one thing. Does it well."',
+    ],
+    services: [
+      '"You\'ve seen this problem before. So have we."',
+      '"Specific outcomes, clear process."',
+      '"We don\'t do vague."',
+    ],
+    personal: [
+      '"This is what I work on."',
+      '"Specific interests, honest opinions."',
+      '"No personal brand. Just work."',
+    ],
+    general: [
+      '"Built for a reason."',
+      '"Useful before impressive."',
+      '"Does what it says."',
+    ],
+  };
+
+  const allPhrases = [
+    ...phrasesByTone,
+    ...(catPhrases[catType] ?? catPhrases.general),
+  ];
+
+  return {
+    attributes: tone.length > 0 ? tone : ['direct', 'clear'],
+    voiceNotes,
+    avoidList,
+    examplePhrases: pickN(allPhrases, Math.min(4, allPhrases.length)),
+  };
+}
+
+// ── Titles ────────────────────────────────────────────────────────────────────
+
+function generateTitles(ctx: GenContext): string[] {
+  const { name, category, purpose, audience, hasTone } = ctx;
+
+  const purposeWords = purpose.split(/\s+/);
+  const coreVerb = purposeWords[0] ?? 'build';
+  const audienceShort = audience.split(/\s+/).slice(0, 3).join(' ');
+
+  // Distinct structural patterns — each must look meaningfully different
+  const variants: string[] = [
+    `${name} — ${category} for ${audienceShort}`,
+    `${name} / ${audience}`,
+    `${name}: ${cap(coreVerb)} without ceremony`,
+    `${name} for ${audienceShort}`,
+    `${name} — built to ${coreVerb}`,
+  ];
+
+  // Tone-specific variants
+  if (hasTone('minimal')) variants.push(`${name}.`);
+  if (hasTone('technical')) {
+    variants.push(`${name} — ${category} utility`);
+    variants.push(`${name} — ${coreVerb}, ship, repeat`);
+  }
+  if (hasTone('bold')) {
+    variants.push(`${name}. Built for ${audienceShort}.`);
+    variants.push(`${name}. No ceremony.`);
+  }
+  if (hasTone('calm')) variants.push(`${name} — a ${category} for ${audienceShort}`);
+
+  // Deduplicate and ensure first is always bare name
+  const pool = variants.filter(v => v !== name);
+  const extras = pickN(pool, 2);
+  return [name, ...extras];
+}
+
+// ── Subtitles ─────────────────────────────────────────────────────────────────
+
+function generateSubtitles(ctx: GenContext): string[] {
+  const { name, category, purpose, audience } = ctx;
+  const purposeShort = purpose.split(/\s+/).slice(0, 6).join(' ');
+
+  const all: string[] = [
+    `${cap(category)} for ${audience}.`,
+    `${cap(purpose)}.`,
+    `A ${category} built to ${purpose}.`,
+    `Built for ${audience} who need to ${purposeShort}.`,
+    `${name}: the ${category} for ${audience}.`,
+    `${cap(category)}. For ${audience}.`,
+    `${cap(purpose)}. No overhead.`,
+    `The ${category} for ${audience} who know what they need.`,
+  ];
+
+  return pickN(all, 3);
+}
+
+// ── Taglines ──────────────────────────────────────────────────────────────────
+
+function generateTaglines(ctx: GenContext): string[] {
+  const { category, purpose, audience, catType, hasTone } = ctx;
+
+  const purposeWords = purpose.split(/\s+/);
+  const coreVerb = purposeWords[0] ?? 'build';
+  // Take only the object noun (skip prepositions like "without", "for", "with")
+  const stopWords = new Set(['without', 'with', 'for', 'and', 'or', 'the', 'a', 'an']);
+  const coreNounWords = purposeWords.slice(1).filter(w => !stopWords.has(w.toLowerCase()));
+  const coreNoun = coreNounWords[0] || 'your work';
+  const audienceShort = audience.split(/\s+/).slice(0, 2).join(' ');
+
+  const catTaglines: Record<CategoryType, string[]> = {
+    developer: [
+      `${cap(coreVerb)}, ship, move on.`,
+      `Less boilerplate. More control.`,
+      `${cap(category)} that stays out of your way.`,
+      `One command. Done.`,
+      `Configure once. Forget about it.`,
+      `Less ceremony. More output.`,
+      `Built for ${audienceShort} who ship.`,
+      `${cap(coreNoun)}, no overhead.`,
+    ],
+    creative: [
+      `The work is the argument.`,
+      `Good work, clearly presented.`,
+      `No introduction needed.`,
+      `${cap(coreNoun)}, done properly.`,
+      `Craft over noise.`,
+      `Say less. Show more.`,
+      `Quality, consistently.`,
+    ],
+    product: [
+      `Does one thing. Does it well.`,
+      `Fits where you already work.`,
+      `No setup tax.`,
+      `Built for ${audienceShort}.`,
+      `${cap(coreVerb)} without the friction.`,
+      `One less problem.`,
+      `Useful before impressive.`,
+    ],
+    services: [
+      `You've seen this problem before. So have we.`,
+      `Specific outcomes. Clear process.`,
+      `Experience, not enthusiasm.`,
+      `We don't do vague.`,
+      `The result is the product.`,
+      `Built for the problem you actually have.`,
+    ],
+    personal: [
+      `This is what I work on.`,
+      `Specific interests. Honest opinions.`,
+      `Work, not performance.`,
+      `Making things that matter.`,
+      `No brand. Just work.`,
+    ],
+    general: [
+      `Built for a reason.`,
+      `Useful before impressive.`,
+      `Does what it says.`,
+      `${cap(coreVerb)} without the noise.`,
+      `For ${audienceShort} who know what they want.`,
+    ],
+  };
+
+  const toneTaglines: string[] = [];
+  if (hasTone('minimal')) toneTaglines.push(`${cap(coreVerb)}. Ship.`, `Simple by design.`);
+  if (hasTone('calm'))
+    toneTaglines.push(`Reliable tools, quietly maintained.`, `Steady. Dependable. Yours.`);
+  if (hasTone('bold'))
+    toneTaglines.push(`Pick it up. It works.`, `No compromises.`, `Built to be used.`);
+  if (hasTone('technical'))
+    toneTaglines.push(`Typed. Composable. Predictable.`, `Deterministic by design.`);
+
+  const pool = [...(catTaglines[catType] ?? catTaglines.general), ...toneTaglines];
+  return pickN(pool, 3);
+}
+
+// ── Visual Directions ─────────────────────────────────────────────────────────
+
+function generateVisualDirections(ctx: GenContext): VisualDirection[] {
+  const { catType, hasTone } = ctx;
+
+  const allDirections: VisualDirection[] = [];
+
+  if (catType === 'developer' || hasTone('technical') || hasTone('minimal')) {
+    allDirections.push(
+      {
+        id: 'terminal-minimal',
+        name: 'Terminal Minimal',
+        description:
+          'Dark background, monospace type throughout, no ornament. Functional and uncompromising. Every element earns its place.',
+        palette: 'Near-black ground (#0d0d0d), off-white text (#e0e0e0), single muted accent (amber or green).',
+        typography: 'Monospace primary — JetBrains Mono or Iosevka. Consistent weight. No italic.',
+        references: 'htop, k9s, the Stripe CLI, Linear issue view.',
+      },
+      {
+        id: 'technical-document',
+        name: 'Technical Document',
+        description:
+          'Off-white ground, dense information layout, RFC/spec aesthetic. Designed for reading, not scanning. Typography does all the work.',
+        palette: 'Warm white (#f5f3ef), dark text (#1a1a1a), minimal color — one functional accent only.',
+        typography: 'Sans-serif (Inter or similar) for body, mono for code. Tight line height. Strong hierarchy through size and weight alone.',
+        references: 'Stripe docs, Oxide Computer RFCs, GNU manpages reformatted.',
+      },
+      {
+        id: 'precision-interface',
+        name: 'Precision Interface',
+        description:
+          'Neutral mid-range palette, strong grid, engineering-tool aesthetic. Balanced between document and application. Calm but capable.',
+        palette: 'Mid-grey ground (#f0f0f0 or #1c1c1c), charcoal type, restrained use of blue or slate as action color.',
+        typography: 'Sans-serif for UI, mono for data. Clear size differentiation. No decorative weight use.',
+        references: 'Figma sidebar, Retool, Zed editor, TablePlus.',
+      }
+    );
+  }
+
+  if (catType === 'creative' || catType === 'personal') {
+    allDirections.push(
+      {
+        id: 'editorial',
+        name: 'Editorial',
+        description:
+          'Strong typographic hierarchy, restrained palette, print-design influences. Work foreground, everything else background.',
+        palette: 'Off-white or cream (#f7f4ef), near-black type, accent used once — a single warm or cool tone.',
+        typography: 'A good serif for display, neutral sans for body. Generous leading. No decorative fonts.',
+        references: 'Are.na, Typewolf, Emigre back catalog, Letterform Archive.',
+      },
+      {
+        id: 'quiet-studio',
+        name: 'Quiet Studio',
+        description:
+          'Neutral and considered. Nothing decorative. Space used to direct attention, not fill it.',
+        palette: 'Warm white (#fafaf8) or deep neutral (#141414), type-only color use. No gradients.',
+        typography: 'One typeface family, two weights. Let leading and spacing create rhythm.',
+        references: 'Pentagram case studies, Swiss International Style, Muji product design.',
+      },
+      {
+        id: 'contemporary-craft',
+        name: 'Contemporary Craft',
+        description:
+          'Tactile references — paper, grain, texture — applied with restraint. Warmth without nostalgia.',
+        palette: 'Off-white base with a warm paper tone, subtle texture overlays, earthy accent.',
+        typography: 'Mix: display serif + utility sans. Comfortable reading size. Generous margins.',
+        references: 'Oak Studio, Analog, Offscreen Magazine, Present & Correct.',
+      }
+    );
+  }
+
+  if (catType === 'product' || catType === 'services') {
+    allDirections.push(
+      {
+        id: 'focused-product',
+        name: 'Focused Product',
+        description:
+          'Clean, professional, information-forward. Looks like it was built to be used, not to be admired. Trust through clarity.',
+        palette: 'White or light grey ground, dark neutral type, one brand color used only for primary actions.',
+        typography: 'Neutral sans-serif, systematic sizing, no personality — the product is the personality.',
+        references: 'Linear, Cron (v1), Vercel dashboard, Raycast.',
+      },
+      {
+        id: 'minimal-commerce',
+        name: 'Minimal Commerce',
+        description:
+          'Premium restraint. No decorative elements. White space signals quality. Type-driven.',
+        palette: 'White ground, black type, one warm accent for selective emphasis.',
+        typography: 'A refined sans-serif. Large display size for key claims. Small, tracked caps for labels.',
+        references: 'Stripe marketing, Basecamp, Arc browser landing page.',
+      },
+      {
+        id: 'structured-trust',
+        name: 'Structured Trust',
+        description:
+          'Grid-heavy, methodical, legible. Communicates that things are in order. More function, less flourish.',
+        palette: 'Light neutral ground, two text weights (body + emphasis), a contained accent color.',
+        typography: 'Professional sans-serif — GT Walsheim or Plus Jakarta or similar. Tight tracking for headings.',
+        references: 'Harvest app, FreshBooks, Notion, Loom landing page.',
+      }
+    );
+  }
+
+  if (allDirections.length === 0) {
+    allDirections.push(
+      {
+        id: 'type-forward',
+        name: 'Type Forward',
+        description:
+          'Typography as the only design element. No illustration, no photography, no pattern. Words do everything.',
+        palette: 'Black and white, one optional accent. No gradients.',
+        typography: 'One great typeface. Multiple weights. Extreme size contrast. Nothing else needed.',
+        references: 'Early Bloomberg Businessweek covers, Helvetica film poster, The Economist.',
+      },
+      {
+        id: 'system-neutral',
+        name: 'System Neutral',
+        description:
+          'Invisible design — system fonts, default spacing, no signature. The brand is in the content, not the container.',
+        palette: 'System defaults. One custom color — the brand color. Everything else inherited.',
+        typography: 'System UI stack. Optimized for the OS it runs on.',
+        references: 'HN, iA Writer, Pinboard, older Stripe.',
+      }
+    );
+  }
+
+  return pickN(allDirections, Math.min(3, allDirections.length));
+}
+
+// ── Logo Concepts ─────────────────────────────────────────────────────────────
+
+function generateLogoConcepts(ctx: GenContext): LogoConcept[] {
+  const { name, catType } = ctx;
+  const initial = name.charAt(0).toUpperCase();
+  const initials =
+    name
+      .split(/\s+/)
+      .slice(0, 2)
+      .map(w => w.charAt(0).toUpperCase())
+      .join('') || initial;
+
+  const concepts: LogoConcept[] = [];
+
+  if (catType === 'developer') {
+    concepts.push(
+      {
+        id: 'geometric-letterform',
+        title: 'Geometric Letterform',
+        concept: `The letter "${initial}" treated as a structural element — not styled, just precise. Think grid construction, not calligraphy.`,
+        mark: 'Monoweight geometric construction. Works at 16px and 1600px. No gradients, no effects.',
+        execution:
+          'Build on a strict grid. Consider negative space as intentional, not leftover. Test at 16×16 favicon size first.',
+      },
+      {
+        id: 'wordmark-mono',
+        title: 'Wordmark in Mono',
+        concept: `"${name}" set in a monospace typeface, tracked slightly loose. The choice of mono is the signal.`,
+        mark: 'Wordmark only. No icon. The name is the mark.',
+        execution:
+          'Try JetBrains Mono, Iosevka, or Commit Mono at medium weight. Adjust tracking. Optically align.',
+      },
+      {
+        id: 'abstract-structure',
+        title: 'Abstract Structure',
+        concept:
+          'A geometric mark suggesting assembly, layering, or composition — aligned with the product metaphor.',
+        mark: 'Two or three simple shapes in precise relation. No ornamentation.',
+        execution:
+          'Explore grid fragments, interlocking forms, or stacked bars. Test inversion on dark and light.',
+      }
+    );
+  } else if (catType === 'creative' || catType === 'personal') {
+    concepts.push(
+      {
+        id: 'custom-wordmark',
+        title: 'Custom Wordmark',
+        concept: `"${name}" as a custom letterform — not a font off the shelf, but drawn. The craft shows.`,
+        mark: 'Wordmark with subtle custom refinements: adjusted spacing, modified terminals, intentional details.',
+        execution:
+          'Start with a base typeface. Modify key letterforms. The goal is invisible craft, not obvious customization.',
+      },
+      {
+        id: 'monogram',
+        title: 'Monogram',
+        concept: `"${initials}" as a tight, legible monogram. Simple enough to stamp, refined enough to scale up.`,
+        mark: 'Two letterforms in structural relation. Not overlapping decoratively — compositionally.',
+        execution:
+          'Grid-align. Consider positive/negative figure-ground play. Must read clearly at 24px.',
+      }
+    );
+  } else {
+    concepts.push(
+      {
+        id: 'clean-wordmark',
+        title: 'Wordmark',
+        concept: `"${name}" set in a well-chosen typeface, thoughtfully spaced. No icon needed.`,
+        mark: 'Wordmark. The typeface selection and spacing carry the identity.',
+        execution:
+          'Choose a typeface with character but not personality. Adjust tracking. Optically center.',
+      },
+      {
+        id: 'initial-mark',
+        title: `"${initial}" Mark`,
+        concept: `A standalone "${initial}" mark that works as a favicon, app icon, and small-scale identifier.`,
+        mark: 'Single letter, geometric or structured. Consistent weight with wordmark.',
+        execution:
+          'Build on an 8-unit grid. Test at 16px, 32px, and 512px. Must work in one color.',
+      }
+    );
+  }
+
+  concepts.push({
+    id: 'symbol-plus-wordmark',
+    title: 'Symbol + Wordmark System',
+    concept:
+      'A mark system: standalone symbol for small contexts, symbol + name for full contexts. Flexible.',
+    mark: 'Two formats: symbol alone, symbol left-aligned with wordmark right.',
+    execution:
+      'Define the relationship (size ratio, spacing) precisely. Lock it. Never deviate. Test both formats in context.',
+  });
+
+  return pickN(concepts, 2);
+}
+
+// ── Usage Examples ────────────────────────────────────────────────────────────
+
+function generateUsageExamples(ctx: GenContext): UsageExample[] {
+  const { name, category, purpose, audience, catType } = ctx;
+  // Take a clean verb phrase for landing copy — stop before prepositions
+  const stopWords = new Set(['without', 'for', 'with', 'and', 'or', 'the', 'a', 'an', 'via', 'using']);
+  const purposeWords = purpose.split(/\s+/);
+  const heroWords: string[] = [];
+  for (const w of purposeWords) {
+    if (stopWords.has(w.toLowerCase()) && heroWords.length > 0) break;
+    heroWords.push(w);
+  }
+  const heroVerb = heroWords.join(' ') || purpose;
+
+  const examples: UsageExample[] = [
+    {
+      context: 'README header',
+      text: `# ${name}\n\n${cap(category)} for ${audience}. ${cap(purpose)}.`,
+    },
+    {
+      context: 'Landing page hero',
+      text: `${cap(heroVerb)} without ceremony.\n\n${name} is a ${category} built for ${audience} who need to ${purpose}.`,
+    },
+    {
+      context: 'Social / bio',
+      text: `Building ${name} — ${category} for ${audience}. ${cap(purpose)}, no overhead.`,
+    },
+    {
+      context: 'One-liner',
+      text: `${name}: ${category} built to ${purpose}.`,
+    },
+  ];
+
+  if (catType === 'developer') {
+    examples.push({
+      context: 'Package registry description',
+      text: `${name} is a ${category} for ${audience}. ${cap(purpose)}. No configuration required.`,
+    });
+    examples.push({
+      context: 'CLI help text intro',
+      text: `${name} — ${cap(purpose)}.`,
+    });
+  }
+
+  if (catType === 'creative' || catType === 'personal') {
+    examples.push({
+      context: 'Portfolio about line',
+      text: `${name} is the practice of ${audience.split(/\s+/).slice(0, 2).join(' ')} ${heroVerb}.`,
+    });
+  }
+
+  if (catType === 'product') {
+    examples.push({
+      context: 'App store description',
+      text: `${name} is a ${category} for ${audience}. It ${purpose} — without the complexity of larger platforms.`,
+    });
+  }
+
+  return examples.slice(0, 6);
+}
+
+// ── Constraints ───────────────────────────────────────────────────────────────
+
+function generateConstraints(ctx: GenContext): string[] {
+  const { tone, avoid, hasTone } = ctx;
+  const list: string[] = [];
+
+  if (tone.length > 0) {
+    list.push(`Tone: ${tone.join(', ')}.`);
+  }
+
+  if (avoid.length > 0) {
+    list.push(`Avoid: ${avoid.join(', ')}.`);
+  }
+
+  if (hasTone('minimal')) {
+    list.push('Headlines: 6 words maximum.');
+    list.push('Body copy: 2 sentences per paragraph maximum.');
+  }
+
+  if (hasTone('technical')) {
+    list.push('Assume reader has domain knowledge. Skip definitions.');
+    list.push('Prefer specific nouns over categorical ones (say the actual thing).');
+  }
+
+  if (hasTone('calm')) {
+    list.push('No urgency language ("act now", "limited time", "don\'t miss").');
+    list.push('No exclamation points.');
+  }
+
+  if (hasTone('bold')) {
+    list.push('Active voice always. No passive constructions.');
+    list.push('Every claim should be substantiable.');
+  }
+
+  list.push('No em dashes in casual contexts. Use a period or restructure.');
+  list.push('Spell out numbers under 10 in prose. Use numerals for data.');
+  list.push('One idea per sentence. Split if in doubt.');
+
+  return list;
+}
+
+// ── Typography ────────────────────────────────────────────────────────────────
+
+export const TYPE_SCALE: TypographyToken[] = [
+  { label: 'Display',    size: '56px', weight: '700', lineHeight: '1.1',  usage: 'Hero headlines, major landing sections' },
+  { label: 'Heading 1',  size: '40px', weight: '700', lineHeight: '1.2',  usage: 'Page titles, primary headers' },
+  { label: 'Heading 2',  size: '28px', weight: '600', lineHeight: '1.25', usage: 'Section headers, card titles' },
+  { label: 'Heading 3',  size: '20px', weight: '600', lineHeight: '1.3',  usage: 'Sub-section headers, feature titles' },
+  { label: 'Body Large', size: '18px', weight: '400', lineHeight: '1.6',  usage: 'Lead paragraphs, key descriptions' },
+  { label: 'Body',       size: '16px', weight: '400', lineHeight: '1.65', usage: 'Default body copy' },
+  { label: 'Caption',    size: '13px', weight: '400', lineHeight: '1.5',  usage: 'Meta info, timestamps, helper text' },
+  { label: 'Label',      size: '11px', weight: '600', lineHeight: '1.4',  usage: 'UI labels, tags, overlines' },
+];
+
+type FontPair = { primary: string; secondary: string; mono: string; pairNote: string };
+
+const FONT_PAIRS: Record<string, FontPair[]> = {
+  developer: [
+    { primary: 'Geist',         secondary: 'Geist',         mono: 'Geist Mono',       pairNote: 'Single-family system. Clean, neutral, interface-optimized.' },
+    { primary: 'Inter',         secondary: 'Inter',         mono: 'JetBrains Mono',   pairNote: 'Inter for all UI copy; JetBrains Mono for code.' },
+    { primary: 'IBM Plex Sans', secondary: 'IBM Plex Sans', mono: 'IBM Plex Mono',    pairNote: 'IBM Plex family — coherent, technical, widely legible.' },
+  ],
+  creative: [
+    { primary: 'Playfair Display',    secondary: 'Lato',       mono: 'Courier Prime', pairNote: 'High-contrast editorial serif for display; Lato for body.' },
+    { primary: 'Fraunces',            secondary: 'DM Sans',    mono: 'DM Mono',       pairNote: 'Optical-size serif for headlines; DM Sans for body. Expressive and modern.' },
+    { primary: 'Cormorant Garamond',  secondary: 'Nunito Sans', mono: 'Courier Prime', pairNote: 'Refined luxury serif for display; Nunito Sans for readable body.' },
+  ],
+  product: [
+    { primary: 'Plus Jakarta Sans', secondary: 'Plus Jakarta Sans', mono: 'DM Mono',        pairNote: 'Jakarta Sans at varying weights; DM Mono for data and code.' },
+    { primary: 'Inter',             secondary: 'Inter',             mono: 'Fira Code',       pairNote: 'Inter throughout — modern, neutral, excellent hinting.' },
+    { primary: 'Manrope',           secondary: 'Manrope',           mono: 'JetBrains Mono',  pairNote: 'Geometric Manrope for all UI; JetBrains Mono for code blocks.' },
+  ],
+  services: [
+    { primary: 'Libre Baskerville', secondary: 'Source Sans 3', mono: 'Source Code Pro', pairNote: 'Baskerville for authority and trust; Source Sans for approachable body.' },
+    { primary: 'Merriweather',      secondary: 'Open Sans',     mono: 'Roboto Mono',     pairNote: 'Merriweather for credibility; Open Sans keeps body warm.' },
+    { primary: 'Lora',              secondary: 'Nunito Sans',   mono: 'Courier Prime',   pairNote: 'Lora brings warmth to headlines; Nunito Sans lightens the reading.' },
+  ],
+  personal: [
+    { primary: 'Lora',              secondary: 'Nunito',       mono: 'DM Mono',       pairNote: 'Lora for expressive headlines; Nunito for friendly body copy.' },
+    { primary: 'DM Serif Display',  secondary: 'DM Sans',      mono: 'DM Mono',       pairNote: 'Unified DM family. Serif display for character; sans for clarity.' },
+    { primary: 'Playfair Display',  secondary: 'Source Sans 3', mono: 'Courier Prime', pairNote: 'Playfair adds personality; Source Sans 3 grounds body text.' },
+  ],
+  general: [
+    { primary: 'Inter',             secondary: 'Inter',        mono: 'JetBrains Mono', pairNote: 'Inter throughout with weight variation. Universal starting point.' },
+    { primary: 'Plus Jakarta Sans', secondary: 'Lora',         mono: 'Fira Code',      pairNote: 'Geometric sans for UI; Lora serif for long-form content.' },
+    { primary: 'Manrope',           secondary: 'Merriweather', mono: 'Source Code Pro', pairNote: 'Friendly geometric sans paired with a trusted editorial serif.' },
+  ],
+};
+
+function generateTypography(ctx: GenContext): Typography {
+  const pairs = FONT_PAIRS[ctx.catType] ?? FONT_PAIRS.general;
+  return { ...pick(pairs), scale: TYPE_SCALE };
+}
+
+// ── Color Palette ─────────────────────────────────────────────────────────────
+
+function makeSwatches(entries: [string, string, string][]): ColorSwatch[] {
+  return entries.map(([name, hex, role], i) => ({ id: `s${i}`, name, hex, role }));
+}
+
+function generateColorPalette(ctx: GenContext): ColorPalette {
+  type SwatchEntry = [string, string, string]; // [name, hex, role]
+  type PaletteSet = SwatchEntry[][];
+
+  const palettes: Record<string, PaletteSet> = {
+    developer: [
+      [
+        ['Background', '#0d1117', 'background'],
+        ['Surface', '#161b22', 'neutral'],
+        ['Border', '#30363d', 'neutral'],
+        ['Primary', '#58a6ff', 'primary'],
+        ['Accent', '#3fb950', 'accent'],
+        ['Text', '#f0f6fc', 'text'],
+      ],
+      [
+        ['Background', '#0a0010', 'background'],
+        ['Surface', '#160025', 'neutral'],
+        ['Neutral', '#2d1f3d', 'neutral'],
+        ['Primary', '#7c3aed', 'primary'],
+        ['Accent', '#a78bfa', 'accent'],
+        ['Text', '#e2d9f3', 'text'],
+      ],
+      [
+        ['Background', '#0a0a0a', 'background'],
+        ['Surface', '#141414', 'neutral'],
+        ['Neutral', '#292929', 'neutral'],
+        ['Primary', '#e5e5e5', 'primary'],
+        ['Accent', '#c9a96e', 'accent'],
+        ['Text', '#f5f5f5', 'text'],
+      ],
+    ],
+    creative: [
+      [
+        ['Background', '#faf7f2', 'background'],
+        ['Surface', '#f5efe4', 'neutral'],
+        ['Neutral', '#e8d9c4', 'neutral'],
+        ['Primary', '#c07850', 'primary'],
+        ['Accent', '#4a7c5f', 'accent'],
+        ['Text', '#1a1410', 'text'],
+      ],
+      [
+        ['Background', '#0f0f0f', 'background'],
+        ['Surface', '#1a1a1a', 'neutral'],
+        ['Neutral', '#2e2e2e', 'neutral'],
+        ['Primary', '#ff6b35', 'primary'],
+        ['Accent', '#ffd700', 'accent'],
+        ['Text', '#f8f8f8', 'text'],
+      ],
+      [
+        ['Background', '#f8f4ef', 'background'],
+        ['Surface', '#efe9e0', 'neutral'],
+        ['Neutral', '#d4c4b0', 'neutral'],
+        ['Primary', '#8b5e3c', 'primary'],
+        ['Accent', '#6b8f71', 'accent'],
+        ['Text', '#2c2018', 'text'],
+      ],
+    ],
+    product: [
+      [
+        ['Background', '#fafbfc', 'background'],
+        ['Surface', '#f0f4f8', 'neutral'],
+        ['Neutral', '#d1dce8', 'neutral'],
+        ['Primary', '#2563eb', 'primary'],
+        ['Accent', '#7c3aed', 'accent'],
+        ['Text', '#1e293b', 'text'],
+      ],
+      [
+        ['Background', '#0f172a', 'background'],
+        ['Surface', '#1e293b', 'neutral'],
+        ['Neutral', '#334155', 'neutral'],
+        ['Primary', '#6366f1', 'primary'],
+        ['Accent', '#22d3ee', 'accent'],
+        ['Text', '#f1f5f9', 'text'],
+      ],
+      [
+        ['Background', '#f0fafa', 'background'],
+        ['Surface', '#e0f5f5', 'neutral'],
+        ['Neutral', '#b2dede', 'neutral'],
+        ['Primary', '#0d9488', 'primary'],
+        ['Accent', '#f59e0b', 'accent'],
+        ['Text', '#134e4a', 'text'],
+      ],
+    ],
+    services: [
+      [
+        ['Background', '#f8fafd', 'background'],
+        ['Surface', '#edf2fa', 'neutral'],
+        ['Neutral', '#ccd9ee', 'neutral'],
+        ['Primary', '#1d4ed8', 'primary'],
+        ['Accent', '#0f9e6e', 'accent'],
+        ['Text', '#1a2038', 'text'],
+      ],
+      [
+        ['Background', '#0c1421', 'background'],
+        ['Surface', '#152035', 'neutral'],
+        ['Neutral', '#243450', 'neutral'],
+        ['Primary', '#3b82f6', 'primary'],
+        ['Accent', '#34d399', 'accent'],
+        ['Text', '#f8fafc', 'text'],
+      ],
+      [
+        ['Background', '#faf8f5', 'background'],
+        ['Surface', '#f0ebe0', 'neutral'],
+        ['Neutral', '#d9cdb8', 'neutral'],
+        ['Primary', '#78523a', 'primary'],
+        ['Accent', '#2d6a4f', 'accent'],
+        ['Text', '#1c1410', 'text'],
+      ],
+    ],
+    personal: [
+      [
+        ['Background', '#fffef9', 'background'],
+        ['Surface', '#fdf8ee', 'neutral'],
+        ['Neutral', '#f0e6cc', 'neutral'],
+        ['Primary', '#c9a96e', 'primary'],
+        ['Accent', '#7c9e87', 'accent'],
+        ['Text', '#2d2d2d', 'text'],
+      ],
+      [
+        ['Background', '#fafafa', 'background'],
+        ['Surface', '#f5f5f5', 'neutral'],
+        ['Neutral', '#e5e5e5', 'neutral'],
+        ['Primary', '#171717', 'primary'],
+        ['Accent', '#737373', 'accent'],
+        ['Text', '#404040', 'text'],
+      ],
+      [
+        ['Background', '#f9f8ff', 'background'],
+        ['Surface', '#f0eeff', 'neutral'],
+        ['Neutral', '#ddd8f7', 'neutral'],
+        ['Primary', '#4f46e5', 'primary'],
+        ['Accent', '#ec4899', 'accent'],
+        ['Text', '#1e1b4b', 'text'],
+      ],
+    ],
+    general: [
+      [
+        ['Background', '#111111', 'background'],
+        ['Surface', '#1a1a1a', 'neutral'],
+        ['Neutral', '#2e2e2e', 'neutral'],
+        ['Primary', '#c9a96e', 'primary'],
+        ['Accent', '#6b8f71', 'accent'],
+        ['Text', '#dedede', 'text'],
+      ],
+      [
+        ['Background', '#ffffff', 'background'],
+        ['Surface', '#f5f5f5', 'neutral'],
+        ['Neutral', '#e0e0e0', 'neutral'],
+        ['Primary', '#1a1a1a', 'primary'],
+        ['Accent', '#3b82f6', 'accent'],
+        ['Text', '#333333', 'text'],
+      ],
+      [
+        ['Background', '#0f0f1a', 'background'],
+        ['Surface', '#1a1a2e', 'neutral'],
+        ['Neutral', '#252545', 'neutral'],
+        ['Primary', '#4f8ef7', 'primary'],
+        ['Accent', '#a78bfa', 'accent'],
+        ['Text', '#e2e8f0', 'text'],
+      ],
+    ],
+  };
+
+  const pool = palettes[ctx.catType] ?? palettes.general;
+  return { swatches: makeSwatches(pick(pool) as SwatchEntry[]) };
+}
+
+// ── Entry point ───────────────────────────────────────────────────────────────
+
+export function generate(inputs: BrandInputs): BrandOutputs {
+  const ctx = buildContext(inputs);
+
+  return {
+    overview: generateOverview(ctx),
+    positioning: generatePositioning(ctx),
+    tone: generateToneGuidance(ctx),
+    titles: generateTitles(ctx),
+    subtitles: generateSubtitles(ctx),
+    taglines: generateTaglines(ctx),
+    visualDirections: generateVisualDirections(ctx),
+    palette: generateColorPalette(ctx),
+    typography: generateTypography(ctx),
+    logoConcepts: generateLogoConcepts(ctx),
+    usageExamples: generateUsageExamples(ctx),
+    constraints: generateConstraints(ctx),
+  };
+}
diff --git a/src/hooks/usePackages.ts b/src/hooks/usePackages.ts
new file mode 100644
index 0000000..b137160
--- /dev/null
+++ b/src/hooks/usePackages.ts
@@ -0,0 +1,112 @@
+import { useState, useCallback } from 'react';
+
+export interface PackageSlot {
+  id: string;
+  name: string;
+  createdAt: string;
+  storageKey: string;
+}
+
+interface PackagesStore {
+  activeId: string;
+  slots: PackageSlot[];
+}
+
+const STORE_KEY = 'bw:packages';
+const LEGACY_KEY = 'bw:workspace';
+
+function makeId(): string {
+  return `pkg-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
+}
+
+function makeSlot(name: string, id = makeId()): PackageSlot {
+  return { id, name, createdAt: new Date().toISOString(), storageKey: `bw:ws:${id}` };
+}
+
+function loadStore(): PackagesStore {
+  try {
+    const raw = localStorage.getItem(STORE_KEY);
+    if (raw) return JSON.parse(raw);
+  } catch { /* ignore */ }
+
+  // First run — migrate any legacy single-workspace data into the first slot
+  const firstSlot = makeSlot('Package 1');
+  try {
+    const legacy = localStorage.getItem(LEGACY_KEY);
+    if (legacy) localStorage.setItem(firstSlot.storageKey, legacy);
+  } catch { /* ignore */ }
+
+  const store: PackagesStore = { activeId: firstSlot.id, slots: [firstSlot] };
+  saveStore(store);
+  return store;
+}
+
+function saveStore(store: PackagesStore): void {
+  try { localStorage.setItem(STORE_KEY, JSON.stringify(store)); } catch { /* ignore */ }
+}
+
+export function usePackages() {
+  const [store, setStore] = useState<PackagesStore>(loadStore);
+
+  const activeSlot = store.slots.find(s => s.id === store.activeId) ?? store.slots[0];
+
+  // useWorkspace saves directly to each slot's storageKey on every change,
+  // so switching just needs to update activeId — no snapshot/copy required.
+  const switchTo = useCallback((id: string) => {
+    setStore(prev => {
+      if (prev.activeId === id) return prev;
+      const next = { ...prev, activeId: id };
+      saveStore(next);
+      return next;
+    });
+  }, []);
+
+  const createNew = useCallback(() => {
+    setStore(prev => {
+      const slot = makeSlot(`Package ${prev.slots.length + 1}`);
+      // New slot has no data in localStorage — useWorkspace will start fresh
+      const next = { activeId: slot.id, slots: [...prev.slots, slot] };
+      saveStore(next);
+      return next;
+    });
+  }, []);
+
+  const remove = useCallback((id: string) => {
+    setStore(prev => {
+      if (prev.slots.length <= 1) return prev;
+      const target = prev.slots.find(s => s.id === id);
+      try { if (target) localStorage.removeItem(target.storageKey); } catch { /* ignore */ }
+      const slots = prev.slots.filter(s => s.id !== id);
+      const activeId = prev.activeId === id ? slots[0].id : prev.activeId;
+      const next = { activeId, slots };
+      saveStore(next);
+      return next;
+    });
+  }, []);
+
+  const rename = useCallback((id: string, name: string) => {
+    setStore(prev => {
+      const slots = prev.slots.map(s => s.id === id ? { ...s, name } : s);
+      const next = { ...prev, slots };
+      saveStore(next);
+      return next;
+    });
+  }, []);
+
+  const duplicate = useCallback((id: string) => {
+    setStore(prev => {
+      const source = prev.slots.find(s => s.id === id);
+      if (!source) return prev;
+      const newSlot = makeSlot(`${source.name} copy`);
+      try {
+        const data = localStorage.getItem(source.storageKey);
+        if (data) localStorage.setItem(newSlot.storageKey, data);
+      } catch { /* ignore */ }
+      const next = { activeId: newSlot.id, slots: [...prev.slots, newSlot] };
+      saveStore(next);
+      return next;
+    });
+  }, []);
+
+  return { slots: store.slots, activeId: store.activeId, activeSlot, switchTo, createNew, remove, rename, duplicate };
+}
diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts
new file mode 100644
index 0000000..2452945
--- /dev/null
+++ b/src/hooks/useSettings.ts
@@ -0,0 +1,41 @@
+import { useState } from 'react';
+
+export interface OllamaSettings {
+  enabled: boolean;
+  baseUrl: string;
+  model: string;
+}
+
+const DEFAULTS: OllamaSettings = {
+  enabled: false,
+  baseUrl: 'http://localhost:11434',
+  model: 'llama3.2',
+};
+
+const KEY = 'bw:settings';
+
+function load(): OllamaSettings {
+  try {
+    const raw = localStorage.getItem(KEY);
+    return raw ? { ...DEFAULTS, ...JSON.parse(raw) } : { ...DEFAULTS };
+  } catch {
+    return { ...DEFAULTS };
+  }
+}
+
+// Non-reactive read — used inside callbacks without needing the hook
+export function readSettings(): OllamaSettings {
+  return load();
+}
+
+export function useSettings() {
+  const [settings, setSettingsRaw] = useState<OllamaSettings>(load);
+
+  const setSettings = (next: Partial<OllamaSettings>) => {
+    const merged = { ...settings, ...next };
+    try { localStorage.setItem(KEY, JSON.stringify(merged)); } catch { /* ignore */ }
+    setSettingsRaw(merged);
+  };
+
+  return { settings, setSettings };
+}
diff --git a/src/hooks/useWorkspace.ts b/src/hooks/useWorkspace.ts
new file mode 100644
index 0000000..c583269
--- /dev/null
+++ b/src/hooks/useWorkspace.ts
@@ -0,0 +1,225 @@
+import { useState, useCallback, useRef } from 'react';
+import type { BrandInputs, BrandOutputs, LockedSections, WorkspaceState } from '../types';
+import { generate } from '../engine/generator';
+import { generateWithAI } from '../engine/aiGenerator';
+import { readSettings } from './useSettings';
+import { sanitizeOutputs } from '../lib/sanitize';
+
+const DEFAULT_INPUTS: BrandInputs = {
+  name: '',
+  category: '',
+  purpose: '',
+  audience: '',
+  tone: [],
+  avoid: [],
+  notes: '',
+};
+
+const DEFAULT_LOCKED: LockedSections = {
+  overview: false,
+  positioning: false,
+  tone: false,
+  messaging: false,
+  visual: false,
+  palette: false,
+  typography: false,
+  logo: false,
+  usage: false,
+  constraints: false,
+};
+
+function loadState(storageKey: string): Partial<WorkspaceState> {
+  try {
+    const raw = localStorage.getItem(storageKey);
+    if (raw) {
+      const state = JSON.parse(raw) as Partial<WorkspaceState>;
+      // Always run through sanitizeOutputs — coerces any corrupted/legacy field
+      // (e.g. AI-returned objects in string fields) so React never crashes on render.
+      if (state.outputs) {
+        state.outputs = sanitizeOutputs(state.outputs) ?? null;
+        if (!state.outputs) state.edits = {};
+      }
+      return state;
+    }
+  } catch { /* ignore */ }
+  return {};
+}
+
+function saveState(storageKey: string, state: WorkspaceState): void {
+  try { localStorage.setItem(storageKey, JSON.stringify(state)); } catch { /* ignore */ }
+}
+
+function applyLocks(
+  merged: BrandOutputs,
+  locked: LockedSections,
+  outputs: BrandOutputs | null,
+  edits: Partial<BrandOutputs>,
+): BrandOutputs {
+  if (!outputs) return merged;
+  if (locked.overview)    merged.overview    = edits.overview    ?? outputs.overview;
+  if (locked.positioning) merged.positioning = edits.positioning ?? outputs.positioning;
+  if (locked.tone)        merged.tone        = edits.tone        ?? outputs.tone;
+  if (locked.messaging) {
+    merged.titles    = edits.titles    ?? outputs.titles;
+    merged.subtitles = edits.subtitles ?? outputs.subtitles;
+    merged.taglines  = edits.taglines  ?? outputs.taglines;
+  }
+  if (locked.visual)      merged.visualDirections = edits.visualDirections ?? outputs.visualDirections;
+  if (locked.palette)     merged.palette          = edits.palette          ?? outputs.palette;
+  if (locked.typography)  merged.typography       = edits.typography       ?? outputs.typography;
+  if (locked.logo)        merged.logoConcepts     = edits.logoConcepts     ?? outputs.logoConcepts;
+  if (locked.usage)       merged.usageExamples    = edits.usageExamples    ?? outputs.usageExamples;
+  if (locked.constraints) merged.constraints      = edits.constraints      ?? outputs.constraints;
+  return merged;
+}
+
+export function useWorkspace(storageKey = 'bw:workspace') {
+  const saved = loadState(storageKey);
+
+  const [inputs, setInputsRaw]   = useState<BrandInputs>(saved.inputs ?? DEFAULT_INPUTS);
+  const [outputs, setOutputs]    = useState<BrandOutputs | null>(saved.outputs ?? null);
+  const [edits, setEditsRaw]     = useState<Partial<BrandOutputs>>(saved.edits ?? {});
+  const [locked, setLocked]      = useState<LockedSections>(saved.locked ?? DEFAULT_LOCKED);
+  const [isGenerating, setIsGenerating] = useState(false);
+  const [generateMode, setGenerateMode] = useState<'template' | string>('template');
+  const [generateError, setGenerateError] = useState<string | null>(null);
+  const [canUndo, setCanUndo] = useState(false);
+  const [canRedo, setCanRedo] = useState(false);
+
+  const editStack = useRef<Array<Partial<BrandOutputs>>>([saved.edits ?? {}]);
+  const stackIdx  = useRef(0);
+  const abortRef  = useRef<AbortController | null>(null);
+
+  const stateRef = useRef({ inputs, outputs, edits, locked });
+  stateRef.current = { inputs, outputs, edits, locked };
+
+  const syncUndoRedo = () => {
+    setCanUndo(stackIdx.current > 0);
+    setCanRedo(stackIdx.current < editStack.current.length - 1);
+  };
+
+  const setEdits = useCallback((next: Partial<BrandOutputs>, pushHistory: boolean) => {
+    if (pushHistory) {
+      editStack.current = editStack.current.slice(0, stackIdx.current + 1);
+      editStack.current.push(next);
+      stackIdx.current = editStack.current.length - 1;
+    }
+    setEditsRaw(next);
+    syncUndoRedo();
+  }, []);
+
+  const persistInputs = useCallback((value: BrandInputs) => {
+    const { outputs, edits, locked } = stateRef.current;
+    saveState(storageKey, { inputs: value, outputs, edits, locked });
+  }, [storageKey]);
+
+  const setInputs = useCallback((next: BrandInputs | ((prev: BrandInputs) => BrandInputs)) => {
+    setInputsRaw(prev => {
+      const value = typeof next === 'function' ? next(prev) : next;
+      persistInputs(value);
+      return value;
+    });
+  }, [persistInputs]);
+
+  const runGenerate = useCallback(async () => {
+    // Cancel any in-flight request
+    abortRef.current?.abort();
+    const abort = new AbortController();
+    abortRef.current = abort;
+
+    const { inputs, outputs, edits, locked } = stateRef.current;
+    const settings = readSettings();
+
+    setIsGenerating(true);
+    setGenerateError(null);
+    setGenerateMode(settings.enabled ? settings.model : 'template');
+
+    try {
+      let newOutputs: BrandOutputs;
+
+      if (settings.enabled) {
+        newOutputs = await generateWithAI(inputs, settings, abort.signal);
+      } else {
+        await new Promise(r => setTimeout(r, 120));
+        if (abort.signal.aborted) return;
+        newOutputs = generate(inputs);
+      }
+
+      if (abort.signal.aborted) return;
+
+      const merged = applyLocks({ ...newOutputs }, locked, outputs, edits);
+      setOutputs(merged);
+      editStack.current = [{}];
+      stackIdx.current  = 0;
+      setEditsRaw({});
+      syncUndoRedo();
+      saveState(storageKey, { inputs, outputs: merged, edits: {}, locked });
+    } catch (err) {
+      if ((err as Error).name === 'AbortError') return;
+      setGenerateError((err as Error).message);
+    } finally {
+      setIsGenerating(false);
+    }
+  }, [storageKey]);
+
+  const cancelGenerate = useCallback(() => {
+    abortRef.current?.abort();
+    setIsGenerating(false);
+    setGenerateError(null);
+  }, []);
+
+  const updateEdit = useCallback(<K extends keyof BrandOutputs>(key: K, value: BrandOutputs[K]) => {
+    const { inputs, outputs, locked } = stateRef.current;
+    const next = { ...stateRef.current.edits, [key]: value };
+    setEdits(next, true);
+    saveState(storageKey, { inputs, outputs, edits: next, locked });
+  }, [storageKey, setEdits]);
+
+  const undo = useCallback(() => {
+    if (stackIdx.current <= 0) return;
+    stackIdx.current -= 1;
+    const prev = editStack.current[stackIdx.current];
+    const { inputs, outputs, locked } = stateRef.current;
+    setEdits(prev, false);
+    saveState(storageKey, { inputs, outputs, edits: prev, locked });
+  }, [storageKey, setEdits]);
+
+  const redo = useCallback(() => {
+    if (stackIdx.current >= editStack.current.length - 1) return;
+    stackIdx.current += 1;
+    const next = editStack.current[stackIdx.current];
+    const { inputs, outputs, locked } = stateRef.current;
+    setEdits(next, false);
+    saveState(storageKey, { inputs, outputs, edits: next, locked });
+  }, [storageKey, setEdits]);
+
+  const toggleLock = useCallback((section: keyof LockedSections) => {
+    setLocked(prev => {
+      const next = { ...prev, [section]: !prev[section] };
+      const { inputs, outputs, edits } = stateRef.current;
+      saveState(storageKey, { inputs, outputs, edits, locked: next });
+      return next;
+    });
+  }, [storageKey]);
+
+  const effectiveOutputs: BrandOutputs | null = outputs ? { ...outputs, ...edits } : null;
+
+  return {
+    inputs,
+    setInputs,
+    outputs: effectiveOutputs,
+    isGenerating,
+    generateMode,
+    generateError,
+    locked,
+    runGenerate,
+    cancelGenerate,
+    updateEdit,
+    toggleLock,
+    undo,
+    redo,
+    canUndo,
+    canRedo,
+    hasOutput: outputs !== null,
+  };
+}
diff --git a/src/lib/clipboard.ts b/src/lib/clipboard.ts
new file mode 100644
index 0000000..d735a51
--- /dev/null
+++ b/src/lib/clipboard.ts
@@ -0,0 +1,17 @@
+export async function copyToClipboard(text: string): Promise<boolean> {
+  try {
+    await navigator.clipboard.writeText(text);
+    return true;
+  } catch {
+    // Fallback for older browsers
+    const el = document.createElement('textarea');
+    el.value = text;
+    el.style.position = 'fixed';
+    el.style.left = '-9999px';
+    document.body.appendChild(el);
+    el.select();
+    const ok = document.execCommand('copy');
+    document.body.removeChild(el);
+    return ok;
+  }
+}
diff --git a/src/lib/export.ts b/src/lib/export.ts
new file mode 100644
index 0000000..9e1a7f1
--- /dev/null
+++ b/src/lib/export.ts
@@ -0,0 +1,326 @@
+import type { BrandInputs, BrandOutputs } from '../types';
+
+export function toMarkdown(inputs: BrandInputs, outputs: BrandOutputs): string {
+  const lines: string[] = [];
+
+  lines.push(`# Brand Package — ${inputs.name || 'Untitled'}`);
+  lines.push('');
+  lines.push(`*Generated ${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}*`);
+  lines.push('');
+  lines.push('---');
+  lines.push('');
+
+  lines.push('## Overview');
+  lines.push('');
+  lines.push(outputs.overview);
+  lines.push('');
+
+  lines.push('## Positioning');
+  lines.push('');
+  lines.push(outputs.positioning);
+  lines.push('');
+
+  lines.push('## Tone & Voice');
+  lines.push('');
+  lines.push(`**Attributes:** ${outputs.tone.attributes.join(', ')}`);
+  lines.push('');
+  lines.push(`**Voice:** ${outputs.tone.voiceNotes}`);
+  lines.push('');
+  if (outputs.tone.avoidList.length > 0) {
+    lines.push(`**Avoid:** ${outputs.tone.avoidList.join(', ')}`);
+    lines.push('');
+  }
+  if (outputs.tone.examplePhrases.length > 0) {
+    lines.push('**Example phrases:**');
+    outputs.tone.examplePhrases.forEach(p => lines.push(`- ${p}`));
+    lines.push('');
+  }
+
+  lines.push('## Messaging');
+  lines.push('');
+  lines.push('### Titles');
+  outputs.titles.forEach((t, i) => lines.push(`${i + 1}. ${t}`));
+  lines.push('');
+  lines.push('### Subtitles');
+  outputs.subtitles.forEach((s, i) => lines.push(`${i + 1}. ${s}`));
+  lines.push('');
+  lines.push('### Taglines');
+  outputs.taglines.forEach((t, i) => lines.push(`${i + 1}. ${t}`));
+  lines.push('');
+
+  lines.push('## Color Palette');
+  lines.push('');
+  outputs.palette.swatches.forEach(s => {
+    lines.push(`- **${s.name}** — \`${s.hex}\` *(${s.role})*`);
+  });
+  lines.push('');
+
+  lines.push('## Typography');
+  lines.push('');
+  lines.push(`**Primary:** ${outputs.typography.primary}`);
+  if (outputs.typography.secondary !== outputs.typography.primary)
+    lines.push(`**Secondary:** ${outputs.typography.secondary}`);
+  lines.push(`**Monospace:** ${outputs.typography.mono}`);
+  lines.push('');
+  lines.push(`*${outputs.typography.pairNote}*`);
+  lines.push('');
+  lines.push('| Style | Size | Weight | Usage |');
+  lines.push('|-------|------|--------|-------|');
+  outputs.typography.scale.forEach(t => {
+    lines.push(`| ${t.label} | ${t.size} | ${t.weight} | ${t.usage} |`);
+  });
+  lines.push('');
+
+  lines.push('## Visual Direction');
+  lines.push('');
+  outputs.visualDirections.forEach(dir => {
+    lines.push(`### ${dir.name}`);
+    lines.push('');
+    lines.push(dir.description);
+    lines.push('');
+    lines.push(`**Palette:** ${dir.palette}`);
+    lines.push('');
+    lines.push(`**Typography:** ${dir.typography}`);
+    lines.push('');
+    lines.push(`**References:** ${dir.references}`);
+    lines.push('');
+  });
+
+  lines.push('## Logo Concepts');
+  lines.push('');
+  outputs.logoConcepts.forEach(lc => {
+    lines.push(`### ${lc.title}`);
+    lines.push('');
+    lines.push(lc.concept);
+    lines.push('');
+    lines.push(`**Mark:** ${lc.mark}`);
+    lines.push('');
+    lines.push(`**Execution:** ${lc.execution}`);
+    lines.push('');
+  });
+
+  lines.push('## Usage Examples');
+  lines.push('');
+  outputs.usageExamples.forEach(ex => {
+    lines.push(`### ${ex.context}`);
+    lines.push('');
+    lines.push('```');
+    lines.push(ex.text);
+    lines.push('```');
+    lines.push('');
+  });
+
+  lines.push('## Constraints');
+  lines.push('');
+  outputs.constraints.forEach(c => lines.push(`- ${c}`));
+  lines.push('');
+
+  lines.push('---');
+  lines.push('');
+  lines.push('*Brand Workbench*');
+
+  return lines.join('\n');
+}
+
+export function toJSON(inputs: BrandInputs, outputs: BrandOutputs): string {
+  return JSON.stringify(
+    {
+      project: {
+        name: inputs.name,
+        category: inputs.category,
+        purpose: inputs.purpose,
+        audience: inputs.audience,
+      },
+      brand: outputs,
+      meta: {
+        generated: new Date().toISOString(),
+        version: '1.0',
+      },
+    },
+    null,
+    2
+  );
+}
+
+export function toHTML(inputs: BrandInputs, outputs: BrandOutputs): string {
+  const date = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
+  const esc = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
+
+  const swatchesHtml = outputs.palette.swatches.map(s => `
+    <div class="swatch">
+      <div class="swatch-color" style="background:${esc(s.hex)}"></div>
+      <div class="swatch-name">${esc(s.name)}</div>
+      <div class="swatch-hex">${esc(s.hex)}</div>
+    </div>`).join('');
+
+  const scaleRows = outputs.typography.scale.map(t => `
+    <tr><td>${esc(t.label)}</td><td>${esc(t.size)}</td><td>${esc(t.weight)}</td><td>${esc(t.usage)}</td></tr>`).join('');
+
+  const dirsHtml = outputs.visualDirections.map(d => `
+    <div class="card">
+      <h3>${esc(d.name)}</h3>
+      <p>${esc(d.description)}</p>
+      <dl>
+        <dt>Palette</dt><dd>${esc(d.palette)}</dd>
+        <dt>Typography</dt><dd>${esc(d.typography)}</dd>
+        <dt>References</dt><dd>${esc(d.references)}</dd>
+      </dl>
+    </div>`).join('');
+
+  const logosHtml = outputs.logoConcepts.map(l => `
+    <div class="card">
+      <h3>${esc(l.title)}</h3>
+      <p>${esc(l.concept)}</p>
+      <dl>
+        <dt>Mark</dt><dd>${esc(l.mark)}</dd>
+        <dt>Execution</dt><dd>${esc(l.execution)}</dd>
+      </dl>
+    </div>`).join('');
+
+  const usageHtml = outputs.usageExamples.map(ex => `
+    <div class="usage-item">
+      <div class="usage-label">${esc(ex.context)}</div>
+      <pre>${esc(ex.text)}</pre>
+    </div>`).join('');
+
+  const constraintsHtml = outputs.constraints.map(c => `<li>${esc(c)}</li>`).join('\n');
+
+  const secondaryRow = outputs.typography.secondary !== outputs.typography.primary
+    ? `<tr><td>Secondary</td><td>${esc(outputs.typography.secondary)}</td></tr>` : '';
+
+  return `<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>${esc(inputs.name || 'Brand')} — Brand Guidelines</title>
+<style>
+  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+  html { font-size: 15px; }
+  body { font-family: ui-sans-serif, -apple-system, system-ui, sans-serif; color: #1a1a1a; background: #fff; line-height: 1.6; }
+  .guide { max-width: 860px; margin: 0 auto; padding: 60px 40px 100px; }
+  .guide-header { border-bottom: 2px solid #111; padding-bottom: 24px; margin-bottom: 48px; }
+  .guide-header h1 { font-size: 36px; font-weight: 700; letter-spacing: -0.02em; }
+  .guide-header .meta { font-size: 13px; color: #777; margin-top: 6px; }
+  h2 { font-size: 11px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: #999; margin-bottom: 20px; padding-bottom: 8px; border-bottom: 1px solid #e5e5e5; }
+  section { margin-bottom: 56px; }
+  p { color: #333; margin-bottom: 12px; }
+  .font-table, .scale-table { width: 100%; border-collapse: collapse; font-size: 14px; margin-bottom: 16px; }
+  .font-table td, .scale-table td, .scale-table th { padding: 8px 12px; border-bottom: 1px solid #eee; text-align: left; }
+  .scale-table th { font-size: 11px; font-weight: 600; color: #999; text-transform: uppercase; letter-spacing: 0.06em; }
+  .font-table td:first-child { color: #999; width: 120px; }
+  .pair-note { font-size: 13px; color: #777; font-style: italic; margin-top: 8px; }
+  .swatches { display: flex; flex-wrap: wrap; gap: 12px; }
+  .swatch { width: 100px; }
+  .swatch-color { width: 100px; height: 64px; border-radius: 6px; border: 1px solid rgba(0,0,0,.08); margin-bottom: 6px; }
+  .swatch-name { font-size: 12px; font-weight: 500; color: #333; }
+  .swatch-hex { font-size: 11px; font-family: ui-monospace, monospace; color: #777; }
+  .card { border: 1px solid #e5e5e5; border-radius: 8px; padding: 20px; margin-bottom: 16px; }
+  .card h3 { font-size: 15px; font-weight: 600; margin-bottom: 8px; }
+  .card p { font-size: 14px; color: #555; margin-bottom: 12px; }
+  dl { display: grid; grid-template-columns: 100px 1fr; gap: 4px 16px; font-size: 13px; }
+  dt { color: #999; font-weight: 500; }
+  dd { color: #444; }
+  .usage-item { margin-bottom: 20px; }
+  .usage-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #999; margin-bottom: 6px; }
+  pre { background: #f5f5f5; border-radius: 6px; padding: 14px 16px; font-size: 13px; font-family: ui-monospace, monospace; white-space: pre-wrap; color: #333; line-height: 1.6; }
+  ul { padding-left: 20px; }
+  li { font-size: 14px; color: #444; margin-bottom: 6px; }
+  .tone-pills { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
+  .tone-pill { background: #f0f0f0; border-radius: 4px; padding: 3px 10px; font-size: 12px; color: #555; }
+  .tone-row { font-size: 14px; color: #444; margin-bottom: 8px; }
+  .tone-row strong { color: #999; display: inline-block; min-width: 100px; }
+  .taglines ol, .titles ol { padding-left: 20px; }
+  .taglines li, .titles li { font-size: 15px; color: #333; margin-bottom: 8px; }
+</style>
+</head>
+<body>
+<div class="guide">
+  <div class="guide-header">
+    <h1>${esc(inputs.name || 'Brand')} Guidelines</h1>
+    <div class="meta">Generated ${date}${inputs.category ? ` · ${esc(inputs.category)}` : ''}${inputs.audience ? ` · ${esc(inputs.audience)}` : ''}</div>
+  </div>
+
+  <section>
+    <h2>Overview</h2>
+    <p>${esc(outputs.overview)}</p>
+  </section>
+
+  <section>
+    <h2>Positioning</h2>
+    <p>${esc(outputs.positioning)}</p>
+  </section>
+
+  <section>
+    <h2>Tone &amp; Voice</h2>
+    <div class="tone-pills">${outputs.tone.attributes.map(a => `<span class="tone-pill">${esc(a)}</span>`).join('')}</div>
+    <div class="tone-row"><strong>Voice</strong>${esc(outputs.tone.voiceNotes)}</div>
+    ${outputs.tone.avoidList.length ? `<div class="tone-row"><strong>Avoid</strong>${esc(outputs.tone.avoidList.join(', '))}</div>` : ''}
+  </section>
+
+  <section>
+    <h2>Messaging</h2>
+    <div class="titles">
+      <p><strong>Titles</strong></p>
+      <ol>${outputs.titles.map(t => `<li>${esc(t)}</li>`).join('')}</ol>
+    </div>
+    <br>
+    <div class="taglines">
+      <p><strong>Taglines</strong></p>
+      <ol>${outputs.taglines.map(t => `<li>${esc(t)}</li>`).join('')}</ol>
+    </div>
+  </section>
+
+  <section>
+    <h2>Color Palette</h2>
+    <div class="swatches">${swatchesHtml}</div>
+  </section>
+
+  <section>
+    <h2>Typography</h2>
+    <table class="font-table">
+      <tr><td>Primary</td><td>${esc(outputs.typography.primary)}</td></tr>
+      ${secondaryRow}
+      <tr><td>Monospace</td><td>${esc(outputs.typography.mono)}</td></tr>
+    </table>
+    <p class="pair-note">${esc(outputs.typography.pairNote)}</p>
+    <br>
+    <table class="scale-table">
+      <thead><tr><th>Style</th><th>Size</th><th>Weight</th><th>Usage</th></tr></thead>
+      <tbody>${scaleRows}</tbody>
+    </table>
+  </section>
+
+  <section>
+    <h2>Visual Direction</h2>
+    ${dirsHtml}
+  </section>
+
+  <section>
+    <h2>Logo Concepts</h2>
+    ${logosHtml}
+  </section>
+
+  <section>
+    <h2>Usage Examples</h2>
+    ${usageHtml}
+  </section>
+
+  <section>
+    <h2>Constraints</h2>
+    <ul>${constraintsHtml}</ul>
+  </section>
+</div>
+</body>
+</html>`;
+}
+
+export function downloadFile(content: string, filename: string, mimeType: string): void {
+  const blob = new Blob([content], { type: mimeType });
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement('a');
+  a.href = url;
+  a.download = filename;
+  a.click();
+  URL.revokeObjectURL(url);
+}
diff --git a/src/lib/sanitize.ts b/src/lib/sanitize.ts
new file mode 100644
index 0000000..d63eb19
--- /dev/null
+++ b/src/lib/sanitize.ts
@@ -0,0 +1,141 @@
+/**
+ * Defensive coercion helpers for AI-generated brand outputs.
+ * AI models sometimes return structured objects instead of plain strings/arrays;
+ * these helpers normalise any value to the expected type so React never receives
+ * an object where it expects a renderable child.
+ */
+import type { BrandOutputs } from '../types';
+import { TYPE_SCALE } from '../engine/generator';
+
+// ---------------------------------------------------------------------------
+// Primitive coercions
+// ---------------------------------------------------------------------------
+
+/** Coerce any value to a non-empty string. */
+export function str(v: unknown, fallback = ''): string {
+  if (typeof v === 'string') return v;
+  if (typeof v === 'number' || typeof v === 'boolean') return String(v);
+  if (v && typeof v === 'object') {
+    const o = v as Record<string, unknown>;
+    for (const k of ['text', 'content', 'value', 'description', 'name', 'label']) {
+      if (typeof o[k] === 'string' && o[k]) return o[k] as string;
+    }
+    const vals = Object.values(o).filter(x => typeof x === 'string') as string[];
+    if (vals.length) return vals.join(' — ');
+  }
+  return fallback;
+}
+
+/** Coerce any value to an array of non-empty strings. */
+export function strArr(v: unknown, fallback: string[] = []): string[] {
+  if (Array.isArray(v)) return v.map(x => str(x)).filter(Boolean);
+  if (typeof v === 'string' && v) return [v];
+  if (v && typeof v === 'object') return [str(v)].filter(Boolean);
+  return fallback;
+}
+
+function normalizeHex(hex: string): string {
+  const h = hex.trim().toLowerCase();
+  const clean = h.startsWith('#') ? h : `#${h}`;
+  return /^#[0-9a-f]{6}$/.test(clean) ? clean : '#888888';
+}
+
+// ---------------------------------------------------------------------------
+// Full output sanitiser — safe to call on any untrusted value
+// ---------------------------------------------------------------------------
+
+/**
+ * Recursively coerce every field of a brand output object to its expected type.
+ * Returns `null` if the input is clearly not a valid brand output at all.
+ */
+export function sanitizeOutputs(raw: unknown): BrandOutputs | null {
+  if (!raw || typeof raw !== 'object') return null;
+  const r = raw as Record<string, unknown>;
+
+  // Must have at least the core keys to be worth keeping
+  if (!r.overview && !r.positioning && !r.tone) return null;
+
+  const tone = (r.tone && typeof r.tone === 'object' ? r.tone : {}) as Record<string, unknown>;
+  const typo = (r.typography && typeof r.typography === 'object' ? r.typography : {}) as Record<string, unknown>;
+  const palette = (r.palette && typeof r.palette === 'object' ? r.palette : {}) as Record<string, unknown>;
+
+  const swatches = Array.isArray(palette.swatches)
+    ? palette.swatches.map((s: unknown, i: number) => {
+        const sw = (s && typeof s === 'object' ? s : {}) as Record<string, unknown>;
+        return {
+          id:   str(sw.id,   `s${i}`),
+          name: str(sw.name, 'Color'),
+          hex:  normalizeHex(str(sw.hex, '#888888')),
+          role: str(sw.role, 'accent'),
+        };
+      })
+    : [];
+
+  if (!swatches.length) return null;
+
+  const visualDirections = Array.isArray(r.visualDirections)
+    ? r.visualDirections.map((v: unknown, i: number) => {
+        const d = (v && typeof v === 'object' ? v : {}) as Record<string, unknown>;
+        return {
+          id:          str(d.id,          `v${i + 1}`),
+          name:        str(d.name,        `Direction ${i + 1}`),
+          description: str(d.description, ''),
+          palette:     str(d.palette,     ''),
+          typography:  str(d.typography,  ''),
+          references:  str(d.references,  ''),
+        };
+      })
+    : [];
+
+  const logoConcepts = Array.isArray(r.logoConcepts)
+    ? r.logoConcepts.map((v: unknown, i: number) => {
+        const c = (v && typeof v === 'object' ? v : {}) as Record<string, unknown>;
+        return {
+          id:        str(c.id,        `l${i + 1}`),
+          title:     str(c.title,     `Concept ${i + 1}`),
+          concept:   str(c.concept,   ''),
+          mark:      str(c.mark,      ''),
+          execution: str(c.execution, ''),
+        };
+      })
+    : [];
+
+  const usageExamples = Array.isArray(r.usageExamples)
+    ? r.usageExamples.map((v: unknown) => {
+        const e = (v && typeof v === 'object' ? v : {}) as Record<string, unknown>;
+        return {
+          context: str(e.context, 'Example'),
+          text:    str(e.text,    ''),
+        };
+      })
+    : [];
+
+  // Preserve an existing valid scale (already-saved data) or fall back to default
+  const existingScale = Array.isArray(typo.scale) ? typo.scale : TYPE_SCALE;
+
+  return {
+    overview:    str(r.overview,    ''),
+    positioning: str(r.positioning, ''),
+    tone: {
+      attributes:     strArr(tone.attributes,     []),
+      voiceNotes:     str(tone.voiceNotes,     ''),
+      avoidList:      strArr(tone.avoidList,      []),
+      examplePhrases: strArr(tone.examplePhrases, []),
+    },
+    titles:    strArr(r.titles,    []),
+    subtitles: strArr(r.subtitles, []),
+    taglines:  strArr(r.taglines,  []),
+    palette:   { swatches },
+    typography: {
+      primary:   str(typo.primary,   'Inter'),
+      secondary: str(typo.secondary, 'Inter'),
+      mono:      str(typo.mono,      'JetBrains Mono'),
+      pairNote:  str(typo.pairNote,  ''),
+      scale:     existingScale,
+    },
+    visualDirections,
+    logoConcepts,
+    usageExamples,
+    constraints: strArr(r.constraints, []),
+  };
+}
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..4c5213a
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import './styles/globals.css';
+import App from './App';
+
+createRoot(document.getElementById('root')!).render(
+  <StrictMode>
+    <App />
+  </StrictMode>
+);
diff --git a/src/styles/globals.css b/src/styles/globals.css
new file mode 100644
index 0000000..8fb1ae3
--- /dev/null
+++ b/src/styles/globals.css
@@ -0,0 +1,1594 @@
+/* ── Reset ───────────────────────────────────────────────────────────────── */
+
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+html { font-size: 14px; }
+
+/* ── Tokens ──────────────────────────────────────────────────────────────── */
+
+:root {
+  --bg:          #0d0d0d;
+  --bg-1:        #111111;
+  --bg-2:        #161616;
+  --bg-3:        #1c1c1c;
+  --bg-4:        #222222;
+
+  --border:      #242424;
+  --border-2:    #1a1a1a;
+  --border-3:    #2e2e2e;
+
+  --text:        #dedede;
+  --text-2:      #999999;
+  --text-3:      #555555;
+  --text-4:      #333333;
+
+  --accent:      #c9a96e;
+  --accent-dim:  #7a6341;
+
+  --success:     #4a9470;
+  --danger:      #8a3030;
+
+  --font-sans:   ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
+  --font-mono:   ui-monospace, 'JetBrains Mono', 'Fira Code', 'Cascadia Code', Menlo, monospace;
+
+  --r:           4px;
+  --r-lg:        6px;
+  --header-h:    44px;
+  --panel-w:     360px;
+}
+
+/* ── Base ────────────────────────────────────────────────────────────────── */
+
+body {
+  background: var(--bg);
+  color: var(--text);
+  font-family: var(--font-sans);
+  font-size: 13px;
+  line-height: 1.5;
+  -webkit-font-smoothing: antialiased;
+  overflow: hidden;
+  height: 100vh;
+}
+
+#root { height: 100vh; display: flex; flex-direction: column; }
+
+/* ── App Header ──────────────────────────────────────────────────────────── */
+
+.app-header {
+  height: var(--header-h);
+  min-height: var(--header-h);
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 16px;
+  border-bottom: 1px solid var(--border);
+  background: var(--bg-1);
+  flex-shrink: 0;
+  gap: 12px;
+}
+
+.app-header-left {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  min-width: 0;
+}
+
+.app-wordmark {
+  font-size: 12px;
+  font-weight: 600;
+  letter-spacing: 0.05em;
+  color: var(--text);
+  text-transform: uppercase;
+  flex-shrink: 0;
+}
+
+.app-wordmark span {
+  color: var(--text-3);
+  font-weight: 400;
+}
+
+.app-project-name {
+  font-size: 12px;
+  color: var(--text-3);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.app-project-name::before {
+  content: '/ ';
+  color: var(--text-4);
+}
+
+.app-header-right {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  flex-shrink: 0;
+}
+
+/* ── Package Switcher ────────────────────────────────────────────────────── */
+
+.pkg-switcher {
+  display: flex;
+  align-items: center;
+  gap: 2px;
+  overflow-x: auto;
+  scrollbar-width: none;
+  max-width: 480px;
+}
+
+.pkg-switcher::-webkit-scrollbar { display: none; }
+
+.pkg-tab {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  padding: 0 8px;
+  height: 26px;
+  border-radius: var(--r);
+  border: 1px solid transparent;
+  font-size: 12px;
+  color: var(--text-3);
+  cursor: pointer;
+  white-space: nowrap;
+  user-select: none;
+  transition: all 0.1s;
+  flex-shrink: 0;
+}
+
+.pkg-tab:hover {
+  background: var(--bg-3);
+  color: var(--text-2);
+}
+
+.pkg-tab.active {
+  background: var(--bg-3);
+  border-color: var(--border);
+  color: var(--text);
+}
+
+.pkg-tab-name {
+  max-width: 120px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.pkg-tab-input {
+  width: 100px;
+  background: transparent;
+  border: none;
+  outline: none;
+  font-size: 12px;
+  font-family: inherit;
+  color: var(--text);
+  padding: 0;
+}
+
+.pkg-tab-actions {
+  display: flex;
+  align-items: center;
+  gap: 2px;
+  margin-left: 2px;
+  opacity: 0;
+  transition: opacity 0.1s;
+}
+
+.pkg-tab:hover .pkg-tab-actions,
+.pkg-tab.active .pkg-tab-actions {
+  opacity: 1;
+}
+
+.pkg-tab-close, .pkg-tab-menu-btn {
+  background: none;
+  border: none;
+  color: var(--text-4);
+  cursor: pointer;
+  font-size: 12px;
+  padding: 0 2px;
+  line-height: 1;
+  display: flex;
+  align-items: center;
+  border-radius: 2px;
+  height: 16px;
+}
+
+.pkg-tab-close:hover, .pkg-tab-menu-btn:hover {
+  background: var(--bg-4);
+  color: var(--text-2);
+}
+
+.pkg-tab-menu-btn {
+  letter-spacing: -1px;
+  font-size: 10px;
+}
+
+.pkg-tab-dropdown {
+  background: var(--bg-3);
+  border: 1px solid var(--border-3);
+  border-radius: var(--r-lg);
+  padding: 4px;
+  z-index: 100;
+  min-width: 120px;
+  box-shadow: 0 4px 16px rgba(0,0,0,0.4);
+}
+
+.pkg-tab-dropdown button {
+  display: block;
+  width: 100%;
+  text-align: left;
+  background: none;
+  border: none;
+  color: var(--text-2);
+  font-size: 12px;
+  font-family: inherit;
+  padding: 5px 8px;
+  border-radius: var(--r);
+  cursor: pointer;
+}
+
+.pkg-tab-dropdown button:hover {
+  background: var(--bg-4);
+  color: var(--text);
+}
+
+.pkg-tab-dropdown button.danger { color: #c04040; }
+.pkg-tab-dropdown button.danger:hover { background: rgba(192,64,64,0.15); }
+
+.pkg-new-btn {
+  width: 26px;
+  height: 26px;
+  border-radius: var(--r);
+  border: 1px dashed var(--border-3);
+  background: none;
+  color: var(--text-4);
+  font-size: 16px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  transition: all 0.1s;
+}
+
+.pkg-new-btn:hover {
+  border-color: var(--text-3);
+  color: var(--text-2);
+  background: var(--bg-3);
+}
+
+/* ── App Shell ───────────────────────────────────────────────────────────── */
+
+.app-shell {
+  display: flex;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+
+/* ── Input Panel ─────────────────────────────────────────────────────────── */
+
+.input-panel {
+  width: var(--panel-w);
+  min-width: var(--panel-w);
+  border-right: 1px solid var(--border);
+  display: flex;
+  flex-direction: column;
+  overflow-y: auto;
+  background: var(--bg-1);
+}
+
+.input-panel::-webkit-scrollbar { width: 4px; }
+.input-panel::-webkit-scrollbar-track { background: transparent; }
+.input-panel::-webkit-scrollbar-thumb { background: var(--border-3); border-radius: 2px; }
+
+.input-section {
+  padding: 16px;
+  border-bottom: 1px solid var(--border-2);
+}
+
+.input-section:last-of-type { border-bottom: none; }
+
+.input-section-label {
+  font-size: 10px;
+  font-weight: 600;
+  letter-spacing: 0.1em;
+  text-transform: uppercase;
+  color: var(--text-3);
+  margin-bottom: 12px;
+}
+
+/* ── Form Fields ─────────────────────────────────────────────────────────── */
+
+.field {
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
+  margin-bottom: 10px;
+}
+
+.field:last-child { margin-bottom: 0; }
+
+.field-label {
+  font-size: 11px;
+  color: var(--text-2);
+  font-weight: 500;
+}
+
+.field-input,
+.field-textarea {
+  background: var(--bg);
+  border: 1px solid var(--border);
+  border-radius: var(--r);
+  color: var(--text);
+  font-family: var(--font-sans);
+  font-size: 13px;
+  padding: 7px 10px;
+  width: 100%;
+  transition: border-color 0.1s;
+  outline: none;
+}
+
+.field-input::placeholder,
+.field-textarea::placeholder {
+  color: var(--text-4);
+}
+
+.field-input:focus,
+.field-textarea:focus {
+  border-color: var(--border-3);
+}
+
+.field-textarea {
+  resize: vertical;
+  min-height: 72px;
+  line-height: 1.5;
+}
+
+/* ── Token Input ─────────────────────────────────────────────────────────── */
+
+.token-field {
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
+}
+
+.token-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 5px;
+  background: var(--bg);
+  border: 1px solid var(--border);
+  border-radius: var(--r);
+  padding: 6px;
+  min-height: 36px;
+  cursor: text;
+  transition: border-color 0.1s;
+}
+
+.token-container:focus-within {
+  border-color: var(--border-3);
+}
+
+.token {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  background: var(--bg-3);
+  border: 1px solid var(--border);
+  border-radius: 3px;
+  padding: 2px 7px 2px 8px;
+  font-size: 12px;
+  color: var(--text-2);
+  line-height: 1;
+  height: 22px;
+}
+
+.token-remove {
+  background: none;
+  border: none;
+  color: var(--text-3);
+  cursor: pointer;
+  font-size: 14px;
+  line-height: 1;
+  padding: 0;
+  display: flex;
+  align-items: center;
+  margin-left: 1px;
+  opacity: 0.7;
+}
+
+.token-remove:hover { color: var(--text); opacity: 1; }
+
+.token-input {
+  background: none;
+  border: none;
+  color: var(--text);
+  font-family: var(--font-sans);
+  font-size: 12px;
+  outline: none;
+  min-width: 80px;
+  flex: 1;
+  padding: 2px 2px;
+}
+
+.token-input::placeholder { color: var(--text-4); }
+
+.token-suggestions {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px;
+  margin-top: 6px;
+}
+
+.token-suggestion {
+  background: none;
+  border: 1px solid var(--border);
+  border-radius: 3px;
+  color: var(--text-3);
+  cursor: pointer;
+  font-family: var(--font-sans);
+  font-size: 11px;
+  padding: 2px 7px;
+  transition: all 0.1s;
+}
+
+.token-suggestion:hover {
+  border-color: var(--border-3);
+  color: var(--text-2);
+}
+
+.token-suggestion.active {
+  background: var(--bg-3);
+  border-color: var(--border-3);
+  color: var(--text);
+}
+
+/* ── Generate Button ─────────────────────────────────────────────────────── */
+
+.generate-area {
+  padding: 12px 16px 16px;
+}
+
+.btn-generate {
+  width: 100%;
+  background: var(--text);
+  border: none;
+  border-radius: var(--r);
+  color: var(--bg);
+  cursor: pointer;
+  font-family: var(--font-sans);
+  font-size: 13px;
+  font-weight: 600;
+  height: 36px;
+  letter-spacing: 0.01em;
+  transition: opacity 0.1s, background 0.1s;
+}
+
+.btn-generate:hover:not(:disabled) {
+  opacity: 0.9;
+}
+
+.btn-generate:disabled {
+  opacity: 0.4;
+  cursor: not-allowed;
+}
+
+.btn-generate.generating {
+  background: var(--bg-3);
+  color: var(--text-2);
+}
+
+/* ── Shared Buttons ──────────────────────────────────────────────────────── */
+
+.btn {
+  align-items: center;
+  background: var(--bg-3);
+  border: 1px solid var(--border);
+  border-radius: var(--r);
+  color: var(--text-2);
+  cursor: pointer;
+  display: inline-flex;
+  font-family: var(--font-sans);
+  font-size: 12px;
+  gap: 5px;
+  height: 28px;
+  padding: 0 10px;
+  transition: all 0.1s;
+  white-space: nowrap;
+}
+
+.btn:hover {
+  border-color: var(--border-3);
+  color: var(--text);
+}
+
+.btn-ghost {
+  background: none;
+  border-color: transparent;
+  color: var(--text-3);
+}
+
+.btn-ghost:hover {
+  background: var(--bg-3);
+  border-color: var(--border);
+  color: var(--text-2);
+}
+
+.btn-icon {
+  padding: 0;
+  width: 28px;
+  justify-content: center;
+}
+
+/* ── Preview Panel ───────────────────────────────────────────────────────── */
+
+.preview-panel {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  background: var(--bg);
+}
+
+.preview-toolbar {
+  height: 40px;
+  min-height: 40px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 20px;
+  border-bottom: 1px solid var(--border-2);
+  background: var(--bg);
+  flex-shrink: 0;
+  gap: 12px;
+}
+
+.preview-toolbar-left {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.preview-toolbar-right {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.preview-doc-label {
+  font-size: 11px;
+  color: var(--text-3);
+  font-weight: 500;
+}
+
+.preview-scroll {
+  flex: 1;
+  overflow-y: auto;
+  padding: 40px 48px 80px;
+}
+
+.preview-scroll::-webkit-scrollbar { width: 6px; }
+.preview-scroll::-webkit-scrollbar-track { background: transparent; }
+.preview-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
+
+/* ── Empty State ─────────────────────────────────────────────────────────── */
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  gap: 8px;
+  color: var(--text-4);
+}
+
+.empty-state-title {
+  font-size: 14px;
+  color: var(--text-3);
+  font-weight: 500;
+}
+
+.empty-state-sub {
+  font-size: 12px;
+  color: var(--text-4);
+}
+
+/* ── Brand Document ──────────────────────────────────────────────────────── */
+
+.brand-doc {
+  max-width: 720px;
+}
+
+.brand-doc-header {
+  margin-bottom: 40px;
+  padding-bottom: 24px;
+  border-bottom: 1px solid var(--border-2);
+}
+
+.brand-doc-title {
+  font-size: 28px;
+  font-weight: 600;
+  letter-spacing: -0.02em;
+  color: var(--text);
+  line-height: 1.2;
+  margin-bottom: 6px;
+}
+
+.brand-doc-meta {
+  font-size: 11px;
+  color: var(--text-3);
+  display: flex;
+  gap: 12px;
+}
+
+.brand-doc-meta-item {
+  display: flex;
+  gap: 5px;
+}
+
+.brand-doc-meta-label {
+  color: var(--text-4);
+}
+
+/* ── Doc Section ─────────────────────────────────────────────────────────── */
+
+.doc-section {
+  margin-bottom: 36px;
+  border-radius: var(--r);
+  padding: 2px;
+  margin: -2px -2px 36px;
+}
+
+.doc-section.is-locked {
+  background: rgba(201, 169, 110, 0.025);
+}
+
+.doc-section-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 14px;
+  gap: 8px;
+}
+
+.doc-section-title {
+  font-size: 10px;
+  font-weight: 600;
+  letter-spacing: 0.12em;
+  text-transform: uppercase;
+  color: var(--text-3);
+  display: flex;
+  align-items: center;
+  gap: 7px;
+}
+
+.section-lock {
+  background: none;
+  border: 1px solid transparent;
+  border-radius: 3px;
+  color: var(--text-4);
+  cursor: pointer;
+  font-family: var(--font-sans);
+  font-size: 10px;
+  letter-spacing: 0.04em;
+  padding: 2px 6px;
+  height: 20px;
+  display: inline-flex;
+  align-items: center;
+  transition: all 0.1s;
+  line-height: 1;
+  white-space: nowrap;
+}
+
+.section-lock:hover {
+  border-color: var(--border);
+  color: var(--text-3);
+  background: var(--bg-3);
+}
+
+.section-lock.locked {
+  color: var(--accent);
+  border-color: var(--accent-dim);
+  background: rgba(201, 169, 110, 0.06);
+}
+
+.doc-section-actions {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  opacity: 0;
+  transition: opacity 0.15s;
+}
+
+.doc-section:hover .doc-section-actions { opacity: 1; }
+.doc-section .doc-section-actions:has(.section-lock.locked) { opacity: 1; }
+
+.doc-section-body {
+  border-left: 1px solid var(--border);
+  padding-left: 16px;
+}
+
+.doc-section.is-locked .doc-section-body {
+  border-left-color: var(--accent-dim);
+  border-left-width: 2px;
+}
+
+/* ── Editable Text ───────────────────────────────────────────────────────── */
+
+.editable-text {
+  cursor: text;
+  border-radius: 3px;
+  transition: background 0.1s, outline 0.1s;
+  line-height: 1.65;
+  font-size: 14px;
+  color: var(--text);
+  min-height: 1em;
+  outline: 1px solid transparent;
+}
+
+.editable-text:hover {
+  background: var(--bg-2);
+  outline-color: var(--border-2);
+}
+
+.editable-text.editing {
+  background: var(--bg-2);
+  outline: 1px solid var(--border-3);
+  border-radius: 3px;
+}
+
+/* ── Numbered List ───────────────────────────────────────────────────────── */
+
+.numbered-list {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.numbered-item {
+  display: flex;
+  gap: 10px;
+  align-items: flex-start;
+}
+
+.numbered-item-num {
+  font-size: 11px;
+  color: var(--text-4);
+  font-family: var(--font-mono);
+  min-width: 16px;
+  margin-top: 2px;
+  flex-shrink: 0;
+}
+
+.numbered-item-text {
+  flex: 1;
+  font-size: 14px;
+  color: var(--text);
+  line-height: 1.5;
+  cursor: text;
+  border-radius: 3px;
+  padding: 1px 4px;
+  margin: -1px -4px;
+  transition: background 0.1s;
+}
+
+.numbered-item-text:hover { background: var(--bg-2); }
+.numbered-item-text:focus { background: var(--bg-2); outline: 1px solid var(--border-3); }
+
+/* ── Bullet List ─────────────────────────────────────────────────────────── */
+
+.bullet-list {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.bullet-item {
+  display: flex;
+  gap: 8px;
+  align-items: flex-start;
+  font-size: 13px;
+  color: var(--text-2);
+  line-height: 1.5;
+}
+
+.bullet-item::before {
+  content: '–';
+  color: var(--text-4);
+  flex-shrink: 0;
+  margin-top: 0;
+}
+
+/* ── Tone Section ────────────────────────────────────────────────────────── */
+
+.tone-grid {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.tone-row {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.tone-row-label {
+  font-size: 11px;
+  color: var(--text-3);
+  font-weight: 500;
+}
+
+.tone-row-value {
+  font-size: 13px;
+  color: var(--text-2);
+  line-height: 1.55;
+}
+
+.tone-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 5px;
+}
+
+.tone-tag {
+  background: var(--bg-3);
+  border: 1px solid var(--border);
+  border-radius: 3px;
+  color: var(--text-2);
+  font-size: 11px;
+  padding: 2px 8px;
+}
+
+.tone-phrases {
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
+}
+
+.tone-phrase {
+  font-family: var(--font-mono);
+  font-size: 12px;
+  color: var(--text-2);
+  padding: 5px 8px;
+  background: var(--bg-2);
+  border-radius: 3px;
+  border-left: 2px solid var(--border-3);
+}
+
+/* ── Visual Directions ───────────────────────────────────────────────────── */
+
+.directions-grid {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.direction-card {
+  border: 1px solid var(--border);
+  border-radius: var(--r-lg);
+  padding: 16px;
+  background: var(--bg-1);
+}
+
+.direction-name {
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--text);
+  margin-bottom: 8px;
+}
+
+.direction-desc {
+  font-size: 13px;
+  color: var(--text-2);
+  line-height: 1.6;
+  margin-bottom: 12px;
+}
+
+.direction-attrs {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.direction-attr {
+  display: flex;
+  gap: 8px;
+  font-size: 12px;
+  line-height: 1.5;
+}
+
+.direction-attr-label {
+  color: var(--text-4);
+  font-weight: 500;
+  min-width: 88px;
+  flex-shrink: 0;
+}
+
+.direction-attr-value {
+  color: var(--text-2);
+}
+
+/* ── Logo Concepts ───────────────────────────────────────────────────────── */
+
+.logo-grid {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.logo-card {
+  border: 1px solid var(--border);
+  border-radius: var(--r-lg);
+  padding: 16px;
+  background: var(--bg-1);
+}
+
+.logo-card-title {
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--text);
+  margin-bottom: 8px;
+}
+
+.logo-card-concept {
+  font-size: 13px;
+  color: var(--text-2);
+  line-height: 1.6;
+  margin-bottom: 12px;
+}
+
+.logo-card-attrs {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.logo-card-attr {
+  display: flex;
+  gap: 8px;
+  font-size: 12px;
+  line-height: 1.5;
+}
+
+.logo-card-attr-label {
+  color: var(--text-4);
+  font-weight: 500;
+  min-width: 72px;
+  flex-shrink: 0;
+}
+
+.logo-card-attr-value {
+  color: var(--text-2);
+}
+
+/* ── Usage Examples ──────────────────────────────────────────────────────── */
+
+.usage-grid {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.usage-item {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.usage-context {
+  font-size: 10px;
+  font-weight: 600;
+  letter-spacing: 0.08em;
+  text-transform: uppercase;
+  color: var(--text-4);
+}
+
+.usage-text {
+  font-family: var(--font-mono);
+  font-size: 12px;
+  color: var(--text-2);
+  background: var(--bg-2);
+  border: 1px solid var(--border-2);
+  border-radius: var(--r);
+  padding: 10px 12px;
+  white-space: pre-wrap;
+  line-height: 1.6;
+  position: relative;
+}
+
+.usage-copy {
+  position: absolute;
+  top: 6px;
+  right: 6px;
+  opacity: 0;
+  transition: opacity 0.1s;
+}
+
+.usage-item:hover .usage-copy { opacity: 1; }
+
+/* ── Typography Section ──────────────────────────────────────────────────── */
+
+.type-section {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.type-fonts {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.type-font-row {
+  display: flex;
+  align-items: baseline;
+  gap: 10px;
+  font-size: 13px;
+}
+
+.type-font-role {
+  font-size: 11px;
+  color: var(--text-4);
+  font-weight: 500;
+  min-width: 80px;
+  flex-shrink: 0;
+}
+
+.type-font-name {
+  color: var(--text);
+  font-weight: 500;
+}
+
+.type-pair-note {
+  font-size: 12px;
+  color: var(--text-3);
+  margin-top: 4px;
+}
+
+.type-scale {
+  display: flex;
+  flex-direction: column;
+}
+
+.type-scale-header {
+  display: grid;
+  grid-template-columns: 90px 52px 56px 1fr;
+  gap: 0 12px;
+  font-size: 10px;
+  font-weight: 600;
+  color: var(--text-4);
+  letter-spacing: 0.06em;
+  text-transform: uppercase;
+  padding: 0 0 6px;
+  border-bottom: 1px solid var(--border-2);
+}
+
+.type-scale-row {
+  display: grid;
+  grid-template-columns: 90px 52px 56px 1fr;
+  gap: 0 12px;
+  font-size: 12px;
+  padding: 7px 0;
+  border-bottom: 1px solid var(--border-2);
+  align-items: center;
+}
+
+.type-scale-row:last-child { border-bottom: none; }
+
+.type-scale-label { color: var(--text-2); font-weight: 500; }
+.type-scale-size  { color: var(--text-3); font-family: var(--font-mono); }
+.type-scale-weight{ color: var(--text-3); font-family: var(--font-mono); }
+.type-scale-usage { color: var(--text-4); }
+
+/* ── Color Palette ───────────────────────────────────────────────────────── */
+
+.color-palette {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  align-items: flex-start;
+}
+
+.color-swatch-card {
+  display: flex;
+  flex-direction: column;
+  width: 88px;
+  gap: 6px;
+}
+
+.color-swatch-preview {
+  position: relative;
+  width: 88px;
+  height: 60px;
+  border-radius: var(--r);
+  border: 1px solid rgba(0,0,0,0.15);
+  overflow: hidden;
+  cursor: pointer;
+}
+
+.color-swatch-picker {
+  position: absolute;
+  inset: 0;
+  width: 100%;
+  height: 100%;
+  opacity: 0;
+  cursor: pointer;
+  border: none;
+  padding: 0;
+}
+
+.color-swatch-remove {
+  position: absolute;
+  top: 4px;
+  right: 4px;
+  width: 16px;
+  height: 16px;
+  border-radius: 50%;
+  background: rgba(0,0,0,0.5);
+  border: none;
+  color: #fff;
+  font-size: 12px;
+  line-height: 1;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  opacity: 0;
+  transition: opacity 0.1s;
+  padding: 0;
+  z-index: 1;
+}
+
+.color-swatch-card:hover .color-swatch-remove { opacity: 1; }
+
+.color-swatch-info {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+}
+
+.color-swatch-name {
+  font-size: 11px;
+  font-weight: 500;
+  color: var(--text-2);
+  cursor: text;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.color-swatch-name:hover { color: var(--text); }
+
+.color-swatch-hex {
+  font-family: var(--font-mono);
+  font-size: 10px;
+  color: var(--text-3);
+  cursor: text;
+}
+
+.color-swatch-hex:hover { color: var(--text-2); }
+
+.color-swatch-field-input {
+  width: 100%;
+  background: var(--bg-2);
+  border: 1px solid var(--border-3);
+  border-radius: 3px;
+  color: var(--text);
+  font-family: inherit;
+  font-size: 11px;
+  padding: 1px 4px;
+  outline: none;
+}
+
+.color-swatch-hex-input {
+  font-family: var(--font-mono);
+  font-size: 10px;
+}
+
+.color-swatch-add {
+  width: 88px;
+  height: 60px;
+  border-radius: var(--r);
+  border: 1px dashed var(--border-3);
+  background: none;
+  color: var(--text-4);
+  font-size: 20px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.1s;
+  align-self: flex-start;
+}
+
+.color-swatch-add:hover {
+  border-color: var(--text-3);
+  color: var(--text-2);
+  background: var(--bg-2);
+}
+
+/* ── Constraints ─────────────────────────────────────────────────────────── */
+
+.constraints-list {
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
+}
+
+.constraint-item {
+  font-size: 13px;
+  color: var(--text-2);
+  line-height: 1.5;
+  display: flex;
+  gap: 8px;
+}
+
+.constraint-item::before {
+  content: '·';
+  color: var(--text-4);
+  flex-shrink: 0;
+}
+
+/* ── Copy feedback ───────────────────────────────────────────────────────── */
+
+.copy-btn {
+  background: var(--bg-3);
+  border: 1px solid var(--border);
+  border-radius: 3px;
+  color: var(--text-3);
+  cursor: pointer;
+  font-family: var(--font-sans);
+  font-size: 10px;
+  padding: 2px 7px;
+  height: 20px;
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  transition: all 0.1s;
+  white-space: nowrap;
+}
+
+.copy-btn:hover {
+  border-color: var(--border-3);
+  color: var(--text-2);
+}
+
+.copy-btn.copied {
+  color: var(--success);
+  border-color: var(--success);
+}
+
+/* ── Export Bar ──────────────────────────────────────────────────────────── */
+
+.export-bar {
+  height: 44px;
+  min-height: 44px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 20px;
+  border-top: 1px solid var(--border);
+  background: var(--bg-1);
+  flex-shrink: 0;
+}
+
+.export-bar-left {
+  font-size: 11px;
+  color: var(--text-4);
+}
+
+.export-bar-right {
+  display: flex;
+  gap: 6px;
+}
+
+/* ── AI badge ────────────────────────────────────────────────────────────── */
+
+.settings-open-btn { font-size: 16px; }
+
+.ai-badge {
+  font-size: 9px;
+  font-weight: 700;
+  letter-spacing: 0.08em;
+  color: var(--accent);
+  border: 1px solid var(--accent-dim);
+  border-radius: 3px;
+  padding: 1px 5px;
+  cursor: default;
+  flex-shrink: 0;
+}
+
+/* ── Generate error banner ───────────────────────────────────────────────── */
+
+.generate-error {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 20px;
+  background: rgba(138, 48, 48, 0.15);
+  border-bottom: 1px solid rgba(138, 48, 48, 0.3);
+  font-size: 12px;
+  flex-shrink: 0;
+}
+
+.generate-error-icon { color: #c04040; flex-shrink: 0; }
+.generate-error-msg  { color: var(--text-2); flex: 1; line-height: 1.4; }
+.generate-error-retry { margin-left: auto; font-size: 11px; flex-shrink: 0; }
+
+/* ── Settings overlay + panel ────────────────────────────────────────────── */
+
+.settings-overlay {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.6);
+  z-index: 200;
+  display: flex;
+  align-items: flex-start;
+  justify-content: flex-end;
+  padding: var(--header-h) 0 0;
+}
+
+.settings-panel {
+  width: 400px;
+  height: calc(100vh - var(--header-h));
+  background: var(--bg-2);
+  border-left: 1px solid var(--border-3);
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.settings-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 20px;
+  height: 44px;
+  border-bottom: 1px solid var(--border);
+  flex-shrink: 0;
+}
+
+.settings-title {
+  font-size: 12px;
+  font-weight: 600;
+  color: var(--text);
+  letter-spacing: 0.04em;
+  text-transform: uppercase;
+}
+
+.settings-close {
+  background: none;
+  border: none;
+  color: var(--text-3);
+  font-size: 18px;
+  cursor: pointer;
+  padding: 0;
+  line-height: 1;
+  width: 24px;
+  height: 24px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: var(--r);
+}
+
+.settings-close:hover { background: var(--bg-3); color: var(--text); }
+
+.settings-body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 20px;
+  display: flex;
+  flex-direction: column;
+  gap: 28px;
+}
+
+.settings-section {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.settings-section-label {
+  font-size: 10px;
+  font-weight: 700;
+  letter-spacing: 0.1em;
+  text-transform: uppercase;
+  color: var(--text-4);
+}
+
+.settings-toggle-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  cursor: pointer;
+}
+
+.settings-toggle-info {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+}
+
+.settings-toggle-name {
+  font-size: 13px;
+  color: var(--text);
+  font-weight: 500;
+}
+
+.settings-toggle-desc {
+  font-size: 11px;
+  color: var(--text-3);
+  line-height: 1.4;
+}
+
+/* Toggle switch */
+.toggle {
+  width: 36px;
+  height: 20px;
+  background: var(--bg-4);
+  border: 1px solid var(--border-3);
+  border-radius: 10px;
+  cursor: pointer;
+  position: relative;
+  flex-shrink: 0;
+  transition: background 0.15s, border-color 0.15s;
+}
+
+.toggle.on {
+  background: var(--accent-dim);
+  border-color: var(--accent);
+}
+
+.toggle-thumb {
+  position: absolute;
+  top: 2px;
+  left: 2px;
+  width: 14px;
+  height: 14px;
+  border-radius: 50%;
+  background: var(--text-3);
+  transition: transform 0.15s, background 0.15s;
+}
+
+.toggle.on .toggle-thumb {
+  transform: translateX(16px);
+  background: var(--accent);
+}
+
+/* Settings fields */
+.settings-field {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.settings-field-label {
+  font-size: 11px;
+  font-weight: 500;
+  color: var(--text-3);
+}
+
+.settings-field-row {
+  display: flex;
+  gap: 6px;
+}
+
+.settings-input {
+  flex: 1;
+  background: var(--bg-1);
+  border: 1px solid var(--border-3);
+  border-radius: var(--r);
+  color: var(--text);
+  font-family: var(--font-mono);
+  font-size: 12px;
+  padding: 6px 10px;
+  outline: none;
+  width: 100%;
+  transition: border-color 0.1s;
+}
+
+.settings-input:focus { border-color: var(--accent-dim); }
+
+.settings-test-btn { flex-shrink: 0; }
+.settings-test-btn.testing { opacity: 0.6; }
+
+.settings-status {
+  font-size: 11px;
+  padding: 4px 8px;
+  border-radius: var(--r);
+  line-height: 1.4;
+}
+
+.settings-status.ok    { background: rgba(74, 148, 112, 0.15); color: #4a9470; }
+.settings-status.error { background: rgba(138, 48, 48, 0.15);  color: #c04040; }
+
+.settings-model-list {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  max-height: 180px;
+  overflow-y: auto;
+  border: 1px solid var(--border);
+  border-radius: var(--r);
+  padding: 4px;
+  background: var(--bg-1);
+}
+
+.settings-model-item {
+  text-align: left;
+  background: none;
+  border: none;
+  color: var(--text-2);
+  font-family: var(--font-mono);
+  font-size: 12px;
+  padding: 5px 8px;
+  border-radius: 3px;
+  cursor: pointer;
+}
+
+.settings-model-item:hover  { background: var(--bg-3); color: var(--text); }
+.settings-model-item.active { background: var(--bg-3); color: var(--accent); }
+
+.settings-field-hint {
+  font-size: 11px;
+  color: var(--text-4);
+  line-height: 1.5;
+}
+
+.settings-field-hint code {
+  font-family: var(--font-mono);
+  color: var(--text-3);
+  background: var(--bg-1);
+  padding: 1px 4px;
+  border-radius: 3px;
+}
+
+.settings-help p {
+  font-size: 12px;
+  color: var(--text-3);
+  line-height: 1.6;
+  margin-bottom: 8px;
+}
+
+.settings-help p:last-child { margin-bottom: 0; }
+
+.settings-help strong { color: var(--text-2); font-weight: 500; }
+
+.settings-help code {
+  font-family: var(--font-mono);
+  font-size: 11px;
+  color: var(--text-3);
+  background: var(--bg-1);
+  padding: 2px 5px;
+  border-radius: 3px;
+  display: inline-block;
+  margin-top: 4px;
+}
+
+/* ── Scrollbar global ────────────────────────────────────────────────────── */
+
+::-webkit-scrollbar { width: 6px; height: 6px; }
+::-webkit-scrollbar-track { background: transparent; }
+::-webkit-scrollbar-thumb { background: var(--border-3); border-radius: 3px; }
+::-webkit-scrollbar-thumb:hover { background: var(--border-3); }
+
+/* ── Utilities ───────────────────────────────────────────────────────────── */
+
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  white-space: nowrap;
+  border: 0;
+}
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..e1b436e
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,100 @@
+export interface BrandInputs {
+  name: string;
+  category: string;
+  purpose: string;
+  audience: string;
+  tone: string[];
+  avoid: string[];
+  notes: string;
+}
+
+export interface ToneGuidance {
+  attributes: string[];
+  voiceNotes: string;
+  avoidList: string[];
+  examplePhrases: string[];
+}
+
+export interface VisualDirection {
+  id: string;
+  name: string;
+  description: string;
+  palette: string;
+  typography: string;
+  references: string;
+}
+
+export interface LogoConcept {
+  id: string;
+  title: string;
+  concept: string;
+  mark: string;
+  execution: string;
+}
+
+export interface UsageExample {
+  context: string;
+  text: string;
+}
+
+export interface TypographyToken {
+  label: string;
+  size: string;
+  weight: string;
+  lineHeight: string;
+  usage: string;
+}
+
+export interface Typography {
+  primary: string;
+  secondary: string;
+  mono: string;
+  pairNote: string;
+  scale: TypographyToken[];
+}
+
+export interface ColorSwatch {
+  id: string;
+  name: string;
+  hex: string;
+  role: string;
+}
+
+export interface ColorPalette {
+  swatches: ColorSwatch[];
+}
+
+export interface BrandOutputs {
+  overview: string;
+  positioning: string;
+  tone: ToneGuidance;
+  titles: string[];
+  subtitles: string[];
+  taglines: string[];
+  visualDirections: VisualDirection[];
+  palette: ColorPalette;
+  typography: Typography;
+  logoConcepts: LogoConcept[];
+  usageExamples: UsageExample[];
+  constraints: string[];
+}
+
+export interface LockedSections {
+  overview: boolean;
+  positioning: boolean;
+  tone: boolean;
+  messaging: boolean;
+  visual: boolean;
+  palette: boolean;
+  typography: boolean;
+  logo: boolean;
+  usage: boolean;
+  constraints: boolean;
+}
+
+export interface WorkspaceState {
+  inputs: BrandInputs;
+  outputs: BrandOutputs | null;
+  edits: Partial<BrandOutputs>;
+  locked: LockedSections;
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..109f0ac
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,20 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "module": "ESNext",
+    "skipLibCheck": true,
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "isolatedModules": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+    "jsx": "react-jsx",
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": ["src"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..0466183
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+  plugins: [react()],
+});