diff --git a/CLAUDE.md b/CLAUDE.md
index 6ac9ed2..d729eae 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -6,9 +6,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
`scadaproj` is primarily an umbrella/index workspace that aggregates a family of
related SCADA / OT / Wonderware / OPC UA "sister projects" that live as **sibling
-directories under `~/Desktop/`**. It now also **hosts one piece of source itself** —
-the shared [`ZB.MOM.WW.Auth/`](ZB.MOM.WW.Auth/) library (its own nested git repo), the
-realized output of the auth component normalization (see [Component normalization](#component-normalization)).
+directories under `~/Desktop/`**. It now also **hosts two pieces of source itself** —
+the shared [`ZB.MOM.WW.Auth/`](ZB.MOM.WW.Auth/) library and the shared
+[`ZB.MOM.WW.Theme/`](ZB.MOM.WW.Theme/) UI kit — both the realized output of their
+respective component normalizations (see [Component normalization](#component-normalization)).
The point of this file is to give a high-level scan of each sister project — its purpose,
location, stack, and primary commands — so a fresh Claude Code session can orient across
the whole family without opening each repo first.
@@ -117,6 +118,7 @@ each project's **code-verified current state**, and the **gaps** between. See
| Component | Status | Goal | Design | Implementation |
|---|---|---|---|---|
| Auth (login / identity / authz) | Built (lib `0.1.0`) | Shared `ZB.MOM.WW.Auth` lib | [`components/auth/`](components/auth/) | [`ZB.MOM.WW.Auth/`](ZB.MOM.WW.Auth/) |
+| UI Theme (layout / tokens / components) | Built (lib `0.1.0`) | Shared `ZB.MOM.WW.Theme` RCL | [`components/ui-theme/`](components/ui-theme/) | [`ZB.MOM.WW.Theme/`](ZB.MOM.WW.Theme/) |
The auth component is fully populated: a normalized [`spec`](components/auth/spec/SPEC.md), a
proposed [`shared-contract`](components/auth/shared-contract/ZB.MOM.WW.Auth.md), three
@@ -132,6 +134,21 @@ The shared library is **built and lives in this repo** at [`ZB.MOM.WW.Auth/`](ZB
Build/test from `ZB.MOM.WW.Auth/`: `dotnet test`. Consumer matrix: OtOpcUa → Abstractions+Ldap+AspNetCore;
MxAccessGateway & ScadaBridge → all four (ApiKeys not used by OtOpcUa).
+The UI-theme component is fully populated: a normalized [`spec`](components/ui-theme/spec/SPEC.md),
+a [`design-tokens`](components/ui-theme/spec/DESIGN-TOKENS.md) reference, a
+[`shared-contract`](components/ui-theme/shared-contract/ZB.MOM.WW.Theme.md), three
+[`current-state`](components/ui-theme/current-state/) docs, and an adoption [`GAPS`](components/ui-theme/GAPS.md)
+backlog. Shared = Technical-Light tokens + IBM Plex fonts + side-rail shell + widgets; left
+per-project = each app's `site.css` page layout, route content, scoped `.razor.css`.
+
+The shared RCL is **built and lives in this repo** at [`ZB.MOM.WW.Theme/`](ZB.MOM.WW.Theme/)
+(.NET 10 Razor Class Library; single package; 32 bUnit tests; `dotnet pack` → 1 nupkg @ 0.1.0).
+The implementation plan is at
+[`docs/plans/2026-06-01-zb-mom-ww-theme-shared-library.md`](docs/plans/2026-06-01-zb-mom-ww-theme-shared-library.md).
+**Not yet adopted** by the three apps — that's the follow-on tracked in [`components/ui-theme/GAPS.md`](components/ui-theme/GAPS.md).
+Build/test from `ZB.MOM.WW.Theme/`: `dotnet test`. Consumer matrix: all three apps consume
+the single `ZB.MOM.WW.Theme` package (OtOpcUa AdminUI, MxGateway Server, ScadaBridge Host + CentralUI).
+
## Per-project primary commands
Run these from inside each project directory (not from `scadaproj`).
diff --git a/README.md b/README.md
index e721f5d..ac65e54 100644
--- a/README.md
+++ b/README.md
@@ -17,8 +17,10 @@ it produces.
| [`CLAUDE.md`](CLAUDE.md) | High-level index of the sister projects + working guidance |
| [`components/`](components/) | Component-normalization framework (per concern: target spec, current state, gaps) |
| [`components/auth/`](components/auth/) | First normalized component — login / identity / authorization |
-| [`docs/plans/`](docs/plans/) | Implementation plans (e.g. the ZB.MOM.WW.Auth build) |
+| [`components/ui-theme/`](components/ui-theme/) | Second normalized component — UI theme / design tokens / layout |
+| [`docs/plans/`](docs/plans/) | Implementation plans (e.g. the ZB.MOM.WW.Auth and ZB.MOM.WW.Theme builds) |
| [`ZB.MOM.WW.Auth/`](ZB.MOM.WW.Auth/) | **Built shared library** (.NET 10) — the realized output of the auth normalization |
+| [`ZB.MOM.WW.Theme/`](ZB.MOM.WW.Theme/) | **Built shared RCL** (.NET 10) — the realized output of the UI-theme normalization |
## The sister projects
@@ -52,6 +54,42 @@ The sister repos kept re-implementing the same cross-cutting concerns and drifti
| Component | Status | Folder |
|---|---|---|
| Auth (login / identity / authz) | Built (library 0.1.0); apps not yet adopted | [`components/auth/`](components/auth/) |
+| UI Theme (layout / tokens / components) | Built (RCL 0.1.0); apps not yet adopted | [`components/ui-theme/`](components/ui-theme/) |
+
+## `ZB.MOM.WW.Theme` — the shared UI kit
+
+The UI-theme component, realized as a single-package .NET 10 Razor Class Library the three
+apps can adopt to stop copy-pasting the Technical-Light design system. **Built and tested at
+0.1.0; adoption by the apps is the follow-on** (tracked in
+[`components/ui-theme/GAPS.md`](components/ui-theme/GAPS.md)).
+
+| Asset / Component | Purpose | Used by |
+|---|---|---|
+| `_content/ZB.MOM.WW.Theme/css/theme.css` | Design tokens, IBM Plex typography, Bootstrap overrides | all |
+| `_content/ZB.MOM.WW.Theme/css/layout.css` | Side-rail shell layout, nav CSS, chip/card helpers | all |
+| `_content/ZB.MOM.WW.Theme/fonts/*.woff2` | IBM Plex Sans 400/600 + Mono 500, vendored | all |
+| `ThemeHead`, `ThemeShell`, `BrandBar` | Shell entry point and chassis components | all |
+| `NavRailItem`, `NavRailSection` | Rail nav components | all |
+| `StatusPill` (`StatusState`) | Inline status chip — replaces per-app `StatusBadge` | all |
+| `LoginCard` | Static form-POST sign-in card | OtOpcUa, ScadaBridge (MxGateway when login page added) |
+| `TechButton`, `TechCard`, `TechField` | Common controls (Bootstrap 5 wrappers) | all |
+
+**Consumer matrix:** all three apps consume the single `ZB.MOM.WW.Theme` package —
+OtOpcUa `AdminUI`, MxAccessGateway `Server`, ScadaBridge `Host` + `CentralUI`.
+
+### Build & test
+
+```bash
+cd ZB.MOM.WW.Theme
+dotnet build -c Release # 0 warnings (TreatWarningsAsErrors)
+dotnet test # 32 bUnit tests
+./build/pack.sh # → ./artifacts/ZB.MOM.WW.Theme.0.1.0.nupkg
+```
+
+Stack: .NET 10 · Razor Class Library · bUnit · xUnit · central package management.
+More detail: [`ZB.MOM.WW.Theme/README.md`](ZB.MOM.WW.Theme/README.md).
+
+---
## `ZB.MOM.WW.Auth` — the shared library
@@ -98,4 +136,7 @@ ZB_LDAP_IT=1 dotnet test # requires a reachable GLAuth (e.g. a sister repo's i
- ✅ Auth component normalized (spec + canonical roles + current-state + gaps).
- ✅ `ZB.MOM.WW.Auth` shared library built and tested (0.1.0).
- ⬜ Adopt `ZB.MOM.WW.Auth` in OtOpcUa, MxAccessGateway, ScadaBridge — [`components/auth/GAPS.md`](components/auth/GAPS.md) (#8).
+- ✅ UI-theme component normalized (spec + design tokens + current-state + gaps).
+- ✅ `ZB.MOM.WW.Theme` shared UI kit built and tested (0.1.0); apps not yet adopted.
+- ⬜ Adopt `ZB.MOM.WW.Theme` in OtOpcUa [low risk], ScadaBridge [med], MxAccessGateway [high risk] — [`components/ui-theme/GAPS.md`](components/ui-theme/GAPS.md).
- ⬜ Normalize the next cross-cutting component.
diff --git a/ZB.MOM.WW.Theme/.gitignore b/ZB.MOM.WW.Theme/.gitignore
new file mode 100644
index 0000000..0808c4a
--- /dev/null
+++ b/ZB.MOM.WW.Theme/.gitignore
@@ -0,0 +1,482 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from `dotnet new gitignore`
+
+# dotenv files
+.env
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+# but not Directory.Build.rsp, as it configures directory-level build defaults
+!Directory.Build.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+.idea/
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Vim temporary swap files
+*.swp
diff --git a/ZB.MOM.WW.Theme/Directory.Build.props b/ZB.MOM.WW.Theme/Directory.Build.props
new file mode 100644
index 0000000..b234d68
--- /dev/null
+++ b/ZB.MOM.WW.Theme/Directory.Build.props
@@ -0,0 +1,10 @@
+
+
+ net10.0
+ enable
+ enable
+ latest
+ 0.1.0
+ true
+
+
diff --git a/ZB.MOM.WW.Theme/Directory.Packages.props b/ZB.MOM.WW.Theme/Directory.Packages.props
new file mode 100644
index 0000000..2dda526
--- /dev/null
+++ b/ZB.MOM.WW.Theme/Directory.Packages.props
@@ -0,0 +1,12 @@
+
+
+ true
+
+
+
+
+
+
+
+
+
diff --git a/ZB.MOM.WW.Theme/README.md b/ZB.MOM.WW.Theme/README.md
new file mode 100644
index 0000000..da25a9f
--- /dev/null
+++ b/ZB.MOM.WW.Theme/README.md
@@ -0,0 +1,93 @@
+# ZB.MOM.WW.Theme
+
+Shared Technical-Light UI kit for the ZB.MOM.WW SCADA family: design tokens + IBM Plex
+fonts (static web assets) and a canonical side-rail shell + widgets. The kit ships one
+.NET 10 Razor Class Library (RCL) with CSS custom-property tokens, the three IBM Plex
+woff2 fonts, and side-rail layout CSS — all served from `_content/ZB.MOM.WW.Theme/…` —
+plus a set of Blazor SSR components that carry no inline colours and reuse the token
+classes. Bootstrap 5 is **not** vendored; each app keeps its own Bootstrap link.
+
+## Adopt
+
+1. Reference the NuGet package in your app; keep your own Bootstrap 5 `` in `App.razor`.
+2. In `App.razor` `
`, **after** your Bootstrap link, add ``:
+ ```razor
+ @* Bootstrap — yours, not vendored by the kit *@
+
+ ```
+3. Make your `MainLayout` a thin delegate to `ThemeShell` — `@layout` cannot pass
+ parameters, so `ThemeShell` is a regular component and `MainLayout` is a 3-line wrapper:
+
+ ```razor
+ @* Layouts/MainLayout.razor *@
+ @inherits LayoutComponentBase
+
+
+ @* session info / sign-out link *@
+ @Body
+
+ ```
+
+Add `@using ZB.MOM.WW.Theme` to your `_Imports.razor` so all components are available
+without per-file usings.
+
+### Login page
+
+Use `` for the sign-in form. The card posts to a server endpoint
+(`/auth/login` by default) so `SignInAsync` can run before the response starts.
+You **must** inject `` inside the card and **validate `ReturnUrl`
+server-side** before redirecting (open-redirect risk):
+
+```razor
+
+
+
+```
+
+## Components
+
+| Component | Parameters (key) | Notes |
+|---|---|---|
+| `ThemeHead` | — | Emits `` tags for `theme.css` and `layout.css`. Place in `` after Bootstrap. |
+| `ThemeShell` | `Product`*, `Accent`, `Logo`, `Nav`, `RailFooter`, `ChildContent` | Side-rail chassis. Not a layout — delegated to from `MainLayout`. `Accent` overrides `--accent` token. |
+| `BrandBar` | `Product`*, `Logo` | Brand glyph + product name; rendered inside `ThemeShell`'s rail header. |
+| `NavRailItem` | `Href`*, `Text`*, `Icon`, `Match` | Wraps ``. |
+| `NavRailSection` | `Title`*, `Expanded` (default `true`), `ChildContent` | CSS-only collapsible `` group; no JS, works in static SSR. |
+| `StatusPill` (`StatusState`) | `State`* (`Ok`/`Warn`/`Bad`/`Idle`/`Info`), `ChildContent` | Inline chip. `StatusState` enum is in `ZB.MOM.WW.Theme`. |
+| `LoginCard` | `Product`*, `Action` (default `/auth/login`), `ReturnUrl`, `Error`, `ChildContent` | Static form-POST sign-in card. Inject `` via `ChildContent`; validate `ReturnUrl` server-side. |
+| `TechButton` (`ButtonVariant`) | `Variant` (`Primary`/`Secondary`/`Danger`/`Ghost`), `Type`, `Busy`, `ChildContent`, splatted attrs | `Busy` disables the button and shows a spinner. `ButtonVariant` enum is in `ZB.MOM.WW.Theme`. |
+| `TechCard` | `Title`, `Header`, `ChildContent`, `Footer`, `Class` | Panel with optional head/body/footer slots. |
+| `TechField` | `Label`*, `Hint`, `Error`, `ChildContent` | Form field wrapper with label, hint text, and inline error. |
+
+\* `EditorRequired` parameter.
+
+Flat namespace: all components and enums live in `ZB.MOM.WW.Theme`. One `@using` covers everything.
+
+## Static assets
+
+Served at `_content/ZB.MOM.WW.Theme/…` by the ASP.NET static-web-asset pipeline:
+
+| Path | Contents |
+|---|---|
+| `css/theme.css` | Design tokens (`--accent`, `--ok`, `--warn`, …), typography, utility helpers |
+| `css/layout.css` | Side-rail shell layout, collapsible nav, `StatusPill` variants, card/field helpers |
+| `fonts/ibm-plex-sans-400.woff2` | IBM Plex Sans Regular |
+| `fonts/ibm-plex-sans-600.woff2` | IBM Plex Sans SemiBold |
+| `fonts/ibm-plex-mono-500.woff2` | IBM Plex Mono Medium |
+
+`theme.css` declares `@font-face` with `url('../fonts/…')` — correct relative path from
+`css/` to `fonts/`. (OtOpcUa's original `url('fonts/…')` was a latent 404; the kit fixes it.)
+
+## Build
+
+```bash
+# from ZB.MOM.WW.Theme/
+dotnet build -c Release # TreatWarningsAsErrors — expect 0 warnings
+dotnet test # 32 bUnit tests
+./build/pack.sh # → ./artifacts/ZB.MOM.WW.Theme.0.1.0.nupkg
+```
diff --git a/ZB.MOM.WW.Theme/ZB.MOM.WW.Theme.slnx b/ZB.MOM.WW.Theme/ZB.MOM.WW.Theme.slnx
new file mode 100644
index 0000000..5ed5942
--- /dev/null
+++ b/ZB.MOM.WW.Theme/ZB.MOM.WW.Theme.slnx
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/ZB.MOM.WW.Theme/build/pack.sh b/ZB.MOM.WW.Theme/build/pack.sh
new file mode 100755
index 0000000..c166ead
--- /dev/null
+++ b/ZB.MOM.WW.Theme/build/pack.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+# pack.sh — produce the ZB.MOM.WW.Theme NuGet packages into ./artifacts.
+#
+# Usage:
+# ./build/pack.sh
+
+set -euo pipefail
+
+dotnet pack -c Release -o ./artifacts
diff --git a/ZB.MOM.WW.Theme/build/push.sh b/ZB.MOM.WW.Theme/build/push.sh
new file mode 100755
index 0000000..cd1ce5d
--- /dev/null
+++ b/ZB.MOM.WW.Theme/build/push.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+# push.sh — pack and push all ZB.MOM.WW.Theme NuGet packages to the Gitea feed.
+#
+# Required environment variables:
+# GITEA_NUGET_SOURCE — full URL of the Gitea NuGet feed
+# e.g. https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json
+# GITEA_NUGET_KEY — Gitea access token with package:write permission
+#
+# Usage:
+# export GITEA_NUGET_SOURCE="https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json"
+# export GITEA_NUGET_KEY="your-gitea-token"
+# ./build/push.sh
+
+set -euo pipefail
+
+: "${GITEA_NUGET_SOURCE:?set GITEA_NUGET_SOURCE to your Gitea NuGet feed URL}"
+: "${GITEA_NUGET_KEY:?set GITEA_NUGET_KEY to your Gitea access token}"
+
+dotnet pack -c Release -o ./artifacts
+
+dotnet nuget push "./artifacts/*.nupkg" \
+ --source "$GITEA_NUGET_SOURCE" \
+ --api-key "$GITEA_NUGET_KEY" \
+ --skip-duplicate
diff --git a/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/ButtonVariant.cs b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/ButtonVariant.cs
new file mode 100644
index 0000000..37d0146
--- /dev/null
+++ b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/ButtonVariant.cs
@@ -0,0 +1,3 @@
+namespace ZB.MOM.WW.Theme;
+
+public enum ButtonVariant { Primary, Secondary, Danger, Ghost }
diff --git a/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/BrandBar.razor b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/BrandBar.razor
new file mode 100644
index 0000000..d1420d9
--- /dev/null
+++ b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/BrandBar.razor
@@ -0,0 +1,12 @@
+@* Components/BrandBar.razor *@
+@namespace ZB.MOM.WW.Theme
+
+ @if (Logo is not null) { @Logo }
+ else { ▮ }
+ @Product
+
+
+@code {
+ [Parameter, EditorRequired] public string Product { get; set; } = string.Empty;
+ [Parameter] public RenderFragment? Logo { get; set; }
+}
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..0f6fb3e
--- /dev/null
+++ b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/LoginCard.razor
@@ -0,0 +1,58 @@
+@namespace ZB.MOM.WW.Theme
+@* Components/LoginCard.razor — static form-POST sign-in card.
+ SECURITY NOTES:
+ - ReturnUrl is echoed into a hidden field verbatim; the consuming app's POST handler
+ MUST validate it is a local/relative URL before redirecting to prevent open-redirect.
+ - This form is NOT auto-protected by Blazor antiforgery; the caller MUST pass an
+ antiforgery token via ChildContent (e.g. ). *@
+
+
+
+
@Product — sign in
+
+
+
+
+
+@code {
+ [Parameter, EditorRequired] public string Product { get; set; } = string.Empty;
+ [Parameter] public string Action { get; set; } = "/auth/login";
+
+ ///
+ /// Optional URL to redirect to after a successful login. Echoed into a hidden
+ /// returnUrl field. The consuming app's POST handler MUST validate this is
+ /// a local/relative URL before redirecting — do not redirect to arbitrary values
+ /// to prevent open-redirect vulnerabilities.
+ ///
+ [Parameter] public string? ReturnUrl { get; set; }
+
+ [Parameter] public string? Error { get; set; }
+
+ ///
+ /// Content rendered inside the form, before the username/password fields.
+ /// The caller MUST supply an antiforgery token here (e.g. <AntiforgeryToken />)
+ /// because this static POST form is not auto-protected by Blazor's antiforgery middleware.
+ ///
+ [Parameter] public RenderFragment? ChildContent { get; set; }
+}
diff --git a/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/NavRailItem.razor b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/NavRailItem.razor
new file mode 100644
index 0000000..dd5d982
--- /dev/null
+++ b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/NavRailItem.razor
@@ -0,0 +1,13 @@
+@* Components/NavRailItem.razor *@
+@namespace ZB.MOM.WW.Theme
+
+ @if (Icon is not null) { @Icon }
+ @Text
+
+
+@code {
+ [Parameter, EditorRequired] public string Href { get; set; } = string.Empty;
+ [Parameter, EditorRequired] public string Text { get; set; } = string.Empty;
+ [Parameter] public RenderFragment? Icon { get; set; }
+ [Parameter] public NavLinkMatch Match { get; set; } = NavLinkMatch.Prefix;
+}
diff --git a/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/NavRailSection.razor b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/NavRailSection.razor
new file mode 100644
index 0000000..b620734
--- /dev/null
+++ b/ZB.MOM.WW.Theme/src/ZB.MOM.WW.Theme/Components/NavRailSection.razor
@@ -0,0 +1,13 @@
+@* Components/NavRailSection.razor — CSS-only collapsible (no JS, works in static SSR).
+ Apps that want cookie-persisted expand state keep their own interactive NavSection. *@
+@namespace ZB.MOM.WW.Theme
+
+ @Title
+