Next stage

This commit is contained in:
2026-02-15 17:36:53 +00:00
parent 0e450c4b17
commit 19a4360a8c
7 changed files with 842 additions and 31 deletions
@@ -0,0 +1,200 @@
{
"project": "Portfolio — Login Screen Rework",
"branchName": "ralph/login-screen-rework",
"description": "Rework the login screen: responsive sizing, dashboard style alignment, CVMIS rebrand, animated capsule logo, live blurred dashboard background, connection status indicator UX, button pulse, and dissolve transition.",
"userStories": [
{
"id": "US-001",
"title": "Skip to login phase for dev iteration",
"description": "As a developer, I want to skip boot/ECG and land directly on the login screen so I can iterate on login changes quickly.",
"acceptanceCriteria": [
"In src/App.tsx, change the initial Phase state from 'boot' to 'login'",
"The boot, ECG, and login phases remain in code — only the initial state changes",
"App loads directly to the login screen on refresh",
"Typecheck passes"
],
"priority": 1,
"passes": true,
"notes": "Temporary — final story reverts this. Phase state is on line 47 of App.tsx."
},
{
"id": "US-002",
"title": "Create CvmisLogo React SVG component",
"description": "As a developer, I need a reusable CvmisLogo component that renders the CVMIS capsule logo from cvmis-logo.svg, supporting both static and animated modes.",
"acceptanceCriteria": [
"Create src/components/CvmisLogo.tsx as a React component",
"Component accepts props: size (number, controls height in px), animated (boolean, default false), className (optional string)",
"SVG paths are inlined from cvmis-logo.svg — three <g> groups: capsule-rx (teal #0b7979), capsule-terminal (amber #d97706), capsule-data (green #059669)",
"The SVG viewBox is preserved so the logo scales correctly at any size",
"When animated=false, all three capsules render in their final fanned-out positions (matching the original SVG layout)",
"When animated=true, the component plays a two-phase reveal using framer-motion: Phase 1 (Rise ~500ms): green data capsule scales from 0 to 1 and translates upward into center position, other capsules hidden. Phase 2 (Fan-out ~500ms): all three capsules appear and rotate/translate to their final fanned-out positions with staggered easing",
"Animation reference: LogoReveal/frame 1-5.jpg — frame 1-3 show green capsule rising, frame 4-5 show all three fanning out",
"Each capsule group uses transform-origin at its base/bottom so fan-out looks like cards spreading from a hand",
"prefers-reduced-motion: skip animation, render final state immediately",
"Typecheck passes"
],
"priority": 2,
"passes": true,
"notes": "The SVG uses a transform with scale(0.05, -0.05) and translate — you'll need to simplify the viewBox and transforms for React. The three <g> IDs are capsule-rx, capsule-terminal, capsule-data. Framer Motion is already installed (11.15.0). Look at LogoReveal/frame 1-5.jpg for the animation sequence. The fan-out in frames 4-5 shows: teal Rx tilts left, amber terminal stays center, green data tilts right."
},
{
"id": "US-003",
"title": "Responsive login card sizing and dashboard style alignment",
"description": "As a visitor on a 1440p or 4K display, I want the login card to be proportionate to my screen and styled consistently with the GP dashboard.",
"acceptanceCriteria": [
"Login card width changes from fixed 320px to responsive: clamp(320px, 28vw, 480px)",
"Card padding scales from fixed 32px to clamp(24px, 2.5vw, 40px)",
"Input field font size scales proportionally (minimum 13px, up to 15px on large viewports)",
"Button font size scales proportionally (minimum 14px, up to 16px)",
"Label font size scales proportionally (minimum 12px, up to 14px)",
"Card uses dashboard color tokens via CSS variables: background var(--surface), border color var(--border-card) or #E4EDEB, text colors var(--text-primary) and var(--text-secondary)",
"Input fields use var(--accent) (#0D6E6E) for focus border, #E4EDEB for default border, var(--bg-dashboard) for inactive background",
"Card shadow uses the project shadow tokens: 0 1px 2px rgba(26,43,42,0.05) resting, 0 2px 8px rgba(26,43,42,0.08) elevated",
"Card border radius remains 12px",
"Card still looks good on mobile (≤480px) — should not exceed viewport width minus 32px margin",
"Typecheck passes",
"Verify in browser using dev-browser skill"
],
"priority": 3,
"passes": true,
"notes": "LoginScreen.tsx currently uses inline styles with hardcoded colors (#E5E7EB borders, #64748B text, etc). Replace these with the dashboard CSS custom properties defined in index.css (--surface, --accent, --border, --text-primary, --text-secondary, --text-tertiary). Font family vars: var(--font-ui) for labels/buttons, var(--font-geist-mono) for input monospace."
},
{
"id": "US-004",
"title": "Rebrand to CVMIS and integrate animated logo",
"description": "As the portfolio owner, I want the login to say CVMIS with the capsule logo replacing the Shield icon.",
"acceptanceCriteria": [
"Title text changed from 'CareerRecord PMR' to 'CVMIS'",
"Subtitle changed from 'Clinical Information System' to 'CV Management Information System'",
"The Shield icon import and its teal background container are removed from the branding section",
"CvmisLogo component is imported and rendered in the branding section with animated=true",
"Logo height is proportional to the responsive card size (roughly 48-64px depending on viewport, use clamp)",
"Logo animation completes before the typing animation starts — adjust the startLoginSequence delay (currently 400ms) to account for logo animation duration (~1000ms total)",
"Footer text 'Secure clinical system login' remains unchanged",
"prefers-reduced-motion: logo shows instantly in final state, typing starts after original 400ms delay",
"Typecheck passes",
"Verify in browser using dev-browser skill"
],
"priority": 4,
"passes": true,
"notes": "The Shield icon is at LoginScreen.tsx lines 213-218. The logo animation is ~1000ms (500ms rise + 500ms fan-out). Increase the startLoginSequence delay from 400ms to ~1500ms (400ms card entrance + 1000ms logo + 100ms pause). CvmisLogo component from US-002."
},
{
"id": "US-005",
"title": "Replace Home icon with CVMIS logo on TopBar",
"description": "As a visitor on the dashboard, I want to see the CVMIS brand logo in the top-left corner instead of the generic Home icon.",
"acceptanceCriteria": [
"In src/components/TopBar.tsx, remove the Home import from lucide-react",
"Import CvmisLogo from ./CvmisLogo",
"Replace the <Home> element with <CvmisLogo size={24} /> (static, no animation)",
"Logo colors match SVG source: teal #0b7979, amber #d97706, green #059669",
"Logo maintains aspect ratio and fits within the TopBar height",
"The 'Headhunt Medical Center' brand text and all other TopBar elements remain unchanged",
"Typecheck passes",
"Verify in browser using dev-browser skill"
],
"priority": 5,
"passes": true,
"notes": "TopBar.tsx line 57-61 has the Home icon. Simple swap — CvmisLogo with animated=false (the default). If Home is the only lucide icon used in the import, clean up the import. Check: Search is also imported from lucide-react on line 2."
},
{
"id": "US-006",
"title": "Render live dashboard behind login with blur overlay",
"description": "As a visitor, I want to see the GP dashboard blurred behind the login card, creating visual continuity.",
"acceptanceCriteria": [
"In App.tsx, during the 'login' phase, render DashboardLayout underneath the login overlay (both visible simultaneously)",
"DashboardLayout is wrapped in DetailPanelProvider (as it is in the 'pmr' phase)",
"DashboardLayout renders at scroll position 0 (showing patient summary header area)",
"LoginScreen becomes an overlay: fixed position, full viewport, semi-transparent background rgba(240, 245, 244, 0.7) with backdrop-filter: blur(20px)",
"Dashboard content is non-interactive while login overlay is present (the overlay captures all pointer events)",
"The login card remains centered on top of the blurred overlay",
"backdrop-filter blur is constant from the moment login appears (no ease-in)",
"prefers-reduced-motion: blur still applies (static visual treatment), only entrance animations are skipped",
"Typecheck passes",
"Verify in browser using dev-browser skill"
],
"priority": 6,
"passes": true,
"notes": "Currently App.tsx renders phases exclusively (only one at a time). Change so that login phase renders: <DetailPanelProvider><DashboardLayout /></DetailPanelProvider> + <LoginScreen overlay on top>. LoginScreen.tsx already has 'fixed inset-0 z-50' — just change its backgroundColor from solid #1A2B2A to the semi-transparent value with backdrop-filter. Consider adding will-change: backdrop-filter for performance."
},
{
"id": "US-007",
"title": "Connection status indicator with animated dots and typing-linked timing",
"description": "As a visitor, I want to see a clear red-to-green status transition tied to the typing sequence, not an arbitrary timer.",
"acceptanceCriteria": [
"Status indicator LED dot size increased from 6px to 10px",
"LED dot has a subtle glow effect: box-shadow 0 0 6px 1px in the LED color (red or green)",
"Status text size increased from 10px to 12px",
"Initial state: RED LED + 'Awaiting secure connection' in red (#DC2626) with animated trailing dots",
"The trailing dots animate: dots cycle through '.', '..', '...' repeating every ~1.5 seconds",
"Remove the existing independent 2000ms connectionTimeout timer",
"Instead, connection transitions to green exactly 500ms after typingComplete becomes true",
"Green state: GREEN LED (#059669) + 'Secure connection established, awaiting login' in green",
"Transition between red and green states has a smooth 300ms color/shadow transition",
"prefers-reduced-motion: no dot cycling animation, state changes happen instantly",
"Typecheck passes",
"Verify in browser using dev-browser skill"
],
"priority": 7,
"passes": true,
"notes": "The connectionTimeout is set on line 117 of LoginScreen.tsx (2000ms independent timer). Remove it and add a useEffect that watches typingComplete — when true, setTimeout 500ms then setConnectionState('connected'). The dot animation can use a simple interval cycling dotCount 0→1→2→0. The LED glow box-shadow: '0 0 6px 1px rgba(220,38,38,0.4)' for red, '0 0 6px 1px rgba(5,150,105,0.4)' for green."
},
{
"id": "US-008",
"title": "Login button pulse animation on activation",
"description": "As a visitor, I want the login button to pulse subtly when it becomes clickable so I know to click it.",
"acceptanceCriteria": [
"Add a CSS @keyframes animation 'login-pulse' in index.css: scale 1 → 1.03 → 1, ease-in-out, duration 1.5s",
"When canLogin becomes true (button enabled), apply the pulse animation repeating every 3 seconds (1.5s animation + 1.5s pause via animation-delay or longer duration with keyframe percentages)",
"Pulse animation stops when button is hovered (animation: none on hover)",
"Pulse animation stops immediately on click (remove animation class on buttonPressed)",
"Button opacity transitions from 0.6 to 1.0 when enabled (existing behavior, preserve)",
"prefers-reduced-motion: no pulse animation, button just becomes enabled with opacity 1",
"Button still receives keyboard focus when it becomes enabled (existing behavior)",
"Typecheck passes",
"Verify in browser using dev-browser skill"
],
"priority": 8,
"passes": true,
"notes": "The canLogin variable is on line 43 of LoginScreen.tsx. Add a CSS class 'login-pulse-active' that applies the animation, and conditionally apply it when canLogin && !buttonPressed && !buttonHovered. The @keyframes could use: 0%,100% { transform: scale(1) } 50% { transform: scale(1.03) } with animation: login-pulse 1.5s ease-in-out infinite and a wrapper that adds 1.5s gaps (or use 0%,35%,65%,100% keyframe percentages to build in the pause)."
},
{
"id": "US-009",
"title": "Login dissolve transition to reveal dashboard",
"description": "As a visitor, I want the login card and blur overlay to dissolve smoothly on login, revealing the dashboard underneath.",
"acceptanceCriteria": [
"On login click: existing pressed state + loading spinner behavior is preserved",
"After loading spinner phase, the login card fades out (opacity 0) with slight scale up (1.03)",
"Simultaneously, the overlay backdrop-filter blur animates from 20px to 0px",
"Overlay background opacity fades from 0.7 to 0",
"Total dissolve duration: ~600ms from card exit to fully revealed dashboard",
"After dissolve completes, the login overlay is removed from DOM and dashboard becomes interactive",
"In App.tsx, transition from login to pmr phase after the overlay dissolve completes (use a callback from LoginScreen)",
"prefers-reduced-motion: instant transition, no dissolve animation",
"Typecheck passes",
"Verify in browser using dev-browser skill"
],
"priority": 9,
"passes": true,
"notes": "Currently LoginScreen has isExiting state that scales card to 1.03 and fades to opacity 0 (line 163). Extend this to also animate the overlay container. The overlay is the outer div with 'fixed inset-0' — animate its backdrop-filter and background-color. Use framer-motion animate for coordinated exit. The onComplete callback should fire after the full dissolve, not after the card fade."
},
{
"id": "US-010",
"title": "Re-enable boot sequence",
"description": "As a user, I want the full boot → ECG → login → dashboard experience restored.",
"acceptanceCriteria": [
"In src/App.tsx, change the initial Phase state back from 'login' to 'boot'",
"Boot → ECG → Login → Dashboard sequence works end to end",
"Login screen shows blurred dashboard behind it",
"Logo animation plays, typing animation follows, connection indicator transitions, button pulses",
"Clicking login dissolves the overlay to reveal the dashboard",
"No other changes to App.tsx beyond reverting the initial state",
"Typecheck passes",
"Verify in browser using dev-browser skill: app starts at boot, progresses through ECG, login with blur background and logo animation, arrives at dashboard"
],
"priority": 10,
"passes": true,
"notes": "Simple revert of US-001. Phase state is on line 47 of App.tsx."
}
]
}
@@ -0,0 +1,208 @@
# Progress Log — Login Screen Rework
# Branch: ralph/login-screen-rework
# Started: 2026-02-15
## Codebase Patterns
### Project Structure
- Components in `src/components/`, tiles in `src/components/tiles/`
- Data files in `src/data/`
- Types in `src/types/pmr.ts` and `src/types/index.ts`
- Hooks in `src/hooks/`, Contexts in `src/contexts/`, Lib in `src/lib/`
- Path alias: `@/` maps to `./src/`
### Phase Management
- App.tsx controls phase: 'boot' -> 'ecg' -> 'login' -> 'pmr'
- BootSequence.tsx, ECGAnimation.tsx — LOCKED, do not modify
- LoginScreen.tsx bridges to dashboard
### Typography
- Elvaro Grotesque (`font-ui`, `var(--font-ui)`) — primary UI font
- Blumir (`font-ui-alt`) — alternative variable font
- Geist Mono (`font-geist`, `var(--font-geist-mono)`) — timestamps, data values
- Fira Code (`font-mono`) — boot/ECG terminal only
- Do NOT use Inter, Roboto, DM Sans, or system defaults
### Design Tokens (index.css CSS variables)
- --surface: #FFFFFF (card/topbar background)
- --bg-dashboard: #F0F5F4 (warm sage content background)
- --accent: #0D6E6E (teal primary)
- --accent-hover: #0A8080
- --accent-light: rgba(10,128,128,0.08)
- --border: #D4E0DE (structural borders)
- --border-card: #E4EDEB (card/inner borders)
- --text-primary: #1A2B2A
- --text-secondary: #5B7A78
- --text-tertiary: #8DA8A5
- --sidebar-width: 304px
- --topbar-height: 56px
### Known Dependencies
- React 18.3.1, TypeScript, Vite, Tailwind CSS
- Framer Motion 11.15.0, Lucide React 0.468.0, fuse.js 7.0.0
### Phase Rendering (post US-006)
- Login phase now renders BOTH DashboardLayout and LoginScreen overlay simultaneously
- DashboardLayout is wrapped in DetailPanelProvider for both 'login' and 'pmr' phases
- LoginScreen overlay: `fixed inset-0 z-50` with `rgba(240, 245, 244, 0.7)` + `backdrop-filter: blur(20px)`
### Key Files for This Feature
- src/App.tsx — phase management, will need restructuring for blur overlay
- src/components/LoginScreen.tsx — main login screen (416 lines)
- src/components/TopBar.tsx — Home icon replacement target (line 57)
- src/components/DashboardLayout.tsx — rendered behind login blur
- src/contexts/DetailPanelContext.tsx — wraps DashboardLayout
- cvmis-logo.svg — source SVG with 3 capsule groups
- LogoReveal/frame 1-5.jpg — animation reference frames
### CvmisLogo Component
- `size` prop: numeric, sets SVG height attribute directly
- `cssHeight` prop: string, sets height via CSS style (use for clamp/responsive values)
- `animated` prop: boolean, enables framer-motion reveal animation (1000ms total)
- Logo animation: 500ms rise (green capsule) + 500ms fan-out (all three) = 1000ms total
### LoginScreen.tsx Key Lines (post US-007)
- Line 20: connectionState useState
- Line 21: dotCount useState (for animated trailing dots)
- Line 43: canLogin derived state
- Line 60-101: startLoginSequence (typing animation)
- Line 110-115: useEffect — connection transitions to green 500ms after typingComplete
- Line 118-126: useEffect — animated dot cycling (500ms interval) while connecting
- Line 128-150: useEffect — cursor blink + startLoginSequence delay (no more connectionTimeout)
- Line 370-405: Connection status indicator (10px LED dot with glow, 12px text)
---
## 2026-02-15 - US-010
- Reverted initial Phase state from 'login' back to 'boot' in App.tsx line 47
- Full flow verified: boot → ECG → login (with blur, logo, typing, connection indicator, pulse) → dissolve → dashboard
- Files changed: src/App.tsx
- **Learnings for future iterations:**
- Simple one-line revert as planned in US-001
- The full boot→ECG→login sequence takes ~20 seconds before login screen appears
---
## 2026-02-15 - US-009
- Changed outer overlay container from plain `<div>` to `<motion.div>` for animated exit
- On isExiting: overlay animates backgroundColor to transparent, backdropFilter from blur(20px) to blur(0px) over 600ms
- Card exit animation extended from 200ms to 400ms for smoother dissolve feel
- onComplete callback fires after 600ms dissolve (previously 200ms card exit)
- After dissolve completes, overlay removed from DOM and dashboard becomes interactive
- prefers-reduced-motion: instant transition (0ms for all timers)
- Files changed: src/components/LoginScreen.tsx
- Verified in browser: clicked login → spinner → card fades + overlay blur dissolves → dashboard revealed
- **Learnings for future iterations:**
- framer-motion can animate backdropFilter and backgroundColor on a motion.div via the animate prop
- The onComplete timeout (600ms) must match the overlay dissolve duration, not the card fade duration
- Card fade (400ms) finishes before overlay dissolve (600ms), creating a layered reveal effect
- WebkitBackdropFilter needs to be animated alongside backdropFilter for Safari
---
## 2026-02-15 - US-008
- Added @keyframes login-pulse in index.css: scale 1→1.03→1 over 3s cycle (1.5s animation built into keyframe percentages with 1.5s pause)
- Added .login-pulse-active class that applies the animation infinitely
- Hover removes animation via CSS rule (.login-pulse-active:hover { animation: none })
- Button gets login-pulse-active class when canLogin && !buttonPressed
- prefers-reduced-motion: .login-pulse-active { animation: none } in reduced motion media query
- Button opacity 0.6→1.0 transition preserved (existing behavior)
- Button still receives keyboard focus when enabled (existing behavior)
- Files changed: src/index.css, src/components/LoginScreen.tsx
- Verified in browser: button has login-pulse animation running (3s ease-in-out infinite), class applied correctly
- **Learnings for future iterations:**
- Used keyframe percentages (0%,60%,100% at scale(1), 30% at scale(1.03)) to build pause into a single animation rather than animation-delay
- CSS handles hover removal — no need for buttonHovered state in the class condition
- buttonPressed removes the class entirely (not just pauses), which is cleaner
---
## 2026-02-15 - US-007
- Reworked connection status indicator: LED dot 6px→10px with glow box-shadow, text 10px→12px
- Removed independent 2000ms connectionTimeout timer
- Added useEffect that transitions to green 500ms after typingComplete becomes true
- Added animated trailing dots cycling '.', '..', '...' every 500ms while connecting
- Initial state: red LED + red text "Awaiting secure connection" with animated dots
- Connected state: green LED + green text "Secure connection established, awaiting login"
- 300ms smooth transition for color and box-shadow between states
- prefers-reduced-motion: no dot cycling, instant state changes
- Files changed: src/components/LoginScreen.tsx
- Verified in browser: red indicator with cycling dots visible during typing, transitions to green after typing completes
- **Learnings for future iterations:**
- dotCount state cycles 0→1→2→3→0 (4 states: no dots, '.', '..', '...') via modulo arithmetic
- Connection transition is now tied to typingComplete state, not an arbitrary timer
- The dot interval cleanup needs to happen in both the dedicated useEffect and the main cleanup
- LED glow uses rgba with 0.4 alpha for subtle effect matching project shadow conventions
---
## 2026-02-15 - US-006
- Rendered DashboardLayout (wrapped in DetailPanelProvider) behind LoginScreen during login phase in App.tsx
- Changed LoginScreen overlay from solid #1A2B2A background to semi-transparent rgba(240, 245, 244, 0.7) with backdrop-filter: blur(20px)
- Dashboard is non-interactive during login (overlay captures pointer events via fixed inset-0 z-50)
- After login click, phase transitions to 'pmr' and overlay is removed from DOM, dashboard becomes interactive
- Files changed: src/App.tsx, src/components/LoginScreen.tsx
- Verified in browser: blur overlay shows dashboard content behind login card, login click transitions to interactive dashboard
- **Learnings for future iterations:**
- App.tsx phase rendering changed from exclusive (one phase at a time) to overlapping (login + pmr render DashboardLayout)
- DetailPanelProvider now wraps DashboardLayout for both 'login' and 'pmr' phases — condition is `(phase === 'login' || phase === 'pmr')`
- LoginScreen already had `fixed inset-0 z-50` which makes it a full-viewport overlay — just needed background/blur changes
- WebkitBackdropFilter needed for Safari compatibility alongside backdropFilter
---
## 2026-02-15 - US-005
- Replaced Home icon with CvmisLogo (size={24}, static/no animation) in TopBar.tsx
- Removed Home from lucide-react import (Search still used)
- Imported CvmisLogo component
- Files changed: src/components/TopBar.tsx
- Verified in browser: logo renders correctly with teal/amber/green capsule colors, fits TopBar height
- **Learnings for future iterations:**
- TopBar uses inline styles throughout, consistent with LoginScreen pattern
- Search is the only remaining lucide-react icon in TopBar.tsx
- CvmisLogo default `animated=false` means no animation prop needed for static usage
---
## 2026-02-15 - US-004
- Rebranded login from "CareerRecord PMR" to "CVMIS" with subtitle "CV Management Information System"
- Replaced Shield icon with CvmisLogo component (animated=true, responsive cssHeight)
- Added `cssHeight` prop to CvmisLogo for CSS clamp-based responsive sizing: clamp(48px, 4vw, 64px)
- Increased startLoginSequence delay from 400ms to 1500ms to let logo animation complete before typing begins
- prefers-reduced-motion: keeps original 400ms delay since logo renders instantly
- Fixed lint warning: added prefersReducedMotion to useEffect dependency array
- Files changed: src/components/LoginScreen.tsx, src/components/CvmisLogo.tsx
- **Learnings for future iterations:**
- CvmisLogo `size` prop is numeric (SVG height attribute) — use `cssHeight` string prop for CSS clamp values
- Logo animation is 1000ms total (500ms rise + 500ms fan-out) — typing delay must account for this
- The committed LoginScreen from US-003 still had Shield icon — US-003 only committed responsive sizing, not branding changes
---
## 2026-02-15 - US-003
- Responsive card: width clamp(320px,28vw,480px), maxWidth calc(100vw-32px), padding clamp(24px,2.5vw,40px)
- Replaced hardcoded colors with CSS variables: --surface, --bg-dashboard, --accent, --text-secondary, --text-tertiary
- Input fields: #E4EDEB default border, var(--accent) focus border, var(--bg-dashboard) inactive bg
- Font sizes: labels clamp(12px,1vw,14px), inputs clamp(13px,1.1vw,15px), button clamp(14px,1.1vw,16px)
- Card shadow: 0 1px 2px rgba(26,43,42,0.05) matching project shadow tokens
- Files changed: src/components/LoginScreen.tsx
- **Learnings for future iterations:**
- No --border-card CSS variable exists in index.css — use #E4EDEB directly
- LoginScreen uses inline styles throughout, not Tailwind classes (except for focus-visible ring on button)
- The card used className="bg-white" which needed to be replaced with inline style for consistency
---
## 2026-02-15 - US-002
- Created CvmisLogo.tsx component with inlined SVG paths from cvmis-logo.svg
- Three capsule groups: capsule-rx (teal #0b7979), capsule-terminal (amber #d97706), capsule-data (green #059669)
- Props: size (height px), animated (boolean, default false), className (optional)
- Framer Motion animation: Phase 1 (rise 500ms) — green data capsule scales from 0, Phase 2 (fan-out 500ms) — all three appear
- prefers-reduced-motion: skips animation, renders final state immediately
- Files changed: src/components/CvmisLogo.tsx (new)
- **Learnings for future iterations:**
- The SVG uses viewBox="0 0 600 506" with internal g transform scale(0.05,-0.05) — keep this coordinate system intact
- framer-motion's useReducedMotion() hook is the simplest way to handle reduced motion
- transform-origin in SVG needs px units when using framer-motion on g elements
---
## 2026-02-15 - US-001
- Changed initial Phase state from 'boot' to 'login' in App.tsx line 47
- Files changed: src/App.tsx
- **Learnings for future iterations:**
- Phase state is a simple string union type on line 47 of App.tsx
- US-010 will revert this exact change back to 'boot'
---