## Context
On mobile (max-width: 639px), the app stacks the specializations panel above the course selection panel in a single scrolling page. The user loses sight of specialization statuses when scrolling down to pick courses. There are no existing sticky/fixed elements in the codebase — all positioning is static or component-relative.
The app uses inline `React.CSSProperties` exclusively (no CSS framework), React 19, and the `useMediaQuery` hook for responsive breakpoints.
## Goals / Non-Goals
**Goals:**
- Show a compact, fixed-position banner at the top of the viewport on mobile when the specializations section scrolls out of view
- Summarize all four status categories with counts and color-coded badges matching `STATUS_STYLES`
- Allow tapping the banner to scroll back to the specializations section
- Animate the banner in/out smoothly
**Non-Goals:**
- No banner on tablet or desktop — those layouts already keep specializations visible via the two-column grid with `maxHeight: 85vh` scroll containers
- No interactive controls in the banner (no reordering, no expansion) — it's read-only summary
- No persistence of banner visibility preference
## Decisions
### 1. Visibility trigger: IntersectionObserver on a sentinel ref
**Decision**: Attach a `ref` to the specializations wrapper `
` in `App.tsx`. Use an `IntersectionObserver` to detect when this element leaves the viewport. When the element is not intersecting, show the banner.
**Alternatives considered**:
- *Scroll event listener with threshold*: Requires computing offsets, fires frequently, needs throttling. IntersectionObserver is more efficient and declarative.
- *CSS `position: sticky`*: Cannot transform the full specializations panel into a compact summary. Sticky would keep the full panel visible, which is too large on mobile.
### 2. Component placement: Rendered in App.tsx, outside the panel layout
**Decision**: Mount `` at the top of the `App` return, before the ``. It renders as `position: fixed; top: 0` so DOM order doesn't matter for visual placement, but placing it early keeps the component tree logical.
**Rationale**: The banner needs `optimizationResult.statuses` which is already available in `App`. Placing it at the App level avoids prop drilling through the specializations component.
### 3. Data flow: Pass statuses record directly
**Decision**: Pass `result.statuses` (the `Record`) to the banner component. The component counts each status category internally.
**Alternatives considered**:
- *Pass pre-computed counts*: Would reduce banner's logic but adds computation to App. The count loop over 14 entries is trivial.
- *Pass the full AllocationResult*: Over-exposes data the banner doesn't need.
### 4. Animation: CSS transition on transform
**Decision**: Use `transform: translateY(-100%)` when hidden, `translateY(0)` when visible, with a `transition: transform 200ms ease-out`. The element is always mounted when on mobile; visibility is toggled via transform.
**Alternatives considered**:
- *Conditional rendering with mount/unmount*: Cannot animate exit without additional logic (e.g., `onTransitionEnd` cleanup). Always-mounted with transform is simpler.
- *opacity fade*: Banner would still occupy space or need pointer-events toggling. Transform slide is cleaner — slides up off-screen.
### 5. Scroll-to-top behavior: `scrollIntoView` on the specializations ref
**Decision**: When the banner is tapped, call `specSectionRef.current.scrollIntoView({ behavior: 'smooth' })`. This reuses the same ref used for the IntersectionObserver.
**Rationale**: Simple, built-in browser API. No offset calculations needed since we want the section at the top of the viewport.
### 6. Styling approach: Inline styles, consistent with codebase
**Decision**: Use inline `React.CSSProperties` for the banner, matching the existing project convention. Reuse `STATUS_STYLES` colors from `SpecializationRanking.tsx` by exporting them.
**Alternative**: Could duplicate the color constants in the banner file. But exporting avoids drift if status colors change.
### 7. z-index: 1000
**Decision**: Use `z-index: 1000` for the fixed banner. This is the first fixed element in the app, so any reasonable value works. 1000 leaves room for future layering needs (modals, tooltips) without collision.
## Risks / Trade-offs
- **First `position: fixed` element**: Introduces a new positioning context. If future components also use fixed positioning, z-index conflicts could arise. → Mitigation: Document the z-index value; 1000 leaves headroom.
- **IntersectionObserver on older browsers**: Supported in all modern browsers (Safari 12.1+, Chrome 58+). EMBA students use modern devices. → No polyfill needed.
- **Banner covers page content**: Fixed element at top overlaps scrollable content. → Mitigation: The banner is compact (~40px tall) and only appears when the user has already scrolled past the specializations section, so it replaces information that's already off-screen. The slide-in animation makes the appearance non-jarring.
- **STATUS_STYLES export**: Changing the export surface of `SpecializationRanking.tsx`. → Minimal risk — it's a simple constant export, not a behavioral change.