## Context
The desktop layout in `App.tsx` today is a 2-column grid: a 340px specialization rail on the left and a 1fr right pane on the right that stacks `TopPlans` over `CourseSelection` and scrolls together. The right pane must accommodate three concerns at once — search progress, plan list, and a 12-set schedule — and ranking the specs ties up significant width that the schedule could use.
Within `CourseSelection`, each elective set renders its courses as full-width vertically stacked rows. On desktop the right pane is ~836px wide, so each row uses lots of horizontal space for one course; the schedule extends well past the viewport.
The search progress bar lives inside `TopPlans`, so when a user is scrolled down looking at the schedule it disappears.
This change reflows the desktop layout to:
1. A horizontal spec strip at the top (drag L→R to rank).
2. A hoisted, full-width progress bar.
3. A 2-column workspace below: Top Plans (left) | Schedule (right), each with independent scroll.
4. Schedule blocks render their course choices as a horizontal flex row of buttons rather than stacked rows.
Mobile (≤768px in the project's `useMediaQuery` hook) keeps its current vertical arrangement — this is a desktop-only redesign.
## Goals / Non-Goals
**Goals:**
- Eliminate the "Top Plans pushes Schedule below the fold" problem by giving each its own column with independent scroll.
- Free vertical space in the schedule by laying course choices out horizontally per elective set.
- Keep search progress visible from any scroll position by hoisting the progress bar.
- Preserve all existing semantics: pinning, recommended star, ceiling tags, info popover, cancelled/already-selected states, per-course searching, mode comparison banner, achievement count.
- Preserve mobile UX exactly — no regression in the mobile experience.
**Non-Goals:**
- No solver, worker, or data-layer changes. The decision-tree pipeline and leaf cache are untouched.
- No new features (e.g., new filters, new modes, search of plans). Pure UI reflow.
- No tablet-specific rework. Tablet is treated the same as desktop in `useMediaQuery` (returns `desktop` for ≥768px), so it inherits the new layout. No separate intermediate layout.
- The horizontal chip strip is desktop-only; mobile is not getting horizontal chips.
## Decisions
### D1: Specs go to a horizontal strip with hover popover detail
Compact 15-chip strip in a single row, each ~70px. Each chip shows rank number, 3-letter abbreviation (already in `SPECIALIZATIONS`), and a micro credit bar. Background color encodes status using the existing `STATUS_STYLES` palette.
Detail (full name, status word, allocated/threshold numeric, contributing-courses breakdown) lives in a hover popover anchored to the chip. Touch users on hybrid devices get the same popover via tap-toggle (`onClick`).
**Why not vertical-with-collapsed-detail?** Considered, but the user's mental model is "left = highest priority" — horizontal flow matches reading direction and frees the entire left rail for the workspace below. A wrap-to-2-rows variant was rejected because it breaks the left-to-right priority continuity.
**Why not include the full row content (name, numbers, status word) in each chip?** 70px is too narrow. Hover popover gets the detail without crowding the strip.
### D2: Drag strategy switches to horizontal
`@dnd-kit/sortable` is already a dependency. Switch the `SortableContext` strategy from `verticalListSortingStrategy` to `horizontalListSortingStrategy` on desktop. Mobile keeps `verticalListSortingStrategy`. We branch on `isMobile` from the existing `useMediaQuery` hook inside `SpecializationRanking`.
### D3: Progress bar hoisted into a global strip
Today `TopPlans` renders both the progress bar and the "search complete / cap hit" status text. Extract a small `SearchProgressStrip` component (or inline JSX in `App.tsx`) that consumes the same `searchProgress` and `treeLoading` state from `useAppState`. `TopPlans` keeps the static "search complete" text in its header (it pertains to the plan list specifically), but the animated progress bar moves out.
On mobile the progress bar stays inside `TopPlans` to avoid stealing precious vertical space at the top.
### D4: 2-column workspace with independent scroll
```
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
flex: 1;
min-height: 0; /* required so children can shrink and scroll */
each child: overflowY: auto; min-height: 0;
```
This is a small reuse of the existing pattern in `App.tsx` (the current right pane already uses `overflowY: auto; min-height: 0`).
**Why 1fr / 1fr (50/50)?** Plans rows render 12 mini-set buttons that benefit from width; schedule blocks render 3-5 buttons per row that also benefit from width. Neither dominates. Asymmetric splits (e.g., 40/60) showed plan mini-cards wrapping awkwardly in the 40 case.
### D5: Schedule course buttons go horizontal (flex stretch)
Each non-pinned elective set replaces its column-of-rows with:
```jsx
{courses.map(c => )}
```
Equal width per button within a set; widths vary set-to-set (3-button sets have wider buttons than 5-button sets). No flex-wrap — tested width math (109px minimum for 5-button case in a 588px column) is acceptable for the 2-line line-clamp.
**Per-button anatomy:**
- Top row: info icon (existing `i` button, top-left), recommended star (top-right when applicable).
- Middle: course name with `WebkitLineClamp: 2-3`. Title attribute carries full name for hover.
- Footer: spec-tag row (existing `SpecTag` component), allowed to wrap if a course has 4+ specs.
- Cancelled/already-selected: gray background, strikethrough name, small footer text "Cancelled" or "Already selected".
- Searching: pulsing skeleton bar where spec tags would render.
- Required-for note: small amber footer line below the spec tags.
**Pinned sets stay as today:** single line "Course Name + Clear button". Only unpinned sets get the new horizontal layout.
**Why not a CSS grid with auto-fit?** Considered `grid-template-columns: repeat(auto-fit, minmax(110px, 1fr))` which would wrap automatically. Rejected because it can produce inconsistent per-set widths that don't align with the set's actual course count, and because the courses-per-set is bounded (3–5) so flex with no wrap is simpler.
### D6: Term headings (Spring/Summer/Fall) remain as section dividers
User confirmed. They're cheap (~30px each) and orient the user temporally. No change needed.
### D7: CreditLegend collapses into the spec strip header
Today `CreditLegend` is its own block in the right pane. It would be in the way of the new column split. Rendered as a small `[▸ legend]` toggle button in the spec strip's right-aligned header area; clicking expands an inline panel below the strip with the same content the legend already provides. The component itself stays largely as-is — it's already a self-contained collapsible.
### D8: Hover popover positioning for spec chips
Anchor to the chip's bounding rect. Reuse the smart-flip logic already in `CourseInfoPopover` (`spaceBelow` / `spaceAbove`, `placeAbove`) for vertical fit. Horizontally, clamp `left` to `Math.min(rect.left, window.innerWidth - popoverWidth - 8)` so chips near the right edge don't clip the popover off-screen.
Use a 150ms close delay (same pattern as the course info popover) so the user can move the cursor from chip → popover without it disappearing.
## Risks / Trade-offs
- **Risk: 70px chip width doesn't render legibly at narrower desktop viewports (e.g., 1024px usable).** → Mitigation: the layout's `maxWidth: 1200px` is centered, so a 1024px viewport gives a smaller container. We allow horizontal scrolling on the chip strip if the strip content exceeds container width (`overflowX: auto`) as a safety valve. Cleaner option: enforce a min container width (e.g., 1200px → fall back to mobile-style vertical layout below it). Decision: take the `overflowX: auto` safety valve; the project uses a desktop breakpoint of `≥768px` and below ~1100px the chip strip will get a horizontal scrollbar on the strip element only.
- **Risk: Course buttons with 4+ spec tags overflow at narrow column widths.** → Mitigation: spec tag row uses `flex-wrap: wrap`; button heights flex-stretch so the row of buttons stays vertically aligned even when one button wraps to a 3-line spec-tag area.
- **Risk: Touch device on the desktop breakpoint (e.g., iPad in landscape, when `useMediaQuery` may treat it as desktop) has no real hover.** → Mitigation: tap on a chip toggles the popover open/closed; tap outside closes. Same pattern as `CourseInfoPopover`. Use `onClick` for parity with hover on desktop.
- **Risk: Drag-and-drop horizontal feels different from vertical and may need tweaked activation distance.** → Mitigation: keep the existing `PointerSensor` activation distance (5px). `horizontalListSortingStrategy` works out of the box for left-right reorders. Test with keyboard-driven reorder (arrow keys via `KeyboardSensor`).
- **Risk: Hoisted progress bar feels disconnected from "Top Plans" since the static "Search complete · X explored" text remains in the plan list header.** → Mitigation: keep the static text in `TopPlans` (it's a state summary), move only the animated bar. The user mental model is "progress = global; result counts per panel = local." Acceptable.
- **Risk: Mobile regression.** → Mitigation: `App.tsx` already branches on `isMobile`; we keep the mobile branch's JSX identical and only restructure the desktop branch. Spec strip, course button row, and column split are all gated behind `!isMobile`. The existing `MobileStatusBanner` and `MobileCourseBanner` continue to anchor to `specSectionRef` and `courseSectionRef` — those refs need to point to mobile-only sections, which we keep.
- **Trade-off: Lost at-a-glance numeric credit display in the spec strip.** Users wanting "5.5 / 9.0" must hover. Acceptable given the strip's information density goal.
- **Trade-off: Lost at-a-glance status text label ("Achieved", "Achievable", etc.) in the spec strip.** Background color carries this. Status word still visible in the hover popover. Color-only meets design goal but accessibility could improve later with optional text dot/glyph; not in scope here.
## Migration Plan
This is a UI-only change. No data migration, no API contract change, no breaking persistence changes (`localStorage` keys for ranking and pinned courses untouched).
Rollout: ship in v1.4.0 (minor bump per CHANGELOG convention). Single deployment; no flag gating needed since the behavior is purely visual.
If the redesign needs to be reverted, revert the App.tsx, SpecializationRanking.tsx, TopPlans.tsx, CourseSelection.tsx changes and restore the version bump. State and solver code are untouched.
## Open Questions
- None blocking implementation. Width edge cases (very narrow desktop viewports, very wide popover content) are handled by `overflowX: auto` + popover smart-flip.