html, body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

a, .btn-link {
    color: #006bb7;
}

.btn-primary {
    color: #fff;
    background-color: #1b6ec2;
    border-color: #1861ac;
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
  box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}

.content {
    padding-top: 1.1rem;
}

h1:focus {
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: #e50000;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}
/* --------------------------------------------------------------------------
   Blazor reconnect UI
   Blazor drives these states by toggling classes on #components-reconnect-modal:
     show / retrying  — a retry is in flight
     failed           — retries exhausted; a reload is needed
     rejected         — the server discarded the circuit; state is gone
   Hidden by default, because the element is always in the DOM.
   -------------------------------------------------------------------------- */
#components-reconnect-modal {
    display: none;
}

#components-reconnect-modal .reconnect-panel {
    display: none;
}

/* ── Retrying: a slim bar that never blocks the page ────────────────────── */
#components-reconnect-modal.components-reconnect-show,
#components-reconnect-modal.components-reconnect-retrying {
    display: block;
}

.reconnect-bar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 30000;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 8px 16px;
    background: #1565c0;
    color: #fff;
    font-size: 0.875rem;
    font-weight: 500;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
}

.reconnect-spinner {
    width: 14px;
    height: 14px;
    border: 2px solid rgba(255, 255, 255, 0.35);
    border-top-color: #fff;
    border-radius: 50%;
    animation: reconnect-spin 0.8s linear infinite;
}

@keyframes reconnect-spin {
    to { transform: rotate(360deg); }
}

/* ── Failed or rejected: the user has to act, so block and say so ───────── */
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-rejected {
    display: flex;
    position: fixed;
    inset: 0;
    z-index: 30000;
    align-items: center;
    justify-content: center;
    background: rgba(13, 27, 75, 0.72);
    backdrop-filter: blur(3px);
}

/* The bar belongs to the retry state only. */
#components-reconnect-modal.components-reconnect-failed .reconnect-bar,
#components-reconnect-modal.components-reconnect-rejected .reconnect-bar {
    display: none;
}

#components-reconnect-modal.components-reconnect-failed .reconnect-panel,
#components-reconnect-modal.components-reconnect-rejected .reconnect-panel {
    display: block;
    max-width: 420px;
    margin: 24px;
    padding: 28px 32px;
    border-radius: 16px;
    background: #fff;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
    text-align: center;
}

.reconnect-panel h2 {
    margin: 0 0 10px;
    font-size: 1.2rem;
    font-weight: 700;
    color: #0d1b4b;
}

.reconnect-panel p {
    margin: 0 0 20px;
    color: #64748b;
    line-height: 1.6;
    font-size: 0.925rem;
}

.reconnect-panel button {
    padding: 10px 26px;
    border: none;
    border-radius: 50px;
    background: #1565c0;
    color: #fff;
    font-weight: 600;
    font-size: 0.925rem;
    cursor: pointer;
}

.reconnect-panel button:hover {
    background: #0d47a1;
}

/* --------------------------------------------------------------------------
   Signup path chooser
   These two cards are the only control on the first signup screen, so they have
   to look pressable. Colours come from the MudBlazor palette variables rather
   than literals, so the cards follow the light/dark theme the visitor picked on
   the landing page.
   -------------------------------------------------------------------------- */
.signup-choice {
    cursor: pointer;
    height: 100%;
    text-align: center;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 14px;
    background-color: var(--mud-palette-surface);
    /* A resting shadow, so the card reads as raised before it is touched. */
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.10), 0 1px 2px rgba(0, 0, 0, 0.06);
    transition: box-shadow 0.22s ease, transform 0.22s ease, border-color 0.22s ease;
}

.signup-choice:hover {
    transform: translateY(-3px);
    border-color: var(--mud-palette-primary);
    box-shadow: 0 10px 24px rgba(0, 0, 0, 0.14), 0 4px 8px rgba(0, 0, 0, 0.08);
}

/* Settles back down on press, so the click has a physical result. */
.signup-choice:active {
    transform: translateY(-1px);
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
}

.signup-choice:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: 3px;
}

/* Shadows tuned for a light ground disappear on a dark one; deepen them so the
   card still separates from the surface behind it. */
:root[data-theme="dark"] .signup-choice {
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}

:root[data-theme="dark"] .signup-choice:hover {
    box-shadow: 0 10px 26px rgba(0, 0, 0, 0.62), 0 4px 10px rgba(0, 0, 0, 0.45);
}

