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
path
provides
contains
apps/pwa/src/components/BrandSlot.tsx
Logo img swapped in for the placeholder div, decorative (alt empty, aria-hidden)
logo.svg
path
provides
contains
apps/pwa/index.html
favicon link set + theme-color meta
favicon.svg
from
to
via
pattern
apps/pwa/vite.config.ts
apps/pwa/public/icon-maskable-512.png
manifest icons[] references the separate maskable file with purpose maskable
icon-maskable-512.png
from
to
via
pattern
apps/pwa/src/components/BrandSlot.tsx
apps/pwa/public/logo.svg
img src /logo.svg
logo.svg
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.
@.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
Task 1: Swap BrandSlot placeholder div for the logo img and set --brand-logo-border-radius
apps/pwa/src/components/BrandSlot.tsx, apps/pwa/src/styles/tokens.css
- 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)
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.
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
- 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.
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.
Task 2: Wire index.html favicons + theme-color and fix the vite.config.ts manifest maskable + accent
apps/pwa/index.html, apps/pwa/vite.config.ts, apps/pwa/src/styles/tokens.css
- 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)
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.
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
- 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.
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.
<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>
- 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)
<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>
Create `.planning/phases/17-ui-optimization-polish/17-04-SUMMARY.md` when done.