diff --git a/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/LoginCard.razor b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/LoginCard.razor
new file mode 100644
index 0000000..4cfc8fd
--- /dev/null
+++ b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/LoginCard.razor
@@ -0,0 +1,39 @@
+@namespace ZB.MOM.WW.Theme
+@* Components/LoginCard.razor — static form-POST sign-in card. *@
+
+
+@code {
+ [Parameter, EditorRequired] public string Product { get; set; } = string.Empty;
+ [Parameter] public string Action { get; set; } = "/auth/login";
+ [Parameter] public string? ReturnUrl { get; set; }
+ [Parameter] public string? Error { get; set; }
+ [Parameter] public RenderFragment? ChildContent { get; set; }
+}
diff --git a/ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/LoginCardTests.cs b/ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/LoginCardTests.cs
new file mode 100644
index 0000000..a98a74c
--- /dev/null
+++ b/ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/LoginCardTests.cs
@@ -0,0 +1,44 @@
+namespace ZB.MOM.WW.Theme.Tests;
+
+public class LoginCardTests : TestContext
+{
+ [Fact]
+ public void Posts_to_action_with_username_password_fields()
+ {
+ var cut = RenderComponent(p => p
+ .Add(x => x.Product, "OtOpcUa")
+ .Add(x => x.Action, "/auth/login"));
+ var form = cut.Find("form");
+ Assert.Equal("post", form.GetAttribute("method"));
+ Assert.Equal("/auth/login", form.GetAttribute("action"));
+ Assert.NotNull(cut.Find("input#username"));
+ Assert.NotNull(cut.Find("input#password"));
+ Assert.Contains("OtOpcUa", cut.Find(".login-title").TextContent);
+ }
+
+ [Fact]
+ public void ReturnUrl_renders_hidden_input()
+ {
+ var cut = RenderComponent(p => p
+ .Add(x => x.Product, "OtOpcUa")
+ .Add(x => x.ReturnUrl, "/clusters"));
+ var hidden = cut.Find("input[name=returnUrl]");
+ Assert.Equal("/clusters", hidden.GetAttribute("value"));
+ }
+
+ [Fact]
+ public void Error_renders_notice()
+ {
+ var cut = RenderComponent(p => p
+ .Add(x => x.Product, "OtOpcUa")
+ .Add(x => x.Error, "Bad credentials"));
+ Assert.Contains("Bad credentials", cut.Find(".notice").TextContent);
+ }
+
+ [Fact]
+ public void No_returnUrl_no_hidden_input()
+ {
+ var cut = RenderComponent(p => p.Add(x => x.Product, "OtOpcUa"));
+ Assert.Empty(cut.FindAll("input[name=returnUrl]"));
+ }
+}