---
phase: 4
slug: 04-harden-clean-up-optional
status: approved
reviewed_at: 2026-05-20
reviewer: gsd-ui-checker (5 PASS + 1 FLAG non-blocking on inherited type scale)
shadcn_initialized: false
preset: none
created: 2026-05-20
scope_kind: thin-design-surface
ui_surface: dark-logo-contrast-only
inherits_from:
  - 01-12-SUMMARY.md (design system locked: Lora display + IBM Plex Sans UI + Loom palette + Mokosh mark + canonical tokens.css + 16 i18n keys with en↔ru parity)
  - 01-10-SUMMARY.md (welcome page pattern: data-mokosh-slot + data-mokosh-key + data-mokosh-i18n-key three-pipeline DOM population; 17-key i18n matrix post-notifStartup split)
  - 03-UI-SPEC.md (null-spec precedent — Phase 4 deviates because dark-logo IS real designer work)
in_scope:
  - dark-surface logo contrast strategy (Plan 01-10 obs 2026-05-20 — operator: "also on dark surfaces probably either we need to place the logo on the light background or dunno")
behavioral_inheritance_only:
  - getDisplayMedia cursor visibility constraint `video: { cursor: 'always' }` (Plan 01-07 obs 2026-05-15) — 1-line change in src/offscreen/recorder.ts per Chrome `CursorCaptureConstraint` enum; operator-perceptible but NOT a design surface
---

# Phase 4 — UI Design Contract

> Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker.

---

## Scope Determination

**Phase 4 has ONE thin design surface in scope: dark-surface logo contrast.**

Unlike Phase 3's null-spec, Phase 4 carries a genuine designer-side decision. Per `.plan-phase-preferences.md` (2026-05-20):

> "If /gsd-ui-phase 4 surfaces the 'is this a UI phase?' question, answer 'yes — but narrow scope (dark-logo design only; cursor is behavioral)'"

### Designer-side decision (THIS DOCUMENT decides)

