---
phase: 06-ux-polish
plan: "04"
subsystem: pwa/styles
tags: [css, animation, d-13]
dependency_graph:
  requires: []
  provides:
    - "@keyframes pulse (apps/pwa/src/styles/tokens.css)"
  affects:
    - LiveSyncIndicator (reconnecting dot now resolves the global pulse keyframe)
    - PushPermissionPrompt (redundant local spin redefinition removed)
tech_stack:
  added: []
  patterns:
    - Global keyframe resolution: all animation consumers reference tokens.css keyframes, not local <style> blocks
key_files:
  created: []
  modified:
    - apps/pwa/src/styles/tokens.css
    - apps/pwa/src/components/PushPermissionPrompt.tsx
decisions:
  - D-13: @keyframes pulse added globally to tokens.css so the LiveSyncIndicator reconnecting dot animates regardless of component mount order; the redundant local spin redefinition in PushPermissionPrompt.tsx removed for hygiene
metrics:
  duration_minutes: 5
  completed_date: "2026-06-10"
  tasks_completed: 2
  files_changed: 2
---

# Phase 06 Plan 04: Sync-Indicator Animations (D-13) Summary

**One-liner:** Added global `@keyframes pulse` to tokens.css and removed the redundant local `@keyframes spin` block from PushPermissionPrompt.tsx so both sync-state animations resolve from the stylesheet.

## Tasks Completed

| Task           | Name                                                               | Commit              | Files                                |
| -------------- | ------------------------------------------------------------------ | ------------------- | ------------------------------------ |
| 1              | Add @keyframes pulse globally; remove redundant spin redefinition  | `81f2678`           | tokens.css, PushPermissionPrompt.tsx |
| 2 (checkpoint) | playwright-cli — confirm spinner spins and reconnecting dot pulses | (verification only) | —                                    |

## What Was Built

The plan's corrected diagnosis was that `@keyframes spin` was already global in `tokens.css` (lines 140–147) so the SyncStateToast spinner already worked. The two real bugs were:

- **Missing `@keyframes pulse`** in `tokens.css` → `LiveSyncIndicator`'s reconnecting dot (`animation: 'pulse 1.4s ease-in-out infinite'`) never animated.
- **Redundant local `<style>` block** in `PushPermissionPrompt.tsx` that redefined `@keyframes spin` — harmless but incorrect; removed for hygiene.

Fix (commit `81f2678`):

- Added `@keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.4 } }` to `tokens.css` directly after `@keyframes spin`, matching the existing block format (no vendor prefixes, no `animation-fill-mode` inside).
- Deleted the `<style>` block from `PushPermissionPrompt.tsx`. The inline `animation: 'spin 1s linear infinite'` style on the Loader2 element was left intact — it still resolves to the global keyframe.
- `LiveSyncIndicator.tsx` and `SyncStateToast.tsx` were not modified; their inline animation references are correct.

## Checkpoint Verification (Task 2)

Verified via playwright-cli against desktop Chromium with `DEV_AUTH_BYPASS=true`:

- **SyncStateToast Loader2 spinner** — computed `animationName === 'spin'`; transform sampled rotating (1s linear infinite). PASS.
- **LiveSyncIndicator reconnecting dot** — computed `animationName === 'pulse'`; opacity oscillating at 1.4s ease-in-out. PASS.
- No console errors about undefined keyframes.

No follow-up fixes were needed after the checkpoint.

## Residual Device-Only Item

**CP-04.3 (iOS device-only):** `PushPermissionPrompt`'s spinner renders only inside an installed iOS/standalone PWA — not drivable in desktop Chromium. Code-confirmed: the spinner uses the same inline `animation: 'spin 1s linear infinite'` that resolves to the global `@keyframes spin` in tokens.css. A human/device spot-check during Phase 3 Gate 2 or the go-live deploy is sufficient.

## Deviations from Plan

None — plan executed exactly as written.

## Threat Flags

None. Pure CSS keyframe addition and redundant inline-style deletion. No data, no network, no auth surface touched. T-06-04 accepted per threat model.

## Self-Check: PASSED

- `apps/pwa/src/styles/tokens.css` — FOUND (contains `@keyframes pulse`)
- `apps/pwa/src/components/PushPermissionPrompt.tsx` — FOUND (no `@keyframes spin` block)
- Commit `81f2678` — FOUND (`fix(06-04): add global pulse keyframe and drop redundant spin redefinition`)
- `@keyframes pulse` present exactly once in tokens.css — CONFIRMED
- No `@keyframes spin` in PushPermissionPrompt.tsx — CONFIRMED