@media (prefers-reduced-motion: reduce) {
    .signup-choice,
    .signup-choice:hover,
    .signup-choice:active {
        transform: none;
        transition: box-shadow 0.01ms;
    }
}

/* --------------------------------------------------------------------------
   App bar brand
   The link and its text take their colour from the app bar rather than from
   MudLink's palette entry. Left to MudLink, "SchoolMaster" rendered in the
   primary colour — the same colour the bar is painted — so it was invisible
   while still consuming space.
   -------------------------------------------------------------------------- */
.app-brand,
.app-brand .mud-typography {
    color: var(--mud-palette-appbar-text) !important;
}

/* The name stays on phones. It costs ~126px at h6, and the bar had 8px to spare,
   so the space is taken back from padding and margins rather than from the word:
   smaller wordmark and logo, tighter gutters, and the gaps between the right-hand
   controls closed up. Icon buttons stop at 40px — below that they stop being
   comfortable to tap, which is a worse trade than a slightly tighter bar. */
@media (max-width: 599.98px) {
    .app-brand-text {
        font-size: 1rem;
        letter-spacing: -0.2px;
    }

    .app-brand img {
        height: 28px;
    }

    /* me-2 on the logo, me-3 on the link — both far wider than needed here. */
    .app-brand img.me-2 {
        margin-right: 6px !important;
    }

    .app-brand.me-3 {
        margin-right: 4px !important;
    }

    .mud-appbar .mud-toolbar {
        padding-left: 8px;
        padding-right: 8px;
    }

    .mud-appbar .mud-icon-button {
        padding: 8px;
    }

    /* Close the gaps between language / theme / bell / menu. */
    .mud-appbar .mud-toolbar > .me-1,
    .mud-appbar .mud-toolbar > .mud-menu.me-1,
    .mud-appbar .mud-toolbar > .mud-badge-root.me-1 {
        margin-right: 0 !important;
    }

    /* The language menu renders as a text button; its default button padding is
       sized for a label far longer than "EN". */
    .mud-appbar .mud-menu > .mud-button-root {
        min-width: 0;
        padding-left: 6px;
        padding-right: 6px;
    }
}

/* Very narrow phones (older Androids at 320px) genuinely cannot hold the word and
   four controls at a tappable size, so there the logo carries it alone. */
@media (max-width: 359.98px) {
    .app-brand-text {
        display: none;
    }
}

/* --------------------------------------------------------------------------
   School dashboard
   .dashboard-banner, .quick-action-btn and .quick-action-icon were defined only
   in landing.css, which is loaded solely by PublicLayout. The dashboard runs
   under MainLayout, so on this page they resolved to nothing and the cards
   rendered completely unstyled. They live here now, where every signed-in page
   can see them. Colours use MudBlazor palette variables so the dashboard follows
   the light/dark theme rather than the landing page's own --dark literal.
   -------------------------------------------------------------------------- */
.dashboard-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
}

.dashboard-banner-text {
    min-width: 0;   /* lets the school name shrink instead of forcing the flex row wider */
}

.dashboard-school-name {
    font-weight: 800;
    color: var(--mud-palette-text-primary);
}

.dashboard-banner-year {
    color: var(--mud-palette-text-secondary);
}

/* ── Stat cards ─────────────────────────────────────────────────────────── */
.stat-card {
    height: 100%;
    /* One floor for every summary strip, so cards in a row align even when one
       has a shorter caption. Was 104px with 16px padding, which kept holding the
       cards 24px above their own content once the padding was tightened — the
       cards looked stubbornly tall no matter what the contents did. */
    /* 76 desktop / 72 phone: each sits exactly on the tallest natural content, so
       a Students card and a dashboard card are the same height rather than
       differing by a stray pixel or four, and neither carries dead space below
       its progress bar. The earlier 86/80 was measured before the padding and the
       inner gaps were tightened below, and left the cards standing ~8px taller
       than anything they contained. */
    min-height: 76px;
    padding: 10px 12px !important;
    border-radius: 12px;
}

/* The two gaps inside a card — under the heading row and under the caption — are
   4px each by way of MudBlazor's mb-1. Halving them takes 4px off every card in
   the app from one rule, rather than editing the markup of six pages. */
.stat-card > .mb-1 {
    margin-bottom: 2px !important;
}

.stat-card-label {
    color: var(--mud-palette-text-secondary);
}

