diff --git a/Controls/LeftPanel.xaml b/Controls/LeftPanel.xaml
index 6276f79..6a6e8c8 100644
--- a/Controls/LeftPanel.xaml
+++ b/Controls/LeftPanel.xaml
@@ -447,6 +447,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ViewModels/MainViewModel.Scoreboard.cs b/ViewModels/MainViewModel.Scoreboard.cs
index 43433b9..b33cebe 100644
--- a/ViewModels/MainViewModel.Scoreboard.cs
+++ b/ViewModels/MainViewModel.Scoreboard.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Media;
@@ -59,6 +60,76 @@ public partial class MainViewModel
/// (unit 7) — kept here so the panel binds one object instead of six primitives.
internal ScoreboardStyle ScoreboardStyleState => _scoreboardStyle;
+ // ─── Properties panel surface (per layout: one scoreboard look) ────────────
+
+ public string ScoreboardTextColor
+ {
+ get => _scoreboardStyle.TextColor;
+ set { _scoreboardStyle = _scoreboardStyle with { TextColor = value }; PushStyle(); }
+ }
+
+ public bool ScoreboardBackgroundEnabled
+ {
+ get => _scoreboardStyle.BackgroundEnabled;
+ set { _scoreboardStyle = _scoreboardStyle with { BackgroundEnabled = value }; PushStyle(); }
+ }
+
+ public string ScoreboardBackgroundColor
+ {
+ get => _scoreboardStyle.BackgroundColor;
+ set { _scoreboardStyle = _scoreboardStyle with { BackgroundColor = value }; PushStyle(); }
+ }
+
+ public double ScoreboardBackgroundOpacity
+ {
+ get => _scoreboardStyle.BackgroundOpacity;
+ set
+ {
+ _scoreboardStyle = _scoreboardStyle with { BackgroundOpacity = Math.Clamp(value, 0, 1) };
+ PushStyle();
+ }
+ }
+
+ public double ScoreboardFontSize
+ {
+ get => _scoreboardStyle.FontSize;
+ set
+ {
+ _scoreboardStyle = _scoreboardStyle with
+ {
+ FontSize = Math.Clamp(value, ScoreboardLayout.FloorFontSize, ScoreboardLayout.MaxFontSize),
+ };
+ PushStyle();
+ }
+ }
+
+ /// The colours offered for text and plate: the app's one palette.
+ public IReadOnlyList ScoreboardColorOptions => BorderColorPalette.All;
+
+ public bool ScoreboardShowSubscribers { get => _scoreboardTiles.Subscribers; set => SetTile(t => t.Subscribers = value); }
+ public bool ScoreboardShowWatchHours { get => _scoreboardTiles.WatchHours; set => SetTile(t => t.WatchHours = value); }
+ public bool ScoreboardShowSuperChats { get => _scoreboardTiles.SuperChats; set => SetTile(t => t.SuperChats = value); }
+ public bool ScoreboardShowNewMembers { get => _scoreboardTiles.NewMembers; set => SetTile(t => t.NewMembers = value); }
+ public bool ScoreboardShowMilestones { get => _scoreboardTiles.Milestones; set => SetTile(t => t.Milestones = value); }
+ public bool ScoreboardShowLastMember { get => _scoreboardTiles.LastMember; set => SetTile(t => t.LastMember = value); }
+ public bool ScoreboardShowLastSuperChat { get => _scoreboardTiles.LastSuperChat; set => SetTile(t => t.LastSuperChat = value); }
+ public bool ScoreboardShowLastGift { get => _scoreboardTiles.LastGift; set => SetTile(t => t.LastGift = value); }
+
+ private void SetTile(Action change)
+ {
+ change(_scoreboardTiles);
+ _scoreboardPresenter?.SetTiles(_scoreboardTiles);
+ RefreshScoreboardPreviews();
+ OnPropertyChanged(nameof(ScoreboardTilesState));
+ }
+
+ private void PushStyle()
+ {
+ _scoreboardPresenter?.SetStyle(_scoreboardStyle);
+ RefreshScoreboardPreviews();
+ OnPropertyChanged(nameof(ScoreboardStyleState));
+ }
+
/// Publishes the strip to every scoreboard layer's preview element. The pump thread
/// must never touch a WriteableBitmap, so this runs on the UI thread only.
private void RefreshScoreboardPreviews()
diff --git a/ytLive.Tests/ScoreboardPropertiesTests.cs b/ytLive.Tests/ScoreboardPropertiesTests.cs
new file mode 100644
index 0000000..899e0de
--- /dev/null
+++ b/ytLive.Tests/ScoreboardPropertiesTests.cs
@@ -0,0 +1,54 @@
+using System.Reflection;
+using Xunit;
+using ytLive.ViewModels;
+
+namespace ytLive.Tests;
+
+/// The scoreboard's look and its eight fact switches live in the LAYER PROPERTIES, and the
+/// panel must use the app's widgets — a hand-painted control is the TASK 58/63 defect, and the
+/// guard that catches it is here because the type-based sweep cannot see a bespoke picker.
+public class ScoreboardPropertiesTests
+{
+ [Fact]
+ public void The_scoreboard_properties_are_themed_controls_bound_to_the_layer_surface()
+ {
+ var panel = SourceAssert.Text("Controls", "LeftPanel.xaml");
+
+ // Gated on the selected layer being a scoreboard.
+ Assert.Contains("SelectedElement.IsScoreboard", panel);
+
+ // The eight fact switches.
+ foreach (var binding in new[]
+ {
+ "ScoreboardShowSubscribers", "ScoreboardShowWatchHours", "ScoreboardShowSuperChats",
+ "ScoreboardShowNewMembers", "ScoreboardShowMilestones", "ScoreboardShowLastMember",
+ "ScoreboardShowLastSuperChat", "ScoreboardShowLastGift",
+ })
+ Assert.Contains(binding, panel);
+
+ // Look controls: text colour, plate colour, plate opacity, text size.
+ foreach (var binding in new[]
+ {
+ "ScoreboardTextColor", "ScoreboardBackgroundColor",
+ "ScoreboardBackgroundOpacity", "ScoreboardFontSize",
+ })
+ Assert.Contains(binding, panel);
+
+ // Theme widgets only: the colour pickers are the app's drop-down (YtComboBox), never a
+ // hand-built menu, and the toggles are YtCheckBox.
+ var block = panel[panel.IndexOf("Scoreboard config", System.StringComparison.Ordinal)..];
+ block = block[..block.IndexOf("", System.StringComparison.Ordinal)];
+ Assert.Contains("Style=\"{StaticResource YtComboBox}\"", block);
+ Assert.Contains("Style=\"{StaticResource YtCheckBox}\"", block);
+ Assert.DoesNotContain("ContextMenu", block);
+
+ // The VM surface exists for every binding above (a typo would bind to nothing silently).
+ var type = typeof(MainViewModel);
+ foreach (var name in new[]
+ {
+ "ScoreboardTextColor", "ScoreboardBackgroundColor", "ScoreboardBackgroundOpacity",
+ "ScoreboardFontSize", "ScoreboardColorOptions", "ScoreboardShowSubscribers",
+ })
+ Assert.NotNull(type.GetProperty(name));
+ }
+}