**Mokosh-mark dark-surface contrast strategy** — the canonical mark at `src/shared/brand/mokosh-mark.svg` hardcodes `stroke="#181b2a"` (`--mks-ink-900`, deep indigo). On the welcome page hero (Plan 01-10) the mark already renders inside a `.welcome-hero__mark` wrapper with `background: var(--mks-rec)` (madder-orange #b2543d) → strong contrast per D-04 Loom palette intent. But on ANY browser/OS dark-mode-aware rendering surface where the SVG renders directly on the system background (e.g. operator's browser running in dark theme rendering a saved screenshot or future Phase 4-introduced dark-mode-aware surface), the dark-ink stroke loses contrast.

Cycle-1 operator empirical 2026-05-20: *"also on dark surfaces probably either we need to place the logo on the light background or dunno."* — observation is real but resolution latitude is designer-side.

### Behavioral-only inheritance (NOT a design surface)

**getDisplayMedia cursor visibility constraint** (Plan 01-07 obs 2026-05-15) — adding `video: { cursor: 'always' }` to the existing `navigator.mediaDevices.getDisplayMedia(...)` call site in `src/offscreen/recorder.ts` per Chrome's `CursorCaptureConstraint` enum (`always` / `motion` / `never`). The captured frames will then show the operator's cursor — operator-perceptible (highest-signal cue for pointer-driven bug reproduction) but NO design contract decision required. This is listed here for awareness; the planner will route it directly through normal Phase 4 plan tasks.

---

## Design System (INHERITED — DO NOT MODIFY)

The design system is **closed for Phase 4** EXCEPT for the surgical dark-logo strategy described in this document. Plan 01-12 Wave 7 operator brand-fit ack 2026-05-20 verbatim "all good" sealed the canonical artifacts. Plan 01-10 Wave 4 cycle-2 operator ack 2026-05-20 verbatim "All good" sealed the welcome surface. Phase 4 must NOT introduce new tokens, new fonts, new colors, new copy keys, new icons (beyond the SVG stroke recolor described below), new layout primitives, OR new component patterns into the user-facing surface.

| Property | Value | Source |
|----------|-------|--------|
| Tool | none (manual canonical token system) | Plan 01-12 Wave 1 Task 2 — `src/shared/tokens.css` |
| Preset | not applicable | — |
| Component library | none (vanilla DOM; three-pipeline populate pattern for welcome page) | Plan 01-10 patterns-established |
| Icon library | static PNG artifacts at `icons/icon{16,48,128}.png` (8-bit RGBA; Loom mark rasterized from `src/shared/brand/mokosh-mark.svg` via `scripts/rasterize-icons.sh`) | Plan 01-12 Wave 2 |
| Display font | `"Lora", "Iowan Old Style", "Times New Roman", serif` (Cyreal foundry; OFL-1.1; full Cyrillic via R2 substitution 2026-05-19) | Plan 01-12 Wave 1 Task 2 + brand-decisions-v1-followup-display-font.md |
| UI font | `"IBM Plex Sans", "Segoe UI", -apple-system, BlinkMacSystemFont, sans-serif` (OFL-1.1; full Cyrillic) | `src/shared/tokens.css` line 146 |
| Mono font | `"IBM Plex Mono", "SF Mono", Menlo, Consolas, monospace` (OFL-1.1; full Cyrillic) | `src/shared/tokens.css` line 147 |

**Read-only canonical sources:**

- `src/shared/tokens.css` (~355 lines) — single source of truth; ALREADY contains a `.dark, [data-theme="dark"]` block at lines 234-251 with full dark-theme token overrides
- `src/shared/fonts/*.woff2` (8 files, ~155 KB total) — self-hosted under MV3 CSP
- `src/shared/brand/{mokosh-mark.svg,mokosh-lockup.svg}` — canonical brand assets
- `_locales/{en,ru}/messages.json` — 17 i18n keys per locale (Plan 01-12 + Plan 01-10 closure-cycle split notifStartup → notifStartupCta + notifRecordingStarted)
- `icons/icon{16,48,128}.png` — committed static artifacts (NOT regenerated at build time)

---

## Spacing Scale (INHERITED)

Declared values from `src/shared/tokens.css` lines 178-188. Phase 4 must not introduce new spacing values.

| Token | Value | Usage |
|-------|-------|-------|
| `--mks-space-1` | 4px | Icon gaps, inline padding |
| `--mks-space-2` | 8px | Compact element spacing |
| `--mks-space-3` | 12px | (used by component CSS as needed) |
| `--mks-space-4` | 16px | Default element spacing |
| `--mks-space-5` | 20px | (used by component CSS as needed) |
| `--mks-space-6` | 24px | Section padding |
| `--mks-space-8` | 32px | Layout gaps |
| `--mks-space-10` | 40px | (used by component CSS as needed) |
| `--mks-space-12` | 48px | Major section breaks |
| `--mks-space-16` | 64px | Page-level spacing |
| `--mks-space-20` | 80px | (used by component CSS as needed) |

Exceptions: none in Phase 4 (the dark-logo strategy below does not add new spacing).

---

## Typography (INHERITED)

Declared values from `src/shared/tokens.css` lines 151-167 + semantic helpers lines 254-333. Phase 4 must not introduce new font sizes or weights.

| Role | Size | Weight | Line Height | Token / class |
|------|------|--------|-------------|---------------|
| Body | 15px | 400 (regular) | 1.5 | `.mks-body` |
| Label (small) | 13px | 400 | 1.5 | `.mks-body-sm` |
| Heading | 17px | 600 (semibold) | 1.3 | `.mks-h3` |
| Heading large | 20px | 600 | 1.3 | `.mks-h2` |
| Heading display | 28px | 400 | 1.15 | `.mks-h1` |
| Display | 40px (or 56px for hero) | 400 | 1.15 | `.mks-display-2` / `.mks-display-1` |

Inherited type scale: 11, 13, 15, 17, 20, 28, 40, 56 px. Phase 4 must not add intermediate sizes.

Inherited weights: 400 regular, 500 medium, 600 semibold, 700 bold.

---

## Color (INHERITED — Loom palette per D-04)

Declared values from `src/shared/tokens.css` lines 89-137. Phase 4 must not introduce new colors.

| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | `--mks-linen-50` = #faf7f1 | Page background |
| Surface raised (cards/popup) | `--mks-linen-100` = #f3eee4 | Card / popup surface |
| Secondary (30%) | `--mks-linen-200` = #e8e0d0 | Hairline / divider on linen |
| Foreground primary | `--mks-ink-900` = #181b2a | Primary text, deepest surface, **current canonical mark stroke** |
| Foreground inverse | `--mks-linen-50` = #faf7f1 | Foreground on dark surfaces (`fg-inverse` token) |
| Accent (10%) — REC | `--mks-madder-600` = #b2543d | Recording state badge + REC dot + welcome hero mark BG only |
| Accent — success | `--mks-moss-600` = #5a7349 | "Готово ✓" SAVE-success state only |
| Accent — warning | `--mks-amber-600` = #c98b3a | Recoverable error / SAVING transient state only |
| Destructive | `--mks-brick-600` = #a23a2b | Unrecoverable error state only |

Phase 4 must NOT introduce additional accent surfaces. The dark-mode surfaces consume the existing `.dark, [data-theme="dark"]` block at `src/shared/tokens.css` lines 234-251 (already canonical).

Accent reserved for: REC badge background; REC pulse-dot; SAVE button primary-CTA fill (madder-700 at hover); SAVING transient (amber-600); DONE transient (moss-600); ERROR (brick-600); **welcome-hero mark wrapper background (madder-600)**. Per Plan 01-12 Wave 4 BADGE_REC_COLOR flip from material-green to madder + Plan 01-10 welcome-hero pattern.

---

## Copywriting Contract (INHERITED — read-only)

All operator-facing copy is locked. Phase 4 must NOT add operator-facing strings (the dark-logo strategy adds ZERO new copy — the alt-text fallback already exists at `welcome.hero.mark.alt` in COPY map and the canonical Russian default `Знак Mokosh` at `src/welcome/welcome.ts` line 163).

| Element | Copy | Source |
|---------|------|--------|
| Extension name (EN) | "Mokosh — Session Capture" | _locales/en/messages.json extName (D-07) |
| Extension name (RU) | "Mokosh — Запись сессии" | _locales/ru/messages.json extName |
| Extension description (EN) | "Thirty seconds ago, always at hand." | _locales/en/messages.json extDesc (D-08) |
| Extension description (RU) | "Тридцать секунд назад, всегда под рукой." | _locales/ru/messages.json extDesc |
| Primary CTA (popup SAVE) | "Сохранить отчёт об ошибке" (`popupSaveCta`) | Plan 01-09 + REQ-popup-ui |
| Empty state heading | "Готов к записи" (popupSavePrompt + popupInfoText) | Plan 01-12 Wave 3 |
| Saving transient | "Сохраняю..." (`popupSaving`) | Plan 01-09 state machine |
| Done transient | "Готово! ✓" (`popupSaveDone`) | Plan 01-09 state machine |
| Startup notification CTA | "Mokosh ready. Click to start a recording." (`notifStartupCta` — split from notifStartup per Plan 01-10 closure-cycle debug 4bba679) | Plan 01-10 cycle-1 debug fix |
| Recording started notification | (`notifRecordingStarted`) | Plan 01-10 cycle-1 debug fix |
| Recovery notification | (`notifRecovery`) | Plan 01-09 + Plan 01-12 |
| Welcome hero (RU) | "Тридцать секунд назад, всегда под рукой." (`welcomeHeroRu`) | Plan 01-10 D-08 |
| Welcome hero (EN) | "Thirty seconds ago, always at hand." (`welcomeHeroEn`) | Plan 01-10 D-08 |
| Toolbar tooltip (off / rec / err) | `tooltipOff` / `tooltipRecPrefix` / `tooltipErr` | Plan 01-12 Wave 3 |
| Mokosh mark alt (RU graceful-degradation) | "Знак Mokosh" (`COPY['welcome.hero.mark.alt']` fallback in welcome.ts:163) | Plan 01-10 Wave 4 cycle-1 mark-bundling debug |

**Destructive actions in Phase 4:** none. Phase 4 has no buttons, no confirmations, no UI state machine changes. The dark-logo strategy is a static stroke recolor + a CSS rule.

---

## Dark-Surface Logo Contrast Strategy (PHASE 4 IN-SCOPE DECISION)

### Decision: Option A — `currentColor` SVG + CSS `color` driven via `prefers-color-scheme`

**One-line summary:** Convert the hardcoded `stroke="#181b2a"` in `src/shared/brand/mokosh-mark.svg` to `stroke="currentColor"`; let the rendering container's CSS `color` property drive the stroke; the existing `.dark, [data-theme="dark"]` block at `src/shared/tokens.css` lines 234-251 already provides the dark-surface inverse-token swap.

### Rationale (why A over B, C, D)

This is a designer-side recommendation. The four options surfaced in CONTEXT.md `<specifics>`:

- **(A) media query `@media (prefers-color-scheme: dark)` + `currentColor` SVG** — canonical SVG-theme pattern; works on any rendering surface; respects user's OS-level dark-mode preference automatically; no new tokens; minimum coupling.
- **(B) wrap logo in tinted neutral background (white-circle/off-white card)** — the welcome page ALREADY does this (`.welcome-hero__mark` has madder-600 BG → dark ink stroke contrasts strongly); this is the existing pattern for the only current first-class render surface. Wrapping every future surface in a neutral background is brittle and clashes with the warm-earthy Loom palette intent (D-04).
- **(C) swap to a contrast-tested neutral palette across all surfaces** — would re-open D-04 brand-palette decision; out of scope for Phase 4 hardening and would break the operator brand-fit ack 2026-05-20 "all good".
- **(D) other** — not needed; option A is the standard.

**Why A wins:**

1. **Lowest coupling.** A 1-character edit to the SVG (`#181b2a` → `currentColor`) + the existing `.dark`/`@media` infrastructure in tokens.css handles every present and future render surface. No per-surface override CSS.
2. **Canonical SVG-theme pattern.** `currentColor` is the W3C-blessed mechanism for theming SVG strokes via parent CSS `color`; it's the standard approach used by lucide-icons, heroicons, phosphor-icons, and every major icon library. (Reference: https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint)
3. **Respects OS dark-mode preference.** `@media (prefers-color-scheme: dark)` (already a sibling of the existing `@media (prefers-reduced-motion: reduce)` block at tokens.css lines 336-342) plus the explicit `.dark` class fallback both work; users on macOS/Windows/Linux with system dark mode get the contrast for free without manual config.
4. **Preserves the welcome-hero pattern.** The mark on the welcome page is wrapped in `.welcome-hero__mark { background: var(--mks-rec); color: var(--mks-fg-inverse); }` (welcome.css lines 64-73). After the SVG recolor, the stroke inherits `color: var(--mks-fg-inverse)` → renders as linen-50 (off-white) ON the madder-orange BG → contrast PRESERVED. The existing welcome-hero appearance is unchanged.
5. **Survives future dark-mode-aware surfaces.** Any future Phase 4+ surface (dark popup variant, smoke diagnostic overlay, dark-theme welcome variant — the tokens.css comment at line 232-233 even anticipates these) gets correct contrast without touching the SVG again.
6. **Zero new tokens. Zero new copy. Zero new components.** Surgical, additive, reversible (`git revert` on the SVG file restores prior behavior).

### Implementation contract (planner consumes this)

**File: `src/shared/brand/mokosh-mark.svg`**

```svg
<!-- BEFORE (current canonical) -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none"
     stroke="#181b2a" stroke-width="2.25" stroke-linecap="square"
     stroke-linejoin="miter">

<!-- AFTER -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none"
     stroke="currentColor" stroke-width="2.25" stroke-linecap="square"
     stroke-linejoin="miter">
```

Only `stroke="#181b2a"` → `stroke="currentColor"` on the root `<svg>` element. The 12 child `<line>` and 1 child `<rect>` elements inherit `stroke` from the SVG root; they remain unchanged.

**File: `src/welcome/welcome.css`** (adjust `.welcome-hero__mark-img` rule lines 91-95)

Add `color: inherit;` to `.welcome-hero__mark-img` so the SVG `currentColor` resolves from the wrapper's `color: var(--mks-fg-inverse)` (already at `.welcome-hero__mark` line 72). Or rely on default CSS color inheritance (`.welcome-hero__mark-img` is a direct child of `.welcome-hero__mark` which sets `color: var(--mks-fg-inverse)`, so inheritance happens automatically — verify in implementation).

Wait — the SVG is bundled as `<img>` via Vite `?url` import (welcome.ts line 46 + 166). `<img>` elements do NOT inherit `color` for SVG `currentColor` resolution because they render the SVG as an opaque image, not as inline DOM. **This means option A as drafted above fails for the welcome page hero.**

### Implementation amendment (post-research)

The welcome page consumes the mark via `<img src={markUrl}>` (Vite `?url` import yields a `data:image/svg+xml,...` data URL for assets ≤4096 B). `<img>`-rendered SVGs run in an isolated SVG document context — they do NOT inherit the parent CSS `color`. The `stroke="currentColor"` resolves to the SVG's own root `color`, which defaults to `canvastext` (black on most user agents).

**Two-part solution:**

**Part 1: Inline the SVG into the DOM** (replace `<img src={markUrl}>` with inline `<svg>` injected at `populateMark()` time). The Vite `?url` import becomes a Vite `?raw` import that returns the SVG source text; `populateMark()` parses + inserts the SVG as a DOM child of `.welcome-hero__mark`. Inline-DOM SVGs DO inherit parent CSS `color`, so `stroke="currentColor"` resolves correctly.

```ts
// AT welcome.ts line 46:
// BEFORE: import markUrl from '../shared/brand/mokosh-mark.svg?url';
// AFTER:  import markSvg from '../shared/brand/mokosh-mark.svg?raw';

// AT welcome.ts populateMark() function:
// BEFORE: const img = document.createElement('img'); img.src = markUrl; ...
// AFTER: parse markSvg via DOMParser; append the <svg> root to the slot;
//        apply class='welcome-hero__mark-img' to the inline <svg>; preserve
//        aria-hidden + role='img' + aria-label attributes.
```

**Part 2: PNG icons keep the hardcoded dark stroke.** Toolbar icons at `icons/icon{16,48,128}.png` are static rasterizations from the SVG (8-bit RGBA via `scripts/rasterize-icons.sh`). They render in `chrome://extensions` settings, the toolbar, and the Chrome menu — all surfaces that Chrome owns and that DO respect Chrome's own dark-theme handling (Chrome auto-inverts dark icons on dark toolbars). PNG icons do NOT consume CSS `color` (they're rasterized pixels). They keep `stroke="#181b2a"` baked into their pixels per the existing `scripts/rasterize-icons.sh` recipe. **No change needed for PNG icons.**

