/* ==========================================================================
   Timetable DESKTOP layout (≥769px) — loaded ONLY by timetable/timetable.html.

   Why a separate file: layout.css and components.css are also loaded by
   shared.html / print.html, so any live-page-only restyling added there
   leaks into the share and print views. This file makes the scoping
   structural — those pages never load it.

   Rules live inside @media (min-width: 769px) (the same desktop seam as
   layout.css:63 / mobile.css). The only rules allowed OUTSIDE the media
   query are hide-by-default rules for new live-page-only elements, so the
   mobile rendering stays byte-identical.

   Scoping hooks (added by timetable.html only):
     main.tt-page  — the <main> wrapper (via {% block page_content_class %})
     .slots-ready  — set on <main> by pages/timetable/layout-slots.js once
                     the mobile action bar's controls have been moved into
                     the desktop rows (gates hiding the bar)
     .tt-shell     — the page content wrapper div
     .tt-row       — the desktop toolbar row (#tt-toolbar); it ships `hidden`
                     and is un-hidden by layout-slots.js. .tt-actions is its
                     right-hand button cluster
     .tt-wide      — set on <main> by layout-slots.js while the WIDE band is
                     in effect (three columns; ≥1440px, or ≥1680px with
                     weekend columns). Without it the desktop page is the
                     two-column NARROW band. The media queries are CONFIG.UI.
     .tt-weekend   — set on <main> by grid-weekends.js while the weekend
                     columns are shown (it is what raises the wide threshold)
   ========================================================================== */

/* --------------------------------------------------------------------------
   Live-page-only elements hidden at ALL widths by default (the one category
   of rule allowed outside the media query — see the header note). Revealed
   desktop-only inside the media block below.
   -------------------------------------------------------------------------- */
/* The favourites row's own fields. Search results and favourites are built
   by the same component and told apart only by their container, so each of
   these is hidden at every width here and revealed under #favoritesList
   inside the media block — a search row can never pick them up. */
.course-result .result-section,
.course-result .result-room,
.course-result .result-seats,
.course-result .result-conflict {
    display: none;
}
/* Course Information's desktop-only nodes. The panel renders on mobile too
   and is not being restyled there, so each of these is ADDED beside the
   mobile node it replaces (never in place of it): the alternate row label,
   the per-meeting-pattern lines, and the Credits row. */
#courseInfo .ci-label-alt,
#courseInfo .ci-patterns,
#courseInfo .ci-credits {
    display: none;
}
/* Font Awesome's .fas display rule loads after Tailwind and beats `hidden`,
   so the right column's disclosure chevrons are hidden here instead (this
   file loads after FA). Shown desktop-only below. */
.right-panel .panel-chevron {
    display: none;
}
.course-block .block-meta {
    display: none;
}
.expert-tip-card .tips-close {
    display: none;
}
#time-format-btn .tf-switch {
    display: none;
}
.floating-label-container .search-kbd-hint {
    display: none;
}
#credit-counter-btn .gpa-label {
    display: none;
}
/* A draft tab's course count — desktop only, revealed by the toolbar rules
   in the media block. Its term · campus badge (.tab-ctx) shows at every
   width: on mobile every tab says which term and campus it is (Round B). */
.version-tab .tab-count {
    display: none;
}
/* The desktop rows ship `hidden`; the UA stylesheet already hides them —
   this makes it explicit and independent of the built Tailwind bundle. */
.tt-row[hidden] {
    display: none;
}

