/*--------------------------------------------------------------
# DotEdx - app.dotedx.com stylesheet
# DotTut family theme (BRAND-GUIDELINES.md). Dark-first; light via
# html[data-theme="light"]. Every color comes from a token below.
--------------------------------------------------------------*/

:root {
    --default-font: "Roboto", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --heading-font: "Ubuntu", sans-serif;
    --nav-font: "Inter", sans-serif;

    --accent-color:    #40b605;
    --btn-green:       #2f8000;
    --btn-green-hover: #287000;
    --brand-grad:      linear-gradient(135deg, #79c32f 0%, #3f9c0c 100%);
    --brand-grad-soft: linear-gradient(135deg, rgba(121,195,47,.16), rgba(63,156,12,.16));
    /* Selected-chip fill. --accent-color is a display green — white on it is 2.65:1 — and the
       selected chip names the class/term you are looking at, so it needs the darker button
       green (4.99:1 with #fff) to stay readable. */
    --chip-on-bg:      var(--btn-green);

    /* Accent-derived aliases. The DotTut console re-points these in one place (.platform-body),
       so no rule below may hardcode a green literal that is meant to follow the accent. The
       var() aliases resolve per theme, because --emerald/--link-color/--glow-green are
       theme-scoped on the same element. */
    --focus-color:       var(--emerald);
    --accent-link:       var(--link-color);
    --accent-link-hover: var(--link-hover);
    --accent-ring:       rgba(64,182,5,.18);
    --accent-border:     rgba(64,182,5,.35);
    --accent-glow:       var(--glow-green);
    --btn-grad:          linear-gradient(180deg, #368f00, var(--btn-green));
    --btn-shadow:        47,128,0;

    /* Dark-theme status hues; html[data-theme="light"] retunes all four. Each is painted as
       text on a 14%-alpha fill of itself and carries the decision (paid / overdue / at risk),
       so every value must clear 4.5:1 against that composite in its own theme. */
    --status-good: #22c55e;
    --status-warn: #f59e0b;
    --status-bad:  #f87171;
    --status-info: #38bdf8;

    --sidebar-w: 268px;
    --topbar-h: 62px;
    --radius: 16px;
    --radius-sm: 11px;

    /* Spacing scale — every gap/padding in the app comes from these, so density stays
       consistent as screens grow. The page gutter and content width are fluid: they breathe
       on a laptop and stop growing before lines get unreadably long on an ultrawide. */
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 20px;
    --space-6: 24px;
    --space-8: 32px;
    --space-10: 40px;
    --gutter: clamp(16px, 2.2vw, 34px);
    --content-max: 1680px;

    scroll-behavior: smooth;
}

:root, html[data-theme="dark"] {
    color-scheme: dark;
    --background-color: #040a01;
    --surface-color:    #0f1412;
    --surface-2:        #131a16;
    --surface-deep:     #0b0f0d;
    --sidebar-bg:       #060c03;
    --input-bg:         #121614;
    --input-border:     #1f2a25;

    --default-color: #f3f6f4;
    --heading-color: #eaf3ee;
    --text-strong:   #ffffff;
    --text-soft:     rgba(255,255,255,.74);
    /* Raised from .5/.35: these carry real content (table column headers, captions, hints),
       so they must clear 4.5:1 against --surface-color, not just look quiet. */
    --text-muted:    rgba(255,255,255,.68);
    --text-faint:    rgba(255,255,255,.56);
    --hairline:      rgba(255,255,255,.07);
    --hairline-strong: rgba(255,255,255,.12);
    --hover-bg:      rgba(255,255,255,.045);

    --emerald:    #22c55e;
    --link-color: #4ade80;
    --link-hover: #9be15d;
    --card-top:   rgba(255,255,255,.05);

    --elev-1: 0 1px 0 var(--card-top) inset, 0 12px 30px -18px rgba(0,0,0,.8);
    --elev-2: 0 1px 0 var(--card-top) inset, 0 30px 60px -30px rgba(0,0,0,.85);
    --glow-green: 0 22px 50px -26px rgba(64,182,5,.5);
    --ambient: rgba(64,182,5,.13);
    --scroll-thumb: rgba(255,255,255,.14);
    --scroll-thumb-hover: rgba(121,195,47,.55);
}

html[data-theme="light"] {
    color-scheme: light;
    --background-color: #eef3ee;
    --surface-color:    #ffffff;
    --surface-2:        #f7faf7;
    --surface-deep:     #fbfdfb;
    /* Not #ffffff: the rail has to differ from --surface-color, or the campus switcher and its
       dropdown are white cards on a white rail held apart by a 1.4:1 hairline. */
    --sidebar-bg:       #f5f9f5;
    --input-bg:         #f2f6f2;
    --input-border:     #d6e1d7;

    --default-color: #1c2a21;
    --heading-color: #0e1f14;
    --text-strong:   #0b1810;
    --text-soft:     rgba(14,31,20,.78);
    /* See the dark theme: these carry real content and must clear 4.5:1 on white. */
    --text-muted:    rgba(14,31,20,.72);
    --text-faint:    rgba(14,31,20,.6);
    --hairline:      rgba(14,31,20,.09);
    --hairline-strong: rgba(14,31,20,.15);
    --hover-bg:      rgba(14,31,20,.04);

    --emerald:    #16a34a;
    --link-color: #15803d;
    --link-hover: #166534;
    --card-top:   rgba(255,255,255,.9);

    /* The dark hues sit at ~2:1 on white and on the 14%-alpha fills, which is exactly where
       .risk pills and KPI deltas live. Retuned: 6.3:1 / 6.2:1 / 5.4:1 on their own fill. */
    --status-good: #166534;
    --status-warn: #854d0e;
    --status-bad:  #b91c1c;
    --status-info: #075985;

    --elev-1: 0 1px 0 var(--card-top) inset, 0 10px 26px -18px rgba(20,40,28,.24);
    --elev-2: 0 1px 0 var(--card-top) inset, 0 30px 56px -30px rgba(20,40,28,.28);
    --glow-green: 0 22px 46px -26px rgba(47,128,0,.3);
    --ambient: rgba(64,182,5,.1);
    --scroll-thumb: rgba(14,31,20,.2);
    --scroll-thumb-hover: rgba(63,156,12,.55);
}

* { box-sizing: border-box; margin: 0; }
html { background: var(--background-color); }
body { background: var(--background-color); color: var(--default-color); font-family: var(--default-font); font-size: 15px; line-height: 1.6; }
h1, h2, h3, h4 { font-family: var(--heading-font); color: var(--heading-color); line-height: 1.2; }
a { color: var(--accent-link); text-decoration: none; }
a:hover { color: var(--accent-link-hover); }
.icon { width: 1.15em; height: 1.15em; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; vertical-align: -0.2em; }
.num { font-variant-numeric: tabular-nums; font-family: var(--nav-font); }
:focus-visible { outline: 2px solid var(--focus-color); outline-offset: 2px; border-radius: 6px; }

/* ---------- Modern scrollbars ---------- */
* { scrollbar-width: thin; scrollbar-color: var(--scroll-thumb) transparent; }
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: 999px; border: 3px solid transparent; background-clip: padding-box; transition: background .2s ease; }
::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); background-clip: padding-box; }
::-webkit-scrollbar-corner { background: transparent; }

