diff --git a/DESIGN.md b/DESIGN.md
index dd68092..609c2c1 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -1,7 +1,10 @@
# DESIGN.md — TeamsISO design system
-Target framework: **WinUI 3 (Windows App SDK)**. Tokens are framework-agnostic;
-the WinUI XAML implementation lives in `src/TeamsISO.App/Themes/` (post-port).
+Target framework: **WPF .NET 8**. Tokens are framework-agnostic; the WPF
+XAML implementation lives in `src/TeamsISO.App/Themes/`. (A WinUI 3 rebuild
+was attempted and rolled back — see
+`docs/shapes/2026-05-13-teamsiso-v2-studio-terminal.md` for the v2 shape
+this design serves.)
## Color
@@ -106,39 +109,49 @@ The toggle is also surfaced inside the settings drawer under an "Appearance"
group as a tri-state pill (System / Dark / Light), so power users find it in
the obvious place too.
-### Implementation (WinUI 3)
+### Implementation (WPF)
-WinUI 3's `ThemeDictionary` pattern handles automatic swapping based on
-`FrameworkElement.RequestedTheme`. Tokens live as `Color` resources keyed by
-theme, then `SolidColorBrush` references them via `{ThemeResource}`:
+WPF doesn't have WinUI 3's `ThemeDictionary` pattern. The equivalent is to
+**split tokens by theme into separate ResourceDictionary files**, all
+addressed via `DynamicResource` (NOT `StaticResource`) so the values can
+be swapped at runtime.
-```xml
-
-
-
- #0A0A0A
-
-
-
- #FAFAFB
-
-
-
-
+```
+Themes/
+ Theme.Tokens.xaml ← styles, control templates, key shape (no colors)
+ Theme.Dark.xaml ← color resources only — Dark variant
+ Theme.Light.xaml ← color resources only — Light variant
```
-At runtime, the root `Page.RequestedTheme = ElementTheme.Dark | Light`
-switch propagates down the visual tree instantly — no app restart, no
-flicker. The custom title bar (drawn via `AppWindow.TitleBar.ExtendsContent`)
-gets a manual color update on the same code path since its system buttons
-aren't part of the XAML tree.
+`Theme.Dark.xaml` and `Theme.Light.xaml` define the SAME set of keys —
+`Wd.Bg.Canvas`, `Wd.Accent.Cyan`, etc. — with different `Color` values.
+`Theme.Tokens.xaml` references them via `DynamicResource` from styles and
+templates. At startup, `App.xaml` merges `Theme.Tokens.xaml` plus exactly
+one of `Theme.Dark.xaml` or `Theme.Light.xaml`. At runtime, `ThemeManager`
+swaps the merged dictionary's color file:
+
+```csharp
+var app = Application.Current;
+var oldDict = app.Resources.MergedDictionaries
+ .First(d => d.Source?.OriginalString.EndsWith("Theme.Dark.xaml") == true
+ || d.Source?.OriginalString.EndsWith("Theme.Light.xaml") == true);
+var idx = app.Resources.MergedDictionaries.IndexOf(oldDict);
+app.Resources.MergedDictionaries[idx] = new ResourceDictionary {
+ Source = new Uri($"/Themes/Theme.{newTheme}.xaml", UriKind.Relative)
+};
+```
+
+`DynamicResource`-backed `SolidColorBrush` instances re-resolve on the
+dictionary swap, so the visual tree repaints without an app restart.
### System mode
-When `UIPreferences.Theme == System`, the app reads
-`Application.Current.RequestedTheme` at startup and re-reads when the OS
-theme changes (via `UISettings.ColorValuesChanged`). This is the default —
-operators who don't care get whatever their Windows session is set to.
+When `UIPreferences.Theme == "System"`, `ThemeManager` reads
+`HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize\AppsUseLightTheme`
+at startup. It also subscribes to `SystemEvents.UserPreferenceChanged` so
+the app re-resolves the theme when the operator flips Windows app-mode
+mid-session. This is the default — operators who don't care get whatever
+their Windows session is set to.
## Typography
@@ -157,11 +170,12 @@ operators who don't care get whatever their Windows session is set to.
Body text caps at 65–75ch where it wraps. Inline status text doesn't wrap —
it truncates with ellipsis.
-### Fonts in WinUI 3
+### Fonts in WPF
-WPF's `pack://application:,,,/Assets/Fonts/#Inter` resource URI doesn't carry
-over. WinUI 3 uses `ms-appx:///Assets/Fonts/Inter.ttf#Inter`. Migration is
-mechanical but required.
+Bundled fonts ship in `src/TeamsISO.App/Assets/Fonts/` and resolve via
+`pack://application:,,,/Assets/Fonts/#Inter` / `#JetBrains Mono`. The
+`` glob in `TeamsISO.App.csproj` already covers the `.ttf` files;
+new font weights go in the same directory and pick up automatically.
## Spacing (8px grid)
diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md
new file mode 100644
index 0000000..ec7bf84
--- /dev/null
+++ b/NEXT_STEPS.md
@@ -0,0 +1,89 @@
+# Where we left off — v2 "Studio Terminal" shell landed (2026-05-13 night)
+
+## What's done (uncommitted on local main)
+
+**v2 redesign shape:** Approved brief at `docs/shapes/2026-05-13-teamsiso-v2-studio-terminal.md`. Aesthetic register is "broadcast-engineering instrument" — Linear's keyboard-first density × Avid console legibility. Goes hard against the "screams AI" failure mode.
+
+**PRODUCT.md + DESIGN.md** updated to reflect WPF as the host (WinUI 3 lines removed), v2 IA decisions absorbed, recording references softened, theme implementation rewritten for WPF DynamicResource swap.
+
+**Theme system split:**
+- `src/TeamsISO.App/Themes/Theme.Dark.xaml` — color brushes only, dark variant
+- `src/TeamsISO.App/Themes/Theme.Light.xaml` — color brushes only, light variant
+- `src/TeamsISO.App/Themes/WildDragonTheme.xaml` — styles + control templates (no color brushes anymore; uses `DynamicResource` for every brush ref)
+- `src/TeamsISO.App/Services/ThemeManager.cs` — singleton that swaps the merged dictionary at runtime, reads `HKCU\...\AppsUseLightTheme` for System mode, subscribes to `SystemEvents.UserPreferenceChanged`, persists via `UIPreferences.Theme`.
+- `App.xaml` merges Theme.Dark.xaml + WildDragonTheme.xaml by default; `App.xaml.cs.OnStartup` calls `ThemeManager.Current.Apply()` before MainWindow shows.
+- `UIPreferences.Prefs` record gets a new `Theme = "System"` field (forward-compatible — old json files still load).
+- New brush key: `Wd.Accent.CyanText` (Dark `#97EDF0` / Light `#0E7C82`) for cyan-as-text contrast on light canvas. The existing `Wd.Accent.Cyan` stays bright in both modes for fill use.
+
+**v2 main window shell:**
+- Default Windows title bar (no more custom chromeless caption buttons — they looked generic and broke on DPI scaling).
+- 32px header: Wild Dragon mark + "TeamsISO" wordmark left; three icon buttons right (⌘K command palette, theme toggle, settings gear). The mark is small (20px) as a quality cue — click opens About.
+- 40px transport strip: mono-typed single line. `● 02:14:32 PART 4 · LIVE 2 CTRL :9755`. Cyan dot + timer only when at least one ISO live. CTRL cluster right-aligned in a Grid column.
+- Body: alert banner + update banner + action toolbar (Enable / Refresh / Presets / Stop all + Teams launch/hide icons) + participants DataGrid.
+- Conditional meeting bar at bottom — appears only when `IsTeamsInCall == true`, with Mute / Cam / Leave.
+- 72px left rail: **gone**.
+- 380px permanent settings panel: **gone**.
+- Six-column footer: **gone**.
+- Settings: slide-over drawer overlay (420px from right) triggered by the header gear; OUTPUT / NETWORK / APP tabs (DISPLAY renamed to APP); same bindings as v1. Scrim click dismisses; Esc dismisses.
+
+**Hotkeys preserved + new:**
+- F1 help, Ctrl+R refresh, Ctrl+Shift+S panic stop, 1–9 / NumPad 1–9 toggle Nth participant — all preserved.
+- **Ctrl+T toggle theme (NEW)** — cycles dark ↔ light. Hooked through `MainViewModel.ToggleThemeCommand` → `ThemeManager.Toggle()`.
+- **Ctrl+K command palette (placeholder)** — currently opens the help dialog. Task 40 replaces this with a real fuzzy-search palette window.
+
+**View-model additions (MainViewModel):**
+- `ParticipantCount` and `LiveCount` — feed the transport strip's "PART N · LIVE N" readout. Updated on the 1Hz stats tick.
+- `ToggleThemeCommand` — wraps `ThemeManager.Current.Toggle()`.
+
+**MainWindow code-behind cleanup:**
+- Removed `OnMinimize`, `OnMaximizeRestore`, `OnClose`, `OnWindowStateChanged`, the maximize-icon swap logic (no more custom title bar).
+- Added `OnCommandPaletteClick`, `OnSettingsScrimClick`, `OnPreviewKeyDown` (Esc to close drawer).
+- `OnSettingsToggleClick` now toggles `SettingsDrawerOverlay.Visibility` (the slide-over) instead of toggling the v1 right-column width.
+
+## To build, push, and demo
+
+```powershell
+cd "C:\Users\zacga\Documents\Claude\Projects\Teams ISO"
+
+# Clear the corrupt local index from the earlier session
+Remove-Item .git\index -Force -ErrorAction SilentlyContinue
+git reset
+
+# Build the WPF host
+dotnet build src\TeamsISO.App\TeamsISO.App.csproj -c Release
+
+# If the build is clean:
+.\src\TeamsISO.App\bin\Release\net8.0-windows\TeamsISO.exe
+```
+
+In the running app, test:
+1. **Theme toggle** — press `Ctrl+T`. Should swap dark ↔ light without restart. Header colors, surfaces, and text foregrounds all repaint.
+2. **System theme follow** — open Windows Settings → Personalization → Colors → "Choose your mode" → flip between Dark and Light. TeamsISO should track the OS automatically (default preference is `System`).
+3. **Settings drawer** — click the header gear icon. 420px drawer slides in from the right with OUTPUT / NETWORK / APP tabs. Esc or click-scrim dismisses.
+4. **Transport strip** — should show the session timer when at least one ISO goes live, and the PART/LIVE counts always.
+5. **Conditional meeting bar** — only appears when Teams is in a call.
+
+If anything regresses, the v1 shell is preserved in git history at `1d1ce6a` — easy rollback with `git reset --hard 1d1ce6a` then publish.
+
+## Commit + push when ready
+
+```powershell
+git add -A src/TeamsISO.App/ docs/ PRODUCT.md DESIGN.md
+git commit -m "feat(wpf): v2 'Studio Terminal' shell — theme system, header, transport strip, drawer
+
+- Theme split: Theme.Dark.xaml + Theme.Light.xaml + ThemeManager
+- New shell: 32px header (mark + wordmark + 3 icons), 40px transport strip,
+ conditional meeting bar, slide-over settings drawer
+- Removed: 72px rail, 380px permanent settings panel, 6-column footer,
+ custom chromeless title bar buttons
+- Ctrl+T toggles theme; follows Windows app-mode by default
+- Shape doc at docs/shapes/2026-05-13-teamsiso-v2-studio-terminal.md"
+git push origin HEAD
+```
+
+## What's still queued (tasks 39 + 40)
+
+- **Task 39** — Participants table redesign. The DataGrid columns in the v2 shell are still v1-style (Name / Source / ISO toggle). v2 wants: 5 columns (state LED, name+codec, audio meter, output name mono, ISO pill), 52px rows, full-row active-speaker tint instead of left stripe, hard-edged 8px state LED.
+- **Task 40** — Real Ctrl+K command palette window. Floating 560×360 dialog, fuzzy search across categories (Quick / Teams / Presets / Output / Network / App). Replaces the current placeholder that opens the help dialog.
+
+Both are scoped commits and depend on the v2 shell being confirmed working first. After this lands and you've verified the theme swap + drawer + transport strip render correctly, ping me and we'll knock those out.
diff --git a/PRODUCT.md b/PRODUCT.md
index 643d692..afb292d 100644
--- a/PRODUCT.md
+++ b/PRODUCT.md
@@ -15,14 +15,15 @@ OBS, Resolve, Ross, hardware capture), and does three things:
1. **Routes** each guest as a clean, individually-addressable, normalized NDI
source (consistent framerate, resolution, aspect, audio routing — regardless
of what each participant's webcam is doing).
-2. **Records** every active ISO to disk simultaneously — raw BGRA + manifest
- + ffmpeg convert script — so post-production gets a per-guest archive
- ready to cut from.
-3. **Orchestrates Teams itself** — launch/hide Teams windows, drive in-call
+2. **Orchestrates Teams itself** — launch/hide Teams windows, drive in-call
controls (mute, camera, share, leave, raise hand, quick-join) via
UIAutomation, so the operator never has to alt-tab away from the routing
table while the show is live.
+(Recording — previously the second pillar — was removed in the WPF rollback
+on 2026-05-13. The engine plumbing is intact for a future re-introduction,
+but no UI surface, view-model command, REST route, or OSC route exposes it.)
+
External control surface (REST + WebSocket + OSC on localhost) lets a
Companion / Stream Deck / TouchOSC controller drive routing remotely.
@@ -113,9 +114,11 @@ behind purposeful entry points.
### 4. At-a-glance status is sacred.
-Recording state, disk health, control-surface reachability, session timer,
-NDI signal-per-participant — these are the operator's situational awareness.
-They must be readable in peripheral vision, in one place, without scanning.
+Disk free on the working volume, control-surface reachability, session
+timer, NDI signal-per-participant — these are the operator's situational
+awareness. They must be readable in peripheral vision, in one place,
+without scanning. (Recording state was a historical fifth field; it's
+removed.)
### 5. Confident neutrality over decorative warmth.
@@ -148,11 +151,15 @@ read as AI-generated. Concretely, this means none of:
## Technical constraints (informing design)
- Windows-only (Teams' NDI is Windows-only anyway).
-- **WinUI 3 / Windows App SDK** is the new frontend target.
+- **WPF .NET 8** is the supported frontend host. (A WinUI 3 rebuild was
+ attempted in May 2026; it proved fragile — XAML parser crashes on
+ DataTemplate, theme-glyph rendering issues — and was abandoned. The
+ rollback commit `1d1ce6a` is the canonical baseline.)
- Engine layer (.NET 8) is preserved verbatim — view-model surface is the
swap boundary.
-- Bundled fonts via WinUI 3's `FontFamily` packaging (the WPF
- `pack://...#Inter` resource URI doesn't translate; needs migration).
+- Fonts are bundled via WPF's `pack://application:,,,/Assets/Fonts/#Inter`
+ resource URI so the operator's machine doesn't have to have Inter or
+ JetBrains Mono installed.
- MSIX-signed installer is on the v1.0 path; the new shell needs to package
cleanly through that pipeline.
- The external control surface (REST/WebSocket on `:9755`, OSC on `:9000`)
diff --git a/docs/shapes/2026-05-13-teamsiso-v2-studio-terminal.md b/docs/shapes/2026-05-13-teamsiso-v2-studio-terminal.md
new file mode 100644
index 0000000..ea99298
--- /dev/null
+++ b/docs/shapes/2026-05-13-teamsiso-v2-studio-terminal.md
@@ -0,0 +1,194 @@
+# TeamsISO v2 — Studio Terminal (approved shape brief)
+
+**Date approved:** 2026-05-13
+**Approver:** Zac (operator + product owner)
+**Host:** WPF .NET 8 (`src/TeamsISO.App/`). WinUI 3 rebuild is abandoned.
+**Predecessor:** the WPF rollback at `1d1ce6a` (recording axed, settings pane tab fix, settings button wired).
+
+## Why this redesign
+
+The v1 GUI failed the "AI made that" test. Quote from the operator: "its cluttered, screams that AI made it - and relatively inefficient to navigate." The PRODUCT.md anti-references — card-grid-of-icons, always-visible side panel, footer-as-theatre — all describe the current build. v2 commits to a different aesthetic register entirely.
+
+## Aesthetic register
+
+**Broadcast-engineering instrument.** Not a SaaS dashboard. Not Material. Not Fluent default.
+
+Reference proximity: Linear's keyboard-first density × Avid S6 console legibility × Blackmagic ATEM's information hierarchy. The operator mental model is "I'm sitting at an audio mixer; every region has a job, no region is theatre."
+
+## What goes away
+
+- The 72px left rail (no actual navigation — there's only one screen)
+- The 380px always-visible settings pane (settings change rarely, shouldn't claim permanent real estate)
+- The 6-column footer status row (theatre, not information)
+- The custom chromeless title-bar caption buttons (look worse than system chrome, break on DPI scaling)
+- The "by Wild Dragon" pill and the always-visible "TeamsISO" wordmark as decorative chrome
+- The in-call control bar as a permanent strip (only relevant in-call; should appear conditionally)
+- The seven identical ghost buttons in the in-call bar (textbook card-grid anti-pattern)
+
+## What replaces it
+
+```
+┌─ system Windows title bar [_ □ ✕] ─────────────────────┐
+│ 🐉 TeamsISO [⌘K] [☾] [⚙] │ 32px header — mark + wordmark left, 3 icons right
+├────────────────────────────────────────────────────────┤
+│ ● 02:14:32 PART 4 · LIVE 2 DISK 482g CTRL :9755 │ transport strip — single mono line, replaces footer
+├────────────────────────────────────────────────────────┤
+│ │
+│ ▮ alice ▮▮▮▮ t:5ms alice [LIVE] │
+│ ▯ bob ▮▮ t:8ms bob [— OFF]│ participants table = the canvas
+│ ▮ carlos ▮▮▮▮▮ t:9ms carlos [LIVE] │ (cyan-tinted row bg = active speaker)
+│ ▮ guest 4 -- NO SIG guest_4 [ERROR]│
+│ │
+├────────────────────────────────────────────────────────┤
+│ IN CALL · Daily standup [mute] [cam] [leave] │ conditional — only renders when in call
+└────────────────────────────────────────────────────────┘
+```
+
+### Header (32px)
+
+Left: Wild Dragon mark (~20px) + "TeamsISO" wordmark in Inter 13 Medium. Click on mark opens About.
+Right: three icon buttons.
+- `⌘K` (Tabler `ti-command`) — opens command palette (also Ctrl+K, Ctrl+P shortcut)
+- `☾` / `☀` (Tabler `ti-moon` / `ti-sun`) — cycles theme dark ↔ light. Tooltip "Theme (System / Dark / Light)" — long-press could open the tri-state, but for v2 just a one-click cycle.
+- `⚙` (Tabler `ti-settings`) — opens settings drawer
+
+That's all the chrome. No nav rail because there's nothing to navigate to.
+
+### Transport strip
+
+Single horizontal line. Mono type (JetBrains Mono 12). Replaces the entire footer.
+
+Fields:
+- `● 02:14:32` — green dot + session timer when at least one ISO is live; both hidden otherwise
+- `PART 4 · LIVE 2` — participant count and live-ISO count; "PART" / "LIVE" in Inter 11 SemiBold UPPER tracking 0.06em, numbers in mono
+- `DISK 482g` — free disk space on the working volume; coral text if <10GB, hidden if no relevant volume is configured
+- `CTRL :9755` — control surface bind; cyan text when active, hidden when off
+
+No icons. No badges. No backgrounds. Just typed status — a console heads-up display.
+
+### Participants table — the canvas
+
+Five columns:
+
+| # | Width | Content | Type |
+|---|---|---|---|
+| 1 | 24px | State LED — 8×8 filled cyan/coral or hollow neutral | hard-edged square, no rounding |
+| 2 | * | Name (Inter 13/Medium) + codec/latency caption (Mono 11/Regular, tertiary fg) | "Alice Wong" / "NDIV5 · t:5ms" |
+| 3 | 110px | Audio meter — 5 vertical bars, instantaneous level | hard-edged, cyan when LIVE, neutral when OFF |
+| 4 | 130px | Output name | Mono 12 |
+| 5 | 100px | ISO toggle pill | LIVE = cyan fill / OFF = hollow neutral / ERROR = coral outline |
+
+Row height: 52px (was 56).
+
+Active speaker: full-row background tint `bg.active-speaker` (cyan-tinted muted neutral). NOT a left-edge stripe — that trips the impeccable "side-stripe border" ban.
+
+Each row reacts to:
+- Click anywhere → focuses the row, keyboard-actions apply
+- Click the pill → toggle ISO
+- Right-click → context menu (preview, custom name, copy NDI source name, save snapshot)
+- Hover → reveals a kebab affordance in column 5 right edge for less-frequent actions
+
+### Conditional meeting bar
+
+Renders below the table only when `TeamsControlBridge.DetectCallState().IsInCall == true`. Slides up from below on transition (~120ms ease-out-quart on `RenderTransform.Y` + `Opacity`).
+
+Content: `IN CALL` label (Inter 11 SemiBold UPPER, cyan accent) + meeting title (Mono 12, truncated with ellipsis) + three buttons right-aligned (Mute / Cam / Leave). Share and Notes do NOT live here — they move to ⌘K, where they're invocable any time without the bar fighting for attention.
+
+Width matches the table — not full-bleed; respects the page padding.
+
+### Ctrl+K command palette
+
+The redesign's navigation move. Replaces ~80% of what's in the v1 rail + tabbed settings.
+
+Behavior:
+- `Ctrl+K` (also `Ctrl+P`) opens a centered floating window over the main shell, 560×360px
+- Search input at the top, results list below
+- Empty input → frequent + recent commands
+- Typing → fuzzy-matches across command label + category + keywords
+- ↑/↓ navigates, Enter invokes, Esc closes
+
+Command categories (each command has icon, label, optional value preview, optional shortcut hint):
+- **Quick** — Enable all online, Stop all ISOs, Refresh discovery, Drop snapshot of all
+- **Teams** — Launch Teams, Hide / show Teams windows, Mute, Toggle camera, Open share, Leave call
+- **Presets** — Apply … (one row per saved preset), Save current as preset, Manage presets
+- **Output** — Framerate 24 / 30 / 60, Resolution 1080p / 720p, Aspect Pillarbox / Letterbox / Stretch
+- **Network** — Apply transcoder topology, Restore default NDI groups, Edit output name template
+- **App** — Theme dark / light / system, Open settings, About TeamsISO, Help (F1)
+
+This is the keyboard-first surface broadcasters with Stream Decks already mentally use.
+
+### Settings — slide-over drawer
+
+Triggered from the header gear icon, or from `Open settings` in the palette, or hotkey `,` (comma).
+
+- 420px wide, slides in from the right
+- 40% canvas scrim behind
+- Three tabs: **OUTPUT** (framerate / resolution / aspect / audio + Reset to defaults), **NETWORK** (discovery / output groups + Apply transcoder topology + Restore defaults + output name template), **APP** (theme tri-state, minimize to tray, sort order, Launch Teams on startup, Auto-hide Teams windows)
+- Apply Changes button pinned to drawer footer; Esc dismisses; click outside the drawer dismisses
+
+DISPLAY tab from v1 gets renamed APP and absorbs the theme tri-state.
+
+### Empty states
+
+- No participants yet: a single centered mono sentence, "no ndi sources yet — open teams and start a meeting", and one tertiary button "Refresh discovery (Ctrl+R)". No illustration, no mascot.
+- Not in a call: meeting bar simply doesn't render. No placeholder.
+- Discovery degraded: amber dot in transport strip's session timer position, mono text "NDI discovery — restarting". No banner.
+
+## Color, theme, motion
+
+**Color strategy:** Restrained (impeccable product default). Cyan accent earns its place — reserved for LIVE state, focus ring, active speaker tint. Coral reserved for destructive + error. Status amber for warnings. Green NOT used (would compete with cyan for "ok / live" semantics).
+
+**Theme default:** Follow Windows. Theme persists per-operator via `UIPreferences.Theme`. Implementation: split `WildDragonTheme.xaml` into a single style + token-shape file plus two color-only ResourceDictionary files (`Theme.Dark.xaml`, `Theme.Light.xaml`). At runtime `ThemeManager` swaps the merged dictionary entry. WPF analog of WinUI's `ThemeDictionary`.
+
+**Motion:**
+- 120ms `cubic-bezier(0.16, 1, 0.3, 1)` (ease-out-quart) on the meeting bar slide-in/out
+- 200ms ease-out on the drawer slide
+- 180ms cross-fade on theme swap
+- 90ms on focus + hover transitions
+- No bounce, no elastic, no spring overshoots. Animate `RenderTransform` and `Opacity` only — never layout properties.
+
+## Typography commitments
+
+| Token | Family | Size | Weight | Used for |
+|---|---|---|---|---|
+| `text.timer` | JetBrains Mono | 14 | Medium | Session timer in transport strip — instrument-grade |
+| `text.caption` | Inter | 11 | SemiBold (600) | UPPER + tracking 0.06em — transport-strip labels, "IN CALL", "SPEAKING" |
+| `text.display` | Inter | 22 | SemiBold | Settings drawer headings only |
+| `text.title` | Inter | 13 | Medium | Wordmark, table column headers |
+| `text.body` | Inter | 13 | Regular | Participant display names |
+| `text.mono.code` | JetBrains Mono | 12 | Regular | Output names, NDI IDs, meeting title |
+| `text.mono.tech` | JetBrains Mono | 11 | Regular | Latency readouts, codec captions, transport-strip values |
+
+## What this is NOT
+
+- Not Fluent-styled. Default Fluent accent integration is generic Windows; TeamsISO is a broadcaster's tool.
+- Not minimalism for its own sake. The participants table is *dense*. Density is the broadcaster's virtue.
+- Not chromeless. Default system title bar stays. Chromeless windows break embarrassingly at 4K + DPI scaling.
+- Not vanity-branded. The Wild Dragon mark sits small in the header as a quality cue, never as decoration.
+
+## Migration path
+
+The view-model surface in `src/TeamsISO.App/ViewModels/` is the contract. The redesign rewrites `MainWindow.xaml` and `Themes/*` but leaves view-models, the engine, the control surface server, and the OSC bridge untouched.
+
+Order of operations (each step builds clean before the next):
+
+1. **Theme split** — Refactor `WildDragonTheme.xaml` → `Themes/Theme.Tokens.xaml` (styles + key shape) + `Themes/Theme.Dark.xaml` + `Themes/Theme.Light.xaml` (color resources only). Port `ThemeManager` from the deleted WinUI project; wire system app-mode detection via registry (`HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize\AppsUseLightTheme`).
+2. **Main window shell** — Replace MainWindow.xaml's outer Grid. Add 32px header, transport strip, full-width content area, conditional meeting bar. Delete the 72px rail, the 380px right pane, the footer.
+3. **Participants table redesign** — 5 columns, LED state, instantaneous audio meter, ISO pill.
+4. **Settings drawer** — Slide-over from right, dismissable; reuses existing settings view-model.
+5. **Command palette** — `Ctrl+K` floating window with fuzzy command list.
+
+Each step is a self-contained commit so the v1 build remains shippable at any rollback point.
+
+## Anti-references — explicit on the "AI made that" failure
+
+These are the failure modes the redesign defends against:
+- Card-grid-of-icons (the v1 in-call bar's seven identical ghost buttons)
+- Always-visible side panel (the v1 380px settings sidebar)
+- Decorative chrome (the v1 "by Wild Dragon" pill, the 72px nav rail, the six-column footer)
+- Generic Inter at 13 for everything
+- Default WPF DataGrid (Excel)
+- Custom chromeless title bars that look generic
+- Gradient text, glassmorphism, side-stripe borders (impeccable absolute bans)
+- "Hero metric + supporting stats + gradient" SaaS dashboards
+- Mascots, "Welcome!" copy, illustrated onboarding cards
diff --git a/src/TeamsISO.App/App.xaml b/src/TeamsISO.App/App.xaml
index 0c6fdab..76d6e06 100644
--- a/src/TeamsISO.App/App.xaml
+++ b/src/TeamsISO.App/App.xaml
@@ -4,6 +4,18 @@
+
+
diff --git a/src/TeamsISO.App/App.xaml.cs b/src/TeamsISO.App/App.xaml.cs
index 309cb25..e2b0837 100644
--- a/src/TeamsISO.App/App.xaml.cs
+++ b/src/TeamsISO.App/App.xaml.cs
@@ -76,6 +76,12 @@ public partial class App : Application
DispatcherUnhandledException += OnDispatcherUnhandled;
System.Threading.Tasks.TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
+ // Resolve and apply the theme BEFORE any window is shown so we don't
+ // paint a dark frame for one tick then flip to light (or vice versa).
+ // ThemeManager.Apply swaps Application.Resources.MergedDictionaries
+ // in place; DynamicResource refs in WildDragonTheme.xaml re-bind.
+ TeamsISO.App.Services.ThemeManager.Current.Apply();
+
// Single-instance gate: if another TeamsISO is already running for this user,
// broadcast the bring-to-front message and exit silently. This prevents the
// NDI/config contention seen during testing where two finders, two senders
diff --git a/src/TeamsISO.App/MainWindow.xaml b/src/TeamsISO.App/MainWindow.xaml
index 31ce6c0..93668c7 100644
--- a/src/TeamsISO.App/MainWindow.xaml
+++ b/src/TeamsISO.App/MainWindow.xaml
@@ -1,60 +1,54 @@
-
+ SnapsToDevicePixels="True"
+ TextOptions.TextRenderingMode="ClearType"
+ TextOptions.TextFormattingMode="Display"
+ d:DataContext="{d:DesignInstance Type=vm:MainViewModel}">
-
-
-
-
-
+
-
-
-
-
+
+
+
@@ -76,159 +70,221 @@
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
-
+ BorderThickness="0,0,0,1">
+
+
+
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Width="14" Height="14"
+ Stretch="None"/>
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+ Margin="0,0,10,0"/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Margin="0,0,12,0"/>
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ Command="{Binding EnableAllOnlineCommand}"
+ Content="Enable all"
+ Padding="14,7"
+ Margin="0,0,8,0"
+ ToolTip="Enable ISO routing for every online participant"/>
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/TeamsISO.App/MainWindow.xaml.cs b/src/TeamsISO.App/MainWindow.xaml.cs
index 2292bf7..a9990b8 100644
--- a/src/TeamsISO.App/MainWindow.xaml.cs
+++ b/src/TeamsISO.App/MainWindow.xaml.cs
@@ -1,7 +1,7 @@
+using System;
using System.Linq;
using System.Windows;
using System.Windows.Input;
-using System.Windows.Shapes;
using TeamsISO.App.Services;
using TeamsISO.App.ViewModels;
@@ -12,9 +12,12 @@ public partial class MainWindow : Window
public MainWindow()
{
InitializeComponent();
- StateChanged += OnWindowStateChanged;
SourceInitialized += OnSourceInitialized;
Closing += OnClosing;
+ // Esc dismisses the settings drawer when it's open. Bound at the
+ // window level so any focused control inside the drawer also gets
+ // the affordance.
+ PreviewKeyDown += OnPreviewKeyDown;
}
public MainWindow(MainViewModel viewModel) : this()
@@ -38,32 +41,6 @@ public partial class MainWindow : Window
WindowStateStore.Save(this);
}
- /// Custom min button — chrome'd window has no system caption buttons.
- private void OnMinimize(object sender, RoutedEventArgs e) =>
- WindowState = WindowState.Minimized;
-
- /// Toggles maximize/restore. Bound to the maximize button + double-click on the drag region.
- private void OnMaximizeRestore(object sender, RoutedEventArgs e) =>
- WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
-
- /// Custom close button.
- private void OnClose(object sender, RoutedEventArgs e) => Close();
-
- ///
- /// Toggle the right-hand settings pane. Collapses the 380px column to 0
- /// so the participants list claims the full content width, then restores
- /// it on the next click. The pane's children stay loaded — we move the
- /// grid column, not the panel's Visibility — so scroll position and any
- /// in-flight edits survive the toggle.
- ///
- private void OnSettingsToggleClick(object sender, RoutedEventArgs e)
- {
- if (SettingsColumn is null) return;
- SettingsColumn.Width = SettingsColumn.Width.Value > 0
- ? new System.Windows.GridLength(0)
- : new System.Windows.GridLength(380);
- }
-
/// Opens the About dialog — version, NDI runtime, build SHA.
private void OnAboutClick(object sender, RoutedEventArgs e)
{
@@ -131,14 +108,9 @@ public partial class MainWindow : Window
///
/// Three-state click behavior matching operator intuition:
/// 1. Teams not running → launch it via TeamsLauncher's fallback chain.
- /// 2. Teams running but its windows are hidden (we toggled them off, OR
- /// it launched into the tray) → restore the windows + foreground them.
- /// This is the case the previous "ask to stop" dialog was ambushing —
- /// operators don't think of a hidden Teams as "running and ready to
- /// stop", they think of it as "I clicked Launch and nothing happened".
- /// 3. Teams running with visible windows → bring the most recent one to
- /// the foreground. (Stopping Teams is now a right-click action;
- /// see OnLaunchTeamsRightClick.)
+ /// 2. Teams running but its windows are hidden → restore + foreground them.
+ /// 3. Teams running with visible windows → bring the most recent to front.
+ /// (Stopping Teams is a right-click action; see OnLaunchTeamsRightClick.)
///
private void OnLaunchTeamsClick(object sender, RoutedEventArgs e)
{
@@ -173,10 +145,6 @@ public partial class MainWindow : Window
return;
}
- // Teams is running. Always try to restore + foreground its window —
- // if windows are already visible, ShowWindows is a SetForegroundWindow
- // no-op besides bringing them forward; if they were hidden by our
- // own toggle, this is the operator's intuitive "show me Teams" path.
var shown = TeamsLauncher.ShowWindows();
_teamsWindowsHidden = false;
toast?.Show(shown > 0
@@ -185,23 +153,10 @@ public partial class MainWindow : Window
}
///
- /// Right-click on the rail Launch button asks to stop Teams. Split out
- /// from the left-click so a normal click is "open / surface" rather than
- /// the previous "open OR ambush you with a stop dialog".
+ /// Right-click on the Launch button asks to stop Teams. Split out from the
+ /// left-click so a normal click is "open / surface" rather than the previous
+ /// "open OR ambush you with a stop dialog".
///
- ///
- /// Open the experimental Teams embed window. Operator enables the
- /// preference first; this button materializes the host. See
- /// for the SetParent lifecycle.
- ///
- private void OnOpenEmbedWindowClick(object sender, RoutedEventArgs e)
- {
- // Non-modal so the operator can keep using TeamsISO's controls.
- // Owner = this so it minimizes / closes with TeamsISO.
- var w = new TeamsEmbedWindow { Owner = this };
- w.Show();
- }
-
private void OnLaunchTeamsRightClick(object sender, MouseButtonEventArgs e)
{
if (!TeamsLauncher.IsRunning()) return;
@@ -228,17 +183,65 @@ public partial class MainWindow : Window
}
///
- /// Swap the maximize-button glyph between the "single rectangle" (when normal) and the
- /// "two-overlapping-rectangles" (when maximized) variants, matching the Windows 11
- /// caption-button conventions.
+ /// Open the experimental Teams embed window. Operator enables the
+ /// preference first; this button materializes the host.
///
- private void OnWindowStateChanged(object? sender, EventArgs e)
+ private void OnOpenEmbedWindowClick(object sender, RoutedEventArgs e)
{
- if (FindName("MaximizeIcon") is not Path icon) return;
- icon.Data = WindowState == WindowState.Maximized
- // Two-rectangle "restore" glyph
- ? System.Windows.Media.Geometry.Parse("M 2,0 L 10,0 L 10,8 M 0,2 L 8,2 L 8,10 L 0,10 Z")
- // Single-rectangle "maximize" glyph
- : System.Windows.Media.Geometry.Parse("M 0,0 L 10,0 L 10,10 L 0,10 Z");
+ var w = new TeamsEmbedWindow { Owner = this };
+ w.Show();
+ }
+
+ ///
+ /// Toggle the v2 settings drawer overlay. The header gear button and the
+ /// drawer's own Close button both call this. State is held by the
+ /// overlay's directly — no separate
+ /// flag — so the toggle is idempotent regardless of how many entry
+ /// points open / close it.
+ ///
+ private void OnSettingsToggleClick(object sender, RoutedEventArgs e)
+ {
+ if (SettingsDrawerOverlay is null) return;
+ SettingsDrawerOverlay.Visibility = SettingsDrawerOverlay.Visibility == Visibility.Visible
+ ? Visibility.Collapsed
+ : Visibility.Visible;
+ }
+
+ ///
+ /// Clicking the scrim behind the drawer dismisses it — same affordance as
+ /// every well-behaved slide-over on every platform.
+ ///
+ private void OnSettingsScrimClick(object sender, MouseButtonEventArgs e)
+ {
+ if (SettingsDrawerOverlay is null) return;
+ SettingsDrawerOverlay.Visibility = Visibility.Collapsed;
+ }
+
+ ///
+ /// Command palette placeholder. Task 40 builds the real Ctrl+K floating
+ /// window; for the v2 shell commit, the header button opens the same
+ /// help dialog that F1 does so the affordance isn't dead.
+ ///
+ private void OnCommandPaletteClick(object sender, RoutedEventArgs e)
+ {
+ if (DataContext is MainViewModel vm && vm.ShowHelpCommand.CanExecute(null))
+ {
+ vm.ShowHelpCommand.Execute(null);
+ }
+ }
+
+ ///
+ /// Esc closes the drawer when it's open. We use PreviewKeyDown rather than
+ /// KeyDown so the drawer's nested inputs (TextBox, ComboBox) don't swallow
+ /// the key before this handler sees it.
+ ///
+ private void OnPreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
+ {
+ if (e.Key != Key.Escape) return;
+ if (SettingsDrawerOverlay?.Visibility == Visibility.Visible)
+ {
+ SettingsDrawerOverlay.Visibility = Visibility.Collapsed;
+ e.Handled = true;
+ }
}
}
diff --git a/src/TeamsISO.App/Services/ThemeManager.cs b/src/TeamsISO.App/Services/ThemeManager.cs
new file mode 100644
index 0000000..1ec2c9a
--- /dev/null
+++ b/src/TeamsISO.App/Services/ThemeManager.cs
@@ -0,0 +1,194 @@
+using System;
+using System.Linq;
+using System.Windows;
+using Microsoft.Win32;
+
+namespace TeamsISO.App.Services;
+
+///
+/// Owns the active theme for the WPF host. Three preferences:
+///
+/// System — follows the Windows app-mode setting (default for new
+/// users; re-reads on ).
+/// Dark — pin dark regardless of OS.
+/// Light — pin light regardless of OS.
+///
+/// The two color files (Theme.Dark.xaml + Theme.Light.xaml) are
+/// kept in lockstep on the same set of brush keys; this manager swaps the
+/// MergedDictionaries entry at runtime. Styles + control templates in
+/// WildDragonTheme.xaml reach the brushes via ,
+/// so the visual tree re-resolves without an app restart.
+///
+/// Preference is persisted via 's Theme field,
+/// reserved on disk during the v1 → v2 rollout so the rebuild doesn't lose the
+/// operator's choice.
+///
+public sealed class ThemeManager
+{
+ public static ThemeManager Current { get; } = new();
+
+ private const string DarkUri = "/Themes/Theme.Dark.xaml";
+ private const string LightUri = "/Themes/Theme.Light.xaml";
+ private const string PreferenceKeySystem = "System";
+ private const string PreferenceKeyDark = "Dark";
+ private const string PreferenceKeyLight = "Light";
+
+ private ThemeManager()
+ {
+ // Hydrate preference from disk on first access. UIPreferences.Load()
+ // is best-effort — disk failures fall back to defaults so the app
+ // always boots into a deterministic theme.
+ try
+ {
+ var prefs = UIPreferences.Load();
+ if (IsValidPreference(prefs.Theme))
+ {
+ _preference = prefs.Theme;
+ }
+ }
+ catch
+ {
+ // Defensive — singleton ctor must not throw or the app loses theming.
+ }
+
+ // Re-evaluate when Windows app-mode flips, but only when the
+ // operator hasn't pinned a preference. The explicit choice wins.
+ SystemEvents.UserPreferenceChanged += OnSystemPreferenceChanged;
+ }
+
+ private string _preference = PreferenceKeySystem;
+
+ /// Current preference. One of "System", "Dark", "Light".
+ public string Preference => _preference;
+
+ /// Fires after a theme swap with the resolved (absolute) theme.
+ public event EventHandler? Themed;
+
+ ///
+ /// Resolve the preference to an absolute theme name ("Dark" or "Light")
+ /// suitable for the dictionary lookup. "System" resolves to the OS
+ /// app-mode at the time of the call.
+ ///
+ public string ResolveTheme() => _preference switch
+ {
+ PreferenceKeyDark => PreferenceKeyDark,
+ PreferenceKeyLight => PreferenceKeyLight,
+ _ => IsSystemDark() ? PreferenceKeyDark : PreferenceKeyLight,
+ };
+
+ ///
+ /// Set the operator's preference, persist, and apply the resolved theme.
+ ///
+ public void Set(string preference)
+ {
+ if (!IsValidPreference(preference))
+ {
+ throw new ArgumentException(
+ "Preference must be 'System', 'Dark', or 'Light'.",
+ nameof(preference));
+ }
+
+ _preference = preference;
+ try { UIPreferences.SetTheme(preference); }
+ catch { /* persistence is best-effort */ }
+ Apply();
+ }
+
+ ///
+ /// Cycle the theme between Dark and Light (one-click toggle from the header
+ /// theme icon). If the current preference is "System", the cycle pins to
+ /// the OPPOSITE of the currently-resolved theme so the click has a
+ /// visible effect.
+ ///
+ public void Toggle()
+ {
+ var current = ResolveTheme();
+ Set(current == PreferenceKeyDark ? PreferenceKeyLight : PreferenceKeyDark);
+ }
+
+ ///
+ /// Apply the current resolved theme. Should be called once during app
+ /// startup (after Application.Current.Resources is initialized) and
+ /// whenever changes — already
+ /// does the latter for you.
+ ///
+ public void Apply()
+ {
+ var theme = ResolveTheme();
+ var uri = theme == PreferenceKeyDark ? DarkUri : LightUri;
+ SwapColorDictionary(uri);
+ Themed?.Invoke(this, theme);
+ }
+
+ private static void SwapColorDictionary(string newUri)
+ {
+ var app = Application.Current;
+ if (app is null) return;
+ var dicts = app.Resources.MergedDictionaries;
+
+ // Find the existing theme color dictionary by source URI. We
+ // distinguish "color" dictionaries from "WildDragonTheme" by name —
+ // the color files are at Theme.Dark.xaml / Theme.Light.xaml; the
+ // styles file is at WildDragonTheme.xaml. Replace in place to
+ // preserve merge order so DynamicResource refs resolve to the new
+ // brushes.
+ ResourceDictionary? old = null;
+ for (var i = 0; i < dicts.Count; i++)
+ {
+ var src = dicts[i].Source?.OriginalString ?? string.Empty;
+ if (src.EndsWith("Theme.Dark.xaml", StringComparison.OrdinalIgnoreCase) ||
+ src.EndsWith("Theme.Light.xaml", StringComparison.OrdinalIgnoreCase))
+ {
+ old = dicts[i];
+ break;
+ }
+ }
+
+ var fresh = new ResourceDictionary { Source = new Uri(newUri, UriKind.Relative) };
+ if (old is null)
+ {
+ dicts.Insert(0, fresh);
+ }
+ else
+ {
+ var idx = dicts.IndexOf(old);
+ dicts.RemoveAt(idx);
+ dicts.Insert(idx, fresh);
+ }
+ }
+
+ ///
+ /// Read Windows' AppsUseLightTheme registry value. 1 = light, 0 = dark.
+ /// Returns true (dark) on any read failure — the dark scene is the
+ /// default per DESIGN.md so a missing value still lands somewhere sensible.
+ ///
+ private static bool IsSystemDark()
+ {
+ try
+ {
+ using var key = Registry.CurrentUser.OpenSubKey(
+ @"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize");
+ if (key?.GetValue("AppsUseLightTheme") is int value)
+ {
+ return value == 0;
+ }
+ }
+ catch
+ {
+ // Registry access can fail under unusual security contexts.
+ }
+ return true;
+ }
+
+ private void OnSystemPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e)
+ {
+ if (e.Category != UserPreferenceCategory.General) return;
+ if (_preference != PreferenceKeySystem) return;
+ // Marshal to the UI thread — registry events fire on a system pool
+ // thread and resource dictionary mutations require dispatcher access.
+ Application.Current?.Dispatcher.BeginInvoke(new Action(Apply));
+ }
+
+ private static bool IsValidPreference(string? value) =>
+ value is PreferenceKeySystem or PreferenceKeyDark or PreferenceKeyLight;
+}
diff --git a/src/TeamsISO.App/Services/UIPreferences.cs b/src/TeamsISO.App/Services/UIPreferences.cs
index 45129a4..e460d04 100644
--- a/src/TeamsISO.App/Services/UIPreferences.cs
+++ b/src/TeamsISO.App/Services/UIPreferences.cs
@@ -52,7 +52,20 @@ public static class UIPreferences
// into a TeamsISO-owned host. WebView2 in modern Teams can render
// weirdly after reparent; if so the operator unticks and falls
// back to auto-hide mode. Off by default.
- bool EmbedTeamsWindow = false);
+ bool EmbedTeamsWindow = false,
+ // Theme preference for the v2 redesign. One of "System" (follow
+ // Windows app-mode), "Dark", or "Light". ThemeManager hydrates
+ // from this on startup and persists back here on toggle. Default
+ // "System" matches DESIGN.md's "Follow Windows" choice — the
+ // operator who doesn't care gets whatever Windows is set to.
+ string Theme = "System");
+
+ /// Update just the Theme field without touching other prefs.
+ public static void SetTheme(string theme)
+ {
+ var current = Load();
+ Save(current with { Theme = theme });
+ }
public static Prefs Load()
{
diff --git a/src/TeamsISO.App/Themes/Theme.Dark.xaml b/src/TeamsISO.App/Themes/Theme.Dark.xaml
new file mode 100644
index 0000000..8bad20b
--- /dev/null
+++ b/src/TeamsISO.App/Themes/Theme.Dark.xaml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/TeamsISO.App/Themes/Theme.Light.xaml b/src/TeamsISO.App/Themes/Theme.Light.xaml
new file mode 100644
index 0000000..f1e64bc
--- /dev/null
+++ b/src/TeamsISO.App/Themes/Theme.Light.xaml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/TeamsISO.App/Themes/WildDragonTheme.xaml b/src/TeamsISO.App/Themes/WildDragonTheme.xaml
index 82f205f..e213c31 100644
--- a/src/TeamsISO.App/Themes/WildDragonTheme.xaml
+++ b/src/TeamsISO.App/Themes/WildDragonTheme.xaml
@@ -40,42 +40,23 @@
1216
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ 0:0:0.090
+ 0:0:0.180
+ 0:0:0.280
-
-
+
+
-
+
-
-
+
+
-
+
@@ -193,8 +174,8 @@
@@ -764,7 +755,7 @@
-
+
@@ -788,7 +779,7 @@
(the RepeatButton does the work) but render as invisible hit zones.
-->
@@ -902,10 +893,10 @@
verbose explanation doesn't stretch across a whole monitor.
-->