/* CCIS palette, taken from the college seal: gold #F0C102, white, and the
   black of its linework. The seal is only those colours, which constrains the
   theme in one important way — GOLD IS NOT A TEXT COLOUR ON LIGHT. Measured:
   gold on white is 1.70:1 and white on gold is 1.70:1, both far below the 4.5:1
   needed for body text. Gold on a dark ground is 10.74:1, and ink on gold is
   10.22:1, which is why gold appears here only as a FILL behind dark text, or
   as a foreground on dark. --brand is a deep bronze drawn from the same hue so
   links and active state read as the gold family while staying legible
   (7.42:1 on white). Every value below was measured, not eyeballed. */
:root {
    --bg: #faf7ee;
    --surface: #ffffff;
    --text: #1a1a1a;
    --muted: #5a5a5a;
    --border: #e6dfc9;
    --brand: #6b5200;
    /* The readable foreground for a --brand BACKGROUND. It has to be a
       token because --brand flips from deep bronze to gold in the dark
       theme: white reads on the bronze (7.42:1) but is 1.70:1 on gold,
       so a hard-coded #fff would be unreadable in one of the two. */
    --on-brand: #ffffff;
    /* Page watermark strength. Measured: at 5% over --bg the seal costs
       body text 16.25:1 -> 15.83:1 and muted 6.44 -> 6.27, so it stays
       comfortably AA. Raise this and re-measure before shipping. */
    --watermark-opacity: .05;
    /* Colours a rendered document may use. Restated in the dark block: a
       document's red has to stay red in both themes, but #C00000 on a dark
       ground is muddy, so each theme gets a tone that actually reads. */
    --doc-red: #c00000;
    --doc-blue: #1a4f9c;
    --doc-green: #1a7f4b;
    --doc-mark: #fff3a3;
    --doc-mark-ink: #1a1a1a;
    /* The seal's gold. A background for dark text, never a text colour here. */
    --gold: #f0c102;
    --gold-strong: #d9ae00;
    --gold-ink: #1a1a1a;
    --gold-soft: #fdf3cf;
    --ok: #1a7f4b;
    --ok-bg: #e6f5ec;
    --err: #b42318;
    --err-bg: #fdeceb;
    /* The readable foreground for an --err BACKGROUND, for the same reason
       --on-brand exists: --err flips from a deep red to a light one between
       themes, so a hard-coded #fff is 6.57:1 here and 2.78:1 there — below
       the minimum for the small bold text it is used on. Measured both ways. */
    --on-err: #ffffff;
    --info: #6b5200;
    --info-bg: #fdf3cf;
    --shadow: 0 1px 3px rgba(60, 48, 10, .10), 0 1px 2px rgba(60, 48, 10, .06);
    /* Sidebar width and the screen width below which it stops being a column.
       Kept as tokens so the layout and its breakpoint are stated once. */
    --sidebar-w: 240px;
    /* The top bar is out of flow, so its height has to be stated: the main
       column pads by it and the watermark insets by it. One token so the
       three cannot drift apart. */
    --topbar-h: 3.25rem;
}

@media (prefers-color-scheme: dark) {
    /* On a dark ground the constraint inverts: the seal's gold becomes the
       most legible accent available (10.74:1 on --bg), so --brand IS gold
       here. The neutrals are warmed slightly so white text does not read cold
       against it. */
    :root {
        --bg: #12151a;
        --surface: #1a1f26;
        --text: #e7e3d8;
        --muted: #a39c88;
        --border: #2e3440;
        --brand: #f0c102;
        --on-brand: #1a1a1a;
        /* Slightly stronger: gold on a dark ground reads fainter. At 6%
           body is 12.91:1 and muted 6.05:1 — still AA. */
        --watermark-opacity: .06;
        --doc-red: #ff8f85;
        --doc-blue: #8ab4f8;
        --doc-green: #6ecf95;
        --doc-mark: #5c4a00;
        --doc-mark-ink: #fdf3cf;
        --gold: #f0c102;
        --gold-strong: #ffd426;
        --gold-ink: #1a1a1a;
        --gold-soft: #2a2410;
        --ok: #4ec27e;
        --ok-bg: #12271b;
        --err: #f0776b;
        --err-bg: #2a1614;
        /* 6.26:1 on #f0776b, where white is 2.78:1. */
        --on-err: #1a1a1a;
        --info: #f0c102;
        --info-bg: #2a2410;
    }
}

* { box-sizing: border-box; }

/* The whole scale hangs off this one number. Everything in the file is sized
   in rem, so raising the root raises type, padding, gaps and the top bar
   together and the proportions survive — which is why this is one declaration
   rather than ninety edited font-sizes. At the old 16px the densest screens
   ran to 12px table headings and 10px pills, which is under what anyone should
   have to read across a desk at a defence. 17px is the smallest step that
   fixes that without making the app feel like a large-print edition.

   A percentage, not a px value: it scales the browser's own font setting
   instead of overriding it, so a reader who has already chosen larger text
   keeps their choice. */
html { font-size: 106.25%; }

body {
    margin: 0;
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    background: var(--bg);
    color: var(--text);
    line-height: 1.5;
}

.wrap {
    /* 900px is the readable measure for a column of prose. Almost nothing here
       is prose: these screens are tables of 6 to 16 columns, and that cap made
       them scroll sideways inside their own box while a third of a 1920px
       screen sat empty beside them. Wide enough now for the widest table to
       land whole, still capped so a large monitor does not stretch a row into
       an unreadable line.

       Forms are unaffected: .form-card keeps its own 480px and centres itself,
       so the sign-in, profile and requirement screens look exactly as before. */
    max-width: 1240px;
    margin: 0 auto;
    padding: 3rem 1.25rem;
}

.hero { text-align: center; margin-bottom: 2.5rem; }

.badge {
    display: inline-block;
    font-size: .75rem;
    font-weight: 700;
    letter-spacing: .08em;
    color: var(--on-brand);
    background: var(--brand);
    padding: .3rem .6rem;
    border-radius: 999px;
    margin-bottom: 1rem;
}

.hero h1 { margin: 0 0 .35rem; font-size: 1.9rem; }
.hero .sub { margin: 0; color: var(--muted); font-size: .95rem; }

.cards {
    display: grid;
    /* auto-FILL, not auto-fit: auto-fit collapses the empty tracks in a
       partially-filled row, so a lone card left over on the last row (e.g. the
       4th KPI card at some widths) would stretch to the full grid width and
       read as a different, larger component. auto-fill keeps the empty tracks,
       so every card stays the same width as its neighbours above. */
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 1rem;
    /* Vertical rhythm so stacked sections (e.g. dashboard KPI grid + feature
       grid, or a card grid above a table) never butt directly against each
       other. Block-margin collapse keeps the gap a consistent 1.5rem. */
    margin-bottom: 1.5rem;
}

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 4px solid var(--border);
    border-radius: 10px;
    padding: 1.25rem;
    box-shadow: var(--shadow);
}

.card h2 { margin: 0 0 .5rem; font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
.card .status { margin: 0 0 .35rem; font-size: 1.25rem; font-weight: 700; }
.card .detail { margin: 0; font-size: .85rem; color: var(--muted); word-break: break-word; }

.card.ok { border-left-color: var(--ok); }
.card.ok .status { color: var(--ok); }
.card.err { border-left-color: var(--err); background: var(--err-bg); }
.card.err .status { color: var(--err); }

/* --- Dashboard status-distribution bar (inline SVG; CSP-safe, no inline styles) --- */
.status-dist { margin: 0 0 1.5rem; }
.status-bar {
    width: 100%;
    height: 1.15rem;
    display: block;
    border-radius: 6px;
    overflow: hidden;
    background: var(--border);
    border: 1px solid var(--border);
}
.status-bar rect.seg-pending   { fill: var(--muted); }
.status-bar rect.seg-submitted { fill: var(--info); }
.status-bar rect.seg-approved  { fill: var(--ok); }
.status-bar rect.seg-revised  { fill: var(--err); }

.status-legend {
    list-style: none;
    margin: .6rem 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: .4rem 1.25rem;
    font-size: .85rem;
}
.legend-item { display: flex; align-items: center; gap: .4rem; }
.legend-swatch {
    width: .7rem;
    height: .7rem;
    border-radius: 3px;
    flex-shrink: 0;
    display: inline-block;
}
.legend-swatch.seg-pending   { background: var(--muted); }
.legend-swatch.seg-submitted { background: var(--info); }
.legend-swatch.seg-approved  { background: var(--ok); }
.legend-swatch.seg-revised  { background: var(--err); }
.legend-pct { color: var(--muted); }

.foot { margin-top: 2.5rem; text-align: center; color: var(--muted); font-size: .85rem; }
.foot code { background: var(--border); padding: .1rem .35rem; border-radius: 4px; }

/* --- Shared app layout: fixed sidebar + content column ---
   The sidebar is a grid column rather than position:fixed so the content
   column's width is computed by the browser and never needs a matching
   margin-left kept in sync by hand. */
.app-shell { min-height: 100vh; }

/* Named areas rather than source order: the top bar has to come BEFORE the
   sidebar in the DOM (so the narrow-screen drawer opens under its own button)
   but must render BESIDE it, above the content column, at desktop. Areas let
   one DOM order serve both, and the narrow-screen override below is a single
   re-declaration instead of a reshuffle. */
.app-shell-auth {
    display: grid;
    grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
    grid-template-areas:
        "sidebar topbar"
        "sidebar main";
    align-items: start;
    /* Both bars are position:fixed, so neither is in flow and the "topbar" row
       is empty. With the shell at min-height:100vh, the default align-content
       (stretch) then shares the leftover height between the two auto rows —
       which on any page shorter than the window silently pushed the content
       down by half the free space: about 150px of nothing above the page
       title on the faculty checklist. start keeps the spare height at the
       bottom, where it belongs. */
    align-content: start;
}
.app-shell-auth > .topbar { grid-area: topbar; }
.app-shell-auth > .sidebar { grid-area: sidebar; }
.app-shell-auth > .app-main { grid-area: main; }

/* Keyboard users would otherwise walk the whole nav on every page load. */
.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 10;
    background: var(--brand);
    color: var(--on-brand);
    padding: .6rem 1rem;
    border-radius: 0 0 8px 0;
    font-size: .85rem;
    font-weight: 600;
    text-decoration: none;
}
.skip-link:focus { left: 0; }