/* ---------- Buttons & inputs ---------- */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: .45rem; font-family: var(--nav-font); font-weight: 600; font-size: .9rem; padding: .6rem 1.05rem; border-radius: 11px; border: 1px solid transparent; cursor: pointer; transition: transform .2s ease, background .2s ease, border-color .2s ease, box-shadow .2s ease; }
.btn-sm { padding: .5rem .85rem; font-size: .85rem; }
/* A submitted button (submit-guard.js) and any genuinely disabled one read the same: dimmed,
   no hover lift, and not clickable. Opacity rather than a flat grey so the button keeps its
   role colour — a greyed-out primary button reads as "broken", not "working". */
.btn:disabled, .btn.is-submitting { opacity: .6; cursor: progress; pointer-events: none; transform: none; box-shadow: none; }
.btn-block { width: 100%; }
.btn-primary { background: var(--btn-grad); color: #fff; box-shadow: 0 12px 24px -14px rgba(var(--btn-shadow),.8), 0 1px 0 rgba(255,255,255,.2) inset; }
.btn-primary:hover { color: #fff; transform: translateY(-1px); box-shadow: 0 16px 30px -14px rgba(var(--btn-shadow),.9), 0 1px 0 rgba(255,255,255,.2) inset; }
.btn-outline { border-color: var(--hairline-strong); color: var(--default-color); background: transparent; }
.btn-outline:hover { border-color: var(--accent-color); color: var(--accent-link-hover); }
.btn-ghost { color: var(--text-muted); background: transparent; }
.btn-ghost:hover { color: var(--text-strong); background: var(--hover-bg); }

.dx-input { width: 100%; background: var(--input-bg); border: 1px solid var(--input-border); border-radius: 10px; color: var(--default-color); font-family: var(--default-font); font-size: .95rem; padding: .65rem .8rem; transition: border-color .2s ease, box-shadow .2s ease; }
/* Mouse focus gets the soft brand ring; keyboard focus additionally keeps the real outline,
   so removing the ring for aesthetics never costs keyboard users their position. */
.dx-input:focus { border-color: var(--accent-color); box-shadow: 0 0 0 3px var(--accent-ring); }
.dx-input:focus:not(:focus-visible) { outline: none; }
.dx-input::placeholder { color: var(--text-muted); }

.icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px; border-radius: 10px; border: 1px solid transparent; background: transparent; color: var(--text-soft); cursor: pointer; position: relative; transition: color .2s ease, background .2s ease; }
.icon-btn:hover { color: var(--text-strong); background: var(--hover-bg); }

.eyebrow-lbl { font-family: var(--nav-font); font-size: .74rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--text-faint); }

/* ---------- App frame ---------- */
.app { display: flex; min-height: 100vh; }

/* Three-part column: fixed header, scrolling nav, pinned footer. Only .sb-nav scrolls, so the
   campus switcher and the account controls never scroll out of reach. */
/* dvh, not vh: on mobile browsers the URL bar makes 100vh taller than the visible area, which
   pushed the footer (profile + sign-out) off-screen. */
.sidebar { width: var(--sidebar-w); flex-shrink: 0; background: var(--sidebar-bg); border-right: 1px solid var(--hairline); position: sticky; top: 0; height: 100vh; height: 100dvh; display: flex; flex-direction: column; z-index: 40; transition: width .18s cubic-bezier(.22,1,.36,1); }
.sb-header { flex: none; }
.sb-top { display: flex; align-items: center; justify-content: space-between; padding: var(--space-4) var(--space-4) var(--space-3); }
.sb-brand { display: inline-flex; align-items: center; gap: .5rem; font-family: var(--heading-font); font-weight: 700; font-size: 1.12rem; color: var(--text-strong); white-space: nowrap; }
.sb-brand svg { border-radius: 8px; flex: none; }
.sb-brand .edx { color: var(--accent-color); }
html[data-theme="light"] .sb-brand .edx { color: var(--btn-green); }
.sb-close { display: none; }

/* Nav: the only scrolling region, with a slim overlay scrollbar that only shows on hover. */
.sb-nav { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: var(--space-2) var(--space-3) var(--space-4); scrollbar-width: thin; scrollbar-color: transparent transparent; transition: scrollbar-color .2s ease; }
.sb-nav:hover, .sb-nav:focus-within { scrollbar-color: var(--scroll-thumb) transparent; }
.sb-nav::-webkit-scrollbar { width: 8px; }
.sb-nav::-webkit-scrollbar-thumb { background: transparent; border-radius: 999px; border: 2px solid transparent; background-clip: padding-box; transition: background .2s ease; }
.sb-nav:hover::-webkit-scrollbar-thumb, .sb-nav:focus-within::-webkit-scrollbar-thumb { background: var(--scroll-thumb); background-clip: padding-box; }
.sb-nav::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); background-clip: padding-box; }

.sb-group { margin-bottom: var(--space-4); }
.sb-group:last-child { margin-bottom: 0; }
.sb-gtitle { font-family: var(--nav-font); font-size: .66rem; font-weight: 600; letter-spacing: .09em; text-transform: uppercase; color: var(--text-faint); padding: 0 10px; margin-bottom: 6px; white-space: nowrap; }
.sb-link { display: flex; align-items: center; gap: 11px; padding: 8px 10px; border-radius: var(--radius-sm); color: var(--text-soft); font-family: var(--nav-font); font-size: .875rem; font-weight: 500; margin-bottom: 2px; white-space: nowrap; transition: color .16s ease, background .16s ease; }
.sb-link .icon { width: 1.1rem; height: 1.1rem; flex: none; color: var(--text-muted); transition: color .16s ease; }
.sb-link:hover { background: var(--hover-bg); color: var(--text-strong); }
.sb-link:hover .icon { color: var(--text-soft); }
.sb-link.on { background: var(--brand-grad-soft); color: var(--text-strong); box-shadow: inset 3px 0 0 var(--accent-color); }
.sb-link.on .icon { color: var(--accent-color); }
.sb-label { overflow: hidden; text-overflow: ellipsis; }

