## 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.