diff --git a/.claude/launch.json b/.claude/launch.json
new file mode 100644
index 0000000000..54bf2386f9
--- /dev/null
+++ b/.claude/launch.json
@@ -0,0 +1,12 @@
+{
+ "version": "0.0.1",
+ "configurations": [
+ {
+ "name": "mapgen-studio",
+ "runtimeExecutable": "bun",
+ "runtimeArgs": ["run", "dev"],
+ "cwd": "apps/mapgen-studio",
+ "port": 5173
+ }
+ ]
+}
diff --git a/apps/mapgen-studio/.interface-design/system.md b/apps/mapgen-studio/.interface-design/system.md
new file mode 100644
index 0000000000..b74068fc5d
--- /dev/null
+++ b/apps/mapgen-studio/.interface-design/system.md
@@ -0,0 +1,149 @@
+# MapGen Studio — Design System (decisions)
+
+> **This file holds DECISIONS, not extraction.** The as-built baseline lives in
+> [`../system.md`](../system.md). When they disagree, this file wins — it is the
+> target the redesign realizes. Confirmed with the user 2026-06-08. Downstream
+> slices and agents APPLY this; they do not re-litigate it.
+>
+> Realized + value-tuned in the P1 implementation slice (Tailwind v4 + shadcn).
+> Starting token values below are the committed intent; exact final values are
+> validated against the running app during P1.
+
+## Direction
+
+**Cartographer's instrument.** A dense, dark-first precision tool for *sculpting a
+world by adjusting generation parameters and reading the map*. The deck.gl map is
+the hero and carries all terrain color; the chrome is a recessed graphite
+instrument bezel that never competes with it. **The instrument makes way for the
+matter:** it lives in its own robust substrate layers but never steals the show.
+Keep the existing DNA — dense, border-tiered, micro-typography, a single restrained
+cool accent — but make every choice intentional.
+
+**Accent decision (user-confirmed, refined 2026-06-08):** the accent is an
+**elevated cool-steel slate**, not a vivid hue. Teal/cyan was considered and set
+aside *because the map is the matter* — a saturated accent would pull the eye to
+the chrome. The commodity tell today is not "slate" itself; it is that slate is
+*unintentional* (declared indigo, used gray, no system). The fix is to make slate
+**deliberate and robust**: a committed cool-steel hue with a defined ramp, a
+rigorous substrate-elevation system, and a contour signature carried by
+**luminance** rather than color. Craft, not chroma.
+
+- **Who:** a Civ7 map-mod author in long, dark, tweak-and-observe sessions.
+- **Verb:** tune a recipe → watch the world respond → run it in-game.
+- **Feel:** quiet, technical, precise. Like a DAW / topographic survey console.
+- **Light mode:** dark-first, but ship a genuinely correct light theme via
+ shadcn's single `.dark` class strategy (one switch, no prop drilling).
+
+## Signature
+
+The **contour line**, carried by **luminance, not a loud hue**. The map domain's
+defining mark (topographic contour / graticule) becomes the interface's signature:
+
+- **Focus** renders as a 1px *contour ring* in a brighter cool-steel slate — a
+ luminance step above the chrome, not a saturated glow block.
+- **Active/selected** states use a thin steel rule/underline (a contour), not a
+ filled slab — fills are reserved for the one primary action, itself a restrained
+ elevated slate.
+- **Hypsometric mini-legends:** elevation/rainfall/biome readouts use real
+ cartographic ramps (relief greens→tans→snow; bathymetric blues) so data viz
+ belongs to the product's world. This is the one place real color appears in the
+ chrome — and it appears as *the matter*, the same domain color the map carries,
+ not as instrument identity.
+
+## Primary craft lever (from the live app, 2026-06-08)
+
+A screenshot of the running app revealed the real departure is **not color** — it
+is **flatness**. Page, panel, and nested surfaces sit ~4% lightness apart with no
+floating-layer shadows, so borders carry all structure and the UI reads as one
+hollow dark plane (the squint test shows almost no hierarchy). The empty map
+center reads as *dead space*, not *a stage awaiting matter*.
+
+So P1's highest-value work, in order:
+1. **Robust substrate elevation** — widen the (still whisper-quiet) page→panel→
+ nested lightness tiers + a real border progression so structure is *felt*. This
+ is what makes a neutral slate instrument feel machined instead of flat.
+2. Elevate the slate as quiet identity (committed steel hue, contour-luminance focus).
+3. Make "awaiting matter" intentional — the empty stage frames the map
+ (graticule/contour), reads as ready, not hollow.
+4. Under the hood: named tokens, real overlay motion, `.dark`-class theming repair.
+
+## Foundation & depth
+
+- **Foundation:** cool graphite/ink, near-monochrome. Same hue across surfaces;
+ hierarchy by lightness step only (never different hues per surface).
+- **Depth:** **borders-only**, tiered by background lightness step + 1px hairline.
+ This substrate-elevation system is where the craft lives — the layering must be
+ *felt, not seen* (whisper-quiet lightness steps; the squint test must still show
+ hierarchy with nothing jumping out). A robust tier system is what makes a neutral
+ slate read as intentional rather than flat. Shadows appear ONLY on floating
+ layers (dialog/popover/toast). `backdrop-blur` on chrome that floats over the
+ map. Do not mix depth strategies.
+- **Accent:** ONE elevated cool-steel slate, used with restraint (primary action +
+ active/focus contours). It replaces the commodity declared-indigo-but-used-gray
+ non-choice by being *committed and systematic*, not by being louder.
+
+## Tokens (HSL, shadcn `:root` / `.dark`)
+
+Dark is the default app theme. Values are the committed starting point.
+
+| Token | Dark | Light | Role |
+|---|---|---|---|
+| `--background` | `240 20% 5%` | `240 20% 96%` | page / deck.gl backdrop tint |
+| `--foreground` | `240 14% 92%` | `215 28% 17%` | primary text |
+| `--card` | `240 9% 9%` | `0 0% 100%` | panels, headers |
+| `--card-foreground` | `240 14% 92%` | `215 28% 17%` | |
+| `--popover` | `240 8% 11%` | `0 0% 100%` | one tier above card (dropdowns) |
+| `--popover-foreground` | `240 14% 92%` | `215 28% 17%` | |
+| `--muted` | `240 8% 13%` | `240 14% 93%` | hover/active surface, tags |
+| `--muted-foreground` | `240 6% 56%` | `220 9% 46%` | secondary/label text |
+| `--primary` | `216 18% 42%` | `216 22% 32%` | elevated cool-steel slate accent |
+| `--primary-foreground` | `210 20% 96%` | `0 0% 100%` | text on primary |
+| `--border` | `240 9% 18%` | `220 13% 91%` | workhorse hairline |
+| `--input` | `240 9% 25%` | `220 13% 86%` | control border |
+| `--ring` | `212 28% 60%` | `216 25% 45%` | focus contour (luminance step, not chroma) |
+| `--destructive` | `350 70% 58%` | `350 72% 48%` | rose (commit; kill rose/red drift) |
+| `--destructive-foreground` | `0 0% 98%` | `0 0% 100%` | |
+| `--radius` | `0.25rem` | — | base; card = `0.5rem` |
+
+Secondary text tiers: a tertiary/muted level (`240 7% 38%` dark) for placeholders/
+help. Semantic (used sparingly, slightly desaturated in dark): success
+`160 55% 45%`, warning `40 80% 55%`, info = the steel-slate accent (no separate
+info hue — keep the chrome monochrome-cool).
+
+## Type
+
+- **Sans:** Inter. **Mono:** JetBrains Mono (tabular numerals for data). Self-host
+ (remove render-blocking Google Fonts `@import`).
+- **Named scale (replace ad-hoc `text-[11px]`):** `text-data` 11px / `text-label`
+ 10px uppercase `tracking-wider` / `text-xs` 12px / `text-sm` 14px (dialog
+ titles). Weights: medium (500) default emphasis, semibold (600) headers. No bold
+ in chrome. 11px is the body default; 10px the eyebrow label.
+
+## Spacing / radius / motion
+
+- **Spacing base 4px.** Scale: 4, 6, 8, 10, 12, 16. Field rows `py-1`, label↔input
+ `gap-3`. Cards `p-2.5`. Nested `p-2`.
+- **Radius:** 4px inputs/buttons/tags, 8px cards/dialogs/dropdowns, full for pills/
+ switch. (Drop the 6px/`xl`/`2xl` noise.)
+- **Motion:** real, quiet, deceleration easing — install the Tailwind v4 animate
+ layer so the (currently dead) `animate-in` overlay transitions actually run.
+ 150ms fast / 200ms normal. No spring/bounce. `animate-pulse-subtle` stays for
+ running state.
+
+## Theming mechanism (the repair)
+
+- Single **`.dark` class** on `` (shadcn strategy). Set Tailwind
+ `darkMode: 'class'` (v4 equivalent). One theme switch writes the class.
+- **Delete** `createTheme()` runtime string interpolation (JIT-invisible — a real
+ bug), the `lightMode` boolean prop threaded through ~24 files, the duplicate
+ `colors.light|dark` config, and the dead `--spacing-*`/`--radius-*` vars. The
+ `lightMode`-prop removal lands as components migrate (P3/P4); P1 establishes the
+ `.dark` system and tokens so both can briefly coexist without conflict.
+
+## Component patterns (preserve as-built dimensions, re-skin onto shadcn)
+
+Keep the dense dimensions from the extraction baseline — Button `h-8`/`h-7`, Input
+`h-7`, Switch 36×20, Field row `justify-between gap-3 py-1` with `min-w-[96px]`
+label, Card `rounded-lg border p-2.5`, eyebrow label uppercase 10px. The migration
+changes the *mechanism* (Radix shadcn primitives, token-driven) and adds the
+contour-focus signature + motion — not the density.
diff --git a/apps/mapgen-studio/components.json b/apps/mapgen-studio/components.json
new file mode 100644
index 0000000000..622679107e
--- /dev/null
+++ b/apps/mapgen-studio/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/index.css",
+ "baseColor": "slate",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
diff --git a/apps/mapgen-studio/index.html b/apps/mapgen-studio/index.html
index fd4591b925..1a1a5ae9b0 100644
--- a/apps/mapgen-studio/index.html
+++ b/apps/mapgen-studio/index.html
@@ -4,16 +4,31 @@
MapGen Studio
+
diff --git a/apps/mapgen-studio/package.json b/apps/mapgen-studio/package.json
index c704888489..605341481c 100644
--- a/apps/mapgen-studio/package.json
+++ b/apps/mapgen-studio/package.json
@@ -21,6 +21,8 @@
"@civ7/plugin-mods": "workspace:*",
"@civ7/studio-server": "workspace:*",
"@deck.gl/core": "^9.0.43",
+ "@fontsource/inter": "^5.1.1",
+ "@fontsource/jetbrains-mono": "^5.1.1",
"@deck.gl/layers": "^9.0.43",
"@deck.gl/react": "^9.0.43",
"@orpc/client": "^1.14.4",
@@ -44,12 +46,12 @@
"typebox": "^1.0.80"
},
"devDependencies": {
+ "@tailwindcss/vite": "^4.1.13",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
- "autoprefixer": "latest",
- "postcss": "latest",
- "tailwindcss": "3.4.17",
+ "tailwindcss": "^4.1.13",
+ "tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"vite": "^7.3.1"
}
diff --git a/apps/mapgen-studio/postcss.config.js b/apps/mapgen-studio/postcss.config.js
deleted file mode 100644
index 2aa7205d4b..0000000000
--- a/apps/mapgen-studio/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-};
diff --git a/apps/mapgen-studio/src/index.css b/apps/mapgen-studio/src/index.css
index 422c21e9d5..07a1419b7b 100644
--- a/apps/mapgen-studio/src/index.css
+++ b/apps/mapgen-studio/src/index.css
@@ -1,138 +1,210 @@
-
/* ============================================================================
- Font Imports
- ============================================================================ */
-@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap");
-
-/* ============================================================================
- Tailwind Base
- ============================================================================ */
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-/* ============================================================================
- CSS Variables (Theme Tokens)
+ MapGen Studio — design system foundation (Tailwind v4, CSS-first)
+ Direction + token rationale: apps/mapgen-studio/.interface-design/system.md
+ A dense, dark-first "cartographer's instrument": graphite substrate, one
+ elevated cool-steel slate accent, a felt elevation hierarchy, and a
+ contour-line focus signature carried by luminance, not chroma.
============================================================================ */
+@import "tailwindcss";
+@import "tw-animate-css";
+
+/* Dark is selected by a single `.dark` class on (set pre-paint in
+ index.html). No prefers-color-scheme coupling, no per-component prop. */
+@custom-variant dark (&:is(.dark *));
+
+/* ----------------------------------------------------------------------------
+ Token → utility mapping. `inline` keeps the var reference in generated CSS so
+ flipping `.dark` re-themes at runtime. Custom tokens (border-subtle/-strong,
+ input-background, elevation-*, surface-sunken, success, warning) carry the
+ substrate-elevation craft the stock shadcn set doesn't express.
+ ---------------------------------------------------------------------------- */
+@theme inline {
+ --color-background: hsl(var(--background));
+ --color-foreground: hsl(var(--foreground));
+ --color-card: hsl(var(--card));
+ --color-card-foreground: hsl(var(--card-foreground));
+ --color-popover: hsl(var(--popover));
+ --color-popover-foreground: hsl(var(--popover-foreground));
+ --color-primary: hsl(var(--primary));
+ --color-primary-foreground: hsl(var(--primary-foreground));
+ --color-secondary: hsl(var(--secondary));
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
+ --color-muted: hsl(var(--muted));
+ --color-muted-foreground: hsl(var(--muted-foreground));
+ --color-accent: hsl(var(--accent));
+ --color-accent-foreground: hsl(var(--accent-foreground));
+ --color-destructive: hsl(var(--destructive));
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
+ --color-border: hsl(var(--border));
+ --color-input: hsl(var(--input));
+ --color-ring: hsl(var(--ring));
+
+ /* substrate elevation + semantics (the primary craft lever) */
+ --color-border-subtle: hsl(var(--border-subtle));
+ --color-border-strong: hsl(var(--border-strong));
+ --color-input-background: hsl(var(--input-background));
+ --color-elevation-1: hsl(var(--elevation-1));
+ --color-elevation-2: hsl(var(--elevation-2));
+ --color-surface-sunken: hsl(var(--surface-sunken));
+ --color-success: hsl(var(--success));
+ --color-warning: hsl(var(--warning));
+
+ --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
+ --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", monospace;
+
+ --radius-sm: 0.25rem; /* 4px — inputs, buttons, tags (the default) */
+ --radius-md: 0.375rem; /* 6px — rare */
+ --radius-lg: 0.5rem; /* 8px — cards, dialogs, dropdowns */
+ --radius-xl: 0.75rem;
+
+ --animate-pulse-subtle: pulse-subtle 2s ease-in-out infinite;
+}
+
+/* ----------------------------------------------------------------------------
+ Tokens. Values are HSL channels (H S% L%). Dark is the product default; the
+ ladder below is intentionally a felt hierarchy — page → panel → floating each
+ occupy a distinct (still quiet) lightness tier so structure survives the
+ squint test instead of reading as one hollow plane.
+ ---------------------------------------------------------------------------- */
:root {
- /* Light mode colors */
- --color-bg-primary: #f5f5f7;
- --color-bg-secondary: #ffffff;
- --color-bg-tertiary: #f0f0f2;
-
- --color-text-primary: #1f2933;
- --color-text-secondary: #6b7280;
- --color-text-muted: #9ca3af;
-
- --color-border-primary: #e5e7eb;
- --color-border-secondary: #d1d5db;
-
- --color-accent: #4b5563;
- --color-accent-hover: #374151;
-
- /* Spacing */
- --spacing-xs: 4px;
- --spacing-sm: 8px;
- --spacing-md: 16px;
- --spacing-lg: 24px;
- --spacing-xl: 32px;
-
- /* Border radius */
- --radius-sm: 4px;
- --radius-md: 6px;
- --radius-lg: 8px;
- --radius-xl: 12px;
-
- /* Transitions */
- --transition-fast: 150ms ease;
- --transition-normal: 200ms ease;
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --color-bg-primary: #0a0a12;
- --color-bg-secondary: #16161d;
- --color-bg-tertiary: #1f1f28;
-
- --color-text-primary: #e2e2e9;
- --color-text-secondary: #7a7a8c;
- --color-text-muted: #6a6a7c;
-
- --color-border-primary: #26262e;
- --color-border-secondary: #32323e;
-
- --color-accent: #5e5ce6;
- --color-accent-hover: #7472f7;
+ color-scheme: light;
+
+ --background: 240 20% 97%;
+ --foreground: 215 25% 17%;
+ --card: 0 0% 100%;
+ --card-foreground: 215 25% 17%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 215 25% 17%;
+ --primary: 216 22% 32%;
+ --primary-foreground: 0 0% 100%;
+ --secondary: 240 14% 94%;
+ --secondary-foreground: 215 25% 22%;
+ --muted: 240 14% 94%;
+ --muted-foreground: 220 9% 46%;
+ --accent: 240 14% 92%;
+ --accent-foreground: 215 25% 17%;
+ --destructive: 350 72% 48%;
+ --destructive-foreground: 0 0% 100%;
+ --border: 220 13% 88%;
+ --input: 220 13% 84%;
+ --ring: 216 25% 45%;
+
+ --border-subtle: 220 14% 92%;
+ --border-strong: 220 13% 78%;
+ --input-background: 0 0% 100%;
+ --elevation-1: 0 0% 100%;
+ --elevation-2: 0 0% 100%;
+ --surface-sunken: 240 16% 95%;
+ --success: 160 55% 36%;
+ --warning: 38 80% 42%;
+
+ --radius: 0.25rem;
+
+ /* legacy vars still referenced by scrollbar/select below — repointed to tokens */
+ --color-border-secondary: hsl(var(--border-strong));
+ --color-text-muted: hsl(var(--muted-foreground));
+}
+
+.dark {
+ color-scheme: dark;
+
+ --background: 240 14% 6%; /* page — the deck.gl canvas backdrop */
+ --foreground: 240 13% 92%;
+ --card: 240 9% 10%; /* panel — sits a felt step above the page */
+ --card-foreground: 240 13% 92%;
+ --popover: 240 9% 13%; /* floating layers — a step above panels */
+ --popover-foreground: 240 13% 92%;
+ --primary: 216 18% 44%; /* elevated cool-steel slate — the one accent */
+ --primary-foreground: 210 20% 96%;
+ --secondary: 240 7% 15%;
+ --secondary-foreground: 240 12% 88%;
+ --muted: 240 7% 15%;
+ --muted-foreground: 240 5% 58%;
+ --accent: 240 8% 17%; /* ghost/hover surface */
+ --accent-foreground: 240 13% 92%;
+ --destructive: 350 70% 58%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 8% 22%; /* findable hairline against panels */
+ --input: 240 9% 26%;
+ --ring: 212 28% 62%; /* focus contour — a luminance step, not a hue */
+
+ --border-subtle: 240 8% 15%; /* dividers within a surface */
+ --border-strong: 240 9% 32%; /* emphasis / control focus border */
+ --input-background: 240 12% 8%; /* inset: darker than its panel */
+ --elevation-1: 240 9% 10%; /* = panel */
+ --elevation-2: 240 9% 13%; /* = floating / raised-nested */
+ --surface-sunken: 240 12% 7%; /* nested cards: between page and panel */
+ --success: 160 50% 46%;
+ --warning: 40 80% 56%;
+}
+
+/* ----------------------------------------------------------------------------
+ Base
+ ---------------------------------------------------------------------------- */
+@layer base {
+ * {
+ border-color: hsl(var(--border));
}
-}
-/* ============================================================================
- Base Styles
- ============================================================================ */
-html {
- font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
+ html {
+ font-family: var(--font-sans);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
-body {
- margin: 0;
- padding: 0;
- min-height: 100vh;
+ body {
+ margin: 0;
+ padding: 0;
+ min-height: 100vh;
+ background-color: hsl(var(--background));
+ color: hsl(var(--foreground));
+ }
+
+ /* Signature: focus is a 1px luminous contour ring (the ring token), offset
+ so it reads as a drawn contour rather than a filled glow. */
+ *:focus {
+ outline: none;
+ }
+ *:focus-visible {
+ outline: 1px solid hsl(var(--ring));
+ outline-offset: 2px;
+ }
}
-/* Mono font for code/data */
.font-mono {
- font-family: "JetBrains Mono", "SF Mono", "Fira Code", monospace;
+ font-family: var(--font-mono);
}
-/* ============================================================================
- Custom Scrollbar
- ============================================================================ */
+/* ----------------------------------------------------------------------------
+ Custom scrollbar — quiet by default, findable on hover.
+ ---------------------------------------------------------------------------- */
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: var(--color-border-secondary) transparent;
}
-
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
-
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
border-radius: 3px;
}
-
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: var(--color-border-secondary);
border-radius: 3px;
border: 1px solid transparent;
}
-
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: var(--color-text-muted);
}
-
.custom-scrollbar:not(:hover)::-webkit-scrollbar-thumb {
background-color: transparent;
}
-/* ============================================================================
- Focus Styles
- ============================================================================ */
-*:focus {
- outline: none;
-}
-
-*:focus-visible {
- outline: 2px solid var(--color-accent);
- outline-offset: 2px;
-}
-
-/* ============================================================================
- Form Element Resets
- ============================================================================ */
+/* ----------------------------------------------------------------------------
+ Form element resets (kept from the as-built baseline)
+ ---------------------------------------------------------------------------- */
select {
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
@@ -141,23 +213,28 @@ select {
background-size: 1.25em 1.25em;
padding-right: 2rem;
}
-
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
-
input[type="number"] {
-moz-appearance: textfield;
}
-/* ============================================================================
- Utility Classes
- ============================================================================ */
+button {
+ cursor: pointer;
+}
+button:disabled,
+select:disabled,
+input:disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
@supports not (backdrop-filter: blur(8px)) {
.backdrop-blur-sm {
- background-color: rgba(255, 255, 255, 0.95);
+ background-color: hsl(var(--popover) / 0.95);
}
}
@@ -170,36 +247,6 @@ input[type="number"] {
opacity: 0.7;
}
}
-
.animate-pulse-subtle {
animation: pulse-subtle 2s ease-in-out infinite;
}
-
-.transition-colors {
- transition-property: color, background-color, border-color;
- transition-timing-function: ease;
- transition-duration: 150ms;
-}
-
-/* ============================================================================
- Component-specific Overrides
- ============================================================================ */
-input[type="checkbox"] {
- cursor: pointer;
-}
-
-button {
- cursor: pointer;
- border: none;
- background: none;
- padding: 0;
- font: inherit;
-}
-
-button:disabled,
-select:disabled,
-input:disabled {
- cursor: not-allowed;
- opacity: 0.5;
-}
-
diff --git a/apps/mapgen-studio/src/lib/utils.ts b/apps/mapgen-studio/src/lib/utils.ts
new file mode 100644
index 0000000000..59b04332e5
--- /dev/null
+++ b/apps/mapgen-studio/src/lib/utils.ts
@@ -0,0 +1,11 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+/**
+ * Merge class names with Tailwind-aware conflict resolution.
+ * The canonical shadcn helper: `clsx` resolves conditionals, `twMerge` ensures
+ * the last conflicting Tailwind utility wins (e.g. `px-2 px-3` → `px-3`).
+ */
+export function cn(...inputs: ClassValue[]): string {
+ return twMerge(clsx(inputs));
+}
diff --git a/apps/mapgen-studio/src/main.tsx b/apps/mapgen-studio/src/main.tsx
index 0ab49baf08..6c4134ab2f 100644
--- a/apps/mapgen-studio/src/main.tsx
+++ b/apps/mapgen-studio/src/main.tsx
@@ -1,3 +1,10 @@
+// Self-hosted fonts (replaces the render-blocking Google Fonts @import).
+import "@fontsource/inter/400.css";
+import "@fontsource/inter/500.css";
+import "@fontsource/inter/600.css";
+import "@fontsource/inter/700.css";
+import "@fontsource/jetbrains-mono/400.css";
+import "@fontsource/jetbrains-mono/500.css";
import "./index.css";
import { StrictMode } from "react";
diff --git a/apps/mapgen-studio/tailwind.config.js b/apps/mapgen-studio/tailwind.config.js
deleted file mode 100644
index 13e0624026..0000000000
--- a/apps/mapgen-studio/tailwind.config.js
+++ /dev/null
@@ -1,71 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-export default {
- content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
- theme: {
- extend: {
- fontFamily: {
- sans: ["Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", "sans-serif"],
- mono: ["JetBrains Mono", "SF Mono", "Fira Code", "monospace"],
- },
- colors: {
- light: {
- bg: {
- primary: "#f5f5f7",
- secondary: "#ffffff",
- tertiary: "#f0f0f2",
- },
- text: {
- primary: "#1f2933",
- secondary: "#6b7280",
- muted: "#9ca3af",
- },
- border: {
- primary: "#e5e7eb",
- secondary: "#d1d5db",
- },
- accent: "#4b5563",
- },
- dark: {
- bg: {
- primary: "#0a0a12",
- secondary: "#16161d",
- tertiary: "#1f1f28",
- },
- text: {
- primary: "#e2e2e9",
- secondary: "#7a7a8c",
- muted: "#6a6a7c",
- },
- border: {
- primary: "#26262e",
- secondary: "#32323e",
- },
- accent: "#5e5ce6",
- },
- },
- spacing: {
- 18: "4.5rem",
- 88: "22rem",
- },
- borderRadius: {
- xl: "0.75rem",
- "2xl": "1rem",
- },
- transitionDuration: {
- 150: "150ms",
- 200: "200ms",
- },
- animation: {
- "pulse-subtle": "pulse-subtle 2s ease-in-out infinite",
- },
- keyframes: {
- "pulse-subtle": {
- "0%, 100%": { opacity: "1" },
- "50%": { opacity: "0.7" },
- },
- },
- },
- },
- plugins: [],
-};
-
diff --git a/apps/mapgen-studio/tsconfig.json b/apps/mapgen-studio/tsconfig.json
index a36976f7b4..287b841e6a 100644
--- a/apps/mapgen-studio/tsconfig.json
+++ b/apps/mapgen-studio/tsconfig.json
@@ -7,6 +7,10 @@
"skipLibCheck": true,
"moduleResolution": "bundler",
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ },
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
diff --git a/apps/mapgen-studio/vite.config.ts b/apps/mapgen-studio/vite.config.ts
index 8ff4a44da9..0f67e9fb29 100644
--- a/apps/mapgen-studio/vite.config.ts
+++ b/apps/mapgen-studio/vite.config.ts
@@ -1,5 +1,6 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
+import tailwindcss from "@tailwindcss/vite";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
@@ -1144,6 +1145,7 @@ function createStudioServerContextForApp(viteCommand: string): StudioServerConte
export default defineConfig(({ command }) => ({
plugins: [
react(),
+ tailwindcss(),
{
name: "repo-backed-map-configs",
configureServer(server) {
diff --git a/bun.lock b/bun.lock
index 321ba391af..bacf88cfa8 100644
--- a/bun.lock
+++ b/bun.lock
@@ -43,6 +43,8 @@
"@deck.gl/core": "^9.0.43",
"@deck.gl/layers": "^9.0.43",
"@deck.gl/react": "^9.0.43",
+ "@fontsource/inter": "^5.1.1",
+ "@fontsource/jetbrains-mono": "^5.1.1",
"@orpc/client": "^1.14.4",
"@orpc/contract": "^1.14.4",
"@orpc/server": "^1.14.4",
@@ -64,12 +66,12 @@
"typebox": "^1.0.80",
},
"devDependencies": {
+ "@tailwindcss/vite": "^4.1.13",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
- "autoprefixer": "latest",
- "postcss": "latest",
- "tailwindcss": "3.4.17",
+ "tailwindcss": "^4.1.13",
+ "tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"vite": "^7.3.1",
},
@@ -622,6 +624,10 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
+ "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="],
+
+ "@fontsource/jetbrains-mono": ["@fontsource/jetbrains-mono@5.2.8", "", {}, "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ=="],
+
"@hpcc-js/wasm": ["@hpcc-js/wasm@2.32.3", "", { "dependencies": { "yargs": "18.0.0" }, "bin": { "dot-wasm": "deprecated/dot-wasm.js" } }, "sha512-VyqR+nDpasbbCQceDTL7ioTSSi4AGxR9yrP+rFemvvC+fb6PYC20Xre9pDpM1QAiINP9ZUS15Z/c0hudlsB0+A=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
@@ -1114,6 +1120,36 @@
"@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="],
+ "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="],
+
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="],
+
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="],
+
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="],
+
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="],
+
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="],
+
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="],
+
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="],
+
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="],
+
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="],
+
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="],
+
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="],
+
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="],
+
+ "@tailwindcss/vite": ["@tailwindcss/vite@4.3.0", "", { "dependencies": { "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "tailwindcss": "4.3.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw=="],
+
"@tanstack/query-core": ["@tanstack/query-core@5.101.0", "", {}, "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow=="],
"@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="],
@@ -1304,8 +1340,6 @@
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
- "autoprefixer": ["autoprefixer@10.5.0", "", { "dependencies": { "browserslist": "^4.28.2", "caniuse-lite": "^1.0.30001787", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong=="],
-
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"avsc": ["avsc@5.7.9", "", {}, "sha512-yOA4wFeI7ET3v32Di/sUybQ+ttP20JHSW3mxLuNGeO0uD6PPcvLrIQXSvy/rhJOWU5JrYh7U4OHplWMmtAtjMg=="],
@@ -1334,7 +1368,7 @@
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
- "baseline-browser-mapping": ["baseline-browser-mapping@2.10.32", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg=="],
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.9.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA=="],
"basic-ftp": ["basic-ftp@5.1.0", "", {}, "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw=="],
@@ -1352,7 +1386,7 @@
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
- "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
+ "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
@@ -1384,7 +1418,7 @@
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
- "caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="],
+ "caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="],
"capital-case": ["capital-case@1.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", "upper-case-first": "^2.0.2" } }, "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A=="],
@@ -1594,7 +1628,7 @@
"ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
- "electron-to-chromium": ["electron-to-chromium@1.5.361", "", {}, "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA=="],
+ "electron-to-chromium": ["electron-to-chromium@1.5.278", "", {}, "sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -1606,6 +1640,8 @@
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
+ "enhanced-resolve": ["enhanced-resolve@5.23.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA=="],
+
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
@@ -1762,8 +1798,6 @@
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
- "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
-
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
"front-matter": ["front-matter@4.0.2", "", { "dependencies": { "js-yaml": "^3.13.1" } }, "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg=="],
@@ -2098,6 +2132,30 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+ "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
+
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
@@ -2336,7 +2394,7 @@
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
- "node-releases": ["node-releases@2.0.46", "", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="],
+ "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
"normalize-package-data": ["normalize-package-data@6.0.2", "", { "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g=="],
@@ -2790,7 +2848,9 @@
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
- "tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="],
+ "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="],
+
+ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"tar": ["tar@6.1.15", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A=="],
@@ -2866,6 +2926,8 @@
"turbo-windows-arm64": ["turbo-windows-arm64@2.7.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-0wDVnUJLFAWm4ZzOQFDkbyyUqaszorTGf3Rdc22IRIyJTTLd6ajqdb+cWD89UZ1RKr953+PZR1gqgWQY4PDuhA=="],
+ "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
+
"tweezer.js": ["tweezer.js@1.5.0", "", {}, "sha512-aSiJz7rGWNAQq7hjMK9ZYDuEawXupcCWgl3woQQSoDP2Oh8O4srWb/uO1PzzHIsrPEOqrjJ2sUb9FERfzuBabQ=="],
"twoslash": ["twoslash@0.3.6", "", { "dependencies": { "@typescript/vfs": "^1.6.2", "twoslash-protocol": "0.3.6" }, "peerDependencies": { "typescript": "^5.5.0" } }, "sha512-VuI5OKl+MaUO9UIW3rXKoPgHI3X40ZgB/j12VY6h98Ae1mCBihjPvhOPeJWlxCYcmSbmeZt5ZKkK0dsVtp+6pA=="],
@@ -3072,8 +3134,6 @@
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
- "@babel/helper-compilation-targets/browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
-
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@civ7/studio-server/effect": ["effect@3.21.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-RqwU7WnJ6CqYhyjpOVJA5vh1Sgkn6eVECO6mnD0EjlbWcC2M3LJaPglXXr13Rdo/Y+B+wTEPzGRYFNL2xKxNeQ=="],
@@ -3224,6 +3284,20 @@
"@stoplight/yaml/@stoplight/types": ["@stoplight/types@14.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.4", "utility-types": "^3.10.0" } }, "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g=="],
+ "@tailwindcss/node/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"aggregate-error/clean-stack": ["clean-stack@4.2.0", "", { "dependencies": { "escape-string-regexp": "5.0.0" } }, "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg=="],
@@ -3450,12 +3524,6 @@
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
- "tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
-
- "tailwindcss/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
-
- "tailwindcss/postcss-load-config": ["postcss-load-config@4.0.2", "", { "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ=="],
-
"tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
@@ -3492,14 +3560,6 @@
"@aws-sdk/signature-v4-multi-region/@aws-sdk/middleware-sdk-s3/@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-RM5Mmo/KJ593iMSrALlHEOcc9YOIyOsDmS5x2NLOMdEmzv1o00fcpAkCQ02IGu1eFneBFT7uX0Mpag0HI+Cz2g=="],
- "@babel/helper-compilation-targets/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.9.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA=="],
-
- "@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="],
-
- "@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.278", "", {}, "sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw=="],
-
- "@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
-
"@inquirer/confirm/@inquirer/core/@inquirer/type": ["@inquirer/type@2.0.0", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag=="],
"@inquirer/confirm/@inquirer/core/mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="],
@@ -3730,12 +3790,6 @@
"string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
- "tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
-
- "tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
-
- "tailwindcss/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
-
"update-notifier/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"vite/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
@@ -3834,8 +3888,6 @@
"package-json/got/cacheable-request/responselike": ["responselike@1.0.2", "", { "dependencies": { "lowercase-keys": "^1.0.0" } }, "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ=="],
- "tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
-
"vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
diff --git a/docs/projects/mapgen-studio-redesign/00-GOAL.md b/docs/projects/mapgen-studio-redesign/00-GOAL.md
index 5325d3a1ff..fcc61306e0 100644
--- a/docs/projects/mapgen-studio-redesign/00-GOAL.md
+++ b/docs/projects/mapgen-studio-redesign/00-GOAL.md
@@ -188,7 +188,10 @@ Phase order per [`FRAME.md`](FRAME.md) §7 (design-system-first):
- [x] Frame, isolate, research, audit, target architecture, slice plan
- [x] Re-frame + un-pause + normative FRAME.md + goal reset (2026-06-08)
-- [ ] **P1 Design system** (`design:*` chain) — FIRST DOMINO, in progress next
+- [x] **P1 Design system foundation** — DONE (slice `design/ds-foundation`:
+ `971627971` spec + `4274e60f4` impl). Tailwind v4 + shadcn + tokens + `.dark`
+ theming + self-hosted fonts. Verified: tsc + prod build + worker bundle + live
+ render, no console errors, non-breaking. OpenSpec `mapgen-studio-design-system`.
- [ ] P2 Client data layer (oRPC-native TanStack Query + Zustand, behind seam)
- [ ] P3 Component decomposition (un-god App.tsx onto the design system)
- [ ] P4 Primitives→shadcn + new components + rjsf re-skin + craft
diff --git a/openspec/changes/mapgen-studio-design-system/.openspec.yaml b/openspec/changes/mapgen-studio-design-system/.openspec.yaml
new file mode 100644
index 0000000000..39ea9f20ca
--- /dev/null
+++ b/openspec/changes/mapgen-studio-design-system/.openspec.yaml
@@ -0,0 +1,3 @@
+schema: spec-driven
+created: 2026-06-08
+status: draft
diff --git a/openspec/changes/mapgen-studio-design-system/design.md b/openspec/changes/mapgen-studio-design-system/design.md
new file mode 100644
index 0000000000..328eb4bc64
--- /dev/null
+++ b/openspec/changes/mapgen-studio-design-system/design.md
@@ -0,0 +1,44 @@
+## Context
+
+Foundation slice (P1) of the MapGen Studio redesign. Controlling frame:
+`docs/projects/mapgen-studio-redesign/FRAME.md`. Confirmed design direction and
+token values: `apps/mapgen-studio/.interface-design/system.md` (decisions) over
+`apps/mapgen-studio/system.md` (as-built extraction).
+
+## Key decisions
+
+- **Tailwind v4, CSS-first.** React 19.2 + Vite 7 already; the v3 config is largely
+ dead. v4's `@theme` + `@tailwindcss/vite` removes the PostCSS/autoprefixer/
+ config surface. Existing arbitrary hex utilities (`bg-[#141418]`, `text-[11px]`)
+ are valid in v4 and render unchanged — this is what makes a non-breaking
+ foundation possible.
+- **Additive, non-breaking.** This slice adds the token + shadcn + `.dark` layer
+ alongside the existing hex styling. Components are NOT re-skinned here; the old
+ `lightMode`/`createTheme` path is retired later as components migrate onto
+ tokens (P3/P4). Coexistence is deliberate and bounded.
+- **Substrate elevation is the primary craft lever.** The live app reads flat
+ (page/panel/nested ~4% apart, no floating shadows). The token system encodes a
+ felt elevation tier scale + border progression so structure is perceived without
+ anything shouting (squint test is the gate). This is where a neutral slate
+ becomes intentional.
+- **Accent = elevated cool-steel slate, not chroma.** One restrained accent
+ (`--primary 216 18% 42%`); the contour focus signature is a luminance step
+ (`--ring 212 28% 60%`), not a saturated glow. Teal was considered and set aside
+ because the deck.gl map is the only thing that should carry color.
+- **`.dark` class theming.** Single switch on ``, no prop drilling, no-flash
+ bootstrap. Replaces the OS-`prefers-color-scheme` + JIT-invisible `createTheme()`
+ mechanism.
+
+## Risks / mitigations
+
+- *v4 migration regresses rendering* → existing hex utilities are v4-valid; verify
+ by build + live screenshot diff before stacking. Legacy `--color-*` vars
+ re-pointed at new tokens so focus/scrollbar/select keep working.
+- *Two theming systems coexist* → bounded and intentional; `.dark` is
+ authoritative now, `lightMode` removed as components migrate.
+- *Monorepo build coupling* → all changed config is app-local to `apps/mapgen-studio`.
+
+## Verification
+
+`tsc --noEmit` + `vite build` + worker-bundle check; live screenshot in dark and
+light. Behavior parity: no component logic touched.
diff --git a/openspec/changes/mapgen-studio-design-system/proposal.md b/openspec/changes/mapgen-studio-design-system/proposal.md
new file mode 100644
index 0000000000..c2bca50b45
--- /dev/null
+++ b/openspec/changes/mapgen-studio-design-system/proposal.md
@@ -0,0 +1,52 @@
+## Why
+
+MapGen Studio's UI has no design system. Color lives in 242 arbitrary hex
+utilities across components; three disagreeing dark palettes exist
+(`tailwind.config.js`, `src/index.css`, `src/ui/hooks/useTheme.ts`); the declared
+accent (indigo `#5e5ce6`) is not the used accent (slate `#4b5563`); theming is
+structurally broken (`createTheme()` builds JIT-invisible runtime classes, and a
+`lightMode` prop threaded through ~24 files fights OS `prefers-color-scheme`); the
+substrate is flat (page/panel/nested ~4% apart, no felt hierarchy); and overlay
+motion classes (`animate-in`) are dead (no keyframes installed).
+
+This is the FIRST domino of the MapGen Studio redesign (see
+`docs/projects/mapgen-studio-redesign/FRAME.md`). Every later component slice
+consumes the system this change establishes. Direction is user-confirmed and
+recorded in `apps/mapgen-studio/.interface-design/system.md`: a dense, dark-first
+**cartographer's instrument** with an elevated cool-steel **slate** accent, a
+robust **substrate-elevation** system as the primary craft lever, and a
+**contour-line** focus signature carried by luminance, not chroma.
+
+## What Changes
+
+- Migrate the studio build from Tailwind v3 (config + PostCSS) to **Tailwind v4**
+ (CSS-first `@theme`, `@tailwindcss/vite`), removing `tailwind.config.js`,
+ `postcss.config.js`, and `autoprefixer`.
+- Establish a single **tokenized design system**: HSL design tokens in `:root` +
+ `.dark`, including a felt substrate-elevation tier scale and a border
+ progression; map them through `@theme` to Tailwind utilities and shadcn aliases.
+- Initialize **canonical shadcn/ui** scaffolding: `components.json`, `lib/utils.ts`
+ (`cn`), and the New York / CSS-vars / slate base configuration. (Primitive
+ components are migrated in a later change.)
+- Repair theming: adopt shadcn's single **`.dark` class** strategy with a
+ no-flash bootstrap (default dark), replacing the `prefers-color-scheme` + dead
+ `createTheme()` mechanism. The legacy `lightMode` prop path is removed as
+ components migrate (later changes); this change makes the `.dark` system
+ authoritative and lets both coexist without conflict.
+- Install a real overlay **motion** layer so the previously dead `animate-in`
+ transitions run; commit the single accent and self-host fonts.
+
+This change is **foundation only**. It does not re-skin existing components or
+change their behavior; their 242 arbitrary hex classes keep rendering unchanged
+until later migration slices retire them.
+
+## Affected Owners
+
+- `apps/mapgen-studio` (build config, `src/index.css`, `src/lib/utils.ts`,
+ `components.json`, theme bootstrap)
+
+## Out Of Scope
+
+- Component primitive migration to shadcn, rjsf re-skin, App.tsx decomposition,
+ client-state/server-data rework, and the studio server — each its own later
+ change in the redesign workstream.
diff --git a/openspec/changes/mapgen-studio-design-system/specs/mapgen-studio/spec.md b/openspec/changes/mapgen-studio-design-system/specs/mapgen-studio/spec.md
new file mode 100644
index 0000000000..724903f631
--- /dev/null
+++ b/openspec/changes/mapgen-studio-design-system/specs/mapgen-studio/spec.md
@@ -0,0 +1,59 @@
+## ADDED Requirements
+
+### Requirement: Studio UI Uses A Single Tokenized Design System
+
+MapGen Studio SHALL express interface color, elevation, border, radius, and focus
+through a single set of named design tokens defined once in `:root` (light) and
+`.dark` (dark), rather than per-component hardcoded values or multiple disagreeing
+palettes.
+
+#### Scenario: Tokens are the source of truth
+
+- **WHEN** the design system is established
+- **THEN** background, foreground, card, popover, muted, primary, border, input,
+ ring, destructive, and radius are defined as HSL tokens in `:root` and `.dark`
+- **AND** a single committed accent (elevated cool-steel slate) is used for
+ primary action and active/focus states
+- **AND** the legacy `--color-*` variables referenced by focus, scrollbar, and
+ select styles resolve to those same tokens
+
+#### Scenario: Foundation does not break existing components
+
+- **WHEN** the token + Tailwind v4 + shadcn foundation is added
+- **THEN** components still styled with arbitrary hex utilities render unchanged
+- **AND** no component behavior is modified by this change
+
+### Requirement: Studio Surfaces Use A Felt Substrate-Elevation Hierarchy
+
+The design system SHALL encode a substrate-elevation tier scale and a border
+progression such that page, panel, nested, and floating surfaces are perceivable
+as a hierarchy without any element visually shouting.
+
+#### Scenario: Hierarchy survives the squint test
+
+- **WHEN** surfaces are rendered using the elevation tokens
+- **THEN** page → panel → nested → floating each occupy a distinct (whisper-quiet)
+ lightness tier
+- **AND** floating layers (dialog, popover, toast) carry shadow while in-page
+ surfaces rely on the border progression
+- **AND** structure remains perceivable when interface detail is blurred
+
+### Requirement: Studio Theming Uses A Single `.dark` Class Strategy
+
+MapGen Studio SHALL select light or dark theme via a single `.dark` class on the
+document root, applied before first paint, rather than OS `prefers-color-scheme`
+coupling or runtime-interpolated theme classes.
+
+#### Scenario: Theme is deterministic and flash-free
+
+- **WHEN** the app loads
+- **THEN** the stored theme (default dark) is applied to the document root before
+ first paint
+- **AND** toggling the theme switches the `.dark` class with no per-component prop
+ threading
+
+#### Scenario: Focus renders as a contour signature
+
+- **WHEN** an interactive element receives keyboard focus
+- **THEN** focus renders as a 1px contour ring using the ring token (a luminance
+ step above the chrome), not a saturated glow block
diff --git a/openspec/changes/mapgen-studio-design-system/tasks.md b/openspec/changes/mapgen-studio-design-system/tasks.md
new file mode 100644
index 0000000000..7828e0515c
--- /dev/null
+++ b/openspec/changes/mapgen-studio-design-system/tasks.md
@@ -0,0 +1,44 @@
+## 1. Tailwind v4 migration
+
+- [x] 1.1 Add `tailwindcss@4`, `@tailwindcss/vite`, `tw-animate-css`; remove
+ `autoprefixer`. Add the `@tailwindcss/vite` plugin to `vite.config.ts` (do not
+ touch the existing `/api` middleware).
+- [x] 1.2 Delete `tailwind.config.js` and `postcss.config.js` (CSS-first v4); fold
+ the still-live bits (font families, `pulse-subtle`) into CSS.
+- [x] 1.3 Rewrite `src/index.css` head to `@import "tailwindcss"` +
+ `@import "tw-animate-css"` + `@custom-variant dark`.
+
+## 2. Token system
+
+- [x] 2.1 Define the cartographer's-instrument HSL tokens in `:root` (light) and
+ `.dark` (dark) per `apps/mapgen-studio/.interface-design/system.md`: background/
+ foreground, card/popover, muted, primary (elevated steel slate),
+ primary-foreground, border/input, ring (contour luminance), destructive, radius.
+- [x] 2.2 Add the felt **substrate-elevation** tier scale (page→panel→nested→
+ floating) and a **border progression** (subtle/default/strong) — the primary
+ craft lever; widen the lightness steps so the squint test shows hierarchy.
+- [x] 2.3 Map tokens through `@theme inline` to Tailwind color/radius utilities and
+ shadcn aliases. Preserve the legacy `--color-*` vars (focus/scrollbar/select
+ still reference them) by re-pointing them at the new tokens.
+
+## 3. shadcn scaffolding
+
+- [x] 3.1 Add `components.json` (New York, CSS vars, base color slate, path
+ aliases) and `src/lib/utils.ts` exporting `cn` (clsx + tailwind-merge).
+
+## 4. Theming repair + craft foundation
+
+- [x] 4.1 Add a no-flash `.dark` bootstrap (inline head script, default dark,
+ localStorage-backed) and make `.dark` authoritative; keep a
+ `prefers-color-scheme` fallback for first paint during coexistence.
+- [x] 4.2 Replace the global `:focus-visible` outline with the contour-ring focus
+ signature (ring token, luminance step). Self-host Inter + JetBrains Mono
+ (remove the render-blocking Google Fonts `@import`).
+
+## 5. Verify
+
+- [x] 5.1 `tsc --noEmit` clean; `vite build` succeeds; worker-bundle check passes.
+- [x] 5.2 App renders in dark (live screenshot, no console errors) with no
+ regression to existing hex-styled components; tokens ready for component
+ adoption in later slices. (Light-theme readback deferred to the first
+ token-consuming component slice, where `.dark` toggling becomes observable.)