/* Footer: who you are, plus theme and sign-out. */
.sb-foot { flex: none; display: flex; align-items: center; gap: var(--space-2); padding: var(--space-3); border-top: 1px solid var(--hairline); background: var(--sidebar-bg); }
.sb-user { display: flex; align-items: center; gap: 9px; min-width: 0; flex: 1; }
.sb-user-meta { min-width: 0; }
.sb-user-meta b { display: block; font-family: var(--nav-font); font-size: .82rem; font-weight: 600; color: var(--text-strong); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sb-user-meta em { display: block; font-style: normal; font-size: .7rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sb-user-actions { display: flex; align-items: center; gap: 2px; flex: none; }
.sb-user-actions .icon-btn { width: 32px; height: 32px; }
.logout-form { display: inline-flex; margin: 0; }

/* Collapse handle: a hit target straddling the sidebar's right edge. */
.sb-collapse { position: absolute; top: 50%; right: -11px; transform: translateY(-50%); width: 22px; height: 42px; display: none; align-items: center; justify-content: center; padding: 0; border: 1px solid var(--hairline-strong); border-radius: 8px; background: var(--surface-color); color: var(--text-muted); cursor: pointer; z-index: 41; transition: color .2s ease, border-color .2s ease, background .2s ease; }
.sb-collapse:hover { color: var(--text-strong); border-color: var(--accent-color); }
.sb-collapse .icon { width: .9rem; height: .9rem; transform: rotate(90deg); transition: transform .2s ease; }
@media (min-width: 901px) { .sb-collapse { display: inline-flex; } }

/* ---------- Collapsed rail ---------- */
html.sidebar-collapsed { --sidebar-w: 76px; }
html.sidebar-collapsed .sb-brand-text,
html.sidebar-collapsed .sb-label,
html.sidebar-collapsed .sb-gtitle,
html.sidebar-collapsed .campus-name,
html.sidebar-collapsed .campus-chev,
html.sidebar-collapsed .sb-user-meta { display: none; }
html.sidebar-collapsed .sb-top { justify-content: center; padding-inline: 0; }
html.sidebar-collapsed .sb-link { justify-content: center; padding-inline: 0; gap: 0; }
html.sidebar-collapsed .sb-group { margin-bottom: var(--space-3); padding-top: var(--space-3); border-top: 1px solid var(--hairline); }
html.sidebar-collapsed .sb-group:first-child { border-top: 0; padding-top: 0; }
html.sidebar-collapsed .campus-trigger { justify-content: center; padding-inline: 0; }
html.sidebar-collapsed .campus-switch { margin-inline: var(--space-2); }
html.sidebar-collapsed .sb-foot { flex-direction: column; gap: var(--space-2); padding-inline: var(--space-2); }
html.sidebar-collapsed .sb-user { justify-content: center; }
/* Two 32px buttons in a row need 66px; the rail only offers ~59px of content width, so they
   would spill over the page. Stack them instead. */
html.sidebar-collapsed .sb-user-actions { flex-direction: column; }
html.sidebar-collapsed .sb-collapse .icon { transform: rotate(-90deg); }
/* The menu would be clipped inside a 76px rail, so float it beside the rail instead. */
html.sidebar-collapsed .campus-menu { left: calc(100% + 8px); right: auto; top: 0; width: 260px; }

.sb-scrim { display: none; }

/* ---------- Shell + topbar ---------- */
.app-shell { flex: 1; min-width: 0; display: flex; flex-direction: column; }
/* The bar stays full-bleed for its blur and border, but its contents take the same inset as
   .content: --gutter normally, plus the centring offset once the shell is wider than
   --content-max. Without it the topbar title sits up to 320px left of the page title below. */
.topbar { position: sticky; top: 0; z-index: 30; height: var(--topbar-h); display: flex; align-items: center; gap: 12px; padding-inline: max(var(--gutter), calc((100% - var(--content-max)) / 2 + var(--gutter))); background: color-mix(in srgb, var(--background-color) 82%, transparent); backdrop-filter: blur(14px) saturate(1.2); border-bottom: 1px solid var(--hairline); }
.topbar-menu { display: none; }
.topbar-title { font-size: 1.05rem; font-weight: 600; color: var(--heading-color); }
.topbar-search { flex: 1; max-width: 460px; margin: 0 auto 0 0; display: flex; align-items: center; gap: 9px; background: var(--input-bg); border: 1px solid var(--input-border); border-radius: 11px; padding: 8px 12px; color: var(--text-muted); }
.topbar-search .icon { color: var(--accent-color); }
.topbar-search input { flex: 1; background: transparent; border: none; outline: none; color: var(--default-color); font-family: var(--default-font); font-size: .9rem; }
.topbar-search input::placeholder { color: var(--text-faint); }
.topbar-search:focus-within { border-color: var(--accent-color); box-shadow: 0 0 0 3px rgba(64,182,5,.16); }
.topbar-actions { display: flex; align-items: center; gap: 6px; margin-left: auto; }
.avatar { display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px; border-radius: 9px; background: var(--brand-grad); color: #fff; font-family: var(--nav-font); font-weight: 700; font-size: .78rem; }

/* Fluid gutter, centred, capped so text lines never get absurd on ultrawide monitors. */
.content { padding: var(--space-6) var(--gutter) var(--space-10); width: 100%; max-width: var(--content-max); margin-inline: auto; }
.content > * + * { margin-top: var(--space-5); }
.content > .page-head + * { margin-top: 0; }

/* ---------- Page header ---------- */
.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; margin-bottom: var(--space-6); }
.page-title { font-size: 1.6rem; font-weight: 700; margin-top: 3px; }
.page-actions { display: flex; gap: 8px; }

/* ---------- KPI row ---------- */
/* .kpi-row and .stat-grid wrap the same .kpi card and are used interchangeably across screens,
   so they share one rule — otherwise four identical cards render 4-across on one page and 3+1
   on the next.
   The max track MUST stay flexible. auto-fit counts repetitions against the max when the max is
   definite, so a ceiling like 300px sized the row off 300 rather than the floor: the 880px
   content column of a 1200px viewport fitted only two tracks (four cards went 2x2) and left the
   rest of the row empty, because tracks stopped growing at 300px. 1fr both restores the count
   and lets the surviving tracks — auto-fit collapses the empty ones — absorb the full width.
   The floor is now the only thing that sets a wrap point, so it is chosen against the narrowest
   laptop we support 4-across: 4*200 + 3*16 = 848 fits the 880px column at 1200px, whereas 210px
   needs 888 and drops those four cards to 3+1. 200px is also about what a card needs before its
   longest label ("Placement offers") wraps beside the 30px icon. Five tracks fit from a 1064px
   column, i.e. a ~1390px viewport, so the Admissions funnel stays on one row.
   min() keeps the floor from exceeding a narrow container and forcing overflow. */
.kpi-row, .stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr)); gap: var(--space-4); margin-bottom: var(--space-5); }
.kpi { background: var(--surface-color); border: 1px solid var(--hairline); border-radius: var(--radius); padding: 15px 16px; box-shadow: var(--elev-1); transition: transform .2s ease, border-color .2s ease, box-shadow .2s ease; }
.kpi:hover { transform: translateY(-3px); border-color: var(--accent-border); box-shadow: var(--elev-2), var(--accent-glow); }
.kpi-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.kpi-ic { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; border-radius: 9px; background: var(--brand-grad-soft); color: var(--accent-color); }
.kpi-ic.warn { background: rgba(245,158,11,.14); color: var(--status-warn); }
.kpi-ic .icon { width: 1rem; height: 1rem; }
.kpi-head em { font-style: normal; font-family: var(--nav-font); font-size: .72rem; letter-spacing: .03em; text-transform: uppercase; color: var(--text-faint); }
.kpi b { font-family: var(--nav-font); font-size: 1.7rem; font-weight: 700; color: var(--text-strong); font-variant-numeric: tabular-nums; }
.kpi u { display: block; text-decoration: none; font-size: .76rem; margin-top: 2px; }
.kpi .up { color: var(--status-good); }
.kpi .warn { color: var(--status-warn); }

