All Wd.Button.* styles set FocusVisualStyle=x:Null which suppresses WPF's default dotted focus rectangle. With no replacement IsKeyboardFocused trigger, tab-navigating gave NO visual cue — accessibility regression.
Match the keyboard-focus visual to the hover visual so mouse and keyboard land on the same affordance:
- Ghost: cyan border on focus (same as hover, minus the bg fill change so focus + hover compound visibly)
- Caption: lifts to Wd.Button.HoverBg
- RailIcon: lifts to Wd.Button.HoverBg + cyan icon
- IsoToggle: 2px cyan border (same as hover; status bg preserved)
Verified building cleanly. Tab-cycle through the IN-CALL bar / header pills / settings tabs now lights the focused control.
Default WPF ScrollBar template renders chunky Win9x line-up/line-down arrow buttons + a 3D thumb that look out of place on the dark canvas. Replace with a slim transparent track + tinted thumb pattern (Edge / VS Code / GitHub style):
- Thumb: BorderStrong by default, lifts to Text.Tertiary on hover, switches to cyan accent while dragging.
- No arrow buttons — track-clicks above/below the thumb still page-scroll via invisible RepeatButtons (preserves PageUp/PageDown behavior).
- Horizontal orientation has its own template via Style.Triggers so it picks the right Track command set (PageLeft/PageRight).
Affects every ScrollViewer in the app — settings panel, presets dialog list, Notes window, About dialog, Help cheat sheet.
Hover state on Wd.Button.Ghost / Caption / RailIcon / IsoToggle was barely visible — the Wd.SurfaceHover fill (#242424) was only 14 luminance points off the canvas (#0A0A0A) and 16 off the cards (#141414), so the hover state read as 'no change' to the user.
Changes:
1. Bumped Wd.SurfaceHover #242424 -> #2A2A2A and Wd.SurfaceActive #2D2D2D -> #363636 across the theme (carries to DataGrid rows etc. with the same too-subtle problem).
2. Added Wd.Button.HoverBg (#33333A) + Wd.Button.PressBg (#3F3F47) — dedicated stronger hover/press fills for buttons specifically. The slight blue tint adds chroma so hover reads obviously distinct from a flat darken.
3. Wd.Button.Ghost hover now also sets BorderBrush=Wd.Accent.Cyan — dual cue (lighter fill + bright cyan border) makes the affordance unmistakable on any surface.
4. Wd.Button.RailIcon hover sets foreground=Wd.Accent.Cyan in addition to the brighter fill (rail buttons have icons, not text — color shift on the icon is the strongest signal).
5. Wd.Button.IsoToggle keeps its status-coded background on hover (LIVE cyan / ERROR coral / NO SIGNAL amber) so at-a-glance state isn't lost; instead bumps to a 2px cyan border for the affordance.
Verified: cleanly built, app launches, hover over Refresh button now has obvious brighter fill + tooltip vs. neighbors at rest.
Four polish improvements aimed at production-floor usability.
1. Empty-state placeholder for the participants card. When Participants.Count == 0, the DataGrid is hidden in favor of a friendly 'Waiting for Teams' panel: faded dragon mark, headline, explainer, and a four-item checklist (Teams running? NDI broadcast on? Discovery group correct? Firewall clear?). New CountToVisibilityConverter (with optional 'empty' parameter to invert) drives both the placeholder and the DataGrid visibility from the same Participants.Count source.
2. Per-pipeline error / no-signal surfacing. IsoHealthStats grows an init-only State property populated from IsoPipeline.State. ParticipantViewModel.UpdateStats maps that to a StateLabel ('LIVE' / 'NO SIGNAL' / 'ERROR' / 'STARTING' / '—'). The ISO toggle button gains DataTriggers on StateLabel — coral-tinted '● ERROR' when the supervisor gives up, amber-tinted '● NO SIGNAL' when the slate threshold trips. Operators can see at a glance which pipelines are broken.
3. JetBrains Mono Variable v2.304 (OFL) bundled at Assets/Fonts/JetBrainsMono.ttf. Wd.Font.Mono now points at the embedded font so machine names, timecodes, and stat counters render in JetBrains Mono regardless of system fonts. Falls back to Cascadia Mono / Consolas if the resource is missing.
4. Tooltip pass over every interactive control in the settings panel (framerate / resolution / aspect / audio / discovery group / output group / hide-local checkbox / Apply button / per-row Output Name textbox / per-row ISO toggle). Operators learn affordances on hover instead of by trial and error.
Tests: 76/76 unit + 9/9 NDI integration green.
Four polish items + a test pass.
1. Inter Variable (rsms/inter v3.19, OFL) is bundled at Assets/Fonts/Inter.ttf (~800 KB) and registered as a WPF Resource. WildDragonTheme.xaml's Wd.Font.Sans now points at pack://application:,,,/Assets/Fonts/#Inter so the typography matches wilddragon.net regardless of whether the user has Inter installed system-wide. Falls back to Segoe UI Variable Display if the resource is missing.
2. 'Stop all ISOs' button at the right of the participants header. Bound to a new MainViewModel.StopAllIsosCommand that snapshots the enabled list, awaits DisableIsoAsync sequentially, and silently swallows per-pipeline failures (best-effort emergency stop). CanExecute gates on whether any ISO is currently enabled.
3. WindowStateStore service persists the main window's Left/Top/Width/Height/State to %LOCALAPPDATA%\\TeamsISO\\window.json on close and restores it on SourceInitialized. Multi-monitor friendly: a saved position with no corner inside any virtual screen is rejected so a disconnected monitor doesn't strand the window off-screen.
4. Two new unit tests cover FrameProcessor's drops + duplicates accounting. 76/76 unit tests pass (was 74).
MainWindow drops the standard Windows title bar (WindowStyle=None + WindowChrome with CaptionHeight=44, ResizeBorderThickness=6, UseAeroCaptionButtons=False) and draws its own minimize / maximize-restore / close buttons inline in the existing header strip. The custom buttons opt into shell:WindowChrome.IsHitTestVisibleInChrome=True so clicks fire on them rather than starting a window drag.
Result: the entire top of the window is now ours, matching the Microsoft Teams desktop client's flush header look. The 'TeamsISO + by Wild Dragon' branding sits at the same baseline as the engine-status pill and the caption controls, and dragging anywhere not occupied by an interactive widget moves the window.
Caption-button styles in the theme: 46x32 hover-tinted, with the close button turning the Windows 11 #C42B1C red on hover. Maximize-button glyph swaps between the single-rectangle and overlapping-rectangles variants on StateChanged.
Drive-by: ParticipantViewModel.{FramesIn,FramesOut,IncomingResolution} setters dropped from private to public so {Run Text=...} bindings (which default to TwoWay on Run) can attach without WPF throwing 'cannot work on read-only property'.
Replaces the Stone theme with Wild Dragon branding (canvas #0A0A0A, accent cyan #97EDF0, secondary #9AE0FD, coral alert #FB819C — sourced from wilddragon.net) and reorganizes MainWindow into a Microsoft Teams-style three-column layout: a 72px left rail (logo + Participants/Settings nav + engine-status indicator), a center content area (header + participants card), and a right settings panel.
Adds InitialsConverter so participant avatars render real initials (Brendon Power -> 'BP', '(Local)' -> 'L') instead of a generic glyph. Drops the obsolete StoneTheme.xaml; the project now ships exactly one theme dictionary.
Typography: Inter (with Segoe UI Variable Display fallback) for the sans stack, JetBrains Mono (Cascadia Mono fallback) for machine names and timecodes — matching the wilddragon.net site.
Verified live against the running Teams meeting: app launches, participant 'Brendon Power' displays with avatar, settings panel surfaces NDI groups + Hide-Local toggle, engine status pill shows green/live.