---
phase: 17-ui-optimization-polish
plan: 03
type: execute
wave: 2
depends_on: ["17-01"]
files_modified:
  - apps/pwa/src/components/CalendarShell.tsx
  - apps/pwa/src/App.tsx
  - apps/pwa/e2e/layout.spec.ts
autonomous: true
requirements: [D-01, D-02]
must_haves:
  truths:
    - "The New Event FAB never intersects the BottomTabBar rect on iphone/pixel profiles"
    - "Phone content scrolls fully above the BottomTabBar — the color-legend chips are not occluded"
    - "Desktop layout geometry is unchanged (no extra bottom padding, no FAB offset change)"
    - "A permanent overlap regression assertion guards the FAB↔BottomTabBar geometry in CI"
  artifacts:
    - path: "apps/pwa/e2e/layout.spec.ts"
      provides: "Permanent FAB↔BottomTabBar overlap assertion (iphone + pixel, skipped on desktop)"
      contains: "New Event FAB does not overlap BottomTabBar"
  key_links:
    - from: "apps/pwa/src/components/CalendarShell.tsx"
      to: "apps/pwa/src/styles/tokens.css"
      via: "FAB bottom offset reads var(--bottom-chrome-h)"
      pattern: "var\\(--bottom-chrome-h\\)"
    - from: "apps/pwa/src/App.tsx"
      to: "apps/pwa/src/styles/tokens.css"
      via: "phone contentStyle paddingBottom reads var(--bottom-chrome-h)"
      pattern: "var\\(--bottom-chrome-h\\)"
---

<objective>
Fix the long-standing phone (≤767px) fixed-chrome overlap where the `position: fixed` BottomTabBar covers the New Event FAB and occludes the bottom of the calendar + color-legend chips (D-01, the phase seed defect), using the shared `--bottom-chrome-h` token added by plan 17-01 (D-02 fix technique). Add a permanent overlap regression assertion to `layout.spec.ts` (D-02 regression guard) and run the bounded small-viewport sweep across all three Playwright profiles. CSS-only — no behaviour change, no desktop geometry change.

Purpose: the FAB currently lands on the Admin tab and the "Dev User" legend is clipped at 390×844; this fix lifts the FAB above the bar and reserves content padding so nothing is occluded at rest.
Output: corrected FAB offset (CalendarShell.tsx), phone-only content padding (App.tsx), and a new CI overlap assertion (layout.spec.ts).
</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
</context>

<tasks>

