diff --git a/docs/superpowers/specs/2026-05-03-m7-363-branding-polish-design.md b/docs/superpowers/specs/2026-05-03-m7-363-branding-polish-design.md
new file mode 100644
index 00000000..cfeb057c
--- /dev/null
+++ b/docs/superpowers/specs/2026-05-03-m7-363-branding-polish-design.md
@@ -0,0 +1,253 @@
+# M7 #363 — Branding polish (app title, OG meta, install instructions)
+
+**Status:** Design
+**Milestone:** M7 (v1.0 web-first sweep)
+**Issue:** Fable #363
+**Scope:** Web SPA + Go static-serve handler + repo README. No Flutter changes; no PWA / service worker / installable-app work (deliberately deferred — Flutter #356 is the canonical mobile path, Tauri is the canonical desktop path).
+
+## Goal
+
+Final-mile branding polish before the v1.0 tag:
+
+- Per-route page titles in the browser tab.
+- Per-instance app-name override (`SMARTMUSIC_BRANDING_APP_NAME`) that surfaces in the SPA shell header, page titles, OG meta, and theme color hints.
+- OG / Twitter share-preview metadata so a URL pasted into Slack / Discord / iMessage renders correctly.
+- `` that follows the dark/light theme chosen by the user (and the prefers-color-scheme media query for first paint).
+- Operator-first README rewrite — discovery surface for someone landing on the Forgejo repo cold.
+
+## Out of scope
+
+- PWA manifest, "Install Minstrel" app prompt, Apple touch icon, Android maskable icon — Flutter (#356) covers mobile, Tauri (post-v1) covers desktop. Adding a PWA layer in between is unneeded work.
+- Service worker / offline shell.
+- Hand-designed OG / share image — a programmatically-rendered placeholder ships with this slice; the operator-quality artwork is a separate one-off task.
+- Per-page OG images (each route shares the same single image).
+- Localised page titles (English-only in v1).
+- A general-purpose `/api/instance` runtime config endpoint — `window.__MINSTREL__` exposes the values the SPA needs synchronously without an extra request.
+
+## Architecture
+
+### Branding flows through Go template injection, not a runtime fetch
+
+```
+config.yaml / env: branding.{app_name, description}
+ │
+ ▼ config.Load
+Server.BrandingCfg{ AppName, Description }
+ │
+ ▼ on every GET / (SPA index)
+internal/server/spa.ServeIndex
+ │ parses embedded index.html as html/template once at startup
+ │ renders into a bytes.Buffer, writes response
+ ▼
+HTTP body: rendered index.html with
+ -
{{ .AppName }} ← default; per-route overrides
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+```
+
+Other static SPA assets (`/_app/*`, `/favicon.png`, `/brand/og-image.png`) continue to be served as raw bytes through the existing `http.FileServer` — only the index.html path is templated.
+
+Why template injection instead of a public `/api/instance` endpoint:
+
+1. Share-preview crawlers (Slack, Discord, iMessage, Twitter, OpenGraph aggregators) do not run JavaScript. OG meta has to be present in the served HTML, not injected after hydration.
+2. Once the server is templating HTML for the crawlers, exposing the same values via an inline `` block is free for the SPA — no fetch round-trip, no loading state, no flash of "Minstrel" before the operator's chosen name renders.
+3. The current set of "instance config" is two strings. A typed config endpoint can be added later without redoing this layer; the inline global naturally extends to hold more keys.
+
+### Config surface
+
+`internal/config/config.go` gains a `Branding` struct:
+
+```go
+type BrandingConfig struct {
+ AppName string `yaml:"app_name" env:"SMARTMUSIC_BRANDING_APP_NAME"`
+ Description string `yaml:"description" env:"SMARTMUSIC_BRANDING_DESCRIPTION"`
+}
+```
+
+`Config` adds `Branding BrandingConfig`. Defaults are applied in `Load`:
+
+- `AppName` empty → `"Minstrel"`
+- `Description` empty → `"Self-hosted music server with Subsonic compatibility, smart shuffle, and Lidarr integration."`
+
+`config.example.yaml` documents the new section under a `branding:` block.
+
+### Server wiring
+
+`server.New` gains a `BrandingConfig` parameter — same pattern just shipped for `DataDir` in `6d1709c`. `cmd/minstrel/main.go` threads the config through.
+
+`internal/server/spa.go` (new file, or absorbed into the existing static-serve handler):
+
+- At construction time, reads `index.html` from the embedded SPA filesystem and parses it as `html/template.Template`.
+- `ServeHTTP` renders the template into a `bytes.Buffer` against a small data struct (`{ AppName, Description, OGImageURL }`) and writes the body. Cache-control is `no-cache` so a config change on container restart is reflected immediately.
+- Other paths (`/_app/*`, `/favicon.png`, `/brand/*`) continue to be served by the existing static handler, untouched.
+
+`html/template` context-aware escaping handles XSS automatically: the values land inside an HTML element body for the meta-tag content attributes, and inside a JS string literal in the inline `
"` gets their string safely escaped in both contexts.
+
+### SPA wiring
+
+**`$lib/branding.ts`** (new, ~10 lines):
+
+```ts
+declare global {
+ interface Window {
+ __MINSTREL__?: { appName: string; description: string };
+ }
+}
+export const appName = (): string =>
+ (typeof window !== 'undefined' && window.__MINSTREL__?.appName) || 'Minstrel';
+export const description = (): string =>
+ (typeof window !== 'undefined' && window.__MINSTREL__?.description) ||
+ 'Self-hosted music server.';
+export const pageTitle = (section?: string): string =>
+ section ? `${appName()} · ${section}` : appName();
+```
+
+The `typeof window !== 'undefined'` guard is defensive against any future SSR / pre-render contexts; current SPA mode (`ssr=false`) doesn't strictly need it.
+
+**Per-route titles.** Each `+page.svelte` declares a `` block:
+
+```svelte
+{pageTitle('Search')}
+```
+
+Format follows Q5 decision (larger-to-smaller): `Minstrel · Search`, `Minstrel · Library · Artists`, `Minstrel · Artist · Radiohead`. Dynamic routes resolve from existing query data, with a fallback while loading:
+
+```svelte
+{pageTitle(artist.data ? `Artist · ${artist.data.name}` : 'Artist')}
+```
+
+Routes to wire (audited during implementation; not exhaustively enumerated here):
+
+- Static: `/`, `/search`, `/library`, `/library/artists`, `/library/albums`, `/library/songs`, `/library/hidden`, `/discover`, `/queue`, `/history`, `/playlists`, `/settings`, `/login`, `/admin/*`.
+- Dynamic: `/artist/[id]`, `/album/[id]`, `/playlists/[id]`.
+
+The implementer audits every `+page.svelte` and adds a ``; missing pages fall back to the document-level `{{ .AppName }}` from the templated shell.
+
+**Shell header.** `Shell.svelte` currently renders the literal text `Minstrel` in the header. Replace with `{appName()}`. One-line change.
+
+**Theme-color follow-through.** The two server-templated `` tags with prefers-color-scheme media queries cover first paint and pre-hydration. After hydration, `+layout.svelte` adds an `$effect` that updates a single live `` element's `content` attribute reactively against `theme.resolved` (the existing rune from #362):
+
+```ts
+$effect(() => {
+ const meta = document.querySelector(
+ 'meta[name="theme-color"]:not([media])'
+ ) ?? (() => {
+ const m = document.createElement('meta');
+ m.name = 'theme-color';
+ document.head.appendChild(m);
+ return m;
+ })();
+ meta.content = theme.resolved === 'light' ? '#F8F5EE' : '#14171A';
+});
+```
+
+The result: on a phone, when the user toggles Dark / Light / System in Settings, the address-bar / status-bar tint follows in real time.
+
+## OG / share image
+
+`web/static/brand/og-image.png` — committed PNG, 1200×630, fresh asset. The slice ships a **placeholder** generated once via `tools/gen-og-placeholder.mjs`:
+
+- A small Node script (one-off; runs locally, not in CI) using `@resvg/resvg-js` (or `sharp` if simpler) to rasterise a tiny inline SVG.
+- SVG: Fraunces wordmark "Minstrel" in `parchment` (#E8E4D8), centred on `obsidian` (#14171A) background, accent forest-teal `#4A6B5C` underline rule.
+- Output committed to `web/static/brand/og-image.png`.
+
+Operators / future hand-designed artwork drop a replacement PNG at the same path — no code change required.
+
+If you'd rather treat the hand-designed artwork as a hard prerequisite and skip the placeholder generator, drop `tools/gen-og-placeholder.mjs` from this slice and add a single hand-designed PNG instead.
+
+## README rewrite
+
+New `README.md` structure (operator-first, top-down):
+
+1. **What is Minstrel** — one paragraph, tagline; `` placeholder for a real screenshot to be added later.
+2. **Highlights** — six bullets distilled from `docs/smart-music-server-spec.md`:
+ - OpenSubsonic-compatible (works with existing Subsonic clients).
+ - Server-side smart shuffle (state lives where it belongs).
+ - Dual-like model (general + contextual likes).
+ - Session-aware radio + ListenBrainz similarity.
+ - Lidarr integration for library import + automation.
+ - Web SPA today; Flutter companion in flight (#356).
+3. **Quickstart** — copy-pasteable `docker compose up` block; minimal `compose.yaml` and `config.yaml` snippets; pointer to `config.example.yaml` for the full surface.
+4. **Configuration** — most-asked env vars at a glance: `SMARTMUSIC_STORAGE_DATA_DIR`, `SMARTMUSIC_BRANDING_APP_NAME`, scanner settings, Lidarr / ListenBrainz pointers.
+5. **Updating** — `:main` rolling vs `:v1.0.x` pinned; brief note on database migrations being automatic.
+6. **Specs** — links to the existing `docs/smart-music-{server,client}-spec.md`.
+7. **Development** — current dev workflow / dual-process content, condensed and moved to the bottom (contributor audience).
+8. **License**.
+
+Stale content from the current README is replaced rather than appended. The CI / container-image section becomes a sub-section of "Updating" with current Forgejo Actions truth.
+
+## Error handling
+
+| Failure mode | Behaviour |
+|---|---|
+| `BrandingConfig.AppName == ""` | Falls back to `"Minstrel"` in `config.Load`; SPA `appName()` helper has the same fallback so a missing inline-script bootstrap can't crash titles. |
+| `BrandingConfig.Description == ""` | Falls back to default tagline as above. |
+| `branding:` section absent from yaml | Same as above — zero values become defaults. |
+| Operator sets `AppName` to a string containing HTML / JS | `html/template` escapes safely in both meta-attribute and JS-string contexts. Verified by test (see below). |
+| `og-image.png` missing on disk | Static file 404; share-preview crawlers tolerate broken images. Placeholder generator above prevents this in practice. |
+| `+page.svelte` author forgets to add `` | Falls back to document-level `{{ .AppName }}` — visible but not catastrophic. |
+
+## Testing
+
+Tests are written but **not run during implementation** — CI verifies on push (per project rule).
+
+**Go (`internal/server/spa_test.go`, new):**
+
+- Render index.html with `BrandingCfg{AppName: "Custom", Description: "Custom desc."}`; assert `Custom` present, `og:title` contains `Custom`, inline `window.__MINSTREL__` block present and parseable.
+- Render with zero-value config; assert defaults `"Minstrel"` and the default tagline applied.
+- Render with `AppName = "
"`; assert the rendered body contains escaped output (no live `` close inside the inline `