.sidebar {
    /* Fixed, not sticky. Sticky depends on every ancestor staying free of
       `overflow`, and on the grid area being taller than the element — both
       are invisible conditions that a later, unrelated change can break
       without any error. The grid column below still reserves the width, so
       taking the sidebar out of flow does not let content slide under it. */
    position: fixed;
    top: 0;
    left: 0;
    width: var(--sidebar-w);
    z-index: 30;
    align-self: start;
    /* Own scrollbar: the Secretary's menu plus the account block can outrun a
       short laptop viewport, and the page must not scroll to reach "Log out".
       min-height pins the other end: the grid is align-items:start, so without
       it the panel is only as tall as its own content and stops mid-page. The
       Secretary's longer menu hid that — the Faculty sidebar, being four items,
       ended below the account block with page background showing beneath it.
       min + max together make it exactly one viewport, scrolling inside. */
    min-height: 100vh;
    max-height: 100vh;
    overflow-y: auto;
    /* Without this, reaching the end of the sidebar's own scroll hands the
       gesture to the page, so a scroll aimed at the menu silently moves the
       content behind it — and the menu appears to drift on its own. */
    overscroll-behavior: contain;
    background: var(--surface);
    border-right: 1px solid var(--border);
    padding: 1.25rem 1rem;
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

.brand {
    display: flex;
    align-items: center;
    gap: .6rem;
    text-decoration: none;
    color: var(--text);
}

.brand .badge { margin-bottom: 0; }
.brand-text { display: flex; flex-direction: column; gap: .3rem; align-items: flex-start; }
.brand-name { font-weight: 600; font-size: .95rem; line-height: 1.25; }

/* Fixed box so a tall, wide, or not-quite-square logo cannot shift the whole
   sidebar layout; object-fit keeps its aspect ratio inside that box. */
.brand-logo {
    width: 40px;
    height: 40px;
    flex: none;
    object-fit: contain;
}

.sidenav { display: flex; flex-direction: column; gap: .15rem; }

.sidenav-heading {
    margin: .9rem 0 .25rem;
    font-size: .7rem;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
}
.sidenav-heading:first-child { margin-top: 0; }

.sidenav-link {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .5rem .65rem;
    border-radius: 8px;
    font-size: .875rem;
    font-weight: 600;
    color: var(--muted);
    text-decoration: none;
}
.sidenav-link:hover { color: var(--brand); background: var(--info-bg); }

/* Current page: colour alone would fail WCAG 1.4.1, so it also carries a solid
   fill and a left rule. */
.sidenav-link.is-current {
    color: var(--brand);
    background: var(--info-bg);
    box-shadow: inset 3px 0 0 var(--brand);
}

/* --- Status sub-navigation under Review (FR-38) ---
   A level below .sidenav-link rather than three more of them: the indent and
   the rule down the left edge are what say "these belong to the group above",
   which is the whole point of the client's request — four peers in the Review
   group would read as four unrelated screens. The links themselves are still
   .sidenav-link, so `.is-current` needs no second rule set here. */
.sidenav-sub {
    display: flex;
    flex-direction: column;
    gap: .15rem;
    margin: .15rem 0 .15rem .65rem;
    padding-left: .6rem;
    border-left: 1px solid var(--border);
}

.sidenav-sublink {
    padding-top: .35rem;
    padding-bottom: .35rem;
    font-size: .8rem;
}

.sidebar-foot {
    margin-top: auto;
    padding-top: 1rem;
    border-top: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    gap: .5rem;
    align-items: flex-start;
}

/* --- Top bar: persistent at every width ---
   Carries the two controls that must be reachable from any screen without
   opening a menu — the unread-notification indicator (FR-23) and logout
   (FR-4). The "Menu" button and the brand inside it are the narrow-screen
   half: both are hidden here and switched on only under `.js` below the
   sidebar breakpoint, where the sidebar has collapsed into a drawer. At
   desktop the sidebar shows the brand and there is no drawer to open, so the
   bar is just its right-hand actions. */
/* Pinned: the search, the unread count and Log out are wanted at any scroll
   position, and the bar is short enough that holding it costs little height.
   z-index keeps it over the content it scrolls past; the sidebar is a separate
   column so the two never overlap. */
.topbar {
    /* Fixed for the same reason, and because as a grid item its containing
       block was row 1 only — exactly its own height — so sticky had no room to
       travel and it scrolled away even before the rule above clobbered it. */
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    gap: .75rem;
    min-height: 3.25rem;
    padding: .55rem 1.25rem;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
}

.topbar-brand { display: none; align-items: center; gap: .5rem; text-decoration: none; }
.topbar-brand .badge { margin-bottom: 0; }
.topbar-logo { width: 28px; height: 28px; object-fit: contain; }

/* --- Top-bar search (FR-37) ---
   The one element in the bar that may take the leftover width: flex-basis
   stops it collapsing to nothing between the brand and the icons, and the
   max-width stops it stretching the full width of a desktop, where a
   700px-wide input reads as a mistake rather than a search box. */
.topbar-search {
    display: flex;
    align-items: center;
    gap: .25rem;
    flex: 1 1 12rem;
    /* min-width:0 against the flex default of `auto`: without it the input's
       intrinsic minimum width wins over flex-shrink and the bar overflows
       instead of the field giving way. */
    min-width: 0;
    max-width: 22rem;
}

/* An <input> inherits neither the app's font nor its colours, and the UA
   default border is a different grey from every other control on the page. */
.topbar-search-input {
    flex: 1 1 auto;
    min-width: 0;
    padding: .4rem .65rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg);
    color: var(--text);
    font: inherit;
    font-size: .85rem;
}
.topbar-search-input::placeholder { color: var(--muted); }
.topbar-search-input:focus-visible {
    border-color: var(--brand);
    outline: 2px solid var(--brand);
    outline-offset: 1px;
}

/* Also a .topbar-action, so it is the same size and hover as the bell beside
   it; only the label differs, and that one is .sr-only because there is no
   room for a third piece of text in the bar. */
.topbar-search-btn { flex: none; }

/* Around 400px the bar also has to hold Menu + brand + bell + logout, so the
   field takes a row of its own instead of squeezing the four of them off the
   edge. `order` moves it down visually without moving it in the DOM, so the
   tab order stays brand -> search -> bell -> logout at every width. */
@media (max-width: 767px) {
    .topbar { flex-wrap: wrap; }

    .topbar-search {
        order: 3;
        flex-basis: 100%;
        max-width: none;
    }
}

/* margin-left:auto, not justify-content:space-between — the two controls stay
   pinned to the trailing edge whether or not the Menu button and brand before
   them are rendered at this width. */
.topbar-actions {
    display: flex;
    align-items: center;
    gap: .35rem;
    margin-left: auto;
}

/* One shared appearance for the bell (an <a>) and logout (a <button> inside a
   POST form): both must read as the same kind of control, and a <button>
   inherits neither font nor colour without being told to. */
.topbar-action {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    padding: .45rem .6rem;
    border: 1px solid transparent;
    border-radius: 8px;
    background: none;
    font: inherit;
    font-size: .85rem;
    font-weight: 600;
    color: var(--muted);
    text-decoration: none;
    cursor: pointer;
}
.topbar-action:hover { color: var(--brand); background: var(--info-bg); }

/* The icons are inline <svg> (no icon library, and the CSP forbids off-origin
   assets). Everything that paints them lives here rather than on presentation
   attributes, and currentColor means the hover and the dark theme need no
   second rule set. */
