---
phase: 06-ux-polish
plan: 04
type: execute
wave: 1
depends_on: []
files_modified:
- apps/pwa/src/styles/tokens.css
- apps/pwa/src/components/PushPermissionPrompt.tsx
autonomous: false
requirements: []
must_haves:
truths:
- "Sync indicators actually animate — the SyncStateToast spinner spins and the LiveSyncIndicator reconnecting dot pulses (D-13)"
- "@keyframes pulse exists globally in tokens.css so LiveSyncIndicator's reconnecting dot animates regardless of which components are mounted (D-13)"
artifacts:
- path: "apps/pwa/src/styles/tokens.css"
provides: "global @keyframes pulse (added) alongside the existing @keyframes spin"
contains: "@keyframes pulse"
- path: "apps/pwa/src/components/PushPermissionPrompt.tsx"
provides: "redundant local @keyframes spin ` block (lines ~357–363) to delete; the inline `animation: 'spin 1s linear infinite'` stays
- apps/pwa/src/components/LiveSyncIndicator.tsx — `animation: 'pulse 1.4s ease-in-out infinite'` (~line 69) — the consumer that needs the new keyframe
- apps/pwa/src/components/SyncStateToast.tsx — `animation: 'spin 1s linear infinite'` (~line 158) — already-working consumer, confirm unchanged
- .planning/phases/06-ux-polish/06-RESEARCH.md §"Focus 5" (the corrected diagnosis) + .planning/phases/06-ux-polish/06-UI-SPEC.md §"Animation Contract" (exact pulse keyframe)
In `tokens.css`, add a `@keyframes pulse` block (0%,100% opacity 1; 50% opacity 0.4) directly after the existing `@keyframes spin` block, matching the surrounding format. In `PushPermissionPrompt.tsx`, delete ONLY the redundant local `