<task type="auto">
  <name>Task 1: Lift the FAB and reserve phone content padding via --bottom-chrome-h</name>
  <files>apps/pwa/src/components/CalendarShell.tsx, apps/pwa/src/App.tsx</files>
  <read_first>
    - apps/pwa/src/components/CalendarShell.tsx lines 463–491 (the phone-only FAB block; current `bottom: 'var(--space-6)'` is the defect site)
    - apps/pwa/src/App.tsx lines 64, 84, 155–163 (`isPhone()`, the `phone` boolean, and `contentStyle` which lacks paddingBottom)
    - apps/pwa/src/components/BottomTabBar.tsx line ~73 (bar height `calc(56px + env(safe-area-inset-bottom, 0px))` — resolves identically to `--bottom-chrome-h`)
    - 17-UI-SPEC.md §"Workstream A — Phone-Layout Overlap Fix" → "Fix contract" + "Visual invariants" (exact before/after values)
    - 17-PATTERNS.md §"apps/pwa/src/components/CalendarShell.tsx" and §"apps/pwa/src/App.tsx" (line-anchored before/after excerpts + the `...(phone ? {...} : {})` spread pattern)
    - 17-RESEARCH.md §"Common Pitfalls" → Pitfall 2 (paddingBottom must be phone-only)
    - 17-01-SUMMARY.md (confirms `--bottom-chrome-h` is available in tokens.css)
  </read_first>
  <action>
    In `apps/pwa/src/components/CalendarShell.tsx`, in the phone-only FAB style block, change ONLY the `bottom` property from `'var(--space-6)'` to `'calc(var(--bottom-chrome-h) + var(--space-6))'`. This sits the FAB `var(--space-6)` (24px) above the BottomTabBar top edge regardless of the safe-area-inset value. Leave `right`, `width`, `height`, `zIndex`, and all other FAB style properties unchanged.

    In `apps/pwa/src/App.tsx`, add `paddingBottom` to `contentStyle` PHONE-ONLY using the existing `phone` boolean and the established spread pattern: append `...(phone ? { paddingBottom: 'var(--bottom-chrome-h)' } : {})` to the `contentStyle` object literal. Do NOT add this unconditionally — desktop has no BottomTabBar and must not gain extra bottom padding (RESEARCH Pitfall 2). Do not change any other contentStyle property.

    Optionally (not required for the visual invariant) reference `var(--bottom-chrome-h)` from BottomTabBar.tsx's height; if you do, the value must remain identical (`calc(56px + env(safe-area-inset-bottom, 0px))`). Skipping this is fine — keep the diff minimal.
  </action>
  <verify>
    <automated>grep -q 'calc(var(--bottom-chrome-h) + var(--space-6))' apps/pwa/src/components/CalendarShell.tsx && grep -q "paddingBottom: 'var(--bottom-chrome-h)'" apps/pwa/src/App.tsx && pnpm --filter @familysync/pwa build</automated>
  </verify>
  <acceptance_criteria>
    - CalendarShell.tsx FAB `bottom` is exactly `calc(var(--bottom-chrome-h) + var(--space-6))`.
    - App.tsx `contentStyle` adds `paddingBottom: 'var(--bottom-chrome-h)'` ONLY inside the `phone` branch (the `...(phone ? {...} : {})` spread); desktop branch has no paddingBottom.
    - `pnpm --filter @familysync/pwa build` exits 0.
    - No other FAB/contentStyle properties changed (diff is minimal, CSS-only).
  </acceptance_criteria>
  <done>The FAB clears the BottomTabBar by var(--space-6), phone content reserves --bottom-chrome-h padding, desktop geometry is untouched, and the build passes.</done>
</task>

<task type="auto">
  <name>Task 2: Add the FAB↔BottomTabBar overlap regression assertion to layout.spec.ts</name>
  <files>apps/pwa/e2e/layout.spec.ts</files>
  <read_first>
    - apps/pwa/e2e/layout.spec.ts lines 1–60 (file header, imports, existing `test.describe` + `boundingBox()` assertion pattern, the `navigation`/`Main navigation` role locator)
    - apps/pwa/playwright.config.ts (the three profiles: iphone 390×844 WebKit, pixel 412×915 Chromium, desktop 1280×720 — the assertion skips desktop via `testInfo.project.name`)
    - 17-UI-SPEC.md §"Workstream A" → "Regression guard (D-02 decision)" (the exact assertion contract)
    - 17-PATTERNS.md §"apps/pwa/e2e/layout.spec.ts" (the new overlap assertion excerpt)
    - 17-VALIDATION.md §"Wave 0 Requirements" (this assertion is the Wave 0 overlap-regression item)
  </read_first>
  <action>
    Add a new Playwright test to `apps/pwa/e2e/layout.spec.ts` named "New Event FAB does not overlap BottomTabBar (A — phone only)". It must: skip on the desktop profile via `test.skip(testInfo.project.name === 'desktop', 'Phone-only assertion')`; `page.goto('/calendar')`; locate the FAB via `page.getByRole('button', { name: 'New Event' })` and the bar via `page.getByRole('navigation', { name: 'Main navigation' })`; read both `boundingBox()`; assert neither is null; and assert `fabBox.y + fabBox.height <= navBox.y` (the FAB bottom edge is at or above the BottomTabBar top edge). Match the existing file's test style and import (`import { test, expect } from '@playwright/test'`).

    This is the permanent regression guard for the seed defect. It runs on iphone + pixel (the profiles that exposed the long-standing, desktop-invisible defect).
  </action>
  <verify>
    <automated>grep -q 'New Event FAB does not overlap BottomTabBar' apps/pwa/e2e/layout.spec.ts && pnpm --filter @familysync/pwa exec playwright test --project=iphone --project=pixel layout.spec.ts</automated>
  </verify>
  <acceptance_criteria>
    - The new test "New Event FAB does not overlap BottomTabBar (A — phone only)" exists in layout.spec.ts.
    - It skips on desktop (`testInfo.project.name === 'desktop'`).
    - It asserts `fabBox.y + fabBox.height <= navBox.y`.
    - `pnpm --filter @familysync/pwa exec playwright test --project=iphone --project=pixel layout.spec.ts` is GREEN (the assertion passes against the Task 1 fix — proving the overlap is resolved on both phone profiles).
  </acceptance_criteria>
  <done>The overlap assertion is committed to layout.spec.ts, skips desktop, and passes green on iphone + pixel — confirming the fix and guarding against regression.</done>
