/* Small additions on top of Bootstrap. Everything else is Bootstrap's own. */

/* Dashboard quick-access tiles — lift and take the accent on hover. */
.dashboard-tile { transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease; }
.dashboard-tile:hover {
    border-color: var(--app-primary, #675dd8) !important;
    box-shadow: 0 4px 14px rgba(33, 38, 60, .08);
    transform: translateY(-2px);
}

/* Inter — the app's UI font. VENDORED (wwwroot/lib/inter), not loaded from a network, so it renders
   identically on every machine and the app still runs fully offline. One variable file covers every
   weight from 100 to 900. The theme named "Public Sans" but shipped no file, so it was silently
   falling back to whatever the OS had; this makes the choice deliberate and consistent. */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('/lib/inter/InterVariable.woff2') format('woff2');
}

/* Zoho's UI runs a touch more compact than the theme's 15px default — match its ~14px base. */
:root { --bs-body-font-size: .875rem; }

body {
    font-size: .875rem;
    font-family: 'Inter', "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
}

/* ---------------------------------------------------------------------------
   User-chosen accent colour. --app-primary is set per request by _ThemeStyle
   from the user's cookie (default Indigo). Sneat hard-codes its primary in a
   lot of places, so the accent is re-applied to the elements that carry it.
   --------------------------------------------------------------------------- */
:root {
    --app-primary: #696cff;
    --app-primary-hover: #5a5de8;
    --bs-primary: var(--app-primary);
}

/* ---------------------------------------------------------------------------
   STATUS COLOURS — muted, not fluorescent.

   The theme ships a highlighter palette: lime green (#71dd37) for success and
   electric cyan (#03c3ec) for info. Next to the indigo accent they read as
   neon, and a school ERP is a document people work in all day, not a dashboard
   demo — a "Nothing to do" tick should be calm, not shouting.

   These are set as VARIABLES rather than as per-badge rules on purpose. The
   theme derives everything from them — .text-success, .bg-success, the soft
   .bg-label-* tints, alerts, progress bars — so replacing the four values
   re-tones every status indicator in the product at once, and a screen built
   next year inherits the calm palette without anybody remembering to ask.

   Chosen for contrast as well as calm: each passes WCAG AA for normal text on
   white, which the lime never did.
   --------------------------------------------------------------------------- */
:root,
[data-bs-theme="light"],
[data-bs-theme="dark"] {
    /* Green, not lime. */
    --bs-success: #16a34a;
    --bs-success-rgb: 22, 163, 74;

    /* Amber, not school-bus yellow — it has to stay readable as text. */
    --bs-warning: #b45309;
    --bs-warning-rgb: 180, 83, 9;

    /* Red that reads as serious rather than as an alarm. */
    --bs-danger: #dc2626;
    --bs-danger-rgb: 220, 38, 38;

    /* Blue, not electric cyan. */
    --bs-info: #0284c7;
    --bs-info-rgb: 2, 132, 199;
}

.btn-primary {
    background-color: var(--app-primary) !important;
    border-color: var(--app-primary) !important;
}

.btn-primary:hover,
.btn-primary:focus,
.btn-primary:active,
.btn-primary.active {
    background-color: var(--app-primary-hover) !important;
    border-color: var(--app-primary-hover) !important;
}

.btn-outline-primary {
    color: var(--app-primary) !important;
    border-color: var(--app-primary) !important;
}

.btn-outline-primary:hover,
.btn-outline-primary:active {
    background-color: var(--app-primary) !important;
    border-color: var(--app-primary) !important;
    color: #fff !important;
}

.text-primary { color: var(--app-primary) !important; }
.bg-primary { background-color: var(--app-primary) !important; }
.app-brand-text.text-primary { color: var(--app-primary) !important; }

