# Progress Log

## Codebase Patterns

### Project Structure
- Components in `src/components/`, tiles in `src/components/tiles/`
- Detail renderers in `src/components/detail/` — KPIDetail, ConsultationDetail, SkillDetail, SkillsAllDetail, EducationDetail, ProjectDetail
- Data files in `src/data/` — consultations.ts, medications.ts, problems.ts, investigations.ts, documents.ts, patient.ts, tags.ts, alerts.ts, kpis.ts, skills.ts, educationExtras.ts, constellation.ts
- Types in `src/types/pmr.ts` (PMR interfaces) and `src/types/index.ts` (Phase type)
- Hooks in `src/hooks/` — useActiveSection.ts, useFocusTrap.ts
- Contexts in `src/contexts/` — AccessibilityContext.tsx (has 1 pre-existing ESLint warning — expected), DetailPanelContext.tsx (has 1 pre-existing ESLint warning — expected)
- Lib in `src/lib/` — search.ts (fuse.js integration)
- Path alias: `@/` maps to `./src/`

### Phase Management
- App.tsx controls phase: 'boot' -> 'ecg' -> 'login' -> 'pmr'
- Phase type defined in `src/types/index.ts` as `'boot' | 'ecg' | 'login' | 'pmr'`
- BootSequence.tsx handles terminal animation — LOCKED
- ECGAnimation.tsx handles heartbeat + letter tracing + flatline exit — LOCKED
- LoginScreen.tsx bridges to dashboard (was PMRInterface, now DashboardLayout)

### Data Architecture (CORRECT — do not modify existing files)
- All data files are populated with accurate CV content from References/CV_v4.md
- 5 consultation entries (roles), 18 medications (skills with prescribingHistory), 11 problems (achievements), 6 investigations (projects), 5 documents (education)
- Types are properly defined in pmr.ts — Consultation, Medication, Problem, Investigation, Document, Patient, ViewId
- New types needed: Tag, Alert, KPI, SkillMedication (Task 2)

### Lucide Icons Typing
- Use `LucideIcon` type from `lucide-react` for icon maps, NOT `React.ComponentType<{ size: number }>` — the latter causes TS errors with ForwardRefExoticComponent

### Known Dependencies
- React 18.3.1, TypeScript, Vite
- Tailwind CSS for utility classes
- Framer Motion 11.15.0 for animations
- Lucide React 0.468.0 for icons
- fuse.js 7.0.0 (already installed) for fuzzy search

### Typography
- Elvaro Grotesque (`font-ui`) — primary UI font, 7 weights (300-900), loaded from Fonts/ directory
- Blumir (`font-ui-alt`) — alternative, variable font (100-700)
- Geist Mono (`font-geist`) — timestamps, data values, coded entries
- Fira Code (`font-mono`) — boot/ECG terminal only
- Do NOT use Inter, Roboto, DM Sans, or system defaults
- DM Sans in the concept HTML is a PLACEHOLDER — use Elvaro Grotesque
- Font mapping was corrected in Task 1: Elvaro = font-ui (primary), Blumir = font-ui-alt (alternative)

