---
phase: 17-ui-optimization-polish
plan: 04
type: execute
wave: 2
depends_on: ["17-01", "17-02"]
files_modified:
  - apps/pwa/src/components/BrandSlot.tsx
  - apps/pwa/index.html
  - apps/pwa/vite.config.ts
  - apps/pwa/src/styles/tokens.css
autonomous: true
requirements: [D-04, D-05]
must_haves:
  truths:
    - "The real logo renders in BrandSlot on /login with no LoginPage layout shift; <h1> still carries the app name"
    - "index.html links favicon.svg + favicon.ico + apple-touch-icon, and theme-color matches the approved accent"
    - "The PWA manifest references the proper separate icon-maskable-512.png (not a reused icon-512.png)"
    - "The approved brand accent is applied consistently across tokens.css, index.html, and the manifest"
  artifacts:
    - path: "apps/pwa/src/components/BrandSlot.tsx"
      provides: "Logo img swapped in for the placeholder div, decorative (alt empty, aria-hidden)"
      contains: "logo.svg"
    - path: "apps/pwa/index.html"
      provides: "favicon link set + theme-color meta"
      contains: "favicon.svg"
  key_links:
    - from: "apps/pwa/vite.config.ts"
      to: "apps/pwa/public/icon-maskable-512.png"
      via: "manifest icons[] references the separate maskable file with purpose maskable"
      pattern: "icon-maskable-512.png"
    - from: "apps/pwa/src/components/BrandSlot.tsx"
      to: "apps/pwa/public/logo.svg"
      via: "img src /logo.svg"
      pattern: "logo.svg"
---

<objective>
Wire the approved branding assets (from plan 17-02) into the app (D-05, D-04): swap the BrandSlot placeholder div for a decorative logo img through the existing seam (no LoginPage layout change), add the missing favicon links to index.html, fix the vite.config.ts manifest to reference the proper separate `icon-maskable-512.png`, and apply the operator-selected brand accent plus `--brand-logo-border-radius` (from the 17-02 checkpoint) consistently across tokens.css, index.html, and the manifest.

This plan runs AFTER 17-02 (assets exist + accent/border-radius approved at the checkpoint) and 17-01 (tokens.css restructured, `--brand-logo-*` live in the combined block). It does not touch logo art — only wiring + the approved token values.

Purpose: replace placeholder stubs/missing favicons with the real, approved identity and fix the improper-maskable defect.
Output: BrandSlot img, index.html favicon links + theme-color, manifest icons[] with separate maskable, and the accent/border-radius token values.
</objective>