.topbar-icon {
    display: block;
    width: 20px;
    height: 20px;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Unread count, pinned to the bell's upper-right. Positioned against
   .topbar-action, which is the relatively-positioned box. */
.topbar-badge {
    position: absolute;
    top: .1rem;
    left: 55%;
    min-width: 1.1rem;
    padding: 0 .25rem;
    border-radius: 999px;
    background: var(--err);
    color: #fff;
    font-size: .65rem;
    font-weight: 700;
    line-height: 1.1rem;
    text-align: center;
}

/* display:none by default: the drawer this opens only exists below the sidebar
   breakpoint, and only when app.js is running to re-open it. */
.nav-toggle {
    display: none;
    align-items: center;
    gap: .5rem;
    font: inherit;
    font-size: .85rem;
    font-weight: 600;
    color: var(--text);
    background: none;
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: .4rem .7rem;
    cursor: pointer;
}
.nav-toggle:hover { color: var(--brand); border-color: var(--brand); }

/* Three bars drawn from one element: a box plus two shadows, so the button
   needs no icon font and no extra markup. */
.nav-toggle-bars,
.nav-toggle-bars::before,
.nav-toggle-bars::after {
    display: block;
    width: 16px;
    height: 2px;
    background: currentColor;
    border-radius: 2px;
}
.nav-toggle-bars { position: relative; }
.nav-toggle-bars::before,
.nav-toggle-bars::after { content: ""; position: absolute; left: 0; }
.nav-toggle-bars::before { top: -5px; }
.nav-toggle-bars::after { top: 5px; }

/* --- Signed-out brand bar (login screen) --- */
.auth-brandbar {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .75rem;
    padding: 2.5rem 1.25rem 0;
}
/* The sign-in screen is the one place the seal is the subject rather than a
   marker, so it is shown at a size where the ring text is actually legible —
   below roughly 64px the wording in the seal turns to mush. */
.auth-logo { width: 132px; height: 132px; object-fit: contain; }
@media (max-width: 480px) {
    .auth-logo { width: 104px; height: 104px; }
}

/* Now an <a> to /profile, not a <span>. There is no site-wide anchor reset, so
   without these it would carry the UA's permanent underline and give no hover
   feedback — every other nav link (.notif-link, .topbar-action) uses exactly
   this idiom instead.
   Avatar and text are a two-column row that must NOT wrap: the sidebar is only
   240px, so it is the name that has to give (min-width:0 + ellipsis below),
   not the layout. */
.who {
    font-size: .85rem;
    color: var(--muted);
    display: flex;
    align-items: center;
    gap: .6rem;
    width: 100%;
    text-decoration: none;
}
.who:hover { color: var(--brand); }
.who-text { display: flex; flex-direction: column; min-width: 0; }
.who-name {
    font-weight: 600;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.who:hover .who-name { color: var(--brand); text-decoration: underline; }

/* Role, in place of the old .role-pill. It is what the whole menu above is
   keyed to, so it stays visible — just demoted to a caption under the name
   now that the avatar owns the eye-catching part of this block. */
.who-role {
    font-size: .72rem;
    color: var(--muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Profile avatar. A fixed circular frame whose single child is what varies:
   today .avatar-initials, and from the profile-image work an <img
   class="avatar-img"> dropped in alongside it — the frame, its size and its
   place in .sidebar-foot do not move either way. Tokens only, so it keeps its
   contrast in the dark theme without a second rule set. */
.avatar {
    flex: none;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: var(--info-bg);
    color: var(--brand);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}
.avatar-initials {
    font-size: .8rem;
    font-weight: 700;
    letter-spacing: .02em;
    line-height: 1;
}
.avatar-img { width: 100%; height: 100%; object-fit: cover; }

/* FR-40 profile-photo card. .avatar-lg is the same circular frame at the size
   the profile screen shows it, so the sidebar and the profile page cannot drift
   apart — only the dimensions and the initials' type size change. Tokens only:
   the dark theme follows without a second rule set. */
.avatar-lg {
    width: 96px;
    height: 96px;
}
.avatar-lg .avatar-initials {
    font-size: 2rem;
}
.photo-row {
    display: flex;
    align-items: center;
    gap: 1.25rem;
    flex-wrap: wrap;
}
.photo-actions {
    display: flex;
    flex-direction: column;
    gap: .75rem;
    min-width: 0;
}
.photo-form {
    display: flex;
    flex-direction: column;
    gap: .4rem;
    align-items: flex-start;
}
/* The file input is the control most likely to force a horizontal scrollbar on
   a phone: its rendered width is decided by the browser from the filename, not
   by the layout. Capping it is what keeps the card inside the viewport. */
.photo-form .file-field {
    max-width: 100%;
}

@media (max-width: 480px) {
    .photo-row {
        gap: 1rem;
    }
    .avatar-lg {
        width: 72px;
        height: 72px;
    }
    .avatar-lg .avatar-initials {
        font-size: 1.5rem;
    }
}

.role-pill {
    display: inline-block;
    font-size: .7rem;
    font-weight: 700;
    letter-spacing: .04em;
    color: var(--brand);
    background: var(--ok-bg);
    padding: .15rem .5rem;
    border-radius: 999px;
}

.logout-link {
    font-size: .85rem;
    font-weight: 600;
    color: var(--brand);
    text-decoration: none;
}
.logout-link:hover { text-decoration: underline; }

/* Logout is a POST form (FR-4), so it needs the margin reset a bare <form>
   would otherwise contribute to the top bar's flex row. Its button is styled
   by .topbar-action, shared with the notification bell. */
.logout-form { margin: 0; }

/* --- Login --- */
.auth-card {
    max-width: 380px;
    margin: 3rem auto 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 2rem 1.75rem;
    box-shadow: var(--shadow);
    text-align: center;
}

/* The seal inside the sign-in card. Sized so the ring wording is still
   readable (it turns to mush below roughly 64px) without crowding the form. */
.auth-card-logo {
    display: block;
    width: 96px;
    height: 96px;
    margin: 0 auto .25rem;
    object-fit: contain;
}
@media (max-width: 480px) {
    .auth-card-logo { width: 76px; height: 76px; }
}

.auth-card h1 { margin: .75rem 0 .25rem; font-size: 1.4rem; }
.auth-card .sub { margin: 0 0 1.25rem; color: var(--muted); font-size: .85rem; }

.auth-form { text-align: left; }

.field { margin-bottom: 1rem; }
.field label,
.field-label { display: block; font-size: .85rem; font-weight: 600; margin-bottom: .3rem; }
.field input {
    width: 100%;
    padding: .55rem .7rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg);
    color: var(--text);
    font-size: .95rem;
}
.field input:focus { outline: 2px solid var(--brand); outline-offset: 1px; }

.field select {
    width: 100%;
    padding: .55rem .7rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg);
    color: var(--text);
    font-size: .95rem;
    font-family: inherit;
}
.field select:focus { outline: 2px solid var(--brand); outline-offset: 1px; }

.field input:disabled,
.field select:disabled,
.form-card textarea:disabled {
    opacity: .6;
    cursor: not-allowed;
}

.field-help { margin: .3rem 0 0; font-size: .85rem; color: var(--muted); }

/* --- Requirement audience control (FR-35): a radio group where two of the
   three options carry a payload panel underneath. All tokenised, so it follows
   the dark theme without a second rule set. `.is-hidden` is the collapse hook
   app.js toggles — a class, because the CSP forbids inline style attributes.
   Without JavaScript nothing carries the class and every panel stays open,
   which is the progressive-enhancement fallback the form is built for. --- */
.audience-field {
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: .9rem 1rem 1rem;
}
.audience-field legend {
    font-size: .85rem;
    font-weight: 600;
    padding: 0 .35rem;
}
.audience-field .radio-label,
.audience-field .checkbox-label {
    display: flex;
    align-items: center;
    gap: .45rem;
    font-size: .92rem;
    font-weight: 400;
    margin: .5rem 0 0;
}
/* .field input is width:100% for text inputs; a radio/checkbox must not be. */
.audience-field input[type="radio"],
.audience-field input[type="checkbox"] { width: auto; flex-shrink: 0; }
.audience-field .field-help { margin: .15rem 0 0 1.55rem; }

.audience-panel {
    margin: .5rem 0 .25rem 1.55rem;
    padding-left: .85rem;
    border-left: 2px solid var(--border);
}
.audience-panel.is-hidden { display: none; }

.checkbox-list {
    list-style: none;
    margin: .25rem 0 0;
    padding: 0;
    max-height: 15rem;
    overflow-y: auto;
}
.checkbox-list .muted-note { margin-left: .2rem; }

/* The seal's gold carrying ink, measured at 10.22:1 — the most legible
   control in the application, and the one place the brand colour appears at
   full strength. White on gold would have been 1.70:1, so the label is dark. */
.btn-primary {
    width: 100%;
    padding: .65rem 1rem;
    border: none;
    border-radius: 8px;
    background: var(--gold);
    color: var(--gold-ink);
    font-size: .95rem;
    font-weight: 700;
    cursor: pointer;
}
.btn-primary:hover { background: var(--gold-strong); }

.alert { border-radius: 8px; padding: .6rem .8rem; font-size: .85rem; margin: 0 0 1rem; text-align: left; }
.alert-err { background: var(--err-bg); color: var(--err); }
.alert-ok { background: var(--ok-bg); color: var(--ok); }

/* --- Role dashboards --- */
.dash-hero { text-align: center; margin: 1rem 0 2rem; }
.dash-hero h1 { margin: .5rem 0 .25rem; font-size: 1.6rem; }
.dash-hero .sub { margin: 0; color: var(--muted); font-size: .9rem; }
/* Which academic period the figures below belong to — every role dashboard
   shows counts scoped to the active period, so every role dashboard says so. */
.dash-period {
    display: inline-block;
    margin: .6rem 0 0;
    padding: .2rem .6rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    font-size: .78rem;
    font-weight: 600;
    color: var(--muted);
}

.card .status.muted { color: var(--muted); font-size: .85rem; font-weight: 600; }

/* --- Prominent dashboard call-to-action (e.g. "Open Monitoring Board") --- */
.dash-cta { text-align: center; margin: 0 0 1.5rem; }

/* --- 403 --- */
.error-card { max-width: 480px; margin: 3rem auto 0; text-align: center; }
.error-card .badge.err { background: var(--err); }
.error-card h1 { margin: .75rem 0 .5rem; }
.error-card .sub { color: var(--muted); }

/* --- Narrow screens: the sidebar stops being a column ---
   Two behaviours, deliberately layered so the menu is never unreachable:

   * No JavaScript — the sidebar simply stacks above the content as a normal
     block. Longer to scroll past, but every link works.
   * With JavaScript (app.js puts `.js` on <html> before paint) it collapses
     into a drawer behind the "Menu" button in the mobile header.

   The `.js` scoping is what makes that safe: the collapsed state is only ever
   applied when the script that can re-open it is actually running. */
@media (max-width: 899px) {
    .app-shell-auth {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas:
            "topbar"
            "sidebar"
            "main";
    }

    .sidebar {
        position: static;
        /* Stacked block / drawer here, not a full-height column: a viewport of
           min-height would push the page content off-screen below it. */
        min-height: 0;
        max-height: none;
        border-right: 0;
        border-bottom: 1px solid var(--border);
    }

    /* The two narrow-screen halves of the top bar. Scoped to `.js` because
       both only make sense once the drawer exists: without script the sidebar
       below is a plain stacked block that already shows the brand and needs
       no button to reveal it. */
    .js .nav-toggle { display: flex; }
    .js .topbar-brand { display: flex; }

    /* The drawer proper. `display` (not height/transform) because the sidebar
       is a flex container whose collapsed state must take no space at all.
       DOM order is already header -> sidebar -> content, so an open drawer
       lands directly beneath its button and pushes the page down; nothing is
       hidden or overlaid. */
    .js .sidebar { display: none; }
    .js .nav-open .sidebar { display: flex; }
}

/* Tighter gutters on a phone; the desktop rule is a comfortable 3rem. */
@media (max-width: 599px) {
    .wrap { padding: 1.75rem 1rem; }

    /* Around 400px the top bar has to fit Menu + brand + bell + logout. The
       logout label is clipped rather than removed, so the button keeps the
       accessible name it would otherwise lose by becoming a bare icon. */
    .topbar { padding: .55rem .85rem; gap: .5rem; }
    .topbar-action-text {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }
}

/* --- Card link (dashboard shortcuts into a feature area) --- */
.card-link { font-weight: 600; color: var(--brand); text-decoration: none; font-size: .85rem; }
.card-link:hover { text-decoration: underline; }

/* --- Admin list pages: heading row + "New ..." action --- */
.page-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin: 1rem 0 1.5rem;
}
.page-head h1 { margin: .4rem 0 .25rem; font-size: 1.5rem; }
.page-head .sub { margin: 0; color: var(--muted); font-size: .9rem; }

/* --- Buttons --- */
.btn-inline { width: auto; display: inline-block; text-decoration: none; text-align: center; }

.btn-sm {
    display: inline-block;
    padding: .35rem .7rem;
    border-radius: 6px;
    font-size: .8rem;
    font-weight: 600;
    text-decoration: none;
    border: 1px solid transparent;
    cursor: pointer;
    font-family: inherit;
}

.btn-secondary {
    background: var(--surface);
    color: var(--text);
    border-color: var(--border);
}
.btn-secondary:hover { background: var(--bg); }

.btn-danger {
    background: var(--err-bg);
    color: var(--err);
    border-color: var(--err-bg);
}
.btn-danger:hover { opacity: .85; }

.btn-primary-sm {
    background: var(--brand);
    color: var(--on-brand);
    border-color: var(--brand);
}
.btn-primary-sm:hover { opacity: .9; }

.inline-form { display: inline; margin: 0; }

/* --- Upload form (faculty checklist Action column) ---
   A block-level flex row, and the line break it forces is now wanted. It used
   to be inline-flex so that Download and Details could sit beside it, which is
   what left those links wrapping to a different place in every row. They are
   wrapped in .action-links below and get a line of their own, so the cell
   reads the same way whatever state the requirement is in: what you can do to
   it, then where you can read about it. */
.upload-form {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: .4rem;
    margin: 0;
}

/* --- Compact file control (faculty checklist Action column, FR-7) ---
   A raw <input type="file"> renders as "Choose File / No file chosen": ~200px
   of vendor chrome inside a table cell, which is what made the Action column
   twice the width of a Download-only row and pushed the whole table into a
   horizontal scrollbar.

   Two states, layered on the `.js` hook app.js sets on <html>, because the
   native control is the only thing that echoes the chosen filename without
   script:

   * no JS  — the native input renders exactly as it always did. The label's
              text is clipped (not removed) so it still names the control.
   * `.js`  — the input shrinks to a focusable 1px box and the label becomes a
              fixed-width button whose text app.js swaps for the filename.

   Either way the form is a plain POST + CSRF + multipart submit; nothing here
   is required for it to work. */
.file-field { display: inline-block; vertical-align: middle; }

.file-field-input {
    font-size: .78rem;
    color: var(--text);
    max-width: 200px;
}

.file-field-text {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.js .file-field {
    position: relative;
    max-width: 7.5rem;
    padding: .35rem .7rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--surface);
    color: var(--text);
    font-size: .8rem;
    font-weight: 600;
    cursor: pointer;
}
.js .file-field:hover { background: var(--bg); }

/* The real focus ring: the input itself is invisible, so the ring has to be
   drawn on the box the user actually sees. */
.js .file-field:focus-within { outline: 2px solid var(--brand); outline-offset: 1px; }

/* Clipped, not display:none — a `required` control the browser cannot focus
   makes Chrome refuse the submit outright ("not focusable") instead of showing
   its validation bubble. */
.js .file-field-input {
    position: absolute;
    top: 0;
    left: 0;
    width: 1px;
    height: 1px;
    opacity: 0;
}

.js .file-field-text {
    position: static;
    display: block;
    width: auto;
    height: auto;
    padding: 0;
    margin: 0;
    clip: auto;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* A file is staged but not yet sent — the row's Upload button is still the
   step that matters, so the cue is a colour change, not another control. */
.js .file-field.has-file { border-color: var(--brand); color: var(--brand); }

/* --- Data table --- */
.table-wrap {
    overflow-x: auto;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: var(--shadow);
}

/* A step above the global bump, because the table IS the content on most of
   these screens — the surrounding chrome can afford to stay quieter than the
   data it frames. */
.table { width: 100%; border-collapse: collapse; font-size: .95rem; }
.table th, .table td {
    /* Row height carries as much legibility as type size does: at the old
       padding a dense table read as a solid block and the eye lost its row. */
    padding: .8rem 1rem;
    text-align: left;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
    /* Every cell gets a definite vertical alignment so a row with mixed
       content heights (e.g. a wrapped two-line Description next to a
       single-line Actions cell) still lines up on one shared baseline
       instead of each cell defaulting to `vertical-align: baseline`.
       .matrix-table th overrides this back to `bottom` further down, for its
       own multi-line column headers. */
    vertical-align: middle;
}
.table th:nth-child(2), .table td:nth-child(2) { white-space: normal; }
.table thead th {
    /* Uppercase and letter-spaced, which costs legibility at small sizes; it
       needs the extra step more than the body rows do. */
    font-size: .82rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
    background: var(--bg);
}
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr:hover { background: var(--bg); }
.table-empty { text-align: center; color: var(--muted); white-space: normal; }
/* The Actions cell used to be `display: flex`, which takes a <td> out of
   normal table-cell layout entirely: a flex cell aligns its box to the top
   of the row, ignoring `vertical-align`, while its sibling cells (plain text
   nodes) stay baseline/middle-aligned per normal cell rules — that mismatch
   is what made the action buttons sit visibly higher than the rest of the
   row. Keeping this a normal table cell means it inherits `vertical-align:
   middle` from `.table td` above, so it centers exactly like every other
   cell. Its children (links, <form class="inline-form">, the upload form)
   are already inline / inline-block / inline-flex, so they still sit
   side by side; the `> * + *` rule below replaces the old flexbox `gap` with
   ordinary margins since normal inline flow has no `gap` property. */
.table-actions {
    white-space: nowrap;
}
.table-actions > * {
    vertical-align: middle;
}
.table-actions > * + * {
    margin-left: .5rem;
}

/* --- Faculty checklist table (FR-6) ---
   This table carries more per row than any other in the system: a title, a
   type, a deadline, a status, a date, an upload control, a note field and
   three links. The rules below are what keep that inside the page width
   without a horizontal scrollbar and without any two rows reading
   differently.

   1. Requirement is the only prose column, so it is the only one allowed to
      wrap, and it takes the width the Document Type column used to hold —
      that column is gone, folded into a second line under the title. A nowrap
      title was reserving its full single-line width before the Action column
      had been measured at all, which is what produced three-line titles next
      to a two-word type.
   2. Deadline has to be put back to nowrap. It is now the second column, and
      `.table td:nth-child(2)` makes the second column of every table wrap —
      a rule written when Document Type was sitting there.
   3. One declared width for every Action cell, so a Pending row and an
      Approved row do not each size to their own content. */
@media (min-width: 641px) {
    .table--checklist th:first-child,
    .table--checklist td:first-child { white-space: normal; width: 34%; }

    .table--checklist th:nth-child(2),
    .table--checklist td:nth-child(2) { white-space: nowrap; }

    /* The Overdue badge on its own line under the date. On one nowrap line
       with it, "Oct 15, 2026 OVERDUE" is the widest unbreakable run in the
       table after the Action cell, and an auto-layout table reserves that
       width in every row — including the rows with no badge at all. Stacking
       it takes the column from about 195px down to about 120px, which is the
       width the Requirement column gets back. max-content so the pill is as
       wide as its own word and not as wide as the cell. */
    .table--checklist td:nth-child(2) .overdue {
        display: block;
        width: max-content;
        margin: .25rem 0 0;
    }

    .table--checklist .table-actions { width: 15.5rem; }
}

/* 4. And one shared rhythm inside the Action cell at every width: the upload
   form on its own line, then every link that only reads — View, Download,
   Details, in that order — on the next. Overriding .table-actions' nowrap and
   its `> * + *` left margin, both of which assume a single inline run of
   buttons. */
.table--checklist .table-actions { white-space: normal; }
.table--checklist .table-actions > * + * { margin-left: 0; }
.table--checklist .upload-form + .action-links { margin-top: .5rem; }

/* 5. And within the upload form, two lines rather than three: the picker and
   Upload together, the comment field beneath them. The comment is free text —
   fifteen characters of it is useless — so it is the one control that takes a
   line of its own, and `order` puts it last so it wraps there instead of
   splitting the two buttons. This was the tallest part of the cell: picker,
   comment and Upload each claimed a line, and with View / Download / Details
   under them a Pending row ran to five. */
.table--checklist .upload-form .file-field { flex: 1 1 auto; min-width: 0; }
.table--checklist .upload-form .upload-submit { flex: 0 0 auto; }
.table--checklist .upload-form .upload-note { order: 3; margin: 0; }

.action-links { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; }

/* The title and its document type, stacked. The type is a label for the line
   above it, not a value anyone scans down the page, which is why it gave up
   its column. */
.checklist-title { display: block; font-weight: 600; line-height: 1.35; }
.checklist-subtitle {
    display: block;
    margin-top: .15rem;
    font-size: .78rem;
    color: var(--muted);
}

/* --- Faculty checklist: the reviewer's message on a returned requirement ---
   Its own row spanning the table, rather than a paragraph inside the Status
   cell. The message is prose: in that column it had about fifteen characters
   to a line and made Status the tallest cell in the row. The row above it
   carries .has-note, which drops the border between the two so they read as
   one item, and extends the hover highlight across both. */
.table--checklist tr.has-note > td { border-bottom: none; padding-bottom: .45rem; }
.checklist-note-row > td { padding-top: 0; }
.table--checklist tr.has-note:hover,
.table--checklist tr.has-note:hover + .checklist-note-row { background: var(--bg); }
.checklist-note {
    margin: 0;
    padding: .5rem .75rem;
    font-size: .85rem;
    line-height: 1.5;
    white-space: normal;
    background: var(--err-bg);
    border-left: 3px solid var(--err);
    border-radius: 0 6px 6px 0;
}
.checklist-note strong { color: var(--err); font-weight: 700; }
.checklist-note a { margin-left: .35rem; white-space: nowrap; }

/* --- Status pill --- */
.status-pill {
    display: inline-block;
    font-size: .7rem;
    font-weight: 700;
    letter-spacing: .04em;
    padding: .15rem .5rem;
    border-radius: 999px;
}
.status-pill-active { color: var(--ok); background: var(--ok-bg); }
.status-pill-inactive { color: var(--muted); background: var(--border); }

/* --- Submission status pills (faculty checklist) --- */
.status-pill-pending { color: var(--muted); background: var(--border); }
.status-pill-submitted { color: var(--info); background: var(--info-bg); }
.status-pill-approved { color: var(--ok); background: var(--ok-bg); }
.status-pill-revised { color: var(--err); background: var(--err-bg); }

/* --- Overdue marker (faculty checklist) --- */
.overdue {
    display: inline-block;
    margin-left: .4rem;
    font-size: .65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--err);
    background: var(--err-bg);
    padding: .05rem .4rem;
    border-radius: 999px;
}

.muted-note { color: var(--muted); font-size: .85rem; }

/* --- Create/edit forms (document types, etc.) --- */
.form-card {
    max-width: 480px;
    margin: 1rem auto 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 2rem 1.75rem;
    box-shadow: var(--shadow);
}
.form-card h1 { margin: .75rem 0 1.25rem; font-size: 1.3rem; }
/* The profile page stacks two form-cards, each headed by an h2 rather than the
   badge + h1 every single-card page uses. Without this the UA default renders
   those h2s LARGER than the h1 titles on every other form screen. */
.form-card h2 { margin: 0 0 1.25rem; font-size: 1.3rem; }
/* Keep the app's 1.5rem vertical rhythm between stacked cards; .form-card sets
   no bottom margin, so two in a row would otherwise sit 1rem apart. */
.form-card + .form-card { margin-top: 1.5rem; }

.form-card textarea {
    width: 100%;
    padding: .55rem .7rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg);
    color: var(--text);
    font-size: .95rem;
    font-family: inherit;
    resize: vertical;
}
.form-card textarea:focus { outline: 2px solid var(--brand); outline-offset: 1px; }