/* ---------- Panels ---------- */
.grid-2col { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(320px, 1fr); gap: var(--space-5); align-items: start; }
/* Several call sites render the aside only for Admin/Staff or only once there is data. Without
   this the surviving panel keeps the 1.6fr track and leaves the whole aside column empty. */
.grid-2col > :only-child { grid-column: 1 / -1; }
/* Two peer forms, not table + aside: when the FIRST child is a form panel the 1.6fr/1fr ratio
   is the wrong shape — the 560px .form-panel cap leaves a ~450px hole between the two cards.
   Give those two screens equal, content-sized tracks instead. */
.grid-2col:has(> .form-panel:first-child) { grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 560px)); }
@media (max-width: 1080px) { .grid-2col, .grid-2col:has(> .form-panel:first-child) { grid-template-columns: minmax(0, 1fr); } }
.panel { background: var(--surface-color); border: 1px solid var(--hairline); border-radius: var(--radius); padding: var(--space-5) var(--space-6); box-shadow: var(--elev-1); }
.panel + .panel { margin-top: var(--space-5); }
.panel-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-4); flex-wrap: wrap; }
.panel-head h3 { font-size: 1.02rem; font-weight: 600; letter-spacing: -.01em; }
/* Tables sit flush to the panel edge so wide data uses the full card width. */
.panel > .table-scroll { margin-inline: calc(var(--space-6) * -1); }
.panel > .table-scroll .dx-table th:first-child,
.panel > .table-scroll .dx-table td:first-child { padding-left: var(--space-6); }
.panel > .table-scroll .dx-table th:last-child,
.panel > .table-scroll .dx-table td:last-child { padding-right: var(--space-6); }
.panel-link { font-family: var(--nav-font); font-size: .8rem; font-weight: 600; color: var(--accent-link); }

/* The SVG declares preserveAspectRatio="none", i.e. it expects CSS to pin its height. With
   height:auto the browser used the 640:220 viewBox ratio instead, so one trend line grew from
   ~100px on a phone to ~540px on a 1920px monitor. Bound it, and keep strokes uniform once the
   now-live non-uniform scale kicks in. */
.area-chart { width: 100%; height: clamp(150px, 14vw, 240px); color: var(--text-faint); display: block; }
.area-chart line, .area-chart path { vector-effect: non-scaling-stroke; }
.chart-cap { display: flex; justify-content: space-between; font-family: var(--nav-font); font-size: .72rem; color: var(--text-faint); margin-top: 6px; }

/* Transparent border, not none: .btn-reject carries a 1px border and the two sit adjacent in
   the same cell, so without it the reject button is 2px taller than the approve button. */
.btn-approve { font-family: var(--nav-font); font-size: .78rem; font-weight: 600; color: #fff; background: var(--btn-green); border: 1px solid transparent; border-radius: 8px; padding: 6px 12px; cursor: pointer; transition: background .2s ease; }
.btn-approve:hover { background: var(--btn-green-hover); }
.btn-approve.sm, .btn-reject.sm { padding: 4px 10px; font-size: .74rem; }
.btn-reject { font-family: var(--nav-font); font-size: .78rem; font-weight: 600; color: var(--status-bad); background: transparent; border: 1px solid rgba(239,68,68,.35); border-radius: 8px; padding: 6px 12px; cursor: pointer; transition: background .2s ease; }
.btn-reject:hover { background: rgba(239,68,68,.1); }


/* Table. max-height gives the sticky <th> a scroll container to stick against — without a
   bounded height, position:sticky has nothing to stick to and headers scroll away with the
   page. Keyboard users can reach the scroll area because it is focusable. */
.table-scroll { overflow: auto; max-height: min(72vh, 720px); }
.table-scroll:focus-visible { outline: 2px solid var(--focus-color); outline-offset: -2px; }
/* Short tables should not get a scrollbar or a sticky header they do not need. */
.table-scroll.compact { max-height: none; overflow-x: auto; overflow-y: visible; }
.dx-table { width: 100%; border-collapse: collapse; font-size: .875rem; }
.dx-table th { text-align: left; font-family: var(--nav-font); font-size: .7rem; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--text-faint); padding: 10px 14px; border-bottom: 1px solid var(--hairline); white-space: nowrap; position: sticky; top: 0; background: var(--surface-color); z-index: 1; }
.dx-table td { padding: 12px 14px; border-bottom: 1px solid var(--hairline); color: var(--text-soft); }
/* Data-entry tables mark their identifying first cell as <th scope="row"> so a screen reader
   names the pupil/invoice with every control in the row. Visually it must stay an ordinary
   cell — reset everything the sticky column-header rule above applies. */
.dx-table tbody th { padding: 12px 14px; border-bottom: 1px solid var(--hairline); color: var(--text-soft); font-family: inherit; font-size: inherit; font-weight: inherit; letter-spacing: normal; text-transform: none; white-space: normal; position: static; background: none; z-index: auto; }
.dx-table tbody th b { color: var(--text-strong); }
.dx-table tr:last-child td, .dx-table tbody tr:last-child th { border-bottom: 0; }
.dx-table tbody tr { transition: background .15s ease; }
.dx-table tbody tr:hover { background: var(--hover-bg); }
/* A row the API refused is tinted so it can be found in a long sheet. It has to be a class, not
   an inline style: an inline background outranks the hover rule above, and the rejected rows are
   exactly the ones the user is about to point at and edit. */