<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/17-ui-optimization-polish/17-UI-SPEC.md
@.planning/phases/17-ui-optimization-polish/17-PATTERNS.md
@.planning/phases/17-ui-optimization-polish/17-RESEARCH.md
@.planning/phases/17-ui-optimization-polish/17-01-SUMMARY.md
@.planning/phases/17-ui-optimization-polish/17-02-SUMMARY.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Swap BrandSlot placeholder div for the logo img and set --brand-logo-border-radius</name>
  <files>apps/pwa/src/components/BrandSlot.tsx, apps/pwa/src/styles/tokens.css</files>
  <read_first>
    - apps/pwa/src/components/BrandSlot.tsx lines 29-50 (the placeholder div to replace; note the h1 FamilySync and tagline p must stay)
    - 17-UI-SPEC.md section "Workstream B" -> "BrandSlot swap contract (D-05)" and section "Surface Architecture" -> "Surface B-1" (the exact img style object)
    - 17-PATTERNS.md section "apps/pwa/src/components/BrandSlot.tsx" (before/after excerpt)
    - 17-RESEARCH.md section "Common Pitfalls" -> Pitfall 5 (--brand-logo-border-radius 50% clips an SVG into a circle)
    - 17-02-SUMMARY.md (the operator-approved --brand-logo-border-radius value from the checkpoint)
    - .planning/phases/19-local-auth-no-oidc-mode/19-UI-SPEC.md section "Brand Slot" (the Phase 19 seam contract — LoginPage layout MUST NOT change)
  </read_first>
  <action>
    In apps/pwa/src/components/BrandSlot.tsx, replace the placeholder div (currently rendering the "FS" initials with aria-hidden) with a decorative logo image element: src "/logo.svg", empty alt, aria-hidden true. Apply the existing brand-logo token surface to the image per UI-SPEC Surface B-1: width var(--brand-logo-size, 48px), height var(--brand-logo-size, 48px), borderRadius var(--brand-logo-border-radius), margin "0 auto var(--space-2, 8px)", display block, aspectRatio "1 / 1", objectFit contain, flexShrink 0. The --brand-logo-bg background is NOT applied (no background div now). Keep the h1 "FamilySync" and the tagline p exactly as-is — the logo is decorative; the h1 remains the accessible page name. Do NOT modify LoginPage.tsx (the Phase 19 seam contract forbids it). Do NOT use dangerouslySetInnerHTML.

    In apps/pwa/src/styles/tokens.css, update --brand-logo-border-radius from 50% to the value the operator approved at the 17-02 checkpoint (recorded in 17-02-SUMMARY — likely 0 if the SVG draws its own rounded shape, or 12px for a square mark; per RESEARCH Pitfall 5, leaving it at 50% would clip the logo into a circle). This is the only tokens.css edit in this task; it goes inside the combined :root, [data-theme="light"] block.
  </action>
  <verify>
    <automated>grep -q 'src="/logo.svg"' apps/pwa/src/components/BrandSlot.tsx && grep -q 'alt=""' apps/pwa/src/components/BrandSlot.tsx && grep -q 'FamilySync' apps/pwa/src/components/BrandSlot.tsx && ! grep -q 'dangerouslySetInnerHTML' apps/pwa/src/components/BrandSlot.tsx && pnpm --filter @familysync/pwa build</automated>
  </verify>
  <acceptance_criteria>
    - BrandSlot renders a decorative logo image (src "/logo.svg", empty alt, aria-hidden); the h1 "FamilySync" and tagline p are unchanged.
    - --brand-logo-border-radius in tokens.css equals the operator-approved value from 17-02-SUMMARY (not the old 50%, unless the operator explicitly chose circular).
    - LoginPage.tsx is NOT in this plan's diff (seam contract honored).
    - playwright-cli on /login: the logo renders, no layout shift vs the placeholder (aspect-ratio 1/1 + explicit width), h1 "FamilySync" still present as text. Observation noted in SUMMARY.
    - pnpm --filter @familysync/pwa build exits 0.
  </acceptance_criteria>
  <done>BrandSlot shows the real decorative logo via the seam with no LoginPage change, the border-radius token matches the approved shape, and the login view has no layout shift.</done>
</task>

