# Shared Theme — design **Milestone:** M3 (Visual foundation & shell), item **M3-a** — the first of three cycles (Theme → Main Window shell 2a → Title). See `docs/roadmap.md` and `CLAUDE.md` §16. **§2 side:** n/a — pure presentation. Owns no game state, makes no AI/network call. It is the visual system every later screen inherits. **Depends on:** nothing (foundation). **Unblocks:** every M3+ screen. --- ## 1. Goal Deliver the one shared visual system the mockups describe (`/mockups/README.md`, "Global Design System") as reusable Godot resources, so every screen built after this pulls the same palette, fonts, styleboxes, and signature surfaces — "the look is one system." Nothing renders a real screen yet; this is the toolbox the screens are built from. Because a Theme has no behavior, its **proof is a showcase harness** a human eyeballs in the editor — the same "live-proven / human-confirmed" bar M0–M2 held. ## 2. The core architectural split A Godot `StyleBox` **cannot run a shader** — the engine draws styleboxes itself. So the deliverable splits in two, each half using the right tool: | Concern | Mechanism | Covers | |---|---|---| | **Crisp bordered UI** | `game_theme.tres` (StyleBoxFlat + fonts) | buttons/CTA, tabs active/inactive, chips/tags, item tiles, cards, insets, dark panels; all three font families at their sizes | | **The 3 signature surfaces** | `ShaderMaterial` on `Panel`/`ColorRect`, wrapped as drop-in mini-scenes | parchment sheet, dark diagonal-stripe bay, radial vignette overlay | | **Code-driven color** | `palette.gd` consts | rarity bars, HP/MP/stamina gradients, disposition/turn-order colour coding | The "procedural shaders" decision applies to the **three big backdrops only**. A chip or button is flat-bordered and fits `StyleBoxFlat` natively; a shader on every chip would be wasted work. `palette.gd` is the **single source of truth** for every hex value. The `game_theme.tres` styleboxes are authored to those same values; the shaders take them as uniforms. **One palette, three consumers.** ## 3. Fonts (redistribution constraint) Godot exports must **bundle** their fonts. The mock's three families become: | Mock role | Bundled face | Licence | Notes | |---|---|---|---| | Georgia (prose / headings / logo) | **EB Garamond** (Regular, Italic, Bold) | OFL | Redistributable. Georgia is Microsoft-proprietary and **cannot** ship. EB Garamond is a warm old-style serif — the "aged tome" DM-prose voice. Its strong italic carries the narrative-body *italic* the mock uses. | | Architects Daughter (hand-inked accent) | **Architects Daughter** (Regular) | OFL | Ships as-is; the mock already used the Google face. Nameplates, big stat numbers, DM-header labels. | | Monospace (UI chrome) | **JetBrains Mono** (Regular) | OFL | Default pick (clean, wide, reads well UPPERCASE with letter-spacing); swappable for another OFL mono without design change. Labels, keybinds, numeric readouts, tags. | ## 4. File layout ``` client/ scripts/theme/ palette.gd # const Color tokens — the source of truth theme_keys.gd # const StringNames for type-variations, so scripts never # hardcode Theme string keys assets/theme/ fonts/ EBGaramond-Regular.ttf EBGaramond-Italic.ttf EBGaramond-Bold.ttf ArchitectsDaughter-Regular.ttf JetBrainsMono-Regular.ttf shaders/ parchment.gdshader # vertical gradient #ece2ca -> #e4d8bd dark_bay.gdshader # 135deg repeating stripe #26221c/#221e18 + radial vignette vignette.gdshader # standalone dim/blur-edge overlay (pause, dialogue) game_theme.tres # the Theme resource — fonts, sizes, StyleBoxFlat variations scenes/theme/ surfaces/ ParchmentPanel.tscn # Panel + parchment material (drop-in backdrop) DarkBay.tscn # Panel + dark_bay material VignetteOverlay.tscn # ColorRect + vignette material theme_showcase.tscn # the proof: every token, stylebox, font, surface, one screen theme_showcase.gd tests/unit/ test_palette.gd test_theme_resource.gd ``` Follows existing conventions: `scripts//`, harness scenes under `scenes/`, `tests/unit/test_*.gd`. ## 5. Contents ### 5.1 `palette.gd` Every hex from the README's "Color tokens" section as `const Color`, grouped by role: - **Dark backgrounds** — near-black stage, bay stripe pair, translucent panels, the dark-border ladder (`#6b6152`…`#1a1512`). - **Parchment** — sheet gradient stops, card fills + insets, parchment borders. - **Ink** — heading / body / muted / label / gold-accent inks. - **Text-on-dark** — bone/cream primaries, secondaries, muted monos. - **Accents** — blood-red set, gold/brass set, steel blue-teal set, green set. - **Stat bars** — HP / MP / stamina gradient endpoints + track/border. - **Rarity** — the five rarity colours as an ordered array. Plus the small helpers the code-driven cases need: - `rarity_color(rarity) -> Color` - stat-bar gradient stops for HP / MP / stamina (returned as a pair, or two named consts each). This is the file the whole game reads from for any colour a Theme can't express. ### 5.2 `theme_keys.gd` `const StringName`s for every type-variation name (`PrimaryCTA`, `Tab`, `TabActive`, `Chip`, `ItemTile`, `ParchmentCard`, `DarkPanel`, …), so scripts that set `theme_type_variation` reference a constant, never a raw string that can typo silently. ### 5.3 `game_theme.tres` The six reusable pieces from the README, each authored as a Godot **type variation** (applied via `theme_type_variation` on any Control): - **PrimaryCTA** — blood-red CTA button (gradient-look `StyleBoxFlat`, `#8f3a34` border, cream text, brightened hover). - **Tab** / **TabActive** — inactive (parchment `#efe6cf` + `#cbb684` border, muted text) vs active (solid `#8f3a34`, cream text). - **Chip** — mono-uppercase tag base; the *semantic* colour (red/gold/grey) is set by the consuming script from `palette.gd`, since it varies per use. - **ItemTile** — rounded square, 2px border (rarity colour applied by script), plus a recessed dashed **empty** variant. - **ParchmentCard** — `#f1e8d2` fill, `#bfa878` border, and a darker inset variant. - **DarkPanel** — translucent `rgba(18,15,11,.7)`. Plus the three font families wired into the Theme's default / heading / mono font roles at the README's sizes and letter-spacing. ### 5.4 The three surface scenes Each wraps one shader as a **one-line drop-in**: later screens instance `DarkBay.tscn` / `ParchmentPanel.tscn` / `VignetteOverlay.tscn` instead of rebuilding a backdrop. Shader uniforms default to the `palette.gd` values (baked into the `.tscn`/material), so the surfaces match the tokens without a runtime dependency on the script. - `parchment.gdshader` — vertical gradient `#ece2ca → #e4d8bd`. - `dark_bay.gdshader` — 135° repeating stripe `#26221c` / `#221e18` (16px bands) plus the radial vignette overlay. - `vignette.gdshader` — standalone edge-dim overlay for the pause/dialogue dim. ## 6. The proof — `theme_showcase.tscn` A single scrollable screen rendering, for a human to eyeball in the editor: - the full palette as labelled swatches, - every stylebox type-variation (CTA, both tabs, chips in each semantic colour, filled + empty item tiles, parchment card + inset, dark panel), - all three fonts at their real sizes (heading, prose body italic, hand-inked accent, mono chrome), - the rarity ladder, - a sample HP / MP / stamina bar, - the three shader surfaces side by side. Pure presentation — no AI, no network, no game state. This is what confirms "the look is one system" before any real screen is built. ## 7. Testing - **`test_palette.gd`** — asserts the documented tokens exist and are `Color`; `rarity_color()` maps each rarity to the right colour and handles out-of-range safely; the stat-bar stop helpers return the documented values. - **`test_theme_resource.gd`** — `game_theme.tres` loads without error, and every named type-variation in `theme_keys.gd` resolves on the Theme (no dangling key). **Visual correctness is not GUT-testable** — it is an explicit **human-confirm** step against `theme_showcase.tscn`, exactly as M2's live model proofs were human-confirmed. The spec is not "done" until a human has opened the showcase and signed off that it reads as the mockups' system. ## 8. Explicitly out of scope (this cycle) - Any real screen (the Main Window shell 2a and Title are the next two M3 cycles). - Real art of any kind — portraits, key art, iso/battlefield renders, item/ability icons remain the mocks' dashed **art slots**, filled later. - Responsive/stretch tuning beyond the project's existing 1920×1080 canvas assumption (`canvas_items` stretch is a shell-cycle concern, not a Theme one). - Baked-PNG texture variants — the three surfaces are shader-only this cycle. - Animation (ember particles, pulse/`cturn` keyframes) — those belong to the screens that use them, not the Theme. ## 9. Reconciliations touched None. The two flagged reconciliations (damage determinism → M5, class names → M4) do not touch presentation. This cycle introduces no charter conflict.