## Context
The app already has a `MobileStatusBanner` component (top banner for specialization statuses) implemented with IntersectionObserver + `position: fixed; top: 0`. This change adds a symmetric bottom banner for course selection progress, following the same patterns.
On mobile, the layout is a single-column flex with specializations above, course selection below. The user needs to scroll between them. The existing top banner appears when specializations scroll out of view; this bottom banner appears when the course selection scrolls out of view (above the viewport).
## Goals / Non-Goals
**Goals:**
- Show a compact, fixed-position banner at the bottom of the viewport on mobile when the course selection section is scrolled above the viewport
- Display course selection progress: "{N} of 12 selected" with a compact visual indicator
- Tapping the banner scrolls to the course selection section
- Follow the same animation and styling patterns as the existing top banner
**Non-Goals:**
- No banner on tablet or desktop
- No course-level detail in the banner (just the count)
- No interaction beyond tap-to-scroll
## Decisions
### 1. Visibility trigger: IntersectionObserver on the course section ref
**Decision**: Attach a `ref` to the course selection wrapper `
` in `App.tsx`. Use an IntersectionObserver to detect when this element leaves the viewport. Show the banner when the element is not intersecting.
**Rationale**: Same approach as the specializations banner — consistent, efficient, proven pattern.
### 2. Position: fixed at bottom
**Decision**: Use `position: fixed; bottom: 0` with `z-index: 1000`, matching the top banner's z-index. The banner slides up from the bottom using `translateY(100%)` → `translateY(0)`.
**Rationale**: Symmetric with the top banner. Same z-index is fine since the two banners don't overlap (one is at top, one at bottom).
### 3. Summary content: Selected count out of 12
**Decision**: Display the number of pinned courses out of 12 total elective sets. Show as "{N} / 12 courses selected". Derive the count from `Object.keys(pinnedCourses).length`.
**Rationale**: Simple, glanceable summary. The 12-total is fixed (defined in `ELECTIVE_SETS`). The count gives immediate progress awareness.
### 4. Component structure: Separate `MobileCourseBanner` component
**Decision**: Create a new `MobileCourseBanner` component rather than making the existing `MobileStatusBanner` generic. Props: `selectedCount: number`, `totalSets: number`, `visible: boolean`, `onTap: () => void`.
**Rationale**: The two banners display fundamentally different data (status badges vs. course count). A shared abstraction would add complexity for little benefit. Keep them simple and independent.
### 5. Integration: Second observer in App.tsx
**Decision**: Add a `courseSectionRef` and a second `useEffect` with its own IntersectionObserver in `App.tsx`, alongside the existing `specSectionRef` observer. Use a separate `courseBannerVisible` state.
**Rationale**: Two independent observers are cleaner than combining logic. Each has its own ref, state, and cleanup.
## Risks / Trade-offs
- **Two fixed banners at once**: On very short mobile viewports, both banners could theoretically appear simultaneously (specializations off top, courses off bottom), reducing visible content. → Acceptable: each is ~40px; this scenario means the user is in the middle of the page with neither section visible, which is the exact moment summaries are most useful.
- **Observer count**: Two IntersectionObservers on mobile. → Negligible performance cost; observers are event-driven, not polling.