<task type="auto">
  <name>Task 2: Wire index.html favicons + theme-color and fix the vite.config.ts manifest maskable + accent</name>
  <files>apps/pwa/index.html, apps/pwa/vite.config.ts, apps/pwa/src/styles/tokens.css</files>
  <read_first>
    - apps/pwa/index.html (current: one apple-touch-icon link, theme-color #4A90D9, no rel=icon links)
    - apps/pwa/vite.config.ts lines ~33 (theme_color) and ~38-42 (the icons array; the last entry reuses icon-512.png for maskable — the defect)
    - 17-UI-SPEC.md section "Workstream B" -> "index.html wiring contract" and "vite.config.ts manifest wiring contract" (exact link set + icons[] + theme_color rules)
    - 17-PATTERNS.md sections "apps/pwa/index.html" and "apps/pwa/vite.config.ts" (before/after excerpts)
    - 17-02-SUMMARY.md (the operator-selected accent hex from the checkpoint — applies to index.html theme-color + manifest theme_color; tokens.css --color-member-0 only if Variant B)
    - 17-UI-SPEC.md section "Color" -> "Brand accent checkpoint" (which files flip for the accent)
  </read_first>
  <action>
    In apps/pwa/index.html, add the favicon links in order (SVG first for modern browsers, ICO second for legacy): a rel=icon link to /favicon.svg with type image/svg+xml, then a rel=icon link to /favicon.ico with sizes "any". Update the existing apple-touch-icon link to include sizes "180x180". Add the apple-mobile-web-app meta tags per UI-SPEC (capable yes, status-bar-style default, title FamilySync). Set the theme-color meta content to the operator-approved accent hex from 17-02-SUMMARY (#4A90D9 if Variant A was kept).

    In apps/pwa/vite.config.ts, fix the manifest icons array so the maskable entry references the SEPARATE /icon-maskable-512.png file (src /icon-maskable-512.png, sizes 512x512, type image/png, purpose maskable) — remove the defective reuse of /icon-512.png for the maskable purpose. Keep the /icon-192.png and /icon-512.png (purpose any) entries. Update the manifest theme_color to the same approved accent hex so it matches index.html.

    If the operator selected the warm accent (Variant B) at the 17-02 checkpoint, ALSO update --color-member-0 in tokens.css to the chosen hex (this is the only additional tokens.css edit, inside the combined block; --sx-color-primary already maps to --color-member-0 and follows automatically). If Variant A was kept, no tokens.css color edit is needed.
  </action>
  <verify>
    <automated>grep -q 'favicon.svg' apps/pwa/index.html && grep -q 'favicon.ico' apps/pwa/index.html && grep -q 'icon-maskable-512.png' apps/pwa/vite.config.ts && ! grep -E "/icon-512.png'.*maskable|maskable.*/icon-512.png'" apps/pwa/vite.config.ts && pnpm --filter @familysync/pwa build</automated>
  </verify>
  <acceptance_criteria>
    - index.html contains rel=icon links to /favicon.svg (svg first) and /favicon.ico (sizes any), plus the apple-touch-icon with sizes 180x180.
    - index.html theme-color content equals the approved accent hex; vite.config.ts manifest theme_color matches it.
    - vite.config.ts manifest maskable entry references /icon-maskable-512.png (the separate file), and no manifest entry uses /icon-512.png with purpose maskable.
    - If Variant B accent was chosen: tokens.css --color-member-0 updated to the chosen hex; otherwise tokens.css color unchanged.
    - pnpm --filter @familysync/pwa build exits 0; playwright-cli on / confirms manifest icon links resolve (no 404). Observation noted in SUMMARY.
  </acceptance_criteria>
  <done>index.html links the full favicon set with the approved theme-color, the manifest references the proper separate maskable icon and matching theme_color, the accent is applied consistently, and the build passes.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| (none new) | Wiring static asset references + an img element + token values. No runtime data flow, no user input, no new endpoints. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-17-04-01 | Tampering | BrandSlot img / index.html links | accept | The logo img is decorative with empty alt; no dangerouslySetInnerHTML (T-05-24 invariant maintained); favicon links and manifest entries point at committed static files. No new threat above LOW for Workstream B wiring. |

No new high/medium-severity threats. Static asset wiring + token value edits; no executable content, no input surface.
</threat_model>

<verification>
- grep favicon.svg / favicon.ico in apps/pwa/index.html — links present
- grep icon-maskable-512.png in apps/pwa/vite.config.ts — maskable references the separate file
- no manifest entry uses icon-512.png for maskable purpose
- grep src="/logo.svg" + alt="" in BrandSlot.tsx; no dangerouslySetInnerHTML
- pnpm --filter @familysync/pwa build — exits 0
- playwright-cli /login (logo, no layout shift, h1 intact) + / (manifest icons resolve)
</verification>

<success_criteria>
The real logo is wired through the BrandSlot seam with no LoginPage change, the full favicon set + theme-color are in index.html, the manifest references the proper separate maskable icon, the approved accent + logo border-radius are applied consistently, and the build is green.
</success_criteria>

<output>
Create `.planning/phases/17-ui-optimization-polish/17-04-SUMMARY.md` when done.
</output>
