## Context The app uses React 19 with inline styles and no CSS framework. Current animations are limited to: - A `skeleton-pulse` keyframe for loading placeholders in CourseSelection - `transform` + `transition` on mobile banners (MobileStatusBanner, MobileCourseBanner) - dnd-kit's built-in drag transform/transition on sortable items All other state changes (course pin/unpin, credit bar updates, expand/collapse, status badge changes, mode toggle) are instant. The app has no animation library dependency. ## Goals / Non-Goals **Goals:** - Smooth transitions for all interactive state changes so users can follow what changed - Keep bundle size unchanged — CSS-only approach using `transition` and `@keyframes` - Respect `prefers-reduced-motion` globally - Maintain existing inline-style pattern (no migration to CSS modules or Tailwind) **Non-Goals:** - Page/route transition animations (app is single-page, no routing) - Drag-and-drop animation improvements (dnd-kit handles this already) - Loading skeleton animation changes (already animated) - Mobile banner slide animations (already implemented) - Scroll animations or parallax effects ## Decisions ### 1. CSS transitions on inline styles, no animation library **Choice**: Add `transition` properties to existing inline styles. **Rationale**: The app already uses inline styles everywhere. Adding framer-motion or react-spring would introduce a runtime dependency (15-40 KB) for effects that CSS `transition` handles natively. CSS transitions are GPU-accelerated and zero-JS-overhead. **Alternative considered**: framer-motion `AnimatePresence` for mount/unmount animations. Rejected because the bundle cost isn't justified — we can approximate enter/exit with max-height or opacity transitions on always-mounted wrappers. ### 2. Expand/collapse via max-height + overflow, not conditional rendering **Choice**: For CreditLegend and AllocationBreakdown, keep elements mounted but toggle between `max-height: 0; overflow: hidden` and `max-height: ` with a transition. **Rationale**: CSS cannot transition elements that are conditionally rendered (`{open &&
...}
`). To animate open/close, the element must remain in the DOM. Using `max-height` with a generous upper bound (e.g., `500px`) provides a smooth slide without needing to measure actual content height. **Alternative considered**: `requestAnimationFrame` + `scrollHeight` measurement for precise height animation. Rejected — adds JS complexity for marginal visual improvement. The max-height approach is standard and sufficient. ### 3. Global `prefers-reduced-motion` reset in index.css **Choice**: Add a single CSS rule in `index.css`: ```css @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } } ``` **Rationale**: A global rule is simpler and more reliable than adding motion checks to each component. This is the recommended pattern from MDN. Using `0.01ms` instead of `0s` ensures transition events still fire. ### 4. Credit bar transitions via CSS `transition: width` **Choice**: Add `transition: width 300ms ease-out` to the CreditBar's inner `
` elements. **Rationale**: The bars already use percentage-based widths. Adding a transition property is a one-line change that produces a smooth fill/drain animation as credit allocations change. ### 5. Status badge color transitions **Choice**: Add `transition: background 200ms, color 200ms` to the status badge `` in SortableItem. **Rationale**: When a specialization's status changes (e.g., achievable → achieved), the badge colors will cross-fade instead of snapping. Since the badge is always rendered (just with different colors), a simple transition property works. ### 6. Mode toggle sliding indicator **Choice**: Add `transition: background 150ms, box-shadow 150ms` to both toggle buttons. **Rationale**: The toggle already swaps `background` and `boxShadow` between active/inactive. Adding transitions creates a smooth switch effect with no structural changes. ### 7. ModeComparison banner enter/exit **Choice**: Always render the banner div but use `opacity` + `max-height` transitions to animate in/out, with the conditional logic controlling style values rather than mount/unmount. **Rationale**: Same pattern as expand/collapse. The banner either shows or hides based on whether the two modes differ. Transitioning opacity provides a fade effect. ### 8. Course set pin/unpin transitions **Choice**: Add `transition: border-color 200ms, background 200ms` to the ElectiveSet container div. **Rationale**: When a course is pinned, the set's border changes from dashed gray to solid blue and background shifts. Transitioning these properties creates a visual confirmation of the selection. ## Risks / Trade-offs - **max-height overshoot**: Using a fixed max-height (e.g., 500px) means the transition timing won't perfectly match content height. Short content will appear to "accelerate" at the end. → Mitigation: Use `ease-out` timing and a reasonable max-height close to typical content size. - **Inline style transitions are slightly verbose**: Each component gets `transition: ...` added to its style object. → Mitigation: This is consistent with the existing pattern. Extracting shared transition strings into constants keeps it DRY. - **Content flash on expand/collapse refactor**: Changing from conditional render to always-mounted-but-hidden may cause brief layout shifts during the transition. → Mitigation: Use `overflow: hidden` on the collapsible wrapper to clip content during animation.