diff --git a/src/Web/AdminPanel/Components/Layout/MainLayout.razor.css b/src/Web/AdminPanel/Components/Layout/MainLayout.razor.css
index 600a6b702..cc170d025 100644
--- a/src/Web/AdminPanel/Components/Layout/MainLayout.razor.css
+++ b/src/Web/AdminPanel/Components/Layout/MainLayout.razor.css
@@ -9,7 +9,7 @@ main {
}
.sidebar {
- background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
+ background-image: var(--omu-sidebar-gradient);
}
.header-search {
@@ -24,6 +24,7 @@ main {
.breadcrumb-bar {
min-height: 2.5rem;
+ background-color: var(--omu-surface-2);
}
.breadcrumb-bar ::deep .breadcrumb-nav {
@@ -58,8 +59,8 @@ main {
}
#blazor-error-ui {
- color-scheme: light only;
- background: lightyellow;
+ background: var(--omu-error-bg);
+ color: var(--omu-error-text);
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
diff --git a/src/Web/AdminPanel/_Imports.razor b/src/Web/AdminPanel/_Imports.razor
index cb068c1e4..372482e58 100644
--- a/src/Web/AdminPanel/_Imports.razor
+++ b/src/Web/AdminPanel/_Imports.razor
@@ -5,6 +5,7 @@
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
+@using Microsoft.AspNetCore.Http
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
diff --git a/src/Web/Shared/Components/Form/AutoForm.razor.css b/src/Web/Shared/Components/Form/AutoForm.razor.css
index 73af722ac..59c4d3c40 100644
--- a/src/Web/Shared/Components/Form/AutoForm.razor.css
+++ b/src/Web/Shared/Components/Form/AutoForm.razor.css
@@ -1,6 +1,7 @@
.auto-form {
- background: #fff;
- border: 1px solid #dee2e6;
+ background: var(--omu-surface-2);
+ color: var(--omu-text);
+ border: 1px solid var(--omu-border);
border-radius: 0.375rem;
padding: 1.5rem;
margin-bottom: 1rem;
@@ -9,9 +10,9 @@
.form-actions {
position: sticky;
bottom: 0;
- background-color: #fff;
+ background-color: var(--omu-surface-2);
margin: 1rem 0 0 0;
- border-top: 1px solid #dee2e6;
+ border-top: 1px solid var(--omu-border);
display: flex;
gap: 0.5rem;
justify-content: flex-start;
@@ -38,6 +39,6 @@
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
- color: #6c757d;
+ color: var(--omu-text-muted);
pointer-events: none;
}
diff --git a/src/Web/Shared/Components/Form/Typeahead.razor.css b/src/Web/Shared/Components/Form/Typeahead.razor.css
index 861f20ebe..61db19d3d 100644
--- a/src/Web/Shared/Components/Form/Typeahead.razor.css
+++ b/src/Web/Shared/Components/Form/Typeahead.razor.css
@@ -13,16 +13,17 @@
padding: 0.375rem 2.2rem;
min-height: calc(1.5em + 0.75rem + 2px);
cursor: text;
- border: 1px solid #ced4da;
+ border: 1px solid var(--omu-border);
border-radius: 0.25rem;
- background-color: #fff;
+ background-color: var(--omu-surface-2);
+ color: var(--omu-text);
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.typeahead-controls:focus-within {
- color: #495057;
- background-color: #fff;
- border-color: #80bdff;
+ color: var(--omu-text);
+ background-color: var(--omu-surface-2);
+ border-color: var(--omu-link);
outline: 0;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
@@ -39,8 +40,9 @@
.typeahead-multi-value {
display: inline-flex;
align-items: center;
- background-color: #e9ecef;
- border: 1px solid #ced4da;
+ background-color: var(--omu-surface-hover);
+ color: var(--omu-text);
+ border: 1px solid var(--omu-border);
border-radius: 0.2rem;
padding: 0.1rem 0.4rem;
font-size: 0.875rem;
@@ -51,7 +53,7 @@
.typeahead-multi-value-remove {
background: none;
border: none;
- color: #6c757d;
+ color: var(--omu-text-muted);
cursor: pointer;
font-size: 1rem;
font-weight: bold;
@@ -93,7 +95,7 @@
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
- color: #6c757d;
+ color: var(--omu-text-muted);
pointer-events: none;
font-size: 0.9rem;
}
diff --git a/src/Web/Shared/Components/ThemeSelector.razor b/src/Web/Shared/Components/ThemeSelector.razor
new file mode 100644
index 000000000..69e16ab54
--- /dev/null
+++ b/src/Web/Shared/Components/ThemeSelector.razor
@@ -0,0 +1,20 @@
+@*
+ A simple toggle that switches the UI between the "light" and "dark" themes.
+ Persists the selection by calling the ThemeController (cookie-based, same pattern as CultureSelector).
+*@
+@using MUnique.OpenMU.Web.Shared.Properties
+
+
diff --git a/src/Web/Shared/Components/ThemeSelector.razor.cs b/src/Web/Shared/Components/ThemeSelector.razor.cs
new file mode 100644
index 000000000..068d8ce31
--- /dev/null
+++ b/src/Web/Shared/Components/ThemeSelector.razor.cs
@@ -0,0 +1,105 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Web.Shared.Components;
+
+using Microsoft.AspNetCore.Components;
+using Microsoft.JSInterop;
+
+///
+/// A toggle component which switches between the light and dark UI themes.
+/// Persists the selection by calling the
+/// (cookie-based, mirrors the approach).
+///
+public partial class ThemeSelector : IAsyncDisposable
+{
+ private static readonly string JsModulePath =
+ $"./_content/{typeof(ThemeSelector).Assembly.GetName().Name}/themeSelector.js";
+
+ private IJSObjectReference? _jsModule;
+
+ private bool _isDarkInternal;
+
+ private bool _hydrated;
+
+ ///
+ /// Gets or sets the current dark-mode state as seen by the server-side renderer.
+ ///
+ ///
+ /// Only used until the first interactive render hydrates the value from the live DOM
+ /// (the cascading HttpContext is null during interactive updates so this parameter
+ /// would otherwise flip back to false even when the cookie is "dark").
+ ///
+ [Parameter]
+ public bool IsDark { get; set; }
+
+ ///
+ /// Gets or sets the navigation manager used for the post-toggle redirect.
+ ///
+ [Inject]
+ private NavigationManager NavigationManager { get; set; } = null!;
+
+ ///
+ /// Gets or sets the JS runtime used to load the theme reader module.
+ ///
+ [Inject]
+ private IJSRuntime JS { get; set; } = null!;
+
+ private bool EffectiveIsDark => this._hydrated ? this._isDarkInternal : this.IsDark;
+
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ if (this._jsModule is not null)
+ {
+ try
+ {
+ await this._jsModule.DisposeAsync().ConfigureAwait(false);
+ }
+ catch (JSDisconnectedException)
+ {
+ // Ignore: circuit already gone.
+ }
+ }
+ }
+
+ ///
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await base.OnAfterRenderAsync(firstRender).ConfigureAwait(true);
+ if (!firstRender)
+ {
+ return;
+ }
+
+ try
+ {
+ this._jsModule = await this.JS
+ .InvokeAsync
("import", JsModulePath)
+ .ConfigureAwait(true);
+ var theme = await this._jsModule
+ .InvokeAsync("current")
+ .ConfigureAwait(true);
+ this._isDarkInternal = string.Equals(theme, "dark", StringComparison.OrdinalIgnoreCase);
+ this._hydrated = true;
+ this.StateHasChanged();
+ }
+ catch
+ {
+ // Fall back to the SSR parameter if JS is unavailable.
+ }
+ }
+
+ private void Toggle()
+ {
+ var next = this.EffectiveIsDark ? "light" : "dark";
+ var uri = new Uri(this.NavigationManager.Uri)
+ .GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
+ var themeEscaped = Uri.EscapeDataString(next);
+ var uriEscaped = Uri.EscapeDataString(uri);
+
+ var fullUri = $"/Theme/Set?theme={themeEscaped}&redirectUri={uriEscaped}";
+ this.NavigationManager.NavigateTo(fullUri, forceLoad: true);
+ }
+}
diff --git a/src/Web/Shared/Components/ThemeSelector.razor.css b/src/Web/Shared/Components/ThemeSelector.razor.css
new file mode 100644
index 000000000..436671bde
--- /dev/null
+++ b/src/Web/Shared/Components/ThemeSelector.razor.css
@@ -0,0 +1,22 @@
+.theme-selector {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 2rem;
+ height: 2rem;
+ line-height: 1;
+ color: inherit;
+ text-decoration: none;
+ border-radius: 999px;
+}
+
+.theme-selector:hover,
+.theme-selector:focus {
+ background-color: rgba(0, 0, 0, 0.06);
+ text-decoration: none;
+ color: inherit;
+}
+
+.theme-selector__icon {
+ font-size: 1.1rem;
+}
diff --git a/src/Web/Shared/Exports.cs b/src/Web/Shared/Exports.cs
index 8d3d04ff5..8d544a776 100644
--- a/src/Web/Shared/Exports.cs
+++ b/src/Web/Shared/Exports.cs
@@ -48,6 +48,7 @@ private static IEnumerable SharedStylesheets
get
{
yield return $"{Prefix}/css/shared.css";
+ yield return $"{Prefix}/css/theme.css";
}
}
}
\ No newline at end of file
diff --git a/src/Web/Shared/Properties/Resources.Designer.cs b/src/Web/Shared/Properties/Resources.Designer.cs
index 1a6333ded..4b45b9c49 100644
--- a/src/Web/Shared/Properties/Resources.Designer.cs
+++ b/src/Web/Shared/Properties/Resources.Designer.cs
@@ -356,5 +356,23 @@ public static string Refresh {
return ResourceManager.GetString("Refresh", resourceCulture);
}
}
+
+ public static string ToggleTheme {
+ get {
+ return ResourceManager.GetString("ToggleTheme", resourceCulture);
+ }
+ }
+
+ public static string SwitchToDarkMode {
+ get {
+ return ResourceManager.GetString("SwitchToDarkMode", resourceCulture);
+ }
+ }
+
+ public static string SwitchToLightMode {
+ get {
+ return ResourceManager.GetString("SwitchToLightMode", resourceCulture);
+ }
+ }
}
}
diff --git a/src/Web/Shared/Properties/Resources.resx b/src/Web/Shared/Properties/Resources.resx
index 552913f24..e174d6f5c 100644
--- a/src/Web/Shared/Properties/Resources.resx
+++ b/src/Web/Shared/Properties/Resources.resx
@@ -273,4 +273,13 @@
Refresh
+
+ Toggle theme
+
+
+ Switch to dark mode
+
+
+ Switch to light mode
+
\ No newline at end of file
diff --git a/src/Web/Shared/Services/ThemeController.cs b/src/Web/Shared/Services/ThemeController.cs
new file mode 100644
index 000000000..3bc30fabc
--- /dev/null
+++ b/src/Web/Shared/Services/ThemeController.cs
@@ -0,0 +1,63 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Web.Shared.Services;
+
+using Microsoft.AspNetCore.Mvc;
+
+///
+/// A controller which writes the UI theme preference to a cookie and redirects to the specified uri.
+///
+[Route("[controller]/[action]")]
+public class ThemeController : Controller
+{
+ ///
+ /// Gets the name of the cookie that stores the selected theme.
+ ///
+ public static string CookieName { get; } = "OpenMU.Theme";
+
+ ///
+ /// Gets the default theme used when no cookie is present.
+ ///
+ public static string DefaultTheme { get; } = "light";
+
+ ///
+ /// Sets the UI theme by writing the theme cookie and redirects to the specified URI.
+ ///
+ /// The theme to set, e.g. "light" or "dark".
+ /// The URI to redirect to after the cookie has been set. Must be a local URL; otherwise the user is sent to the application root.
+ /// A local redirect to , or to "/" if the URI is missing or non-local.
+ public IActionResult Set(string? theme, string? redirectUri)
+ {
+ var normalized = NormalizeTheme(theme);
+ this.HttpContext.Response.Cookies.Append(
+ CookieName,
+ normalized,
+ new Microsoft.AspNetCore.Http.CookieOptions
+ {
+ Expires = DateTimeOffset.UtcNow.AddYears(1),
+ IsEssential = true,
+ SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax,
+ });
+
+ // LocalRedirect throws InvalidOperationException (-> 500) on empty / non-local URIs.
+ // Fall back to the app root so a malformed request never crashes the response.
+ if (string.IsNullOrEmpty(redirectUri) || !this.Url.IsLocalUrl(redirectUri))
+ {
+ return this.LocalRedirect("/");
+ }
+
+ return this.LocalRedirect(redirectUri);
+ }
+
+ ///
+ /// Normalizes the supplied theme value to a known token.
+ ///
+ /// The raw value from the request.
+ /// Either "dark" or "light".
+ public static string NormalizeTheme(string? theme)
+ {
+ return string.Equals(theme, "dark", StringComparison.OrdinalIgnoreCase) ? "dark" : "light";
+ }
+}
diff --git a/src/Web/Shared/wwwroot/css/theme.css b/src/Web/Shared/wwwroot/css/theme.css
new file mode 100644
index 000000000..6e51d204b
--- /dev/null
+++ b/src/Web/Shared/wwwroot/css/theme.css
@@ -0,0 +1,413 @@
+/*
+ * Theme tokens for the admin panel (light + dark).
+ *
+ * Loaded as a separate stylesheet via Exports.Stylesheets so it does NOT depend
+ * on Dart Sass being available at build time (the SCSS -> shared.css pipeline
+ * is skipped in Docker / CI builds where ci=true).
+ *
+ * Active theme is selected by the [data-theme] attribute on , set in
+ * App.razor from the OpenMU.Theme cookie (written by ThemeController).
+ */
+
+:root,
+[data-theme="light"] {
+ --omu-bg: #ffffff;
+ --omu-surface: #f8f9fa;
+ --omu-surface-2: #ffffff;
+ --omu-surface-muted: #f7f7f7;
+ --omu-surface-hover: #e9e9e9;
+ --omu-text: #212529;
+ --omu-text-muted: #5f6368;
+ --omu-text-secondary: #6c757d;
+ --omu-border: #d6d5d5;
+ --omu-link: #0366d6;
+ --omu-link-primary: #1b6ec2;
+ --omu-link-primary-border: #1861ac;
+ --omu-mark-bg: #ffff00;
+ --omu-error-bg: lightyellow;
+ --omu-error-text: #212529;
+ --omu-sidebar-gradient: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
+ --omu-nav-link: #d7d7d7;
+ --omu-spinner-dot: rgb(5, 39, 103);
+}
+
+[data-theme="dark"] {
+ /*
+ * Tell the browser to render native form controls (checkbox, radio,
+ * scrollbars, date pickers, native select arrows) in their dark variant
+ * so unchecked checkboxes/radios don't render as bright white tiles.
+ */
+ color-scheme: dark;
+ --omu-bg: #121417;
+ --omu-surface: #1c1f24;
+ --omu-surface-2: #1a1d22;
+ --omu-surface-muted: #23272d;
+ --omu-surface-hover: #2a2f36;
+ --omu-text: #e6e6e6;
+ --omu-text-muted: #a0a4ab;
+ --omu-text-secondary: #b0b4bb;
+ --omu-border: #2a2f36;
+ --omu-link: #66b2ff;
+ --omu-link-primary: #2e86d6;
+ --omu-link-primary-border: #1f6fb8;
+ --omu-mark-bg: #6b5e00;
+ --omu-error-bg: #3a2f00;
+ --omu-error-text: #f5e9b3;
+ --omu-sidebar-gradient: linear-gradient(180deg, #0a1330 0%, #1a0322 70%);
+ --omu-nav-link: #d7d7d7;
+ --omu-spinner-dot: #66b2ff;
+}
+
+html,
+body {
+ background-color: var(--omu-bg);
+ color: var(--omu-text);
+}
+
+/* ===== Dark-mode overrides for Bootstrap 4 surfaces ===== */
+[data-theme="dark"] .bg-light,
+[data-theme="dark"] .navbar-light,
+[data-theme="dark"] .breadcrumb,
+[data-theme="dark"] .breadcrumb-bar,
+[data-theme="dark"] .bg-white {
+ background-color: var(--omu-surface) !important;
+ color: var(--omu-text);
+}
+
+[data-theme="dark"] .border-bottom,
+[data-theme="dark"] .border-top,
+[data-theme="dark"] .border-left,
+[data-theme="dark"] .border-right,
+[data-theme="dark"] .border {
+ border-color: var(--omu-border) !important;
+}
+
+[data-theme="dark"] .text-secondary,
+[data-theme="dark"] small.text-secondary {
+ color: var(--omu-text-secondary) !important;
+}
+
+[data-theme="dark"] .navbar-light .navbar-text,
+[data-theme="dark"] .navbar-light .navbar-brand,
+[data-theme="dark"] .navbar-light .navbar-nav .nav-link {
+ color: var(--omu-text);
+}
+
+/* Tables (used everywhere via @extend .table .table-striped .table-hover in Tables.scss) */
+[data-theme="dark"] .table,
+[data-theme="dark"] table {
+ color: var(--omu-text);
+ background-color: transparent;
+}
+
+[data-theme="dark"] .table-striped tbody tr:nth-of-type(odd),
+[data-theme="dark"] table tbody tr:nth-of-type(odd) {
+ background-color: rgba(255, 255, 255, 0.04);
+ color: var(--omu-text);
+}
+
+[data-theme="dark"] .table-hover tbody tr:hover,
+[data-theme="dark"] table tbody tr:hover {
+ background-color: rgba(255, 255, 255, 0.08);
+ color: var(--omu-text);
+}
+
+[data-theme="dark"] .table th,
+[data-theme="dark"] .table td,
+[data-theme="dark"] table th,
+[data-theme="dark"] table td {
+ border-color: var(--omu-border);
+}
+
+/* Form controls */
+[data-theme="dark"] .form-control,
+[data-theme="dark"] .custom-select,
+[data-theme="dark"] .custom-select-sm,
+[data-theme="dark"] input:not([type="checkbox"]):not([type="radio"]),
+[data-theme="dark"] select,
+[data-theme="dark"] textarea {
+ background-color: var(--omu-surface-muted);
+ color: var(--omu-text);
+ border-color: var(--omu-border);
+}
+
+[data-theme="dark"] .form-control:focus,
+[data-theme="dark"] .custom-select:focus,
+[data-theme="dark"] input:focus,
+[data-theme="dark"] select:focus,
+[data-theme="dark"] textarea:focus {
+ background-color: var(--omu-surface-muted);
+ color: var(--omu-text);
+ border-color: var(--omu-link);
+ box-shadow: 0 0 0 0.2rem rgba(102, 178, 255, 0.25);
+}
+
+[data-theme="dark"] .input-group-text {
+ background-color: var(--omu-surface-hover);
+ color: var(--omu-text);
+ border-color: var(--omu-border);
+}
+
+/* Dropdowns / popovers */
+[data-theme="dark"] .dropdown-menu {
+ background-color: var(--omu-surface);
+ color: var(--omu-text);
+ border-color: var(--omu-border);
+}
+
+[data-theme="dark"] .dropdown-item {
+ color: var(--omu-text);
+}
+
+[data-theme="dark"] .dropdown-item:hover,
+[data-theme="dark"] .dropdown-item:focus {
+ background-color: var(--omu-surface-hover);
+ color: var(--omu-text);
+}
+
+/* Modals */
+[data-theme="dark"] .modal-content {
+ background-color: var(--omu-surface);
+ color: var(--omu-text);
+ border-color: var(--omu-border);
+}
+
+[data-theme="dark"] .modal-header,
+[data-theme="dark"] .modal-footer {
+ border-color: var(--omu-border);
+}
+
+[data-theme="dark"] .close {
+ color: var(--omu-text);
+ text-shadow: none;
+}
+
+/* Alerts / cards / list groups */
+[data-theme="dark"] .alert-secondary {
+ background-color: var(--omu-surface-hover);
+ color: var(--omu-text);
+ border-color: var(--omu-border);
+}
+
+[data-theme="dark"] .card,
+[data-theme="dark"] .list-group-item {
+ background-color: var(--omu-surface);
+ color: var(--omu-text);
+ border-color: var(--omu-border);
+}
+
+[data-theme="dark"] .btn-secondary {
+ background-color: #3a3f47;
+ border-color: #3a3f47;
+ color: var(--omu-text);
+}
+
+[data-theme="dark"] .btn-light {
+ background-color: var(--omu-surface-hover);
+ border-color: var(--omu-border);
+ color: var(--omu-text);
+}
+
+/*
+ * Link color in dark mode — narrow the `a` selector to skip anchors styled as
+ * buttons (Bootstrap pattern ``, used by
+ * EditConfigGrid.razor and ItemTable.razor for Edit links). Without :not(.btn)
+ * our --omu-link (#66b2ff) would beat .btn-info's white text and tank contrast.
+ * .btn-link is intentionally still recolored — its semantics is a link.
+ */
+[data-theme="dark"] a:not(.btn),
+[data-theme="dark"] .btn-link {
+ color: var(--omu-link);
+}
+
+[data-theme="dark"] .btn-primary {
+ background-color: var(--omu-link-primary);
+ border-color: var(--omu-link-primary-border);
+ color: #fff;
+}
+
+/* Open-Iconic glyphs sometimes inherit black; force visible color in dark mode */
+[data-theme="dark"] .oi {
+ color: inherit;
+}
+
+/* Sidebar gradient — overrides hard-coded gradient in shared.css when dark */
+[data-theme="dark"] .sidebar > nav {
+ background-image: var(--omu-sidebar-gradient);
+}
+
+/*
+ * Sidebar dropdown (Game Configuration submenu).
+ * Navigation.scss uses `@extend .dropdown-menu` so the actual element does NOT carry
+ * the `.dropdown-menu` class — it shares styles via the compound selector. So we have
+ * to target the SCSS selector directly to recolor it.
+ */
+[data-theme="dark"] .sidebar > nav > div > ul > li.dropdown > div {
+ background-color: var(--omu-surface);
+ color: var(--omu-text);
+ border-color: var(--omu-border);
+}
+
+[data-theme="dark"] .sidebar > nav > div > ul > li.dropdown > div > a {
+ color: var(--omu-text);
+}
+
+[data-theme="dark"] .sidebar > nav > div > ul > li.dropdown > div > a:hover,
+[data-theme="dark"] .sidebar > nav > div > ul > li.dropdown > div > a:focus {
+ background-color: var(--omu-surface-hover);
+ color: var(--omu-text);
+}
+
+[data-theme="dark"] .sidebar > nav > div > ul > li.dropdown > div > a.active {
+ background-color: var(--omu-surface-hover);
+ color: var(--omu-text);
+}
+
+[data-theme="dark"] .sidebar > nav > div > ul > li.dropdown > div hr {
+ border-color: var(--omu-border);
+}
+
+/* Breadcrumb bar (used in MainLayout) */
+[data-theme="dark"] .breadcrumb-bar {
+ background-color: var(--omu-surface) !important;
+}
+
+/* Blazor error banner */
+[data-theme="dark"] #blazor-error-ui {
+ background: var(--omu-error-bg);
+ color: var(--omu-error-text);
+}
+
+/* CreationPanel sticky form-actions footer (hardcoded #fff / #dee2e6 in CreationPanel.razor.css) */
+[data-theme="dark"] .creation-panel-body .form-actions {
+ background-color: var(--omu-surface) !important;
+ border-top: 1px solid var(--omu-border) !important;
+}
+
+/* Sticky "Add New" bar in EditConfigGrid (hardcoded #fff / #dee2e6 in EditConfigGrid.razor.css, added in #788) */
+[data-theme="dark"] .add-new-bar {
+ background-color: var(--omu-surface) !important;
+ border-top: 1px solid var(--omu-border) !important;
+}
+
+/* Blazored.Modal panel (NuGet package hardcodes background-color: #fff in blazored.modal.bundle.scp.css) */
+[data-theme="dark"] .blazored-modal {
+ background-color: var(--omu-surface) !important;
+ border-color: var(--omu-border) !important;
+ color: var(--omu-text);
+}
+
+/* MultiLookupField (used in Plugins modal, ItemEdit excellent/wing options, etc.) */
+[data-theme="dark"] .multi-lookup-field__selected-items {
+ background: var(--omu-surface-2);
+ border-color: var(--omu-border);
+ color: var(--omu-text);
+}
+
+[data-theme="dark"] .multi-lookup-field__tag {
+ background-color: var(--omu-surface-hover);
+ border-color: var(--omu-border);
+ color: var(--omu-text);
+}
+
+[data-theme="dark"] .multi-lookup-field__tag-remove {
+ color: var(--omu-text-muted);
+}
+
+[data-theme="dark"] .multi-lookup-field__dropdown {
+ background: var(--omu-surface-2);
+ border-color: var(--omu-border);
+}
+
+[data-theme="dark"] .multi-lookup-field__dropdown-item {
+ color: var(--omu-text);
+}
+
+[data-theme="dark"] .multi-lookup-field__dropdown-item:hover,
+[data-theme="dark"] .multi-lookup-field__dropdown-item--selected {
+ background-color: var(--omu-surface-hover);
+}
+
+[data-theme="dark"] .multi-lookup-field__dropdown-item--selected:hover {
+ background-color: var(--omu-surface-muted);
+}
+
+[data-theme="dark"] .multi-lookup-field__dropdown-item--loading,
+[data-theme="dark"] .multi-lookup-field__dropdown-item--empty {
+ color: var(--omu-text-muted);
+}
+
+/*
+ * QuickGrid column header. shared.css applies `button { color: #212529 }` globally;
+ * `.col-title` is a bare