</task>

<task type="auto">
  <name>Task 3: Run the bounded small-viewport sweep across all three profiles</name>
  <files>apps/pwa/e2e/layout.spec.ts (sweep run — fix only violations flagged, CSS-only)</files>
  <read_first>
    - 17-UI-SPEC.md §"Workstream A" → "Small-viewport sweep (D-01)" (the checklist: Admin tab tap target, ColorLegend chips visible, no overflow on phone profiles)
    - 17-VALIDATION.md §"Per-Task Verification Map" D-01 rows (color-legend visible, no horizontal overflow Rule 2, tap targets Rule 1)
    - apps/pwa/e2e/layout.spec.ts (Rules 1–4 assertions: ≥44px/≥56px tap targets, no overflow, in-viewport, accessible names)
    - .claude/skills/playwright-cli/ (for the manual color-legend visibility/scroll observation the spec suite cannot fully cover)
  </read_first>
  <action>
    Run the full `layout.spec.ts` suite across all three profiles (`test:e2e`). The D-01 sweep is checklist-driven by the existing Rules 1–4 assertions — fix ANY violation they flag, staying within the no-behaviour-change boundary (CSS/layout only). Expected areas to confirm per UI-SPEC: the Admin tab still meets ≥44px after the layout fix, the ColorLegend "Dev User"/member chips are fully visible (not occluded) now that content padding is added, and no horizontal overflow on phone profiles.

    Use the playwright-cli skill at 390×844 to visually confirm the color-legend chips are reachable by scrolling and not clipped behind the bar (the occlusion symptom from the seed defect). Do NOT undertake a free-form audit or touch desktop geometry — fix only what the assertions flag.
  </action>
  <verify>
    <automated>pnpm --filter @familysync/pwa test:e2e</automated>
  </verify>
  <acceptance_criteria>
    - `pnpm --filter @familysync/pwa test:e2e` (full 3-profile suite) is GREEN — Rules 1–4 (tap targets, no overflow, in-viewport, accessible names) all pass on iphone/pixel/desktop after the fix.
    - playwright-cli @390×844: the ColorLegend chips are fully visible (scroll-reachable, not clipped behind the BottomTabBar) — observation noted in SUMMARY.
    - No desktop geometry regression (desktop profile assertions unchanged/green).
  </acceptance_criteria>
  <done>The full layout.spec.ts suite is green on all three profiles, the color-legend occlusion is confirmed resolved via playwright-cli, and no desktop geometry changed.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| (none new) | CSS-only layout offsets + a Playwright test. No runtime data flow, no new endpoints, no user input. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-17-03-01 | Tampering | FAB/content CSS offsets | accept | Pure layout geometry via existing CSS custom property; no executable content, no input. No new threat above LOW for Workstream A. |

No new high/medium-severity threats. CSS-only positioning change plus a geometry test.
</threat_model>

<verification>
- `grep -q 'calc(var(--bottom-chrome-h) + var(--space-6))' apps/pwa/src/components/CalendarShell.tsx` — FAB lifted
- `grep -q "paddingBottom: 'var(--bottom-chrome-h)'" apps/pwa/src/App.tsx` — phone padding added
- `grep -q 'New Event FAB does not overlap BottomTabBar' apps/pwa/e2e/layout.spec.ts` — regression guard added
- `pnpm --filter @familysync/pwa exec playwright test --project=iphone --project=pixel layout.spec.ts` — overlap assertion green
- `pnpm --filter @familysync/pwa test:e2e` — full 3-profile sweep green
</verification>

<success_criteria>
The FAB no longer overlaps the BottomTabBar on phone, the color-legend is no longer occluded, desktop geometry is unchanged, a permanent overlap assertion guards the regression in CI, and the full Playwright suite is green across all three profiles.
</success_criteria>

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