diff --git a/DeepDrftPublic.Client/Common/PublicSiteSettings.cs b/DeepDrftPublic.Client/Common/PublicSiteSettings.cs
new file mode 100644
index 0000000..1b83b16
--- /dev/null
+++ b/DeepDrftPublic.Client/Common/PublicSiteSettings.cs
@@ -0,0 +1,29 @@
+using Microsoft.AspNetCore.Components;
+
+namespace DeepDrftPublic.Client.Common;
+
+///
+/// The single public-site listener-settings object (Phase 18 wave 18.6, §4a). The generalized analogue of
+/// : one scoped holder for every remembered listener preference, seeded at
+/// server prerender, carried into WASM via , and persisted to a cookie on
+/// change. Today it carries one preference — streaming quality; tomorrow dark mode (and whatever follows)
+/// folds in here as another property without disturbing the menu that reads it.
+///
+/// Built design-for-adaptability per §4a: a new preference is a new [PersistentState] property here
+/// plus a new in the menu — not a rewire. Dark mode is intentionally
+/// not migrated in now (it keeps its own seam); this object is shaped
+/// so that consolidation is later a merge of two identical seams, not a reconciliation of two different ones.
+///
+///
+public class PublicSiteSettings
+{
+ ///
+ /// The listener's streaming-quality preference. Defaults to (Opus,
+ /// capability-gated — OQ2). Seeded from the streamQuality cookie at prerender; persisted on change
+ /// by the client cookie service. The player reads this to decide which ?format= to request, but
+ /// the capability gate and C2 fallback still apply on top, so a
+ /// preference never forces an unplayable stream.
+ ///
+ [PersistentState]
+ public StreamQuality StreamQuality { get; set; } = StreamQuality.LowData;
+}
diff --git a/DeepDrftPublic.Client/Common/SettingsItem.cs b/DeepDrftPublic.Client/Common/SettingsItem.cs
new file mode 100644
index 0000000..9cdfa6b
--- /dev/null
+++ b/DeepDrftPublic.Client/Common/SettingsItem.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Components;
+
+namespace DeepDrftPublic.Client.Common;
+
+///
+/// One entry in the public-site Settings menu (Phase 18 wave 18.6, §4a). The settings-item abstraction the
+/// menu renders instead of a hard-coded control list: a plus a
+/// fragment bound to a persisted preference. Adding a future tenant (e.g. dark mode) is appending one of
+/// these — not rewiring the menu. The control fragment owns its own binding to
+/// and its own persistence call, so each item is self-contained and the menu stays preference-agnostic.
+///
+public sealed record SettingsItem(string Label, RenderFragment Control);
diff --git a/DeepDrftPublic.Client/Common/StreamQuality.cs b/DeepDrftPublic.Client/Common/StreamQuality.cs
new file mode 100644
index 0000000..5dd1508
--- /dev/null
+++ b/DeepDrftPublic.Client/Common/StreamQuality.cs
@@ -0,0 +1,18 @@
+namespace DeepDrftPublic.Client.Common;
+
+///
+/// The listener's streaming-quality preference (Phase 18 wave 18.6, §4). This is the user's intent,
+/// not the wire format that ultimately gets served: means "give me Opus if you can,"
+/// but the player still capability-gates and C2-falls-back to lossless when Opus can't play (a browser that
+/// can't decode Ogg Opus, or a track with no Opus artifact). It is therefore deliberately distinct from
+/// DeepDrftModels.Enums.AudioFormat (the delivery rendering resolved per request): one is the
+/// remembered preference, the other is what a given stream request actually asks for.
+///
+public enum StreamQuality
+{
+ /// Bandwidth-friendly Opus (capability-gated; the default before any choice — OQ2).
+ LowData,
+
+ /// The lossless WAV path, always playable everywhere.
+ Lossless
+}
diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerProvider.razor.cs b/DeepDrftPublic.Client/Controls/AudioPlayerProvider.razor.cs
index 5202023..22e4a8a 100644
--- a/DeepDrftPublic.Client/Controls/AudioPlayerProvider.razor.cs
+++ b/DeepDrftPublic.Client/Controls/AudioPlayerProvider.razor.cs
@@ -1,3 +1,4 @@
+using DeepDrftPublic.Client.Common;
using DeepDrftPublic.Client.Services;
using DeepDrftPublic.Client.Clients;
using Microsoft.AspNetCore.Components;
@@ -13,6 +14,7 @@ public partial class AudioPlayerProvider : ComponentBase, IAsyncDisposable
[Inject] public required BeaconInterop Beacon { get; set; }
[Inject] public required IPlayEventSink PlayEventSink { get; set; }
[Inject] public required IAnonIdProvider AnonId { get; set; }
+ [Inject] public required PublicSiteSettings Settings { get; set; }
private IStreamingPlayerService? _audioPlayerService;
private QueueService? _queueService;
@@ -26,7 +28,12 @@ public partial class AudioPlayerProvider : ComponentBase, IAsyncDisposable
// EnsureInitializedAsync — that path is correct because audio contexts
// require a user gesture anyway. Initializing eagerly here causes 4+
// SignalR round-trips before any content is stable.
- var player = new StreamingAudioPlayerService(AudioInterop, TrackMediaClient, Logger);
+ // Construct the preference-aware player (Phase 18 wave 18.6): it honours the listener's streaming-
+ // quality choice via the ResolveStreamFormatAsync seam while inheriting the 18.5 capability gate and
+ // C2 fallback. PublicSiteSettings is scoped data (already prerender-seeded + WASM-bridged), so passing
+ // it through the constructor is cheap and carries no lifecycle — the telemetry tracker still binds
+ // post-construction below, exactly as before.
+ var player = new PreferenceAwareStreamingPlayerService(AudioInterop, TrackMediaClient, Logger, Settings);
// Phase 16: bind the play-session tracker to the player after construction, the same way the
// queue binds — the player is built with `new`, not DI, so threading telemetry through its
diff --git a/DeepDrftPublic.Client/Controls/Settings/SettingsMenu.razor b/DeepDrftPublic.Client/Controls/Settings/SettingsMenu.razor
new file mode 100644
index 0000000..9fd1a60
--- /dev/null
+++ b/DeepDrftPublic.Client/Controls/Settings/SettingsMenu.razor
@@ -0,0 +1,40 @@
+@using DeepDrftPublic.Client.Common
+@using DeepDrftPublic.Client.Controls.Settings
+
+@*
+ The public-site Settings menu (Phase 18 wave 18.6, §4a). An app-bar trigger opening a MudMenu that renders
+ a settings-item list — NOT a hard-coded control stack. Each entry is a SettingsItem (label + a control
+ fragment bound to a persisted preference), so a future tenant (dark mode) plugs in as a new list entry, not
+ a menu rewire. Today the list holds one item: the streaming-quality toggle.
+
+ The MudMenu items carry OnClick="@(() => {})" + OnTouch so a click inside a control row does not dismiss the
+ menu (MudMenu auto-closes on item activation otherwise), keeping the radio group usable.
+*@
+
+
+
+
+@code {
+ // The settings-item list. Built once; adding a preference is appending one SettingsItem with its control
+ // fragment — the menu body above renders whatever is here without knowing what each item is.
+ private readonly List _items =
+ [
+ new SettingsItem("Streaming quality", @)
+ ];
+}
diff --git a/DeepDrftPublic.Client/Controls/Settings/StreamQualitySetting.razor b/DeepDrftPublic.Client/Controls/Settings/StreamQualitySetting.razor
new file mode 100644
index 0000000..4fcff26
--- /dev/null
+++ b/DeepDrftPublic.Client/Controls/Settings/StreamQualitySetting.razor
@@ -0,0 +1,65 @@
+@using DeepDrftPublic.Client.Common
+@using DeepDrftPublic.Client.Services
+
+@*
+ The streaming-quality control (Phase 18 wave 18.6, §4) — the first occupant of the Settings menu. Binds
+ the listener's choice to PublicSiteSettings.StreamQuality and persists it via the cookie seam. Honest
+ capability gate (OQ2 / AC7): on a browser that cannot decode Ogg Opus the Low-data option still selects,
+ but a note tells the listener the effective stream is lossless — we never let the choice silently imply a
+ format that can't play.
+*@
+
+
+
+ Low-data (Opus)
+
+
+ Lossless (WAV)
+
+
+
+ @if (_opusUnavailable && _quality == StreamQuality.LowData)
+ {
+
+ This browser can't decode Opus — you'll stream lossless.
+
+ }
+
+
+@code {
+ [Inject] public required PublicSiteSettings Settings { get; set; }
+ [Inject] public required SettingsCookieService CookieService { get; set; }
+ [Inject] public required AudioInteropService AudioInterop { get; set; }
+
+ private StreamQuality _quality;
+
+ // Null until the capability probe runs (post-render JS interop). false → can decode Opus; true → cannot.
+ private bool _opusUnavailable;
+
+ protected override void OnInitialized()
+ {
+ // Read the current preference (already seeded at prerender + bridged into WASM).
+ _quality = Settings.StreamQuality;
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (!firstRender) return;
+
+ // Capability probe is JS interop — only valid once interactive. Surfaces the honest note when the
+ // browser can't decode Ogg Opus, so a Low-data pick reads as "effectively lossless" rather than
+ // silently failing. The player applies the same gate independently; this is purely the UI honesty.
+ var canDecodeOpus = await AudioInterop.CanDecodeOggOpus();
+ if (canDecodeOpus == _opusUnavailable)
+ {
+ _opusUnavailable = !canDecodeOpus;
+ StateHasChanged();
+ }
+ }
+
+ private async Task OnQualityChanged(StreamQuality quality)
+ {
+ _quality = quality;
+ await CookieService.SetStreamQualityAsync(quality);
+ }
+}
diff --git a/DeepDrftPublic.Client/Layout/DeepDrftMenu.razor b/DeepDrftPublic.Client/Layout/DeepDrftMenu.razor
index f8808bf..27b7028 100644
--- a/DeepDrftPublic.Client/Layout/DeepDrftMenu.razor
+++ b/DeepDrftPublic.Client/Layout/DeepDrftMenu.razor
@@ -1,5 +1,6 @@
@using DeepDrftPublic.Client.Common
@using DeepDrftPublic.Client.Controls
+@using DeepDrftPublic.Client.Controls.Settings
@using DeepDrftPublic.Client.Services
@* Desktop Menu *@
@@ -42,6 +43,7 @@
+
@@ -74,7 +76,8 @@
@onclick="ToggleMobileMenu">
-
+
+
diff --git a/DeepDrftPublic.Client/Layout/MainLayout.razor b/DeepDrftPublic.Client/Layout/MainLayout.razor
index bdfc432..66ec0d5 100644
--- a/DeepDrftPublic.Client/Layout/MainLayout.razor
+++ b/DeepDrftPublic.Client/Layout/MainLayout.razor
@@ -42,6 +42,7 @@
@code {
private string _audioPlayerClass = "minimized";
private const string DarkModeKey = "darkMode";
+ private const string StreamQualityKey = "streamQuality";
private bool _isDarkMode = false;
private bool? _lastAppliedDarkMode = null;
private PersistingComponentStateSubscription _persistingSubscription;
@@ -49,6 +50,7 @@
[Inject] public required PersistentComponentState PersistentState { get; set; }
[Inject] public required DarkModeSettings DarkModeSettings { get; set; }
+ [Inject] public required PublicSiteSettings PublicSiteSettings { get; set; }
[Inject] public required IJSRuntime JS { get; set; }
protected override void OnInitialized()
@@ -66,8 +68,17 @@
_isDarkMode = DarkModeSettings.IsDarkMode;
}
+ // Restore the prerender-seeded streaming-quality preference (Phase 18 wave 18.6). Same bridge dark
+ // mode uses: the server SettingsService seeded PublicSiteSettings from the streamQuality cookie, and
+ // this carries it into WASM so the client boots already knowing the preference (no re-read flash, no
+ // wrong default before the first stream).
+ if (PersistentState.TryTakeFromJson(StreamQualityKey, out var restoredQuality))
+ {
+ PublicSiteSettings.StreamQuality = restoredQuality;
+ }
+
// Register to persist state when prerendering completes
- _persistingSubscription = PersistentState.RegisterOnPersisting(PersistDarkMode);
+ _persistingSubscription = PersistentState.RegisterOnPersisting(PersistState);
}
// Sync dark mode class on so portaled MudBlazor elements (popovers, menus, selects)
@@ -91,9 +102,10 @@
// Theme wrapper class for CSS targeting
private string ThemeWrapperClass => _isDarkMode ? "deepdrft-theme-dark" : "deepdrft-theme-light";
- private Task PersistDarkMode()
+ private Task PersistState()
{
PersistentState.PersistAsJson(DarkModeKey, _isDarkMode);
+ PersistentState.PersistAsJson(StreamQualityKey, PublicSiteSettings.StreamQuality);
return Task.CompletedTask;
}
diff --git a/DeepDrftPublic.Client/Services/AudioInteropService.cs b/DeepDrftPublic.Client/Services/AudioInteropService.cs
index 4c109ee..6ea87e5 100644
--- a/DeepDrftPublic.Client/Services/AudioInteropService.cs
+++ b/DeepDrftPublic.Client/Services/AudioInteropService.cs
@@ -76,7 +76,7 @@ public class AudioInteropService : IAsyncDisposable
/// stays on the universal lossless path (AC7 — no listener ever gets silence over a codec gap). Probe
/// failures degrade to false (assume incapable) so an interop error can never silence playback.
///
- public async Task CanDecodeOggOpus(string playerId)
+ public async Task CanDecodeOggOpus()
{
try
{
diff --git a/DeepDrftPublic.Client/Services/PreferenceAwareStreamingPlayerService.cs b/DeepDrftPublic.Client/Services/PreferenceAwareStreamingPlayerService.cs
new file mode 100644
index 0000000..1375bed
--- /dev/null
+++ b/DeepDrftPublic.Client/Services/PreferenceAwareStreamingPlayerService.cs
@@ -0,0 +1,49 @@
+using DeepDrftModels.Enums;
+using DeepDrftPublic.Client.Clients;
+using DeepDrftPublic.Client.Common;
+using Microsoft.Extensions.Logging;
+
+namespace DeepDrftPublic.Client.Services;
+
+///
+/// The production player that honours the listener's streaming-quality preference (Phase 18 wave 18.6).
+/// Extends through the single deliberately-overridable seam,
+/// , so the rest of the streaming stack
+/// (seek, telemetry, the seek-beyond-buffer format reuse) is inherited verbatim.
+///
+/// The override is one branch: a preference returns
+/// immediately; anything else falls through to base, which keeps
+/// the 18.5 invariants intact — the capability gate (AC7: a browser that can't decode Ogg Opus gets lossless)
+/// and the sidecar-absent → lossless fallback (C2: a legacy / un-backfilled / failed-transcode track gets
+/// lossless). So a Lossless pick always yields lossless; a Low-data pick yields Opus only when it can
+/// actually play, and lossless otherwise. No path produces an unplayable stream.
+///
+///
+public class PreferenceAwareStreamingPlayerService : StreamingAudioPlayerService
+{
+ private readonly PublicSiteSettings _settings;
+
+ public PreferenceAwareStreamingPlayerService(
+ AudioInteropService audioInterop,
+ TrackMediaClient trackMediaClient,
+ ILogger logger,
+ PublicSiteSettings settings)
+ : base(audioInterop, trackMediaClient, logger)
+ {
+ _settings = settings;
+ }
+
+ protected override async Task ResolveStreamFormatAsync(string entryKey, CancellationToken cancellationToken)
+ {
+ // Listener explicitly chose lossless — request it directly, no Opus probe / sidecar fetch needed.
+ if (_settings.StreamQuality == StreamQuality.Lossless)
+ {
+ return AudioFormat.Lossless;
+ }
+
+ // Low-data preference: defer to the base capability-gated resolution, which probes Opus support and
+ // the sidecar's presence and degrades to lossless when either is missing. Both 18.5 invariants are
+ // inherited here, not re-implemented.
+ return await base.ResolveStreamFormatAsync(entryKey, cancellationToken);
+ }
+}
diff --git a/DeepDrftPublic.Client/Services/SettingsCookieService.cs b/DeepDrftPublic.Client/Services/SettingsCookieService.cs
new file mode 100644
index 0000000..e58f08b
--- /dev/null
+++ b/DeepDrftPublic.Client/Services/SettingsCookieService.cs
@@ -0,0 +1,34 @@
+using DeepDrftPublic.Client.Common;
+using Microsoft.JSInterop;
+
+namespace DeepDrftPublic.Client.Services;
+
+///
+/// Client-side runtime writer for public-site settings (Phase 18 wave 18.6), the analogue of
+/// . Reads the current preference off the in-memory
+/// (already seeded at prerender and bridged into WASM), and writes a
+/// 365-day cookie via document.cookie interop when the listener changes it in the Settings menu —
+/// the same durable-truth seam dark mode uses, so the choice survives the session and seeds the next visit's
+/// prerender (no flash).
+///
+public class SettingsCookieService(PublicSiteSettings settings, IJSRuntime js) : SettingsServiceBase
+{
+ private const int ExpiryDays = 365;
+
+ public StreamQuality GetStreamQuality() => settings.StreamQuality;
+
+ public async ValueTask SetStreamQualityAsync(StreamQuality quality)
+ {
+ if (settings.StreamQuality == quality) return;
+
+ await WriteCookieAsync(StreamQualityCookieName, FormatStreamQuality(quality));
+ settings.StreamQuality = quality;
+ }
+
+ private async ValueTask WriteCookieAsync(string name, string value)
+ {
+ var expires = DateTime.UtcNow.AddDays(ExpiryDays).ToString("R");
+ await js.InvokeVoidAsync("eval",
+ $"document.cookie = '{name}={value}; expires={expires}; path=/; SameSite=Lax'");
+ }
+}
diff --git a/DeepDrftPublic.Client/Services/SettingsServiceBase.cs b/DeepDrftPublic.Client/Services/SettingsServiceBase.cs
new file mode 100644
index 0000000..73845c5
--- /dev/null
+++ b/DeepDrftPublic.Client/Services/SettingsServiceBase.cs
@@ -0,0 +1,28 @@
+using DeepDrftPublic.Client.Common;
+
+namespace DeepDrftPublic.Client.Services;
+
+///
+/// Shared cookie contract for the public-site settings seam (Phase 18 wave 18.6), the analogue of
+/// . Holds the cookie names and the (de)serialization for each preference
+/// so the server prerender-read service and the client cookie-write service agree on one wire format —
+/// the load-bearing reason this is shared rather than duplicated. Each new preference adds its cookie name
+/// and a parse/format pair here, keeping the round-trip in one place.
+///
+public abstract class SettingsServiceBase
+{
+ protected const string StreamQualityCookieName = "streamQuality";
+
+ ///
+ /// Parses the streamQuality cookie value into , defaulting to
+ /// (the OQ2 default) for an absent, empty, or unrecognized value so
+ /// a missing/garbled cookie never produces a surprising preference.
+ ///
+ protected static StreamQuality ParseStreamQuality(string? cookieValue) =>
+ Enum.TryParse(cookieValue, ignoreCase: true, out var parsed)
+ ? parsed
+ : StreamQuality.LowData;
+
+ /// Formats a for cookie storage (round-trips with ).
+ protected static string FormatStreamQuality(StreamQuality quality) => quality.ToString();
+}
diff --git a/DeepDrftPublic.Client/Services/StreamingAudioPlayerService.cs b/DeepDrftPublic.Client/Services/StreamingAudioPlayerService.cs
index bf49532..9b6f856 100644
--- a/DeepDrftPublic.Client/Services/StreamingAudioPlayerService.cs
+++ b/DeepDrftPublic.Client/Services/StreamingAudioPlayerService.cs
@@ -280,7 +280,7 @@ public class StreamingAudioPlayerService : AudioPlayerService, IStreamingPlayerS
protected virtual async Task ResolveStreamFormatAsync(string entryKey, CancellationToken cancellationToken)
{
// Capability gate first (AC7): never hand Ogg Opus to a browser that cannot decode it.
- if (!await _audioInterop.CanDecodeOggOpus(PlayerId))
+ if (!await _audioInterop.CanDecodeOggOpus())
{
return AudioFormat.Lossless;
}
diff --git a/DeepDrftPublic.Client/Startup.cs b/DeepDrftPublic.Client/Startup.cs
index 55781aa..89bccf7 100644
--- a/DeepDrftPublic.Client/Startup.cs
+++ b/DeepDrftPublic.Client/Startup.cs
@@ -14,6 +14,13 @@ public static class Startup
services.AddScoped();
services.AddScoped();
+ // Public-site listener settings (Phase 18 wave 18.6). PublicSiteSettings is the generalized,
+ // prerender-seeded preference object (today: streaming quality); SettingsCookieService writes the
+ // 365-day cookie at runtime. Same scoped lifetime + cookie seam as the dark-mode pair above, so the
+ // preference survives SPA nav within a session and seeds the next visit's prerender.
+ services.AddScoped();
+ services.AddScoped();
+
// Track Client. The HTTP-backed ITrackDataService is used by both WASM and SSR
// prerender — both call DeepDrftAPI over the "DeepDrft.API" client.
services.AddScoped();
diff --git a/DeepDrftPublic/Components/App.razor b/DeepDrftPublic/Components/App.razor
index e36ba5b..851143b 100644
--- a/DeepDrftPublic/Components/App.razor
+++ b/DeepDrftPublic/Components/App.razor
@@ -34,11 +34,13 @@
@code {
[Inject] public required DarkModeService DarkModeService { get; set; }
+ [Inject] public required SettingsService SettingsService { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
DarkModeService.CheckDarkMode();
+ SettingsService.CheckSettings();
}
}
diff --git a/DeepDrftPublic/Services/SettingsService.cs b/DeepDrftPublic/Services/SettingsService.cs
new file mode 100644
index 0000000..efc475e
--- /dev/null
+++ b/DeepDrftPublic/Services/SettingsService.cs
@@ -0,0 +1,25 @@
+using DeepDrftPublic.Client.Common;
+using DeepDrftPublic.Client.Services;
+
+namespace DeepDrftPublic.Services;
+
+///
+/// Server-side prerender reader for public-site listener settings (Phase 18 wave 18.6), the sibling of
+/// . Reads each preference's cookie via
+/// during prerender and seeds the scoped , which MainLayout then
+/// rounds through PersistentComponentState into WASM — so the first paint already reflects the
+/// listener's choice with no wrong-default flash (the streaming-quality analogue of the wrong-theme fix).
+/// Inherits the shared cookie names + parsers from so the server read and
+/// the client write agree on one wire format.
+///
+public class SettingsService(PublicSiteSettings settings, IHttpContextAccessor httpAccessor) : SettingsServiceBase
+{
+ public void CheckSettings()
+ {
+ var cookies = httpAccessor.HttpContext?.Request.Cookies;
+ if (cookies is null) return;
+
+ cookies.TryGetValue(StreamQualityCookieName, out var streamQuality);
+ settings.StreamQuality = ParseStreamQuality(streamQuality);
+ }
+}
diff --git a/DeepDrftPublic/Startup.cs b/DeepDrftPublic/Startup.cs
index 4ab30e1..de3cd8e 100644
--- a/DeepDrftPublic/Startup.cs
+++ b/DeepDrftPublic/Startup.cs
@@ -11,5 +11,10 @@ public static class Startup
builder.Services
.AddHttpContextAccessor()
.AddScoped();
+
+ // Server prerender read for public-site listener settings (Phase 18 wave 18.6), sibling to
+ // DarkModeService. PublicSiteSettings itself is registered in the client Startup (shared by SSR and
+ // WASM); this seeds it from the streamQuality cookie during prerender.
+ builder.Services.AddScoped();
}
}
diff --git a/DeepDrftPublic/wwwroot/styles/deepdrft-styles.css b/DeepDrftPublic/wwwroot/styles/deepdrft-styles.css
index f2c3b5c..ea4a26a 100644
--- a/DeepDrftPublic/wwwroot/styles/deepdrft-styles.css
+++ b/DeepDrftPublic/wwwroot/styles/deepdrft-styles.css
@@ -463,6 +463,46 @@ h2, h3, h4, h5, h6,
flex: 1 1 auto;
}
+/* Public-site Settings menu (Phase 18 wave 18.6). The MudMenu body renders inside .mud-popover, which
+ already re-points --mud-palette-surface to the theme-aware --deepdrft-popover-surface (see above), so the
+ panel inherits the correct surface + text in both themes with no dark override. These rules are layout
+ only: padding, the section heading, and each settings item's label/control stacking. Scoped via the
+ global stylesheet (not CSS isolation) because the menu body portals out of the component's DOM scope. */
+.dd-settings-panel {
+ padding: 0.75rem 1rem;
+ min-width: 240px;
+ max-width: 320px;
+ color: var(--deepdrft-page-text);
+}
+
+.dd-settings-heading {
+ font-family: "DM Sans", sans-serif;
+ font-size: 0.7rem;
+ font-weight: 600;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ opacity: 0.7;
+ margin-bottom: 0.5rem;
+}
+
+.dd-settings-item + .dd-settings-item {
+ margin-top: 0.75rem;
+ padding-top: 0.75rem;
+ border-top: 1px solid var(--deepdrft-popover-surface);
+}
+
+.dd-settings-item-label {
+ font-weight: 500;
+ margin-bottom: 0.25rem;
+}
+
+/* The honest capability note under the quality control (OQ2 / AC7). */
+.dd-setting-note {
+ font-size: 0.75rem;
+ opacity: 0.75;
+ margin-top: 0.25rem;
+}
+
.deepdrft-share-embed-field .mud-input-slot {
font-family: var(--deepdrft-font-mono) !important;
font-size: 0.75rem;