.field-err { margin: .3rem 0 0; font-size: .8rem; color: var(--err); }

.form-actions { display: flex; align-items: center; gap: .75rem; margin-top: 1.5rem; }

/* --- Reviewer queue: document-type filter --- */
.filter-form {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: .75rem;
    margin-bottom: 1rem;
}
.filter-form .field { margin-bottom: 0; min-width: 220px; }

/* --- Review detail page: submission metadata --- */
.review-meta {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 1.25rem 1.5rem;
    box-shadow: var(--shadow);
    margin-bottom: 1.5rem;
}

.meta-list {
    display: grid;
    /* auto-FILL for the same reason as .cards: auto-fit collapses the empty
       tracks on a partially-filled last row, so a metadata list whose item
       count is not a multiple of the column count (the document-detail page
       has 8 items over 3 columns) would stretch its final entries to full
       width and read as a different kind of field. */
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: .9rem 1.5rem;
    margin: 0 0 1rem;
}
.meta-list div { margin: 0; }
.meta-list dt {
    font-size: .75rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
    margin: 0 0 .2rem;
}
.meta-list dd { margin: 0; font-size: .95rem; }

/* --- Review history --- */
.review-history { margin-bottom: 1.5rem; }
.review-history h2 { font-size: 1.05rem; margin: 0 0 .75rem; }

.history-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .75rem; }
.history-list li {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: .75rem 1rem;
}
.history-meta { margin-left: .5rem; font-size: .8rem; color: var(--muted); }
.history-comment { margin: .5rem 0 0; font-size: .9rem; }

/* --- Decision form (review detail page) --- */
.decision-form-wrap {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 1.25rem 1.5rem;
    box-shadow: var(--shadow);
}

.decision-form .field { margin-bottom: 1.25rem; }
.decision-form .radio-label {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    font-weight: 400;
    font-size: .9rem;
    margin: .3rem 1.25rem .3rem 0;
}
.decision-form .radio-label input { width: auto; }
.decision-form textarea {
    width: 100%;
    padding: .55rem .7rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg);
    color: var(--text);
    font-size: .95rem;
    font-family: inherit;
    resize: vertical;
}
.decision-form textarea:focus { outline: 2px solid var(--brand); outline-offset: 1px; }

