Adds a small auto-dismissing pill notification at the bottom-center of the participants area: 'Settings saved' on Apply Changes, 'Transcoder topology applied — restart Teams to take effect' after the one-click NDI groups setup. ToastViewModel owns its own DispatcherTimer and resets the dismissal countdown on successive calls, so the most recent message is always the one visible. Hooked into MainViewModel and threaded into GlobalSettingsViewModel via constructor injection.
_NEXT.md rewritten to reflect the May 2026 hardening pass: separates engine / UI / networking / Phase E.1 / diagnostics / CI / tests sections, lists every shipped item, and re-prioritizes the remaining work (Phase E.2-E.3 embedded Teams, code-signing the MSI, refresh-discovery affordance, output thumbnail previews, settings panel UX, auto-disable on departure, operator presets).
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).
Six related polish items, all building on tonight's groundwork.
1. App icon: teamsiso.ico generated from dragon-mark.png at 7 sizes (16-256), wired as ApplicationIcon in the WPF csproj, MainWindow.Icon, AboutWindow.Icon, and ARPPRODUCTICON in the WiX MSI. Taskbar / window / Add-Remove-Programs all show the dragon mark now.
2. Running incoming FPS: ring buffer of last 30 frame timestamps in IsoPipeline; ComputeFps() returns moving-average rate. Surfaced on IsoHealthStats.IncomingFps and shown in the Source column of the participants DataGrid as 'WxH · 59.94 fps'. Resets cleanly on every supervisor restart.
3. Drops counter: FrameProcessor.Stats already aggregated FramesDropped (closest-frame strategy when the receiver outpaces the processor) and FramesDuplicated; just plumbed _liveProcessor through IsoPipeline so GetStats() can read them. Exposed in the Live column under the in/out counters as a coral-tinted 'drop N'.
4. Console --version flag: prints engine version (with embedded git SHA), .NET version, OS, NDI runtime banner, expected prefix, exit-code legend, plus a wilddragon.net link. Useful for support tickets.
5. About dialog: chromeless modal with the dragon mark + version / .NET / OS / NDI runtime fields and a link to wilddragon.net. Triggered by clicking the rail logo.
6. Teams launcher Stop toggle: TeamsLauncher gains IsRunning() and StopAll(). The rail's Teams button now toggles — if Teams is up, ask to close all Teams windows via WM_CLOSE; otherwise launch as before. Confirms before stopping so we don't kill the user's call mid-transition.
Tests: 74/74 unit + 9/9 NDI integration green throughout. MSI builds clean and now embeds the dragon icon for ARP.
Two related deliverables addressing the user's morning asks.
1. Branding: Dragon WHITE.png and Wild Dragon Logo WHITE.png from the brand kit are copied into src/TeamsISO.App/Assets/ and registered as <Resource> items in the .csproj. The rail's placeholder 'W' glyph is replaced by the real dragon mark (40x40, HighQuality bitmap scaling) with a 'Wild Dragon' caption underneath.
2. NDI Access Manager automation: NdiAccessManagerConfig service reads/writes %APPDATA%\\NDI\\ndi-config.v1.json, working in JsonNode trees so we don't clobber unrelated keys. ApplyTranscoderTopology() sets groups.send=[teamsiso-input] and groups.recv=[public, teamsiso-input] so all local senders (Teams + anything else) broadcast on the private group while local receivers can still see public sources too. Engine-side, the user's per-pipeline OutputGroups override pushes TeamsISO outputs back onto Public so downstream switchers see clean ISOs.
Atomic write: temp + replace, with timestamped backup of the prior config. ReadCurrentGroups() can be used by future UI to show what's currently configured. RestoreDefaults() reverts.
Settings panel grows an 'Apply transcoder topology' button under the NDI Network section. Click writes the system config, sets the engine's discovery=teamsiso-input / output=public, refreshes the bound text boxes, and pops a dialog with a 'restart Teams' reminder + the backup path.
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'.
IsoPipeline now publishes refs to its currently-live NdiReceiver and NdiSender (set by RunInnerPipelineAsync, cleared on exit) so a stats poll from any thread can read FramesCaptured / FramesSent without entangling the pipeline's lifetime with its observer. The receiver's raw-frame channel is wrapped with a TappedChannelWriter so the most recent RawFrame is captured for source-resolution display, again without changing the receiver's contract.
IsoController.GetStats() drops the stub return-Empty and instead reads the live pipeline.GetStats() outside the gate so a slow stats read can't serialize the controller's other operations.
WPF: MainViewModel runs a 1 Hz DispatcherTimer that pulls stats for every participant view-model and pushes them via UpdateStats(). ParticipantViewModel grows three displayable properties — FramesIn, FramesOut, IncomingResolution — bound into the participants DataGrid as a new 'Live' column showing the down/up frame counts and the source resolution underneath the machine name.
Tests: 74/74 unit + 9/9 NDI integration green; the existing round-trip integration test exercises the new wiring at runtime (live receiver/sender refs are set, frames flow, channels close cleanly).
Adopts the design language from Dammyjay93/interface-design: warm Stone neutrals, accent orange (#EA580C), borders-only depth (no shadows), 8px spacing grid, all-caps section labels, mono typography for machine names and timecodes.
Themes/StoneTheme.xaml is the single source of truth for design tokens (color brushes, typography styles, spacing) plus restyled control templates (Button, TextBox, ComboBox, ComboBoxItem, CheckBox, DataGrid + DataGridColumnHeader / DataGridRow / DataGridCell, ScrollBar). MainWindow consumes the tokens and is laid out with a header (title + status pill), section-headed Settings sidebar (Output Format / NDI Network / Display), card-wrapped participant list, and a mono status footer.
Settings sidebar surfaces the new NDI group configuration (discovery + output) and a Hide-(Local) checkbox. The latter filters the user's own self-preview from the participants list at the MainViewModel layer (HideLocalSelf=true by default) so operators don't accidentally route their own preview as an ISO. Apply Changes round-trips both FrameProcessingSettings and NdiGroupSettings through the controller in one go.