From 5fb46bf5ebbc68f89ac5be381b98cbba9eaa6f52 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Wed, 17 Jun 2026 11:44:33 -0400 Subject: [PATCH] docs(product): spec CMS public landing page (Phase 13) Splash owns /, catalogue moves to /catalogue, authed users redirected via HierarchicalRoleAuthorizeView. Skipper's public-layout pattern, branded to DeepDrft. Adds Phase 13 to PLAN.md. --- PLAN.md | 38 +++ product-notes/cms-public-landing.md | 343 ++++++++++++++++++++++++++++ 2 files changed, 381 insertions(+) create mode 100644 product-notes/cms-public-landing.md diff --git a/PLAN.md b/PLAN.md index 4832f06..9bc38f8 100644 --- a/PLAN.md +++ b/PLAN.md @@ -387,6 +387,44 @@ a lava lamp on the landing page is no surprise. --- +## Phase 13 — CMS Public Landing + +Give `DeepDrftManager` (the CMS) a true public face: an unauthenticated splash at `/` with DeepDrft +branding and a single **Login** CTA; authenticated admins are redirected past it to the catalogue. +Today `/` is the `[Authorize]`-gated catalogue, so an anonymous hit falls straight through to the login +form — there is no front door. Pattern borrowed from Skipper's `MainHomeLayout` / `Home.razor` +(dedicated public layout + `HierarchicalRoleAuthorizeView` redirect-the-authed-user idiom), branded to +the DeepDrft navy/green/off-white identity (`DeepDrftPalettes.Cms`), **not** Skipper's nautical look. +Additive — the admin experience is intact; only the catalogue's *route* moves. Full spec, routing-reshape +rationale, file responsibilities, hero/CTA composition, asset path, and acceptance criteria: +`product-notes/cms-public-landing.md`. + +**Routing decision (recommended, spec §2): Option A — splash owns `/`, catalogue moves to `/catalogue`.** +A new `Home.razor` (`@page "/"`, no `[Authorize]`, new `CmsHomeLayout`) wraps its body in +``: `Authorized` → redirect to `/catalogue`; `NotAuthorized` → hero + +Login CTA. `Index.razor` changes `@page "/"` → `@page "/catalogue"` (one-line; otherwise untouched). +`Routes.razor` and `Program.cs` need no change — the host is already `AllowAnonymous` at the endpoint and +page auth is owned by `AuthorizeRouteView`, so a no-`[Authorize]` page renders for everyone. The entire +cost of Option A is a small, mechanical link-repoint (every internal `/` that meant *catalogue* → +`/catalogue`; spec §6). Options B (layout-by-auth-state, conflates page concerns) and C (splash at a +side route, defeats the goal) were weighed and rejected. + +- **New files:** `Components/Layout/CmsHomeLayout.razor` (lean public layout — same `DeepDrftPalettes.Cms` + theme, front-door AppBar, centered narrow `MudContainer`), `Components/Pages/Home.razor` (the splash), + `Components/RedirectToCatalogue.razor` (mirrors existing `RedirectToAccessDenied`; inline redirect + acceptable). **Changed:** `Index.razor` route line; `CmsLayout.razor` "Back to site" `Href="/"` → + `/catalogue`. +- **Hero asset (Daniel-supplied):** `DeepDrftManager/wwwroot/img/cms-hero.png` (creates the `wwwroot/img/` + dir, net-new); referenced `Src="img/cms-hero.png"`. The page must compile/render without it. +- **No new shared component, no new palette, no `@rendermode` override, no Register CTA** (CMS is + invite/seed-only — single Login button). DRY/MudBlazor-first throughout; the only bespoke CSS is the + layout's one viewport `min-height`. +- **One open copy question for Daniel (non-structural):** AppBar/title wording — "Deep Drft" vs. + "Deep Drft — Admin" (spec §4 recommends "Deep Drft — Admin" in the bar, "Deep Drft" as the hero title). + Implementable either way without rework. + +--- + ## Working with this file - **Add items by extending an existing phase first**; only create a new phase when the addition genuinely doesn't fit any of 1–5. Phase numbers are organisational, not sequencing. diff --git a/product-notes/cms-public-landing.md b/product-notes/cms-public-landing.md new file mode 100644 index 0000000..42f224a --- /dev/null +++ b/product-notes/cms-public-landing.md @@ -0,0 +1,343 @@ +# CMS Public Landing — a true public face for DeepDrftManager — Design Spec + +Status: **proposed** (2026-06-17). Author: product-designer. Date: 2026-06-17. +Implementer: staff-engineer. Reference pattern: Skipper `Home.razor` / `MainHomeLayout.razor`. + +## 0. Goal + +Give `DeepDrftManager` (the CMS) a public face: an unauthenticated splash at `/` with DeepDrft +branding and a single clear **Login** call-to-action. Authentication happens beyond it. Today an +anonymous hit to `/` falls straight through to the login form (the catalogue page at `/` carries +`[Authorize]`, so `AuthorizeRouteView` redirects). After this change, an anonymous visitor sees a +composed, on-brand splash; an authenticated admin is sent onward to the catalogue without ever seeing +the splash. + +This is **additive**. The authenticated admin experience (catalogue dashboard, `CmsLayout`, all CMS +pages) is preserved unchanged except for the catalogue's route moving off `/`. + +--- + +## 1. What exists today (confirmed from live source 2026-06-17) + +- **`Components/Pages/Index.razor`** — `@page "/"`, `@attribute [Authorize]`, `@layout + Layout.CmsLayout`. This **is** the authenticated catalogue dashboard (Tracks / Releases / Genres + summary cards, each loading a count concurrently). No public face exists. +- **`Components/Routes.razor`** — `AuthorizeRouteView` with `DefaultLayout = Layout.CmsLayout`. + `NotAuthorized`: authenticated-but-unauthorized → `RedirectToAccessDenied`; unauthenticated → + `RedirectToLogin` (the AuthBlocksWeb component, not a Manager-local one). `AdditionalAssemblies` + already includes `typeof(AuthBlocksWeb._Imports).Assembly`. +- **`Components/Layout/CmsLayout.razor`** — `MudThemeProvider IsDarkMode="false" Theme="DeepDrftPalettes.Cms"` + (light-only, solid navy AppBar). Dense AppBar "Deep Drft — Admin" + a "Back to site" home button + (`Href="/"`). Providers (Popover/Dialog/Snackbar) live here. +- **`Program.cs`** — `MapRazorComponents().AddInteractiveServerRenderMode()...AllowAnonymous()`. + Endpoint auth is anonymous by design (JWT lives in browser localStorage, never reaches the server on + a nav); **page authorization is owned entirely by `AuthorizeRouteView`**. Consequence: a routable + page with **no `[Authorize]` attribute renders for everyone**, no server 401, no redirect. This is + the seam the splash uses. +- **Auth components available** (from `Cerebellum.AuthBlocks.Web`, assembly already wired into the + router): `[HierarchicalRoleAuthorize]` attribute and `` view + component. Bare = "requires authenticated"; with roles = walks the role hierarchy. +- **No `wwwroot/img/` directory exists** in `DeepDrftManager` today. The host's only wwwroot asset is + `app.css`. The hero asset and its folder are net-new. +- **Theme** — `DeepDrftPalettes.Cms` (in `DeepDrftShared.Client.Common`): navy `#0D1B2A` ground / + green-accent `#3D7A68` / warm off-white `#FAFAF8`; typography Cormorant Garamond (display) / Geist + Mono (`Subtitle1`/`Button`/`Caption`) / DM Sans (body). This is the DeepDrft identity — **do not + import Skipper's nautical styling.** Borrow Skipper's *structure*, not its look. + +### What we borrow from Skipper (pattern, not pixels) + +Skipper splits a public **`MainHomeLayout`** (app bar with logo + dark toggle, centered +`MudContainer`) from the authenticated app layout, routes both at `/`, and inside `Home.razor` wraps +the splash body in ``: `Authorized` → ``, +`NotAuthorized` → hero image + title + subtitle + Login/Register buttons in a centered `MudStack`. +We adopt: (a) a **dedicated public layout** distinct from `CmsLayout`, (b) the +**`HierarchicalRoleAuthorizeView` redirect-the-authed-user** idiom at `/`, (c) the **centered hero + +single CTA** composition. We diverge on branding and drop Register (CMS access is invite/seed-only — +there is no public registration path; see §4). + +--- + +## 2. Routing reshape — the load-bearing decision + +**Recommendation: Option A — splash owns `/`, catalogue moves to `/catalogue`, authed users are +redirected onward from the splash via `HierarchicalRoleAuthorizeView`.** + +This is the Skipper pattern transplanted directly, and it is the right one here. Three options were +weighed: + +### Option A (recommended) — splash at `/`, catalogue at `/catalogue`, redirect authed from `/` + +- New `Home.razor` takes `@page "/"`, **no `[Authorize]`**, public layout. Body wrapped in + ``: `Authorized` → redirect to `/catalogue`; `NotAuthorized` → the + hero + Login CTA. +- `Index.razor` (the catalogue) changes its route from `@page "/"` to `@page "/catalogue"`, + everything else unchanged (keeps `[Authorize]` + `CmsLayout`). +- **Pros:** Mirrors Skipper exactly (proven). `/` is a clean public URL. Anonymous and authed users + share one entry point; the redirect is declarative and lives next to the splash it guards. No + `Routes.razor` change. The catalogue page is otherwise untouched. +- **Cons:** The catalogue's URL changes — internal links to `/` that *meant* "the dashboard" must + repoint to `/catalogue` (see §6 migration checklist; the live count is small — `CmsLayout`'s + "Back to site" button and any nav). One new redirect component (`RedirectToCatalogue`) or an inline + `NavigationManager` redirect. + +### Option B — splash at `/`, catalogue stays at `/`, layout chosen by auth state in `Routes.razor` + +Skipper's *other* mechanism: `Routes.razor` computes `MainHomeLayout` vs app layout from auth state in +`OnParametersSetAsync`, and both the splash and catalogue claim `@page "/"` is **not** possible (route +collision). To keep the catalogue at `/`, the splash would have to be conditionally rendered *inside* +the catalogue page or the layout — collapsing two concerns into one component. + +- **Pros:** Catalogue URL never changes; zero link migration. +- **Cons:** Forces splash + dashboard logic into one routable component or bleaks auth-branching into + the layout. Violates the project's preference for one-source/clean seams. The splash is structurally + a *different page* with a *different layout*; conflating them is the wrong abstraction. Rejected. + +### Option C — splash at a distinct route (`/welcome`), `/` unchanged + +Splash lives at `/welcome`; `/` keeps the catalogue with `[Authorize]`, so anonymous `/` still +redirects to login (today's behavior), and nothing lands on the splash unless explicitly routed there. + +- **Pros:** Smallest diff; no link migration. +- **Cons:** Defeats the goal. The "public face" is the thing an anonymous visitor sees when they + arrive at the app root. A splash nobody routes to is dead. Rejected. + +**Why A over the rest:** the goal is *"`/` is the public face."* That demands the splash own `/`, +which demands the catalogue move. The redirect-authed-user idiom keeps the admin's experience +seamless (they never see the splash). Option A is the only one that satisfies the goal without +conflating page concerns. The link-migration cost is real but small and mechanical. + +### Where AuthBlocks fits + +- The splash body is wrapped in **``** (bare, no roles — "is this + visitor authenticated at all?"). + - `Authorized` slot → redirect to `/catalogue` (mirrors Skipper's `RedirectToDashboard`). The CMS + further gates the catalogue on the `Admin` role via the existing `[Authorize]` / + `AuthorizeRouteView` path — so a logged-in non-admin still hits `RedirectToAccessDenied` at + `/catalogue`, which is correct and unchanged. + - `NotAuthorized` slot → the hero + Login CTA. +- The Login button links to **`/account/login?returnUrl={Uri.EscapeDataString("catalogue")}`** so a + successful login lands the admin on the catalogue, not back on the splash. (Skipper uses + `returnUrl=dashboard`; same idiom, our route.) +- `Routes.razor` needs **no change** — its `AuthorizeRouteView` already renders no-`[Authorize]` pages + for anonymous users, and the host is already `AllowAnonymous` at the endpoint. + +--- + +## 3. New + changed files and their responsibilities + +### 3.1 New: `Components/Layout/CmsHomeLayout.razor` (public splash layout) + +A public counterpart to `CmsLayout`, deliberately lighter. Responsibilities: + +- Own its **own `MudThemeProvider`** — `IsDarkMode="false" Theme="DeepDrftPalettes.Cms"` (same theme + as the admin surface, so the splash is of-a-piece with the CMS, navy AppBar and all). Splash is + light-only like the rest of the CMS; no dark toggle (the CMS has none today — do not introduce one + here). +- Own a **minimal provider set**: include `MudPopoverProvider` (tooltips on the splash, if any need + it) and skip `MudDialogProvider` / `MudSnackbarProvider` unless a splash interaction needs them + (it does not in this spec — omit them to keep the layout lean). +- A **slim AppBar** consistent with `CmsLayout`'s navy bar but read as a *front door*, not an admin + console: brand text "Deep Drft" (or "Deep Drft — Admin" to set expectation; see §4 open question), + `Typo.h6`, DM-Sans, letter-spacing as in `CmsLayout`. No "Back to site" home button (the splash + *is* the front door). No nav drawer. +- `MudMainContent` wrapping a centered `MudContainer MaxWidth="Small"` (the hero is a focused column, + not full-bleed). Vertical centering via MudBlazor utility classes (`d-flex`, `flex-column`, + `justify-center`, `align-center`) and a min-height tied to the viewport (`Style="min-height: + calc(100vh - );"` or the `mud-height-full` utility on the content) so the hero sits centered + in the page, not jammed at the top. Prefer MudBlazor utilities; the single inline `min-height` is the + one justified bespoke style (no utility expresses "viewport minus app bar" cleanly). +- Include the `
` block (copy from `CmsLayout`) so the InteractiveServer + error UI is present on the public surface too. + +**Why a separate layout rather than reusing `CmsLayout`:** `CmsLayout` is built for the admin console +(full-width container `MaxWidth.False`, "Back to site" affordance, dialog/snackbar providers for CRUD +flows). The splash wants a centered narrow column and a front-door AppBar. Forcing one layout to do +both would mean auth-branching inside the layout — the same conflation Option B was rejected for. +This mirrors Skipper's `MainHomeLayout` vs app-layout split exactly. + +### 3.2 New: `Components/Pages/Home.razor` (the splash page) + +- `@page "/"`, `@layout Layout.CmsHomeLayout`, **no `[Authorize]`**. +- `Deep Drft — Admin` (or "Deep Drft" — see §4). +- Body = `` with the two slots from §2 (`Authorized` → redirect to + `/catalogue`; `NotAuthorized` → hero + CTA per §5). +- No data fetch, no injected services beyond `NavigationManager` (only if doing an inline redirect + rather than a `RedirectToCatalogue` component). Keep it presentational. + +### 3.3 New: `Components/RedirectToCatalogue.razor` (optional, recommended) + +Mirrors the existing `RedirectToAccessDenied.razor` exactly (inject `NavigationManager`, redirect in +`OnInitialized`). Target `/catalogue`. Use `NavigateTo("/catalogue", forceLoad: false)` — +client-side nav is fine here (unlike Skipper's `RedirectToDashboard` which uses `forceLoad: true`; +that was a Skipper-specific concern. Default to no force-load and let staff-engineer confirm against +the JWT-in-localStorage auth-state timing — if the authed redirect mis-fires on first paint, escalate +to `forceLoad: true` as Skipper does). Keeping this as a named component matches the existing +`RedirectToAccessDenied` convention; an inline redirect in `Home.razor` is acceptable if staff-engineer +prefers fewer files. + +### 3.4 Changed: `Components/Pages/Index.razor` (the catalogue) + +- **One-line change:** `@page "/"` → `@page "/catalogue"`. Everything else (the `[Authorize]`, + `CmsLayout`, the three summary cards, the concurrent loaders) is unchanged. +- Optional rename for clarity (`Index.razor` → `Catalogue.razor`): **defer.** Not required; a rename + ripples through `ILogger` and any references. Out of scope for this spec — flag only if + staff-engineer is already touching those lines. + +### 3.5 Changed: `Components/Layout/CmsLayout.razor` (the "Back to site" button) + +- The "Back to site" home button currently has `Href="/"`. Post-reshape, `/` is the **public splash**, + not the catalogue. Two readings: + - If "Back to site" means *the admin home / catalogue*, repoint to `Href="/catalogue"`. + - If "Back to site" means *the public DeepDrft site* (i.e., `DeepDrftPublic`), it should point at the + public site's URL, not anything in Manager — but that cross-host URL isn't configured in the CMS + today, so this is likely **not** the intent. + - **Recommendation:** repoint to `/catalogue` (the in-CMS home). The tooltip text "Back to site" is + then slightly off — consider "Catalogue" or "Home". Staff-engineer's call on wording; the *Href* + must change to `/catalogue` regardless, or the admin's home button lands them on the splash, which + then bounces them back to `/catalogue` via the authed-redirect — functional but an ugly double-hop. + +--- + +## 4. Branding, copy, and the no-Register decision + +- **Identity:** DeepDrft navy/green/off-white via `DeepDrftPalettes.Cms`. Display type Cormorant + Garamond (it comes free from the theme's `Typo.h2`/`h3` etc. — do **not** hand-set font-family in + markup; use `Typo` and let the theme resolve it). Body DM Sans, button/caption Geist Mono — again, + theme-resolved. +- **No Register CTA.** Skipper offers Register (marina staff self-onboard with a code). The DeepDrft + CMS is gated by AuthBlocks login + hierarchical `Admin` role, seeded/invited — there is no public + registration page in `DeepDrftManager`, and AuthBlocksWeb's registration (if present) is not a path + we want to advertise on the front door of an admin tool. **Single Login CTA only.** If Daniel later + wants an invite-code flow, that is a separate feature, not part of this splash. +- **Copy (proposed, Daniel may revise):** + - Title: **"Deep Drft"** (Cormorant, `Typo.h2`, the brand) — or stylized **"DEEP DRFT"** uppercase + to echo Skipper's `SKIPPER` treatment. Recommend brand-cased "Deep Drft" to match the public + site's wordmark rather than Skipper's all-caps. + - Subtitle: **"Catalogue Management"** or **"Collective Content Management"** (Geist Mono, + `Typo.subtitle1`, uppercase via `text-uppercase`, muted/secondary) — names what's behind the door + without overselling. Recommend **"Catalogue Management"**. + - CTA button label: **"Login"**. +- **Open question for Daniel (one):** should the AppBar + title say **"Deep Drft"** (front-door, clean) + or **"Deep Drft — Admin"** (sets the expectation that this is the admin tool, consistent with + `CmsLayout`'s AppBar)? Recommend **"Deep Drft — Admin"** in the AppBar for continuity with the + authenticated surface, and **"Deep Drft"** as the large hero title. This is a copy call, not a + structural one — staff-engineer can implement either without rework. + +--- + +## 5. Hero + CTA composition + +A centered, single-column hero in `MudContainer MaxWidth="Small"`, vertically centered in the +viewport. Composition top-to-bottom inside a `MudStack AlignItems="AlignItems.Center" Spacing="4"`: + +1. **Hero image** — `MudImage Fluid="true" Src="img/cms-hero.png"` (asset path §5.1). Constrain its + width with the container (`MaxWidth="Small"`) so it reads as a focused emblem, not a full-bleed + banner. If Daniel's asset is wide/cinematic rather than emblem-shaped, wrap it in a + `MudPaper`-backed rounded region with `Style="border-radius: 8px; overflow: hidden;"` — but default + to the plain `MudImage` and let the asset dictate (see §5.1 open note). +2. **Title** — `MudText Typo="Typo.h2"` "Deep Drft" (Cormorant via theme), centered + (`Align="Align.Center"`). +3. **Subtitle** — `MudText Typo="Typo.subtitle1"` "Catalogue Management" (Geist Mono via theme), + `Class="text-uppercase mud-text-secondary"`, centered. +4. **Spacer** — `MudSpacer` or a `Class="my-4"` gap. +5. **Login CTA** — `MudButton Variant="Variant.Filled" Color="Color.Primary" + Href="/account/login?returnUrl=..."` (see §2), full-width within a `MaxWidth.Small` inner region or + a fixed comfortable width (`Style="min-width: 200px;"`). Label "Login". Single button, centered. + +This is the Skipper hero stack (image / title / subtitle / spacer / CTA) with the Register row and the +two-column button grid removed (one CTA needs no grid). No bespoke CSS beyond the optional rounded-image +wrapper and the layout's one `min-height` — everything else is MudBlazor `Typo`, `Color`, and spacing +utilities. + +### 5.1 Hero asset path and reference + +- **Asset location:** `DeepDrftManager/wwwroot/img/cms-hero.png` (Daniel supplies the file). This + creates the `wwwroot/img/` directory, which does not exist today — staff-engineer creates the folder + when the asset is dropped in. +- **Reference in markup:** `Src="img/cms-hero.png"` (relative; `MapStaticAssets()` in `Program.cs` + serves `wwwroot/`). Matches Skipper's `Src="img/skipper-hero.png"` idiom exactly. Do **not** wrap in + `@Assets[...]` / fingerprinting unless the project's static-asset fingerprinting convention requires + it for cache-busting — for a single rarely-changing hero, the plain relative path is fine and matches + the public site's `img/...` references. +- **Filename:** `cms-hero.png` recommended. If Daniel's asset is a JPEG, name it `cms-hero.jpg` and + update the `Src`. One asset, one reference — keep it boring. +- **Open note (non-blocking):** the spec assumes an emblem/portrait-ish hero suited to a centered + narrow column (Skipper uses a boat illustration). If Daniel's asset is a wide cinematic photo, the + composition still works but consider `MaxWidth="Medium"` on the container and the rounded-paper wrap + from §5.1. Staff-engineer adjusts container width to the asset; no design rework needed. + +--- + +## 6. Migration checklist (link repointing) + +Every internal reference that meant "the catalogue lived at `/`" must move to `/catalogue`: + +1. **`CmsLayout.razor`** "Back to site" button `Href="/"` → `Href="/catalogue"` (§3.5). +2. **`Index.razor`** summary-card "View" buttons → already navigate to `/tracks`, unaffected. +3. **Any nav menu / breadcrumb / link in CMS components pointing at `/`** as "home/dashboard" → + `/catalogue`. staff-engineer: grep `DeepDrftManager` for `Href="/"` and `NavigateTo("/")` / + `NavigateTo("/", ` and repoint the ones that mean *catalogue* (leave any that genuinely mean *the + public front door* pointing at `/`). +4. **Login `returnUrl`** anywhere it defaults to the old root → `catalogue`. + +This grep-and-repoint is the entire cost of Option A. It is mechanical and small (the CMS is a compact +host), but it must be exhaustive or an admin lands on the splash and double-hops. + +--- + +## 7. Constraints honored + +- **DRY / MudBlazor-first:** reuses `DeepDrftPalettes.Cms` (no new palette), reuses the + `RedirectToAccessDenied` redirect idiom for `RedirectToCatalogue`, reuses `HierarchicalRoleAuthorizeView` + (no new auth machinery), composes from `MudAppBar`/`MudContainer`/`MudStack`/`MudText`/`MudImage`/ + `MudButton`. Bespoke CSS limited to one layout `min-height` and an optional rounded-image wrapper. +- **No new shared component worth extracting.** The splash is host-specific (CMS front door); it does + not belong in `DeepDrftShared.Client`. The public site's hero idiom (`ReleaseHeroOverlay`, + full-bleed background + overlaid metadata) is a *different* composition (content hero, not a login + gate) — do not try to reuse it here; the shapes don't match and forcing it would be the wrong borrow. + If a *second* gated host ever needs a login splash, revisit extracting a `LoginSplash` shared + component then — not now (YAGNI; one consumer). +- **Render mode:** no explicit `@rendermode` override anywhere — the host's global + `AddInteractiveServerRenderMode()` governs, per DeepDrftManager convention. The splash is static + presentational content + one auth-view + links; it needs no interactivity of its own, but inherits + the host render mode without an override (correct). +- **Admin experience intact:** only the catalogue's *route* changes; its page, layout, and behavior are + untouched. Authed admins are redirected past the splash and never see it. + +--- + +## 8. Acceptance criteria + +1. An **anonymous** visitor navigating to `/` sees the splash: navy CMS-themed front-door AppBar, + centered hero image (from `wwwroot/img/cms-hero.png`), "Deep Drft" title (Cormorant), a + "Catalogue Management" subtitle (Geist Mono, uppercase, muted), and a single filled "Login" button. + No redirect to the login form occurs; the splash renders. +2. Clicking **Login** navigates to `/account/login` with `returnUrl` resolving to `/catalogue`; a + successful login lands the admin on the catalogue. +3. An **authenticated admin** navigating to `/` is redirected to `/catalogue` (the catalogue + dashboard) without the splash flashing/persisting. +4. An **authenticated non-admin** (logged in, lacking `Admin` role) hitting `/` is redirected toward + `/catalogue` and then to access-denied via the existing `AuthorizeRouteView` path — behavior + unchanged from today's catalogue gating. +5. The **catalogue dashboard** is reachable at `/catalogue`, renders under `CmsLayout` with + `[Authorize]`, and its three summary cards load as before. No regression. +6. `CmsLayout`'s home/"Back to site" button lands an admin on `/catalogue`, not on the splash. +7. The splash uses the **shared `DeepDrftPalettes.Cms` theme** (navy AppBar, off-white ground) — it + reads as the same product as the admin console, not as Skipper and not as a generic MudBlazor page. +8. No `@rendermode` override is introduced; no new palette is introduced; no new entry appears in + `DeepDrftShared.Client`. +9. (If `cms-hero.png` is absent at build time) the page still compiles and renders — only the image is + broken. The splash must not hard-depend on the asset to function (it is an `` `src`, so this + holds by construction). + +--- + +## 9. Out of scope / deferred + +- **Register / invite-code flow** — explicitly omitted (§4). Separate feature if ever wanted. +- **Dark-mode toggle on the splash** — the CMS is light-only today; do not introduce a toggle here. +- **Renaming `Index.razor` → `Catalogue.razor`** — optional cleanup, deferred (§3.4). +- **Cross-host "back to the public site" link** — if "Back to site" should point at `DeepDrftPublic` + rather than `/catalogue`, that needs the public site URL configured in the CMS (it isn't today). + Deferred; default to `/catalogue` (§3.5). +- **Extracting a shared `LoginSplash` component** — YAGNI with one consumer (§7).