/* --- Admin monitoring board (FR-17..FR-20) --- */
.section-title { font-size: 1.05rem; margin: 2rem 0 .75rem; }

.matrix-wrap { margin-bottom: 2rem; }

.matrix-table th { white-space: normal; vertical-align: bottom; min-width: 150px; }
.matrix-table th:first-child, .matrix-table td:first-child { text-align: left; min-width: 160px; }
.matrix-table td.matrix-cell { white-space: nowrap; text-align: center; }

.matrix-faculty-col {
    position: sticky;
    left: 0;
    background: var(--surface);
}
.matrix-table thead .matrix-faculty-col { background: var(--bg); }

.matrix-col-meta {
    display: block;
    font-size: .7rem;
    font-weight: 400;
    text-transform: none;
    letter-spacing: 0;
    color: var(--muted);
    margin-top: .15rem;
}

.matrix-cell-overdue { box-shadow: inset 0 0 0 2px var(--err); border-radius: 4px; }

/* --- Header notification link + unread badge --- */
.notif-link {
    position: relative;
    font-size: .85rem;
    font-weight: 600;
    color: var(--muted);
    text-decoration: none;
}
.notif-link:hover { color: var(--brand); text-decoration: underline; }

/* A count carried by a navigation item: unread notifications, and documents
   waiting in the review queue. Named for what it is rather than for the first
   screen that needed one — the same reasoning as .filter-tabs. */
.nav-badge {
    display: inline-block;
    margin-left: .3rem;
    font-size: .7rem;
    font-weight: 700;
    color: var(--on-err);
    background: var(--err);
    padding: .05rem .45rem;
    border-radius: 999px;
}

/* A sidebar link whose label and count sit at opposite ends, so the numbers
   line up down the menu instead of trailing labels of different lengths. */
.nav-count-link {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
}

/* --- Notification center (FR-22, FR-23) --- */
.notif-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .75rem; }

.notif-item {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: .85rem 1rem;
}

.notif-item.unread {
    border-left: 4px solid var(--brand);
    background: var(--info-bg);
}

.notif-body { flex: 1; min-width: 0; }

.notif-title {
    margin: 0 0 .25rem;
    font-size: .95rem;
    font-weight: 700;
    display: flex;
    align-items: center;
    gap: .4rem;
}

.notif-dot {
    display: inline-block;
    width: .5rem;
    height: .5rem;
    border-radius: 999px;
    background: var(--brand);
    flex-shrink: 0;
}

.notif-message { margin: 0 0 .3rem; font-size: .9rem; }

.notif-meta { margin: 0; font-size: .8rem; color: var(--muted); }
.notif-meta a { color: var(--brand); }

/* --- Filter tabs: one link per state, each carrying a query parameter ---
   Shared by the notification list (All / Unread) and the faculty checklist's
   status filter (FR-46). Named for what it is rather than for the first screen
   that happened to need it: a second, identical-looking row of chips defined
   separately is exactly how two filters on two screens come to look almost
   but not quite the same.

   Links rather than a <select>: every state is bookmarkable, the back button
   works, and there is no change event to hook — which matters, because the CSP
   forbids the inline handler a <select> would need.

   flex-wrap because the checklist has six of these and a phone has room for
   about three; white-space keeps a single tab from breaking mid-label when it
   does wrap. */
.filter-tabs { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0 0 1rem; }
.filter-tab {
    font-size: .85rem;
    font-weight: 600;
    color: var(--muted);
    text-decoration: none;
    padding: .35rem .8rem;
    border-radius: 999px;
    border: 1px solid var(--border);
    background: var(--surface);
    white-space: nowrap;
}
.filter-tab:hover { color: var(--brand); }
.filter-tab.active { color: var(--on-brand); background: var(--brand); border-color: var(--brand); }

/* Overdue is the one tab worth noticing before it is read, and the one whose
   count deliberately does not add into the others. The text colour carries
   that and the border is left alone: --err-bg is LIGHTER than --border in the
   light theme, so tinting the border there would have made this tab look
   fainter than its neighbours rather than more urgent. Tinted only while
   inactive — as the active tab it takes the same brand fill as the rest, so
   "which filter am I looking at" stays a single visual question. --err has a
   dark-theme value, so this reads in either theme (5.6:1 on the dark
   surface). */
.filter-tab-alert:not(.active) { color: var(--err); }
.filter-tab-alert:not(.active):hover { color: var(--err); border-color: var(--err); }

/* Sits directly under the tabs, so it pulls up against their bottom margin. */
.filter-note { margin: -.55rem 0 1rem; }

/* --- Notification severity category tag (approval vs returned vs reminder).
   Text label + colour so the urgency cue is not colour-only. --- */
.notif-cat {
    display: inline-block;
    font-size: .65rem;
    font-weight: 700;
    letter-spacing: .03em;
    text-transform: uppercase;
    padding: .1rem .45rem;
    border-radius: 999px;
    white-space: nowrap;
}
.notif-cat-approved { color: var(--ok); background: var(--ok-bg); }
.notif-cat-action   { color: var(--err); background: var(--err-bg); }
.notif-cat-reminder { color: var(--info); background: var(--info-bg); }
.notif-cat-update   { color: var(--muted); background: var(--border); }

/* --- Responsive stacked table (narrow screens) — opt-in via .table--stack.
   Used by the faculty checklist so each requirement reads as a labelled card
   on a phone. Data tables that must stay tabular (e.g. the monitoring matrix)
   simply do not take this class. Pure CSS, no JS. --- */
@media (max-width: 640px) {
    .table--stack thead {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
    }
    .table--stack,
    .table--stack tbody,
    .table--stack tr,
    .table--stack td { display: block; width: 100%; }
    .table--stack tr { border-bottom: 1px solid var(--border); padding: .35rem 0; }
    .table--stack tbody tr:last-child { border-bottom: none; }
    .table--stack td {
        border-bottom: none;
        white-space: normal;
        display: flex;
        flex-wrap: wrap;
        justify-content: space-between;
        gap: .35rem 1rem;
        padding: .3rem .9rem;
    }
    .table--stack td::before {
        content: attr(data-label);
        font-weight: 700;
        font-size: .7rem;
        text-transform: uppercase;
        letter-spacing: .04em;
        color: var(--muted);
        flex-shrink: 0;
    }
    /* The requirement title and its type are a stacked pair, not a label and
       a value to be pushed to opposite ends of the line by space-between. */
    .table--stack td .checklist-title,
    .table--stack td .checklist-subtitle { flex-basis: 100%; }
    /* Same reasoning for a secondary line under a cell's main value (the
       period list's "2026-2027 · 1st" subtitle): without a full-width basis,
       space-between spreads label / value / subtitle across one line instead
       of putting the subtitle underneath. */
    .table--stack td .muted-note { flex-basis: 100%; margin: .2rem 0 0; }
    .table--stack td.table-actions { display: block; }
    .table--stack td.table-actions::before { display: block; margin-bottom: .35rem; }
    /* The note row is already a full-width colspan cell here (see the
       [colspan] rules below), so all it needs is to stay joined to the row it
       belongs to: no border between them, and no second lot of cell padding
       above it. */
    .table--stack tr.has-note { border-bottom: none; padding-bottom: 0; }
    .table--stack .checklist-note-row { padding-top: 0; }
    /* Full-width cells (the "no rows yet" placeholder) span every column, so
       they have no single column to be labelled with — without this they'd
       render an empty bold ::before box above the message. */
    .table--stack td[colspan] { display: block; }
    .table--stack td[colspan]::before { content: none; }
}

/* --- Audit log (FR-30, FR-31) --- */
.action-label {
    display: inline-block;
    font-size: .78rem;
    font-weight: 600;
    color: var(--brand);
    background: var(--info-bg);
    padding: .1rem .5rem;
    border-radius: 6px;
}

.ip-address {
    font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
    font-size: .85rem;
    color: var(--muted);
}

/* --- Reports page (FR-24, FR-25): CSV download links + print-only title.
   The title area is hidden on screen (the on-screen heading already shows
   it) and shown only in the print/PDF output, once the topbar's branding
   is hidden. --- */
