diff --git a/app/src/components/CourseSelection.tsx b/app/src/components/CourseSelection.tsx
index 6d32a5f..0a79197 100644
--- a/app/src/components/CourseSelection.tsx
+++ b/app/src/components/CourseSelection.tsx
@@ -51,11 +51,13 @@ function ElectiveSet({
return (
@@ -78,11 +80,25 @@ function ElectiveSet({
)}
- {isPinned ? (
+ {/* Pinned view */}
+
{pinnedCourse?.name}
- ) : (
+
+ {/* Course list view */}
+
{courses.map((course) => {
const ceiling = ceilingMap.get(course.id);
@@ -136,7 +152,7 @@ function ElectiveSet({
);
})}
- )}
+
);
}
diff --git a/app/src/components/CreditLegend.tsx b/app/src/components/CreditLegend.tsx
index 80258d2..c225e50 100644
--- a/app/src/components/CreditLegend.tsx
+++ b/app/src/components/CreditLegend.tsx
@@ -14,7 +14,11 @@ export function CreditLegend() {
>
{open ? '▾ How to read this' : '▸ How to read this'}
- {open && (
+ ` 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.
diff --git a/openspec/changes/enhance-ui-animations/proposal.md b/openspec/changes/enhance-ui-animations/proposal.md
new file mode 100644
index 0000000..b55005e
--- /dev/null
+++ b/openspec/changes/enhance-ui-animations/proposal.md
@@ -0,0 +1,29 @@
+## Why
+
+UI state changes (course selection, status transitions, panel expand/collapse) happen instantly, making interactions feel abrupt. Adding subtle CSS transitions and animations will make the app feel more polished and help users track what changed after each action.
+
+## What Changes
+
+- Add CSS transitions to course pin/unpin so the elective set smoothly shifts between its "open" and "pinned" states (border, background color, content swap)
+- Animate credit bar width changes so users can visually follow how selecting a course redistributes credits across specializations
+- Add expand/collapse transitions to CreditLegend and AllocationBreakdown instead of instant mount/unmount
+- Animate status badge changes (e.g., "Achievable" → "Achieved") with a brief highlight or color transition
+- Add a smooth transition to the ModeToggle active indicator when switching optimization modes
+- Animate the ModeComparison notification banner entrance/exit
+- Keep all animations CSS-only (transitions + keyframes) — no new runtime dependencies
+
+## Capabilities
+
+### New Capabilities
+- `ui-transitions`: CSS transition utilities and patterns for smooth state changes across all interactive components (course selection, credit bars, expand/collapse, status badges, mode toggle, notification banners)
+
+### Modified Capabilities
+
+
+## Impact
+
+- **Components affected**: CourseSelection, SpecializationRanking, CreditLegend, ModeToggle, Notifications (ModeComparison)
+- **CSS**: New transition declarations added to inline styles; possible new keyframe definitions in index.css
+- **Dependencies**: None — CSS-only approach, no new packages
+- **Accessibility**: All animations will respect `prefers-reduced-motion` media query
+- **Performance**: CSS transitions are GPU-accelerated; no JS animation loops
diff --git a/openspec/changes/enhance-ui-animations/specs/ui-transitions/spec.md b/openspec/changes/enhance-ui-animations/specs/ui-transitions/spec.md
new file mode 100644
index 0000000..2d5df1d
--- /dev/null
+++ b/openspec/changes/enhance-ui-animations/specs/ui-transitions/spec.md
@@ -0,0 +1,85 @@
+## ADDED Requirements
+
+### Requirement: Course set pin/unpin transition
+The ElectiveSet container SHALL transition its border-color and background-color over 200ms when a course is pinned or unpinned.
+
+#### Scenario: User pins a course
+- **WHEN** user clicks a course button in an unpinned elective set
+- **THEN** the set's border smoothly transitions from dashed gray to solid blue and background fades from gray to blue-tinted over 200ms
+
+#### Scenario: User unpins a course
+- **WHEN** user clicks the "Clear" button on a pinned elective set
+- **THEN** the set's border and background smoothly transition back to the unpinned style over 200ms
+
+### Requirement: Credit bar width transition
+The CreditBar's allocated and potential bar segments SHALL animate their width changes over 300ms with ease-out timing when credit allocations change.
+
+#### Scenario: Credits increase after pinning a course
+- **WHEN** a course is pinned that contributes credits to a specialization
+- **THEN** the CreditBar's filled segments smoothly grow to their new width over 300ms
+
+#### Scenario: Credits decrease after unpinning a course
+- **WHEN** a course is unpinned that was contributing credits
+- **THEN** the CreditBar's filled segments smoothly shrink to their new width over 300ms
+
+### Requirement: Status badge color transition
+The specialization status badge SHALL transition its background-color and text color over 200ms when the specialization's status changes.
+
+#### Scenario: Status changes from achievable to achieved
+- **WHEN** a course selection causes a specialization to reach 9 credits
+- **THEN** the badge colors cross-fade from blue (achievable) to green (achieved) over 200ms
+
+#### Scenario: Status changes from achieved to achievable
+- **WHEN** a course is unpinned causing a specialization to drop below 9 credits
+- **THEN** the badge colors cross-fade from green back to blue over 200ms
+
+### Requirement: CreditLegend expand/collapse animation
+The CreditLegend help panel SHALL animate open and closed with a sliding transition instead of instant mount/unmount.
+
+#### Scenario: User opens the legend
+- **WHEN** user clicks "How to read this"
+- **THEN** the legend content slides open over 200ms using a max-height transition with overflow hidden
+
+#### Scenario: User closes the legend
+- **WHEN** user clicks the toggle again while the legend is open
+- **THEN** the legend content slides closed over 200ms
+
+### Requirement: AllocationBreakdown expand/collapse animation
+The AllocationBreakdown detail panel in achieved specializations SHALL animate open and closed with a sliding transition.
+
+#### Scenario: User expands an achieved specialization
+- **WHEN** user clicks an achieved specialization row to view its allocation breakdown
+- **THEN** the breakdown content slides open over 200ms
+
+#### Scenario: User collapses an achieved specialization
+- **WHEN** user clicks the row again to hide the breakdown
+- **THEN** the breakdown content slides closed over 200ms
+
+### Requirement: Mode toggle transition
+The ModeToggle buttons SHALL transition their background, box-shadow, and text color over 150ms when the active mode changes.
+
+#### Scenario: User switches optimization mode
+- **WHEN** user clicks the inactive mode button
+- **THEN** the active indicator (white background + shadow) smoothly transitions to the newly selected button over 150ms while the previous button fades to the inactive style
+
+### Requirement: ModeComparison banner enter/exit animation
+The ModeComparison notification banner SHALL fade in when it appears and fade out when it disappears, instead of instant mount/unmount.
+
+#### Scenario: Banner appears after mode switch reveals a difference
+- **WHEN** the comparison banner becomes relevant (modes produce different results)
+- **THEN** the banner fades in with an opacity transition over 200ms
+
+#### Scenario: Banner disappears when modes align
+- **WHEN** the comparison banner is no longer relevant (modes produce same results)
+- **THEN** the banner fades out with an opacity transition over 200ms
+
+### Requirement: Reduced motion accessibility
+All transitions and animations SHALL be effectively disabled when the user's system has `prefers-reduced-motion: reduce` enabled.
+
+#### Scenario: User has reduced motion enabled
+- **WHEN** the operating system or browser is configured with `prefers-reduced-motion: reduce`
+- **THEN** all transition and animation durations are reduced to near-zero (0.01ms) so state changes are instant
+
+#### Scenario: User does not have reduced motion enabled
+- **WHEN** no reduced-motion preference is set
+- **THEN** all transitions play at their specified durations
diff --git a/openspec/changes/enhance-ui-animations/tasks.md b/openspec/changes/enhance-ui-animations/tasks.md
new file mode 100644
index 0000000..b71328d
--- /dev/null
+++ b/openspec/changes/enhance-ui-animations/tasks.md
@@ -0,0 +1,34 @@
+## 1. Global Accessibility Setup
+
+- [x] 1.1 Add `prefers-reduced-motion` media query to `index.css` that sets all animation and transition durations to 0.01ms
+
+## 2. Course Selection Transitions
+
+- [x] 2.1 Add `transition: border-color 200ms, background-color 200ms` to the ElectiveSet container div in `CourseSelection.tsx`
+
+## 3. Credit Bar Transitions
+
+- [x] 3.1 Add `transition: width 300ms ease-out` to both the allocated and potential bar segments in the CreditBar component in `SpecializationRanking.tsx`
+
+## 4. Status Badge Transitions
+
+- [x] 4.1 Add `transition: background 200ms, color 200ms` to the status badge span in the SortableItem component in `SpecializationRanking.tsx`
+
+## 5. Expand/Collapse Animations
+
+- [x] 5.1 Refactor CreditLegend to always mount the content panel and use `max-height` + `overflow: hidden` + `transition: max-height 200ms ease-out` instead of conditional rendering
+- [x] 5.2 Refactor AllocationBreakdown in SortableItem to always mount and use `max-height` + `overflow: hidden` + `transition: max-height 200ms ease-out` instead of conditional rendering
+
+## 6. Mode Toggle Transition
+
+- [x] 6.1 Add `transition: background 150ms, box-shadow 150ms, color 150ms` to both mode toggle buttons in `ModeToggle.tsx`
+
+## 7. ModeComparison Banner Animation
+
+- [x] 7.1 Refactor ModeComparison in `Notifications.tsx` to always render the banner div and use `opacity` + `max-height` transitions for enter/exit instead of returning null
+
+## 8. Verification
+
+- [ ] 8.1 Manually verify all transitions play correctly: pin/unpin courses, toggle mode, expand/collapse legend and breakdown, observe credit bars and status badges
+- [ ] 8.2 Verify reduced-motion: confirm all animations are effectively instant when `prefers-reduced-motion: reduce` is active
+- [x] 8.3 Run existing tests to ensure no regressions from the refactored conditional rendering