.dx-table tbody tr.row-rejected { background: rgba(239,68,68,.08); }
.dx-table tbody tr.row-rejected:hover { background: rgba(239,68,68,.15); }
.dx-table td b { color: var(--text-strong); }
.dx-table td.num { font-variant-numeric: tabular-nums; }
.risk { font-family: var(--nav-font); font-size: .68rem; font-weight: 700; border-radius: 6px; padding: 2px 7px; margin-right: 4px; }
.risk.high { color: var(--status-bad); background: rgba(239,68,68,.14); }
.risk.med { color: var(--status-warn); background: rgba(245,158,11,.14); }

/* Live-data notice + data-source tags */
.notice { display: flex; flex-direction: column; gap: 3px; background: var(--brand-grad-soft); border: 1px solid rgba(245,158,11,.3); border-left: 3px solid var(--status-warn); border-radius: 12px; padding: 12px 15px; margin-bottom: 16px; }
.notice b { color: var(--text-strong); font-family: var(--nav-font); font-size: .9rem; }
.notice p { color: var(--text-soft); font-size: .85rem; }
.notice code { font-family: var(--nav-font); font-size: .8rem; color: var(--accent-link); background: var(--input-bg); border: 1px solid var(--hairline); border-radius: 5px; padding: 1px 6px; }
.tag-live, .tag-sample { font-family: var(--nav-font); font-size: .6rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; border-radius: 5px; padding: 2px 6px; }
.tag-live { color: var(--accent-color); background: var(--brand-grad-soft); border: 1px solid rgba(64,182,5,.3); }
.tag-sample { color: var(--text-faint); border: 1px solid var(--hairline-strong); }
.kpi-head { position: relative; }
.kpi .kpi-tag { position: absolute; top: 0; right: 0; }
.section-mini { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--hairline); display: flex; flex-direction: column; gap: 7px; }
.section-mini .row { display: grid; grid-template-columns: 54px 1fr 46px; align-items: center; gap: 10px; font-size: .8rem; }
.section-mini .row b { font-family: var(--nav-font); color: var(--text-soft); font-weight: 600; }
.section-mini .row .bar { height: 7px; border-radius: 99px; background: var(--input-bg); overflow: hidden; }
.section-mini .row .bar i { display: block; height: 100%; background: var(--brand-grad); }
.section-mini .row .pct { text-align: right; font-variant-numeric: tabular-nums; color: var(--text-muted); }

/* Empty module state */
.empty-state { text-align: center; padding: 54px 24px; }
.empty-mark { display: inline-flex; align-items: center; justify-content: center; width: 60px; height: 60px; border-radius: 17px; background: var(--brand-grad); color: #fff; margin-bottom: 14px; box-shadow: var(--accent-glow); }
.empty-mark .icon { width: 1.8rem; height: 1.8rem; }
.empty-state h3 { font-size: 1.25rem; margin-bottom: 8px; }
.empty-state p { color: var(--text-soft); max-width: 48ch; margin: 0 auto 18px; }

/* ---------- Auth ---------- */
.auth-body { min-height: 100vh; position: relative; }
.auth-body::before { content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none; background: radial-gradient(50% 40% at 80% 0%, var(--ambient), transparent 60%), radial-gradient(46% 40% at 5% 100%, var(--ambient), transparent 55%); }
.auth-wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
.auth-card { width: 100%; max-width: 420px; background: var(--surface-color); border: 1px solid var(--hairline-strong); border-radius: 22px; padding: 34px 30px; box-shadow: var(--elev-2), var(--glow-green); }
.auth-brand { display: inline-flex; align-items: center; gap: .55rem; font-family: var(--heading-font); font-weight: 700; font-size: 1.35rem; color: var(--text-strong); margin-bottom: 20px; }
.auth-brand svg { border-radius: 10px; }
.auth-brand .edx { color: var(--accent-color); }
html[data-theme="light"] .auth-brand .edx { color: var(--btn-green); }
.auth-card h1 { font-size: 1.5rem; font-weight: 700; }
.auth-sub { color: var(--text-muted); margin-top: 4px; margin-bottom: 22px; font-size: .92rem; }
.auth-form { display: flex; flex-direction: column; gap: 14px; }
.auth-form label { display: flex; flex-direction: column; gap: .35rem; font-family: var(--nav-font); font-size: .84rem; color: var(--text-soft); }
/* .checkbox is not auth-only — Office/Communication's "Pin to the top" uses it. */
.checkbox { flex-direction: row !important; align-items: center; gap: .45rem !important; color: var(--text-muted); cursor: pointer; }
.checkbox input { accent-color: var(--accent-color); }
.auth-form .btn { margin-top: 4px; }
.auth-alt { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--hairline); font-size: .82rem; color: var(--text-muted); }
.auth-note { margin-top: 8px; font-size: .74rem; color: var(--text-faint); }

/* ---------- Sidebar drawer (mobile) ---------- */
@media (max-width: 900px) {
    /* The rail never applies on mobile — the drawer is always full width when open. */
    html.sidebar-collapsed { --sidebar-w: 268px; }
    html.sidebar-collapsed .sb-brand-text,
    html.sidebar-collapsed .sb-label,
    html.sidebar-collapsed .sb-gtitle,
    html.sidebar-collapsed .campus-name,
    html.sidebar-collapsed .campus-chev,
    html.sidebar-collapsed .sb-user-meta { display: revert; }
    html.sidebar-collapsed .sb-link,
    html.sidebar-collapsed .campus-trigger,
    html.sidebar-collapsed .sb-top { justify-content: flex-start; padding-inline: 10px; }
    html.sidebar-collapsed .sb-foot { flex-direction: row; }
    html.sidebar-collapsed .campus-menu { left: 0; right: 0; top: calc(100% + 6px); width: auto; }

    /* A closed drawer must leave the tab order and the a11y tree, not just slide off-screen —
       visibility:hidden does both, and is re-enabled on the open state below. */
    .sidebar { position: fixed; left: 0; top: 0; width: 288px; transform: translateX(-100%); visibility: hidden; transition: transform .28s cubic-bezier(.22,1,.36,1), visibility .28s; box-shadow: var(--elev-2); }
    body.sidebar-open .sidebar { visibility: visible; }
    .sb-close { display: inline-flex; }
    body.sidebar-open { overflow: hidden; }
    body.sidebar-open .sidebar { transform: translateX(0); }
    body.sidebar-open .sb-scrim { display: block; position: fixed; inset: 0; z-index: 35; background: rgba(0,0,0,.55); animation: scrim-in .2s ease both; }
    .topbar-menu { display: inline-flex; }
    .topbar-title { display: block; font-size: .98rem; }
    /* Wrap the page filter onto its own row rather than hiding it. display:none removed the
       only row-narrowing control on exactly the widths where a 6-column table is hardest to
       scan, and it also stranded any rows the filter had already hidden after a rotate. */
    .topbar { height: auto; min-height: var(--topbar-h); flex-wrap: wrap; row-gap: var(--space-2); padding-block: var(--space-2); }
    .topbar-search { order: 3; flex: 1 0 100%; max-width: none; margin: 0; }
}
@keyframes scrim-in { from { opacity: 0; } to { opacity: 1; } }
@media (max-width: 620px) {
    .topbar-search kbd { display: none; }
    .user-meta { display: none; }
    .content { padding: 16px; }
}