.csv-links { margin: .5rem 0 2rem; }
/* Visible to screen readers only — used to disambiguate link text that repeats
   once per table row ("Details", "Download") so it is meaningful out of
   context. Same clip technique as the .table--stack sr-only thead. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.print-only { display: none; }

@media print {
    /* Ink, and it would sit under the report table. */
    .has-brand-mark::before { display: none; }

    /* Anything only useful on screen: navigation, the period selector, and
       every button/CSV link — none of it belongs on a printed report. */
    .topbar,
    .subnav,
    .filter-form,
    .csv-links,
    button,
    .btn-sm,
    .btn-primary,
    .btn-primary-sm,
    .btn-secondary,
    .btn-danger,
    .btn-inline {
        display: none !important;
    }

    .print-only { display: block; margin: 0 0 1rem; font-weight: 700; }

    body { background: #fff; color: #000; }
    .wrap { max-width: none; margin: 0; padding: 0; }
    .page-head { margin: 0 0 1rem; }
    .page-head .sub, .muted-note, .detail { color: #000; }

    .badge { background: #fff; color: #000; border: 1px solid #000; }

    .cards { display: block; }
    .card {
        display: inline-block;
        width: 30%;
        vertical-align: top;
        margin: 0 1.5% .5rem 0;
        border: 1px solid #000;
        box-shadow: none;
        background: #fff;
        break-inside: avoid;
    }
    .card.ok, .card.err { background: #fff; }
    .card h2, .card .status, .card .detail { color: #000; }

    .section-title { color: #000; break-after: avoid; }

    .table-wrap { border: 1px solid #000; box-shadow: none; overflow: visible; }
    .table { font-size: .85rem; }
    .table th, .table td {
        border: 1px solid #000;
        color: #000;
        white-space: normal;
    }
    .table thead th { background: #fff; }
    .table tbody tr { break-inside: avoid; }
    .table tbody tr:hover { background: #fff; }

    .status-pill { border: 1px solid #000; background: #fff !important; color: #000 !important; }

    .alert { border: 1px solid #000; background: #fff; color: #000; }

    a { color: #000; text-decoration: none; }
}

/* --- Dashboard deadline calendar (FR-39) ---
   A month grid on both dashboards marking the days that carry requirement
   deadlines. Deliberately NOT built on .table: that class is nowrap with
   ~.9rem horizontal padding per cell and picks up the .table--stack phone
   rules, all of which fight a fixed seven-column grid. Every cue below is a
   class, never an inline style — the CSP (default-src 'self', no
   'unsafe-inline') drops inline style attributes outright, and a calendar is
   exactly where per-cell inline colouring is tempting. */
.calendar-section { margin: 0 0 1.5rem; }

.calendar-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: var(--shadow);
    padding: 1rem 1.1rem 1.1rem;
}

.calendar-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    margin: 0 0 .75rem;
}

.calendar-month { margin: 0; font-size: 1rem; font-weight: 700; }

/* Month navigation: plain links, styled as controls. They are the whole
   no-JavaScript story for this component — each one is a GET of the same
   dashboard with a different ?month=YYYY-MM. */
.calendar-nav {
    font-size: .85rem;
    font-weight: 600;
    color: var(--brand);
    text-decoration: none;
    white-space: nowrap;
    padding: .3rem .55rem;
    border: 1px solid var(--border);
    border-radius: 8px;
}
.calendar-nav:hover { background: var(--bg); }
.calendar-nav:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

/* table-layout: fixed is what keeps seven columns inside a ~360px phone
   viewport. Left to size themselves, the columns take their width from the
   widest cell content (a two-digit date plus a count badge) and the table
   overflows sideways — the classic seven-column phone bug. Fixed layout
   divides the available width evenly instead, so the grid always fits and the
   cells simply get narrower. */
.calendar {
    width: 100%;
    table-layout: fixed;
    border-collapse: collapse;
}

.calendar th {
    padding: .2rem 0 .45rem;
    font-size: .7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--muted);
    text-align: center;
}

.cal-cell { padding: 2px; text-align: center; vertical-align: middle; }

/* The child combinator matters: it styles the cell's own box (the plain date,
   or the link that wraps a date with deadlines) without also hitting the
   .cal-day nested INSIDE that link. */
.cal-cell > .cal-day,
.cal-cell > .cal-link {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .1rem;
    min-height: 2.5rem;
    padding: .3rem .15rem;
    border: 1px solid transparent;
    border-radius: 8px;
    font-size: .85rem;
    color: var(--muted);
    text-decoration: none;
}

/* A day with deadlines is the only interactive cell, so it gets the filled
   treatment; overdue days (FR-20's language, same as the checklist's Overdue
   marker) swap to the error tokens. */
.cal-cell > .cal-link {
    color: var(--brand);
    background: var(--info-bg);
}
.cal-cell.has-overdue > .cal-link {
    color: var(--err);
    background: var(--err-bg);
}
.cal-cell > .cal-link:hover { border-color: currentColor; }
.cal-cell > .cal-link:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

.cal-link .cal-day { font-size: .9rem; font-weight: 700; }
.cal-link .cal-count { font-size: .65rem; font-weight: 700; line-height: 1; }

/* Today is marked by the BORDER, a deadline by the fill, so the two read as
   different things when they land on the same date — and the difference
   survives for anyone who cannot separate the two colours. */
.cal-cell.is-today > .cal-day,
.cal-cell.is-today > .cal-link {
    border: 2px dashed var(--brand);
    color: var(--brand);
    font-weight: 700;
}

.cal-legend {
    list-style: none;
    margin: .8rem 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: .3rem 1rem;
    font-size: .8rem;
    color: var(--muted);
}
.cal-legend-item { display: flex; align-items: center; gap: .4rem; }
.cal-swatch {
    width: .9rem;
    height: .9rem;
    flex-shrink: 0;
    border: 1px solid var(--border);
    border-radius: 4px;
}
.cal-swatch-today { border: 2px dashed var(--brand); background: transparent; }
.cal-swatch-deadline { background: var(--info-bg); border-color: var(--info); }
.cal-swatch-overdue { background: var(--err-bg); border-color: var(--err); }

/* The month's deadlines in words, under the grid: a 2.5rem cell only has room
   for a count, and on a phone that count is all the grid can show at all. */
.cal-list {
    list-style: none;
    display: grid;
    gap: .5rem;
    margin: .9rem 0 0;
    padding: .9rem 0 0;
    border-top: 1px solid var(--border);
}
.cal-list-item { display: flex; gap: .6rem; font-size: .88rem; }
.cal-list-date {
    flex: 0 0 3.2rem;
    font-size: .75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--muted);
    padding-top: .15rem;
}
.cal-list-title { min-width: 0; word-break: break-word; }
.cal-list-type { display: block; }

.cal-note { margin: .85rem 0 0; }

.cal-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .75rem;
    margin: .9rem 0 0;
}

/* Phone width: the grid still has to fit seven columns, so the cells lose
   padding rather than the table gaining a scrollbar. */
@media (max-width: 480px) {
    .calendar-card { padding: .85rem .6rem .9rem; }
    .cal-cell { padding: 1px; }
    .cal-cell > .cal-day,
    .cal-cell > .cal-link {
        min-height: 2.2rem;
        padding: .25rem .05rem;
        font-size: .8rem;
    }
    .cal-link .cal-day { font-size: .82rem; }
    .calendar th { font-size: .62rem; }
}

/* FR-41 in-app document viewer. aspect-ratio rather than a fixed height so the
   frame keeps a page-like shape on a phone instead of becoming a letterbox;
   min-height stops it collapsing where aspect-ratio is unsupported. Tokens
   only, so the surround follows the dark theme. */
.doc-actions {
    display: flex;
    align-items: center;
    gap: .75rem;
    flex-wrap: wrap;
}
.doc-viewer {
    margin-top: 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius, 8px);
    overflow: hidden;
    background: var(--bg);
}
.doc-frame {
    display: block;
    width: 100%;
    /* Height from the VIEWPORT, not an aspect ratio. A ratio sizes the frame
       from its own width and so ignores how tall the screen actually is: on a
       wide monitor it produced a frame taller than the window, and on a laptop
       a short one with empty space under it. This takes what is left after the
       top bar and the page's own furniture, so the page fills the screen it
       has. The subtraction is deliberately a little conservative — better a
       short scroll than a frame that overhangs the window. */
    height: calc(100vh - 23rem);
    min-height: 24rem;
    border: 0;
}
@media (max-width: 480px) {
    .doc-frame {
        height: calc(100vh - 13rem);
        min-height: 20rem;
    }
}

/* While a month is being fetched. Deliberately subtle: the swap is usually
   fast enough that a spinner would flash, but with no cue at all a slow month
   looks like a dead button. Tokens only. */
.calendar-section[aria-busy="true"] .calendar-card {
    opacity: .55;
    transition: opacity .12s ease-in;
}
.calendar-section[aria-busy="true"] .calendar-nav {
    pointer-events: none;
}

/* --- The college seal, faint, behind every page ---------------------------
   One fixed layer on the shell rather than a background on each screen, so it
   does not scroll with the content or repeat per card, and so there is exactly
   one place to tune it. It sits above the page background and below everything
   else: cards and the sidebar are opaque, so the mark shows through the gutters
   and around the content, which is what makes it read as a watermark rather
   than as a picture behind text.

   pointer-events: none so it can never intercept a click. Only painted when a
   logo has actually been supplied (.has-brand-mark), so no request is made for
   a file that is not there. One rule per format because the CSP forbids the
   inline style that would otherwise let PHP pass the filename in. */
.app-shell { position: relative; }

.has-brand-mark::before {
    content: "";
    position: fixed;
    /* Centred on the CONTENT PANEL, not the browser window: with the sidebar
       occupying a fixed column, a viewport-centred mark sits visibly off to the
       left of the area it is supposed to sit behind. Inset by the sidebar width
       and the top bar's height so the seal is centred in what the reader is
       actually looking at. Still fixed, so it does not travel with the scroll. */
    top: var(--topbar-h);
    right: 0;
    bottom: 0;
    left: 0;
    background-repeat: no-repeat;
    background-position: center;
    /* Big enough to read as the college seal behind the page rather than a
       small stamp in the middle of it. vmin so it scales off the SHORTER axis
       and cannot overflow a short window; the px cap stops it from becoming a
       texture on a large monitor. */
    background-size: min(78vmin, 780px);
    opacity: var(--watermark-opacity);
    pointer-events: none;
    z-index: 0;
}
/* Signed in, at desktop, the content panel starts after the sidebar. */
.app-shell-auth.has-brand-mark::before { left: var(--sidebar-w); }
/* Signed out there is no sidebar and no top bar, so it fills the window. */
.app-shell:not(.app-shell-auth).has-brand-mark::before { top: 0; left: 0; }

/* Must come AFTER the two rules above: a media query adds no specificity, so
   source order is what decides. Declared earlier (inside the <=899px block up
   the file) it was silently losing to the desktop inset, and the mark stayed
   240px off-centre on a phone. */
@media (max-width: 899px) {
    /* Sidebar is a stacked drawer here, so the panel is the full width again. */
    .app-shell-auth.has-brand-mark::before { left: 0; }
}
.brand-mark-svg::before  { background-image: url("../img/logo.svg"); }
.brand-mark-png::before  { background-image: url("../img/logo.png"); }
.brand-mark-webp::before { background-image: url("../img/logo.webp"); }
.brand-mark-jpg::before  { background-image: url("../img/logo.jpg"); }
.brand-mark-jpeg::before { background-image: url("../img/logo.jpeg"); }

/* Stacking, stated once. The watermark is a fixed pseudo-element, so without an
   explicit layer it would paint over the page.

   ONLY the content column gets this — never a blanket `.app-shell > *`. That
   selector has the same specificity as .topbar and .sidebar and sits later in
   the file, so it overrode BOTH their position (the chrome scrolled away) and
   their z-index (the content painted over the top bar). Each layer is declared
   on the thing it belongs to: watermark 0, content 1, top bar 20, sidebar 30. */
.app-shell > .app-main {
    position: relative;
    z-index: 1;
}

/* --- Signed-out layout ----------------------------------------------------
   The sign-in card is the only thing on the page, so it is centred in the
   window rather than pushed down from the top. min-height on the shell (not
   the card) is what lets `margin: auto` resolve vertically; the card keeps its
   own max-width. Padding rather than pure centring so a short viewport, or a
   validation error growing the card, scrolls instead of clipping it. */
.app-shell:not(.app-shell-auth) {
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: 100vh;
    padding: 2rem 1rem;
}
.app-shell:not(.app-shell-auth) .app-main { width: 100%; }
.app-shell:not(.app-shell-auth) .auth-card { margin: 0 auto; }

/* --- Fixed chrome: the offsets the out-of-flow bars leave behind -----------
   The bar is out of flow, so nothing reserves its height. ONE offset, on the
   shell, rather than one per child: at narrow widths the sidebar is a drawer
   that may or may not be in flow (it is display:none when closed), so padding
   the children instead would either double up when the drawer is open or leave
   the content under the bar when it is closed. The sidebar is fixed at desktop
   and so is unaffected by the shell padding either way. */
.app-shell-auth { padding-top: var(--topbar-h); }

@media (min-width: 900px) {
    /* The bar starts where the sidebar ends. */
    .app-shell-auth > .topbar { left: var(--sidebar-w); }
    /* The sidebar owns the full viewport height; its own scrollbar handles a
       menu longer than that. */
    .app-shell-auth > .sidebar { height: 100vh; }
}

@media (max-width: 899px) {
    /* Stacked: the sidebar is a drawer in normal flow again, so it must not be
       fixed, and the bar spans the full width. */
    .app-shell-auth > .sidebar {
        position: static;
        width: auto;
        height: auto;
    }
    .app-shell-auth > .topbar { left: 0; }
}

/* Extracted .docx text (FR-41). Pre-wrap keeps the paragraph breaks the
   extractor preserved without letting a long unbroken line force the page
   sideways; the max-height plus scroll stops a 30,000-character chapter from
   burying the actions above it. tabindex on the element makes the scroll region
   reachable by keyboard, which a plain overflow container is not. */
.doc-text {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    max-height: 60vh;
    overflow-y: auto;
    padding: 1rem 1.1rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--surface);
    font-size: .9rem;
    line-height: 1.65;
}
.doc-text:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
@media (max-width: 480px) {
    .doc-text { max-height: 50vh; font-size: .85rem; }
}

/* --- Rendered .docx (FR-41) -----------------------------------------------
   A reading surface, not a replica of a Word page: the goal is that a reviewer
   can read the document and judge it, so the type is comfortable and the
   structure (headings, emphasis, tables, images) survives. Page geometry does
   not, and the note above the render says so. Tokens throughout, so it follows
   the dark theme. */
.doc-render {
    max-height: 72vh;
    overflow-y: auto;
    /* The desk the sheets sit on, not the paper itself — the pages inside carry
       the surface colour, and the gap between them is what makes the document
       read as separate pages rather than one scroll. */
    padding: 1.25rem;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg);
    font-size: .92rem;
    line-height: 1.7;
}
.doc-render:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.doc-note { margin-bottom: .75rem; }

.doc-render .dv-p { margin: 0 0 .7rem; }
.doc-render .dv-li { padding-left: 1.5rem; position: relative; }
.doc-render .dv-li::before { content: "•"; position: absolute; left: .5rem; color: var(--muted); }
.doc-render .dv-h,
.doc-render .dv-title {
    margin: 1.4rem 0 .6rem;
    line-height: 1.3;
}
.doc-render .dv-title { font-size: 1.25rem; }
.doc-render h3.dv-h { font-size: 1.15rem; }
.doc-render h4.dv-h { font-size: 1.05rem; }
.doc-render h5.dv-h,
.doc-render h6.dv-h { font-size: .95rem; }
.doc-render .dv-link { color: var(--brand); text-decoration: underline; }
.doc-render .dv-tab { display: inline-block; width: 2em; }

/* Its own scroll container: a wide Word table must not drag the page sideways. */
.doc-render .dv-table-wrap { overflow-x: auto; margin: 0 0 1rem; }
.doc-render .dv-table {
    border-collapse: collapse;
    width: 100%;
    font-size: .85rem;
}
.doc-render .dv-table td {
    border: 1px solid var(--border);
    padding: .4rem .55rem;
    vertical-align: top;
}
.doc-render .dv-table .dv-p:last-child { margin-bottom: 0; }

.doc-render .dv-img {
    max-width: 100%;
    height: auto;
    display: block;
    margin: .75rem 0;
}

@media (max-width: 480px) {
    .doc-render { padding: 1rem; max-height: 60vh; font-size: .88rem; }
}

/* --- Document modal (FR-41) ------------------------------------------------
   Sized to the viewport rather than the content: a document is long, so the
   dialog claims a fixed share of the screen and scrolls inside, instead of
   growing until it is taller than the window and unreachable at the bottom.
   The head is sticky within it so Close is always in reach. */
.doc-modal {
    /* Wider than a form dialog would be: the content is a document, often with
       wide tables, and 920px left them cramped while the dialog had room to
       spare. Capped so the line length stays readable on a large monitor. */
    width: min(1100px, 94vw);
    max-width: 94vw;
    height: min(88vh, 900px);
    padding: 0;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface);
    color: var(--text);
    box-shadow: 0 18px 48px rgba(40, 32, 6, .28);
    overflow: hidden;
}
.doc-modal::backdrop {
    background: rgba(26, 22, 6, .55);
}
.doc-modal-head {
    position: sticky;
    top: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: .9rem 1.25rem;
    border-bottom: 1px solid var(--border);
    background: var(--surface);
}
.doc-modal-title {
    margin: 0;
    font-size: 1.05rem;
    line-height: 1.3;
    overflow-wrap: anywhere;
}
.doc-modal-close {
    flex: none;
    width: 2rem;
    height: 2rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg);
    color: var(--text);
    font-size: 1.15rem;
    line-height: 1;
    cursor: pointer;
}
.doc-modal-close:hover { background: var(--gold); color: var(--gold-ink); }
.doc-modal-close:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.doc-modal-body {
    height: calc(100% - 3.6rem);
    overflow-y: auto;
    padding: 1.1rem 1.25rem 1.5rem;
}
/* Inside the dialog the cards are already framed by it, so they drop their own
   frame rather than stacking a box inside a box. */
.doc-modal-body .form-card {
    /* max-width matters most here: .form-card is capped at 480px because it was
       built for the sign-in and profile forms, and without resetting it the
       document sat in a 480px column inside a 900px dialog with the rest empty.
       A document is not a form and wants the whole width. */
    max-width: none;
    width: 100%;
    border: 0;
    box-shadow: none;
    padding: 0;
    margin: 0 0 1.25rem;
    background: transparent;
}
/* The render and the PDF frame each get their own scroll on the page; inside
   the dialog the dialog scrolls, so they grow instead of nesting scrollbars. */
.doc-modal-body .doc-render { max-height: none; overflow: visible; }
.doc-modal-body .doc-text { max-height: none; }
/* The dialog already caps itself against the viewport, so inside it the frame
   is sized as a share of the dialog rather than of the window — the page rule
   would overhang and make the dialog scroll twice. */
.doc-modal-body .doc-frame { height: 66vh; min-height: 24rem; }

@media (max-width: 480px) {
    .doc-modal { width: 96vw; height: 92vh; border-radius: 10px; }
    .doc-modal-body { padding: .9rem 1rem 1.2rem; }
}

/* The dialog's own actions. "Open full page" sits beside Close because the
   dialog is capped at a share of the viewport, and a long document is easier
   to read on the page that has the whole window. */
.doc-modal-actions {
    display: flex;
    align-items: center;
    gap: .6rem;
    flex: none;
}
.doc-modal-full {
    font-size: .85rem;
    font-weight: 600;
    color: var(--brand);
    text-decoration: none;
    white-space: nowrap;
    padding: .35rem .6rem;
    border: 1px solid var(--border);
    border-radius: 6px;
}
.doc-modal-full:hover { background: var(--gold); color: var(--gold-ink); border-color: var(--gold); }
.doc-modal-full:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

@media (max-width: 480px) {
    /* The title needs the room more than the label does on a phone. */
    .doc-modal-full { padding: .3rem .45rem; font-size: .8rem; }
}

/* The standalone viewer page has the same problem for the same reason: its
   cards are .form-card, so the document was boxed into 480px of a 900px column.
   Scoped to [data-doc-body], which exists only on that page. */
[data-doc-body] .form-card {
    max-width: none;
    width: 100%;
    margin-left: 0;
    margin-right: 0;
}
[data-doc-body] .page-head { max-width: none; }

/* --- Formatting carried over from the document (FR-41) ---------------------
   The renderer emits these as CLASSES because the CSP forbids inline style, so
   every visual property has to be a fixed name. That is also why the indent is
   quantised: a class cannot carry an arbitrary measurement. */
.doc-render .dv-center { text-align: center; }
.doc-render .dv-justify { text-align: justify; }
.doc-render .dv-right { text-align: right; }

/* Word's default first-line indent is 720 twips — half an inch. */
.doc-render .dv-indent { text-indent: 3em; }

.doc-render .dv-pad-1 { margin-left: 2em; }
.doc-render .dv-pad-2 { margin-left: 4em; }
.doc-render .dv-pad-3 { margin-left: 6em; }
.doc-render .dv-pad-4 { margin-left: 8em; }

.doc-render .dv-red { color: var(--doc-red); }
.doc-render .dv-blue { color: var(--doc-blue); }
.doc-render .dv-green { color: var(--doc-green); }

.doc-render .dv-mark {
    background: var(--doc-mark);
    color: var(--doc-mark-ink);
    padding: 0 .1em;
}

/* A centred heading must not also be indented — Word's own precedence, and
   without this a centred-and-indented paragraph drifts off centre. */
.doc-render .dv-center.dv-indent,
.doc-render .dv-right.dv-indent { text-indent: 0; }

/* --- Pages (FR-41) ---------------------------------------------------------
   Proportioned to A4 via min-height rather than a fixed height: the split
   comes from Word's recorded break positions, not from measuring text, so a
   page holds whatever fell on it and must be free to grow rather than clip. */
.doc-render .dv-page {
    position: relative;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 2px;
    box-shadow: var(--shadow);
    margin: 0 auto 1.25rem;
    /* Width and padding come from the paper and margin classes below, which the
       renderer derives from the document's own w:sectPr. CSS inches are a real
       unit (96px), so a Letter page is drawn at the size it would print. */
    box-sizing: border-box;
    max-width: 100%;
}
.doc-render .dv-page:last-child { margin-bottom: 0; }

.doc-render .dv-page-num {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 1rem;
    margin: 0;
    text-align: center;
    font-size: .75rem;
    color: var(--muted);
}

/* A single unpaginated page is not a page — drop the paper metaphor rather
   than pretend a continuous scroll is one sheet. */
.doc-render .dv-page:only-child {
    min-height: 0;
    box-shadow: none;
    border: 0;
    padding: 1rem 1.25rem;
    max-width: none;
}
.doc-render .dv-page:only-child .dv-page-num { display: none; }

@media (max-width: 640px) {
    .doc-render { padding: .75rem; }
    .doc-render .dv-page {
        padding: 1.5rem 1.25rem 2.5rem;
        min-height: 0;
    }
}

/* --- Paper (FR-41) ---------------------------------------------------------
   Sizes as declared in the document, not assumed. min-height rather than
   height: the page split comes from Word's recorded break positions, and this
   renderer's line heights are not Word's, so a page must be free to grow
   rather than clip its own content. Content therefore never disappears; a page
   may simply run a little longer than it does in Word. */
.doc-render .dv-paper-letter { width: 8.5in; min-height: 11in; }
.doc-render .dv-paper-a4     { width: 8.268in; min-height: 11.693in; }
.doc-render .dv-paper-legal  { width: 8.5in; min-height: 14in; }
.doc-render .dv-paper-a5     { width: 5.83in; min-height: 8.27in; }
.doc-render .dv-paper-b5     { width: 6.93in; min-height: 9.84in; }

.doc-render .dv-paper-letter.dv-landscape { width: 11in; min-height: 8.5in; }
.doc-render .dv-paper-a4.dv-landscape     { width: 11.693in; min-height: 8.268in; }
.doc-render .dv-paper-legal.dv-landscape  { width: 14in; min-height: 8.5in; }
.doc-render .dv-paper-a5.dv-landscape     { width: 8.27in; min-height: 5.83in; }
.doc-render .dv-paper-b5.dv-landscape     { width: 9.84in; min-height: 6.93in; }

/* Margins in quarter-inch steps, one class per side, as the document sets them.
   The 1.5in left on both of this college's submissions is a binding margin —
   visibly not the 1in default, and exactly the kind of thing a fixed padding
   would have thrown away. */
.doc-render .dv-mt-1 { padding-top: .25in; }  .doc-render .dv-mb-1 { padding-bottom: .25in; }
.doc-render .dv-mt-2 { padding-top: .5in; }   .doc-render .dv-mb-2 { padding-bottom: .5in; }
.doc-render .dv-mt-3 { padding-top: .75in; }  .doc-render .dv-mb-3 { padding-bottom: .75in; }
.doc-render .dv-mt-4 { padding-top: 1in; }    .doc-render .dv-mb-4 { padding-bottom: 1in; }
.doc-render .dv-mt-5 { padding-top: 1.25in; } .doc-render .dv-mb-5 { padding-bottom: 1.25in; }
.doc-render .dv-mt-6 { padding-top: 1.5in; }  .doc-render .dv-mb-6 { padding-bottom: 1.5in; }
.doc-render .dv-mt-7 { padding-top: 1.75in; } .doc-render .dv-mb-7 { padding-bottom: 1.75in; }
.doc-render .dv-mt-8 { padding-top: 2in; }    .doc-render .dv-mb-8 { padding-bottom: 2in; }

.doc-render .dv-ml-1 { padding-left: .25in; }  .doc-render .dv-mr-1 { padding-right: .25in; }
.doc-render .dv-ml-2 { padding-left: .5in; }   .doc-render .dv-mr-2 { padding-right: .5in; }
.doc-render .dv-ml-3 { padding-left: .75in; }  .doc-render .dv-mr-3 { padding-right: .75in; }
.doc-render .dv-ml-4 { padding-left: 1in; }    .doc-render .dv-mr-4 { padding-right: 1in; }
.doc-render .dv-ml-5 { padding-left: 1.25in; } .doc-render .dv-mr-5 { padding-right: 1.25in; }
.doc-render .dv-ml-6 { padding-left: 1.5in; }  .doc-render .dv-mr-6 { padding-right: 1.5in; }
.doc-render .dv-ml-7 { padding-left: 1.75in; } .doc-render .dv-mr-7 { padding-right: 1.75in; }
.doc-render .dv-ml-8 { padding-left: 2in; }    .doc-render .dv-mr-8 { padding-right: 2in; }

/* A page wider than the screen would otherwise scroll the whole viewer
   sideways. Below the width of a sheet the paper stops being literal: it keeps
   its proportions loosely but fits the screen, because an unreadable accurate
   page is worth less than a readable approximate one. */
@media (max-width: 900px) {
    .doc-render [class*="dv-paper-"] {
        width: 100%;
        min-height: 0;
    }
    .doc-render [class*="dv-ml-"] { padding-left: 1.25rem; }
    .doc-render [class*="dv-mr-"] { padding-right: 1.25rem; }
    .doc-render [class*="dv-mt-"] { padding-top: 1.5rem; }
    .doc-render [class*="dv-mb-"] { padding-bottom: 2rem; }
}

/* A live region mid-fetch. Same restraint as the calendar: the swap is usually
   too fast for a spinner, but with no cue at all a slow search looks like a
   dead button. */
[data-live-region][aria-busy="true"] {
    opacity: .55;
    transition: opacity .12s ease-in;
}

/* --- Giving the document the screen (FR-41) --------------------------------
   .wrap caps content at 900px. That is the right readable measure for forms and
   tables, and the wrong one for a document: on a 1920px screen it left roughly
   480px for the page itself once the sidebar, and the browser's own PDF toolbar
   and thumbnail rail, had taken their share — which is why a document opened at
   69% zoom and had to be read at arm's length.

   :has() is what lets the page widen its own container without a second body
   class or a per-view stylesheet. Where it is unsupported nothing matches and
   the page stays at 900px: the old behaviour, not a broken one. */
.wrap:has([data-doc-body]) {
    max-width: none;
    padding-left: 2rem;
    padding-right: 2rem;
    /* Measured, not guessed: at the page's own 3rem the frame's bottom edge
       fell about 70px below the fold on a 1080px screen. Everything above the
       frame — top bar, title block, card heading, toolbar — comes to roughly
       21rem, which is what the frame's height subtracts. */
    padding-top: 1.75rem;
}

/* Form screens keep the old measure. A 480px card centres itself in whatever
   column it is given, so in a 1240px one it drifted away from its own page
   title, which stays left-aligned — the heading ended up stranded at the far
   left of an empty band. Nothing is gained by the extra width there either: a
   form is a single column of fields, not a table.

   The viewer is excluded because it uses .form-card for the document itself,
   and that is the one card that wants the whole screen. */
.wrap:has(.form-card):not(:has([data-doc-body])) { max-width: 900px; }

/* Full screen is hidden until app.js confirms the browser allows it — the API
   is refused outright in some embedded and iframed contexts, and a button that
   does nothing is worse than no button. */
.doc-fullscreen { display: none; }
.can-fullscreen .doc-fullscreen { display: inline-block; }

.doc-tools {
    display: flex;
    justify-content: flex-end;
    margin: 1rem 0 .4rem;
}
/* The toolbar has taken over the gap the frame used to open with; keeping both
   left a visible step between the button and the document it acts on. */
.doc-viewer-shell .doc-viewer { margin-top: 0; }

/* The shell is what the browser promotes, so the button travels with it and
   stays reachable; :fullscreen matches only that promoted element. The frame
   drops its viewport maths here and simply takes the space left beside the
   toolbar. */
.doc-viewer-shell:fullscreen {
    display: flex;
    flex-direction: column;
    padding: .4rem .6rem .6rem;
    background: var(--bg);
}
.doc-viewer-shell:fullscreen .doc-tools { margin: 0 0 .4rem; }
.doc-viewer-shell:fullscreen .doc-viewer {
    display: flex;
    flex: 1 1 auto;
    /* Against the flex default of min-height:auto, which would let the frame
       push the shell past the screen instead of shrinking to fit it. */
    min-height: 0;
    margin-top: 0;
}
.doc-viewer-shell:fullscreen .doc-frame {
    flex: 1 1 auto;
    height: auto;
    min-height: 0;
}

@media (max-width: 899px) {
    /* The sidebar is a drawer at this width and the screen is small: the wider
       gutters would cost more than the width they buy back. */
    .wrap:has([data-doc-body]) { padding-left: 1.25rem; padding-right: 1.25rem; }
}

/* --- Batch D: upload notes, revision notes, released documents ----------
   All built from the existing tokens so both themes follow automatically;
   nothing here hard-codes a colour. */

/* FR-43: the note a faculty member attached to one upload. Sits under the
   filename in Version history, quiet enough not to compete with it. */
.upload-comment {
    display: block;
    margin-top: .2rem;
    font-size: .85rem;
    font-style: italic;
    color: var(--muted);
}

/* The same note on the upload form itself. Full width under the file picker
   rather than beside it: the row is already tight on a narrow screen. */
.upload-note {
    display: block;
    width: 100%;
    margin: .4rem 0;
    padding: .35rem .5rem;
    font-size: .85rem;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
}

/* FR-44: the reviewer's page-anchored notes, on the review form. */
.revision-notes { border: 1px solid var(--border); border-radius: 8px; padding: .75rem 1rem; }
.revision-notes legend { font-weight: 600; padding: 0 .4rem; }
.note-row { display: flex; gap: .75rem; align-items: flex-end; margin-top: .5rem; }
.note-row .note-page { flex: 0 0 6rem; }
.note-row .note-text { flex: 1 1 auto; }
.note-label { display: block; font-size: .8rem; color: var(--muted); margin-bottom: .15rem; }
.note-row input {
    width: 100%;
    padding: .35rem .5rem;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
}

/* FR-44 again, on the faculty member's side: the list they act on. */
.revision-note-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
.revision-note-list li {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: .6rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--brand);
    border-radius: 8px;
    padding: .6rem .9rem;
}
.note-page-link {
    flex: 0 0 auto;
    font-weight: 600;
    font-size: .85rem;
    white-space: nowrap;
}
/* "General" is not a link, so it must not look like one. */
.note-page-none { color: var(--muted); font-weight: 500; }
.note-body { flex: 1 1 14rem; }