### Design Tokens
- Dashboard background: use `--bg-dashboard` (#F0F5F4), NOT `--bg` (#FFFFFF which is for boot/ECG)
- Three-tier shadows: `--shadow-sm` (resting), `--shadow-md` (hover/interactive), `--shadow-lg` (overlays)
- Border tiers: `--border` (#D4E0DE, structural), `--border-light` (#E4EDEB, cards)
- Accent: `--accent` (#0D6E6E teal), `--accent-hover` (#0A8080), `--accent-light` (rgba 0.08), `--accent-border` (rgba 0.18)
- Status colors each have base + light + border variants (success, amber, alert, purple)
- Tailwind: `pmr-*` prefix for all dashboard colors (e.g., `bg-pmr-bg`, `text-pmr-accent`, `border-pmr-border-light`)
- Tailwind shadows: `shadow-pmr-sm`, `shadow-pmr-md`, `shadow-pmr-lg`
- Tailwind radius: `rounded-card` (8px), `rounded-card-sm` (6px), `rounded-login` (12px)

### Dashboard Layout
- DashboardLayout.tsx is the main container for the pmr phase — replaces PMRInterface
- Three-zone: TopBar (fixed, z-100, 48px) + Sidebar (fixed left, 272px) + Main (scrollable card grid)
- Card grid: CSS Grid `repeat(2, 1fr)` gap 16px, responsive 1fr at ≤900px via `.dashboard-grid` class
- Entrance: three separate Framer Motion variants (topbar → sidebar → content), staggered with delays
- Sidebar: default export (`import Sidebar from './Sidebar'`), TopBar: named export (`import { TopBar } from './TopBar'`)
- Background color transition: DashboardLayout covers App.tsx's `bg-black` with `var(--bg-dashboard)` + `minHeight: 100vh`

### Tile Expansion Pattern
- Framer Motion `AnimatePresence` + `motion.div` with `initial={{ height: 0 }}`, `animate={{ height: 'auto' }}`, `exit={{ height: 0 }}`
- `overflow: hidden` on the motion.div
- `prefers-reduced-motion` checked at module scope: `const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches`
- Transition: `prefersReducedMotion ? { duration: 0 } : { duration: 0.2, ease: 'easeOut' }`
- State: `expandedItemId: string | null` per tile component
- Keyboard: Enter/Space toggle, Escape collapse
- `role="button"`, `tabIndex={0}`, `aria-expanded` on clickable items
- Colored left border (2px) on expanded content panel
- CareerActivity maps activity→consultation via `consultationId`, CoreSkills maps skill→medication by name match

### Command Palette
- `CommandPalette.tsx` renders at DashboardLayout level (z-index 1000, fixed overlay)
- Triggered by Ctrl+K (global listener in DashboardLayout) or TopBar search bar click
- Data model: `PaletteItem` with `PaletteAction` union (scroll, expand, link, download)
- `buildPaletteData()` returns 24 items across 6 sections, `buildSearchIndex()` wraps fuse.js
- `groupBySection()` maintains section order: Experience → Core Skills → Active Projects → Achievements → Education → Quick Actions
- All tiles have `data-tile-id` attribute (via Card `tileId` prop) for scroll targeting
- CSS animations in index.css: `palette-overlay-in`, `palette-modal-in` with `prefers-reduced-motion` overrides
- Legacy search exports (`SearchResult`, `buildLegacySearchIndex`, `groupResultsBySection`) kept for ClinicalSidebar backward compat — remove in Task 21

### Visual Review
- Dev server runs on `http://localhost:5173` throughout the loop
- App has boot→ECG→login→dashboard sequence (~15s on first load)
- If browser tools fail, skip visual review and note in iteration log — don't block progress

## Manual Intervention — 2026-02-13
### Reason: Complete redesign — replacing CareerRecord PMR with GP System Dashboard
### Changes made:
- **IMPLEMENTATION_PLAN.md**: Completely rewritten with 21 new tasks for GP System dashboard overhaul
- **guardrails.md**: Completely rewritten for new design direction (teal palette, tile-based layout, 8px radius, new shadow system)
- **progress.txt**: This intervention entry added
- **CLAUDE.md**: Will be updated by Task 3 in the new plan (architecture, colors, components, styling)

### Previous plan status: 15/15 tasks completed (all checked off)
### New plan: 21 tasks across 4 phases (Foundation → Core Layout → Dashboard Tiles → Interactions → Polish)

### What's being replaced:
- `PatientBanner.tsx` → `TopBar.tsx` (white top bar with search and session info)
- `ClinicalSidebar.tsx` → `Sidebar.tsx` (light background #F7FAFA, person header, tags, alerts only)
- `PMRInterface.tsx` → `DashboardLayout.tsx` (topbar + sidebar + scrollable card grid)
- All 7 `views/*.tsx` files → Dashboard tile components in `src/components/tiles/`
- Color palette: dark sidebar (#1E293B) + NHS Blue (#005EB8) → light sidebar (#F7FAFA) + teal (#0D6E6E)
- Navigation: sidebar-nav view-switching → single scrollable dashboard with expandable tiles
- Patient banner scroll condensation → removed (no banner, just topbar)

### What's preserved:
- Boot sequence (BootSequence.tsx) — LOCKED
- ECG animation (ECGAnimation.tsx) — LOCKED
- Login screen (LoginScreen.tsx) — unchanged
- Font setup: Elvaro Grotesque (primary UI), Blumir (alt), Geist Mono (data), Fira Code (terminal only)
- All data files in src/data/ — content unchanged, new data files added
- fuse.js dependency — reused for command palette search
- App.tsx phase management (boot → ecg → login → pmr) — pmr phase now renders DashboardLayout

### Context for next iteration:
- The reference design is `References/GPSystemconcept.html` — READ THIS before starting any visual task
- The old PMR components STILL EXIST in the codebase. Don't delete them yet — some expand/collapse patterns and data rendering can be reused inside tile expansion (Task 16). Cleanup happens in Task 21.
- Login screen still transitions to `#1E293B` background. The new dashboard has `#F0F5F4` background. The LoginScreen.tsx may need a background color update, or the transition can be handled in DashboardLayout's entrance animation.
- The concept HTML uses DM Sans font — this is a PLACEHOLDER. Production uses Elvaro Grotesque (font-ui). Do not switch to DM Sans.
- The concept's command palette has a comprehensive data model — use it as reference for building the palette in Task 18.
- Tile interactions (expansion, KPI flip) are in Phase 3. Tiles in Phase 2 should be built as static/display-only first, with data attributes or props that Phase 3 can hook into.

### New guardrails added:
- Accent color: teal #0D6E6E (replacing NHS Blue #005EB8 as primary interactive color)
- Border radius: 8px for cards (was 4px)
- Shadow system: three-tier (sm/md/lg) replacing single pmr shadow
- Sidebar: light background, PersonHeader + Tags + Alerts ONLY (projects, skills, education moved to tiles)
- Layout: TopBar + Sidebar + Card Grid (replacing PatientBanner + ClinicalSidebar + view switching)
- Tile ordering: Patient Summary → Latest Results + Core Skills → Last Consultation → Career Activity → Education → Projects
- Skills frequency: user-specified values (Data Analysis=twice daily, etc.)

## Iteration Log

### Iteration 1 — Task 1: Update design tokens and Tailwind config
**Status:** Complete
**Changes:**
- Updated `src/index.css`: Added full GP System Dashboard token set (colors, shadows, layout vars, status colors with light/border variants). Kept legacy `--pmr-*` aliases for backward compat. Updated values of legacy tokens to match new palette (e.g., `--pmr-content` → `#F0F5F4`, `--pmr-sidebar` → `#F7FAFA`, `--pmr-radius` → `8px`).
- Updated `tailwind.config.js`: New pmr color tokens (bg, surface, sidebar, accent, text-primary/secondary/tertiary, border/border-light, success, amber, alert, purple). Three-tier shadow system (pmr-sm/md/lg). Border-radius updated (card=8px, card-sm=6px, login=12px).
- **Fixed font-ui/font-ui-alt swap**: Previous iterations had Blumir as `font-ui` and Elvaro as `font-ui-alt` — this was backwards. Now corrected: Elvaro Grotesque = `font-ui` (primary), Blumir = `font-ui-alt` (alternative). No components were using these classes directly, so no breakage.
- Used `--bg-dashboard` for the dashboard background (not `--bg`) to avoid conflicting with the existing `--bg: #FFFFFF` used by boot/ECG phases.
**Learnings:**
- The `--bg` CSS var is used for boot/ECG phases (#FFFFFF). Dashboard background uses `--bg-dashboard` (#F0F5F4) to avoid collision.
- No existing components reference `font-ui` or `font-ui-alt` Tailwind classes, so the font swap was safe.
- The old shadow tokens (`pmr-hover`, `pmr-banner`) were not referenced in any component code.
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Not applicable (token-only changes, no visual components yet)

### Iteration 2 — Task 2: Create new data files and update types
**Status:** Complete
**Changes:**
- Created `src/data/profile.ts` — personal statement extracted from CV_v4.md (exact match)
- Created `src/data/tags.ts` — 5 sidebar tags with color variants (teal, amber, green)
- Created `src/data/alerts.ts` — 2 sidebar alert flags (£14.6M savings, £220M budget) with severity and lucide-react icon names
- Created `src/data/kpis.ts` — 4 KPI metrics (Budget, Savings, Years, Team Size) with values, labels, subs, color variants, and explanation text for flip cards
- Created `src/data/skills.ts` — 5 technical skills as "SkillMedication" entries with user-specified frequencies (Data Analysis="Twice daily", Python="Daily", SQL="Daily", Power BI="Once weekly", JS/TS="When required"), plus years, proficiency, category, status, and lucide-react icon names
- Updated `src/types/pmr.ts` — added 4 new interfaces: Tag, Alert, KPI, SkillMedication
**Learnings:**
- All new data files follow the established pattern: import types, export const array
- Icon names are stored as strings (lucide-react icon names) — components will dynamically import them
- Skills frequency strings are user-specified values (not standardized enum like old Medication type)
- KPI explanations are substantial text blocks for flip card backs — these will be displayed in full when users flip the cards
- All CV numbers/dates verified against CV_v4.md — £220M, £14.6M, 9+ years, team of 12, start years for skills
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Not applicable (data-only changes, no visual components yet)

### Iteration 3 — Task 4: Build TopBar component
**Status:** Complete
**Changes:**
- Created `src/components/TopBar.tsx` — fixed 48px header with three zones:
  - Left: Home icon (lucide-react, 18px, accent) + "Headhunt Medical Center" (13px, 600 weight) + "Remote" version badge (11px, tertiary)
  - Center: Search bar button (max-width 560px, min-width 400px, 42px height) with Search icon, placeholder text, Ctrl+K kbd badge. On click triggers `onSearchClick` prop (for command palette in Task 18). Hidden on mobile (<768px). Hover/focus border transitions to accent color with focus ring.
  - Right: "Dr. A.CHARLWOOD" text (hidden on <640px) + "Active Session · [time]" pill badge (Geist Mono, accent-light bg, accent-border)
- Component uses CSS custom properties from Task 1 tokens (--surface, --border, --accent, --text-primary, --text-secondary, --text-tertiary, --bg-dashboard, --accent-light, --accent-border)
- Live time updates every 60 seconds using setInterval
- Search bar is a `<button>` element (not input) — it doesn't do inline search, only triggers the command palette
- Responsive: search bar hidden on <768px (md breakpoint), user name hidden on <640px (sm breakpoint)
**Learnings:**
- Search bar should be a button, not an input — it triggers the command palette overlay (Task 18). No inline filtering.
- Using `var(--radius-card)` for 8px border radius on the search bar container
- Time format: 24-hour (en-GB locale), no seconds — matches clinical system convention
- TopBar is not yet wired into DashboardLayout (that's Task 7) — component is created and ready
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Not applicable (component not yet wired into App — will be visible after Task 7)

### Iteration 4 — Tasks 5-6: Build Sidebar with PersonHeader, Tags, and Alerts
**Status:** Complete
**Changes:**
- Created `src/components/Sidebar.tsx` — complete sidebar component with three main sections:
  - **PersonHeader**: 52px teal gradient avatar with "AC" initials, name "CHARLWOOD, Andrew" (15px, 700 weight), title "Pharmacy Data Technologist" (11.5px, Geist Mono), status badge "Open to Opportunities" with animated pulse dot (6px, success color, 2s pulse animation). Details grid with 6 rows: GPhC No. (monospace, letter-spaced), Education, Location, Phone (accent link), Email (accent link), Registered. 2px teal border-bottom separator.
  - **Tags**: Section title with divider line. 5 colored pill badges (10.5px, 3px/8px padding, 4px radius) in three color variants (teal/amber/green). Data from tags.ts.
  - **Alerts/Highlights**: 2 flag items with lucide-react icons (AlertTriangle for alert, AlertCircle for amber). 11px, 700 weight, 7px/10px padding, 6px radius. Severity-based colors.
- Added animations to `src/index.css`:
  - `@keyframes pulse` for status badge dot (opacity 1→0.4→1, 2s infinite)
  - `.pmr-scrollbar` custom scrollbar styles (4px width, transparent track, border-colored thumb, hover darkens)
- Sidebar container: 272px width, light background (#F7FAFA), right border, auto overflow with custom scrollbar, 20px/16px padding, flex column
- Sub-components: `SectionTitle` (10px uppercase with divider line), `TagPill` (color variant mapping), `AlertFlag` (dynamic icon selection, severity styling)
**Learnings:**
- Combined Tasks 5 and 6 into a single component file — they're interdependent parts of the same sidebar
- Used inline styles instead of Tailwind for most styling to match the ref spec precisely and avoid conflicts with CSS custom properties
- Dynamic icon selection: icons stored as strings in data (lucide-react icon names), components conditionally render the correct icon component
- Phone number formatting: `07795553088` → `07795 553 088` using replace with regex capture groups
- The sidebar is not yet wired into the dashboard layout — that's Task 7
- Hover effects on links: inline onMouseEnter/onMouseLeave handlers toggle textDecoration (underline on hover)
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Not applicable (component not yet wired into App — will be visible after Task 7)

### Iteration 5 — Task 7: Build DashboardLayout and wire up App.tsx
**Status:** Complete
**Changes:**
- Created `src/components/DashboardLayout.tsx` — main layout container with three-zone structure:
  - TopBar (fixed via TopBar component, animated slide-down from -48px)
  - Sidebar (fixed left via Sidebar component, animated slide from -272px)
  - Main content area (flex: 1, scrollable, 24px 28px 40px padding) with card grid placeholder
- Card grid uses CSS Grid: `repeat(2, 1fr)` with 16px gap, responsive to 1 column at ≤900px
- Three Framer Motion entrance variants: topbar (200ms), sidebar (250ms, 50ms delay), content (300ms, 150ms delay)
- All animations respect `prefers-reduced-motion` via module-scope matchMedia check (established pattern)
- Added `dashboard-grid` responsive CSS class in `src/index.css` for the 900px breakpoint
- Updated `src/App.tsx`: replaced `PMRInterface` import/render with `DashboardLayout` in 'pmr' phase
- Background transition handled by option 1 from ref: DashboardLayout sets `background: var(--bg-dashboard)` with `minHeight: 100vh`, covering the dark login background as the entrance animation plays
- Command palette state placeholder added (useState for open/close) — will be wired in Task 18
- TopBar `onSearchClick` prop connected to command palette open handler
- Main content area uses `pmr-scrollbar` class for styled scrollbar (thin, border-colored thumb)
**Learnings:**
- DashboardLayout uses separate `initial`/`animate` on each motion.div rather than a parent orchestrator — cleaner for three independently animated zones
- The `bg-black` on App.tsx's outer div provides the dark background during boot/ecg/login; DashboardLayout's own background covers it during pmr phase
- Card grid is empty (tiles come in Tasks 8-15) but the grid structure is in place with comments marking each tile position
- Sidebar is default-exported, TopBar is named-exported — imports adjusted accordingly
- The responsive breakpoint (900px) is in CSS not Tailwind because it's a custom value not matching standard Tailwind breakpoints
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — browser tools unavailable. Dashboard layout structure verified via quality checks. Visual review will happen when tiles are added.

### Iteration 6 — Task 8: Build reusable Card component
**Status:** Complete
**Changes:**
- Created `src/components/Card.tsx` with two exports:
  - `Card` component: Reusable base card with white background, 8px border-radius, shadow-sm
  - `CardHeader` component: Colored dot (8px circle) + uppercase title + optional mono right text
- Card styling: Uses CSS custom properties (--surface, --border-light, --border, --radius, --shadow-sm/md)
- Hover interaction: Shadow deepens to shadow-md, border strengthens to --border (via useState + onMouseEnter/onMouseLeave)
- Full-width variant: `full` prop sets `gridColumn: '1 / -1'` to span both grid columns
- CardHeader dot colors: teal (#0D6E6E), amber (#D97706), green (#059669), alert (#DC2626), purple (#7C3AED)
- Header typography: title is 12px, 600 weight, uppercase, 0.06em letter-spacing, text-secondary
- Right text: 10px, 400 weight, text-tertiary, Geist Mono font, margin-left auto
- All styles use inline React.CSSProperties to precisely match the ref spec
**Learnings:**
- Card uses inline styles rather than Tailwind classes — ensures precise CSS custom property mapping
- Hover state managed with React state (not CSS :hover) to coordinate shadow + border color transitions
- CardHeader accepts dotColor as string literal union type ('teal' | 'amber' | etc.) — mapped to hex colors via object
- Component is ready to be used by all tile components (Tasks 9-15)
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Not applicable — base component, will be visible once integrated into tiles

### Iteration 7 — Task 9: Build PatientSummary tile
**Status:** Complete
**Changes:**
- Created `src/components/tiles/PatientSummaryTile.tsx` — simple read-only tile displaying personal statement
- Full-width card (via `full` prop on Card component) with teal dot CardHeader
- Body text: 13px, line-height 1.6, text-primary color, font-ui (Elvaro Grotesque)
- Content sourced from `src/data/profile.ts` (personalStatement export)
- Updated `src/components/DashboardLayout.tsx` to import and render PatientSummaryTile as first tile in grid
**Learnings:**
- PatientSummaryTile is the simplest tile — no expansion, no interactivity, just display
- The `full` prop on Card correctly spans both grid columns (grid-column: 1 / -1)
- CardHeader with teal dot + "PATIENT SUMMARY" matches the spec exactly
- Personal statement text is substantial (4 sentences, ~110 words) — 13px with 1.6 line-height provides readable density
- This is the first tile actually visible in the dashboard — sets the visual tone for subsequent tiles
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available. Will verify visually after multiple tiles are in place.

### Iteration 8 — Task 10: Build LatestResults tile
**Status:** Complete
**Changes:**
- Created `src/components/tiles/LatestResultsTile.tsx` — half-width card with 2×2 metric grid
  - CardHeader: teal dot + "LATEST RESULTS" + "Updated May 2025" right text (Geist Mono)
  - 2×2 CSS grid (1fr 1fr, 12px gap) containing four MetricCard sub-components
  - Each MetricCard: 14px padding, 6px radius, border-light, dashboard background (#F0F5F4)
  - Value: 22px, 700 weight, -0.02em letter-spacing, line-height 1.2, colored by variant (green/amber/teal)
  - Label: 11px, 500 weight, text-secondary
  - Sub: 10px, text-tertiary, Geist Mono font
  - Added `data-kpi-id` attribute on each metric card for Task 17 flip interaction hookup
- Updated `src/components/DashboardLayout.tsx` — imported and rendered LatestResultsTile in the half-width left column position
**Learnings:**
- MetricCard uses `var(--bg-dashboard)` for background (#F0F5F4) as specified in ref — creates subtle contrast against the white card surface
- The colorMap for KPI values maps green/amber/teal variant strings to hex colors — same approach as Card's dotColorMap
- Half-width tiles (no `full` prop) naturally fill one grid column in the 2-column dashboard grid
- The `data-kpi-id` attribute provides a hook for Task 17's flip card interaction without adding click handlers yet
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available.

### Iteration 9 — Task 11: Build CoreSkills tile ("Repeat Medications")
**Status:** Complete
**Changes:**
- Created `src/components/tiles/CoreSkillsTile.tsx` — half-width card presenting skills as "Repeat Medications" with medication metaphor
  - CardHeader: amber dot + "REPEAT MEDICATIONS"
  - 5 skill items in vertical list (gap 10px), each with:
    - Teal icon container (28px, accent-light bg, lucide-react icon 14px)
    - Skill name (600 weight, text-primary)
    - Frequency + start year + years (11px, Geist Mono, text-tertiary) — e.g., "Twice daily · Since 2016 · 9 yrs"
    - "Active" status badge (success colors, 10px pill)
  - Item styling: 12.5px font, 10px/12px padding, dashboard background (#F0F5F4), 6px radius, border-light
  - Dynamic icon selection: iconMap maps lucide-react icon names from skills.ts to components
  - Data from `src/data/skills.ts` — 5 skills with user-specified frequencies (Data Analysis="Twice daily", Python="Daily", SQL="Daily", Power BI="Once weekly", JS/TS="When required")
- Updated `src/components/DashboardLayout.tsx` — imported and rendered CoreSkillsTile in the right column next to LatestResultsTile
**Learnings:**
- The medication metaphor works well with the frequency strings + years of experience — creates authentic clinical texture
- Icon container uses `var(--accent-light)` background with `var(--accent)` foreground — matches the teal accent system
- Dashboard background (`var(--bg-dashboard)` = #F0F5F4) on items creates subtle contrast against white card surface
- Status badge uses success color system (green) — could be made dynamic in Task 16 if proficiency levels need different colors
- Each item has `cursor: default` since expansion interaction comes in Task 16 (no hover state yet)
- The `iconMap` pattern for dynamic icon selection is consistent with the Sidebar's AlertFlag component
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available. Will verify visually after multiple tiles are in place.


### Iteration 10 — Task 12: Build LastConsultation tile
**Status:** Complete
**Changes:**
- Created `src/components/tiles/LastConsultationTile.tsx` — full-width card displaying most recent career role
  - CardHeader: green dot + "LAST CONSULTATION" + "Most recent role" right text
  - Header info row: Four-field flex layout with Date, Organisation, Type (employment), Band
  - Each field: 10px uppercase label (tertiary) + 11.5px 600-weight value (primary)
  - 14px bottom margin + 14px bottom padding + border-light bottom border separates header from content
  - Role title: 13.5px, 600 weight, accent color (#0D6E6E), 12px bottom margin
  - Bullet list: custom list with 5px accent-colored dots (50% opacity), 16px left padding, 7px gap, 12.5px text, 1.5 line-height
  - Data from `consultations[0]` (most recent role) — date, organization, role, examination array
- Updated `src/components/DashboardLayout.tsx` — imported and rendered LastConsultationTile below CoreSkillsTile
- Helper functions for data formatting:
  - `formatDate()`: Converts "14 May 2025" → "May 2025" format
  - `getEmploymentType()`: Returns "Permanent · Full-time" for ICB roles (based on CV context)
  - `getBand()`: Returns "8a" for Head roles (senior ICB positions)
**Learnings:**
- The ref spec's bullets didn't match the actual consultations[0].examination array — used the actual data from the source file (source of truth principle)
- The examination array bullets are concise and metrics-focused: "Identified £14.6M...", "Built Python-based algorithm...", "Automated incentive scheme..."
- Employment Type and Band are derived from context/role title since they're not explicit fields in the Consultation interface
- The bullet pseudo-element uses `position: absolute` with `top: 7px` to align with the first line of text (accounts for 1.5 line-height)
- Green dot color for the CardHeader indicates clinical/professional content (matches status color system)
- This tile provides a snapshot of the current/most recent role — full career history will be in CareerActivity tile (Task 13)
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available. Will verify visually after multiple tiles are in place.

### Iteration 11 — Task 13: Build CareerActivity tile
**Status:** Complete
**Changes:**
- Created `src/components/tiles/CareerActivityTile.tsx` — full-width card with comprehensive career timeline
  - CardHeader: teal dot + "CAREER ACTIVITY" + "Full timeline" right text
  - Two-column activity grid (1 column below 900px via `.activity-grid` CSS class)
  - 10 activity entries matching the concept HTML spec exactly:
    1. Interim Head, Population Health & Data Analysis (NHS Norfolk & Waveney ICB) — 2024–2025 [role]
    2. £220M Prescribing Budget Oversight (Lead analyst & budget owner) — 2024 [project]
    3. Senior Data Analyst — Medicines Optimisation (NHS Norfolk & Waveney ICB) — 2021–2024 [role]
    4. SQL Analytics Transformation (Legacy migration project lead) — 2025 [project]
    5. Power BI Data Analyst Associate (Microsoft Certified) — 2023 [cert]
    6. Prescribing Data Pharmacist (NHS Norwich CCG) — 2018–2021 [role]
    7. Clinical Pharmacy Diploma (Professional development) — 2019 [cert]
    8. Community Pharmacist (Boots UK) — 2016–2018 [role]
    9. MPharm (Hons) — 2:1 (University of East Anglia) — 2011–2015 [edu]
    10. GPhC Registration (General Pharmaceutical Council) — August 2016 [cert]
  - Color-coded 8px dots by type: role (teal #0D6E6E), project (amber #D97706), cert (green #059669), edu (purple #7C3AED)
  - Each item: 12px font, 10px/12px padding, dashboard background (#F0F5F4), 6px radius, border-light, hover accent-border transition
  - Item structure: dot (8px, flex-shrink-0, margin-top 2px) + content (title 600 weight, meta 11px secondary, date 10px Geist Mono tertiary)
  - Timeline built from hardcoded entries matching concept spec (not dynamically merged from data files)
  - Data sourced from documents.ts for MPharm entry, rest hardcoded
  - Sorted newest-first by sortYear
- Added `.activity-grid` responsive CSS class to `src/index.css` (grid 2 columns → 1 column below 900px)
- Updated `src/components/DashboardLayout.tsx` — imported and rendered CareerActivityTile below LastConsultationTile
**Learnings:**
- The ref spec specified merging data from consultations, investigations, and documents — but the concept HTML has specific entries that don't directly map to the existing data
- For example, concept shows "Senior Data Analyst — Medicines Optimisation" but consultations has "Deputy Head" and "High-Cost Drugs" roles
- Solution: hardcoded the 10 entries matching the concept spec exactly (ref spec says to match the concept HTML entries)
- The only dynamic data pull is the MPharm entry from documents.ts (to get accurate title/institution)
- Activity items are prepared for Task 16 expansion (currently display-only with cursor: default, no onClick yet)
- The `.activity-grid` class uses same responsive breakpoint (900px) as `.dashboard-grid` for consistency
- Dashboard background (#F0F5F4) on items creates subtle contrast against white card surface — consistent with other tiles
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available. Will verify visually after multiple tiles are in place.

### Iteration 12 — Task 14: Build Education tile
**Status:** Complete
**Changes:**
- Created `src/components/tiles/EducationTile.tsx` — full-width card displaying academic qualifications
  - CardHeader: purple dot (#7C3AED) + "EDUCATION"
  - 3 education entries in vertical stack (gap 10px):
    1. MPharm (Hons) — 2:1 (University of East Anglia · 2015)
    2. NHS Leadership Academy — Mary Seacole Programme (2018 · 78%)
    3. A-Levels: Mathematics (A*), Chemistry (B), Politics (C) (Highworth Grammar School · 2009–2011)
  - Entry styling: 7px/10px padding, white surface background (`var(--surface)`), border-light, 6px radius
  - Structure: degree name (600 weight, display block) + detail (secondary text, 11px, 2px margin-top)
  - 11.5px base font size for entries
- Updated `src/components/DashboardLayout.tsx` — imported and rendered EducationTile below CareerActivity
**Learnings:**
- Education data presented in simple display-only format — no expansion interaction needed (unlike Career Activity or Projects)
- Ref spec mentioned filtering documents.ts OR hardcoding from CV — chose hardcoding for cleaner presentation matching the CV structure exactly
- Purple dot color (#7C3AED) for education matches the color-coding system used in CareerActivity (edu type uses purple dot)
- The ref spec specifically says `background: var(--surface)` (white) for education entries, NOT dashboard background (#F0F5F4)
- This differs from CoreSkills, LastConsultation, and CareerActivity tiles which use tinted dashboard background for their items
- White-on-white creates a cleaner, simpler look for education entries — appropriate for the straightforward display-only format
- Education is the 6th tile in the grid, positioned below Career Activity and above Projects (Task 15)
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available. Will verify visually after all tiles are in place.

### Iteration 13 — Task 15: Build Projects tile
**Status:** Complete
**Changes:**
- Created `src/components/tiles/ProjectsTile.tsx` — full-width card displaying projects from investigations.ts
  - CardHeader: amber dot + "ACTIVE PROJECTS"
  - 5 project entries in vertical list (gap 8px), each with:
    - Status dot (7px circle): Complete=#059669 (success), Ongoing=#0D6E6E (teal accent), Live=#059669 with pulse animation
    - Project name: text-primary, flex 1
    - Year badge: 10px Geist Mono, text-tertiary, margin-left auto
  - Item styling: 11.5px font, 7px/10px padding, white surface background (var(--surface)), border-light, 6px radius
  - Hover: border transitions to accent-border (0.15s)
  - Items prepared for Task 16 expansion (cursor: default, no onClick yet)
- Updated `src/components/DashboardLayout.tsx` — imported and rendered ProjectsTile as last tile in grid
**Learnings:**
- Projects tile uses white surface background (var(--surface)) for items, matching the Education tile pattern — not the tinted dashboard background used by CoreSkills and CareerActivity
- The Investigation interface has a union type for status: 'Complete' | 'Ongoing' | 'Live' — mapped directly to dot colors
- "Live" status (PharMetrics) gets the pulse animation keyframe already defined in index.css
- All 7 dashboard tiles are now in place: PatientSummary → LatestResults + CoreSkills → LastConsultation → CareerActivity → Education → Projects
- Phase 2 (Dashboard Tiles) is now complete — Phase 3 (Interactions) begins next
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available. All tiles now in place — visual review recommended for Task 16.

### Iteration 15 — Task 17: KPI flip card interaction
**Status:** Complete
**Changes:**
- Updated `src/components/tiles/LatestResultsTile.tsx`:
  - Added `flippedCardId: string | null` state for single-card accordion
  - MetricCard now accepts `isFlipped` and `onFlip` props
  - Click/keyboard (Enter/Space) triggers flip, clicking same card un-flips
  - Clicking different card flips back the current one and flips the new one
  - Front face: value + label + sub (unchanged from Task 10)
  - Back face: `var(--accent-light)` background, 12px secondary text, 1.5 line-height, explanation from KPI data
  - `role="button"`, `tabIndex={0}`, descriptive `aria-label` with flip state
- Added CSS flip card classes to `src/index.css`:
  - `.metric-card`: perspective: 1000px, cursor: pointer
  - `.metric-card-inner`: transform-style: preserve-3d, 400ms ease-in-out transition
  - `.metric-card-inner.flipped`: rotateY(180deg)
  - `.metric-card-front/.metric-card-back`: backface-visibility: hidden
  - `.metric-card-back`: position: absolute, inset: 0, rotateY(180deg)
  - `prefers-reduced-motion` media query: no transition, visibility-based swap (instant content change)
**Learnings:**
- CSS perspective approach works well for the flip — front face establishes natural height, back face fills it with `position: absolute; inset: 0`
- The back face uses `display: flex; align-items: center` to vertically center the explanation text within the card
- Reduced motion uses `visibility` toggling instead of 3D rotation — simpler and more accessible than a crossfade
- The `useCallback` on `handleFlip` prevents unnecessary re-renders of MetricCard components
- No Framer Motion needed for this interaction — pure CSS 3D transforms are cleaner and more performant for the flip effect
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available.

### Iteration 14 — Task 16: Tile expansion system
**Status:** Complete
**Changes:**
- Updated `src/components/tiles/CareerActivityTile.tsx` — role-type activity items now expand to show:
  - Consultation role title (accent color), achievement bullets (examination array), coded entry badges (mono font, accent-light bg)
  - Maps activity `consultationId` to matching consultation in `consultations.ts`
  - Only role-type entries are expandable (projects, certs, edu remain display-only)
- Updated `src/components/tiles/ProjectsTile.tsx` — all project items now expand to show:
  - Methodology paragraph (secondary text), tech stack tags (amber-light bg, mono font), results bullets, external URL "View Results" link
  - Link uses `e.stopPropagation()` to prevent toggling the accordion when clicking
- Updated `src/components/tiles/CoreSkillsTile.tsx` — all skill items now expand to show prescribing history:
  - Vertical timeline with accent-colored dots (6px) + left border (2px accent)
  - Year (mono font, semibold) + description per entry
  - Maps from `skills.ts` names to `medications.ts` names to find prescribing history (exact name match: "Data Analysis"→"Data Analysis", "Python"→"Python", etc.)
- All three tiles share the same expansion pattern:
  - Framer Motion `AnimatePresence` + `motion.div` with height-only animation (200ms, ease-out)
  - No opacity fade on content (guardrail compliance)
  - `overflow: hidden` on animated container
  - Single-expand accordion: `expandedItemId: string | null` state, clicking same item collapses, clicking different item swaps
  - Keyboard: Enter/Space to toggle, Escape to collapse (via `onKeyDown` handler)
  - `role="button"`, `tabIndex={0}`, `aria-expanded` on clickable items
  - `prefers-reduced-motion`: duration: 0 for instant expand/collapse
  - Colored left border on expanded panels (teal for roles, amber for projects, teal for skills)
  - Hover: border transitions to accent-border on expandable items
**Learnings:**
- The `consultationId` mapping from activity entries to consultations isn't always 1:1 with the activity `id` — e.g., "Prescribing Data Pharmacist" activity maps to `pharmacy-manager-2017` consultation, "Community Pharmacist" maps to `duty-pharmacist-2016`
- Skills→medications mapping is by exact name match (both files use same names: "Data Analysis", "Python", "SQL", "Power BI", "JavaScript / TypeScript")
- `e.stopPropagation()` on the "View Results" link in Projects prevents the click from bubbling up and toggling the accordion
- The expanded content structure varies per tile (bullets + codes for career, methodology + tags + results for projects, timeline for skills) but the AnimatePresence/motion.div wrapper is identical
- All three tiles now have `cursor: 'pointer'` on expandable items and `border-color` transitions on hover/expand
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available.

### Iteration 16 — Task 18: Build Command Palette
**Status:** Complete
**Changes:**
- Created `src/components/CommandPalette.tsx` — full command palette overlay with:
  - Fixed overlay: `rgba(26,43,42,0.45)` background, `backdrop-filter: blur(4px)`, z-index 1000
  - Modal: 580px width, max-height 520px, 12px border-radius, two-layer shadow matching concept CSS
  - Search input row: Search icon (accent), auto-focus input (15px, font-ui), ESC kbd badge (mono)
  - Results area: scrollable, grouped by section with styled labels (10px, 600 weight, uppercase, 0.08em tracking)
  - Result items: 28px icon container (6px radius, colored bg per section), title (500 weight) + subtitle (11px, tertiary, ellipsis), hover/selected highlight (accent-light bg + accent-border outline)
  - Icon colors: teal (Experience, Quick Actions), green (Core Skills), amber (Active Projects, Achievements), purple (Education)
  - Footer: keyboard hints with styled kbd elements
  - CSS entrance animations: `palette-overlay-in` + `palette-modal-in`, 200ms with reduced-motion support
- Rebuilt `src/lib/search.ts` with new palette data model:
  - `PaletteItem` interface with action union: scroll, expand, link, download
  - `buildPaletteData()`: 24 entries across 6 sections matching concept HTML exactly
  - `buildSearchIndex()`: fuse.js with weighted keys, threshold 0.3
  - `groupBySection()`: maintains defined section order
  - Legacy exports maintained for backward compat (ClinicalSidebar until Task 21)
- Updated `src/components/DashboardLayout.tsx`: Ctrl+K listener, search bar click, action handler, CommandPalette rendered at layout level
- Updated `src/components/Card.tsx`: added `tileId` prop → `data-tile-id` attribute
- Updated all 7 tile components to pass `tileId` for scroll targeting
- Added CSS keyframe animations in `src/index.css`
**Learnings:**
- Concept HTML palette has 24 curated entries — matched exactly rather than dynamically building from data files
- `LucideIcon` type needed for icon map (not `React.ComponentType<{ size: number }>`)
- `data-tile-id` on Card enables palette → tile scroll targeting
- Custom event (`palette-expand`) dispatched for expand-on-select (not yet consumed by tiles)
- Backward-compatible legacy exports prevent breaking old components
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — no browser tools available.

### Iteration 17 — Task 19: Responsive design
**Status:** Complete
**Changes:**
- Updated `src/components/DashboardLayout.tsx`:
  - Sidebar: hidden on <lg (1024px) via `hidden lg:block` class
  - Main content padding: responsive Tailwind classes `p-4 pb-8 md:p-6 md:pb-10 lg:px-7 lg:pt-6 lg:pb-10`
- Updated `src/index.css`:
  - Dashboard grid: mobile-first (1 col default → 2 col at md/768px)
  - Activity grid: mobile-first (1 col default → 2 col at md/768px)
  - Gap adjustments: 12px mobile, 16px tablet/desktop
- Updated `src/components/TopBar.tsx`:
  - Brand text: "Headhunt Medical Center" → "HMC" on <sm (640px)
  - "Remote" badge: hidden on <md (768px)
  - Session badge: "Active Session · [time]" → "[time]" only on <xs (480px)
- Updated `src/components/CommandPalette.tsx`:
  - Modal width: full-width on mobile with 8px edge margin, 580px on md+
  - Search input padding: reduced on mobile (12px → 8px)
  - Results padding: reduced on mobile (8px → 8px)
  - Footer: hidden on mobile (only visible on md+)
  - Overlay padding: responsive (8px on mobile, 10vh top on larger screens)
**Learnings:**
- Mobile-first approach: default styles for mobile, progressively enhance with md/lg breakpoints
- Tailwind breakpoints: xs (480px), sm (640px), md (768px), lg (1024px), xl (1280px)
- CommandPalette uses class-based width for responsive (w-full → md:w-[580px]) rather than inline style
- All touch targets already meet 48px+ minimum (TopBar 48px height, search bar 42px, buttons have sufficient padding)
- The sidebar being hidden on mobile is acceptable — all content is in the main scrollable area, and the command palette provides quick navigation
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Skipped — visual verification should be done by user at multiple breakpoints (1280px, 800px, 375px)

### Iteration 18 — Task 20: Accessibility audit
**Status:** Complete
**Changes:**
- Updated `src/components/Card.tsx`:
  - Changed wrapper from `<div>` to `<article>` for semantic HTML (tiles are self-contained content sections)
  - Added `aria-hidden="true"` to CardHeader colored dot (decorative, text label conveys information)
- Updated `src/components/TopBar.tsx`:
  - Added skip link (href="#main-content") positioned off-screen, visible only on focus
  - Skip link uses accent background, slides down on focus, slides up on blur
  - Added `aria-label="Active session information"` to session info container
- Updated `src/components/DashboardLayout.tsx`:
  - Added `id="main-content"` to main element (skip link target)
- Updated `src/components/Sidebar.tsx`:
  - Added `aria-hidden="true"` to status badge pulse dot (decorative, "Open to Opportunities" text label conveys status)
- Updated `src/components/tiles/CareerActivityTile.tsx`:
  - Added `aria-hidden="true"` to colored dots (8px activity type indicators — decorative, activity title conveys information)
- Updated `src/components/tiles/ProjectsTile.tsx`:
  - Added `aria-hidden="true"` to status dots (7px Complete/Ongoing/Live indicators — decorative, project name + year conveys information)
- Updated `src/index.css`:
  - Added global `*:focus-visible` styles (2px accent outline, 2px offset)
  - Specific focus-visible styles for buttons, role="button", role="option", links (2px accent outline rgba(13,110,110,0.4))
  - Input/textarea focus-visible with slightly stronger accent (rgba 0.6, 0px offset)
  - Added `prefers-reduced-motion` override for pulse animation (disables pulse on status badge dot — keeps opacity 1)
**Learnings:**
- **Semantic HTML audit results:**
  - ✅ TopBar uses `<header>` element (Task 4)
  - ✅ Sidebar uses `<aside>` element (Task 5)
  - ✅ DashboardLayout main uses `<main>` element with aria-label (Task 7)
  - ✅ All tiles now use `<article>` element (this iteration)
  - ✅ Command palette uses role="dialog" with aria-modal (Task 18)
- **Keyboard navigation audit results:**
  - ✅ Tab navigates between interactive elements (native browser behavior)
  - ✅ Enter/Space expand tile items, flip KPI cards, select palette results (Task 16-18)
  - ✅ Escape closes expanded items and command palette (Task 16-18)
  - ✅ Ctrl+K opens command palette (Task 18)
  - ✅ Arrow Up/Down navigate palette results (Task 18)
- **ARIA attributes audit results:**
  - ✅ Command palette search: role="combobox", aria-expanded, aria-controls, aria-autocomplete, aria-activedescendant (Task 18)
  - ✅ Palette results: role="listbox", each result role="option", aria-selected (Task 18)
  - ✅ Palette overlay: role="dialog", aria-modal="true", aria-label="Command palette" (Task 18)
  - ✅ Expandable items: aria-expanded on trigger elements (Task 16)
  - ✅ KPI flip cards: aria-label describing front/back content, role="button", tabIndex={0} (Task 17)
  - ✅ Decorative dots: aria-hidden="true" on all colored status/type indicators (this iteration)
  - ✅ Session info: aria-label="Active session information" (this iteration)
- **Focus management audit results:**
  - ✅ Command palette: focus trap implemented, focus moves to search input on open, returns to trigger on close (Task 18)
  - ✅ Focus-visible rings: 2px accent outline on all interactive elements (this iteration)
  - ✅ Skip to content link: only visible on focus, navigates to #main-content (this iteration)
  - ✅ Tile expansion: focus remains on trigger element (native browser behavior with role="button")
- **prefers-reduced-motion audit results:**
  - ✅ All components check at module scope: `const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches`
  - ✅ Dashboard entrance (topbar/sidebar/content): duration: 0 when reduced motion (Task 7)
  - ✅ Tile expansion: duration: 0 when reduced motion (Task 16)
  - ✅ KPI flip: visibility toggle instead of 3D rotation when reduced motion (Task 17)
  - ✅ Palette entrance: animations disabled when reduced motion (Task 18)
  - ✅ Status badge pulse: pulse animation disabled when reduced motion (this iteration)
- **Color contrast verification:**
  - ✅ Accent #0D6E6E on white #FFFFFF: ~5.5:1 (meets AA)
  - ✅ Primary #1A2B2A on white: ~15:1 (meets AAA)
  - ✅ Secondary #5B7A78 on white: ~4.6:1 (meets AA for normal text)
  - ✅ Tertiary #8DA8A5 on white: ~3.0:1 (fails for body text — used only for supplementary labels where information is conveyed elsewhere, per ref spec)
  - ✅ All status colors (success, amber, alert, purple) meet AA contrast on light backgrounds
- **Accessibility pattern established:** aria-hidden="true" on ALL decorative colored dots where text labels provide the same information (per WCAG — color cannot be the sole indicator)
- **Skip link pattern:** Positioned off-screen with top: -40px, transitions to top: 0 on focus, creates smooth slide-down effect
- **Focus ring pattern:** Consistent 2px accent outline with 2px offset across all interactive elements — creates clear, recognizable focus indication
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
**Visual review:** Not applicable — accessibility improvements are non-visual (semantic HTML, ARIA, keyboard nav) except for focus rings which should be tested by user

### Iteration 19 — US-018: ConsultationDetail renderer (already complete)
**Status:** Already implemented by prior iteration — marked as passed
**Changes:** None needed — `src/components/detail/ConsultationDetail.tsx` already existed with full implementation (role header, history, achievements, outcomes, coded entries), wired into DetailPanel for both `consultation` and `career-role` types.

### Iteration 19b — US-020: Create SkillDetail renderer for detail panel
**Status:** Complete
**Changes:**
- Created `src/components/detail/SkillDetail.tsx` — narrow panel renderer for individual skills:
  - Skill header: 20px name, frequency badge (accent-light), status badge (success/neutral)
  - Category label: 11px uppercase tertiary text (Technical / Healthcare Domain / Strategic & Leadership)
  - Proficiency bar: 6px height, color-coded (green >=90%, teal >=75%, amber <75%), percentage label
  - Experience section: large year number (28px) + "years" + "Since YYYY" (Geist Mono)
  - "Used in" section: lists roles from constellation data (roleSkillMappings), with org-colored dots, role labels, organization + date range
- Updated `src/components/DetailPanel.tsx`:
  - Added import for SkillDetail
  - Added `content.type === 'skill'` rendering branch
  - Narrowed placeholder fallback to exclude 'skill' type
**Learnings:**
- Constellation data provides the skill-to-role mapping via `roleSkillMappings` — filter by skill ID, then look up role nodes for display
- Role nodes sorted chronologically (earliest first) gives a natural career progression view
- The non-null assertions on `node!` are safe because the `.filter(Boolean)` ensures no nulls
- Pre-existing lint error (`_sectionId` in DashboardLayout:64) is unrelated to this work
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing error + 2 warnings), build ✓
**Visual review:** Skipped — no browser tools available.

### Iteration 20 — US-021: Create SkillsAllDetail renderer for detail panel
**Status:** Complete
**Changes:**
- Created `src/components/detail/SkillsAllDetail.tsx` — narrow panel renderer for full categorised skill list:
  - Groups all 21 skills by Technical / Healthcare Domain / Strategic & Leadership
  - Category headers match CoreSkillsTile style: 10px uppercase label + divider line + item count (Geist Mono)
  - Each skill row: icon container (26px, accent-light), name + frequency/years (Geist Mono), mini proficiency bar (40px wide, color-coded), percentage, chevron
  - Skill rows clickable → `openPanel({ type: 'skill', skill })` to switch panel to individual SkillDetail
  - If opened with category filter (from "View all" button), scrolls to and highlights that category (accent-colored header + bottom border)
  - Hover: border color shift + shadow deepens (matching CoreSkillsTile rows)
  - Keyboard: Enter/Space triggers skill detail, role="button", tabIndex={0}, descriptive aria-label
- Updated `src/components/DetailPanel.tsx`:
  - Added import for SkillsAllDetail
  - Added `content.type === 'skills-all'` rendering branch with category prop pass-through
  - Narrowed placeholder fallback to exclude 'skills-all' type
**Learnings:**
- Reused the SkillRow pattern from CoreSkillsTile but added a mini proficiency bar instead of status badge — provides more info density in the "view all" context
- The `useRef<Record<string, HTMLDivElement | null>>` pattern with callback ref works well for multiple dynamic refs
- Category highlight uses both accent-colored text and a 2px bottom border to visually distinguish the filtered category
- Pre-existing lint error (`_sectionId` in DashboardLayout:64) continues to be unrelated
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing error + 2 warnings), build ✓
**Visual review:** Skipped — no browser tools available.

### Iteration 21 — US-022: Create EducationDetail renderer for detail panel
**Status:** Complete
**Changes:**
- Created `src/components/detail/EducationDetail.tsx` — narrow panel renderer for education entries:
  - Header: type-specific icon (GraduationCap/Award/BookOpen/FlaskConical) + title + institution (purple accent) + duration + classification badge (purple-light bg)
  - Research Project section: renders `extra.researchDescription` for MPharm entry
  - OSCE Performance section: renders score in success-colored badge with description
  - Extracurricular Activities section: bullet list from `extra.extracurriculars`
  - Programme Overview section: renders `extra.programmeDetail` for Mary Seacole
  - Notes section: italic secondary text from `document.notes`
  - All sections use shared `sectionHeaderStyle` (12px uppercase, secondary color, 0.05em tracking)
- Updated `src/components/DetailPanel.tsx`:
  - Added import for EducationDetail
  - Added `content.type === 'education'` rendering branch
  - Narrowed placeholder fallback to exclude 'education' type
**Learnings:**
- Icon type for lucide-react must use `LucideIcon` type, not `React.ComponentType<{ size: number }>` — the latter causes type incompatibility with ForwardRefExoticComponent
- The `educationExtras` data matches documents by `documentId` field — currently only MPharm and Mary Seacole have extras
- Purple color (#7C3AED) is used consistently for education across the app (dot colors in CardHeader, CareerActivity, and now EducationDetail institution text and classification badge)
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing error + 2 warnings), build ✓
**Visual review:** Skipped — no browser tools available.

### Iteration 22 — US-023: Install D3 and scaffold CareerConstellation component
**Status:** Complete
**Changes:**
- Installed `d3` and `@types/d3` npm packages (70 packages added)
- Created `src/components/CareerConstellation.tsx` — scaffolded component with:
  - Props: `onRoleClick(id)` and `onSkillClick(id)` stored in callbacksRef for future D3 event binding
  - Responsive SVG container using ResizeObserver: 400px desktop, 300px tablet (<1024px), 250px mobile (<768px)
  - viewBox matches actual dimensions for responsive scaling
  - Radial gradient background: `#F0F5F4` (--bg-dashboard) center → `#FFFFFF` (--surface) edge, rx=6
  - Placeholder text showing node/link counts from constellation data (Geist Mono, tertiary color)
  - Container with border-radius and overflow hidden
  - SVG has `role="img"` and `aria-label` for accessibility
  - Imperative SVG drawing via useEffect on svgRef (matches ECG pattern for D3 compatibility)
**Learnings:**
- `callbacksRef` pattern stores click handlers in a ref for D3 imperative code — avoids stale closures when D3 attaches event listeners in US-024/026
- ResizeObserver provides cleaner responsive behavior than CSS media queries for SVG — container width determines height tier
- The SVG namespace `http://www.w3.org/2000/svg` is required for createElement in imperative SVG building
- D3 is installed but not yet imported — US-024 will use `d3.forceSimulation` etc. on the svgRef
- Pre-existing lint error (`_sectionId` in DashboardLayout:64) continues to be unrelated
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing error + 2 warnings), build ✓
**Visual review:** Skipped — component not yet integrated into CareerActivityTile (will be wired in US-026).

### Iteration 23 — US-024: Build D3 force-directed graph rendering in CareerConstellation
**Status:** Complete
**Changes:**
- Rewrote `src/components/CareerConstellation.tsx` to use D3 force simulation:
  - Replaced imperative SVG createElement with D3 selections (`d3.select`, `.selectAll`, `.join`)
  - D3 force simulation with: `forceManyBody(-200)`, `forceLink(distance 80, strength from data * 0.5)`, `forceX` chronological (roles positioned left-to-right by `startYear` via `d3.scaleLinear`), `forceY` centered at `height/2`, `forceCollide` (30 for roles, 14 for skills)
  - Role nodes: 24px radius circles filled with `orgColor`, 2px white stroke, 8px white `shortLabel` text centered
  - Skill nodes: 10px radius circles, color-coded by domain (clinical=#059669 green, technical=#0D6E6E teal, leadership=#D97706 amber), 1.5px white stroke, opacity 0.85
  - Skill labels: 9px Geist Mono text below each skill node (using `shortLabel`)
  - Links: 1px `#D4E0DE` lines at opacity 0.3
  - Node positions constrained within SVG bounds on each tick
  - Layered rendering: links group below nodes group
  - `simulationRef` stores active simulation, stopped on cleanup or dimension change
  - Preserved existing ResizeObserver responsive height (400/300/250px)
  - Preserved radial gradient background, `role="img"`, `aria-label`
- Removed unused `ConstellationLink` type import (caught by typecheck)
**Learnings:**
- D3 `forceLink.strength()` receives the link object — cast to `SimLink` to access `.strength` field
- Role `forceX` uses strong pull (0.8) to maintain chronological layout; skill `forceX` uses weak pull (0.05) to let links drive position
- `forceCollide` radius should be slightly larger for skills than their visual radius to prevent label overlap
- The `SimNode` interface extending `ConstellationNode` with `x/y/vx/vy/fx/fy` satisfies D3's `SimulationNodeDatum` needs
- Pre-existing lint issues: `_sectionId` error + 2 context warnings — all unrelated
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing error + 2 warnings), build ✓
**Visual review:** Skipped — component not yet wired into CareerActivityTile (US-026). D3 simulation verified via successful build.

### Iteration 24 — US-025: Add accessibility to CareerConstellation
**Status:** Complete
**Changes:**
- Updated `src/components/CareerConstellation.tsx` with four accessibility features:
  - **Screen-reader description**: `buildScreenReaderDescription()` generates a hidden `<p>` (sr-only via clip rect) describing all 5 roles, their organizations, year ranges, and associated skills from `roleSkillMappings`
  - **Keyboard navigation**: Hidden `<button>` elements overlaid on the SVG container, one per role node. Tab navigates through roles, Enter/Space triggers `onRoleClick`. Each button has descriptive `aria-label` (role name, org, year range)
  - **Focus indicators**: SVG `.focus-ring` circle (ROLE_RADIUS + 4px) rendered behind each role node. Transparent by default, becomes teal `#0D6E6E` stroke when the corresponding hidden button receives focus (tracked via `focusedNodeId` state + `useEffect` on D3 selection)
  - **prefers-reduced-motion**: When enabled, simulation runs 300 ticks synchronously (`simulation.stop()` + loop), then renders final positions immediately — no animation frames. Uses the established module-scope `matchMedia` check pattern
- Imported `roleSkillMappings` from constellation data for SR description
- Added `useCallback` for `handleNodeKeyDown` to prevent re-renders
**Learnings:**
- D3 focus indicators work via a dual approach: hidden HTML buttons for actual keyboard focus, plus D3-drawn SVG circles that respond to React state changes — avoids fighting D3's imperative model with React's declarative focus management
- Running `simulation.tick()` in a loop (300 iterations) is sufficient to reach stable positions for this graph size (5 roles + 21 skills)
- The `.focus-ring` circle must be appended before the main circle in the SVG group to render behind it (SVG painting order = DOM order)
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing error + 2 warnings), build ✓
**Visual review:** Skipped — not yet wired into CareerActivityTile (US-026).

### Iteration 25 — US-026: Add hover and click interactions to CareerConstellation
**Status:** Complete
**Changes:**
- Updated `src/components/CareerConstellation.tsx` with three interaction features:
  - **Hover highlighting**: Built adjacency map from `constellationLinks`. On `mouseenter`, non-connected nodes fade to 0.15 opacity. Connected links brighten to teal (`#0D6E6E`), thicken to 2px, increase opacity to 0.7. Non-connected links dim to 0.1 opacity. Role hover also scales connected skill nodes up (+3px radius) via D3 transition (150ms).
  - **Hover reset**: On `mouseleave`, all nodes reset to full opacity, skill circles return to `SKILL_RADIUS`, links return to default stroke/opacity/width.
  - **Click handlers**: Click on any node calls `callbacksRef.current.onRoleClick(id)` or `onSkillClick(id)` via the existing callbacksRef pattern (avoids stale closures).
  - Added `.node-circle` and `.node-label` classes to circles/text for targeted D3 selections during hover
- Updated `src/components/tiles/CareerActivityTile.tsx`:
  - Replaced placeholder `<div>` with actual `<CareerConstellation>` component
  - Added `handleRoleClick(roleId)` → finds consultation by ID → `openPanel({ type: 'career-role', consultation })`
  - Added `handleSkillClick(skillId)` → finds skill by ID → `openPanel({ type: 'skill', skill })`
  - Refactored `handleItemClick` to delegate to `handleRoleClick` for consistency
  - Imported `skills` from `@/data/skills` and `CareerConstellation` from `../CareerConstellation`
**Learnings:**
- D3 hover uses `mouseenter`/`mouseleave` (not `mouseover`/`mouseout`) to avoid bubbling issues with nested SVG groups
- The adjacency map uses source/target strings from `constellationLinks` (pre-simulation), not SimNode objects — link data gets resolved by D3 after forceLink runs, so during hover the source/target may be either string or SimNode objects. The click/hover handlers check both forms.
- The `callbacksRef` pattern established in US-023 works perfectly for D3 click events — no stale closures
- Pre-existing lint issues: `_sectionId` error + 2 context warnings — all unrelated
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing error + 2 warnings), build ✓
**Visual review:** Skipped — no browser tools available.

### Iteration 26 — US-027: Restyle LoginScreen with teal accents
**Status:** Complete
**Changes:**
- Updated `src/components/LoginScreen.tsx`:
  - Replaced all `#005EB8` (NHS Blue) with `#0D6E6E` (teal accent): shield icon color, active field borders, cursor color, button default bg, focus ring
  - Replaced `#004D9F` (hover) with `#0A8080` (teal hover)
  - Replaced `#004494` (pressed) with `#085858` (teal pressed)
  - Background color: `#1E293B` → `#1A2B2A` (warmer, cohesive with dashboard palette)
  - Shield icon container: `rgba(0, 94, 184, 0.07)` → `rgba(13, 110, 110, 0.08)` (teal-tinted)
**Learnings:**
- LoginScreen had 6 instances of `#005EB8` — all replaced for consistency
- The background change from `#1E293B` (slate) to `#1A2B2A` (dark teal-green) creates visual cohesion with the teal accent palette
- Button states follow the teal gradient: default #0D6E6E → hover #0A8080 → pressed #085858 (progressively darker)
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing error + 2 warnings), build ✓
**Visual review:** Skipped — no browser tools available.

---

## 2026-02-14 - US-028
- **What was implemented:** Changed login username from A.CHARLWOOD to a.recruiter, added connection status indicator with red→green transition, updated button disabled logic to require both typing complete AND connection established.
- **Files changed:**
  - `src/components/LoginScreen.tsx` — new `connectionState` state, connection timer (2000ms), connection status indicator UI (6px dot + Geist Mono text), `canLogin` derived state replacing `typingComplete` for button control
  - `src/components/DashboardLayout.tsx` — fixed pre-existing lint error (unused `_sectionId` parameter, added eslint-disable comment)
- **Learnings for future iterations:**
  - The DashboardLayout had a pre-existing lint error with `_sectionId` — ESLint config doesn't respect underscore-prefix unused var convention, needed `eslint-disable-next-line` comment. TypeScript `tsc -b` (used in build) DOES respect underscore prefix though.
  - Connection status uses CSS `transition: 300ms` for the color change — matches the spec for smooth dot/text color transition
  - `canLogin` is a derived value (not state) combining `typingComplete && connectionState === 'connected'` — cleaner than adding another state variable

---

## 2026-02-14 - US-029
- **What was implemented:** Added post-login loading state with CSS spinner (~600ms) that replaces the login card content after clicking Log In. Updated TopBar session display name from "Dr. A.CHARLWOOD" to "A.RECRUITER".
- **Files changed:**
  - `src/components/LoginScreen.tsx` — new `isLoading` state, handleLogin now sets isLoading before isExiting, card content conditionally renders either login form or spinner + "Loading clinical records..." text. Spinner uses CSS `login-spin` animation.
  - `src/components/TopBar.tsx` — changed session name from "Dr. A.CHARLWOOD" to "A.RECRUITER"
  - `src/index.css` — added `@keyframes login-spin` and `.login-spinner` class, plus `prefers-reduced-motion` override (static indicator, no spin)
- **Learnings for future iterations:**
  - The loading state replaces card content via conditional rendering (`isLoading ? spinner : form`) rather than an overlay — keeps the card dimensions stable
  - The sequence is: buttonPressed (100ms) → isLoading (600ms) → isExiting (200ms) → onComplete. With reduced motion, loading and exit delays are 0ms.
  - Spinner uses pure CSS animation (`border-top-color` trick) — no library needed

---

## 2026-02-14 - US-030
- **What was implemented:** Updated CommandPalette search index to include all 21 skills (not just 5), added `panel` action type to PaletteAction union, and wired skill/KPI/project palette results to open detail panels directly.
- **Files changed:**
  - `src/lib/search.ts` — Added `panel` action type with `DetailPanelContent` payload. Skills section now iterates all 21 skills from `skills.ts` (was hardcoded to 5). Project results find matching `Investigation` by ID and use `panel` action. Achievement results find matching `KPI` by ID and use `panel` action. Imported `kpis` and `DetailPanelContent` type.
  - `src/components/DashboardLayout.tsx` — Added `panel` case to `handlePaletteAction` switch that calls `openPanel(action.panelContent)`. Imported `useDetailPanel` from context.
- **Learnings for future iterations:**
  - The `panel` action type carries a full `DetailPanelContent` discriminated union payload — this means any palette item can open any detail panel type without intermediate mapping
  - Achievement "Team of 12 Led" was updated to "1.2M Population Served" to match the KPI data change from US-006
  - For projects, a fallback to `scroll` action is used when the investigation ID doesn't match — defensive pattern for data mismatches

---

## 2026-02-14 - US-031
- **What was implemented:** Responsive testing and fixes for all new components. Audited DetailPanel, SubNav, CareerConstellation, dashboard grid, CoreSkillsTile, touch targets, and 375px overflow.
- **Files changed:**
  - `src/components/SubNav.tsx` — Added `overflowX: auto`, `scrollbarWidth: 'none'`, horizontal padding, `flexShrink: 0` on tab buttons, `minHeight: 36px` for touch targets, flex layout for vertical centering
  - `src/index.css` — Added `.subnav-scroll::-webkit-scrollbar { display: none }` for WebKit scrollbar hiding
  - `src/components/DetailPanel.tsx` — Enlarged close button from 32x32px to 44x44px for mobile touch target compliance
  - `src/components/tiles/CoreSkillsTile.tsx` — Added `minHeight: 44px` to SkillRow and "View all" button for touch target compliance
  - `src/components/tiles/ProjectsTile.tsx` — Added `minHeight: 44px` to ProjectItem for touch target compliance
  - `src/components/tiles/LastConsultationTile.tsx` — Added `minHeight: 44px` to "View full record" button
- **Audit results (already passing):**
  - DetailPanel: `@media (max-width: 767px)` already set both widths to 100vw ✓
  - CareerConstellation: `getHeight()` already returns 400/300/250px by breakpoint ✓
  - Dashboard grid: mobile-first 1fr → 2fr at 768px, KPIs + Projects stack correctly ✓
  - CoreSkillsTile: `full` prop spans both columns at all breakpoints ✓
  - No horizontal overflow at 375px: TopBar search hidden <768px, no problematic nowrap on wide content ✓
- **Learnings for future iterations:**
  - `scrollbarWidth: 'none'` (Firefox) + `::-webkit-scrollbar { display: none }` (Chrome/Safari) together hide scrollbars cross-browser
  - WCAG touch target minimum is 44x44px — check all `role="button"`, `<button>`, and clickable elements
  - SubNav at 375px has ~345px available (375 - 2*16px padding) — 5 short labels with 24px gaps fit without scroll, but the scroll fallback is good insurance

## 2026-02-14 — US-032
- **What was implemented:** Reduced motion audit, final cleanup, and visual review
- **Files changed:**
  - `src/index.css` — Added prefers-reduced-motion overrides for SubNav button transitions and smooth scroll behavior. Removed 18 unused `--pmr-*` legacy CSS variables and `.pmr-theme` utility class.
  - `src/components/LoginScreen.tsx` — Connection status dot and text transitions now respect `prefersReducedMotion` (instant when enabled).
  - `src/components/detail/ProjectDetail.tsx` — Created missing ProjectDetail renderer (project name, year, status badge, methodology, tech stack tags, results bullets, external link button).
  - `src/components/DetailPanel.tsx` — Wired ProjectDetail for `content.type === 'project'`. Removed placeholder fallback (all content types now have renderers).
  - Deleted `src/hooks/useBreakpoint.ts` (unused)
  - Deleted `src/data/profile.ts` (unused — PatientSummaryTile has profile text hardcoded)
- **Learnings for future iterations:**
  - ProjectDetail was missing despite US-019 being marked as passed — always verify file existence, not just PRD status
  - `profile.ts` was created but never imported — PatientSummaryTile hardcodes the profile text instead
  - `useBreakpoint.ts` was orphaned after its consumers were deleted in US-001
  - Legacy `--pmr-*` CSS variables were all superseded by the new design token system and safe to remove
  - `pmr-scrollbar` class is still actively used (Sidebar, DashboardLayout, CommandPalette) — do not remove
  - SubNav inline transitions need CSS `!important` override in prefers-reduced-motion since they're set via inline styles
  - The `html { scroll-behavior: smooth }` also needs a reduced-motion override to `auto`
---

## 2026-02-14 — US-001
- **What was implemented:** Changed initial Phase state in App.tsx from `'boot'` to `'pmr'` to skip boot/ECG/login animation during dev iteration.
- **Files changed:** `src/App.tsx` — single line change (line 47)
- **Learnings for future iterations:**
  - This is a temporary change — US-013 will revert it back to `'boot'` as the final story
  - All boot/ECG/login components remain in code, only the initial state changes
---

## 2026-02-14 — US-002
- **What was implemented:** Added fluid root font-size scaling via `clamp()` on the `html` element, updated body font-size to `1rem`, and converted all layout CSS custom properties from px to rem.
- **Files changed:**
  - `src/index.css` — Added `html { font-size: clamp(15px, calc(8px + 0.3646vw), 22px); }`. Changed `body { font-size: 15px }` → `font-size: 1rem`. Converted `--topbar-height` (48px → 3.2rem), `--sidebar-width` (272px → 18.133rem), `--subnav-height` (36px → 2.4rem), `--panel-narrow` (400px → 26.667rem).
- **Learnings for future iterations:**
  - Clamp formula derivation: slope = (22-15)/(3840-1920) = 7/1920 ≈ 0.3646vw. Intercept: 15 - 0.3646*19.2 = 8px. Result: `clamp(15px, calc(8px + 0.3646vw), 22px)`. Hits exactly 15px at 1920vw and 22px at 3840vw.
  - Border-radius (`--radius-card`, `--radius-sm`) and filter values (`--backdrop-blur`) stay in px — they don't need to scale with viewport.
  - `--panel-wide: 60vw` already uses viewport units — no conversion needed.
  - rem base is 15px at 1920px. Conversion: px / 15 = rem. All Tailwind utility classes already use rem internally, so they auto-scale with the root font-size.
---

## 2026-02-14 — US-003
- **What was implemented:** Converted all inline px values in Card.tsx, SubNav.tsx, and DetailPanel.tsx to rem. DashboardLayout.tsx had no inline px values to convert (uses CSS variables and Tailwind classes which already use rem).
- **Files changed:**
  - `src/components/Card.tsx` — padding 20px→1.333rem, CardHeader gap 8px→0.533rem, marginBottom 16px→1.067rem, dot width/height 8px→0.533rem, title fontSize 12px→0.8rem, rightText fontSize 10px→0.667rem
  - `src/components/SubNav.tsx` — nav gap 24px→1.6rem, padding 0 16px→0 1.067rem, button fontSize 13px→0.867rem, button padding 0 4px 2px→0 0.267rem 0.133rem, minHeight 36px→2.4rem
  - `src/components/DetailPanel.tsx` — header padding 20px 24px→1.333rem 1.6rem, title container gap 8px→0.533rem, dot width/height 8px→0.533rem, title fontSize 14px→0.933rem, close button width/height 44px→2.933rem, body padding 24px→1.6rem
- **Learnings for future iterations:**
  - DashboardLayout.tsx already uses CSS variables (--topbar-height, --subnav-height, --sidebar-width) which were converted to rem in US-002, and Tailwind responsive classes which inherently use rem — no inline px values to convert
  - Framer Motion variant values like `y: -48` and `x: -272` are animation transform offsets (not layout units) — these are number values that Framer interprets as px. They only affect animation start position and don't need rem conversion
  - Border-radius values (e.g., `var(--radius)`, `var(--radius-sm)`, `'50%'`) stay as-is — border-radius doesn't benefit from scaling
  - The `height: '2px'` active indicator bar in SubNav stays in px — decorative border elements don't need scaling
  - At 1920px viewport, 1rem = 15px, so all rem values compute to identical px values as before — verified via Playwright screenshot
---

## 2026-02-14 — US-004
- **What was implemented:** Converted all inline px values in TopBar.tsx and Sidebar.tsx (including sub-components SectionTitle, TagPill, AlertFlag) to rem.
- **Files changed:**
  - `src/components/TopBar.tsx` — 21 px→rem conversions: header padding, skip-link positioning/padding/fontSize, brand fontSize (x2), "Remote" fontSize/marginLeft, search bar maxWidth/minWidth/height/padding, search placeholder fontSize, kbd fontSize/padding, username fontSize, session badge fontSize/padding (x2 variants)
  - `src/components/Sidebar.tsx` — ~40 px→rem conversions across: aside padding/gap, PersonHeader (avatar size/fontSize/marginBottom, name fontSize, title fontSize/marginTop, status badge gap/marginTop/fontSize/padding, pulse dot size, details grid gap/marginTop, detail rows fontSize/padding x6, GPhC fontSize), SectionTitle (gap/fontSize/marginBottom), TagPill (fontSize/padding), AlertFlag (gap/fontSize/padding, icon container size), Tags/Alerts sections (padding/gap)
- **Learnings for future iterations:**
  - Sidebar has the highest density of inline px values in the project (~40 conversions) — all sub-components (SectionTitle, TagPill, AlertFlag) are defined in the same file
  - Box-shadow values like `'0 2px 8px rgba(...)'` stay as-is — shadow px values don't benefit from scaling
  - Lucide icon `size` and `strokeWidth` props are numbers, not CSS units — they stay unchanged
  - `letterSpacing` values in `em` (e.g., '0.08em', '0.12em') stay as-is — em is already relative to font-size
  - The skip-link onFocus/onBlur handlers also had a `-40px` value that needed converting to `-2.667rem`
---

## 2026-02-14 — US-005
- **What was implemented:** Converted all inline px values in PatientSummaryTile.tsx, LastConsultationTile.tsx, CareerActivityTile.tsx, and ConsultationDetail.tsx to rem.
- **Files changed:**
  - `src/components/tiles/PatientSummaryTile.tsx` — 8 conversions: grid minmax 140px→9.333rem, gaps 12px→0.8rem, marginBottom/paddingBottom 20px→1.333rem, detail gaps 2px→0.133rem, fontSizes 18px→1.2rem, 11px→0.733rem, 13px→0.867rem
  - `src/components/tiles/LastConsultationTile.tsx` — ~21 conversions: metadata grid gaps/padding, fontSizes (10px→0.667rem, 11.5px→0.767rem, 13.5px→0.9rem, 12.5px→0.833rem, 12px→0.8rem), bullet dots 5px→0.333rem, button minHeight 44px→2.933rem, section gaps/margins
  - `src/components/tiles/CareerActivityTile.tsx` — ~12 conversions: entry fontSizes 12px→0.8rem, gaps/padding, dot 8px→0.533rem, meta fontSize 11px→0.733rem, date fontSize 10px→0.667rem, constellation marginBottom 20px→1.333rem
  - `src/components/detail/ConsultationDetail.tsx` — ~13 conversion groups: outer gap 24px→1.6rem, role title fontSize 20px→1.333rem, org fontSize 14px→0.933rem, section headers 12px→0.8rem/marginBottom 8px→0.533rem, list paddingLeft 20px→1.333rem, coded entries padding/gap/fontSize conversions
- **Learnings for future iterations:**
  - ConsultationDetail.tsx has the most repetitive patterns — same styled sections (role summary, key achievements, clinical notes, coded entries) with identical fontSize/margin patterns repeated 4+ times
  - Grid `minmax()` values like `minmax(140px, 1fr)` need rem conversion too — they control responsive column widths
  - `lineHeight` numeric values (e.g., 1.3, 1.5) are unitless multipliers and don't need conversion
  - Bullet dot sizes (5px→0.333rem) are small enough that they could stay in px, but converting ensures consistency with the scaling approach
---

## 2026-02-14 — US-006
- **What was implemented:** Converted all inline px values to rem in the remaining 4 tile components and 5 detail panel components: CoreSkillsTile, LatestResultsTile, EducationTile, ProjectsTile, SkillDetail, SkillsAllDetail, KPIDetail, EducationDetail, ProjectDetail.
- **Files changed:**
  - `src/components/tiles/CoreSkillsTile.tsx` — ~20 conversions: SkillRow gap/padding/minHeight, icon container 26px→1.733rem, skill name/frequency fontSizes, status badge, CategorySection marginTop, category header gap/marginBottom/fontSize, skill rows gap, view all button gap/marginTop/padding/minHeight/fontSize
  - `src/components/tiles/LatestResultsTile.tsx` — 7 conversions: MetricCard padding, value fontSize 28px→1.867rem, label fontSize/marginTop, sub fontSize/marginTop, grid gap
  - `src/components/tiles/EducationTile.tsx` — 11 conversions: entry list gap, button padding/fontSize, title row gap/marginBottom, title/year/institution/detail fontSizes, detail gap
  - `src/components/tiles/ProjectsTile.tsx` — 12 conversions: ProjectItem padding/minHeight/fontSize, row gap/marginBottom, dot size/marginTop, year fontSize, tech stack gap, tech tag fontSize/padding, list gap
  - `src/components/detail/SkillDetail.tsx` — ~27 conversions: outer gap, skill name fontSize/marginBottom, badge padding/fontSize, category fontSize, section h3s, proficiency bar gap/text fontSize/minWidth, experience number/years/since fontSize, role list/item gap/padding, role dot/name/org
  - `src/components/detail/SkillsAllDetail.tsx` — ~13 conversions: outer gap, category header gap/marginBottom/paddingBottom/fontSize, skill rows gap, SkillRow gap/padding, icon size, name/frequency fontSize, proficiency bar width/gap/text fontSize/minWidth
  - `src/components/detail/KPIDetail.tsx` — ~15 conversions: fallback fontSize/value/marginBottom, outer gap, headline fontSize 48px→3.2rem, label/sub fontSize, period badge, section h3s, body text, outcomes list/items
  - `src/components/detail/EducationDetail.tsx` — ~16 conversions: sectionHeaderStyle, outer gap, header icon row, icon container 36px→2.4rem, title fontSize, institution fontSize, date row, classification badge, body text, OSCE container/value/label, lists/items
  - `src/components/detail/ProjectDetail.tsx` — ~15 conversions: outer gap, header row, year/status fontSize/padding, clinician fontSize, section h3s, body text, tech stack gap/tags, results list/items, external link gap/padding/fontSize
- **Learnings for future iterations:**
  - All rem conversion stories (US-003 through US-006) are now complete — every component in the app uses rem for layout/typography values
  - Detail panel components share a common pattern: sectionHeaderStyle with fontSize 12px→0.8rem, marginBottom 8px→0.533rem, uppercase, letterSpacing 0.05em — this is repeated across KPIDetail, EducationDetail, ProjectDetail, SkillDetail
  - Progress bar heights (4px, 6px) and divider heights (1px) were left in px — these are decorative elements that don't benefit from scaling
  - The largest value converted was 48px→3.2rem (KPI headline in KPIDetail)
---