**Net file-change set (planner reference):**

| File | Change | Lines affected |
|------|--------|----------------|
| `src/shared/brand/mokosh-mark.svg` | `stroke="#181b2a"` → `stroke="currentColor"` on root `<svg>` | Line 2 |
| `src/welcome/welcome.ts` | `?url` import → `?raw` import + `populateMark()` rewrite (DOMParser-based inline-SVG injection) | Lines 46, 159-179 |
| `src/welcome/welcome.css` | (optional) `.welcome-hero__mark-img` styled to target both `img.` and `svg.` selectors during transition | Lines 91-95 |
| `globals.d.ts` | Add ambient module declaration for `*.svg?raw` (alongside existing `*.svg?url`) | append 1 block |
| `tests/uat/extension-page-harness.ts` (A17.8) | Update sub-check to detect inline SVG presence in the welcome chunk (not the data URL) | Wave 3 b112cb7 |
| `scripts/rasterize-icons.sh` | NO CHANGE (PNG icons keep ink-stroke; Chrome handles toolbar dark-mode) | — |
| `icons/icon{16,48,128}.png` | NO CHANGE (rasterized pixels; Chrome auto-inverts on dark toolbars) | — |
| `src/shared/tokens.css` | NO CHANGE (existing `.dark, [data-theme="dark"]` block at lines 234-251 already handles inverse tokens) | — |

