diff --git a/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/ThemeShell.razor b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/ThemeShell.razor
new file mode 100644
index 0000000..0e4977b
--- /dev/null
+++ b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/ThemeShell.razor
@@ -0,0 +1,32 @@
+@* Components/ThemeShell.razor — the one canonical side-rail chassis.
+ Not a LayoutComponentBase: the app's thin MainLayout delegates to this. *@
+@namespace ZB.MOM.WW.Theme
+
+
+
+
+
+
@ChildContent
+
+
+@code {
+ [Parameter, EditorRequired] public string Product { get; set; } = string.Empty;
+ [Parameter] public string? Accent { get; set; }
+ [Parameter] public RenderFragment? Logo { get; set; }
+ [Parameter] public RenderFragment? Nav { get; set; }
+ [Parameter] public RenderFragment? RailFooter { get; set; }
+ [Parameter] public RenderFragment? ChildContent { get; set; }
+
+ private string? AccentStyle => Accent is null ? null : $"--accent: {Accent}";
+}
diff --git a/ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/ThemeShellTests.cs b/ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/ThemeShellTests.cs
new file mode 100644
index 0000000..cb8c6c8
--- /dev/null
+++ b/ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/ThemeShellTests.cs
@@ -0,0 +1,43 @@
+namespace ZB.MOM.WW.Theme.Tests;
+
+public class ThemeShellTests : TestContext
+{
+ [Fact]
+ public void Renders_product_nav_and_body()
+ {
+ var cut = RenderComponent(p => p
+ .Add(x => x.Product, "OtOpcUa")
+ .Add(x => x.Nav, (RenderFragment)(b => b.AddMarkupContent(0, "N")))
+ .AddChildContent("BODY
"));
+ Assert.NotNull(cut.Find(".side-rail .brand"));
+ Assert.Contains("OtOpcUa", cut.Markup);
+ Assert.NotNull(cut.Find(".side-rail .rail-link"));
+ Assert.NotNull(cut.Find("main.page .pagebody"));
+ }
+
+ [Fact]
+ public void Accent_sets_css_variable_on_shell_root()
+ {
+ var cut = RenderComponent(p => p
+ .Add(x => x.Product, "ScadaBridge")
+ .Add(x => x.Accent, "#2f855a"));
+ var shell = cut.Find(".app-shell");
+ Assert.Contains("--accent: #2f855a", shell.GetAttribute("style"));
+ }
+
+ [Fact]
+ public void No_accent_emits_no_style()
+ {
+ var cut = RenderComponent(p => p.Add(x => x.Product, "MXAccess Gateway"));
+ Assert.False(cut.Find(".app-shell").HasAttribute("style"));
+ }
+
+ [Fact]
+ public void RailFooter_renders_when_supplied()
+ {
+ var cut = RenderComponent(p => p
+ .Add(x => x.Product, "OtOpcUa")
+ .Add(x => x.RailFooter, (RenderFragment)(b => b.AddMarkupContent(0, "S"))));
+ Assert.NotNull(cut.Find(".rail-foot .sess"));
+ }
+}