/* FR-45: the Secretary's release form. */
.release-form {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 1rem 1.15rem;
    max-width: 34rem;
}

@media (max-width: 40rem) {
    /* Page and text stack rather than squeeze to unusable widths. */
    .note-row { flex-direction: column; align-items: stretch; gap: .35rem; }
    .note-row .note-page { flex: 1 1 auto; }
}

/* --- Live search panel (FR-37) ---
   Sits at the top of <main>, as a sibling of [data-page-content] rather than
   inside it, and app.js fills it with the results lifted out of /search while
   hiding the page underneath. Empty and hidden until then, so a browser
   without script never renders it and the top bar's form submits exactly as
   it always did.

   The [hidden] rule below is the load-bearing part. `hidden` is only a UA
   `display: none`, so ANY author rule setting `display` on one of these
   elements would silently beat it and leave the panel on screen permanently —
   including a rule added later by someone who has never read this comment.
   Scoped to these two attributes rather than declared globally on [hidden],
   which would reach markup this file knows nothing about. */
.live-search[hidden],
[data-page-content][hidden] { display: none !important; }

.live-search { margin: 0 0 1.25rem; }

/* The one line that is announced as the term changes. Styled as a statement
   about the search rather than as a result: same left-rule treatment as the
   other "here is what you are looking at" notes in the app. :empty so a
   cleared status does not leave an empty bordered box behind it. */
