--- 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 `