@media (prefers-reduced-motion: reduce) {
    * { transition: none !important; animation: none !important; }
    :root { scroll-behavior: auto; }
}

/*--------------------------------------------------------------
# htmx — navigation without full page reloads
--------------------------------------------------------------*/
/* Top progress bar: shows on any in-flight htmx request. */
#nav-progress { position: fixed; inset: 0 auto auto 0; height: 2px; width: 100%; z-index: 100; background: transparent; pointer-events: none; opacity: 0; transition: opacity .15s ease; }
#nav-progress::after { content: ""; display: block; height: 100%; width: 40%; background: var(--brand-grad); border-radius: 0 2px 2px 0; transform: translateX(-100%); }
body.htmx-loading #nav-progress { opacity: 1; }
body.htmx-loading #nav-progress::after { animation: nav-slide 1s ease-in-out infinite; }
@keyframes nav-slide { 0% { transform: translateX(-100%); } 60% { transform: translateX(180%); } 100% { transform: translateX(260%); } }

/* Swapped content fades in so a partial update reads as a page change.
   Driven by app.js, not htmx's own classes: htmx-added lands on each inserted CHILD (never on
   the target) and both it and htmx-swapping are stripped during settle, which truncated the
   animation and left it invisible. */
#content-region.swap-in { animation: content-in .18s ease both; }
@keyframes content-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

/* app.js freezes #content-region for the life of a boosted request so a second click cannot post
   the same form twice. `inert` is invisible, so the cursor has to carry the state — but it cannot
   be hung off the region itself: an inert subtree is out of hit testing, so a pointer over the
   frozen content lands on .app-shell behind it and the region's own cursor is never the used one.
   Put it on that shell, keyed off the .htmx-loading class app.js sets on <body> in the same call
   that goes inert. The topbar stays interactive through the request, so it keeps normal cursors.
   Do NOT dim the region: it fights the #content-region.swap-in animation above. */
body.htmx-loading .app-shell { cursor: progress; }
body.htmx-loading .topbar { cursor: auto; }