.live-search-status {
    margin: 0 0 1.25rem;
    padding: .55rem .85rem;
    font-size: .9rem;
    line-height: 1.5;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--brand);
    border-radius: 0 8px 8px 0;
}
.live-search-status:empty { display: none; }

/* The first heading inside the lifted results has the status line directly
   above it, which is already spaced; .section-title's own 2rem top margin
   doubles that into a gap that reads as a missing element. */
.live-search [data-live-search-target] > .section-title:first-child { margin-top: 0; }

/* --- The composition behind a percentage (FR-16, FR-17, FR-24) ---
   Sits under the figure cards on the two screens that have no status bar to
   spell the parts out. Pulled up against the card grid, because it belongs
   to the figures above it rather than to the section below. */
.figure-composition { margin: -.5rem 0 1.5rem; }

/* --- Review queue: filters and results (FR-12) ---
   The filter row carries five controls now, so it wraps; .filter-actions keeps
   the button and Clear together on whichever line they land on rather than
   letting Clear wrap away from the button it belongs beside. */
.filter-form .filter-actions {
    display: flex;
    align-items: center;
    gap: .5rem;
}

/* The count above the table. It is the first thing that changes when a filter
   is applied, which is what tells the reader the filter did anything at all on
   a result set too long to take in at a glance. */
.queue-count { margin: 0 0 .75rem; }

/* The submission time, beside its date. Quieter than the date because the date
   is what is scanned and the time only matters when a time range is set. */
.queue-time { margin-left: .4rem; font-size: .8rem; color: var(--muted); }

/* Dimmed while a live filter is in flight, so a slow response reads as "this
   is being updated" rather than as stale results that ignored the change.
   A transition, not a spinner: the swap is usually too fast to see one. */
[data-live-filter-region] { transition: opacity .12s ease-in-out; }
[data-live-filter-region].is-updating { opacity: .45; }