### Acceptance criteria for the dark-logo strategy

1. **Welcome-hero appearance unchanged on light surface.** Operator loads extension; opens welcome page; mark renders inside madder-600 circle with linen-50 (off-white) stroke — identical to current Plan 01-10 cycle-2 ack appearance (since `color: var(--mks-fg-inverse)` at `.welcome-hero__mark` resolves to `--mks-linen-50` = #faf7f1, and the inline SVG inherits this via `currentColor`).
2. **Welcome-hero appearance unchanged on dark surface.** Operator with OS dark-mode enabled, OR future dark-theme welcome variant via `.dark`/`[data-theme="dark"]`, sees the mark rendered with `--mks-fg-inverse` overridden to `--mks-ink-900` (deep indigo) per tokens.css lines 234-251 `.dark` block → on dark surface, ink stroke contrasts strongly against the dark background; the wrapper's madder BG remains (REC accent is dark-mode-stable per D-04 palette).
3. **A17.8 sub-check updated.** The Plan 01-10 harness sub-check A17.8 currently asserts the welcome chunk JS contains the inlined `data:image/svg+xml,...` data URL with canonical `viewBox='0 0 32 32'`. After Phase 4 lands, A17.8 asserts the welcome chunk contains the raw SVG source as a string literal with `currentColor` + canonical `viewBox='0 0 32 32'`. (Or A17.8 becomes A17.8a + A17.8b: A17.8a — raw SVG source bundled; A17.8b — inline `<svg>` injected at populateMark() time and `currentColor` stroke resolves via parent CSS color.)
4. **PNG toolbar icons unchanged.** `icons/icon{16,48,128}.png` byte-identical to current commit hashes; `tests/build/icons-present.test.ts` 15-case suite remains GREEN without modification.
5. **MV3 CSP self-host invariant unchanged.** Inline SVG injection uses DOMParser + appendChild — no `innerHTML`, no `eval`, no new script-src cost. Pre-checkpoint bundle gate per `feedback-pre-checkpoint-bundle-gates.md` remains 6/6 GREEN.
6. **Plan 01-10 cycle-2 ack-equivalent UAT.** Operator empirical re-validation on a dark-mode-enabled OS (macOS dark mode OR Chrome dark theme OR Windows dark mode — pick whichever the operator runs) confirms the mark contrasts on a dark surface. **This is the ONE Phase 4 operator-empirical checkpoint required by the dark-logo strategy.** Per `feedback-trust-harness-over-manual-uat.md`, harness coverage of `currentColor` resolution + inline-SVG injection is sufficient for the automated path; operator confirms the dark-surface aesthetic.

### Out of scope for the dark-logo strategy

- **A dedicated dark-theme welcome variant.** The `.dark` block in tokens.css is canonical and future-ready, but Phase 4 does NOT introduce a `.dark` toggle on the welcome page. Phase 4 ships the SVG `currentColor` + inline-injection path so that IF a future plan adds a dark welcome variant, the mark already contrasts correctly. No new dark-mode CSS classes on `body.welcome` or the welcome-hero.
- **A separate dark-variant SVG file.** Option A's strength is one SVG file driven by CSS color; we explicitly reject the two-SVG approach (mokosh-mark.svg + mokosh-mark-dark.svg).
- **An icon-library migration.** Mokosh stays on its single hand-authored canonical brand asset.

---

## Test Fixture Conventions (INHERITED from Phase 3)

Phase 4 may extend `tests/uat/extension-page-harness.ts` with A33+ assertions for the dark-logo strategy + the other Phase 4 hardening items. The probe-page conventions established by Phase 3's UI-SPEC remain canonical:

| Convention | Reference |
|------------|-----------|
| Probe HTML does NOT import canonical `src/shared/tokens.css` (the harness page does it once at the top) | 03-UI-SPEC.md Test Fixture Conventions |
| Probe HTML does NOT use `var(--mks-*)` tokens | 03-UI-SPEC.md |
| Probe HTML does NOT import `_locales/*` strings via chrome.i18n | 03-UI-SPEC.md |
| Probe HTML does NOT inject `data-mokosh-slot` / `data-mokosh-key` / `data-mokosh-i18n-key` attributes | 03-UI-SPEC.md |
| Probe HTML MAY use plain `data-test-*` attributes for querySelector targeting | 03-UI-SPEC.md |

**FORBIDDEN_HOOK_STRINGS lockstep:** Phase 4 inherits the 12-entry inventory baseline from Plan 01-10 (12 entries) + Plan 01-12 (+1 = 13) + Plan 03-* (unchanged at 12 or 13 depending on plan-checker audit). If A33+ adds new test-mode symbols, those must be `__MOKOSH_UAT__`-gated AND added to `tests/background/no-test-hooks-in-prod-bundle.test.ts`. Per CONTEXT.md `<decisions>` Claude's Discretion: "Tier-1 FORBIDDEN_HOOK_STRINGS stays at 12 unless A33+ needs new __MOKOSH_UAT__-gated symbols (audit at plan time; flag if increment needed)."

---

## Registry Safety

| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none — manual canonical token system | not applicable |
| Third-party | none | not applicable |

Phase 4 introduces no third-party UI dependencies. The dark-logo strategy edits the hand-authored canonical Mokosh mark SVG.

---

## Inherited Visual Contracts to Preserve

Phase 4 verification will incidentally exercise these contracts (via existing harness assertions A0-A32 that MUST remain GREEN per CONTEXT.md `<code_context>` Integration Points). Phase 4 must not regress any of them.

| Contract | Source | How Phase 4 preserves it |
|----------|--------|--------------------------|
| MV3 CSP self-host invariant (0 `googleapis` / 0 `https://fonts` in `dist/`) | Plan 01-12 Wave 1 Task 2 + `tests/build/no-remote-fonts.test.ts` | Phase 4 ships no new fonts; SVG inline-injection uses DOMParser (no innerHTML); pre-checkpoint bundle gate re-verifies |
| en↔ru i18n parity (17 keys × 2 locales) | Plan 01-12 Wave 3 + Plan 01-10 cycle-1 split + `tests/i18n/locale-parity.test.ts` | Phase 4 ships no new locale keys; pre-checkpoint bundle gate re-verifies |
| Icons rendering at 16/48/128 (8-bit RGBA) | Plan 01-12 Wave 2 + `tests/build/icons-present.test.ts` + harness A19 | Phase 4 does not modify PNG icons; A19 remains GREEN |
| `--mks-font-display` resolves to Lora stack | Plan 01-12 Wave 1 Task 2 + harness A21 | Phase 4 does not modify tokens.css; A21 remains GREEN |
| `--mks-rec` resolves to `rgb(178, 84, 61)` = #b2543d | Plan 01-12 Wave 4 BADGE_REC_COLOR + harness A17.7 | Phase 4 does not modify palette; A17.7 remains GREEN |
| BADGE_REC_COLOR madder + 3-state state machine | Plan 01-09 + Plan 01-12 Wave 4 | Phase 4 makes no badge changes |
| Welcome page first-install activation (onInstalled('install') + storage flag + tabs.create) | Plan 01-10 Wave 2 helper + harness A15/A16/A17 | Phase 4 makes no onboarding changes |
| Welcome-hero mark visible (canonical Mokosh SVG bundled) | Plan 01-10 Wave 4 cycle-1 mark-bundling debug + A17.8 | A17.8 sub-check UPDATED — see "Dark-Surface Logo" Acceptance Criterion #3 above |
| Welcome-hero appearance on light surface unchanged | Plan 01-10 Wave 4 cycle-2 ack 2026-05-20 "All good" | After `currentColor` + inline-SVG land, `color: var(--mks-fg-inverse)` cascades from `.welcome-hero__mark` wrapper → linen-50 stroke on madder-orange BG → visually identical to cycle-2 ack |

---

## Behavioral-Only Inheritance (NOT a Design Surface)

These items are listed for awareness; they do NOT solicit designer input. Phase 4 planner routes them through normal plan tasks.

| Item | File | Change | Operator-perceptible? | Design contract? |
|------|------|--------|----------------------|------------------|
| getDisplayMedia cursor visibility constraint | `src/offscreen/recorder.ts` | Add `video: { cursor: 'always' }` to `navigator.mediaDevices.getDisplayMedia(...)` constraints per Chrome `CursorCaptureConstraint` enum | YES — captured frames will include the operator's cursor (highest-signal cue for pointer-driven bug reproduction; Plan 01-07 obs 2026-05-15) | NO — no visual surface; no copy; no UI state |

Reference: https://www.w3.org/TR/screen-capture/#dom-cursorcaptureconstraint

---

## Checker Sign-Off

This UI-SPEC declares ONE thin design surface (dark-logo contrast) with a designer-recommended strategy + amendment for the inline-SVG technical requirement. The 6 design quality dimensions are evaluated against the inherited surface + the dark-logo strategy.

- [x] Dimension 1 Copywriting: PASS (inherited; 17-key matrix locked; dark-logo strategy adds ZERO new operator copy)
- [x] Dimension 2 Visuals: PASS (canonical brand artifacts unchanged in shape; only stroke color binding changes — `#181b2a` literal → `currentColor` CSS-driven; visual appearance on the existing welcome-hero surface is identical to Plan 01-10 cycle-2 ack)
- [x] Dimension 3 Color: PASS (inherited; Loom palette locked at `src/shared/tokens.css`; no new colors; dark-logo strategy consumes existing `.dark, [data-theme="dark"]` block at lines 234-251)
- [x] Dimension 4 Typography: PASS (inherited; 3 families × declared sizes/weights locked; no new sizes)
- [x] Dimension 5 Spacing: PASS (inherited; `--mks-space-*` scale locked; 4px base preserved)
- [x] Dimension 6 Registry Safety: PASS (no shadcn use; no third-party registry; vanilla DOM + DOMParser inline SVG)

**Approval:** pending checker review (draft status)

---

## Notes for Downstream Phase 4 Agents

**Planner (`gsd-planner`):** Phase 4 has ONE genuine design surface (dark-logo contrast — see "Dark-Surface Logo Contrast Strategy" section). Allocate it to a small dedicated plan (suggested as plan 04-06 per CONTEXT.md `<decisions>` Claude's Discretion: "04-06: Visual polish — cursor visibility constraint + dark-logo contrast (operator-perceptible)"). The implementation contract above lists the 5 file edits + 4 acceptance criteria. The cursor-visibility constraint is behavioral-only and can co-land in the same plan.

**Executor (`gsd-executor`):** When editing `src/shared/brand/mokosh-mark.svg`, change ONLY `stroke="#181b2a"` to `stroke="currentColor"` on the root `<svg>` element. Do NOT modify the 13 child elements (1 `<rect>` + 12 `<line>`s). When editing `src/welcome/welcome.ts`, swap the `?url` import for `?raw`, then rewrite `populateMark()` to use DOMParser to inject the inline `<svg>` (preserving aria-hidden + role + canonical class names). NEVER use `innerHTML` — DOMParser + appendChild only (MV3 CSP discipline).

**UI auditor (if spawned):** Audit the Phase 1 + Phase 2 + Phase 3 surfaces remain unchanged post-Phase-4. The dark-logo strategy's visual diff is intentional and approved by this document.

**Ceremony note:** If during execution the planner OR executor discovers the dark-logo strategy needs operator input on the recolor approach (e.g. the operator expresses a preference for option B/C/D over A), route through `/gsd-debug` or `/gsd-discuss-phase` per `feedback-gsd-ceremony-for-fixes.md`. Do not hot-edit this UI-SPEC or the canonical design system.

---

*Phase: 04-harden-clean-up-optional*
*UI-SPEC drafted: 2026-05-20*
*Designer-side decision logged: Option A — `currentColor` SVG + CSS `color` driven via existing `.dark, [data-theme="dark"]` block in tokens.css; inline SVG injection in welcome.ts via `?raw` import + DOMParser (amendment after `<img>` `currentColor` isolation discovered)*
