diff --git a/commands/new-tool.md b/commands/new-tool.md
index 9e983c8..1ad9862 100644
--- a/commands/new-tool.md
+++ b/commands/new-tool.md
@@ -1,5 +1,5 @@
---
-description: Scaffold a new PolicyEngine interactive tool (Next.js 14 + Tailwind 4 + ui-kit theme + embedding boilerplate)
+description: Scaffold a new PolicyEngine interactive tool (Next.js 16 + Turbopack + Tailwind 4 + ui-kit theme + embedding boilerplate)
---
# New interactive tool scaffold
@@ -20,8 +20,8 @@ Ask the user for:
## Step 2: Create the project
```bash
-# Create Next.js 14 + Tailwind project
-bunx create-next-app@14 TOOL_NAME --js --app --tailwind --eslint --no-src-dir --import-alias "@/*"
+# Create Next.js 16 + Turbopack + Tailwind project
+bunx create-next-app@latest TOOL_NAME --js --app --tailwind --eslint --no-src-dir --import-alias "@/*" --turbopack
cd TOOL_NAME
# Install dependencies
diff --git a/skills/frontend/policyengine-ui-kit-consumer-skill/SKILL.md b/skills/frontend/policyengine-ui-kit-consumer-skill/SKILL.md
index eb65902..73c68cc 100644
--- a/skills/frontend/policyengine-ui-kit-consumer-skill/SKILL.md
+++ b/skills/frontend/policyengine-ui-kit-consumer-skill/SKILL.md
@@ -283,3 +283,8 @@ Both `colors.gray[N]` and `colors.text.warning` change visible color on migratio
- `policyengine-tailwind-shadcn-skill` — `@theme` namespace mechanics, SVG var() usage
- `policyengine-interactive-tools-skill` — Full tool scaffolding checklist
- `policyengine-vercel-deployment-skill` — Deploying consumer apps
+
+## Related Guides
+
+- `references/migration-from-broken-setup.md` — Fixing repos where ui-kit was imported incorrectly
+- `references/vite-to-nextjs-migration.md` — Migrating Vite/React apps to Next.js 16 App Router
diff --git a/skills/frontend/policyengine-ui-kit-consumer-skill/references/vite-to-nextjs-migration.md b/skills/frontend/policyengine-ui-kit-consumer-skill/references/vite-to-nextjs-migration.md
new file mode 100644
index 0000000..27f5dba
--- /dev/null
+++ b/skills/frontend/policyengine-ui-kit-consumer-skill/references/vite-to-nextjs-migration.md
@@ -0,0 +1,484 @@
+# Migrating Vite → Next.js 16 for PolicyEngine Tools
+
+Step-by-step guide for migrating a Vite/React app to Next.js 16 (App Router, Turbopack) to align with the PolicyEngine portfolio standard stack (Vercel + bun + Tailwind v4 + `@policyengine/ui-kit`).
+
+## When to Use This Guide
+
+- Migrating existing Vite-based PolicyEngine tools to Next.js 16
+- Converting standalone React apps to the standard PolicyEngine tooling stack
+- Repos that need to align with `cost-dashboard`, `council-tax-ctr-map`, `policyengine-model` patterns
+
+## Reference Implementation
+
+See [missouri-transitional-benefits PR #5](https://github.com/PolicyEngine/missouri-transitional-benefits/pull/5) for a complete migration example.
+
+## Directory Structure Decision
+
+For repos with both Python research code and a frontend:
+
+### Option A: Frontend at root (NOT RECOMMENDED)
+```
+app/ ← Next.js App Router
+components/
+package.json
+benefits_cliffs/ ← Python code
+pyproject.toml
+```
+**Rejected:** Collides with existing Python files, creates confusion about what lives at root.
+
+### Option B: Keep `app/` as project root (NOT RECOMMENDED)
+```
+app/
+ app/ ← Next.js App Router (awkward nesting)
+ components/
+ package.json
+```
+**Rejected:** Awkward `app/app/` nesting, breaks intuition, inverts Next.js convention.
+
+### Option C: Rename to `web/` (RECOMMENDED)
+```
+web/
+ app/ ← Next.js App Router
+ components/
+ package.json
+ bun.lock
+benefits_cliffs/ ← Python code unchanged
+pyproject.toml
+```
+**Chosen:** Short, conventional, minimal diff to Python side. Update `vercel.json` and `Makefile` to point at `web/`.
+
+For frontend-only repos, keep the frontend at root with `app/` as the Next.js App Router.
+
+## Migration Checklist
+
+### 1. Create new Next.js structure
+
+```bash
+mkdir -p web/app web/components web/data web/__tests__
+```
+
+### 2. Migrate entry points
+
+**From:** `app/src/main.tsx` + `app/index.html`
+**To:** `web/app/layout.tsx` + `web/app/page.tsx`
+
+Move metadata, GA4, JSON-LD, scroll/time/outbound tracking to `layout.tsx`:
+
+```tsx
+// web/app/layout.tsx
+import "./globals.css";
+import { Metadata } from "next";
+import { GoogleAnalytics } from "@next/third-parties/google";
+import Script from "next/script";
+
+export const metadata: Metadata = {
+ title: "Tool Title | PolicyEngine",
+ description: "Tool description",
+ icons: { icon: "/favicon.svg" },
+ // ... other metadata from index.html tags
+};
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ {/* Move JSON-LD structured data here */}
+
+
+
+ {children}
+
+ {/* Port scroll/time/outbound tracking scripts */}
+
+
+ );
+}
+```
+
+```tsx
+// web/app/page.tsx
+import App from "@/components/App";
+
+export default function Home() {
+ return ;
+}
+```
+
+### 3. Migrate components
+
+**From:** `app/src/App.tsx` and `app/src/components/`
+**To:** `web/components/`
+
+Add `"use client"` directive to components that use:
+- `useState`, `useEffect`, `useContext`
+- Event handlers (`onClick`, `onChange`)
+- Browser APIs
+
+```tsx
+// web/components/App.tsx
+"use client";
+
+import { MantineProvider } from "@mantine/core";
+// ... rest of component
+```
+
+**IMPORTANT:** Hoist providers like `MantineProvider` into `layout.tsx` if they don't need client state.
+
+### 4. Handle client-only libraries (e.g., Plotly)
+
+Libraries that depend on `window` or browser-only APIs must use `next/dynamic`:
+
+```tsx
+// web/components/PlotlyChart.tsx
+"use client";
+
+import dynamic from "next/dynamic";
+
+const Plot = dynamic(() => import("react-plotly.js"), { ssr: false });
+
+export default function PlotlyChart({ data, layout, config }) {
+ return ;
+}
+```
+
+Then import `PlotlyChart` instead of importing `react-plotly.js` directly.
+
+### 5. Migrate data and types
+
+```bash
+# Move data files
+mv app/src/data/*.json web/data/
+
+# Move types
+mv app/src/types.d.ts web/types.d.ts
+
+# Move theme config if present
+mv app/src/theme.ts web/theme.ts
+```
+
+**Preserve TypeScript module augmentations** in `web/types.d.ts`:
+- Keep declarations for `@policyengine/ui-kit/legacy/*`
+- Keep any ambient module declarations for third-party packages
+
+### 6. Update CSS imports
+
+Create `web/app/globals.css`:
+
+```css
+@import "tailwindcss";
+@import "@policyengine/ui-kit/theme.css";
+
+/* Import third-party styles if needed */
+@import "@mantine/core/styles.css";
+@import "katex/dist/katex.min.css";
+```
+
+**Import order matters:** `tailwindcss` must come first, then ui-kit theme.
+
+### 7. Create PostCSS config
+
+```js
+// web/postcss.config.mjs
+export default {
+ plugins: {
+ "@tailwindcss/postcss": {},
+ },
+};
+```
+
+### 8. Create Next.js config
+
+```ts
+// web/next.config.ts
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ transpilePackages: ["@policyengine/ui-kit"],
+ // Add other config as needed
+};
+
+export default nextConfig;
+```
+
+### 9. Update package.json
+
+```json
+{
+ "name": "tool-name",
+ "private": true,
+ "scripts": {
+ "dev": "next dev --turbopack",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint",
+ "test": "vitest"
+ },
+ "dependencies": {
+ "next": "^16.0.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "@policyengine/ui-kit": "^0.9.0",
+ // ... other deps
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "^4.0.0",
+ "postcss": "^8.4.47",
+ "typescript": "^5.6.3",
+ "eslint": "^9.17.0",
+ "eslint-config-next": "^16.0.0",
+ "vitest": "latest",
+ // ... other dev deps
+ }
+}
+```
+
+### 10. Update tsconfig.json
+
+```json
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [{ "name": "next" }],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
+```
+
+### 11. Update environment variables
+
+**From:** `import.meta.env.VITE_*`
+**To:** `process.env.NEXT_PUBLIC_*`
+
+Replace all instances:
+```tsx
+// Before (Vite)
+const apiKey = import.meta.env.VITE_API_KEY;
+
+// After (Next.js)
+const apiKey = process.env.NEXT_PUBLIC_API_KEY;
+```
+
+Update `.env` file variable names accordingly.
+
+### 12. Update Vercel config
+
+For subdirectory deployments (Python + frontend monorepo):
+
+```json
+{
+ "framework": "nextjs",
+ "buildCommand": "cd web && bun run build",
+ "outputDirectory": "web/.next",
+ "installCommand": "cd web && bun install",
+ "devCommand": "cd web && bun run dev"
+}
+```
+
+For frontend-only repos (frontend at root):
+
+```json
+{
+ "framework": "nextjs"
+}
+```
+
+**IMPORTANT:** Do NOT set Vercel's "Root Directory" in the dashboard if you're using `vercel.json` commands. The two configs conflict and break framework detection.
+
+### 13. Update Makefile (if present)
+
+```makefile
+install:
+ cd web && bun install
+
+dev:
+ cd web && bun run dev
+
+build:
+ cd web && bun run build
+```
+
+### 14. Update .gitignore
+
+```
+# Dependencies
+node_modules/
+
+# Next.js
+web/.next/
+web/out/
+web/.turbo
+
+# Build artifacts
+*.tsbuildinfo
+
+# Keep lockfile
+!web/bun.lock
+```
+
+**IMPORTANT:** Commit `bun.lock`. PolicyEngine CI runs `bun install --frozen-lockfile`, which fails without a committed lockfile.
+
+### 15. Update CI workflows
+
+```yaml
+# .github/workflows/pr.yaml
+jobs:
+ lint-web:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v2
+ - run: cd web && bun install --frozen-lockfile
+ - run: cd web && bun run lint
+
+ build-web:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v2
+ - run: cd web && bun install --frozen-lockfile
+ - run: cd web && bun run build
+
+ test-web:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v2
+ - run: cd web && bun install --frozen-lockfile
+ - run: cd web && bun run test
+```
+
+### 16. Add smoke tests
+
+```tsx
+// web/__tests__/smoke.test.tsx
+import { describe, it, expect } from "vitest";
+import snapResults from "@/data/snap_results.json";
+
+describe("Data integrity", () => {
+ it("snap_results has expected structure", () => {
+ expect(Array.isArray(snapResults)).toBe(true);
+ expect(snapResults.length).toBeGreaterThan(0);
+ expect(snapResults[0]).toHaveProperty("income");
+ });
+});
+```
+
+```ts
+// web/vitest.config.ts
+import { defineConfig } from "vitest/config";
+import path from "path";
+
+export default defineConfig({
+ test: {
+ environment: "jsdom",
+ },
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./"),
+ },
+ },
+});
+```
+
+### 17. Clean up old Vite files
+
+```bash
+rm -rf app/dist
+rm app/vite.config.ts
+rm app/index.html
+rm app/bun.lock
+```
+
+## Common Gotchas
+
+### 1. Client-only dependencies fail at build time
+
+**Symptom:** `ReferenceError: window is not defined` during `bun run build`
+
+**Fix:** Use `next/dynamic` with `{ ssr: false }` for browser-only libraries:
+```tsx
+const Plot = dynamic(() => import("react-plotly.js"), { ssr: false });
+```
+
+### 2. Legacy ui-kit imports break
+
+**Symptom:** Type errors on `@policyengine/ui-kit/legacy/charts`
+
+**Fix:** Keep type augmentations in `web/types.d.ts`:
+```ts
+declare module "@policyengine/ui-kit/legacy/charts" {
+ export const chartColors: string[];
+}
+```
+
+### 3. CI fails with "lockfile out of sync"
+
+**Symptom:** `bun install --frozen-lockfile` fails in CI
+
+**Fix:** Commit `web/bun.lock` and remove it from `.gitignore`.
+
+### 4. Vercel build fails with "No framework detected"
+
+**Symptom:** Vercel can't find Next.js even though `next.config.ts` exists
+
+**Fix:** Either:
+- Use `vercel.json` with explicit commands (shown above)
+- OR set Vercel dashboard "Root Directory" to `web/` (don't mix both approaches)
+
+### 5. Styles don't load
+
+**Symptom:** Unstyled page or missing Tailwind utilities
+
+**Fix:** Verify `web/app/globals.css` has:
+```css
+@import "tailwindcss";
+@import "@policyengine/ui-kit/theme.css";
+```
+
+And that `globals.css` is imported in `layout.tsx`:
+```tsx
+import "./globals.css";
+```
+
+## Testing Checklist
+
+Before submitting PR:
+
+- [ ] `bun install --frozen-lockfile` succeeds
+- [ ] `bun run build` compiles cleanly with Turbopack
+- [ ] `bun run lint` reports 0 problems
+- [ ] `bun run test` passes
+- [ ] CI green (all jobs pass)
+- [ ] Vercel preview deploys and renders correctly
+- [ ] Client-only components (charts, etc.) load properly
+- [ ] Metadata (title, description, favicon) correct
+- [ ] Analytics tracking works (GA4, custom events)
+- [ ] Structured data (JSON-LD) present in page source
+
+## Related Skills
+
+- `policyengine-ui-kit-consumer-skill` — CSS setup and Tailwind v4 configuration
+- `policyengine-interactive-tools-skill` — Embedding, hash sync, country detection patterns
+- `policyengine-vercel-deployment-skill` — Deployment and environment configuration