.stat-card-value {
    font-weight: 800;
    line-height: 1.1;
}

/* Male/female split. The bar is a full-width female-coloured track with the male
   portion painted over it, so the two always total exactly 100% with no rounding
   gap between two floated halves. */
.stat-split-bar {
    height: 4px;
    /* Matches the gap the Students-page cards now use between their caption and
       progress bar, so the two cards line up row for row — and, with the sub
       margin below, brings a dashboard card to the same 76px as a list card
       instead of standing 5px taller than it. */
    margin-top: 2px;
    border-radius: 3px;
    overflow: hidden;
    background: var(--mud-palette-secondary);
}

.stat-split-male {
    display: block;
    height: 100%;
    background: var(--mud-palette-primary);
}

.stat-split-legend {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-top: 4px;
    font-size: 0.875rem;
    font-weight: 600;
    white-space: nowrap;
}

.stat-split-dot {
    color: var(--mud-palette-text-secondary);
}

/* Line 1 sits tight against line 2; the default caption line-height adds slack. */
.stat-card-head {
    line-height: 1.2;
}

.stat-card-sub {
    margin-top: 2px;
    /* This line is a hair larger than the captions the list cards use, and its
       default leading was the last thing holding a dashboard card taller than a
       Students card. Tightened so both land on the same 76px. */
    line-height: 1.2;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--mud-palette-text-secondary);
}

.stat-split-m { color: var(--mud-palette-primary); }
.stat-split-f { color: var(--mud-palette-secondary); }

/* ── Quick actions ──────────────────────────────────────────────────────── */
/* A row, not a column. Stacking the icon above two lines of text made every
   tile about 130px tall, and four of them took most of a dashboard that has
   more to carry. Beside the text the same tile is roughly 64px. */
.quick-action-btn {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 12px;
    height: 100%;
    padding: 10px 12px;
    cursor: pointer;
    border: 1px solid var(--mud-palette-lines-default);
    border-radius: 14px;
    background-color: var(--mud-palette-surface);
    /* Same pressable-card treatment as the signup chooser, so anything clickable
       in this app reads the same way. */
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.10), 0 1px 2px rgba(0, 0, 0, 0.06);
    transition: box-shadow 0.22s ease, transform 0.22s ease, border-color 0.22s ease;
}

.quick-action-btn:hover {
    transform: translateY(-3px);
    border-color: var(--mud-palette-primary);
    box-shadow: 0 10px 24px rgba(0, 0, 0, 0.14), 0 4px 8px rgba(0, 0, 0, 0.08);
}

.quick-action-btn:active {
    transform: translateY(-1px);
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
}

.quick-action-btn:focus-visible {
    outline: 2px solid var(--mud-palette-primary);
    outline-offset: 3px;
}