.bg-label-primary {
    background-color: color-mix(in srgb, var(--app-primary) 16%, #fff) !important;
    color: var(--app-primary) !important;
}

.form-check-input:checked {
    background-color: var(--app-primary) !important;
    border-color: var(--app-primary) !important;
}

.form-control:focus,
.form-select:focus {
    border-color: var(--app-primary) !important;
    box-shadow: 0 0 0 .15rem color-mix(in srgb, var(--app-primary) 25%, transparent) !important;
}

/* Menu items — the "Content Studio" look: the active row is a SOFT accent pill (accent text on a
   light accent tint) rather than a solid block, and every row is gently rounded. The tint is mixed
   from the accent so it follows whatever accent the school picks. */
.menu-vertical .menu-item .menu-link {
    border-radius: 8px;
}

.menu-item.active > .menu-link:not(.menu-toggle) {
    color: var(--app-primary) !important;
    background: color-mix(in srgb, var(--app-primary) 12%, #fff) !important;
    font-weight: 600;
}

.menu-item.active > .menu-link:not(.menu-toggle) .menu-icon {
    color: var(--app-primary) !important;
}

.menu-vertical .menu-item:not(.active) > .menu-link:not(.menu-toggle):hover {
    background: color-mix(in srgb, var(--app-primary) 7%, #fff);
}


.page-heading {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: .75rem;
    margin-bottom: 1.25rem;
}

.page-heading h1 {
    font-size: 1.6rem;
    margin: 0;
}

/* The tier badges. Core and Extended must never look alike at a glance: the difference between
   them is the whole entry-level bound. */
.tier-core {
    background-color: #e7f1ff;
    color: #0a58ca;
    border: 1px solid #b6d4fe;
}

.tier-extended {
    background-color: #fff3cd;
    color: #815b00;
    border: 1px solid #ffe69c;
}

.outcome-text {
    max-width: 62ch;
}

/* The Bloom (cognitive) badge — a quiet, neutral pill so it sits ALONGSIDE the Core/Extended tier
   badge without competing with it. The tier is the entry-level bound; Bloom is a second, softer axis. */
.bloom-badge {
    background-color: #eef1f6;
    color: #475069;
    border: 1px solid #dfe3ec;
    font-weight: 600;
}

.form-help {
    font-size: .825rem;
}

/* Settings hub — Zoho's "All Settings" look, sampled from Zoho's own page. The section is NOT a
   panel; each category is its own white card of a fixed width, sitting on the grey page with a gap
   between, so the cards stand out against the grey. Each card has a colour-coded gradient header
   band and quiet flex-link items. Values are Zoho's own. */
/* Zoho's own colours: the whole page (and the section behind the cards) is one off-white — only the
   cards are white. The section is a subtle grouping (a soft shadow, no fill of its own), its heading
   small and un-bold, and the cards are a FIXED width so five fit on a line and all share a height. */
.settings-section {
    background-color: transparent;
    border-radius: 16px;
    padding: 1.25rem 1rem 1.25rem;
    margin-bottom: 1.75rem;
    box-shadow: 0 2px 16px rgba(33, 38, 60, .03);
}

.settings-section-title {
    font-size: 18px;
    font-weight: 400;
    color: #21263c;
    margin-bottom: 1.25rem;
}

.settings-cards {
    display: flex;
    flex-wrap: wrap;
    gap: 20px;
    align-items: stretch;
}

.settings-card {
    flex: 0 0 234px;
    width: 234px;
    background-color: #fff;
    border: 1px solid #ececf3;
    border-radius: 10px;
    padding: .625rem;
}

/* The colour-coded header band — a 45° gradient from the category tint to nothing,
   exactly as Zoho draws it. The icon takes the same hue. */
.settings-cat-head {
    display: flex;
    align-items: center;
    gap: .5rem;
    border-radius: .5rem;
    padding: .625rem .75rem;
    margin-bottom: .5rem;
    font-size: 15px;
    font-weight: 500;
    color: #222536;
}

.settings-cat-head i { font-size: 20px; }

.cat-green  { background-image: linear-gradient(45deg, #e8f7f1, transparent); }
.cat-green  i { color: #28c76f; }
.cat-red    { background-image: linear-gradient(45deg, #feeeee, transparent); }
.cat-red    i { color: #ea5455; }
.cat-orange { background-image: linear-gradient(45deg, #fef4ea, transparent); }
.cat-orange i { color: #ff9f43; }
.cat-blue   { background-image: linear-gradient(45deg, #ebf3fe, transparent); }
.cat-blue   i { color: #299aff; }

.settings-item > a,
.settings-item > span {
    display: flex;
    align-items: center;
    padding: 8px 10px;
    margin-bottom: 2px;
    border-radius: .375rem;
    color: #222536;
    text-decoration: none;
    font-size: 14px;
}

.settings-item > a:hover {
    background-color: #f6f6f9;
    color: var(--bs-primary);
}

/* The "All Settings" header bar — Zoho's own values: a white bar with a hairline
   bottom border and a very soft drop shadow, a plain-text Close, and a single
   rounded search field with the magnifier tucked inside it. */
.settings-header {
    border-bottom: 1px solid #f1f1fa;
    box-shadow: 0 4px 20px rgba(33, 38, 60, .02);
}

.settings-title {
    font-size: 18px;
    font-weight: 500;
    color: #21263c;
    line-height: 1.3;
}

.settings-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: 8px;
    background-color: #f5f5f9;
    color: #5a5f7a;
}

.settings-badge i { font-size: 20px; }

/* The app logo at the far left of the settings header — clicking it goes Home, like Zoho's logo. */
.settings-brand {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    border-radius: 9px;
    background-color: var(--app-primary);
    color: #fff;
    text-decoration: none;
}

.settings-brand:hover { background-color: var(--app-primary-hover); color: #fff; }
.settings-brand i { font-size: 22px; }

.settings-search-wrap {
    position: relative;
    width: 100%;
    max-width: 420px;
}

.settings-search-icon {
    position: absolute;
    left: 12px;
    top: 50%;
    transform: translateY(-50%);
    color: #8a8ba0;
    font-size: 18px;
    pointer-events: none;
}

input.settings-search {
    border: 1px solid #d7d5e2;
    border-radius: 10px;
    padding: 7px 14px 7px 34px;
    font-size: 13px;
    height: 40px;
}

input.settings-search:focus {
    border-color: #a9a7d6;
    box-shadow: none;
}

.settings-close {
    color: #21263c;
    font-size: 13px;
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 4px;
}

.settings-close:hover { color: var(--bs-primary); }
.settings-close i { font-size: 18px; color: #ea5455; }

/* Drill-in detail — the left settings nav (Zoho's own dimensions: a ~250px accordion column)
   and the right content pane. The active item uses the app's brand colour rather than Zoho's
   green, so the drill stays on-brand; the layout is Zoho's. */
.settings-back {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 8px;
    color: #5a5f7a;
    text-decoration: none;
}

.settings-back:hover { background-color: #f5f5f9; color: var(--bs-primary); }
.settings-back i { font-size: 22px; }

/* The drill layout is a fixed-height app shell: a header that stays put, a flush-left sidebar that
   scrolls on its own if the menu grows, and a content pane that scrolls independently — exactly like
   Zoho. No centring container, so nothing wastes space on the left. */
.settings-takeover {
    height: 100vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background-color: #fafafd;
}

.settings-header { flex: 0 0 auto; }

.settings-body {
    flex: 1 1 auto;
    display: flex;
    align-items: stretch;
    overflow: hidden;
}

/* The "All Settings" grid has no sidebar — just a scrolling body under the fixed header. Its
   content is centred with generous side padding (only this landing page; the drill pages stay
   full-width and flush-left). The scrollbar gutter is reserved on BOTH edges so the content stays
   symmetrically centred in the full width — the same centre the header uses — and the two line up. */
.settings-scroll {
    flex: 1 1 auto;
    overflow-y: auto;
    scrollbar-gutter: stable both-edges;
}

.settings-grid-wrap {
    max-width: 1320px;
    margin: 0 auto;
    padding: 1.75rem 1rem 2.5rem;
}

/* On the grid page ONLY, the header content lines up with the centred boxes below — so "All
   Settings" sits above the first box's left edge and "Close Settings" above the last box's right
   edge. Same bounds as .settings-grid-wrap. The drill header stays full-width. */
.settings-header-grid {
    width: 100%;
    max-width: 1320px;
    margin-left: auto;
    margin-right: auto;
    padding-left: 1rem;
    padding-right: 1rem;
}

.settings-nav {
    flex: 0 0 250px;
    width: 250px;
    overflow-y: auto;
    border-right: 1px solid #e7e7f1;
    padding: 1rem .75rem;
    background-color: #fff;
}

.settings-nav-section {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: #a5a5ba;
    margin: 1rem .625rem .25rem;
}

.settings-nav-section:first-child { margin-top: 0; }
.settings-nav-group { margin-bottom: 2px; }

.settings-nav-grouphead {
    display: flex;
    align-items: center;
    gap: 6px;
    width: 100%;
    border: 0;
    background: transparent;
    padding: 8px 10px;
    border-radius: 8px;
    font-size: 14px;
    font-weight: 500;
    color: #222536;
    text-align: left;
}

.settings-nav-grouphead:hover,
.settings-nav-group.open .settings-nav-grouphead { background-color: #f7f7fe; }

/* The expand chevron sits at the LEFT of the group name (Zoho's drill nav), and the group's
   items are indented to line up just under that name. */
.settings-nav-caret {
    font-size: 18px !important;
    color: #8a8ba0;
    flex: 0 0 auto;
    transition: transform .15s ease;
}

.settings-nav-group.open .settings-nav-caret { transform: rotate(90deg); }

.settings-nav-items { display: none; padding: 2px 0 6px; }
.settings-nav-group.open .settings-nav-items { display: block; }

.settings-nav-link {
    display: block;
    padding: 7px 10px 7px 34px;
    margin: 2px 0;
    border-radius: 8px;
    font-size: 13px;
    color: #222536;
    text-decoration: none;
}

.settings-nav-link:hover { background-color: #f6f6f9; color: var(--bs-primary); }
.settings-nav-link.active,
.settings-nav-link.active:hover { background-color: var(--bs-primary); color: #fff; font-weight: 500; }

/* Nav group-header icon hues — the same four tints the grid uses. */
.cat-ic-green  { color: #28c76f; }
.cat-ic-red    { color: #ea5455; }
.cat-ic-orange { color: #ff9f43; }
.cat-ic-blue   { color: #299aff; }

.settings-detail-content {
    flex: 1 1 auto;
    min-width: 0;
    overflow-y: auto;
    padding: 1.5rem 2rem;
    background-color: #f7f7fb;
}

/* On a phone the fixed-height shell gets in the way, so the sidebar stacks above the content and
   the page scrolls normally. */
@media (max-width: 767.98px) {
    .settings-takeover { height: auto; overflow: visible; }
    .settings-scroll { overflow: visible; }
    .settings-body { flex-direction: column; overflow: visible; }
    .settings-nav {
        flex: 0 0 auto;
        width: 100%;
        height: auto;
        overflow: visible;
        border-right: 0;
        border-bottom: 1px solid #e7e7f1;
    }
    .settings-detail-content { overflow: visible; padding: 1.25rem; }
    .settings-detail-content > * { max-width: none; }
}

/* ---------------------------------------------------------------------------
   A consistent, professional shell for every settings screen. The heading,
   the read-only note and the alerts are drawn once by the layout; these styles
   make them — and the tables, cards and buttons beneath — look the same and,
   above all, readable. The near-invisible default muted grey was the reported
   "font not visible", so text here is given a darker floor.
   --------------------------------------------------------------------------- */
.settings-page-head {
    margin-bottom: 1.5rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid #e7e7f1;
}

.settings-page-title {
    margin: 0;
    font-weight: 600;
    color: #2f2b3d;
    font-size: 1.125rem;
}

.settings-page-subtitle {
    margin-top: .15rem;
    font-size: .75rem;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: #8c8a99;
}

.settings-detail-content .text-muted { color: #6f6b7d !important; }
.settings-detail-content .form-text { color: #8c8a99; }

.settings-detail-content .card {
    border: 1px solid #ececf3;
    box-shadow: 0 2px 6px rgba(24, 28, 50, .04);
}

.settings-detail-content .card > .card-header {
    font-weight: 600;
    color: #2f2b3d;
    padding-top: 1rem;
    padding-bottom: 1rem;
}

.settings-detail-content .table > thead > tr > th {
    font-size: .75rem;
    letter-spacing: .03em;
    text-transform: uppercase;
    color: #8c8a99;
    font-weight: 600;
    border-bottom: 1px solid #ececf3;
}

.settings-detail-content .table > tbody > tr > td { vertical-align: middle; }
.settings-detail-content .btn-sm .bx { font-size: 1rem; }

/* A clean record list (Zoho's list pattern): quiet uppercase headers, roomy rows with hairline
   dividers, the record's name as a link, row actions that appear on hover. No enclosing card. */
.settings-list { width: 100%; margin: 0; }

.settings-list > thead > tr > th {
    font-size: .72rem;
    letter-spacing: .04em;
    text-transform: uppercase;
    font-weight: 600;
    color: #8c8a99;
    padding: .5rem 1rem;
    border-bottom: 1px solid #ececf3;
    background-color: transparent;
}

.settings-list > tbody > tr > td {
    padding: .9rem 1rem;
    border-bottom: 1px solid #f2f2f7;
    vertical-align: middle;
    color: #4b4b5a;
}

.settings-list > tbody > tr:hover { background-color: #fafafb; }

.settings-list .list-name {
    color: var(--app-primary);
    font-weight: 500;
    text-decoration: none;
}

.settings-list .list-name:hover { text-decoration: underline; }
.settings-list .list-actions { visibility: hidden; white-space: nowrap; }
.settings-list > tbody > tr:hover .list-actions { visibility: visible; }

/*
   A REJECTED FIELD MUST BE UNMISTAKABLE — ON EVERY SCREEN.

   This was scoped to .settings-detail-content, which meant the style guide's own "Rejected Field"
   example, and every other form in the product, drew the reason in ordinary body text. It was never
   a settings-screen idea: it is what a refused field looks like here, and now that the browser's own
   speech bubble has been replaced by this markup (see form-validate.js) it has to hold everywhere.

   The red is the same --app-required as a mandatory label's asterisk, on purpose: the colour that
   says "this field needs you" before you press the button is the colour that says it afterwards.

   THE LONG SELECTOR LIST IS NOT BELT AND BRACES. A few rules down, ".form-control:focus" sets a
   border colour with !important — and the validator SENDS THE CURSOR to the first field it
   rejected, so the rejected field is, at that exact moment, the focused one. Between two important
   declarations the more specific wins, and a bare ".is-invalid" (0,1,0) loses to ".form-control:focus"
   (0,2,0): the field a reader was just sent to would have shown the ordinary focus ring and no red
   at all. Matching the theme's own shape puts them level, and being later in the file settles it.
*/
.input-validation-error,
.is-invalid,
.form-control.is-invalid,
.form-select.is-invalid,
.form-check-input.is-invalid,
input.is-invalid,
select.is-invalid,
textarea.is-invalid { border-color: var(--app-required) !important; }

.field-validation-error {
    color: var(--app-required);
    font-size: .8125rem;
    margin-block-start: .25rem;
}

/* ---------------------------------------------------------------------------
   The common error page.
   --------------------------------------------------------------------------- */
.error-page { background-color: var(--bs-body-bg, #f5f5f9); }

.error-code {
    font-size: 6rem;
    font-weight: 700;
    line-height: 1;
    letter-spacing: -.03em;
    color: var(--bs-primary, #696cff);
}

.error-description { max-width: 32rem; }
.error-reference { color: #8c8a99; font-size: .8125rem; }
.error-reference code { color: #6f6b7d; }

/* The role permission matrix — pages and their tabs down the side, operations across the top. */
.permission-matrix th.text-center,
.permission-matrix td.text-center { width: 5.5rem; }

.permission-matrix .permission-container > td { background-color: #fafafc; }
.permission-matrix tbody tr > td { border-bottom: 1px solid #f4f4f9; }

/* Collapsible tree: a caret opens a menu to reveal its sub-menus and tabs, so a role with hundreds
   of menus is navigated rather than scrolled. Container (heading) rows are shaded so the levels read. */
.perm-tree .perm-name { display: inline-flex; align-items: center; gap: .25rem; }
.perm-tree .perm-toggle {
    background: none;
    border: 0;
    padding: 0;
    width: 1.4rem;
    height: 1.4rem;
    line-height: 1;
    color: #8a879a;
    cursor: pointer;
    flex: 0 0 auto;
}
.perm-tree .perm-toggle:hover { color: var(--app-primary, #675dd8); }
.perm-tree .perm-toggle .bx { transition: transform .15s ease; font-size: 1.1rem; vertical-align: middle; }
.perm-tree .perm-toggle.open .bx { transform: rotate(90deg); }
.perm-tree .perm-toggle-spacer { display: inline-block; width: 1.4rem; flex: 0 0 auto; }
.perm-tree .perm-container > td:first-child { font-weight: 600; }
.perm-tree .perm-tab-badge { font-size: .62rem; font-weight: 600; letter-spacing: .02em; text-transform: uppercase; }
/* The Full-access column sits a touch apart from the operation columns. */
.perm-tree td.text-center:first-of-type + td, .perm-tree th.text-center:first-of-type + th { border-left: 1px solid #f0f0f5; }

/* The accent-colour swatches on the Theme screen — a filled pill per colour,
   the current one ringed and ticked (Zoho's accent-colour pattern). */
.theme-swatch {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    border: 2px solid transparent;
    border-radius: .5rem;
    padding: .55rem 1rem;
    background-color: var(--sw);
    color: #fff;
    font-weight: 500;
    font-size: .85rem;
    cursor: pointer;
    box-shadow: 0 1px 3px rgba(24, 28, 50, .15);
}

.theme-swatch:hover { filter: brightness(.96); }

.theme-swatch.selected {
    border-color: #2f2b3d;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--sw) 35%, transparent);
}

.theme-swatch .bx { font-size: 1rem; }

/* Success toast — a small floating card that fades away, like Zoho's. */
.app-toast {
    position: fixed;
    top: 1.25rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 1090;
    display: inline-flex;
    align-items: center;
    gap: .6rem;
    background-color: #fff;
    border: 1px solid #ececf3;
    border-radius: .5rem;
    box-shadow: 0 10px 30px rgba(24, 28, 50, .18);
    padding: .7rem 1.1rem;
    color: #2f2b3d;
    font-weight: 500;
    font-size: .9rem;
    animation: app-toast-in .25s ease;
}

.app-toast-ic {
    flex: 0 0 auto;
    width: 1.4rem;
    height: 1.4rem;
    border-radius: 50%;
    background-color: #28c76f;
    color: #fff;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.app-toast-ic .bx { font-size: 1rem; }
.app-toast-hide { opacity: 0; transition: opacity .3s ease; }

@keyframes app-toast-in {
    from { opacity: 0; transform: translate(-50%, -12px); }
    to { opacity: 1; transform: translate(-50%, 0); }
}

/*
   THE MANDATORY RED, WRITTEN ONCE.

   The label and its asterisk were the same colour by coincidence — two literals that happened to
   match — so changing the shade meant finding every one of them and getting them all. Named here,
   there is one place to change and no way for the word and the star beside it to end up different
   reds.
*/
:root { --app-required: #d6141d; }

/* A required field is marked with a red asterisk, Zoho-style. */
.settings-detail-content .form-label .req,
.req { color: var(--app-required); font-weight: 600; }

/*
   A MANDATORY FIELD'S LABEL IS RED, NOT JUST ITS ASTERISK.

   An asterisk alone is a convention people know and still miss: it is one small character at the end
   of a word, and on a long form — a student registration runs to fifty fields — the eye slides past
   it. Colouring the label puts the whole word in the reader's way, so the fields they MUST fill are
   picked out while scrolling rather than found one at a time by the validation message.

   Written with :has() so nothing had to be edited on thirty-odd screens: the rule asks "is there an
   asterisk inside this label?" and every existing field, and every field added later, answers for
   itself. A browser too old for :has() simply keeps a black label and a red asterisk — the meaning
   survives, the emphasis is what is lost, which is the right way round for a progressive enhancement.

   The asterisk is deliberately kept. Colour alone is not a label: somebody who cannot distinguish red
   would otherwise have no way at all to tell which fields are required.

   Scoped to labels and table headings ON PURPOSE. The legend on the student form — "* marks a field
   your school has made mandatory" — contains an asterisk too, and turning that whole sentence red
   would make the explanation shout louder than the thing it explains.
*/
.form-label:has(.req),
th:has(.req) { color: var(--app-required); }

/* Self-contained confirmation dialog (delete, etc.) — not Bootstrap's modal. */
/*
   THE QUESTION ARRIVES FROM THE TOP, not from the middle of the screen.

   Centred, the box landed over the very row it was asking about - so the reader lost sight of the
   thing they were deciding on at the moment they had to decide. From the top it drops in above the
   work and the row stays visible underneath it.

   It also matches where every other interruption in this application comes from: the header is at
   the top, the saved-toast is at the top, and a question that appears somewhere else reads as
   belonging to a different product.
*/
.app-confirm-overlay {
    position: fixed;
    inset: 0;
    background-color: rgba(24, 28, 50, .5);
    z-index: 1090;
    display: none;
    align-items: flex-start;
    justify-content: center;
    padding: 1rem;
}

.app-confirm-overlay.show { display: flex; animation: app-fade-in .15s ease; }

.app-confirm-overlay.show .app-confirm-box {
    /* Clear of the header rather than jammed against the window edge. */
    margin-top: 5rem;
    animation: app-drop-in .18s ease-out;
}

@keyframes app-fade-in { from { opacity: 0; } to { opacity: 1; } }

/* Comes DOWN from above, so the eye is led to it rather than finding it already there. Respecting
   reduced-motion below: somebody who has asked for less movement gets none. */
@keyframes app-drop-in {
    from { transform: translateY(-1.25rem); opacity: 0; }
    to   { transform: translateY(0); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .app-confirm-overlay.show,
    .app-confirm-overlay.show .app-confirm-box { animation: none; }
}

.app-confirm-box {
    background-color: #fff;
    border-radius: .75rem;
    box-shadow: 0 20px 60px rgba(24, 28, 50, .3);
    width: 100%;
    max-width: 26rem;
    padding: 1.75rem;
    text-align: center;
}

.app-confirm-icon { font-size: 3rem; }

/* The duplicate-tab block. Covers the page rather than merely warning above it: the point is that the
   screen underneath must not be used, and a dismissible banner would let it be. */
#tab-lock-overlay {
    position: fixed;
    inset: 0;
    z-index: 1095;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
}

.tab-lock-backdrop {
    position: absolute;
    inset: 0;
    background-color: rgba(24, 28, 50, .55);
}

.tab-lock-card {
    position: relative;
    background-color: #fff;
    border-radius: .75rem;
    box-shadow: 0 20px 60px rgba(24, 28, 50, .3);
    width: 100%;
    max-width: 28rem;
    padding: 2rem;
    text-align: center;
}

.tab-lock-icon { font-size: 2.75rem; color: #ff9f43; line-height: 1; }

.tab-lock-card h5 { margin: .75rem 0 .5rem; }

.tab-lock-card p { color: #6f6b7d; font-size: .9rem; margin-bottom: 1.25rem; }

.tab-lock-actions { display: flex; gap: .5rem; justify-content: center; flex-wrap: wrap; }

/* The sign-in card. Wider and more generously spaced than a default Bootstrap card: this is the first
   screen anyone sees of the product, and a cramped box reads as an afterthought. */
.login-card {
    width: 100%;
    max-width: 27rem;

    /* Centred in the window rather than left in the corner: the wrapper around it is full width,
       and a card with a max-width inside it sits wherever the flow puts it unless told. */
    margin-inline: auto;
    border: 0;
    border-radius: 1rem;
    box-shadow: 0 18px 50px rgba(24, 28, 50, .12);
}

/* The session-takeover card carries a LIST - a browser, a date and a time on each row - so it needs
   more room than a sign-in form's two fields. Still capped: a question this short should not be
   spread across a monitor. */
.login-card-wide { max-width: 40rem; }

/* The Username / Work email chooser, as a segmented control rather than loose links. */
.login-modes {
    background-color: #f1f0f5;
    border-radius: .6rem;
    padding: .25rem;
    gap: .25rem;
}

.login-modes .nav-link {
    border-radius: .45rem;
    color: #6f6b7d;
    font-size: .875rem;
    font-weight: 600;
    padding: .45rem .5rem;
}

.login-modes .nav-link.active { background-color: #fff; color: #675dd8; box-shadow: 0 2px 6px rgba(24, 28, 50, .08); }


/* ---------------------------------------------------------------------------
   COLLAPSING THE SIDEBAR TO ICONS.

   Nothing is styled here on purpose. The theme already draws the collapsed rail
   properly — icon-only, widening on hover — from rules keyed on
   .layout-menu-collapsed on the <html> element.

   What was broken was only the JAVASCRIPT. The theme's helper reads:

       function _setCollapsed(collapsed) {
         if (this.isSmallScreen()) { ... }
       }

   so on a desktop it did nothing at all: the click added a transition class,
   changed no state, and took the class off again. The class its own stylesheet
   waits for was never applied.

   So the fix is in app-ui.js, which adds and removes that class — and remembers
   the choice. Styling it a second time here only fought the theme and made the
   expanded state stick at the narrow width. If the sidebar ever needs restyling,
   change the theme's own variables (--bs-menu-width, --bs-menu-collapsed-width)
   rather than adding rules that compete with 82 existing ones.
   --------------------------------------------------------------------------- */

/* ---------------------------------------------------------------------------
   THE COLLAPSE CONTROL LIVES AT THE BOTTOM, AND THE MENU SCROLLS PAST IT.

   At the top it sat beside the logo, read as part of the brand, and scrolled out
   of reach once the menu grew past the viewport. Pinned to the bottom it is
   always in the same place — which is the whole reason a control gets found.

   The list takes the space between brand and footer and scrolls on its own, so a
   school with many menus never loses the control off the end of it.
   --------------------------------------------------------------------------- */
#layout-menu {
    display: flex;
    flex-direction: column;
}

#layout-menu .menu-inner {
    flex: 1 1 auto;
    overflow-y: auto;
    overflow-x: hidden;

    /*
       THE FIRST MENU ITEM STARTS ON THE PAGE TITLE'S LINE.

       The menu had the theme's py-1 — a quarter of a rem — while the content area beside it opens with
       container-p-y's 1.5rem. Both begin just under a line that runs the width of the screen, so the
       difference was plainly visible: "Dashboard" sat 19px above "Subjects", close enough to look like
       a mistake rather than a choice.

       1.5rem is the CONTENT's number, taken deliberately rather than tuned by eye. Under the divider
       the two columns then both indent by the same amount, and their first line of text lands within a
       pixel of the other — the menu's link padding and the toolbar's centring differ by less than that.

       The bottom keeps a little air so the last item never sits flush against the collapse control.
    */
    padding-block: 1.5rem .5rem;
}

/*
    A RULE UNDER THE LOGO.

    Without one the brand and the first menu row read as a single block, and scrolled items ran up into
    the logo with nothing between them. A hairline says plainly that these are two things — the same
    device the cards on the right use to separate a header from its body.

    It sits above the list and carries the menu's own background, so items scroll cleanly underneath it
    rather than showing through.

    Drawn on the brand rather than on the theme's .menu-inner-shadow element: the theme's JS writes an
    inline display to that element on every scroll, from a perfect-scrollbar internal that does not
    always fire, so anything styled there is either invisible or in a fight.
*/
#layout-menu .app-brand {
    position: relative;
    z-index: 2;
    background: var(--bs-menu-bg, #fff);
    /*
       LEVEL WITH THE BAR BESIDE IT, TO THE PIXEL — and now flush to the top of the window.

       This block used to carry margin-block-start: 1rem, which was RIGHT while the navbar beside it
       was the theme's "detached" card and took the same 1rem inset. It became the fault the moment
       the navbar was pinned flush: the logo block stayed 16px down, so its underline sat 16px below
       the header's and the top of the application read as two panels at different heights.

       Both are now --app-header-h tall and both start at 0, so the menu's dividing line and the
       header's lower edge are one rule running the whole width of the screen. Same token for both
       heights, and the same border colour, so neither can drift from the other again.
    */
    margin-block-start: 0;
    block-size: var(--app-header-h);
    border-bottom: 1px solid var(--bs-border-color);
}

/* The theme's empty element has no styling of its own and only ever showed as a gap. */
#layout-menu .menu-inner-shadow {
    display: none !important;
}

#layout-menu .menu-footer {
    flex: 0 0 auto;
    border-top: 1px solid rgba(0, 0, 0, .11);
    padding: .15rem .5rem;
}

/*
    An icon and nothing else. The label said what the arrow already showed, and it had to be hidden
    again the moment the menu collapsed — so the row is the same shape in both states, and there is
    nothing to re-word. The tooltip and aria-label carry the meaning for anyone who needs it.
*/
#layout-menu .menu-footer .menu-link {
    display: flex;
    align-items: center;

    /*
        EXPANDED, the arrow sits at the far RIGHT — on the edge it will pull the menu towards, so its
        position states which way it moves things. COLLAPSED it moves to the middle, where it lines up
        with the column of icons above it and reads as one of them rather than as a stray control.
    */
    justify-content: flex-end;
    padding: .3rem .5rem;
    min-height: 0;
    border-radius: 8px;
    color: var(--muted, #6f6b7d);
    cursor: pointer;
    user-select: none;
}

#layout-menu .menu-footer .menu-link:hover {
    background: color-mix(in srgb, var(--app-primary) 7%, #fff);
    color: var(--app-primary);
}

#layout-menu .menu-footer .menu-toggle-icon {
    font-size: 1.1rem;
    flex: 0 0 auto;
}

@media (min-width: 1200px) {
    /* Collapsed: the arrow turns to point the way pressing it will move the bar... */
    html.layout-menu-collapsed #layout-menu .menu-footer .menu-toggle-icon {
        transform: rotate(180deg);
    }

    /* ...and centres itself under the column of menu icons, so it reads as one of them. */
    html.layout-menu-collapsed #layout-menu .menu-footer .menu-link {
        justify-content: center;
    }

    /* Hovering the rail re-opens it, so the arrow returns to the edge with everything else. */
    html.layout-menu-collapsed #layout-menu:hover .menu-footer .menu-link {
        justify-content: flex-end;
    }
}

/* ---------------------------------------------------------------------------
   THE HEADER ICONS.

   The theme's bx-sm left the cog noticeably smaller than the school name beside
   it, so the two read as unrelated things sitting near each other rather than as
   one row. Sized to the row instead, and given the same round hover target the
   avatar has, so it looks like a control rather than a decoration.
   --------------------------------------------------------------------------- */
/*
   The school's name at the start of the top bar.

   Truncated rather than wrapped: the bar is a fixed 4rem and a long name — "Shree Swaminarayan
   Gurukul International School" is not an unusual one here — would otherwise push the bar taller and
   knock every alignment on the page out. The full name is in the title attribute and in the menu
   under the avatar, so nothing is lost by cutting it here.
*/
.navbar-school {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;

    /* BIGGER THAN THE PAGE'S TEXT, because it is not page text. At the body's 14px it read as a
       caption somebody had left in the bar; it is the title of everything below it, and the one
       thing on a multi-school platform that must never be misread. Tighter tracking keeps a long
       Indian school name from eating the bar at the larger size. */
    font-size: 1.25rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    line-height: 1.2;
    color: var(--bs-heading-color, var(--bs-body-color));
}

@media (max-width: 767.98px) {
    /* A phone cannot hold a 20px name, the year and the controls. The name stays first in line;
       it just stops being the largest thing on a bar it would otherwise fill on its own. */
    .navbar-school { font-size: 1rem; }
}

.layout-navbar .header-icon {
    width: 2.25rem;
    height: 2.25rem;
    justify-content: center;
    border-radius: 50%;
    color: var(--muted, #6f6b7d);
    transition: background-color .15s ease, color .15s ease;
}

.layout-navbar .header-icon i {
    font-size: 1.375rem;
    line-height: 1;
}

.layout-navbar .header-icon:hover,
.layout-navbar .header-icon:focus-visible {
    background: color-mix(in srgb, var(--app-primary) 10%, #fff);
    color: var(--app-primary);
}

/* ---------------------------------------------------------------------------
   PAGE DENSITY.

   Every screen used to open with an area eyebrow, the page name, a paragraph of
   explanation and often a bordered note — four blocks before the first row of
   data. On a list of twenty records that pushed half of them below the fold, so
   the ordinary act of looking something up began with a scroll.

   The explanation now sits behind the (i) in the heading. What is left is tuned
   to put records on screen: one heading line, a tighter table, and less padding
   around the content.
   --------------------------------------------------------------------------- */
.page-head {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin-bottom: 1rem;
}

.page-head-title {
    font-size: 1.25rem;
    font-weight: 600;
    line-height: 1.2;
    margin: 0;
}

.page-head-subtitle {
    color: var(--muted, #8a8ba0);
    font-size: .8125rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.page-head-actions {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin-left: auto;
}

/* A quiet icon, not a call to action: it is there when wanted and ignorable otherwise. */
.page-head-info {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--muted, #8a8ba0);
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
    transition: background-color .15s ease, color .15s ease;
}

.page-head-info:hover,
.page-head-info[aria-expanded="true"] {
    background: color-mix(in srgb, var(--app-primary) 10%, #fff);
    color: var(--app-primary);
}

.page-info-body {
    margin-bottom: 1rem;
    padding: .75rem 1rem;
    border: 1px solid var(--hairline, #e4e4ed);
    border-radius: .5rem;
    background: #fff;
    font-size: .8125rem;
    line-height: 1.6;
    color: var(--muted, #6f6b7d);
}

/* ---- tables carry the records, so they get the space ---- */
.card > .table-responsive > .table > :not(caption) > * > *,
.card > .table > :not(caption) > * > * {
    padding-top: .5rem;
    padding-bottom: .5rem;
}

.table > thead > tr > th {
    font-size: .7rem;
    letter-spacing: .04em;
    text-transform: uppercase;
    white-space: nowrap;
}

/* The content well itself: the old padding was sized for a page that opened with prose. */
.layout-page .container-xxl.flex-grow-1.container-p-y {
    padding-top: 1.25rem !important;
    padding-bottom: 1.25rem !important;
}


/* ===========================================================================
   ONE PLACE FOR THE SHARED DESIGN.

   Everything below is written against the tokens in this first block. Change a
   token and every screen moves together; change a rule under a screen's own
   class and only that screen moves, and the product drifts apart one page at a
   time. That drift is what makes an application feel like several applications,
   so the rule for this file is: add a token, not an exception.

   The theme's own variables (--bs-*) are left alone and layered on top of, so a
   theme update does not wipe this out.
   =========================================================================== */
:root {
    /* Spacing — a 4px scale. Every gap and padding below is one of these. */
    --sp-1: .25rem;
    --sp-2: .5rem;
    --sp-3: .75rem;
    --sp-4: 1rem;
    --sp-5: 1.5rem;
    --sp-6: 2rem;

    /* Type */
    --fs-page-title: 1.25rem;
    --fs-body:       .875rem;
    --fs-small:      .8125rem;
    --fs-label:      .7rem;

    /* Shape */
    --radius:    .5rem;
    --radius-sm: .375rem;

    /* Density — the one number that decides how many records fit on a screen. */
    --row-py: .5rem;
    --row-px: 1rem;

    /* Where the content sits away from the edges. Overridden per breakpoint below. */
    --page-px: 1.5rem;
    --page-py: 1.25rem;
}

/* ---------------------------------------------------------------------------
   RESPONSIVE: PHONE, TABLET, DESKTOP.

   Two rules do most of the work. Nothing may be wider than the screen, and
   anything laid out in a row must be allowed to wrap. Both are stated here once
   rather than per screen, because the failure they prevent — a page that scrolls
   sideways on a phone — is invisible until somebody opens it on one.
   --------------------------------------------------------------------------- */

/* A page never scrolls sideways. Wide things scroll inside their own box instead. */
html, body { max-width: 100%; overflow-x: hidden; }

img, svg, video, canvas, pre, table { max-width: 100%; }

/* Inline widths on filters ("width: 16rem") are a desktop preference, not a floor. */
.form-select, .form-control, select, input, textarea { max-width: 100%; }

/* A row of filters or actions wraps rather than pushing the page wide. */
.page-head,
.page-head-actions,
form.d-flex,
.card-header.d-flex { flex-wrap: wrap; }

/* ---- tablet and below ---- */
@media (max-width: 991.98px) {
    :root { --page-px: 1rem; --page-py: 1rem; }
}

/* ---- phone ---- */
@media (max-width: 575.98px) {
    :root {
        --page-px: .75rem;
        --page-py: .75rem;
        --fs-page-title: 1.125rem;
        --row-px: .625rem;
    }

    /* Full-width edge to edge: on a narrow screen a rounded, inset card wastes the
       margins that the records need. */
    .card {
        border-radius: 0;
        border-inline: 0;
        margin-inline: calc(var(--page-px) * -1);
    }

    /* A primary action goes full width — a thumb misses a small button in a corner. */
    .page-head-actions .btn { flex: 1 1 auto; }

    /* A filter and its label stack instead of squeezing. */
    form.d-flex > .form-label { flex: 1 0 100%; margin-bottom: var(--sp-1); }
    form.d-flex > .form-select,
    form.d-flex > .form-control { flex: 1 1 100%; width: auto !important; }
}

/* The content well reads from the tokens, so its spacing changes with the breakpoint. */
.layout-page .container-xxl.flex-grow-1.container-p-y {
    padding-top: var(--page-py) !important;
    padding-bottom: var(--page-py) !important;
    padding-inline: var(--page-px) !important;
}

/* Density and page chrome, restated from the tokens so there is one source. */
.page-head-title { font-size: var(--fs-page-title); }

.card > .table-responsive > .table > :not(caption) > * > *,
.card > .table > :not(caption) > * > * {
    padding-top: var(--row-py);
    padding-bottom: var(--row-py);
    padding-inline: var(--row-px);
}

.table > thead > tr > th { font-size: var(--fs-label); }

/* ---------------------------------------------------------------------------
   LIST CHROME: THE SEARCH BAR, THE PAGER, THE EMPTY ROW.

   Every list screen in the product draws these three things, so they are styled
   once here and nowhere else. Written against the tokens above, which is what
   makes "make the lists denser" a one-line change rather than a sweep.
   --------------------------------------------------------------------------- */
/* One line: the heading on the left, everything that acts on the list on the right. */
.list-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2);
    margin-bottom: var(--sp-3);
}

/* The heading inside the toolbar keeps the same size it had on its own row — it is still the page's
   title, and shrinking it to fit beside the search would demote it to a label. */
.list-toolbar > .page-head-title { margin-bottom: 0; }

/*
   The (i) belongs TO the title, so it sits a single space away.

   The toolbar's own gap is sized for the controls on the right, where items are separate things; here
   it pushed the two apart until the icon read as its own item on the bar rather than as part of the
   heading. Cancelling most of the gap puts it where a word's worth of space would be.
*/
.list-toolbar > .page-head-info { margin-inline-start: calc(var(--sp-2) * -1 + 4px); }

/* Everything that ACTS on the list, pushed to the right and kept together. */
.list-toolbar-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2);
    margin-inline-start: auto;
}

/*
   A FIXED width, not a stretching one.

   It used to take the whole row, which reads as the most important thing on the page — and on a
   screen whose job is to show records, the records are. It only has to be wide enough for a name.
*/
.list-toolbar-search {
    /* Narrow. It only has to hold a name or a code, and every rem it takes is a rem the records do
       not get. It still grows to the full row on a phone, below. */
    width: 14rem;
    max-width: 100%;
}


/*
   A focus style of our own.

   The default was a heavy dark outline that squared off the corners and read as an error state — the
   opposite of what focus should say. This is the same primary tint the rest of the product uses, as a
   soft ring OUTSIDE the border rather than a thick border, so the box does not change size or shape
   when you click into it.

   Keyboard focus is not decoration: without a visible one, anybody navigating by Tab cannot tell
   where they are on the page.
*/
.list-toolbar-search .form-control:focus {
    border-color: rgba(var(--bs-primary-rgb), .55);
    box-shadow: 0 0 0 .18rem rgba(var(--bs-primary-rgb), .15);
    outline: 0;
}

/* Square, so it reads as an icon button beside the text ones rather than a button missing its word. */
.list-toolbar-filter {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-1);
    padding-inline: var(--sp-3);
}

.list-toolbar-filter .bx { font-size: 1.05rem; line-height: 1; }

/* A dot rather than a number: "something is narrowing this list" is the whole message. */
.list-toolbar-filter-dot {
    display: inline-block;
    width: .45rem;
    height: .45rem;
    margin-inline-start: var(--sp-1);
    border-radius: 50%;
    background: var(--bs-primary);
    vertical-align: middle;
}

/* The filters sit on their own line under the toolbar, so opening them moves nothing sideways. */
.list-toolbar-filters {
    flex: 1 0 100%;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-3);
    margin-top: var(--sp-2);
    padding: var(--sp-3);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius-sm);
    background: var(--bs-tertiary-bg);
}

.list-toolbar-filters.collapse:not(.show) { display: none; }

.list-toolbar-check { margin-bottom: 0; white-space: nowrap; }

/* ---- sortable headings ---- */

/* A LINK, so it can be middle-clicked, copied and bookmarked — and still works with scripting off. */
.grid-th-sort {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    color: inherit;
    text-decoration: none;
    min-width: 0;
}

.grid-th-sort:hover { color: inherit; text-decoration: none; opacity: .85; }

/* Faint until used: it says "this one sorts" without competing with the heading itself. Full
   strength once it IS the sort, because then it is answering "why is the list in this order?". */
.grid-th-sort-hint { opacity: .45; }

.grid-th-sort.is-sorted { font-weight: 700; }

@media (max-width: 767.98px) {
    /* On a phone the search takes the row and the buttons wrap under it, rather than the search
       being squeezed to a width nothing fits in. */
    .list-toolbar-search { width: 100%; }

    .list-toolbar-actions { margin-inline-start: 0; width: 100%; }
}

.list-pager {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    margin-top: var(--sp-3);
    font-size: var(--fs-small);
    color: var(--bs-secondary-color);
}

.list-pager-size {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin-bottom: 0;
}

.list-pager-size .form-select { width: auto; }

/* Tabular figures so the row range does not jitter as the digits change on each page. */
.list-pager-status,
.list-pager-page { font-variant-numeric: tabular-nums; }

.list-pager-controls {
    display: flex;
    align-items: center;
    gap: var(--sp-1);
}

.list-pager-page { margin-inline: var(--sp-2); }

.table-empty {
    padding-block: var(--sp-6) !important;
    text-align: center;
    color: var(--bs-secondary-color);
}

.table-empty a { margin-inline-start: var(--sp-2); }

@media (max-width: 575.98px) {
    /* The count moves under the search rather than squeezing it to nothing. */
    .list-toolbar-count { margin-inline-start: 0; flex: 1 0 100%; }

    /* A search on a phone is the whole point of the bar, so it gets the width. */
    .list-toolbar-search { flex: 1 0 100%; }

    .list-pager { justify-content: center; }
    .list-pager-status { flex: 1 0 100%; text-align: center; }
}

/* ---------------------------------------------------------------------------
   GRID: RESIZABLE, CHOOSABLE COLUMNS.

   Written against the tokens above, once, for every grid in the product.
   --------------------------------------------------------------------------- */

/* table-layout: fixed is what makes a <colgroup> width mean anything. Left on
   auto the browser treats a width as a suggestion and re-decides from content,
   so a column the reader dragged would spring back the moment a long value
   appeared in it. */
.grid { table-layout: fixed; }

/*
   THE SIDEWAYS SCROLL, at every width. Bootstrap's .table-responsive already carries overflow-x,
   but it is stated here against the grid specifically so it survives a theme upgrade - the whole
   reason a column can go missing is that nothing visibly breaks when this is lost.

   min-width on the table stops a narrow card squeezing fixed columns into illegibility: the table
   keeps the width its columns ask for and the wrapper scrolls to it.
*/
.table-responsive:has(> table.grid) { overflow-x: auto; }

.grid th,
.grid td {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/*
   THE ACTIONS COLUMN NEVER CLIPS ITS OWN BUTTONS.

   Every other cell truncates with an ellipsis, which is right for a long name. Applied to a column
   of icon buttons it produced "..⊗" - the tail of an ellipsis and half a button - because the cell
   was one pixel too narrow for the third action. A truncated WORD is still readable; a truncated
   BUTTON is a control the reader can see and cannot use, and it looks like a rendering fault.

   So the cell sizes to its buttons and the column asks for the room. Nothing here can overflow into
   another column: this is the last one.
*/
.grid td[data-col="actions"],
.grid th[data-col="actions"] {
    overflow: visible;
    text-overflow: clip;
}

.grid td.text-nowrap > .btn { vertical-align: middle; }

.grid th { position: relative; }

/*
   A CODE IN A TABLE IS DATA, NOT A CODE SAMPLE.

   Bootstrap styles <code> magenta, for snippets in documentation. A syllabus code is neither — it is
   an identifier a school reads all day, and rendering it in a colour nobody chose made it look like
   an error. Monospace it stays, because digits in a column want to line up; the colour goes.
*/
.grid code {
    color: var(--bs-body-color);
    background: transparent;
    padding: 0;
    font-variant-numeric: tabular-nums;
}

.grid-th-label {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The drag handle. A wide invisible target over a thin visible line, because a
   1px hit area is a coin-toss with a mouse and impossible on a trackpad. */
.grid-resize {
    position: absolute;
    top: 0;
    inset-inline-end: calc(var(--sp-2) * -1);
    width: var(--sp-4);
    height: 100%;
    cursor: col-resize;
    user-select: none;
    touch-action: none;
    z-index: 1;
}

/*
   THE LINE THAT SAYS "YOU CAN DRAG THIS".

   A faint divider is there ALWAYS, so the boundaries between columns are discoverable without having
   to sweep the mouse along the header hunting for a cursor change. On hover it goes solid and full
   height, which is the moment a person learns the column can be resized.

   White, not black. The line was previously --bs-border-color turning --bs-primary on hover, and this
   header IS --bs-primary — so it was a grey line nobody could see becoming a blue line on blue, which
   is to say invisible exactly when it mattered. On a dark header the contrasting colour is the light
   one. (If the header ever goes pale, this pair is the only thing to change.)
*/
/*
   THE LINE THAT SAYS "YOU CAN DRAG THIS".

   The separator above is always there; THIS one is what appears under the pointer — solid white, the
   full height of the bar, three pixels wide. It has to be unmistakable, because it is the only moment
   a person learns the column can be resized at all.

   White, not black. This was previously --bs-border-color turning --bs-primary on hover, and the
   header IS --bs-primary — a grey line nobody could see becoming blue on blue, invisible at exactly
   the moment it mattered. On a dark bar the contrasting colour is the light one. (If the header ever
   goes pale, this pair and the separator above are the only things to change.)
*/
/*
   THE LINE APPEARS UNDER THE MOUSE, AND ONLY THERE.

   No permanent dividers: a header ruled into boxes is busier than it needs to be, and the boundaries
   are not information anybody wants until they intend to move one. The line is an ANSWER to putting
   the pointer on a column, which is the moment a person is asking "can I change this?".

   It shows for the whole HEADING, not just the sixteen pixels of the handle. Requiring the pointer to
   find the exact boundary first is how a feature stays undiscovered — the cell is the target, the
   line tells you where the edge is, and the handle is what you then grab.

   White, not black. This was --bs-border-color turning --bs-primary on hover, and the header IS
   --bs-primary — a grey line nobody could see becoming blue on blue. On a dark bar the contrasting
   colour is the light one. (If the header ever goes pale, this is the one value to change.)
*/
.grid-resize::after {
    content: "";
    position: absolute;

    /*
       SHORTER THAN THE HEADING ROW, and only the LINE is — the grab area above stays full height, so
       it is no harder to catch with the pointer.

       Run the full height, the line met the top and bottom edges of the band and stopped reading as a
       divider between two headings: it read as the border of a box drawn round each one, which is a
       different thing and a heavier one. Held clear of both edges it is plainly a mark BETWEEN the
       two, which is what it does.

       A percentage rather than a fixed inset, so it keeps its proportion if the heading row's height
       is ever adjusted again.
    */
    inset-block: 28%;
    inset-inline-start: 50%;
    width: 0;

    /* Dark, now the bar is pale. This line has been the wrong colour twice — grey on grey, then blue
       on blue — so it is stated against the header's background rather than picked to look nice on
       its own: the two are changed together or not at all. */
    background: var(--bs-body-color);
    transform: translateX(-50%);
    transition: width .12s ease;
}

/* Anywhere in the heading shows it. */
.grid > thead > tr > th:hover .grid-resize::after { width: 2px; }

/* On the handle itself, or mid-drag, it goes thicker still — you are about to act, not just looking. */
.grid-resize:hover::after,
.grid-resize.is-dragging::after { width: 3px; }

/* While dragging, the whole page shows the resize cursor and stops selecting
   text — otherwise a drag past the header highlights half the table. */
body.grid-resizing { cursor: col-resize; user-select: none; }

.grid-columns-button {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    white-space: nowrap;
}

.grid-columns-count { font-variant-numeric: tabular-nums; font-size: var(--fs-small); }

/* ---- the table's own header bar ---- */

/* A solid header, matching the founder's other product. The point is not the colour: a header that
   reads as one BAR rather than five loose labels is what tells you where the data starts, and it is
   what makes the ☰ at its left read as belonging to the table. */
/*
   A QUIET HEADER, SO THE DATA IS THE LOUDEST THING ON THE PAGE.

   It was a full-strength bar of the school's accent. That reads well in a screenshot and badly at
   four in the afternoon: on a screen somebody works in all day, the brightest thing on it should be
   the records, not the labels above them.

   The accent is not lost — it still marks buttons and links, where it means "this does something".
   Spending it on a row of static headings drains it of that meaning.

   A hairline under the row, and slightly heavier label text, do the separating that colour was doing.
*/
/*
   THE HEADING ROW STAYS PUT WHILE THE ROWS SCROLL.

   Past about twenty rows the headings scrolled off and every column below became an unlabelled strip
   of values — on a wide grid, "1234566" with nothing above it to say whether that is a code, a phone
   number or a roll. The reader's only remedy was to scroll back up, remember, and scroll down again.

   THE PAGE SCROLLS, NOT A BOX INSIDE IT.

   The first attempt gave the table its own scrolling box, which is how Zoho does it. It worked and it
   looked wrong: a second scrollbar appeared inside the card, the list was penned into a short window
   with empty page below it, and the reader now had two things to scroll depending where the pointer
   happened to be.

   So the wrapper stops scrolling instead. Sticky positioning is measured against the nearest
   SCROLLING ancestor, and Bootstrap's .table-responsive is one — it carries overflow-x for wide
   tables, and a box that scrolls in one axis scrolls in both, which is what was pinning the headings
   to the top of the table rather than to the top of the window. With the overflow gone there is no
   box between the heading row and the page, so it holds against the window and the page scrolls once.

   THE ONE THING THAT CANNOT BE HAD BOTH WAYS. A box that scrolls sideways is a box the heading row
   sticks INSIDE. So a grid may have an inner horizontal scrollbar, or a heading row pinned to the
   window, and not both - no amount of CSS settles that, it is what "nearest scrolling ancestor"
   means.

   WE TAKE THE SCROLLBAR, ALWAYS. This used to give the pinned heading to windows wider than 1400px,
   on the reasoning that the widest grid we had would fit there. It did not: .grid is
   table-layout: fixed, so its <colgroup> widths are obeyed whatever the card can hold, and the
   Documents grid alone asks for about 84rem of columns. Inside a card that has lost 17rem to the
   menu, the last two columns - Status and Actions, the row's own EDIT button among them - were
   simply cut off at the card edge with nothing to scroll them into view. The body carries
   overflow-x: hidden, which the browser hands to the WINDOW, so there was no scrollbar anywhere:
   the columns could not be reached at all, on the widest screens, which is where nobody thinks to
   look for a fault like that.

   A heading that scrolls away is a nuisance. A column that cannot be reached is lost work. So the
   wrapper scrolls at every width, and the column chooser (the ☰ at the head of every grid) is how a
   reader who wants the heading in view instead drops the columns they do not need.
*/

.grid > thead > tr > th {
    position: sticky;
    top: 0;

    /* Above the cells sliding under it, and above the resize handles, which carry z-index of their
       own. */
    z-index: 3;

    background: var(--bs-tertiary-bg);
    color: var(--bs-heading-color, var(--bs-body-color));

    /*
       An INSET SHADOW doing the work of the bottom border.

       Reboot collapses this table's borders, and a collapsed border belongs to the table's grid
       rather than to the cell — so it stays where it was drawn while the sticky cell moves over it,
       and the heading row ends up floating with no line under it at all. A shadow is painted by the
       cell, so it travels with it.
    */
    box-shadow: inset 0 -1px 0 var(--bs-border-color);
    text-transform: uppercase;
    letter-spacing: .03em;
    font-weight: 600;
    vertical-align: middle;

    /*
       SHORTER THAN A DATA ROW, on purpose.

       The theme gives every cell 0.782rem top and bottom, which is right for a row you read and
       generous for a row of one-word labels — it made the headings the tallest band on the page,
       above the data they were only there to name. Halved, the heading row reads as a label strip
       and the first record starts higher up the screen, which is what the reader came for.

       The side padding is left alone: it is what keeps a heading clear of the resize handle on its
       edge, and tightening it would put the two within a few pixels of each other.
    */
    padding-block: .4rem;
}

/*
   THE ROW ACTION BUTTON.

   Square, quiet, and the same on every screen. The actions used to be words in outlined buttons —
   "Open", "Edit", "Inactivate", "Reactivate" — and four of those turned the narrowest column into the
   widest one on the page. As icons they cost a fixed 1.9rem each however many there are, so a list
   can carry five actions without the data losing room to them.

   NEUTRAL BY DEFAULT. Colour is spent on the two actions that change something a reader might regret,
   and even then only on hover: a row of red and amber buttons is a row where nothing stands out, and
   a list of two hundred students would have been four hundred coloured buttons shouting at once.

   The word is not lost — the tag helper puts it in the tooltip AND in aria-label, so it is still
   there for a pointer that hovers and for a screen reader that reads.
*/
.btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.9rem;
    height: 1.9rem;
    padding: 0;
    line-height: 1;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius-sm);
    background: var(--bs-body-bg);
    color: var(--bs-secondary-color);
}

.btn-icon > .bx { font-size: 1rem; line-height: 1; }

.btn-icon:hover,
.btn-icon:focus-visible {
    background: rgba(var(--bs-primary-rgb), .1);
    border-color: var(--bs-primary);
    color: var(--bs-primary);
}

/* The two that change a record's standing, and the one that would remove it. Same neutral button
   until the pointer is on it, then the colour of what it is about to do. */
.btn-icon-warning:hover,
.btn-icon-warning:focus-visible {
    background: rgba(var(--bs-warning-rgb), .12);
    border-color: var(--bs-warning);
    color: var(--bs-warning);
}

.btn-icon-success:hover,
.btn-icon-success:focus-visible {
    background: rgba(var(--bs-success-rgb), .12);
    border-color: var(--bs-success);
    color: var(--bs-success);
}

.btn-icon-danger:hover,
.btn-icon-danger:focus-visible {
    background: rgba(var(--bs-danger-rgb), .12);
    border-color: var(--bs-danger);
    color: var(--bs-danger);
}

/* Buttons in a row need air between them, and a gap in the markup is a space the browser may or may
   not keep. Stated here so every actions cell spaces its buttons the same. */
.grid td.text-nowrap > .btn + .btn { margin-inline-start: .35rem; }

/*
   CANCEL, EVERYWHERE, THE SAME.

   It was an outlined button on eleven screens and a bare link on four, so the way out of a form
   changed shape depending which form you were in — and on the four it was a link, it did not read as
   a button at all. Now it is the same solid shape as the button beside it, in grey with dark text:
   equal in weight, so it is easy to find and hard to press by mistake, and unmistakably not the one
   that saves.

   Grey and dark text rather than a light-grey outline because the pair have to read as a PAIR. Two
   solid buttons say "choose one of these"; a solid one next to an outlined one says "this, or
   perhaps nothing", which is not the choice being offered.
*/
.btn-cancel {
    background: var(--bs-secondary-bg);
    border: 1px solid var(--bs-secondary-bg);
    color: var(--bs-heading-color, var(--bs-body-color));
}

.btn-cancel:hover,
.btn-cancel:focus-visible {
    background: var(--bs-tertiary-bg);
    border-color: var(--bs-border-color);
    color: var(--bs-heading-color, var(--bs-body-color));
}

/*
    A FACE IN A LIST ROW.

    Round, one size, and the SAME size whether there is a photograph or not — a row that grows when a
    child has a picture makes the list jump about as it loads. The drawn size is 2.25rem against a 96px
    stored thumbnail, so it stays sharp on a high-density screen without asking for a bigger file.
*/
.roll-name {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-width: 0;
}

.roll-photo {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 50%;
    object-fit: cover;
    background: var(--bs-tertiary-bg);
    vertical-align: middle;
}

/*
    WHICH ROLE THE READER IS ACTING AS, stated in the bar.

    Sized and coloured like the academic-year badge beside it, because they answer the same kind of
    question about the screen — whose data, from when, and as whom. The name is allowed to run to a
    sensible width and then truncate: a school with a role called "Senior Section Coordinator" must not
    push the avatar off the bar.
*/
.role-chip {
    display: inline-flex;
    align-items: center;
    gap: .375rem;
    max-width: 12rem;
    padding: .25rem .625rem;
    border: 1px solid var(--bs-border-color);
    border-radius: 2rem;
    font-size: .8125rem;
    line-height: 1.25;
    color: var(--bs-body-color);
    background: var(--bs-body-bg);
    text-decoration: none;
}

.role-chip-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 600;
}

.role-chip-button { cursor: pointer; }

.role-chip-button:hover {
    border-color: var(--bs-primary);
    color: var(--bs-primary);
}

.role-chip-caret { font-size: 1rem; opacity: .7; }

.role-chip-item { margin-inline-end: .75rem; }

/* A photograph that can be looked at more closely says so before it is hovered. */
.js-photo-zoom { cursor: zoom-in; }

/*
    THE ENLARGED FACE. Floating and fixed, above the table but below a dialog — this is a glance, not a
    thing to interact with, so it must never sit over something being read or answered.
*/
.photo-zoom {
    position: fixed;
    z-index: 1085;
    padding: 0.375rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius-sm, .375rem);
    background: var(--bs-body-bg);
    box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, .18);
    pointer-events: none;      /* never steals the click meant for the row underneath */
    max-width: 15rem;
}

.photo-zoom img {
    display: block;
    width: 12.5rem;
    height: 12.5rem;
    object-fit: cover;
    border-radius: var(--radius-sm, .375rem);
    background: var(--bs-tertiary-bg);
}

.photo-zoom-name {
    display: block;
    margin-top: 0.375rem;
    font-size: 0.8125rem;
    font-weight: 600;
    text-align: center;
    color: var(--bs-body-color);
}

/* The initials standing in for a photograph. Quiet: it is a placeholder, not a fact about the child. */
.roll-photo-blank {
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--bs-secondary-color);
    border: 1px solid var(--bs-border-color);
}

/* The ☰ column. Fixed and narrow: it holds one button and must not take width from the data. */
.grid-menu-col { width: 2.75rem; }

.grid-menu-cell { text-align: center; padding-inline: 0 !important; }

.grid-menu-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: inherit;
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
    transition: background-color .15s ease;
}

.grid-menu-button:hover,
.grid-menu-button:focus-visible { background: rgba(var(--bs-primary-rgb), .12); color: var(--bs-primary); }

/* ---- the chooser dialog ---- */

/*
   Laid out against Zoho's, side by side: a QUIET header, rows that are soft filled strips, and the
   buttons centred at the foot.

   The header used to be a solid block of the accent colour. It was the loudest thing on the screen,
   and it was announcing "Customise columns" — a housekeeping errand, not the point of the page.
   Worse, it moved with the theme, so a school on Green got a green slab and a school on Violet a
   violet one, each shouting a different amount. It went grey, and then plain, and plain is where it
   belongs: the dialog now has ONE band of colour, the grey at the foot holding Save and Cancel, and
   that band is the only thing in it competing for attention — which is right, because pressing one
   of those two buttons is the only reason anybody opened it.

   The rows are filled rather than outlined for the same reason. A border round every row draws
   fifteen rectangles on a small dialog and the eye has to pick the words out from between them; a
   soft fill separates one row from the next just as clearly and draws nothing.
*/
.app-dialog .modal-content { border: 0; }

.app-dialog-header {
    /* Grey, matching the table's own heading row. The dialog then has the same shape as the screens
       behind it — a titled band, then the content — instead of being one undifferentiated white sheet
       where the title had to be found rather than seen. */
    background: var(--bs-tertiary-bg);
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-3) var(--sp-4);
    border-bottom: 1px solid var(--bs-border-color);
}

/*
   A FLEX ROW, so the icon and the words share a centre line.

   An icon dropped inside a heading sits on the TEXT BASELINE, which is the bottom of the letters —
   so a square glyph rides high against them. Laying the two out as flex items with align-items:center
   makes the browser line up their middles, which is what the eye expects.
*/
.app-dialog-header .modal-title {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex: 1 1 auto;
    min-width: 0;
    color: var(--bs-heading-color, var(--bs-body-color));
    font-size: 1rem;
}

.app-dialog-header .modal-title > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The one spot of colour left in the header, and it earns it: the icon is what the eye lands on when
   the dialog opens, and it is the only part of the header that cannot be mistaken for a word. */
.app-dialog-header .modal-title > .bx {
    font-size: 1.15rem;
    line-height: 1;
    flex: 0 0 auto;
    color: var(--bs-primary);
}

/* Near the top, not glued to it. Matches Bootstrap's own non-centred offset. */
.app-dialog-panel { margin-top: 3rem; }

.app-dialog-count {
    flex: 0 0 auto;
    padding: .25rem .7rem;
    border-radius: 10rem;
    /* Filled rather than outlined, now the header behind it is white: on grey it needed an edge to
       separate it, on white the fill IS the edge and a border round it only added a ring. */
    background: var(--bs-tertiary-bg);
    border: 1px solid transparent;

    /* The full heading colour, not the muted one. It is a COUNT — the answer to "how many am I
       showing?" — and muted grey made it the faintest thing in the dialog. */
    color: var(--bs-heading-color, var(--bs-body-color));
    font-size: var(--fs-small);
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.app-dialog-header .btn-close {
    /*
       position: static is the line that matters, and it took several attempts to find.

       This theme positions the modal's close button ABSOLUTELY at right: 13px, so it floated on top
       of the count beside it and every margin I set did nothing at all — margins have no effect on
       where an absolutely positioned element sits. Made a normal flex item, it takes its place in the
       row like everything else.

       The !important is not laziness. The theme's own rule is more specific than anything reasonable
       written here, and matching its selector exactly would break the day the theme is updated.
       Overriding a vendored rule is what !important is actually for. `inset: auto` goes with it, or
       the right/top it was given linger and shift it once static.

       The inversion that used to be here has gone WITH the dark header. Bootstrap's close icon is
       already dark, so inverting it on a pale ground would paint it white on white — invisible, and
       invisible in a way nobody would think to look for.
    */
    transform: none !important;
    box-shadow: none !important;
    position: static !important;
    inset: auto !important;
    margin: 0 !important;
    flex: 0 0 auto;
    background-color: transparent;
    opacity: .5;
}

.app-dialog-header .btn-close:hover { opacity: 1; }

/*
   THE LIST SCROLLS, NOT THE DIALOG.

   A grid with fifteen or twenty columns made the dialog itself grow until it ran off the bottom of
   the screen, taking Save and Cancel with it — the reader could see every column and reach no button.
   Laid out as a column with the list given a ceiling, the header and the buttons stay put and only
   the rows move.

   The search box is pinned above the scrolling area on purpose. Inside it, it scrolled away with the
   rows, so the control for finding a column in a long list disappeared exactly when the list got long
   enough to need it.
*/
/*
   MORE AIR UNDER THE HEADING THAN BESIDE IT.

   With equal padding all round, the first field sat directly against the line under the title and read
   as attached to it — one block rather than a heading and then its content. A little more above the
   first row separates the two without leaving the hole that a whole blank line would.
*/
.app-dialog .modal-body { padding: var(--sp-4) var(--sp-4) var(--sp-3); }

.grid-chooser .modal-body {
    padding: var(--sp-4) var(--sp-4) var(--sp-3);
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
}

.grid-chooser-list {
    list-style: none;
    margin: 0;
    padding: 0;
    flex: 1 1 auto;
    min-height: 0;

    /* vh so it shrinks with a short window, rem so it stops growing on a tall one. Around eleven
       rows before it scrolls, which clears every grid we have without a scrollbar appearing for the
       sake of it. */
    max-height: min(55vh, 28rem);
    overflow-y: auto;

    /* Room for the scrollbar, so it lies beside the rows rather than on top of the arrows. */
    padding-inline-end: var(--sp-1);
}

/*
   A ROW IS A FILLED STRIP.

   Sized between Zoho's and what fifteen columns need: about 40px, so eight rows sit in 320px and
   fifteen in 600px — the latter scrolling inside the dialog, which is what the scrollable body is
   for. The gap between the strips is what separates them; no border is drawn.
*/
.grid-chooser-item {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    margin-bottom: var(--sp-1);
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    font-size: var(--fs-small);

    /*
       A TINT, not the full grey.

       --bs-tertiary-bg is the theme's heading-band grey. It is the right weight for a band that runs
       the width of a table and far too heavy for a stack of small strips: at full strength the rows
       were darker than the white they sat on by more than the words were, so the eye read the shading
       first and the column names second. Diluted towards the page it separates the rows just as well
       and stops competing with them.

       Mixed rather than hard-coded so it follows the theme into dark mode, where the same 35% lands a
       shade LIGHTER than the dialog instead of darker — which is the same relationship, inverted, and
       what a dark theme needs. The plain colour on the line above is the fallback for a browser
       without color-mix; it is the light-mode answer, so nothing looks broken there either.
    */
    background: #f0f1f4;
    background: color-mix(in srgb, var(--bs-tertiary-bg) 35%, var(--bs-body-bg));
}

.grid-chooser-item:last-child { margin-bottom: 0; }

/* The border is transparent rather than absent, so the row does not GROW by two pixels the moment it
   is hovered — a list that shuffles under the pointer is worse than one that never reacted. */
.grid-chooser-item:hover {
    background: #ebeced;
    background: color-mix(in srgb, var(--bs-tertiary-bg) 80%, var(--bs-body-bg));
    border-color: var(--bs-border-color);
}

/*
   A LOCKED ROW KEEPS ITS BACKGROUND.

   It was given a transparent one so it would read as "not yours to change". What it actually read as
   was a gap in the list — one strip missing from a stack of them, which looks like a rendering fault
   rather than a statement about permissions. The padlock, the greyed tick box and the muted words
   already say the row is fixed, three times over; taking the background away said nothing a fourth
   time and cost the list its rhythm.
*/
.grid-chooser-item.is-locked { color: var(--bs-secondary-color); }

.grid-chooser-item.is-dragging { opacity: .4; }

.grid-chooser-item.is-drop-target { box-shadow: inset 0 2px 0 0 var(--bs-primary); }

.grid-chooser-item[hidden] { display: none; }

.grid-chooser-grip { cursor: grab; color: var(--bs-secondary-color); font-size: 1.05rem; }

.grid-chooser-check { flex: 1 1 auto; min-width: 0; }

.grid-chooser-label {
    margin-bottom: 0;
    cursor: pointer;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    display: block;
}

/*
   Grey, not amber.

   Amber was chosen so the padlock would catch the eye of somebody who had just tried to untick the
   row. But amber is the colour this application uses for WARNINGS, and there is nothing wrong here —
   the row is simply not adjustable. It read as a problem to be dealt with, on a dialog where the
   reader had done nothing wrong, every time it opened.
*/
.grid-chooser-lock { color: var(--bs-secondary-color); font-size: 1rem; flex: 0 0 auto; }

.grid-chooser-move { flex: 0 0 auto; display: flex; gap: 2px; }

/*
   Quieted to plain chevrons.

   They were outlined buttons, which put two more rectangles on every row — thirty of them on a
   fifteen-column grid, all competing with the column names, which are the only thing anybody came
   here to read. As bare chevrons they take their weight back on hover, once the reader has shown
   which row they mean.

   NOT hidden until hover, though that would look tidier still. A finger cannot hover, and these
   arrows exist precisely FOR the reader who cannot drag — hiding them would take the reliable path
   away from the people it was put there for.
*/
.grid-chooser-move .btn {
    padding: 0 var(--sp-1);
    height: 1.65rem;
    min-width: 1.65rem;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--bs-secondary-color);
}

.grid-chooser-move .btn:hover,
.grid-chooser-move .btn:focus-visible {
    background: var(--bs-body-bg);
    color: var(--bs-primary);
}

/* The buttons sit in the middle, as they do in the founder's product — a dialog this narrow has no
   far corner for them to hug, and centred they read as the pair of choices they are. */
/*
   Centred BOTH ways in the grey band.

   Bootstrap gives every child of a modal footer its own quarter-rem margin, which sat on top of the
   padding here and pushed the pair off-centre — more space under them than over. The margins are
   cleared and the gap does the spacing instead, so the buttons sit in the middle of the band rather
   than merely somewhere inside it.
*/
.app-dialog-actions {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);

    /* Equal on every side, and !important because the theme sets this footer to "0 24px 24px" —
       nothing above the buttons and 24px below them, which is exactly the amount they looked pushed
       up by. A plain shorthand here lost to it. */
    padding: var(--sp-3) !important;
    border-top: 1px solid var(--bs-border-color);

    /* White, with the grey spent on the header instead. Only one of the two bands can be the quiet
       one, and it should be the title: the buttons are what the dialog is FOR, and a grey tray under
       them made the pair look disabled next to a white body. */
    background: var(--bs-modal-bg, var(--bs-body-bg));
}

.app-dialog-actions > * { margin: 0 !important; }

.app-dialog-actions .btn { min-width: 6rem; }

/* "Reset to default" is a quieter thing than Save and Cancel, so it sits away from them rather than
   beside them where it could be pressed by mistake. */
.app-dialog-actions .btn-link { min-width: 0; margin-inline-end: auto; }

/*
   The column search comes back for LONG lists only.

   With five columns it is a control to read past on the way to the one you want; with fifteen it is
   the fastest way there. Shown by the screen, so a short chooser stays plain.
*/
.grid-chooser-search { margin-bottom: var(--sp-2); flex: 0 0 auto; }

/* Wider than Bootstrap's 500px default: each row carries a grip, a tick box, a name, a padlock and
   two buttons, and at the default width the name is the only one of those with room to lose. */
.app-dialog-panel { max-width: 34rem; }

/*
   THE CLOSE BUTTON STAYS IN THE CORNER IT IS DRAWN IN.

   The theme treats a modal's close button as a floating chip that sits OUTSIDE the panel: it gives it
   a shadow and, on hover, transform: translate(20px, -20px) — a deliberate effect for its own demo
   pages. It then cancels that only for .modal-dialog-scrollable.

   Which is why the two dialogs disagreed. The column chooser IS scrollable, so it landed correctly and
   looked fine; the filter dialog is not, so its close button floated up and out of the corner and
   jumped further away the moment the pointer reached it — a control that runs from the cursor.

   Cancelled here for OUR dialogs by name, so neither depends on whether it happens to scroll.
*/
.app-dialog .btn-close {
    transform: none !important;
    box-shadow: none !important;
    background-color: transparent !important;
}

.app-dialog .btn-close:hover,
.app-dialog .btn-close:focus,
.app-dialog .btn-close:active { transform: none !important; }

/*
   THE APPLIED-FILTER BAR.

   Between the toolbar and the table, where it is read on the way to the rows rather than hunted for.
   Its own tinted band rather than loose chips on the page background: the row has to read as ONE
   statement about the table below it, and a line of separate pills reads as a set of buttons.
*/
.active-filters {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--sp-2);
    margin-bottom: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius-sm);
    background: rgba(var(--bs-primary-rgb), .04);
    font-size: var(--fs-small);
}

.active-filters-label {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    font-weight: 600;
    color: var(--bs-heading-color, var(--bs-body-color));
}

.active-filter {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    padding: .15rem .15rem .15rem .55rem;
    border: 1px solid var(--bs-border-color);
    border-radius: 10rem;
    background: var(--bs-body-bg);
}

/* The FIELD in the muted colour and the VALUE in the strong one. Read quickly, the eye takes the
   values and skips the labels, which is the right way round — "Chemistry" is the news, "Subject name"
   is only where it came from. */
.active-filter-name { color: var(--bs-secondary-color); }

.active-filter-value {
    font-weight: 600;
    color: var(--bs-heading-color, var(--bs-body-color));
}

.active-filter-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 1.25rem;
    block-size: 1.25rem;
    border-radius: 50%;
    color: var(--bs-secondary-color);
    text-decoration: none;
}

.active-filter-remove:hover,
.active-filter-remove:focus-visible {
    background: rgba(var(--bs-danger-rgb), .12);
    color: var(--bs-danger);
}

/* Pushed to the far end as a PAIR, away from the crosses. Clear all beside a chip's own cross is how
   somebody loses four conditions meaning to lose one. */
.active-filters-actions {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    margin-inline-start: auto;
}

.active-filters-action {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    padding: 0 var(--sp-1);
    text-decoration: none;
}

.active-filters-clear { color: var(--bs-danger); }

/*
   THE FILTER DIALOG'S FIELDS.

   Two to a row on a normal screen, one on a narrow one. Two because a filter field is short — a date,
   a status, a code — and one per row would make a four-field dialog as tall as the table it is
   filtering, for no more information. Not three: the labels are sentences ("Created between") and
   three columns starts truncating them.
*/
.filter-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--sp-3);
}

@media (max-width: 575.98px) {
    .filter-grid { grid-template-columns: minmax(0, 1fr); }
}

.filter-field > .form-label { margin-bottom: var(--sp-1); }

/* From and to, with the dash between them carrying the meaning that one label above cannot. */
.filter-range {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
}

.filter-range-box { position: relative; flex: 1 1 0; min-width: 0; }

.filter-range-box > .form-control { width: 100%; }

/* A wrapper that groups two fields for scripting without becoming a box in the layout. */
.d-contents { display: contents; }

/*
   THE DATE PICKER.

   Anchored to the box it belongs to rather than to the page, so it opens inside the dialog instead of
   underneath its backdrop — and moves with it if the dialog is scrolled.
*/
.datepicker-host { position: relative; }

.datepicker {
    position: absolute;
    z-index: 1060;
    inset-inline-start: 0;
    inset-block-start: calc(100% + 4px);
    inline-size: 18rem;
    padding: var(--sp-2);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius-sm);
    background: var(--bs-modal-bg, var(--bs-body-bg));
    box-shadow: 0 8px 24px rgba(0, 0, 0, .14);
}

.datepicker-head {
    display: flex;
    align-items: center;
    gap: var(--sp-1);
    margin-bottom: var(--sp-2);
}

/* The month takes the room, the year takes what it needs: "September" is nine characters and 2026 is
   four, and equal halves truncate the one that cannot afford it. */
.datepicker-month { flex: 1 1 auto; min-width: 0; }
.datepicker-year { flex: 0 0 5.5rem; }

.datepicker-step {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 1.75rem;
    block-size: 1.75rem;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--bs-secondary-color);
}

.datepicker-step:hover { background: var(--bs-tertiary-bg); color: var(--bs-primary); }

.datepicker-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 2px;
}

.datepicker-dow {
    padding-block: var(--sp-1);
    text-align: center;
    font-size: var(--fs-small);
    font-weight: 600;
    color: var(--bs-secondary-color);
}

.datepicker-day {
    padding: 0;
    block-size: 2rem;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--bs-body-color);
    font-size: var(--fs-small);
    font-variant-numeric: tabular-nums;
}

.datepicker-day:hover:not(:disabled) { background: var(--bs-tertiary-bg); }

/* The days either side of the month are SHOWN, faintly. Blanking them makes the grid change shape
   from month to month, which reads as a fault. */
.datepicker-day.is-outside { color: var(--bs-secondary-color); opacity: .55; }

.datepicker-day.is-today { box-shadow: inset 0 0 0 1px var(--bs-primary); }

.datepicker-day.is-chosen {
    background: var(--bs-primary);
    color: #fff;
    font-weight: 600;
}

/* Outside the allowed range. Shown rather than hidden, so a range whose limits have narrowed still
   looks like a calendar and not like a month with holes in it. */
.datepicker-day:disabled { opacity: .3; cursor: not-allowed; }

.datepicker-foot {
    display: flex;
    justify-content: flex-start;
    margin-top: var(--sp-1);
    border-top: 1px solid var(--bs-border-color);
    padding-top: var(--sp-1);
}

.datepicker-clear { padding: 0; text-decoration: none; }

.filter-range-dash { flex: 0 0 auto; color: var(--bs-secondary-color); }

/* Wider than the chooser: that one lists single words, this one holds pairs of dated boxes. */
.filter-dialog .app-dialog-panel { max-width: 42rem; }

/* The prompt after a drag, matching Zoho's: a resize is provisional until kept. */
.grid-save-bar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--sp-2);
    margin-bottom: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--radius-sm);
    background: var(--bs-tertiary-bg);
    font-size: var(--fs-small);
}

.grid-save-bar[hidden] { display: none; }

/*
   The buttons sit NEXT TO the question, not in the far corner.

   "Keep them?" and the answer to it are one thought. Pushed to opposite ends of a wide bar the eye
   has to cross the whole screen to connect them, and on a wide monitor the buttons end up somewhere
   nobody is looking — the question reads as a statement with no reply.
*/
.grid-save-bar-text { margin-inline-end: var(--sp-1); }

@media (max-width: 575.98px) {
    /* Dragging a column edge needs a pointer and room. On a phone the table
       already scrolls sideways inside its own box, so the handles only get in
       the way of scrolling it. */
    .grid-resize { display: none; }

    .grid-chooser { width: 100%; }
}

/* ==========================================================================================
   CHOICE PICKER — the one dropdown, searchable, for one value or several.

   Drawn on top of a real <select multiple>, which stays in the form and stays posted. It is
   hidden by a rule that only applies once the script has claimed it, so with scripting off the
   browser's own control is what people get rather than nothing at all.
   ========================================================================================== */

.choice-picker { position: relative; }

.choice-picker-native { display: none; }

.choice-picker-field { cursor: pointer; }

/* An unset control should not look like one holding a value nobody can read. */
.choice-picker-field.choice-picker-empty-field { color: var(--bs-secondary-color); }

/*
    WHILE OPEN, the panel is attached to the body and positioned in window coordinates — see
    choice-picker.js. Fixed rather than absolute is the whole point: an absolutely positioned panel is
    clipped by any ancestor that scrolls, and a picker can be dropped into a table, a card or a modal.
*/
.choice-picker-panel.is-floating { position: fixed; right: auto; }

.choice-picker-panel {
    position: absolute;
    /*
        ABOVE A DIALOG, NOT MERELY ABOVE ITS BACKDROP.

        This was 1056, chosen against Bootstrap's own 1055 for a modal. The theme raises modals to
        1090 and their backdrop to 1089, so the list opened UNDERNEATH both: the part of it that
        stuck out past the dialog showed through the dim as a grey ghost box, and the part over the
        dialog was hidden entirely. On the Students filter that read as "the Class dropdown has
        nothing in it" — the options were there, painted behind the panel they belong to.

        1094 sits above the dialog (1090) and its tooltips (1092), and below the duplicate-tab block
        (1095), which must cover everything by design.
    */
    z-index: 1094;
    top: calc(100% + 0.25rem);
    left: 0;
    right: 0;
    padding: 0.5rem;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    background: var(--bs-body-bg);
    box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.12);
}

.choice-picker-list {
    max-height: 15rem;
    overflow-y: auto;
    margin-top: 0.5rem;
}

.choice-picker-option {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    width: 100%;
    padding: 0.3rem 0.4rem;
    border-radius: var(--bs-border-radius-sm);
    cursor: pointer;
}

.choice-picker-option:hover { background: var(--bs-secondary-bg); }

/* WHERE ONE VALUE IS CHOSEN the radio is hidden: a list of radio buttons asks the reader to aim at
   a 14px circle when the whole row is already the target, and the chosen row is marked by its own
   tint. The input stays in the DOM so the keyboard and screen readers still have it. */
.choice-picker-radio { position: absolute; opacity: 0; pointer-events: none; }

.choice-picker-option:has(> .choice-picker-radio:checked) {
    background: var(--bs-primary-bg-subtle, var(--bs-secondary-bg));
    font-weight: 600;
}

.choice-picker-empty {
    padding: 0.5rem 0.4rem;
    font-size: 0.8125rem;
    color: var(--bs-secondary-color);
}

/*
   A MULTI-CHOICE FIELD HOLDS ITS CHOICES.

   It grows to fit them rather than scrolling or truncating: a school picking six classes needs to
   see six, and a control that hides half of what it holds is a control people re-open to check.
   min-height keeps an empty one exactly the height of every other field on the form, so a row of
   controls does not step up and down.
*/
.choice-picker-field-many {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.25rem;
    min-height: calc(1.5em + 0.75rem + 2px);
    height: auto;
    padding-block: 0.3rem;
}

.choice-picker-chip {
    display: inline-flex;
    gap: 0.25rem;
    align-items: center;
    padding: 0.125rem 0.25rem 0.125rem 0.5rem;
    border-radius: 1rem;
    font-size: 0.8125rem;
    line-height: 1.4;
    background: var(--bs-primary-bg-subtle, var(--bs-secondary-bg));
    color: var(--bs-primary-text-emphasis, var(--bs-body-color));

    /* The field is a button; its chips are not, so they must not inherit its cursor or alignment. */
    cursor: default;
    white-space: nowrap;
}

.choice-picker-chip-off {
    border: 0;
    background: none;
    line-height: 1;
    padding: 0 0.25rem;
    color: inherit;
    cursor: pointer;
    opacity: 0.65;
}

.choice-picker-chip-off:hover { opacity: 1; }

/* ==========================================================================================
   THE ACADEMIC YEAR IN THE HEADER — beside the school name, because they answer one question
   together: whose data, and from when.

   TWO STATES, AND THEY MUST NOT LOOK ALIKE. On the school's current year the chip is quiet;
   off it the chip is amber and carries the words "not the current year". The colour alone
   would be invisible to somebody who cannot distinguish it, and this is the one thing on the
   page that must never be missed — a register from 2021-2022 looks exactly like this year's,
   and acting on the wrong one is the entire risk the chip exists to prevent.
   ========================================================================================== */

.navbar-year {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    flex-shrink: 0;
    /* No side margin of its own: it sits in the navbar list now, where the list's own spacing
       separates it from the settings cog. A margin here would double up with that. */
    padding: 0.25rem 0.25rem 0.25rem 0.625rem;
    border: 1px solid transparent;
    border-radius: 2rem;
    font-size: 0.8125rem;
    line-height: 1.2;
}

.navbar-year-dot {
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
    flex-shrink: 0;
}

.navbar-year-name {
    font-weight: 600;
    white-space: nowrap;
    /* Digits in a year should not shift width as the year changes. */
    font-variant-numeric: tabular-nums;
}

.navbar-year-warning {
    white-space: nowrap;
    text-transform: uppercase;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.03em;
}

.navbar-year-change {
    padding: 0.125rem 0.5rem;
    font-size: 0.75rem;
    border-radius: 2rem;
    background: rgba(255, 255, 255, 0.55);
    border: 1px solid rgba(0, 0, 0, 0.08);
    color: inherit;
}

.navbar-year-change:hover { background: rgba(255, 255, 255, 0.9); color: inherit; }

/* The years in the switcher, and the column that lines their names up. */
.navbar-year-option {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.navbar-year-tick {
    display: inline-flex;
    justify-content: center;
    width: 1.125rem;
    flex-shrink: 0;
    color: var(--bs-success, #28a745);
}

/* THE ORDINARY STATE — quiet. Most people are on the current year most of the time, and a
   chip that shouts at them every day is a chip they stop reading. */
.navbar-year-current {
    background: var(--bs-success-bg-subtle, #e8f5e9);
    border-color: var(--bs-success-border-subtle, #c8e6c9);
    color: var(--bs-success-text-emphasis, #1b5e20);
}

.navbar-year-current .navbar-year-dot { background: var(--bs-success, #28a745); }

/* AWAY FROM THE CURRENT YEAR — loud on purpose. */
.navbar-year-past {
    background: var(--bs-warning-bg-subtle, #fff3cd);
    border-color: var(--bs-warning-border-subtle, #ffe69c);
    color: var(--bs-warning-text-emphasis, #664d03);
}

.navbar-year-past .navbar-year-dot { background: var(--bs-warning, #ffc107); }

@media (max-width: 991.98px) {
    /* On a narrow screen the school name and the year compete for a bar that cannot hold both.

       WHAT GOES IS WORDS, NEVER THE YEAR ITSELF OR ITS COLOUR. The chip still says which year and
       still turns amber off the current one, so the warning survives in the form that carries it
       furthest. The dropped words are recoverable — the button keeps its aria-label and title, and
       the menu it opens names the current year in full. */
    .navbar-year-warning { display: none; }

    .navbar-year-change-label { display: none; }

    /* Without its label the button is a caret alone, so it needs to stop being label-shaped. */
    .navbar-year-change { padding: 0.125rem 0.25rem; }
    .navbar-year-change::after { margin-left: 0; }
}

/* ==========================================================================================
   A HINT ON A LABEL — the only place a field's own note is allowed to live.

   Screens carry no explanatory line after a field. Where a rule has to be known BEFORE the
   field is used — what an upload may contain, what a password must have — it rides on the
   label instead, lighter than the label's own words so it reads as a qualifier rather than as
   part of the name.
   ========================================================================================== */

.label-hint,
.upload-field-rules {
    font-weight: 400;
    font-size: 0.8125rem;
    color: var(--bs-secondary-color, #6f6b7d);
}

/* Set apart from the label's words without a bullet or a bracket to align. */
.label-hint::before,
.upload-field-rules::before {
    content: "— ";
}

/* ==========================================================================================
   THE HEADER: ONE FULL-WIDTH BAR ACROSS THE TOP, AND IT STAYS PUT.

   WHY IT HAS TO STAY. The bar answers "whose data is this, and from when?" - the two facts
   most easily got wrong on a platform holding many schools and many years. On a short page
   that answer is on screen anyway. On a long one - a student form, a full class list - it
   scrolled away in the first flick of the wheel, and everything after that was read with no
   statement of which school or which year it belonged to. That is exactly when somebody edits
   last year's record believing it is this year's.

   WHY IT IS NO LONGER A FLOATING CARD. The theme draws this bar "detached": inset from the
   edges, rounded, with a shadow. That is a decoration for a bar that scrolls away with the
   page. Pinned, it became a fault - the inset leaves a transparent strip above and beside it,
   and rows scrolling past showed THROUGH that strip, so the page title appeared to be sliced
   in half by nothing. A pinned bar has to be a solid edge-to-edge band or it is not a bar,
   it is a card with content leaking around it.

   So it is flush: no margin, no radius, spanning from the menu's edge to the window's, with a
   line under it instead of a shadow. Its left padding is the SAME token the content well
   uses, so the school name sits directly above the page title rather than near it.

   THE ONE THING THAT HAD TO CHANGE FOR STICKY TO WORK AT ALL. Sticky is measured against the
   nearest SCROLLING ancestor, and `html, body { overflow-x: hidden }` made the body one - a
   box that hides overflow in one axis scrolls in the other. The header would have stuck to
   the top of a box that never moves, which looks exactly like nothing happening. The hiding
   moves to the html element alone, where the browser hands it to the VIEWPORT: pages still
   never scroll sideways, and nothing sits between the header and the window.
   ========================================================================================== */

body { overflow-x: visible; }

/*
   ONE HEIGHT FOR THE WHOLE TOP EDGE — the logo block on the left and the header on the right.

   They were both 4rem tall already and still did not line up, because the theme gives the logo
   block a 1rem top margin: it began 16px lower, so its underline landed 16px below the header's
   and the top of the application read as two panels stacked at different heights. Written as a
   token rather than twice, so the two cannot drift apart the next time either is adjusted.
*/
:root { --app-header-h: 4rem; }

.layout-navbar {
    position: sticky;
    top: 0;

    /* Above the page's content, below Bootstrap's modal backdrop (1050) so a dialog covers it. */
    z-index: 1035;

    /* EDGE TO EDGE. The "container-xxl" and "navbar-detached" classes are gone from the markup
       rather than overridden here - see the note on the element - so these are only a floor
       against a theme upgrade putting the inset back. */
    max-width: none;
    width: 100%;
    margin: 0;
    border-radius: 0;

    /* Lined up with the content below it, from the same token, so the school name sits above
       the page title rather than beside it. */
    padding-inline: var(--page-px);
    padding-block: 0.75rem;
    min-height: var(--app-header-h);

    /* OPAQUE. Rows pass behind this, and a translucent bar shows them through the school name. */
    background: var(--bs-body-bg, #fff);

    /* A line rather than the card's shadow: a shadow says "floating above", and this no longer
       is - it is the top edge of the page. */
    box-shadow: none;
    border-bottom: 1px solid var(--bs-border-color);
}

/*
   THE BAR'S CONTENTS FIT THE BAR.

   The inner row carries Bootstrap's w-100, which was harmless while the bar was a container
   with the padding inside its measured width. Flush and padded, 100% of the CONTENT box plus
   the padding plus the phone's menu button came to more than the window - fifteen pixels over
   on a 375px screen, which is a page that scrolls sideways. The one thing the stylesheet
   promises it never does.

   So the row takes the space that is left rather than all of it, and is allowed to shrink:
   min-width: 0 is what lets the school name's ellipsis do its job instead of the row refusing
   to go below its content's width.
*/
.layout-navbar .navbar-nav-right {
    width: auto;
    flex: 1 1 auto;
    min-width: 0;
}

/* ==========================================================================================
   A FORM'S HEAD — the title, a rule under it, and the way out on the right.

   A list is somewhere you ARE; a form is something you are in the MIDDLE of, and the one thing
   it always needs is an exit that is visible on arrival rather than found by scrolling to the
   bottom. So a form's heading is drawn as a band: title left, X right, one line beneath.

   Its white background and full-bleed are set further down, beside the reasoning for them.
   ========================================================================================== */

.page-head-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    color: var(--bs-secondary-color, #6f6b7d);
    font-size: 1.5rem;
    line-height: 1;
    text-decoration: none;
    transition: background-color .15s ease, color .15s ease;
}

.page-head-close:hover,
.page-head-close:focus-visible {
    background: var(--bs-secondary-bg);
    color: var(--bs-body-color);
}

/* ==========================================================================================
   A FORM PAGE IS AN EDITOR PANE: one white surface, hairlines between sections, and the way
   to finish held at the foot of the window.

   THREE ATTEMPTS GOT HERE, and each failure is worth keeping because each was a reasonable
   idea that the screen disproved:

     1. THE WHOLE AREA WHITE. On a six-field form most of the window was empty white, which
        reads as a page that failed to load.
     2. A WHITE SHEET HUGGING THE FORM. It welded the heading to the fields as one object, and
        the grey resumed under it - so a short form was a small white box adrift on a desk.
     3. THE TITLE BAND WHITE, THE REST GREY. The seam moved rather than closing: the fields
        then sat on grey inside a white-topped page, which is two materials for one job.

   What was wrong in all three was the GREY, not where it stopped. Grey behind a list is doing
   work - it separates one record card from the next. A form has one object and no set, so the
   grey separates nothing and only ever reads as an unfinished area of the page.

   So: white from the title band to the footer, hairlines where sections divide, and the pane
   always as tall as the window. Nothing floats, nothing is boxed, and there is no seam.
   ========================================================================================== */

/*
   THE CHAIN THAT LETS A SHORT FORM STILL REACH THE BOTTOM OF THE WINDOW.

   Every link has to be a full-height flex column. Setting flex on the pane alone did nothing,
   because its parent - the content well - was not a flex container, and flex is a property of
   the PARENT. :has() is what lets the well be told this only on the pages that need it, without
   a second class threaded through the layout.
*/
.container-xxl:has(> .page-sheet-form) {
    display: flex;
    flex-direction: column;
}

.page-sheet-form {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;

    /* THE CARD'S WHITE, not --bs-body-bg. In this theme the body variable IS the grey desk
       (#f5f5f9), so asking for "the body background" returns the colour being replaced - a rule
       that looks correct and changes nothing. */
    background: var(--bs-card-bg, #fff);

    /* Out to the edges of the content well and back in again, so the pane spans the page. */
    margin: calc(var(--page-py) * -1) calc(var(--page-px) * -1);
    padding: var(--page-py) var(--page-px) 0;
}

/* EVERY link, down to the form itself. A single block element anywhere in the chain - the card
   body was one - stops the height propagating and the footer lands wherever the fields end. */
.page-sheet-form > .row,
.page-sheet-form > .row > [class*="col-"],
.page-sheet-form .card,
.page-sheet-form .card > .card-body,
.page-sheet-form form {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
}

/* The columns stop constraining the page; the FIELDS take the reading width instead. A text box
   the full width of a monitor is a box nobody can scan, but a footer that stops two-thirds
   across reads as a card's toolbar rather than the foot of the screen.

   flex-basis as well as max-width: Bootstrap's col-lg-7 sets "flex: 0 0 58.33%", and a max-width
   alone leaves that basis in place - which is why the bar kept stopping at 958px. */
.page-sheet-form:not(.page-sheet-wide) .row > [class*="col-"] {
    /* WIDTH, not flex-basis. The row is a flex COLUMN here, so a basis of 100% applies to the
       height and leaves Bootstrap's own "width: 58.33%" untouched - which is why the footer kept
       stopping at two-thirds of the page however the basis was written. */
    width: 100%;
    max-width: 100%;
}

.page-sheet-form:not(.page-sheet-wide) form > *:not(.form-footer) { max-width: 46rem; }

/* A WIDE FORM KEEPS ITS COLUMNS and the width to lay them out in. Still capped, because three
   columns stretched across a 27-inch monitor put a label and its box a hand's width apart. */
.page-sheet-wide form > *:not(.form-footer) { max-width: 78rem; }

/* THE TITLE BAND is the top of the same pane, so it carries no colour of its own - only the
   hairline that separates it from the fields. */
.page-head-form {
    margin-inline: calc(var(--page-px) * -1);
    padding-inline: var(--page-px);
    margin-top: calc(var(--page-py) * -1);
    padding-top: var(--page-py);
    padding-bottom: 0.875rem;
    margin-bottom: var(--page-py);
    border-bottom: 1px solid var(--bs-border-color);
}

.page-head-form .page-head-title { font-size: 1.375rem; }

/* ------------------------------------------------------------------------------------------
   SECTIONS ARE DIVIDED BY A LINE, NOT BY A BOX.
   ------------------------------------------------------------------------------------------ */

.page-sheet-form .card,
.page-sheet-form .card > .card-header {
    background: transparent;
    border: 0;
    box-shadow: none;
}

.page-sheet-form .card > .card-body { padding: 0; }

.page-sheet-form .card + .card {
    border-top: 1px solid var(--bs-border-color);
    padding-top: 1.5rem;
    margin-top: 1.5rem;
}

/* ------------------------------------------------------------------------------------------
   A WIDE FORM SITS CLOSER TOGETHER.

   The theme gives a card header 24px of padding on every side. That is right for a card with a
   border around it, where the padding IS the card. Here the border is gone and the header is
   simply a section name, so the padding became a band of empty space above every group - and on
   a form of forty fields the reader spends the scroll on gaps rather than on fields.

   The heading now sits just above the fields it names, and the rule between sections carries the
   separation instead.
   ------------------------------------------------------------------------------------------ */
.page-sheet-wide .card > .card-header {
    padding: 0 0 .4rem;
    font-size: .9375rem;
}

.page-sheet-wide .card {
    margin-bottom: 0;
}

.page-sheet-wide .card + .card {
    padding-top: 1.25rem;
    margin-top: 1.25rem;
}

/* THE TABS AND THE FIRST SECTION ARE ONE THING - the strip says which group is on screen, so the
   heading under it is a sub-heading rather than a fresh start. Three separate gaps were stacking
   here: the strip's own margin, the pane's padding, and the header's padding below that. */
.page-sheet-wide .tab-content {
    padding-top: .5rem;
}

.page-sheet-wide .nav-tabs {
    margin-bottom: .5rem !important;
}

/* ------------------------------------------------------------------------------------------
   SAVE AND CANCEL, HELD AT THE FOOT OF THE WINDOW.

   margin-top: auto pushes the bar to the bottom of a SHORT form, so it is not left floating in
   the middle of the pane with nothing under it. sticky holds it there while a LONG form scrolls
   past - the student form runs to seventeen sections, and its Save was the furthest below the
   fold in the application.

   STICKY, NOT FIXED. Fixed takes the bar out of the flow and it then covers the last field of a
   short form, which is the fault this was meant to prevent.
   ------------------------------------------------------------------------------------------ */

.form-footer {
    position: sticky;
    bottom: 0;
    z-index: 3;

    display: flex;
    gap: .5rem;
    align-items: center;

    margin-top: auto;
    margin-inline: calc(var(--page-px) * -1);
    padding: .875rem var(--page-px);

    background: var(--bs-card-bg, #fff);
    border-top: 1px solid var(--bs-border-color);
}

/*
   NO SITE FOOTER UNDER THE ACTION BAR.

   The page's own footer reads "Microweb LMS - <school>", which the header states already. On a
   list it is a harmless closing line; under a form's Save button it puts a strip of chrome
   BELOW the most important control on the screen, and leaves the bar floating 35px short of the
   window edge where it looks like a mistake rather than a decision.
*/
.layout-page:has(.page-sheet-form) > .content-wrapper > .content-footer { display: none; }

/* SAVE FIRST, reading order: the primary action is what somebody came to do. */
.form-footer .btn-primary { order: 0; }

.form-footer .btn-cancel { order: 1; }

/* ==========================================================================================
   THE PROBLEM PANEL — what a form says when it will not save.

   #feedee, the founder's colour: a pale wash rather than a saturated red. The panel has to be
   noticed and then READ, and a strong red is noticed and skimmed — people see "something is
   wrong" and start hunting fields instead of reading the lines that name them.

   SET THROUGH BOOTSTRAP'S OWN VARIABLES, not as background-color. The theme paints this element
   with "background-color: var(--bs-alert-bg)", so a plain background-color declaration here is
   overwritten by a variable that still holds the theme's pink — which is exactly what happened:
   the rule read correctly and the panel stayed rgb(255,224,219).
   ========================================================================================== */

.alert-danger {
    --bs-alert-bg: #feedee;
    --bs-alert-border-color: #f7ccd0;
    --bs-alert-color: #8c2f39;

    /*
       SMALLER THAN THE FORM'S OWN TEXT.

       The panel is already the loudest thing on the page - a block of colour the width of the
       screen, above everything else. The words inside it do not also need to be large, and at
       15px two short messages filled a band deeper than the fields they refer to. The colour
       carries the alarm; the text only has to be read.
    */
    font-size: 0.8125rem;
    line-height: 1.45;
}

/*
   NO DEAD SPACE UNDER THE LAST LINE.

   The validation summary renders a bare <ul>, and the browser's own stylesheet gives a list a
   1rem bottom margin. Added to the panel's padding that left 29px of empty colour below the
   final message — a box that looked like it had lost something.

   The .mb-0 on the wrapper did not reach it: the margin belongs to the LIST inside, not to the
   div around it.
*/
.alert ul {
    margin: 0;
    padding-inline-start: 1.1rem;
    list-style: disc;
}

.alert ul li + li { margin-top: .15rem; }

/* The x that unsets a dropdown. Sits over the field's own chevron side, ahead of it, so the two
   never overlap: the chevron opens the list, this empties it, and they are different jobs. */
.choice-picker-clear {
    position: absolute;
    top: 50%;
    right: 2rem;
    transform: translateY(-50%);
    z-index: 2;

    border: 0;
    background: none;
    padding: 0 .25rem;
    line-height: 1;
    font-size: 1.125rem;
    color: var(--bs-secondary-color, #6f6b7d);
    cursor: pointer;
    opacity: .7;
}

.choice-picker-clear:hover,
.choice-picker-clear:focus-visible { opacity: 1; color: var(--bs-body-color); }

/* Room for it, so a long chosen value does not run underneath. */
.choice-picker:has(> .choice-picker-clear:not([hidden])) .choice-picker-field { padding-right: 3.5rem; }

/* SELECT ALL sits above the list and is separated from it: it acts on every row below rather than
   being one of them, and a reader scanning for a value should not have to read past it twice. */
.choice-picker-all {
    border-bottom: 1px solid var(--bs-border-color);
    border-radius: 0;
    margin-bottom: .25rem;
    padding-bottom: .45rem;
    font-weight: 600;
}

/* ==========================================================================================
   THE TOOLS MENU — the ⋯ beside a list's primary action.

   TIGHT, because it is a menu and not a page. The first pass inherited the theme's dropdown
   padding, which is sized for the account menu's three roomy rows; with nine rows in three
   sections it turned a short list of actions into something to scroll. A reader opens this to
   find one thing and close it again.
   ========================================================================================== */

/*
    THE ⋯ IS A SQUARE, the size of the button beside it.

    It inherited a text button's side padding — 20px each side around a single glyph — which made a
    57px slab for a three-pixel icon. An icon button is as wide as it is tall or it reads as an empty
    button somebody forgot to label. 34px matches the Filter button on its left, so the toolbar ends
    on a straight line.
*/
.list-toolbar-more {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.125rem;
    height: 2.125rem;
    padding: 0;
}

.list-toolbar-more > .bx { font-size: 1.15rem; line-height: 1; }

.list-tools-menu .dropdown-divider,
.list-tools-flyout .dropdown-divider {
    /* Full width across the panel. The side padding is there to inset the ROWS; a divider that
       stopped short of the edges would read as a third thing rather than a line between sections. */
    margin-inline: -.25rem;
}

.list-tools-menu {
    min-width: 12rem;

    /*
        THE PANEL SITS ABOVE THE PAGE, and the shadow is what says so.

        Two layers, because one cannot do both jobs: a tight dark one right under the edge draws the
        lip of the panel, and a wide soft one further down puts distance between it and the page. One
        shadow either looks like a drawn border or like fog. Zoho's is measured at 0 7px 12px; the
        spread here matches, tinted toward our own accent rather than their lavender.

        The hairline border is not decoration either — against a white card the shadow alone leaves
        the top edge with nothing to end it.
    */
    border: 1px solid rgba(67, 56, 202, .12);
    box-shadow:
        0 0 0 1px rgba(24, 28, 50, .02),
        0 2px 4px rgba(24, 28, 50, .09),
        0 14px 32px -6px rgba(52, 43, 158, .28);

    /* Room on all four sides, so a filled row sits INSIDE the panel rather than running into its
       edge — which is what makes the highlight read as a chip rather than a stripe. */
    padding: .25rem;

    /* SMALLER THAN THE PAGE'S TEXT. A menu is chrome — a short list of things to do, read once and
       dismissed — not content. At the body's size it carried the weight of a paragraph. */
    font-size: .8125rem;
}

.list-tools-menu .dropdown-header {
    padding: .35rem .9rem .2rem;
    font-size: .6875rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--bs-secondary-color, #6f6b7d);
}

.list-tools-menu .dropdown-divider { margin-block: .25rem; }

/* A form inside a menu is still a menu row: the button has to fill it, not sit in it. */
.list-tools-menu form { margin: 0; }

.list-tools-menu .dropdown-item {
    display: flex;
    align-items: center;
    gap: .5rem;
    width: 100%;

    /* 34px rows. The text went to 13px and the padding came down with it, which made a menu that
       had to be aimed at. Zoho holds 35px rows at the same 13px — small type, big target. */
    padding: .5rem .9rem;
    font-size: .8125rem;
    border-radius: 4px;
    border: 0;
    background: none;

    /* LEFT EDGE SHARED WITH THE HEADING ABOVE. The column names line up under "Sort by" rather
       than being indented past it, so the section reads as one block. */
    text-align: start;
}

/*
   THE ICONS CARRY THE THEME'S COLOUR, as they do in the founder's reference. Grey icons beside
   black words read as decoration; the accent makes them a column the eye can run down.

   FIXED WIDTH AND NO SHRINKING. The rows are flex, and an icon left to size itself was being
   squeezed by the label beside it - bx-refresh in particular came out visibly narrower than it is
   tall, which reads as a broken glyph rather than a tight one.
*/
.list-tools-menu .dropdown-item > .bx {
    flex: 0 0 auto;

    /* A SQUARE SLOT. The box was 16.8 wide and 15.2 tall, which gave every round icon a shade of
       horizontal room and made the roundest ones look flattened into it. */
    width: 1rem;
    height: 1rem;
    line-height: 1rem;
    font-size: 1rem;
    line-height: 1;
    text-align: center;
    color: var(--app-primary, var(--bs-primary));
}

/* The chevron on a branch row is CHROME, not one of the action icons: it says "there is more this
   way", it does not name a thing to do. Same specificity as the rule above, so it needs the extra
   class to win rather than luck of the source order. */
.list-tools-menu .dropdown-item > .bx.list-tools-chevron {
    color: var(--bs-secondary-color, #6f6b7d);

    /* Keeps a width. These glyphs are drawn by a ::before, so the element has no content of its own
       and "auto" resolved to zero — the chevron was there, coloured, and one pixel wide. */
    width: 1rem;
}

/* ------------------------------------------------------------------------------------------
   THE SORT-BY FLYOUT — a menu row that opens a list of its own.
   ------------------------------------------------------------------------------------------ */

.list-tools-branch { position: relative; }

.list-tools-chevron { transition: transform .12s ease; }

.list-tools-branch.is-open > .js-tools-branch > .list-tools-chevron { transform: rotate(90deg); }

.list-tools-branch.is-open > .js-tools-branch { background: var(--bs-secondary-bg); }

.list-tools-flyout {
    display: none;
    font-size: .8125rem;
    position: absolute;
    top: -.25rem;

    /* OPENS TO THE LEFT. The menu itself is pinned to the right of the screen, so a panel opening
       rightwards would run off the edge on every list. */
    /*
        A REAL GAP, BRIDGED.

        Touching the menu made the two panels read as one wide block with a seam down the middle —
        each needs its own shadow all the way round to look like a separate sheet of paper.

        The gap is why this used to close late: crossing eight pixels of nothing means the pointer
        leaves the branch, so a delay was covering the journey. The bridge on the row covers it
        instead — an invisible strip that belongs to the branch, so the pointer never actually
        leaves. Gap and instant close, rather than one or the other.
    */
    right: 100%;

    /* 7px here shows as 2px between the panels: "right: 100%" measures from the ROW, and the row
       sits 5px inside the panel it lives in — that panel's own padding and border.

       2px, down from 8. Eight was a window onto the page behind, and grid text showing through the
       slot between two menus reads as a rendering fault. Two is a seam: each panel still ends at its
       own edge, and nothing behind can frame itself in the space. */
    margin-right: .4375rem;

    min-width: 12rem;
    padding: .25rem;
    margin-block: 0;
    list-style: none;

    background: var(--bs-dropdown-bg, #fff);
    border-radius: var(--bs-border-radius, .375rem);

    /* A GRID WITH MANY SORTABLE COLUMNS SCROLLS rather than growing past the window. Eight rows is
       where a list stops being scannable anyway. */
    max-height: 17rem;
    overflow-y: auto;
    z-index: 1;

    border: 1px solid rgba(67, 56, 202, .12);
    box-shadow:
        0 0 0 1px rgba(24, 28, 50, .02),
        0 2px 4px rgba(24, 28, 50, .09),
        0 14px 32px -6px rgba(52, 43, 158, .28);
}

.list-tools-branch.is-open > .list-tools-flyout { display: block; }

/*
    THE BRIDGE — the gap, made part of the branch.

    It hangs off the ROW, not off the panel: the panel scrolls (overflow-y below), and overflow clips
    anything drawn outside it, so a strip attached there was cut away before it could catch anything.
    On the row it is a few pixels of nothing that happen to belong to the branch, which is all the
    pointer needs to cross without the menu deciding it has been abandoned. Its width tracks the gap
    above — change one and the other has to follow, or the menu starts closing mid-journey again.
*/
.list-tools-branch.is-open::after {
    content: "";
    position: absolute;
    top: 0;
    left: -.4375rem;
    width: .4375rem;
    height: 100%;
}

@media (max-width: 575.98px) {
    /* No room beside the menu on a phone: the list drops underneath, indented so it still reads as
       belonging to the row above it. */
    .list-tools-flyout {
        position: static;
        right: auto;
        border: 0;
        box-shadow: none;
        border-left: 2px solid var(--bs-border-color);
        border-radius: 0;
        margin-left: .9rem;
        min-width: 0;
    }
}

/*
    THE SORTED COLUMN IS A FILLED ROW, not coloured words.

    It was accent-coloured text, which asks the eye to compare six labels and notice one is a
    different shade — findable, but only once you go looking. A filled bar is found before it is
    read: open the list and the answer is already there. The arrow beside it says which way.

    No tick column either way, so nothing is indented to leave room for a mark most rows do not carry.
*/
.list-tools-sort.is-sorted {
    background-color: var(--app-primary, var(--bs-primary));
    color: #fff;
}

.list-tools-sort.is-sorted > .bx { opacity: 1; color: #fff; }

/*
    THE HIGHLIGHT FOLLOWS THE CURSOR, and the sorted row steps back while it does.

    Two rows filled in the same colour would be two answers to "what happens if I click": one is
    where the list IS sorted, the other is where it is ABOUT to be. So the moment the pointer enters
    the list, the sorted row drops to grey — still marked, still carrying its arrow, but no longer
    competing with the row under the cursor.

    Leave the list and it takes the colour back, because then it is the only answer again.
*/
/*
    EVERY ROW IN EVERY FLYOUT HIGHLIGHTS, not just the sort ones.

    These rules were written against .list-tools-sort back when Sort by was the only branch. Export
    arrived with rows that carry no such class, so its list sat there not reacting — which reads as a
    menu that has stopped working rather than one that simply is not styled.
*/
.list-tools-flyout .dropdown-item:hover,
.list-tools-flyout .dropdown-item:focus {
    background-color: var(--app-primary, var(--bs-primary));
    color: #fff;
}

.list-tools-flyout:hover .list-tools-sort.is-sorted {
    background-color: var(--bs-tertiary-bg, #f1f0f2);
    color: var(--bs-body-color);
}

.list-tools-flyout:hover .list-tools-sort.is-sorted > .bx { color: var(--bs-secondary-color, #6f6b7d); }

/* Whatever the pointer is on wins, including the sorted row itself. */
.list-tools-flyout .list-tools-sort:hover,
.list-tools-flyout .list-tools-sort:focus,
.list-tools-flyout .list-tools-sort.is-sorted:hover {
    background-color: var(--app-primary, var(--bs-primary));
    color: #fff;
}

.list-tools-flyout .list-tools-sort:hover > .bx,
.list-tools-flyout .list-tools-sort.is-sorted:hover > .bx { color: #fff; }

/* THE BRANCH ROW LIGHTS UP WHILE ITS LIST IS OPEN. The list opens to the LEFT, so without this the
   panel appears beside a row with nothing to connect it to — and the pointer has already left. */
.list-tools-branch.is-open > .js-tools-branch,
.list-tools-branch.is-open > .js-tools-branch:hover {
    background-color: var(--app-primary, var(--bs-primary));
    color: #fff;
}

.list-tools-branch.is-open > .js-tools-branch > .bx { color: #fff; }

/* ============================================================================================
   EXPORT DIALOG — one dialog, every list. See _ExportDialog.cshtml.
   ============================================================================================ */

.export-note {
    margin-bottom: .75rem;
    font-size: .8125rem;
    color: var(--bs-secondary-color, #6f6b7d);
}

.export-note:last-child { margin-bottom: 0; }

/*
    SECTIONS DIVIDED BY A LINE, not by cards. The founder's rule for every form here: a panel inside
    a panel makes the dialog look like three dialogs stacked up.
*/
.export-section {
    padding-top: .9rem;
    margin-top: .9rem;
    border-top: 1px solid var(--bs-border-color);
}

.export-legend {
    margin-bottom: .5rem;
    font-size: .75rem;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--bs-secondary-color, #6f6b7d);
}

.export-password-body { padding-top: .75rem; }

/*
    THE RULES IN TWO COLUMNS. Five rules in one column pushes the buttons off a laptop screen; side
    by side they are read at a glance, which is all they are for.
*/
.export-rules {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: .15rem 1.5rem;
    margin: .75rem 0 0;
    padding-left: 1.1rem;
    font-size: .8125rem;
    color: var(--bs-secondary-color, #6f6b7d);
}

@media (max-width: 575.98px) {
    .export-rules { grid-template-columns: 1fr; }
}

/* ============================================================================================
   EXPORT DIALOG, continued — the password rules tick themselves off as they are met.
   ============================================================================================ */

/*
    A RULE GOES THEME-COLOURED WHEN IT IS SATISFIED, with a tick beside it.

    Five rules read as five complaints while the box is empty. Turning each one as it is met makes
    the same list an instruction that answers itself — and somebody who has met four of five can see
    WHICH one is left instead of re-reading all of them.

    Colour is not the only signal: the bullet becomes a tick, so this still works for a reader who
    cannot tell the two colours apart.
*/
.export-rules > li {
    list-style: none;
    position: relative;
    padding-left: 1.1rem;
    transition: color .12s ease;
}

.export-rules > li::before {
    content: "•";
    position: absolute;
    left: .2rem;
    color: var(--bs-secondary-color, #6f6b7d);
}

.export-rules li.is-met,
.export-rules li.is-met::before {
    /* The school's accent, spelled the same way the buttons spell it. */
    color: var(--app-primary, #4338ca);
}

.export-rules li.is-met { font-weight: 500; }

.export-rules li.is-met::before {
    /* The tick is the literal character, not an escape. Written as \2713 once, and the file's
       encoding turned it into two stray letters — a bullet that had become "¹3". */
    content: "✓";
    left: 0;
}

/*
    THE DIALOG SITS NEAR THE TOP OF THE WINDOW.

    The theme pulls a modal upward with a transform meant for its own centred layout, which put the
    heading off the top edge once this one grew a password section. A plain top margin instead: it
    opens where it is expected and grows downward, and the body scrolls if it ever outgrows a laptop
    screen rather than pushing the buttons out of reach.
*/
#export-dialog .modal-dialog {
    /* At the top, not floating below it. Just enough that the shadow has somewhere to fall. */
    margin-top: .75rem;
    transform: none;
}

#export-dialog .modal-body {
    max-height: calc(100vh - 11rem);
    overflow-y: auto;
}

/*
    THE CLOSE X BELONGS INSIDE THE HEADER, level with the title.

    The theme floats it outside the dialog and over the backdrop, which is a look that only works on
    a full-bleed image panel. On a white dialog it read as a stray control sitting on the grey.
*/
/*
    A GREY BAND ACROSS THE TOP. It separates the title from the choices below without a card border
    doing it, and it is the same grey the page's own desk uses — the dialog reads as part of the
    application rather than as a window from somewhere else.
*/
#export-dialog .modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: .9rem 1.25rem;
    background-color: var(--bs-secondary-bg, #f2f2f4);
    border-bottom: 1px solid var(--bs-border-color);
    border-radius: var(--bs-modal-border-radius, .5rem) var(--bs-modal-border-radius, .5rem) 0 0;
}

/* Hard against the right corner, where a close control is looked for. */
#export-dialog .modal-header .btn-close {
    position: static;
    margin: 0 0 0 auto;
    padding: .5rem;
    transform: none;
    background-color: transparent;
    box-shadow: none;
    opacity: .55;
}

#export-dialog .modal-header .btn-close:hover { opacity: 1; }

/*
    HIDDEN CHARACTERS WITHOUT A PASSWORD FIELD.

    The box is a plain text input so no browser offers to save it to a vault (see _ExportDialog);
    this is what still hides what is typed. Both spellings are needed — the unprefixed one is the
    standard, the -webkit- one is what Chrome and Edge actually implement today.
*/
.password-masked {
    -webkit-text-security: disc;
    text-security: disc;

    /* Dots read better with a little air between them at this size. */
    letter-spacing: .08em;
}

.password-masked.is-revealed {
    -webkit-text-security: none;
    text-security: none;
    letter-spacing: normal;
}

.export-problem {
    margin: .75rem 0 0;
    font-size: .8125rem;
}

/*
    A ROW ACTION THAT IS NOT AVAILABLE, because the record is inactive.

    Faded and unclickable, not removed. A row whose buttons vanish reads as a broken row and leaves
    the reader wondering what they did; a dead button with a tooltip says what happened.

    pointer-events is what makes it truly dead to a mouse — the tag helper has already taken the
    address away, so this is the visible half of the same statement.
*/
.btn-icon.is-frozen,
.btn-icon.is-frozen:hover,
.btn-icon.is-frozen:focus {
    opacity: .38;
    pointer-events: none;
    cursor: default;
    background: var(--bs-body-bg);
    color: var(--bs-secondary-color);
    border-color: var(--bs-border-color);
}

/* ---------------------------------------------------------------------------------------------
   SECTIONS - a class opens to show the sections it runs.

   TWO ICONS SWAPPED, not one icon rotated. Rotating the chevron read better, but the theme's own
   rules win over a transform on an icon and it never turned - so the state was invisible, which is
   the one thing this arrow exists to show. Swapping which icon is displayed cannot be overridden
   the same way.
   --------------------------------------------------------------------------------------------- */
.sec-chevron-open {
    display: none;
}

[aria-expanded="true"] .sec-chevron-open {
    display: inline-block;
}

[aria-expanded="true"] .sec-chevron-shut {
    display: none;
}

/* The panel sits under its class, so no line between them - they are one thing. */
.sec-detail > td {
    border-top: 0;
}

.sec-table th {
    font-weight: 600;
}

/* ---------------------------------------------------------------------------------------------
   THE i BESIDE A BOX TOO NARROW FOR ITS VALUE.

   Deliberately quiet - it sits in every row of a column, so anything louder would draw the eye
   away from the values themselves, which are the point of the screen.
   --------------------------------------------------------------------------------------------- */
.btn-see-full {
    padding-inline: .45rem;
    color: var(--bs-secondary-color, #6f6b7d);
}

.btn-see-full i {
    font-size: 1rem;
    vertical-align: middle;
}

/* The whole value, shown on demand. Wraps, because the reason it is here is that it did not fit. */
.app-full-value .popover-body {
    font-family: var(--bs-font-monospace);
    font-size: .8125rem;
    word-break: break-all;
}

/* ---------------------------------------------------------------------------------------------
   A PAIR OF BLOCKS, TOLD APART — the two addresses, and the two parents.

   Side by side each pair holds the same labels twice over, and without a line between them the eye
   reads one wide form rather than two answers. The rule does what the gap alone could not.

   ONLY WHERE THEY ARE ACTUALLY SIDE BY SIDE. Below the large breakpoint the columns stack, and a
   line down the left of the lower one would point at nothing.

   :last-of-type rather than :last-child on the parents, because either block can be hidden when a
   child is with one parent — and a line down the left of the only block on screen is a line to
   nowhere. A hidden column is still the last CHILD; this way the rule goes with it.
   --------------------------------------------------------------------------------------------- */
@media (min-width: 992px) {
    .address-pair > [class*="col-"]:last-child,
    .parent-pair > [class*="col-"]:last-child:not([hidden]) {
        border-left: 1px solid var(--bs-border-color);
    }

    /* One parent on screen stands alone: nothing to divide it from. */
    .parent-pair > [hidden] + [class*="col-"],
    .parent-pair > [class*="col-"]:first-child[hidden] ~ [class*="col-"] {
        border-left: 0;
    }
}

/* The questions ABOUT the family — single parent, which parent, the anniversary — sit above the
   people themselves, and the same rule separates them. Whether they are asked at all decides what
   the blocks below show, so the reader should finish them before starting on a name. */
.family-facts {
    border-bottom: 1px solid var(--bs-border-color);
    padding-bottom: 1.5rem;
    margin-bottom: 1.5rem;
}

/* The guardian sits below the two parents, told apart from them the only way a stacked block can be:
   a rule across, with room either side of it. */
.guardian-block {
    border-top: 1px solid var(--bs-border-color);
    margin-top: 1.5rem;
    padding-top: 1.5rem;
}

/* ---------------------------------------------------------------------------------------------
   NEW STUDENT / ALREADY STUDYING HERE.

   Written out rather than left to btn-outline-primary, which the theme paints green-on-green the
   moment the pointer passes over the unchosen half — the words disappeared exactly when somebody
   was reaching for them.

   The chosen side is filled and the other is plain, so which one is on is readable at a glance and
   stays readable under the cursor.
   --------------------------------------------------------------------------------------------- */
/* !important, and not lightly: the theme paints these buttons with !important of its own, so a
   plain rule here lost however specific it was — the fill stayed on whichever half the page was
   rendered with and the screen contradicted the form. Scoped to this one control. */
.student-kind .btn {
    border: 1px solid var(--bs-border-color) !important;
    background: var(--bs-body-bg) !important;
    color: var(--bs-body-color) !important;
    font-weight: 500;
}

.student-kind .btn:hover {
    background: var(--bs-secondary-bg, #f5f5f9) !important;
    color: var(--bs-body-color) !important;
}

/* .is-on rather than :checked — see the note in the form's own script. */
.student-kind .btn.is-on,
.student-kind .btn.is-on:hover {
    background: var(--bs-primary) !important;
    border-color: var(--bs-primary) !important;
    color: #fff !important;
}

.student-kind .btn-check:focus-visible + .btn {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

/* ---------------------------------------------------------------------------------------------
   THE UPLOAD CONTROL.

   A bordered area somebody can drop a file onto, showing what they chose. The real file input is
   moved out of sight rather than hidden with display:none — a display:none input cannot be focused
   or opened by a click in some browsers, and it is still the thing that posts.
   --------------------------------------------------------------------------------------------- */
.upload-drop {
    border: 1px dashed var(--bs-border-color);
    border-radius: var(--bs-border-radius, .375rem);
    padding: 1rem;
    text-align: center;
    background: var(--bs-body-bg);
    transition: border-color .15s ease, background .15s ease;
}

/* While a file is over it, so the drop target is obvious rather than guessed at. */
.upload-drop.is-over {
    border-color: var(--bs-primary);
    background: var(--bs-secondary-bg, #f5f5f9);
}

.upload-input {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

/*
    THE ICON SITS OVER THE WORDS, CENTRED.

    text-align could not do this on its own. Every icon in the product is a mask-image box a fixed
    1em wide, so making it display:block gave a 28px box against the left edge with the sentence
    centred underneath it — text-align moves TEXT inside a box, never the box itself.

    So the empty state is a centred column instead: the icon and the sentence are two items stacked
    and centred as items, which holds however wide the card is and whatever width the icon set has.
*/
.upload-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .35rem;
}

.upload-empty-icon {
    font-size: 1.75rem;
    color: var(--bs-secondary-color, #6f6b7d);
}

.upload-empty-text {
    font-size: .8125rem;
    color: var(--bs-secondary-color, #6f6b7d);
}

.upload-preview {
    max-height: 150px;
    max-width: 100%;
    border-radius: var(--bs-border-radius, .375rem);
    cursor: zoom-in;
}

.upload-file {
    font-size: .8125rem;
    word-break: break-all;
}

.upload-actions {
    display: flex;
    gap: .75rem;
    justify-content: center;
    margin-top: .5rem;
    font-size: .8125rem;
}

/* The picture at full size. A photograph judged at 150 pixels is a photograph nobody checked. */
.upload-lightbox {
    position: fixed;
    inset: 0;
    z-index: 1080;
    background: rgba(0, 0, 0, .6);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2rem;
}

.upload-lightbox img {
    max-width: 100%;
    max-height: 100%;
    background: #fff;
    border-radius: var(--bs-border-radius, .375rem);
    padding: 1rem;
}

.upload-lightbox-close {
    position: absolute;
    top: 1rem;
    right: 1rem;
    width: 2.25rem;
    height: 2.25rem;
    border: 0;
    border-radius: 50%;
    background: #fff;
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
}

/* ---------------------------------------------------------------------------
   Analysis charts.

   The SVG is drawn on the server (see AnalysisChartRenderer) and inherits the
   page's text colour for its axes, gridlines and labels — so it follows the
   theme without this stylesheet or that renderer knowing which theme is on.
   All that is needed here is the box it sits in and the legend beside it.
   --------------------------------------------------------------------------- */
.analysis-chart {
    display: block;
    max-width: 100%;
    /* The axis numbers and category names are drawn with currentColor, so this
       one declaration colours the whole chart's furniture. */
    color: var(--bs-body-color, #566a7f);
    overflow: visible;
}

/* A ring is a fixed square rather than full width: stretched to the card it
   would become an enormous circle with a thin line of legend beside it. */
.analysis-donut {
    margin: 0 auto;
}

.analysis-legend {
    color: var(--bs-body-color, #566a7f);
}

.analysis-swatch {
    display: inline-block;
    width: .75rem;
    height: .75rem;
    border-radius: .2rem;
    flex: 0 0 auto;
}

/* ---------------------------------------------------------------------------
   Pending Student Data.

   THE SUMMARY STRIP. Three figures used to sit in one card as a row of plain
   text, which read as a sentence rather than as a summary — the eye had nothing
   to land on and the two DIFFERENT kinds of number (how many children, how much
   is missing) looked alike.

   A grid rather than flex, so the tiles stay equal whatever the numbers are:
   flex sizes a tile to its content, which gave "9,664" a wide box and "0" a
   narrow one and made the strip look broken.
   --------------------------------------------------------------------------- */
.stat-strip {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--sp-3, 1rem);

    /* The founder's note: the gap under the filters was doing nothing. This is
       the only space between the strip and the first group of fields. */
    margin-bottom: var(--sp-3, 1rem);
}

@media (max-width: 991.98px) { .stat-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 575.98px) { .stat-strip { grid-template-columns: 1fr; } }

.stat-tile {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .875rem 1rem;
    border-radius: .5rem;
    background: var(--bs-card-bg, #fff);
    border: 1px solid var(--bs-border-color, #e7e7f1);

    /* The accent is a stripe down the leading edge rather than a coloured card:
       four saturated blocks in a row shout at somebody who came to read a table,
       and the stripe still tells them apart from across the desk. */
    border-inline-start: 3px solid var(--tile-accent, #696cff);
}

.stat-tile-icon {
    display: grid;
    place-items: center;
    inline-size: 2.5rem;
    block-size: 2.5rem;
    border-radius: .5rem;
    font-size: 1.35rem;
    flex: 0 0 auto;
    color: var(--tile-accent, #696cff);

    /* The same hue as the stripe at a tenth of its strength — one colour per
       tile, stated once. */
    background: color-mix(in srgb, var(--tile-accent, #696cff) 12%, transparent);
}

.stat-tile-label {
    font-size: .8125rem;
    color: var(--bs-secondary-color, #8592a3);
    line-height: 1.2;
}

.stat-tile-value {
    font-size: 1.5rem;
    font-weight: 600;
    line-height: 1.25;

    /* Digits line up down the strip and between reloads — without this the
       numbers shift sideways as they change, which reads as the page twitching. */
    font-variant-numeric: tabular-nums;
}

.stat-tile-unit {
    font-size: .8125rem;
    font-weight: 400;
    color: var(--bs-secondary-color, #8592a3);
    margin-inline-start: .25rem;
}

.stat-tile-blue  { --tile-accent: #696cff; }
.stat-tile-green { --tile-accent: #71dd37; }
.stat-tile-teal  { --tile-accent: #03c3ec; }
.stat-tile-amber { --tile-accent: #ffab00; }

/* The cards below the strip: one rhythm, and a smaller one than the default
   card margin — the founder's "too much space between the two portions". */
.pending-card { margin-bottom: var(--sp-3, 1rem); }
.pending-card:last-child { margin-bottom: 0; }

/* "4 / 5" — the count first, because a percentage hides the size of the job:
   81% of ten thousand children is eighteen hundred forms to chase. */
.pending-ratio {
    font-variant-numeric: tabular-nums;
    font-size: .8125rem;
    white-space: nowrap;
    min-inline-size: 6.5rem;
    text-align: end;
}

.pending-ratio-of { color: var(--bs-secondary-color, #8592a3); }

.pending-pct {
    font-size: .75rem;
    color: var(--bs-secondary-color, #8592a3);
    font-variant-numeric: tabular-nums;
    min-inline-size: 2.5rem;
    text-align: end;
}

.pending-bar {
    flex: 1 1 auto;
    block-size: .5rem;
    border-radius: 1rem;
    overflow: hidden;
    background: color-mix(in srgb, var(--bs-secondary-color, #8592a3) 18%, transparent);
}

/*
    THE FILL, WHICH GROWS INTO PLACE.

    The founder asked for "some motion in that". It runs ONCE on load, from zero
    to the value, so the bar is read as a quantity arriving rather than as a
    thing that is animating; a bar that pulses forever is a bar nobody can read a
    number off. --fill carries the percentage from the markup, so the animation
    and the width are the same one number.
*/
.pending-bar-fill {
    display: block;
    block-size: 100%;
    inline-size: var(--fill, 0%);
    border-radius: inherit;
    background: var(--bar-colour, #696cff);
    animation: pending-grow .7s cubic-bezier(.22, 1, .36, 1) both;
}

@keyframes pending-grow {
    from { inline-size: 0; }
    to   { inline-size: var(--fill, 0%); }
}

.pending-bar-fill.is-thin { --bar-colour: #ff3e1d; }
.pending-bar-fill.is-part { --bar-colour: #ffab00; }
.pending-bar-fill.is-good { --bar-colour: #03c3ec; }

/* A finished field gets a sheen that travels once across the full bar — the one
   place a flourish earns its keep, because "this one is done" is the thing
   somebody scanning the column is looking for. */
.pending-bar-fill.is-done {
    --bar-colour: #71dd37;
    background-image: linear-gradient(90deg,
        transparent 0%,
        rgba(255, 255, 255, .55) 50%,
        transparent 100%);
    background-size: 40% 100%;
    background-repeat: no-repeat;
    animation: pending-grow .7s cubic-bezier(.22, 1, .36, 1) both,
               pending-sheen 1.1s ease-out .7s 1 both;
}

@keyframes pending-sheen {
    from { background-position: -60% 0; }
    to   { background-position: 160% 0; }
}

/*
    NOTHING MOVES for a reader who has asked for that. A progress bar sliding
    into place is decoration, and decoration is exactly what this setting is
    about — the bar still shows the right width, it simply arrives at it.
*/
@media (prefers-reduced-motion: reduce) {
    .pending-bar-fill,
    .pending-bar-fill.is-done { animation: none; }
}

/*
    THE WAIT, MADE VISIBLE — a sheet of ten thousand children takes a few
    seconds, and a screen that sits there silently is one somebody presses twice.

    Named app- rather than pending- since the export designer raises the same
    overlay: it is a shared component now, markup, styling and script alike.
*/
.app-busy {
    position: fixed;
    inset: 0;
    z-index: 1080;
    display: grid;
    place-items: center;
    background: rgba(0, 0, 0, .35);
}

.app-busy[hidden] { display: none; }

.app-busy-box {
    display: flex;
    align-items: center;
    gap: .875rem;
    padding: 1rem 1.25rem;
    border-radius: .5rem;
    background: var(--bs-card-bg, #fff);
    color: var(--bs-body-color, #566a7f);
    box-shadow: 0 .5rem 1.5rem rgba(0, 0, 0, .2);
    max-inline-size: min(26rem, calc(100vw - 2rem));
}

.app-busy-text { font-size: .875rem; }

/*
    PROGRESS AND A CLOCK, for the long presses that navigate — see busy-download.js. The body stacks the
    words, the bar and the numbers; a download shows only the words, exactly as before.
*/
.app-busy-body {
    display: flex;
    flex-direction: column;
    gap: .5rem;
    min-inline-size: min(16rem, calc(100vw - 6rem));
}

.app-busy-progress .progress { block-size: .5rem; }

.app-busy-bar { transition: inline-size .4s ease, width .4s ease; }

@media (prefers-reduced-motion: reduce) {
    .app-busy-bar { transition: none; }
}

.app-busy-meta {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    font-size: .75rem;
    color: var(--bs-secondary-color, #8592a3);
    font-variant-numeric: tabular-nums;
}

.app-busy-meta[hidden] { display: none; }

/* ---------------------------------------------------------------------------
   Student data export — the report designer.

   THE FOUNDER'S NOTE WAS "space padding is also more", and it was: ninety
   checkboxes set in Bootstrap's default row gutters ran the page to three
   screens of mostly air. The grid below is tighter and the groups closer, so
   the whole design fits where half of it used to.
   --------------------------------------------------------------------------- */
.export-card { margin-bottom: var(--sp-3, 1rem); }

/* THREE OR FOUR TO A ROW, and it follows the width rather than a fixed count:
   at 1400px that is four, on a laptop three, on a tablet two. A hard col-md-3
   gave four narrow columns on a wide screen and truncated the longer labels. */
.export-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: .125rem 1rem;
}

.export-section { margin-bottom: 1rem; }
.export-section:last-child { margin-bottom: 0; }

/* The group's own name and its select-all, on one line and set apart from the
   boxes under it — a heading that looked like another checkbox was the reason
   the list read as one long undifferentiated column. */
.export-section-head {
    display: flex;
    align-items: center;
    gap: .375rem;
    margin-bottom: .375rem;
    padding-bottom: .25rem;
    border-bottom: 1px solid var(--bs-border-color, #e7e7f1);
    font-size: .8125rem;
    text-transform: uppercase;
    letter-spacing: .03em;
    color: var(--bs-secondary-color, #8592a3);
    cursor: pointer;
}

.export-section .form-check { margin-bottom: 0; padding-block: .125rem; }

/* How many columns are chosen, beside the card's heading — see the note in the
   script for why a count earns its place on a list of ninety boxes. */
.export-count {
    font-size: .75rem;
    font-weight: 500;
    color: var(--bs-primary, #696cff);
    background: color-mix(in srgb, var(--bs-primary, #696cff) 12%, transparent);
    border-radius: 1rem;
    padding: .125rem .5rem;
    margin-inline-start: .375rem;
}

.export-count.is-empty {
    color: var(--bs-secondary-color, #8592a3);
    background: color-mix(in srgb, var(--bs-secondary-color, #8592a3) 12%, transparent);
}

/* A section that cannot be chosen says so in words underneath; the control
   itself simply stops taking answers. */
.export-designer select:disabled { background-color: var(--bs-secondary-bg, #f5f5f9); }

/*
    THE CARD HEADS ON THE DESIGNER, cut down to a heading.

    They were 70px tall for one line of 15px text — 24px of padding on every side
    — so four cards spent nearly a screen's worth of height saying four short
    words. On a page whose job is ninety checkboxes and three dropdowns, that is
    the difference between seeing the design and scrolling to find it.

    Scoped to this screen rather than changed globally: the same padding is on
    every card in the product, and quietly restyling all of them from here is how
    one screen's tidy-up becomes another screen's regression.
*/
.export-designer .card-header,
.bulk-photos .card-header {
    padding: .75rem 1.25rem;
    min-height: 0;
    font-size: .9375rem;
}

.export-designer .card-body,
.bulk-photos .card-body { padding: 1.25rem; }

/* Both screens are groups of choices rather than one sheet, so the cards keep an even rhythm down
   the page — see the note at the top of the export view. */
.export-designer > .card,
.bulk-photos > .card { margin-bottom: var(--sp-3, 1rem); }

/* The heading and its select-all stay on one line even when the count grows. */
.export-designer .card-header.d-flex { gap: .75rem; }

/* ---------------------------------------------------------------------------
   A SETTINGS GRID — a label and its control, several to a row.

   Written for Required fields, which lists fifty-nine of them. One field per
   full-width table row gave each a whole page-width to hold a label and a
   twelve-rem dropdown, so the screen ran to eight screenfuls that were two
   thirds empty. It is the same problem the export designer had with ninety
   checkboxes, and it gets the same answer.

   Named setting- rather than for this one screen: the next list of "a setting
   and its value" should use it rather than grow a fourth grid of its own.
   --------------------------------------------------------------------------- */
.setting-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
    gap: .875rem 1.25rem;
}

.setting-cell { min-inline-size: 0; }

.setting-label {
    display: flex;
    align-items: center;
    gap: .375rem;
    margin-bottom: .25rem;
    font-size: .8125rem;
    font-weight: 500;
    color: var(--bs-body-color, #566a7f);
}

/* The label may be longer than its column; it wraps rather than pushing the
   grid wider and putting the whole page into a sideways scroll. */
.setting-label > span:first-child { min-inline-size: 0; overflow-wrap: anywhere; }

.setting-label .badge {
    display: inline-flex;
    align-items: center;
    gap: .2rem;
    flex: none;
    font-size: .6875rem;
}

/*
   A FIXED ANSWER SITS WHERE ITS DROPDOWN WOULD.

   Written as a box the same height as the select beside it rather than a bare
   badge: a row whose control simply vanished read as a rendering fault, and the
   grid lost its alignment down the column.
*/
.setting-fixed {
    display: flex;
    align-items: center;
    min-block-size: 2.375rem;
    padding: .375rem .75rem;
    border: 1px solid transparent;
    border-radius: .375rem;
    background: var(--bs-secondary-bg, #f2f2f4);
    font-size: .8125rem;
    font-weight: 500;
    color: var(--app-required);
}

/* Read-only for somebody who may not change it — the same words, in the ordinary
   colour, because "you cannot edit this" is not the same as "this is mandatory". */
.setting-fixed.is-readonly { color: var(--bs-secondary-color, #6f6b7d); font-weight: 400; }

.setting-count {
    font-size: .75rem;
    font-weight: 500;
    color: var(--app-required);
    font-variant-numeric: tabular-nums;
}

.setting-count.is-empty { color: var(--bs-secondary-color, #a5a3ae); font-weight: 400; }

/* ---------------------------------------------------------------------------
   THE STORAGE FOLDER TREE.

   A file browser: folders on the left, what is inside on the right. Built on a
   native <details> so a folder opens with no script — this screen is read far
   more often than it is used, and a tree that needs JavaScript to expand is a
   tree that shows nothing when a file fails to load.
   --------------------------------------------------------------------------- */
.storage-tree {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* Every level but the first steps in, and carries a hairline back to its parent
   so a deep folder can still be traced to what it belongs to. */
.storage-tree .storage-tree {
    margin-inline-start: .5rem;
    padding-inline-start: .75rem;
    border-inline-start: 1px solid var(--bs-border-color, #e7e7f1);
}

.storage-folder > summary {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .3125rem .375rem;
    border-radius: .375rem;
    cursor: pointer;
    font-size: .8125rem;
    font-weight: 500;
    list-style: none;
}

.storage-folder > summary::-webkit-details-marker { display: none; }
.storage-folder > summary:hover { background: var(--bs-secondary-bg, #f2f2f4); }

.storage-folder > summary .bx { color: var(--app-primary); font-size: 1rem; }
.storage-folder[open] > summary .bx::before { content: "\eb01"; } /* bx-folder-open */

.storage-leaf-row {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .3125rem .375rem;
    border-radius: .375rem;
    font-size: .8125rem;
    color: inherit;
    text-decoration: none;
}

a.storage-leaf-row:hover { background: var(--bs-secondary-bg, #f2f2f4); }

.storage-leaf-row .bx { color: var(--bs-secondary-color, #a5a3ae); font-size: .9375rem; }

/* The chosen folder is the one whose contents fill the right-hand panel, so it
   has to be obvious which one that is — otherwise the two halves of the screen
   look unrelated. */
.storage-leaf.is-open > .storage-leaf-row {
    background: var(--app-primary);
    color: #fff;
}

.storage-leaf.is-open > .storage-leaf-row .bx,
.storage-leaf.is-open > .storage-leaf-row .storage-facts { color: rgba(255, 255, 255, .85); }

/* An empty folder is drawn but not offered — see _StorageLeaf. */
.storage-leaf.is-empty > .storage-leaf-row { color: var(--bs-secondary-color, #a5a3ae); }

.storage-name {
    flex: 1 1 auto;
    min-inline-size: 0;
    overflow-wrap: anywhere;
}

.storage-facts {
    flex: none;
    font-size: .75rem;
    font-variant-numeric: tabular-nums;
    color: var(--bs-secondary-color, #a5a3ae);
}

/* The photographs in an opened folder — a fixed grid rather than Bootstrap
   columns, so the tiles stay square whatever the panel width. */
.storage-thumbs {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr));
    gap: .5rem;
}

.storage-thumbs img {
    inline-size: 100%;
    aspect-ratio: 1 / 1;
    object-fit: cover;
    border: 1px solid var(--bs-border-color, #e7e7f1);
    border-radius: .375rem;
}

/* ---------------------------------------------------------------------------
   THE ROLE PICKER — the dialog that replaced a wall of tick boxes.

   Every row of the user list used to carry a box for every role, so twenty
   roles put five hundred controls on one screen. This holds ONE set, in a
   column that scrolls once it outgrows the dialog, so twenty roles and eighty
   both work.
   --------------------------------------------------------------------------- */
.role-picker {
    display: grid;
    gap: .125rem;
    max-block-size: 22rem;
    overflow-y: auto;
}

.role-picker-item {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .375rem .5rem;
    border-radius: .375rem;
    font-size: .8125rem;
    cursor: pointer;
}

.role-picker-item:hover { background: var(--bs-secondary-bg, #f2f2f4); }
.role-picker-item[hidden] { display: none; }