@media (min-width: 769px) {

    /* ----------------------------------------------------------------------
       Z-INDEX SCALE (the page's paint order — every new layer references
       this table instead of picking a number):

         0   .timetable-section — isolation:isolate. Contains every inline z
             the grid JS writes (preview 5, blocks 10, hover/highlight
             100/200, remove-btn 101, #schDays 10, conflict-indicator 100)
             inside one context that sits below all page chrome.
        30   #tt-toolbar — in the NARROW band the results overlay (45) and
             the tips popover (46) live inside its stacking context; they
             are anchored on the row so they can span the grid card. (In
             the pre-slotting fallback the .search-section itself carries
             the 30 instead.) In the WIDE band nothing floats out of it.
        50   navbar, page modals (Tailwind z-50 — chrome must stay below)
        60   toasts (#notification-container z-[60])
       The toolbar's buttons and the frame's columns have no z-index (root
       level, below everything above). Pre-existing outliers, unused/
       unreached on this page: toast.js's 9999 fallback (only mounts when
       the template container is missing) and components.css's
       #ai-floating-button 1000 (element doesn't exist).
       ---------------------------------------------------------------------- */

    /* ----------------------------------------------------------------------
       Result rows (search column / results overlay, Favorites): code, title
       (two-line clamp) and time — nothing else; the Course Information panel
       carries instructor, room and capacity. Tighter than the mobile card
       so a 17rem search column and the Favorites list show more rows. The
       Favorites row alone keeps the section beside the code (the same
       element module renders both lists; the span is hidden everywhere
       else by the outside-media block).
       ---------------------------------------------------------------------- */
    #searchResults .course-result,
    #favoritesList .course-result {
        padding: 0.6rem 0.75rem;
        margin-bottom: 0.5rem;
    }
    #searchResults .course-result .text-lg,
    #favoritesList .course-result .text-lg {
        font-size: 1rem;
        line-height: 1.4;
        margin-bottom: 0.15rem;
    }
    #searchResults .course-result .line-clamp-2,
    #favoritesList .course-result .line-clamp-2 {
        margin-bottom: 0.35rem;
    }
    #favoritesList .course-result .result-section {
        display: inline;
        font-size: 0.78rem;
        font-weight: 500;
        color: #6b7280;
    }
    .dark #favoritesList .course-result .result-section {
        color: #9ca3af;
    }

    /* ----------------------------------------------------------------------
       Favorites rows carry more than a search row, in less height. A search
       result is being scanned; a favourite is a shortlist entry, and what
       keeps it on the list is its meeting time, its room, and how many
       seats are still there — a number that moves during registration week
       while nobody is looking. So: title on one line (search keeps two),
       and a meta line under it that ellipsizes, so anything added to it
       later cannot push the row taller.
       ---------------------------------------------------------------------- */
    #favoritesList .course-result {
        margin-bottom: 0.4rem;
    }
    #favoritesList .course-result .text-lg {
        margin-bottom: 0.1rem;
    }
    #favoritesList .course-result .line-clamp-2 {
        -webkit-line-clamp: 1;
        margin-bottom: 0.2rem;
    }
    #favoritesList .course-result .result-meta {
        display: block;
        min-width: 0;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    #favoritesList .course-result .result-meta .fa-clock {
        display: none;   /* the meta line is text; the icon costs its width */
    }
    #favoritesList .course-result .result-meta span {
        display: inline;
        font-size: 0.78rem;
        font-weight: 500;
        color: #4b5563;
    }
    /* The line is one run of text now, so space-x-1's inter-child margins
       would double up with the " · " separators. Tailwind emits those as
       `> :not([hidden]) ~ :not([hidden])` (0,3,0) — an id outranks it. */
    #favoritesList .course-result .result-meta > span {
        margin-left: 0;
    }
    /* The row as a grid: the meta and conflict lines run the FULL width
       instead of sharing the text column with the two controls, which only
       ever sit beside the first line anyway. Without this, 1440 (the
       narrowest the right column gets — 340px, the wide band's minimum)
       clipped the meta line by 24px on any row whose button reads
       "Added ✓", losing the word "seats" — the one field on the line that
       has to survive.

       `display: contents` lifts the component's text wrapper out of the
       way so its three lines become grid items directly; the DOM is
       untouched, so the mobile row still renders as the plain flex box it
       always was. */
    #favoritesList .course-result > div {
        display: grid;
        grid-template-columns: minmax(0, 1fr) auto;
        column-gap: 0.5rem;
        align-items: start;
    }
    #favoritesList .course-result > div > .flex-1 {
        display: contents;
    }
    #favoritesList .course-result .text-lg,
    #favoritesList .course-result .line-clamp-2 {
        grid-column: 1;
    }
    #favoritesList .course-result .result-meta,
    #favoritesList .course-result .result-conflict {
        grid-column: 1 / -1;
    }
    /* Beside the code line and the title, not the lines under them. */
    #favoritesList .course-result .gap-3 {
        grid-column: 2;
        grid-row: 1 / 3;
    }
    .dark #favoritesList .course-result .result-meta span {
        color: #9ca3af;
    }
    /* A full section's "Full" (UI-30): the one word on the meta line that is
       a status. The span rule above out-ranks its Tailwind red and would grey
       it with the rest of the line. The red of the conflict line below. */
    #favoritesList .course-result .result-meta [data-status='danger'] {
        font-weight: 600;
        color: #dc2626;
    }
    .dark #favoritesList .course-result .result-meta [data-status='danger'] {
        color: #f87171;
    }

    /* Why this favourite is still a candidate — it clashes with the draft as
       it stands. Only the favourites renderer computes it. */
    #favoritesList .course-result .result-conflict {
        display: block;
        margin-top: 0.15rem;
        font-size: 0.72rem;
        font-weight: 600;
        color: #dc2626;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    .dark #favoritesList .course-result .result-conflict {
        color: #f87171;
    }

    /* ----------------------------------------------------------------------
       Page chrome. <main> is a plain block again (the rail is gone); the
       content frame below centres itself inside the shell.

       The mobile action bar is hidden only once layout-slots.js has moved
       its controls into the toolbar row (`slots-ready` on <main>). Without that
       class — before the module runs, or if its desktop pass failed — the
       bar renders as layout.css's horizontal sticky bar with every control
       still inside it, so the page can never lose its controls.

       The shell clips horizontal overflow only. The rows are built so their
       content shrinks instead of growing past the frame (#draft-tabs-strip
       below), and this clip is the guarantee that no horizontal page scroll
       can ever appear; overflow-y stays visible for the popovers that hang
       below the rows (`overflow-x: clip` + `overflow-y: visible` is a valid
       pair and creates no scroll container, so nothing vertical is
       clipped). The fixed-position modals inside the shell are unaffected:
       their containing block is the viewport.
       ---------------------------------------------------------------------- */
    main.tt-page.slots-ready > #timetable-bar {
        display: none;
    }
    main.tt-page > .tt-shell {
        overflow-x: clip;
        /* 1rem per side instead of Tailwind's lg:px-8: the frame centres
           itself, so the padding only ever binds at the band thresholds —
           where 32px is the difference between a fed and a starved card. */
        padding-inline: 1rem;
    }

    /* ----------------------------------------------------------------------
       Toolbar controls — the mobile bar's buttons, slotted into the row's
       .tt-actions cluster by layout-slots.js. They keep layout.css's base
       .tb-action-btn look (white pill, border, 0.8rem); only emphasis and
       the two special controls are restyled here. Every button is rigid
       (flex-shrink:0); the draft strip (and, in the narrow band, the search
       pill) are the row items that flex.
       ---------------------------------------------------------------------- */
    main.tt-page #import-btn,
    main.tt-page .tb-download,
    main.tt-page #share-btn,
    main.tt-page #add-temp-course-btn,
    main.tt-page #credit-counter-btn,
    main.tt-page #clear-all-btn {
        flex-shrink: 0;
    }
    /* The action cluster sits at the row's right end; its buttons keep DOM
       order (Add Course first, Clear All last). It never shrinks — the draft
       strip and, in the narrow band, the search pill are the flexible items —
       so toolbar-overflow.js can read what it needs from its width. */
    #tt-toolbar > .tt-actions {
        display: flex;
        align-items: center;
        gap: 0.5rem;
        margin-left: auto;
        flex-shrink: 0;
    }
    /* Too short even for the icons beside the active draft tab (UI-21):
       toolbar-overflow.js gives the cluster a line of its own, right-aligned
       under the strip and the pill, with its labels back. */
    #tt-toolbar.is-wrapped {
        flex-wrap: wrap;
    }
    #tt-toolbar.is-wrapped > .tt-actions {
        flex-basis: 100%;
        justify-content: flex-end;
    }
    /* Lines are filled by each item's natural width, and the strip's is every
       tab: capped beside the pill's floor (and the gap) so the two share the
       first line and the strip's tabs scroll, instead of the pill wrapping
       onto a line of its own. */
    #tt-toolbar.is-wrapped:has(> .search-section) > #draft-tabs-strip {
        max-width: calc(100% - 24.5rem - 0.5rem);
    }

    /* Import, Download and Share are one set (UI-16): one bordered box, the
       three buttons joined by hairlines, so the row reads as Add Course, the
       file set, the GPA figure and the menu instead of six equal pills. The
       box carries the border the buttons give up, so its height is a
       button's. An empty box (before layout-slots.js has dealt the buttons
       in) draws nothing. */
    #tt-toolbar .tt-group {
        display: inline-flex;
        align-items: stretch;
        flex-shrink: 0;
        border: 1px solid var(--color-border);
        border-radius: 0.5rem;
        background: white;
    }
    #tt-toolbar .tt-group:empty {
        display: none;
    }
    main.tt-page #tt-toolbar .tt-group > .tb-action-btn {
        border: 0;
        border-radius: 0;
        background: transparent;
    }
    main.tt-page #tt-toolbar .tt-group > .tb-action-btn:first-child {
        border-radius: calc(0.5rem - 1px) 0 0 calc(0.5rem - 1px);
    }
    main.tt-page #tt-toolbar .tt-group > .tb-action-btn:last-child {
        border-radius: 0 calc(0.5rem - 1px) calc(0.5rem - 1px) 0;
    }
    main.tt-page #tt-toolbar .tt-group > .tb-action-btn + .tb-action-btn {
        border-left: 1px solid var(--color-border);
    }
    main.tt-page #tt-toolbar .tt-group > .tb-action-btn:hover {
        background: #f3f4f6;
    }
    /* Short of width, the set and Clear All drop their labels
       (toolbar-overflow.js adds .is-compact): four icons, each still named
       by its title. An armed Clear All keeps its "Confirm?" so the second
       press is never a guess. */
    main.tt-page #tt-toolbar .tt-group.is-compact > .tb-action-btn > span,
    main.tt-page #tt-toolbar #clear-all-btn.is-compact:not(.confirming) > .clear-label {
        display: none;
    }
    /* In the same step "Custom course" reads "Custom" (UI-30): it keeps a
       label, the short one. The full name is 20px wider than the "Add Course"
       it replaced, and at full length the row wrapped at 1040px where it used
       to hold to 1020px, a 1024px window included. */
    main.tt-page #tt-toolbar #add-temp-course-btn.is-compact .label-rest {
        display: none;
    }
    /* A focus ring drawn inside the button: the neighbours would cover one
       drawn outside it. */
    main.tt-page #tt-toolbar .tt-group > .tb-action-btn:focus-visible {
        outline: 2px solid var(--color-primary);
        outline-offset: -2px;
    }
    .dark #tt-toolbar .tt-group {
        background: rgba(31, 41, 55, 0.7);
        border-color: rgba(75, 85, 99, 0.4);
    }
    .dark main.tt-page #tt-toolbar .tt-group > .tb-action-btn + .tb-action-btn {
        border-left-color: rgba(75, 85, 99, 0.4);
    }
    .dark main.tt-page #tt-toolbar .tt-group > .tb-action-btn:hover {
        background: rgba(55, 65, 81, 0.9);
    }

    /* Emphasis inversion: Add Course is the PRIMARY action; the GPA control
       is a stat that happens to open the GPA modal (still the button
       tests/smoke_local.py flow 4 clicks). Overrides .tb-action-btn.primary's
       solid fill desktop-only; the mobile bar keeps the original emphasis. */
    main.tt-page #credit-counter-btn {
        background: white;
        border-color: #c7ebff;
        color: var(--color-primary);
    }
    main.tt-page #credit-counter-btn:hover {
        background: #f5fbff;
        border-color: #9edcff;
    }
    /* "GPA · 12 cr": the GPA label leads and the credit total is its
       secondary figure; the mobile "Credits:" text is hidden here. */
    main.tt-page #credit-counter-btn .credits-label {
        display: none;
    }
    main.tt-page #credit-counter-btn .gpa-label {
        display: inline;
        font-weight: 600;
    }
    main.tt-page #credit-counter-btn #credit-count {
        color: #6b7280;
    }
    main.tt-page #credit-counter-btn #credit-count::after {
        content: ' cr';
    }
    main.tt-page #add-temp-course-btn {
        background: var(--color-primary);
        border-color: var(--color-primary);
        color: white;
    }
    main.tt-page #add-temp-course-btn:hover {
        background: #20719c;
        border-color: #20719c;
    }

    /* Clear All: red text, no pill, last on the row at every desktop width
       (UI-21), and dressed the same in the fallback bar when the slotting
       pass has not run. Its 2-click confirm state (.confirming, layout.css:
       the first press reads "Confirm?", the second clears) is restated at
       this specificity so the id selector above can't flatten it. */
    main.tt-page #clear-all-btn {
        background: transparent;
        border-color: transparent;
        color: #dc2626;
    }
    main.tt-page #clear-all-btn:hover {
        background: #fef2f2;
        border-color: #fecaca;
    }
    main.tt-page #clear-all-btn.confirming {
        background: #fef2f2;
        border-color: #fca5a5;
        color: #dc2626;
    }
    main.tt-page #clear-all-btn.confirming:hover {
        background: #fee2e2;
        border-color: #f87171;
    }

    /* Time format: the bare "12h" label was ambiguous (current mode or
       target?). Desktop shows a two-cell switch — ONE control, a shared
       track whose filled cell marks the CURRENT format; clicking anywhere
       on the button toggles, as before. applyTimeFormat() moves .tf-active. */
    main.tt-page #time-format-label {
        display: none;
    }
    main.tt-page #time-format-btn .tf-switch {
        display: inline-flex;
        border: 1px solid var(--color-border);
        border-radius: 0.375rem;
        overflow: hidden;
        background: #f3f4f6;
    }
    main.tt-page #time-format-btn .tf-seg {
        display: block;
        font-size: 0.6rem;
        line-height: 1.5;
        padding: 0.08rem 0.3rem;
        color: #9ca3af;
    }
    main.tt-page #time-format-btn .tf-seg.tf-active {
        background: var(--color-primary);
        color: white;
    }

    /* ----------------------------------------------------------------------
       Content frame — this is layout.css's .container (overridden here, never
       edited; the two-class selector outranks it and this file loads later).
       A fit-content grid sized AROUND the fixed 675px canvas instead of the
       canvas floating inside a fluid track:
         · column 1 is minmax(0, max-content): sized by the timetable card,
           so the card is exactly canvas + scrollbar gutter + card padding
           and no dead space can open inside it; the 0 minimum lets it
           shrink when the frame can't fit;
         · column 2 (the right column) is clamp(380px, 26vw, 480px);
         · rows: the toolbar (spanning every column), then the grid card |
           right column row taking the rest;
         · width: fit-content = the sum of the tracks, centred by the auto
           margins. max-width:100% lets the frame get NARROWER than that on
           small desktops — the card track then shrinks and the canvas clips
           on the right (today's failure mode), never a horizontal page
           scroll (free space is distributed equally up to each track's
           limit, so the right column keeps its clamp and the card gives).
       Height: layout.css's calc(100vh - 200px) covered the navbar reserve
       (main pt-24 = 96) + the shell's py-4 (32) + 72px of slack left behind
       by the old horizontal action bar; 144 keeps only the real neighbours.
       min-height: layout.css's 800px floor is dropped HERE, in BOTH bands.
       It outlives the height it was written for: with it the frame is 800
       tall whatever the viewport is, so on any screen shorter than 944
       (800 + 144) the frame's bottom — and with it the right column's —
       falls below the fold: 12px at 1366x900, 144px at 1366x768, i.e. on
       exactly the 1366 laptops the narrow band exists for. The grid card
       already scrolls internally, so a shorter frame costs visible hours,
       never content. layout.css is untouched, so the print and shared
       views keep their own floor.
       ---------------------------------------------------------------------- */
    .tt-shell .main-content-grid {
        display: grid;
        grid-template-columns: minmax(0, max-content) clamp(380px, 26vw, 480px);
        grid-template-rows: auto minmax(0, 1fr);
        gap: 1rem;
        width: fit-content;
        max-width: 100%;
        height: calc(100vh - 144px);
        min-height: 0;
        margin-inline: auto;
        padding: 0;
        justify-content: start;
    }
    /* Neutralize the md:space-x-4 sibling margins — grid gap replaces them.
       Tailwind emits them as `> :not([hidden]) ~ :not([hidden])` (0,3,0),
       which outranks a plain `> *` (0,2,0) — so that rule alone never won
       and every non-first child kept a 16px margin-left on desktop (the
       card sat 32px from the frame edge, the panel 32px from the card).
       Mirror the selector to beat it on specificity. */
    .tt-shell .main-content-grid > * {
        margin: 0;
    }
    .tt-shell .main-content-grid > :not([hidden]) ~ :not([hidden]) {
        margin: 0;
    }

    /* The toolbar row (.tt-row). It ships `hidden` in the template;
       layout-slots.js un-hides it once it has moved the controls in (the
       :not([hidden]) guard keeps a still-hidden row at display:none on
       desktop too). contain:inline-size — a fit-content grid takes the width
       of its WIDEST row; without it the row's natural width (the draft
       pills + search + six buttons) would silently widen the
       card track and reopen the dead space this frame exists to close. The
       row sizes to the tracks instead and its content shrinks to fit (see
       #draft-tabs-strip). Inline-size containment creates no containing
       block, so the popovers anchored inside still overflow the row. No
       overflow:hidden here — it would clip the narrow band's results
       overlay and the tips popover; the shell's overflow-x:clip is the
       page-scroll guarantee. */
    .tt-shell .main-content-grid > .tt-row:not([hidden]) {
        display: flex;
        align-items: center;
        gap: 0.5rem;
        min-width: 0;
        contain: inline-size;
    }
    .tt-shell .main-content-grid > #tt-toolbar {
        grid-row: 1;
        grid-column: 1 / -1;
    }
    /* Before layout-slots.js runs — and if its desktop pass fails — the
       search pill is still a direct child of the frame. It is deliberately
       NOT placed: grid auto-placement drops it into row 1's free cell, where
       the toolbar would be, so the page is coherent either way without a
       spare row whose gap would cost 16px in the normal state. */
    /* Narrow band: the search pill takes the flexible middle of the toolbar
       (Tailwind's order-1 is again the mobile order). Anchor for the results
       overlay + tips popover; the overlay floats above the grid card. */
    main.tt-page .search-section {
        position: relative;
        height: auto;
        width: auto;
        margin: 0;
        order: 0;
        z-index: 30;
    }
    #tt-toolbar > .search-section {
        /* Basis = floor: the pill grows into whatever is free and never
           competes with the draft strip for shrink space (a larger basis
           made flexbox squeeze both proportionally, ellipsizing tab names
           while the pill still had room). toolbar-overflow.js budgets the
           pill at exactly this floor. 19rem since Round B: it is where the
           placeholder still reads "Search courses..." beside the / hint, the
           submit and the tips button -- the old 12rem showed "Se", and with
           every draft a tab the pill sits at its floor far more often. The
           tabs scroll now, so they are the ones that give way. 24.5rem
           since UI-18: the same 19rem for the box, plus the Filters button
           with its count showing (measured 78px) and the gap before it. */
        flex: 1 1 24.5rem;
        min-width: 24.5rem;
        /* Inside the row the SECTION is not the anchor any more: the row is,
           so the results overlay and the tips popover can span the grid
           card rather than the pill (see the overlay block below). */
        position: static;
        z-index: auto;
    }
    .tt-shell .main-content-grid > #tt-toolbar:not([hidden]) {
        position: relative;   /* anchor for the results overlay + tips popover */
        z-index: 30;
    }
    .tt-shell .main-content-grid > .timetable-section {
        grid-area: 2 / 1 / 3 / 2;
        min-width: 0;
        min-height: 0;
        height: 100%;
        width: auto;
        /* Level 0 of the z-scale: the grid JS writes inline z-index values
           up to 200 on blocks (hover/highlight) which otherwise compete at
           ROOT level and paint over the results overlay (45) and popovers
           (46). Isolating the section clamps all of them beneath the chrome.
           Audited: every z/positioned descendant is grid-internal — the six
           modals and the toast mount are siblings outside this section. */
        isolation: isolate;
    }
    .tt-shell .main-content-grid > .right-panel {
        grid-area: 2 / 2 / 3 / 3;
        min-height: 0;
        height: auto;
        /* The template's md:w-80 (20rem) is an explicit width, which turns
           off justify-self:stretch — the panel would sit 320px wide inside
           the clamp() track. Let the track size it. */
        width: auto;
    }

    /* ----------------------------------------------------------------------
       WIDE band (main.tt-page.tt-wide — set by layout-slots.js at ≥1440px,
       or ≥1680px while weekend columns are shown): three columns,
       search | grid card | right column.
         · The card track stays minmax(0, max-content) and both side tracks
           are minmax(<min>, 1fr). Measured in Chromium: the max-content
           track is fed FIRST (737px at 1440 with both sides at their
           minima) and only then is the remainder shared equally between the
           fr sides — a fixed-max side track (minmax(17rem, 30rem)) would
           split the free space three ways and starve the card instead.
         · width:100%, not fit-content — with fr sides a fit-content frame
           collapses to the minima (1449px wide at 1920).
         · max-width is the sides' cap (fr tracks have no per-track cap):
           1720 = 737 card + 32 gaps + 2 × 475.5.
         · Minima: 17rem search (code + two-line title + time beside the
           Add/star controls), 21.25rem right column (the Favorites item's
           text column stays ≈190px). 1440 is where both fit beside the 737
           card with a 16px page scrollbar (frame 1392 − 769 = 623 ≥ 612);
           1366 is 63px short, which is why it keeps the two-column frame.
         · min-height:0 comes from the base frame rule now (both bands fit
           the viewport; the grid and both columns scroll internally).
       Without the class (pre-JS, or a failed slotting pass) the page is the
       two-column frame above — the intended degraded state.
       ---------------------------------------------------------------------- */
    main.tt-page.tt-wide .main-content-grid {
        grid-template-columns: minmax(17rem, 1fr) minmax(0, max-content) minmax(21.25rem, 1fr);
        width: 100%;
        max-width: 107.5rem;
        justify-content: center;
    }
    main.tt-page.tt-wide .main-content-grid > .search-section {
        grid-area: 2 / 1 / 3 / 2;
        min-width: 0;
        min-height: 0;
    }
    main.tt-page.tt-wide .main-content-grid > .timetable-section {
        grid-area: 2 / 2 / 3 / 3;
    }
    main.tt-page.tt-wide .main-content-grid > .right-panel {
        grid-area: 2 / 3 / 3 / 4;
    }

    /* ----------------------------------------------------------------------
       Toolbar, far left: the draft strip leaves the grid card
       (layout-slots.js) and loses its card-internal divider. It is the ONE
       row item allowed to shrink — to nothing if it must — clipping its own
       content (it holds no popovers); every button and the search floor are
       rigid. Every button stays on the row (UI-21): when the cluster, the
       search floor and the active tab cannot share one line,
       toolbar-overflow.js first drops labels and then puts the cluster on a
       second line (.is-wrapped), so the row is never wider than the frame.

       The tabs themselves never shrink (Round B). Every draft in every term
       is a tab now, and squeezed tabs cut the names down to a letter or two;
       instead they keep their width and #timetable-tabs scrolls sideways
       when they do not fit, the + button staying outside the scroller.
       timetable-versions-handler.js keeps the active tab in view and turns a
       mouse wheel sideways over the strip. The block padding keeps a tab's
       focus ring inside the scroller's clip.
       ---------------------------------------------------------------------- */
    #tt-toolbar > #draft-tabs-strip {
        flex: 0 1 auto;
        min-width: 0;
        overflow: hidden;
        flex-wrap: nowrap;
        padding-bottom: 0;
        margin-bottom: 0;
        border-bottom: 0;
    }
    #tt-toolbar #timetable-tabs {
        flex: 0 1 auto;
        flex-wrap: nowrap;
        min-width: 0;
        overflow-x: auto;
        overflow-y: hidden;
        overscroll-behavior-x: contain;
        scrollbar-width: thin;
        padding-block: 2px;
    }
    #tt-toolbar .version-tab,
    #tt-toolbar #new-timetable-btn {
        flex: 0 0 auto;
    }
    /* Tab badges (built by renderVersionTabs; both carry .tab-context's look
       from layout.css / dark-mode.css). The toolbar shows ONE per tab: the
       term · campus on the ACTIVE tab — the only place the page states its
       term and campus — and on a tab from another term or campus
       (.tab-elsewhere); the course count on a tab beside the active one in
       its own term and campus. */
    #tt-toolbar .version-tab:not(.active):not(.tab-elsewhere) .tab-ctx {
        display: none;
    }
    #tt-toolbar .version-tab:not(.active):not(.tab-elsewhere) .tab-count {
        display: inline;
    }


    /* ----------------------------------------------------------------------
       Search pill (narrow band, in the toolbar): the old full-height card
       collapses to one slim row; the input fills it from the left (the old
       right-aligned 15rem field that grew on focus left dead space at the
       pill's start).
       ---------------------------------------------------------------------- */
    .search-section > .modern-card {
        height: auto;
        flex-direction: row;
        align-items: center;
        justify-content: flex-start;
        gap: 0.5rem;
        padding: 0.35rem 0.75rem;
        /* The whole pill is a click target (search-overlay.js focuses the
           input); advertise it. */
        cursor: text;
    }
    /* The pill must not be a containing block, or the overlay anchors on it
       instead of on the toolbar: components.css gives .modern-card a
       backdrop-filter (a backdrop filter establishes a containing block for
       absolute descendants, like transform does) and a hover translate.
       Both off in the row; the pill is 95% opaque, the blur was invisible. */
    #tt-toolbar .search-section > .modern-card {
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }
    #tt-toolbar .search-section > .modern-card:hover {
        transform: none;
    }
    /* The floating placeholder must not wrap when the pill sits at its
       19rem floor: one line, clipped short of the kbd hint and buttons. */
    #tt-toolbar .floating-label {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        max-width: calc(100% - 5.5rem);
    }
    /* Slimmer input in the row (py-3 + .modern-input's 12px -> 8px). */
    #tt-toolbar .modern-search-input {
        padding-top: 0.5rem;
        padding-bottom: 0.5rem;
    }
    .search-section #searchForm {
        margin-bottom: 0;
        flex: 1 1 auto;
        min-width: 0;
        width: auto;
        max-width: none;
    }

    /* WIDE band: the search section is a full-height column card — the form
       on top, the tips toggle under it, the results filling the rest and
       scrolling inside the card (the template's flex-1 / min-h-0 /
       overflow-y-auto on #searchResults do that once the card is a column
       again). No z-index: nothing floats out of the column. */
    main.tt-page.tt-wide .search-section {
        z-index: auto;
    }
    main.tt-page.tt-wide .search-section > .modern-card {
        flex-direction: column;
        align-items: stretch;
        height: 100%;
        min-height: 0;
        padding: 0.75rem;
        gap: 0.5rem;
        cursor: default;
    }
    main.tt-page.tt-wide .search-section #searchForm {
        flex: 0 0 auto;
    }
    main.tt-page.tt-wide #search-tips-btn {
        align-self: flex-end;
        margin-top: -0.25rem;
    }

    /* "/" shortcut hint — a small kbd chip left of the clear/search buttons.
       Hidden while typing or focused (same sibling trick layout.css uses for
       the floating label), and inert to clicks. */
    .floating-label-container .search-kbd-hint {
        display: inline-block;
        position: absolute;
        right: 4.25rem;
        top: 50%;
        transform: translateY(-50%);
        padding: 0.05rem 0.45rem;
        border: 1px solid var(--color-border);
        border-radius: 0.375rem;
        font-size: 0.72rem;
        line-height: 1.3;
        font-family: inherit;
        color: #9ca3af;
        background: #f9fafb;
        pointer-events: none;
    }
    .floating-label-container input:focus ~ .search-kbd-hint,
    .floating-label-container input:not(:placeholder-shown) ~ .search-kbd-hint {
        display: none;
    }
    .dark .floating-label-container .search-kbd-hint {
        background: #111827;
        border-color: #374151;
        color: #6b7280;
    }
    /* The idle empty-state placeholder lives inside #searchResults, which is
       hidden until the overlay opens — the row stays slim. */

    /* ----------------------------------------------------------------------
       Results overlay (narrow band): floats under the toolbar row and spans
       exactly the GRID CARD —
       its containing block is the row (position:relative above; the pill
       is static and its hover lift is off, so nothing in between can become
       the containing block), left:0 is the card's left edge and the right
       inset is the right column's clamp() plus the grid gap. Results are
       the primary add-course surface; the old pill-wide panel was narrower
       than the card at every common width. In the pre-slotting fallback
       the section spans both columns itself, so the same inset holds.
       #searchResults itself is the node handed to initRender —
       hidden/shown in place, never replaced.
       ---------------------------------------------------------------------- */
    .search-section #searchResults {
        display: none;
        position: absolute;
        top: 100%;
        left: 0;
        right: calc(clamp(380px, 26vw, 480px) + 1rem);
        margin-top: 0.5rem;
        z-index: 45;
        background: white;
        border: 1px solid var(--color-border);
        border-radius: 0.75rem;
        box-shadow: 0 12px 32px rgba(15, 23, 42, 0.18);
        padding: 0.75rem;
        overflow-y: auto;
        max-height: 24rem; /* fallback — JS fits to 3.5 rows when it can */
    }
    .search-section.overlay-open #searchResults {
        display: block;
    }
    .dark .search-section #searchResults {
        background: #1f2937;
        border-color: #374151;
    }
    /* WIDE band: the results are inline — an ordinary scrolling child of the
       column card, never a floating panel. #searchResults is still the
       initRender root; only its placement changes, so the idle "Ready to
       Search" state now shows in the column. (search-overlay.js never adds
       overlay-open in this band; this rule makes the CSS agree even if it
       did.) */
    main.tt-page.tt-wide .search-section #searchResults {
        display: block;
        position: static;
        margin-top: 0;
        padding: 0;
        border: 0;
        border-radius: 0;
        box-shadow: none;
        background: transparent;
        max-height: none;
    }

    /* ----------------------------------------------------------------------
       Search filters (UI-18): the chips' base look is components.css's; this
       places the strip in each desktop band.

       NARROW band (and the pre-slotting fallback): the search is a one-row
       pill, so the strip cannot sit under the box. It hangs under the
       toolbar row instead, spanning the grid card like the results overlay
       (same containing block, same insets), and shows in two cases: with
       the results (.overlay-open), so a list can be narrowed where it is
       read, and when the Filters button in the pill is pressed
       (.filters-open), so a search can start from a filter alone. The
       results then hang under the strip: search-overlay.js writes the
       strip's height into --sf-h. The button also says how many filters are
       on, since the strip is out of sight while the student is not
       searching.

       WIDE band: the strip is inline under the box in the search column and
       always shown, as on a phone; the button is not needed.
       ---------------------------------------------------------------------- */
    .search-section .search-filters-btn {
        display: inline-flex;
        align-items: center;
        gap: 0.3rem;
        flex-shrink: 0;
        height: 2rem;
        padding: 0 0.625rem;
        border: 1px solid #d1d5db;
        border-radius: 0.5rem;
        background: white;
        color: #374151;
        font-size: 0.75rem;
        font-weight: 600;
        cursor: pointer;
    }
    .search-section .search-filters-btn:hover {
        border-color: #9ca3af;
    }
    .search-section .search-filters-btn:focus-visible {
        outline: 2px solid var(--color-primary);
        outline-offset: 2px;
    }
    .search-section.filters-open .search-filters-btn {
        border-color: var(--color-primary);
    }
    .search-section .search-filters-btn .sf-count {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 1.125rem;
        height: 1.125rem;
        padding: 0 0.25rem;
        border-radius: 9999px;
        background: #0b1f33;
        color: #fff;
        font-size: 0.6875rem;
        line-height: 1;
    }
    .dark .search-section .search-filters-btn {
        background: rgba(31, 41, 55, 0.7);
        border-color: rgba(75, 85, 99, 0.6);
        color: #e5e7eb;
    }
    .dark .search-section .search-filters-btn .sf-count {
        background: #27577f;
    }
    .search-section .search-filters {
        display: none;
        position: absolute;
        top: 100%;
        left: 0;
        right: calc(clamp(380px, 26vw, 480px) + 1rem);
        margin: 0.5rem 0 0;
        padding: 0.5rem 0.75rem;
        z-index: 45;
        background: white;
        border: 1px solid var(--color-border);
        border-radius: 0.75rem;
        box-shadow: 0 12px 32px rgba(15, 23, 42, 0.18);
    }
    .search-section.filters-open .search-filters,
    .search-section.overlay-open .search-filters {
        display: flex;
    }
    /* A mouse is the pointer here: the chips can be a little smaller than a
       phone's. */
    .search-section .sf-chip,
    .search-section .sf-clear {
        height: 1.75rem;
    }
    .search-section .sf-chip.sf-day {
        min-width: 1.75rem;
    }
    /* With the strip showing, the results hang under it. The fallback is the
       strip's two rows (UI-40): 2 x 1.75rem chips, the 0.375rem gap between
       them, 2 x 0.5rem padding and the 2px border. */
    .search-section.overlay-open #searchResults {
        margin-top: calc(0.5rem + var(--sf-h, 5rem) + 0.375rem);
    }
    .dark .search-section .search-filters {
        background: #1f2937;
        border-color: #374151;
    }
    main.tt-page.tt-wide .search-section .search-filters-btn {
        display: none;
    }
    main.tt-page.tt-wide .search-section .search-filters {
        display: flex;
        position: static;
        margin: 0;
        padding: 0;
        border: 0;
        border-radius: 0;
        box-shadow: none;
        background: transparent;
    }

    /* ----------------------------------------------------------------------
       Expert Search Tips: no longer a permanent card — a popover toggled by
       #search-tips-btn (.tips-open on .search-section). Its content is
       forced open inside the popover; the !importants beat the accordion's
       inline max-height/opacity toggles, which still drive the MOBILE
       accordion unchanged.
       ---------------------------------------------------------------------- */
    .search-section .expert-tip-card {
        display: none;
        position: absolute;
        top: 100%;
        right: calc(clamp(380px, 26vw, 480px) + 1rem);  /* the card's right edge */
        width: 24rem;
        max-width: 100%;
        margin: 0.5rem 0 0;
        z-index: 46; /* tips popover level of the z-scale */
        box-shadow: 0 12px 32px rgba(15, 23, 42, 0.18);
        /* Guaranteed-opaque surface UNDER the card's Tailwind gradient
           (background-image) — the popover floats over the grid and must
           never let it show through. */
        background-color: #fff;
        border: 1px solid var(--color-border);
    }
    .search-section.tips-open .expert-tip-card {
        display: block;
    }
    .search-section.tips-open .tips-content {
        max-height: none !important;
        opacity: 1 !important;
    }
    .search-section.tips-open #tips-header {
        pointer-events: none;
    }
    .search-section.tips-open #tips-chevron {
        display: none;
    }
    /* Close X: revealed only in the popover; pointer-events re-enabled under
       the header's pointer-events:none (the mobile accordion header must
       stay inert while the popover is open, but its X must not). */
    .search-section .expert-tip-card .tips-close {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 1.5rem;
        height: 1.5rem;
        border-radius: 0.375rem;
        color: #6b7280;
    }
    .search-section.tips-open .expert-tip-card .tips-close {
        pointer-events: auto;
    }
    .search-section .expert-tip-card .tips-close:hover {
        background: rgba(35, 135, 186, 0.12);
        color: #374151;
    }
    .dark .search-section .expert-tip-card {
        background-color: #1f2937;
        border-color: #374151;
    }
    .dark .search-section .expert-tip-card .tips-close {
        color: #9ca3af;
    }
    .dark .search-section .expert-tip-card .tips-close:hover {
        color: #e5e7eb;
    }
    /* WIDE band: the tips card opens inline under the form and pushes the
       results down — the column has the room, and nothing floats over the
       grid. The .tips-open toggling above is unchanged. */
    main.tt-page.tt-wide .search-section .expert-tip-card {
        position: static;
        width: auto;
        max-width: none;
        margin: 0;
        box-shadow: none;
    }

    /* ----------------------------------------------------------------------
       12h/24h in the grid's corner cell — the empty cell above the time
       gutter, left of Monday (canvas x 1-75, y 1-30: Monday's header sits at
       left:85px minus its 10px margin, the header row is 30px tall).
       layout-slots.js prepends the button into the grid card; it is placed
       over the corner from OUTSIDE the scroll container, so it never scrolls
       — matching the sticky header row. The corner itself has no background
       (#schDays is zero-height; only the headers paint white), so the
       toggle brings the grid's own surface. z-index 11 sits above #schDays
       (10) inside the section's isolated context; no course block can reach
       x < 100. The two-cell .tf-switch is the whole visible control.
       It starts AT the scroll container's edge (card p-6 = 24px), one pixel
       outside the canvas border, and redraws that border corner itself
       (#ddd = layout.css's #schTimes border): a scrolled time label is
       clipped at the container edge, and a toggle that began one pixel
       lower left a hairline of it visible above the switch.
       ---------------------------------------------------------------------- */
    .timetable-section > .bg-white {
        position: relative;
    }
    .timetable-section > .bg-white > #time-format-btn {
        position: absolute;
        top: 24px;
        left: 24px;
        width: 76px;               /* 1px border + the 75px corner cell */
        height: 31px;              /* 1px border + the 30px header row */
        z-index: 11;
        justify-content: center;
        padding: 0;
        border: 0;
        border-top: 1px solid #ddd;
        border-left: 1px solid #ddd;
        border-right: 1px solid var(--color-border);
        border-radius: 0;
        background: white;
    }
    .timetable-section > .bg-white > #time-format-btn:hover {
        background: #f3f4f6;
    }
    .timetable-section > .bg-white > #time-format-btn .fa-clock {
        display: none;
    }
    /* border-color too (UI-20): the corner redraws the canvas's #ddd border
       and its own right edge, and both stayed near-white on a dark page. The
       colour dark-mode.css gives the canvas border and the grid lines. */
    .dark .timetable-section > .bg-white > #time-format-btn {
        background: #1f2937;
        border-color: rgba(75, 85, 99, 0.5);
    }
    .dark .timetable-section > .bg-white > #time-format-btn:hover {
        background: #111827;
    }

    /* ----------------------------------------------------------------------
       The week always fits its card (UI-26). From 769px to about 1150px the
       card is narrower than the 675px canvas (915px with the weekend
       columns), and Thursday and Friday were cut off with nothing to scroll.
       pages/timetable.js applyGridFit() measures the room and, when the
       canvas does not fit, sets --tt-zoom and .tt-fit on the section. Where
       it fits it sets nothing: no rule below matches, and those widths are
       pixel for pixel what they were.
       `zoom`, not `transform`: zoom shrinks the box the canvas takes, so the
       scroll range shrinks with it and the sticky header row keeps working.
       A transform leaves the full-size box behind (phones get away with one
       because there the page scrolls, not the container).
       The two kinds of 1px line are sized to be one pixel AFTER the zoom: a
       0.6px line is rounded away at some positions and kept at others.
       ---------------------------------------------------------------------- */
    .timetable-section.tt-fit #schTimes {
        zoom: var(--tt-zoom);
    }
    .timetable-section.tt-fit #schTimes .hour-line {
        height: calc(1px / var(--tt-zoom));
    }
    .timetable-section.tt-fit #schTimes .day-line {
        width: calc(1px / var(--tt-zoom));
    }
    /* The corner cell shrank with the canvas: 75 x 30, zoomed, plus the one
       pixel of border the button redraws itself, which does not zoom. */
    .timetable-section.tt-fit > .bg-white > #time-format-btn {
        width: calc(75px * var(--tt-zoom) + 1px);
        height: calc(30px * var(--tt-zoom) + 1px);
    }
    .timetable-section.tt-fit > .bg-white > #time-format-btn .tf-switch {
        zoom: var(--tt-zoom);
    }

    /* ----------------------------------------------------------------------
       Grid blocks: AA-contrast text (--block-fg set per block by
       course-block.js from the block's own background), section+room meta,
       and a PERSISTENT conflict treatment — outline plus hatching, never
       color alone. Scoped to #schTimes in this live-page-only file, so the
       print/shared/static-grid renderings are untouched.
       ---------------------------------------------------------------------- */
    #schTimes .course-block {
        color: var(--block-fg, white);
    }
    .course-block .block-meta {
        display: block;
        font-size: 0.62rem;
        font-weight: 500;
        line-height: 1.2;
        margin-top: 2px;
        opacity: 0.85;
    }
    #schTimes .course-block.tt-conflict {
        outline: 2px solid var(--color-danger);
        outline-offset: -2px;
        background-image: repeating-linear-gradient(
            45deg,
            rgba(220, 38, 38, 0.18) 0 6px,
            transparent 6px 12px
        );
    }

    /* ----------------------------------------------------------------------
       Right column: Course Information + Favorites read as ONE card with two
       collapsible sections (disclosure.js toggles .panel-collapsed).

       HEIGHT MODEL — Favorites has the floor and Course Information yields,
       not the other way round. Before this, Course Information won every
       time: its min-height resolves to `auto` (= min-content, its overflow
       being visible), so a selected course pinned the card at its full 467px
       while Favorites — `flex-1 overflow-hidden`, min-height 0 — absorbed
       the whole shortfall. Measured at 1440x900 that left the list 97px
       against a 111px row: not one row fully visible, and 0px at 1440x768.
       So:
         · card 1 gets min-height:0 (it may now shrink below its content) and
           becomes a flex column whose ONE scroller is #courseInfo;
         · card 2 gets `flex: 1 1 0` with a 220px floor — the flex algorithm
           freezes it there and takes the rest out of card 1, which is the
           inversion this block is for. 220 − 48 padding − 48 header = 124px
           of list, so its first row is always whole;
         · the list's inline height (calc(100% − 80px), a pre-panel-head
           chain that also over-subtracts by 32) is replaced by flex sizing;
         · a COLLAPSED card must not hold the floor — see the collapsed rule
           below.
       At 1440x900 (column 694) card 1 keeps its 467 and Favorites takes 227;
       at 1440x768 (562) Favorites freezes at 220 and card 1 shrinks to 342,
       the one case where #courseInfo scrolls.
       ---------------------------------------------------------------------- */
    .right-panel > .panel-card:first-child {
        border-bottom-left-radius: 0;
        border-bottom-right-radius: 0;
        display: flex;
        flex-direction: column;
        flex: 0 1 auto;
        min-height: 0;
        overflow: hidden;
    }
    .right-panel > .panel-card:first-child > #courseInfo {
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;
    }
    .right-panel > .panel-card + .panel-card {
        margin-top: 0;
        border-top-left-radius: 0;
        border-top-right-radius: 0;
        border-top: 1px solid var(--color-border);
        display: flex;
        flex-direction: column;
        flex: 1 1 0;
        min-height: 220px;
    }
    .right-panel .panel-head {
        cursor: pointer;
    }
    .right-panel .panel-chevron {
        display: inline-block;
        margin-left: 0.5rem;
        transition: transform 0.2s ease;
    }
    .right-panel .panel-card.panel-collapsed .panel-chevron {
        transform: rotate(-90deg);
    }
    .right-panel .panel-card.panel-collapsed > :not(.panel-head) {
        display: none;
    }
    .right-panel .panel-card.panel-collapsed {
        flex: 0 0 auto;
        /* Releases Favorites' 220px floor — a collapsed card is its header. */
        min-height: 0;
    }
    .right-panel .panel-card.panel-collapsed .panel-head {
        margin-bottom: 0;
    }
    .dark .right-panel > .panel-card + .panel-card {
        border-top-color: #374151;
    }

    /* Empty states: one compact line instead of the icon poster — "zero
       favorites" and "nothing selected" don't deserve a third of the column.
       Covers both emitters of the same markup (the template's inline states
       and ui/dom.js createEmptyState('favorites' | 'course-info')). Course
       Information keeps its card and header: hovering any search row fills
       it and it never empties again until a draft switch, so a card that
       vanished while empty would jump back within seconds of the first
       search. Its line reads "Hover over a course" (the poster's first
       subtitle); the Favorites line keeps its title. Mobile keeps both
       posters. */
    .right-panel #favoritesList .empty-state,
    .right-panel #courseInfo .empty-state {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 0.5rem;
        padding: 0.75rem 0;
    }
    .right-panel #favoritesList .empty-state .empty-state-icon i,
    .right-panel #courseInfo .empty-state .empty-state-icon i {
        font-size: 1rem;
        margin-bottom: 0;
    }
    .right-panel #favoritesList .empty-state h4 {
        margin-bottom: 0;
    }
    .right-panel #favoritesList .empty-state p,
    .right-panel #courseInfo .empty-state h4,
    .right-panel #courseInfo .empty-state p:last-of-type {
        display: none;
    }
    .right-panel #courseInfo .empty-state p:first-of-type {
        margin: 0;
    }

    /* Favorites takes the rest of the column and scrolls internally — the
       ONE scroller in that card. Both of the list's inline styles are for
       mobile and have to stay inline, so !important is the only way past
       them here (same precedent as the tips-content overrides above):
       max-height:400px caps the list on phones, and height:calc(100% − 80px)
       is a pre-panel-head chain that over-subtracts by 32px and can't see
       the card's flex sizing at all. */
    .right-panel #favoritesList {
        max-height: none !important;
        height: auto !important;
        flex: 1 1 0;
        min-height: 0;
    }

    /* No-Schedule Courses (senior projects, internships), the card under the
       joined one, shown only while the draft holds one. It was the one card
       outside the height model: a plain block at its full content height, so
       every row it listed came out of Course Information first and then out
       of the column itself -- eight rows ended 387px below it at 1440x768.
       Now it yields, and FIRST: its shrink weight dwarfs Course Information's
       (flex shrinks in proportion to weight x size), because Course
       Information is what the student is working with and this list is
       reference. It stops at a floor of its header, its note and one whole
       row: 25 + 56 + 16 + 32 + 12 + 54 + 25 = 220px, measured in Chromium
       with the header wrapped to two lines at the column's 340px minimum
       (32px wider up). The floor needs a number: a scroll container's
       intrinsic height is still its full content, so min-height:auto would
       never let the card shrink at all. Applied only from the second row on
       -- with one row the card's content IS the floor, and a fixed floor
       would pad it with blank space. #noScheduleList is its ONE scroller.
       `:not(.hidden)` is load-bearing: the card is hidden with Tailwind's
       `hidden` class, and an ID rule's display would outrank it and show
       the empty card on every page. */
    .right-panel > #no-schedule-section:not(.hidden) {
        display: flex;
        flex-direction: column;
        flex: 0 100 auto;
    }
    .right-panel > #no-schedule-section:not(.hidden):has(#noScheduleList > :nth-child(2)) {
        min-height: 13.75rem;
    }
    .right-panel #noScheduleList {
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;
    }

    /* Course Information: a compact header band, then the metadata as a
       hairline-divided list (small caps label over the value, the icon
       column kept), then the capacity block. Styled via the inert ci-* /
       cap-* hook classes course-info.js adds (its Tailwind classes are
       untouched — that component renders on mobile too). The space-y-4
       sibling margins are neutralised at matching specificity (Tailwind
       emits them as `> :not([hidden]) ~ :not([hidden])`).

       The metadata is a TWO-COLUMN grid: the short fields pair up so the
       card is five rows instead of eight, which is what buys Favorites its
       room back (the taller the panel, the less the column has left). DOM
       order is untouched — `order` does the pairing, so the mobile panel
       still reads top to bottom in source order:

           Schedule            | Section        (row of two)
           Instructor                           (full width)
           Room                | Credits        (row of two)
           capacity                             (full width)

       A full-width item forces a new grid row, so a missing field just
       closes up. column-gap is 0 with the right-hand cells padded instead,
       so the rows' bottom hairlines meet as one continuous line; items
       stretch, so the two cells of a pair share a baseline for it. */
    .right-panel #courseInfo .space-y-4 > :not([hidden]) ~ :not([hidden]) {
        margin-top: 0;
    }
    .right-panel #courseInfo .space-y-4 {
        display: grid;
        grid-template-columns: 1fr 1fr;
        column-gap: 0;
    }
    .right-panel #courseInfo .ci-head,
    .right-panel #courseInfo .ci-instructor,
    .right-panel #courseInfo .cap-block {
        grid-column: 1 / -1;
    }
    .right-panel #courseInfo .ci-schedule {
        order: 1;
    }
    .right-panel #courseInfo .ci-section {
        order: 2;
    }
    .right-panel #courseInfo .ci-instructor {
        order: 3;
    }
    .right-panel #courseInfo .ci-room {
        order: 4;
    }
    .right-panel #courseInfo .ci-credits {
        order: 5;
        display: flex;   /* revealed: hidden at every width by the block above */
    }
    .right-panel #courseInfo .cap-block {
        order: 6;
    }
    .right-panel #courseInfo .ci-section,
    .right-panel #courseInfo .ci-credits {
        padding-left: 0.75rem;
    }

    /* Schedule row: the desktop label ("Schedule" — the mobile one stays
       "Day/Time" and is hidden here) over ONE LINE PER MEETING PATTERN, so a
       lecture + lab course shows both meetings instead of the raw
       slash-separated SIS string. The raw value is kept in the DOM and
       hidden only when there are pattern lines to replace it. */
    .right-panel #courseInfo .ci-label-alt {
        display: block;
    }
    .right-panel #courseInfo .ci-schedule .ci-label:not(.ci-label-alt) {
        display: none;
    }
    .right-panel #courseInfo .ci-patterns {
        display: block;
    }
    .right-panel #courseInfo .ci-pattern {
        display: block;
        font-size: 0.82rem;
        line-height: 1.4;
        color: #1f2937;
    }
    .right-panel #courseInfo .ci-row.has-patterns > div > .ci-value {
        display: none;
    }
    .dark .right-panel #courseInfo .ci-pattern {
        color: #e5e7eb;
    }
    .right-panel #courseInfo .ci-head {
        padding: 0.6rem 0.75rem;
        margin-bottom: 0.35rem;
        border-bottom-width: 1px;
    }
    .right-panel #courseInfo .ci-head h3 {
        font-size: 1.05rem;
        line-height: 1.3;
        margin-bottom: 0.15rem;
    }
    .right-panel #courseInfo .ci-head p {
        font-size: 0.8rem;
        line-height: 1.4;
    }
    .right-panel #courseInfo .ci-row {
        padding: 0.45rem 0.25rem;
        border-bottom: 1px solid var(--color-border);
    }
    .right-panel #courseInfo .ci-label {
        font-size: 0.66rem;
        font-weight: 600;
        letter-spacing: 0.04em;
        text-transform: uppercase;
        color: #6b7280;
    }
    .right-panel #courseInfo .ci-value {
        font-size: 0.82rem;
        line-height: 1.4;
        color: #1f2937;
    }
    .dark .right-panel #courseInfo .ci-row {
        border-bottom-color: #374151;
    }
    .dark .right-panel #courseInfo .ci-label {
        color: #9ca3af;
    }
    .dark .right-panel #courseInfo .ci-value {
        color: #e5e7eb;
    }

    /* Capacity is the field students act on — give the seats-left figure
       and its bar more weight than the metadata rows, on TWO lines instead
       of four: the figure and the enrolled count share line one with the
       status word appended to it, the bar sits under them. The "Capacity"
       caption goes: the figure says what it is. Every cell is placed
       explicitly (the status word comes last in the DOM but belongs on the
       first line, and auto-placement would push it below the bar). */
    .right-panel #courseInfo .cap-block {
        margin-top: 0.5rem;
        padding-top: 1rem;
        border-top-width: 2px;
        display: grid;
        grid-template-columns: 1fr auto;
        align-items: baseline;
        column-gap: 0.35rem;
        row-gap: 0.4rem;
    }
    .right-panel #courseInfo .cap-caption {
        display: none;
    }
    .right-panel #courseInfo .cap-head {
        grid-row: 1;
        grid-column: 1;
        margin-bottom: 0;
    }
    .right-panel #courseInfo .cap-status {
        grid-row: 1;
        grid-column: 2;
    }
    .right-panel #courseInfo .cap-status::before {
        content: '·';
        margin-right: 0.35rem;
        color: #9ca3af;
    }
    .right-panel #courseInfo .cap-bar {
        grid-row: 2;
        grid-column: 1 / -1;
        height: 0.875rem;
        margin-bottom: 0;
    }
    /* The no-enrolment-data fallback replaces all three. */
    .right-panel #courseInfo .cap-unknown {
        grid-column: 1 / -1;
    }
    .right-panel #courseInfo .cap-seats {
        font-size: 1.25rem;
        font-weight: 800;
        line-height: 1.2;
    }

    /* Accent tie-in: the selected course's block color marks the Course
       Information card, so the panel visibly connects to the block that was
       clicked. Class + --course-accent are set together on the SAME card by
       render/regions/course-info.js (never a positional selector, so they
       can't drift apart). A border carries no text — no contrast concerns. */
    .right-panel .panel-card.accent-on {
        border-left: 4px solid var(--course-accent);
    }

    /* Dark mode for the restyled row controls (dark-mode.css predates this
       file and only knows the base .tb-action-btn). */
    .dark main.tt-page #clear-all-btn {
        color: #f87171;
        border-color: transparent;
    }
    .dark main.tt-page #clear-all-btn:hover {
        background: #450a0a;
        border-color: #7f1d1d;
    }
    .dark main.tt-page #clear-all-btn.confirming {
        background: #450a0a;
        border-color: #991b1b;
        color: #f87171;
    }
    .dark main.tt-page #credit-counter-btn {
        background: #1f2937;
        border-color: #20719c;
        color: #9edcff;
    }
    .dark main.tt-page #credit-counter-btn:hover {
        background: #1a4c68;
    }
    .dark main.tt-page #credit-counter-btn #credit-count {
        color: #9ca3af;
    }
    .dark main.tt-page #time-format-btn .tf-switch {
        background: #111827;
        border-color: #374151;
    }
    .dark main.tt-page #time-format-btn .tf-seg {
        color: #6b7280;
    }
    .dark main.tt-page #time-format-btn .tf-seg.tf-active {
        background: var(--color-primary);
        color: white;
    }
}