/*--------------------------------------------------------------
# Campus switcher
--------------------------------------------------------------*/
.campus-switch { position: relative; margin: 0 var(--space-3) var(--space-3); }
.campus-trigger { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 9px; background: var(--surface-color); border: 1px solid var(--hairline); border-radius: 12px; cursor: pointer; text-align: left; transition: border-color .2s ease, background .2s ease; }
.campus-trigger:hover { border-color: var(--hairline-strong); background: var(--hover-bg); }
.campus-trigger[aria-expanded="true"] { border-color: var(--accent-color); }
.campus-mark { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; flex: none; border-radius: 9px; background: var(--brand-grad); color: #fff; font-family: var(--nav-font); font-weight: 700; font-size: .68rem; letter-spacing: .02em; }
.campus-mark.sm { width: 24px; height: 24px; font-size: .6rem; border-radius: 7px; }
.campus-name { flex: 1; min-width: 0; }
.campus-name b { display: block; font-family: var(--nav-font); font-size: .84rem; font-weight: 600; color: var(--text-strong); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.campus-name em { display: block; font-style: normal; font-size: .7rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.campus-chev { color: var(--text-faint); flex: none; width: 1rem; height: 1rem; }

/* Dropdown: searchable list of campuses. [hidden] drives visibility so it is correct for
   assistive tech, not just visually gone. */
/* The 1px ring is not decoration: --elev-2 has a -30px spread and a +30px offset, so it paints
   nothing along the popover's top and side edges and the boundary would rest on the border
   alone (1.4:1 in the light theme). */
.campus-menu { position: absolute; left: 0; right: 0; top: calc(100% + 6px); z-index: 60; background: var(--surface-color); border: 1px solid var(--hairline-strong); border-radius: 14px; box-shadow: 0 0 0 1px var(--hairline-strong), var(--elev-2); padding: 8px; }
.campus-menu[hidden] { display: none; }
.campus-menu-title { font-family: var(--nav-font); font-size: .64rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--text-faint); padding: 4px 8px 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.campus-search { width: 100%; margin-bottom: 6px; background: var(--input-bg); border: 1px solid var(--input-border); border-radius: 9px; padding: 7px 10px; font-size: .85rem; color: var(--default-color); font-family: var(--default-font); }
.campus-search:focus { outline: none; border-color: var(--accent-color); box-shadow: 0 0 0 3px rgba(64,182,5,.16); }
/* The menu itself must not scroll (it would take the pinned title and search input with it), so
   the list is the only scroller. A flat 280px cap outruns a landscape phone: the menu opens
   ~112px down and carries ~76px of chrome, so a 7-campus list runs off-screen without ever
   getting a scrollbar. dvh, not vh — see .sidebar. */
.campus-list { max-height: 280px; max-height: min(280px, calc(100dvh - 200px)); overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
.campus-form { margin: 0; }
.campus-form[hidden] { display: none; }
.campus-option { display: flex; align-items: center; gap: 9px; width: 100%; padding: 7px 8px; border: 0; border-radius: 9px; background: transparent; cursor: pointer; text-align: left; font-family: var(--nav-font); font-size: .85rem; color: var(--text-soft); }
.campus-option:hover { background: var(--hover-bg); color: var(--text-strong); }
.campus-option.on { background: var(--brand-grad-soft); color: var(--text-strong); }
.campus-option-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.campus-option .tag { margin-left: auto; flex: none; font-size: .62rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--text-faint); }
.campus-tick { flex: none; width: .9rem; height: .9rem; color: var(--accent-color); }
.campus-empty { padding: 10px; font-size: .82rem; color: var(--text-muted); text-align: center; }
.campus-empty[hidden] { display: none; }

/* ---------- Utilities: screen-reader text and the error toast ---------- */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* Skip link: without it, keyboard users tab through ~15 nav items before reaching content. */
.skip-link { position: absolute; left: 50%; top: 0; transform: translate(-50%, -110%); z-index: 200; background: var(--surface-color); border: 1px solid var(--accent-color); border-radius: 0 0 10px 10px; padding: 10px 18px; font-family: var(--nav-font); font-size: .85rem; font-weight: 600; color: var(--text-strong); transition: transform .15s ease; }
.skip-link:focus { transform: translate(-50%, 0); color: var(--text-strong); }
.toast { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 120; max-width: min(92vw, 460px); background: var(--surface-color); border: 1px solid rgba(239,68,68,.4); border-left: 3px solid var(--status-bad); border-radius: 12px; box-shadow: var(--elev-2); padding: 12px 16px; font-size: .88rem; color: var(--text-strong); animation: toast-in .2s ease both; }
.toast[hidden] { display: none; }
@keyframes toast-in { from { opacity: 0; transform: translate(-50%, 8px); } to { opacity: 1; transform: translate(-50%, 0); } }
.filter-empty { color: var(--text-muted); font-size: .86rem; }
/* Honest note when a list is capped, so a truncated table never reads as the full set. */
.list-note { font-family: var(--nav-font); font-size: .74rem; color: var(--text-faint); }
/* Shared muted paragraph — replaces the inline style="color:var(--text-muted);font-size:.9rem"
   that had been copy-pasted across a dozen views. */
.muted-note { color: var(--text-muted); font-size: .89rem; }

/* Breadcrumb trail in a page-head eyebrow. */
.crumb { color: var(--text-muted); }
.crumb:hover { color: var(--accent-link-hover); }

/* Inline role editor in the users table. */
.role-editor { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; margin: 0; }
.role-picks { display: flex; flex-wrap: wrap; gap: 4px 10px; }
.role-pick { display: inline-flex; align-items: center; gap: 5px; font-family: var(--nav-font); font-size: .76rem; color: var(--text-soft); cursor: pointer; white-space: nowrap; }
.role-pick input { accent-color: var(--accent-color); margin: 0; }
.role-pick input:disabled + span { color: var(--text-faint); cursor: not-allowed; }

/*--------------------------------------------------------------
# DotTut platform console
--------------------------------------------------------------*/
/* Re-pointing the three brand tokens was not enough: focus rings, the primary button, the input
   halo, the KPI hover and the role chips all reached past them to green literals, so the console
   mixed both brands on one screen. Everything accent-derived is re-pointed here. */
.platform-body {
    --accent-color: #6366f1;
    --brand-grad: linear-gradient(135deg, #818cf8 0%, #4f46e5 100%);
    --brand-grad-soft: linear-gradient(135deg, rgba(129,140,248,.16), rgba(79,70,229,.16));
    --chip-on-bg: #4f46e5;
    --focus-color: #818cf8;
    --accent-link: #a5b4fc;
    --accent-link-hover: #c7d2fe;
    --accent-ring: rgba(99,102,241,.22);
    --accent-border: rgba(99,102,241,.4);
    --accent-glow: 0 22px 50px -26px rgba(99,102,241,.5);
    --btn-grad: linear-gradient(180deg, #6366f1, #4f46e5);
    --btn-shadow: 79,70,229;
}
/* The indigo above is tuned for dark surfaces; #a5b4fc on white is ~2:1. */
html[data-theme="light"] .platform-body { --focus-color: #4f46e5; --accent-link: #4338ca; --accent-link-hover: #3730a3; }
.platform-badge { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; background: var(--brand-grad-soft); border: 1px solid rgba(99,102,241,.35); color: #a5b4fc; font-family: var(--nav-font); font-size: .68rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
html[data-theme="light"] .platform-badge { color: #4338ca; }
.inst-card { display: block; background: var(--surface-color); border: 1px solid var(--hairline); border-radius: var(--radius); padding: var(--space-5); box-shadow: var(--elev-1); transition: border-color .2s ease, transform .2s ease; }
.inst-card:hover { border-color: rgba(99,102,241,.4); transform: translateY(-2px); }
.inst-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: var(--space-4); }
.inst-head { display: flex; align-items: center; gap: 11px; margin-bottom: var(--space-4); }
.inst-meta { display: flex; flex-wrap: wrap; gap: var(--space-4); font-size: .8rem; color: var(--text-muted); }
.inst-meta b { display: block; font-family: var(--nav-font); font-size: 1.05rem; color: var(--text-strong); font-variant-numeric: tabular-nums; }
.sb-plane { padding: 0 var(--space-4) var(--space-3); }
html.sidebar-collapsed .sb-plane { display: none; }
.viewas-panel { border-color: rgba(245,158,11,.4); }

/* The read-only "view as" banner. It rides above the tenant shell for the whole session, so it
   has to be legible on the tenant's green chrome and impossible to mistake for part of it —
   hence amber, the colour nothing else in the app claims, and a fixed strip rather than a
   dismissible toast. It must never be conditionally hidden: it is the only thing telling a
   support engineer that the record on screen belongs to somebody else's school. */
.viewas-bar {
    position: sticky; top: 0; z-index: 60;
    display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-4);
    padding: 9px var(--space-5);
    background: linear-gradient(90deg, rgba(245,158,11,.22), rgba(245,158,11,.10));
    border-bottom: 2px solid var(--status-warn);
    color: var(--text-strong);
}
.viewas-mark {
    display: inline-flex; align-items: center; gap: 6px; flex: none;
    padding: 3px 10px; border-radius: 999px;
    background: var(--status-warn); color: #1b1300;
    font-family: var(--nav-font); font-size: .68rem; font-weight: 700;
    letter-spacing: .06em; text-transform: uppercase;
}
.viewas-text { flex: 1 1 260px; min-width: 0; font-size: .84rem; line-height: 1.45; }
.viewas-sub { color: var(--text-soft); }
.viewas-sub::before { content: " · "; }
.viewas-exit { flex: none; margin: 0; }
.viewas-exit .btn { border-color: var(--status-warn); }

/*--------------------------------------------------------------
# Responsive polish
--------------------------------------------------------------*/
@media (max-width: 900px) {
    .content { padding-top: var(--space-4); }
    .page-head { align-items: flex-start; flex-direction: column; }
    .page-actions { width: 100%; }
}
@media (min-width: 1600px) {
    /* Extra breathing room once there is plenty of width to spare. */
    .content { padding-top: var(--space-8); }
}

/*--------------------------------------------------------------
# Roster + attendance (SIS screens)
--------------------------------------------------------------*/
.notice.ok { background: var(--brand-grad-soft); border-color: rgba(64,182,5,.3); border-left-color: var(--accent-color); }
.notice.err { background: rgba(239,68,68,.08); border-color: rgba(239,68,68,.3); border-left-color: var(--status-bad); }

/* Section filter chips */
.filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.chip { display: inline-flex; align-items: center; gap: 7px; padding: 6px 13px; border-radius: 999px; border: 1px solid var(--hairline-strong); background: var(--surface-color); color: var(--text-soft); font-family: var(--nav-font); font-size: .82rem; font-weight: 500; text-decoration: none; transition: border-color .15s ease, color .15s ease, background .15s ease; }
.chip:hover { border-color: rgba(64,182,5,.4); color: var(--text-strong); }
.chip.on { background: var(--chip-on-bg); border-color: var(--chip-on-bg); color: #fff; }
.chip em { font-style: normal; opacity: .6; font-variant-numeric: tabular-nums; }
/* The count is content, and no partial opacity clears 4.5:1 on this fill (.9 lands at ~2.4:1,
   .85 at 4.1:1), so the selected chip prints it at full strength. */
.chip.on em { opacity: 1; }

/* Inline toolbar (class + date pickers) */
.toolbar { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px; }
.fld { display: flex; flex-direction: column; gap: .35rem; font-family: var(--nav-font); font-size: .8rem; color: var(--text-soft); }
.fld .dx-input { min-width: 190px; }
select.dx-input { cursor: pointer; }
.dx-input-sm { padding: .4rem .55rem; font-size: .85rem; min-width: 128px; width: auto; }

/* Stacked create forms */
.dx-form { display: flex; flex-direction: column; gap: 15px; max-width: 460px; }
.dx-form label { display: flex; flex-direction: column; gap: .35rem; font-family: var(--nav-font); font-size: .84rem; color: var(--text-soft); }
.dx-form .hint { font-size: .76rem; color: var(--text-faint); }
.form-actions { display: flex; gap: 10px; justify-content: flex-end; align-items: center; margin-top: 4px; }
.form-panel { max-width: 560px; }

/* Right-aligned table filter within a panel head */
.panel-head .toolbar { margin-left: auto; }

/* Topbar sign-out */
.logout-form { display: inline-flex; margin: 0; }

/* Login error banner */
.auth-error { background: rgba(239,68,68,.1); border: 1px solid rgba(239,68,68,.35); border-radius: 10px; color: var(--text-strong); padding: 10px 13px; font-size: .85rem; margin-bottom: 16px; }

/*--------------------------------------------------------------
# Users & roles
--------------------------------------------------------------*/
.role-chip { display: inline-block; background: var(--brand-grad-soft); border: 1px solid var(--accent-border); color: var(--accent-link); border-radius: 999px; font-family: var(--nav-font); font-size: .7rem; font-weight: 600; padding: 2px 9px; margin: 0 4px 3px 0; white-space: nowrap; }
.self-tag { display: inline-block; margin-left: 7px; background: var(--hover-bg); border: 1px solid var(--hairline-strong); color: var(--text-muted); border-radius: 999px; font-family: var(--nav-font); font-size: .64rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; padding: 1px 7px; }
.status-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 7px; vertical-align: middle; }
.status-dot.on { background: var(--status-good); box-shadow: 0 0 0 3px rgba(34,197,94,.16); }
.status-dot.off { background: var(--text-faint); }
.inline-form { display: inline-flex; margin: 0; }

/* Status pill reused across modules (paid / overdue / approved …) */
.risk.ok { color: var(--status-good); background: rgba(34,197,94,.14); }

/* Inline forms inside table cells (receipt, issue, admit) */
.pay-form { display: inline-flex; gap: 6px; align-items: center; margin: 0; }
.pay-form .dx-input-sm { min-width: 92px; }
.inline-form + .inline-form { margin-left: 6px; }

/* Announcement feed */
.ann-list { display: flex; flex-direction: column; gap: 12px; }
.ann { border: 1px solid var(--hairline); border-radius: 12px; padding: 13px 15px; background: var(--surface-2); }
.ann-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
.ann-head b { font-family: var(--nav-font); font-size: .95rem; color: var(--text-strong); }
.ann p { color: var(--text-soft); font-size: .88rem; white-space: pre-wrap; }
.ann em { display: block; font-style: normal; margin-top: 8px; font-size: .76rem; color: var(--text-faint); }

/* Timetable grid */
.tt-grid td { vertical-align: top; min-width: 118px; }
.tt-grid td b { display: block; font-family: var(--nav-font); font-size: .82rem; }

/* Multi-line textarea shares the input skin */
textarea.dx-input { resize: vertical; min-height: 90px; font-family: var(--default-font); }

/* Role picker */
.role-set { border: 1px solid var(--hairline); border-radius: 12px; padding: 14px 15px; display: flex; flex-direction: column; gap: 11px; }
.role-set legend { font-family: var(--nav-font); font-size: .84rem; color: var(--text-soft); padding: 0 6px; }
.role-opt { flex-direction: row !important; align-items: flex-start; gap: .6rem !important; cursor: pointer; }
.role-opt input { accent-color: var(--accent-color); margin-top: 3px; flex: none; }
.role-opt b { display: block; font-family: var(--nav-font); font-size: .86rem; color: var(--text-strong); font-weight: 600; }
.role-opt em { display: block; font-style: normal; font-size: .78rem; color: var(--text-muted); margin-top: 1px; }

/*--------------------------------------------------------------
# Print — report cards, transcripts, receipts
--------------------------------------------------------------*/
/* Browsers drop background colours when printing but keep foreground ones, so a dark-theme
   report card prints near-white text on white paper — a blank sheet. Re-declare the tokens for
   paper. The selector repeats both theme attributes because html[data-theme="dark"] outranks a
   bare :root, so an explicitly chosen theme would otherwise win. */
@media print {
    :root, html[data-theme="dark"], html[data-theme="light"] {
        color-scheme: light;
        --background-color: #fff;
        --surface-color:    #fff;
        --surface-2:        #fff;
        --surface-deep:     #fff;
        --input-bg:         #fff;

        --default-color: #000;
        --heading-color: #000;
        --text-strong:   #000;
        --text-soft:     #1c1c1c;
        --text-muted:    #333;
        --text-faint:    #444;
        --hairline:        rgba(0,0,0,.25);
        --hairline-strong: rgba(0,0,0,.45);

        --accent-link:       #15803d;
        --accent-link-hover: #15803d;
        --status-good: #166534;
        --status-warn: #854d0e;
        --status-bad:  #b91c1c;
        --status-info: #075985;

        /* A transparent shadow rather than `none`: these tokens are substituted into comma
           lists (.auth-card, .campus-menu), and `none` inside a list invalidates the whole
           declaration. */
        --elev-1: 0 0 0 0 transparent;
        --elev-2: 0 0 0 0 transparent;
        --glow-green: 0 0 0 0 transparent;
        --accent-glow: 0 0 0 0 transparent;
    }

    .sidebar, .sb-scrim, .topbar, #nav-progress, .toast, .skip-link,
    .page-actions, .form-actions, .filters, .btn, .btn-approve, .btn-reject, .icon-btn { display: none !important; }

    .app { display: block; }
    .content { padding: 0; max-width: none; }
    .panel { border: 1px solid var(--hairline-strong); box-shadow: none; break-inside: avoid; }
    /* The on-screen scroller would clip a long mark sheet to 72vh of paper. */
    .table-scroll, .table-scroll.compact { overflow: visible; max-height: none; }
    .panel > .table-scroll { margin-inline: 0; }
    .dx-table th { position: static; background: none; }
    .dx-table tr { break-inside: avoid; }
}