.quick-action-icon {
    width: 38px;
    height: 38px;
    border-radius: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

/* The label and its subtitle share the remaining width and truncate rather than
   wrapping, which is what kept the old tile two lines taller than it needed. */
.quick-action-text { min-width: 0; flex: 1; }

.quick-action-btn .quick-action-label {
    margin-top: 0 !important;
    line-height: 1.25;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.quick-action-btn .quick-action-sub {
    display: block;
    line-height: 1.3;
    color: var(--mud-palette-text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

:root[data-theme="dark"] .quick-action-btn {
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}

:root[data-theme="dark"] .quick-action-btn:hover {
    box-shadow: 0 10px 26px rgba(0, 0, 0, 0.62), 0 4px 10px rgba(0, 0, 0, 0.45);
}

@media (prefers-reduced-motion: reduce) {
    .quick-action-btn,
    .quick-action-btn:hover,
    .quick-action-btn:active {
        transform: none;
        transition: box-shadow 0.01ms;
    }
}

/* ── Phones ─────────────────────────────────────────────────────────────── */
@media (max-width: 599.98px) {
    /* Students and Employees only, square, side by side. */
    .dash-stat--wide {
        display: none;
    }

    /* Two tiles share a 390px row, which leaves each label about 106px. The
       subtitle only restates what the label already says, so it goes and the
       label gets the room to wrap to a second line instead of being cut down to
       "Register Stu…". The tile is no taller for it. */
    .quick-action-btn .quick-action-sub { display: none; }

    .quick-action-btn .quick-action-label {
        white-space: normal;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
        font-size: 0.8rem;
        line-height: 1.2;
    }

    .quick-action-btn  { padding: 9px 10px; gap: 10px; }
    .quick-action-icon { width: 34px; height: 34px; border-radius: 9px; }

    /* Padding deliberately absent: the shared .stat-card rule above governs it, so
       a dashboard card and a Students-page card are the same size. No aspect-ratio
       either — forcing 1:1 is what made these tall in the first place. */

    .dash-stat .stat-card-value {
        font-size: 1.35rem;
    }

    /* Full words are wider than "M"/"F", so they step down a little on a phone
       to stay on one line inside a half-width card. */
    .dash-stat .stat-split-legend {
        gap: 4px;
        font-size: 0.78rem;
    }

    /* One line, clipped with an ellipsis. Without min-width:0 above, a long
       school name would force the flex row wider instead of truncating. */
    .dashboard-school-name {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        font-size: 1.15rem;
    }
}

/* Text colours for the quick action cards, previously supplied by landing.css's
   --dark literal, which does not exist under MainLayout. */
.quick-action-label { color: var(--mud-palette-text-primary); }
.quick-action-sub   { color: var(--mud-palette-text-secondary); }

/* A card with nothing counted yet must not paint a full female-coloured bar —
   0 male / 0 female would otherwise read as "all female" rather than "no data". */
.stat-split-bar.stat-split--empty {
    background: var(--mud-palette-lines-default);
}

/* --------------------------------------------------------------------------
   Students page action toolbar
   Desktop shows the search box and the class filter side by side. On a phone
   there is not room for both at a usable width, so they take turns: the one not
   in use collapses to an icon button and the other takes the rest of the row.
   Both are always in the DOM — the MudSelect keeps its open/close state and its
   value across the swap, which re-rendering different markup would discard.
   -------------------------------------------------------------------------- */
.list-toolbar {
    flex-wrap: wrap;
}

.list-toolbar .toolbar-search {
    flex: 1;
    min-width: 200px;
}

.list-toolbar .toolbar-filter {
    min-width: 180px;
    max-width: 230px;
}

/* Pages with two filters put both in the one slot, so the swap works the same
   whether a page filters by one thing or two. Employees and Subjects used to
   opt out of the swap entirely and hide their filters in a collapse panel
   instead, which meant two different toolbars to learn and every select
   declared twice in the markup. */
.list-toolbar .toolbar-filter--multi {
    display: flex;
    gap: 12px;
    max-width: 460px;
    min-width: 0;
}

.list-toolbar .toolbar-filter--multi > * {
    flex: 1 1 0;
    min-width: 0;
}

/* The collapse icons exist only for narrow screens. */
.list-toolbar .toolbar-icon {
    display: none;
}

@media (max-width: 599.98px) {
    /* One line, no wrapping — the whole point of the swap. */
    .list-toolbar {
        flex-wrap: nowrap;
        gap: 8px !important;
    }

    .list-toolbar .toolbar-icon {
        display: inline-flex;
        flex-shrink: 0;
    }

    .list-toolbar .toolbar-filter {
        max-width: none;
        min-width: 0;
    }

    .list-toolbar .toolbar-search {
        min-width: 0;
    }

    /* Search owns the row; the filter is an icon. */
    .list-toolbar.toolbar--search .toolbar-search  { display: block; flex: 1; }
    .list-toolbar.toolbar--search .toolbar-filter  { display: none; }
    .list-toolbar.toolbar--search .toolbar-icon--filter { display: inline-flex; }
    .list-toolbar.toolbar--search .toolbar-icon--search { display: none; }

    /* Filter owns the row; search is an icon. */
    .list-toolbar.toolbar--filter .toolbar-search  { display: none; }
    .list-toolbar.toolbar--filter .toolbar-filter  { display: block; flex: 1; }
    .list-toolbar.toolbar--filter .toolbar-icon--filter { display: none; }
    .list-toolbar.toolbar--filter .toolbar-icon--search { display: inline-flex; }

    /* Two filters stack once they have the row to themselves. Side by side they
       would each get half a phone's width, which is not enough to read an option
       name. The extra class outranks the display:block set just above. */
    .list-toolbar.toolbar--filter .toolbar-filter.toolbar-filter--multi {
        display: flex;
        flex-direction: column;
        gap: 8px;
        max-width: none;
    }

    /* Sit the collapsed search icon level with the first filter rather than
       floating halfway down a stacked pair. Where only one filter occupies the
       row the two are the same height, so this changes nothing there. */
    .list-toolbar.toolbar--filter .toolbar-icon--search {
        align-self: flex-start;
    }
}

/* --------------------------------------------------------------------------
   Page header actions
   One button at every width. It used to swap for an icon-only button on phones;
   now the label stays and the button shrinks around it instead, so the action is
   always readable rather than relying on the icon alone.
   -------------------------------------------------------------------------- */
.hdr-action-full {
    flex-shrink: 0;
}

@media (max-width: 599.98px) {
    .hdr-action-full {
        font-size: 0.75rem;
        padding: 6px 12px;
        min-width: 0;
        white-space: nowrap;
    }

    /* The start icon is sized for the desktop label; scale it with the text and
       close the gap, or it takes more room than the word beside it. */
    .hdr-action-full .mud-icon-root {
        font-size: 1.05rem;
    }

    .hdr-action-full .mud-button-icon-start {
        margin-right: 4px;
        margin-left: -2px;
    }
}

/* --------------------------------------------------------------------------
   Fullscreen-on-phone dialogs
   Applied to the register, edit and class-details dialogs. On a phone the default centred dialog left 32px of dead margin on each side of
   a 390px screen — the form had 278px to work with. Below the sm breakpoint it
   takes the whole screen instead, and the action bar is pinned so Save is always
   reachable rather than nine fields down.
   -------------------------------------------------------------------------- */
@media (max-width: 599.98px) {
    .mud-dialog.dlg-fullscreen-sm {
        margin: 0;
        width: 100vw;
        max-width: 100vw;
        height: 100dvh;
        max-height: 100dvh;
        border-radius: 0;
        display: flex;
        flex-direction: column;
    }

    /* Only the content scrolls; title and actions stay put. */
    .mud-dialog.dlg-fullscreen-sm .mud-dialog-content {
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;
    }

    .mud-dialog.dlg-fullscreen-sm .mud-dialog-title,
    .mud-dialog.dlg-fullscreen-sm .mud-dialog-actions {
        flex: 0 0 auto;
    }

    .mud-dialog.dlg-fullscreen-sm .mud-dialog-actions {
        border-top: 1px solid var(--mud-palette-divider);
        background: var(--mud-palette-surface);
        gap: 8px;
    }

    /* Two tabs need no scroll arrows. MudBlazor renders them as
       .mud-tabs-scroll-button — two at 48px each, which left the labels 246px of a
       363px bar and clipped "Returning Students" to "RETUR". */
    .mud-dialog.dlg-fullscreen-sm .mud-tabs-scroll-button {
        display: none !important;
    }

    /* MudBlazor gives every tab min-width:160px. Two tabs fitted a 342px bar by
       luck; three do not (480px), and with the scroll arrows hidden the third tab
       became unreachable. !important because a plain override lost to MudBlazor. */
    .mud-dialog.dlg-fullscreen-sm .mud-tab {
        min-width: 0 !important;
        flex: 1 1 auto;
        padding-left: 8px;
        padding-right: 8px;
        font-size: 0.8rem;
    }

    /* Safety net: if labels ever outgrow the bar again, it swipes rather than
       silently hiding a tab. */
    .mud-dialog.dlg-fullscreen-sm .mud-tabs-tabbar-content {
        overflow-x: auto;
        scrollbar-width: none;
    }

    .mud-dialog.dlg-fullscreen-sm .mud-tabs-tabbar-content::-webkit-scrollbar {
        display: none;
    }

    /* Even at 0.8rem the two labels came to 363px against a 342px bar. The icons
       are what pushed them over, and the words already say which tab is which. */
    .mud-dialog.dlg-fullscreen-sm .mud-tab .mud-icon-root {
        display: none;
    }
}

/* The photo strip is avatar-beside-buttons on desktop. On a narrow screen the two
   buttons wrapped awkwardly beside an 80px avatar, so it stacks and centres. */
@media (max-width: 480px) {
    .reg-photo-strip {
        flex-direction: column;
        text-align: center;
        gap: 12px !important;
    }

    .reg-photo-strip .reg-photo-actions {
        justify-content: center;
    }
}

/* --------------------------------------------------------------------------
   Full-height page shell
   Used by the list pages that pin a footer (pagination) below a scrolling body.
   The height must be dvh, not vh: on a phone 100vh is the *large* viewport, which
   ignores the collapsing URL bar, so the pinned footer sat under browser chrome
   and the pagination could not be reached. vh is kept first as a fallback for
   browsers without dvh.
   -------------------------------------------------------------------------- */
.page-shell {
    display: flex;
    flex-direction: column;
    height: calc(100vh - 96px);
    height: calc(100dvh - 96px);
    overflow: hidden;
}

/* --------------------------------------------------------------------------
   Summary stat cards
   One set of metrics for every summary strip — Students, Classes, Employees,
   Subjects, Course Enrolment and the dashboard — so the cards read as the same
   component wherever they appear.
   -------------------------------------------------------------------------- */
.stat-card {
    border-radius: 12px;
}

.stat-card .stat-card-value,
.stat-card .mud-typography-h5 {
    line-height: 1.15;
}

/* Phones: tighter still, and the section gaps close up with them. Every row
   reclaimed here is a row of the list that becomes visible. */
@media (max-width: 599.98px) {
    .stat-card {
        padding: 10px 12px !important;
        /* Same floor as desktop on purpose: the padding is already identical, so a
           lower one here only let the dashboard cards shrink below the list cards
           and stopped the two matching. */
        min-height: 76px;
    }

    /* The cards are flex items and stretch to the tallest, so one wrapping caption
       set the height of the whole strip: "8 New · 0 Old" broke onto a second line
       and every card grew 24px to match it. At this size the text fits a 160px card
       comfortably, so it is held to one line. */
    .stat-card .mud-typography-caption {
        white-space: nowrap;
    }

    .stats-scroll {
        gap: 8px;
        margin-bottom: 12px !important;
    }

    .list-toolbar {
        margin-bottom: 12px !important;
    }

    /* The pagination row is the pinned footer; keep it compact but never clipped. */
    .page-shell .mud-pagination {
        margin-top: 8px !important;
    }
}

/* --------------------------------------------------------------------------
   Class details dialog
   Desktop keeps Registered and Available side by side. A phone cannot hold two
   panels plus five controls per row, so below the sm breakpoint the panels
   become sub-tabs and each Available row wraps onto a second line.
   -------------------------------------------------------------------------- */
.row-extras {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
}

/* The Registered/Available switch is a phone-only affordance. */
.detail-panel-switch {
    display: none;
}

@media (max-width: 599.98px) {
    .detail-panel-switch {
        display: flex;
        gap: 8px;
        margin-bottom: 12px;
    }

    .detail-panel-switch > * {
        flex: 1;
    }

    /* One panel at a time, full width. */
    .detail-split .panel-registered,
    .detail-split .panel-available {
        flex-basis: 100%;
        max-width: 100%;
    }

    .detail-split.show-registered .panel-available  { display: none; }
    .detail-split.show-available  .panel-registered { display: none; }

    /* Scoped to the dialog, not bare .detail-panel-scroll: the component renders
       its own <style> block after app.css, so an equal-specificity rule here
       loses on source order and the 360px inner scroll survived. */
    .dlg-fullscreen-sm .detail-panel-scroll {
        max-height: none;
    }

    /* Five controls will not fit one 280px line. The row wraps, and the amount
       and Required controls only appear once the row is actually ticked — so an
       untouched list stays one line per item and remains scannable. */
    .dlg-fullscreen-sm .detail-row {
        flex-wrap: wrap;
    }

    .dlg-fullscreen-sm .detail-row .row-extras {
        display: none;
    }

    .dlg-fullscreen-sm .detail-row.is-selected .row-extras {
        display: flex;
        flex-basis: 100%;
        padding-left: 44px;
        margin-top: 6px;
    }

    /* Widths are set inline for the desktop row, so they need overriding here. */
    .dlg-fullscreen-sm .detail-row.is-selected .row-extras .mud-input-control {
        width: 120px !important;
        flex-shrink: 0;
    }

    /* Hover lift is a pointer affordance; on touch it just makes rows jitter.
       Scoped for the same source-order reason as the scroll height above. */
    .dlg-fullscreen-sm .detail-row:hover {
        transform: none;
    }
}

/* The class-details header stats. Desktop keeps them; a phone gives the space to
   the list instead, and the same numbers are a tab away on the Students page. */
@media (max-width: 599.98px) {
    .detail-stats-strip {
        display: none !important;
    }

    /* Rows here follow the Students-page list: one line for the name, a muted
       second line for the code, and nothing clipped. */
    .dlg-fullscreen-sm .detail-row {
        padding: 10px 12px;
        gap: 10px;
    }

    /* The dialog title is the class name; it should not wrap to three lines. */
    .dlg-fullscreen-sm .mud-dialog-title .mud-typography-h6 {
        font-size: 1.05rem;
    }

    /* Section headers inside a tab carry a title and an action; keep them on one
       row rather than letting a long button push the heading onto its own line. */
    .dlg-fullscreen-sm .tab-section-header {
        gap: 8px;
        flex-wrap: nowrap;
    }

    .dlg-fullscreen-sm .tab-section-header .mud-button-root {
        flex-shrink: 0;
        white-space: nowrap;
    }

    /* "Register Selected" wrapped to two lines inside a squeezed panel header. */
    .dlg-fullscreen-sm .detail-panel-header {
        gap: 8px;
    }

    .dlg-fullscreen-sm .detail-panel-header .mud-button-root {
        flex-shrink: 0;
        white-space: nowrap;
        font-size: 0.72rem;
        padding: 4px 10px;
    }
}

/* Row actions inside the class-details lists. Mirrors the Students and Classes
   pages: the inline pair on a wide screen, one overflow menu on a phone. */
/* No d-flex utility on the element: MudBlazor ships that with !important, which
   beat the display:none below and left both action sets visible on a phone. */
.row-actions-full { display: flex; align-items: center; }
.row-actions-menu { display: none; }

@media (max-width: 599.98px) {
    .row-actions-full { display: none; }
    .row-actions-menu { display: flex; flex-shrink: 0; }
}

/* Employees toolbar. Two filters rather than one, so instead of the take-turns
   swap the search keeps its row and both selects fold into a panel beneath it. */
/* The emp-filters-inline / emp-filters-panel pair lived here. Employees and
   Subjects were the only users, and both now take the search/filter swap that
   every other list page uses, so the rules and the duplicate select markup they
   required are gone. */

/* --------------------------------------------------------------------------
   Summary stat strip
   Base rules were duplicated in the Students and Classes component <style>
   blocks. Employees reused the class name and got none of them: the cards fell
   out of the flex row, stretched to 344px and spilled past the right edge.
   One definition here, used by every page with a summary strip.
   -------------------------------------------------------------------------- */
.stats-scroll {
    display: flex;
    gap: 12px;
    overflow-x: auto;
    padding-bottom: 6px;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
}

.stats-scroll::-webkit-scrollbar {
    height: 4px;
}

.stats-scroll::-webkit-scrollbar-thumb {
    background: var(--mud-palette-divider);
    border-radius: 2px;
}

/* Basis auto, not 160px: with a fixed basis and no shrink the cards stayed
   exactly 160px wide, so a longer label ("Matieres Enregistrees") simply ran out
   past the padding. Auto sizes each card to its own content and 160px becomes a
   floor rather than the width. The strip scrolls sideways, so wider cards cost
   nothing but a little scrolling. */
.stats-scroll > .stat-card {
    min-width: 160px;
    flex: 1 0 auto;
}

/* --------------------------------------------------------------------------
   Collapse-on-scroll stats (phones)

   THE LIST-PAGE PATTERN, and it is the same on every list page here:

     page-shell            fixed height, does not scroll
       PageHeader          flex-shrink:0
       stats-scroll        the ONLY thing scrolling hides
       list-toolbar        flex-shrink:0  — search, filters, action buttons
       ...list-scroll      flex:1, min-height:0, overflow-y:auto

   The search box and the action buttons stay put. They are what you reach for
   while reading a list, so carrying them off the top with the rows makes the
   page harder to use, not roomier. Only the stats cards fold away, and only on
   a phone, where the room is actually short — scrolling back to the top brings
   them straight back. Desktop has the height to spare and is left alone.

   The scroller must carry the list-scroll class: app.js watches for movement in
   anything with it and puts stats-collapsed on the page-shell.
   -------------------------------------------------------------------------- */
@media (max-width: 599.98px) {
    .page-shell .stats-scroll {
        /* A ceiling is needed for max-height to animate against; the strip is
           ~80px plus its scrollbar, so this never actually clips it. */
        max-height: 160px;
        transition: max-height 0.25s ease,
                    opacity 0.2s ease,
                    margin-bottom 0.25s ease,
                    padding-bottom 0.25s ease;
    }

    .page-shell.stats-collapsed .stats-scroll {
        max-height: 0;
        opacity: 0;
        margin-bottom: 0 !important;
        padding-bottom: 0;
        /* Only while collapsed: the strip needs overflow-x auto the rest of the
           time so the cards can be scrolled sideways. */
        overflow: hidden;
    }
}

/* Pinned action bar at the foot of the available-subjects drawer. The drawer
   body is already a flex column with the list at flex:1, so this simply refuses
   to shrink and sits below it whatever the scroll position. */
.avail-drawer-footer {
    flex-shrink: 0;
    padding: 12px 14px;
    border-top: 1px solid var(--mud-palette-divider);
    background: var(--mud-palette-surface);
}

/* --------------------------------------------------------------------------
   Subject Registration page shell
   The grid is the flexible middle: header and stats above it keep their height,
   the grid takes what is left, and the registered list inside does the
   scrolling. Without this the panel grew to fit its rows, so the whole document
   scrolled - the heading scrolled away with it and the list had no scrollbar of
   its own, unlike every other list page.
   -------------------------------------------------------------------------- */
.reg-shell-grid {
    flex: 1;
    min-height: 0;
    overflow: hidden;
}

.reg-shell-grid > .mud-grid-item {
    display: flex;
    min-height: 0;
}

.reg-shell-grid > .mud-grid-item > .reg-panel {
    flex: 1;
    min-height: 0;
}

/* The grid item must be height-bound too, or it lays out to its content height
   (783px inside a 361px grid) and the list inside never gets a bounded box to
   scroll within. On a phone only one column is visible, so nowrap is safe. */
@media (max-width: 599.95px) {
    .reg-shell-grid {
        flex-wrap: nowrap;
    }

    .reg-shell-grid > .mud-grid-item {
        height: 100%;
        max-height: 100%;
    }
}

/* --------------------------------------------------------------------------
   Post bodies
   Shared by the post page and the dashboard, so the same body reads the same
   way wherever it is shown. The editor carries its own copy of these rules
   because a contenteditable region needs them applied while typing too.
   -------------------------------------------------------------------------- */
.rtv { font-size: 0.95rem; line-height: 1.7; color: var(--mud-palette-text-primary); }
.rtv-p     { margin: 0 0 14px; }
.rtv-h2    { margin: 22px 0 10px; font-size: 1.25rem; font-weight: 700; line-height: 1.35; }
.rtv-list  { margin: 0 0 14px; padding-left: 24px; }
.rtv-list li { margin-bottom: 6px; }
.rtv-quote {
    margin: 0 0 14px;
    padding: 8px 0 8px 16px;
    border-left: 3px solid var(--mud-palette-primary);
    color: var(--mud-palette-text-secondary);
    font-style: italic;
}
.rtv a { color: var(--mud-palette-primary); }
.rtv > *:last-child { margin-bottom: 0; }

/* --------------------------------------------------------------------------
   Dashboard news feed
   Replaced the announcements box. Every post type shows here, so the card has
   to work with and without a picture and with a one-word or a three-line title.
   -------------------------------------------------------------------------- */
/* The feed has the dashboard's full width underneath the actions now, so its
   cards lay out across it rather than running down one narrow column. auto-fill
   settles the count on its own: one card on a phone, four on a wide desktop,
   with no breakpoint for each step in between. */
.feed-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 12px;
}

.feed-card {
    display: flex;
    flex-direction: column;
    border: 1.5px solid var(--mud-palette-divider);
    border-radius: 12px;
    overflow: hidden;
    cursor: pointer;
    background: var(--mud-palette-surface);
    transition: box-shadow 0.15s ease, transform 0.18s ease, border-color 0.15s ease;
}
.feed-card:hover {
    box-shadow: 0 6px 20px rgba(0,0,0,0.13);
    transform: translateY(-2px);
    border-color: var(--mud-palette-primary);
}

/* Fixed height rather than an aspect ratio: the feed is a column of cards, and
   pictures of different shapes make it read as a jumble. */
.feed-card-image { height: 128px; overflow: hidden; }
.feed-card-image img { width: 100%; height: 100%; object-fit: cover; display: block; }

.feed-card-body { padding: 12px 14px; }

.feed-chip {
    display: inline-block;
    font-size: 0.62rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border-radius: 4px;
    padding: 2px 7px;
    color: #fff;
    white-space: nowrap;
}

@media (max-width: 599.98px) {
    .feed-card-image { height: 100px; }
    .feed-card-body { padding: 10px 12px; }
}
