/* Project overrides on top of AdminLTE 4. Loaded last and cache-busted on mtime by
   _header2.cfm, so a change here shows up without a hard refresh. */

/* Select2 renders its own control height; without this it sits a pixel or two off
   every Bootstrap .form-select beside it in the same row. */
.select2-container--bootstrap-5 .select2-selection {
    min-height: calc(1.5em + 0.75rem + 2px);
}

/* DataTables' processing overlay defaults to a translucent white band that is
   invisible in dark mode. */
div.dt-processing {
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    /* SIZED TO THE WORDS IN IT. DataTables pins this box to 200px and pulls it back by
       half that to centre it, which is narrower than "Loading, please wait..." beside
       four dots - so the text wrapped inside a box that had no reason to be that size.
       width:auto with the same centring keeps it in the middle of the table and lets it
       be as wide as it needs; max-width stops a long message from spanning the screen. */
    width: auto;
    max-width: 90%;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    margin-left: 0;
    margin-top: 0;
    padding: .75rem 1.25rem;
}

/* THE WAIT HAPPENS INSIDE THE TABLE, and this is what keeps it there. The overlay is
   positioned at the middle of the table, so while the rows are still on their way there
   is nothing under it to be the middle OF - the body is one empty row of no height, and
   the indicator lands in the blank card below the column headers looking like it belongs
   to the page rather than to the list.

   Reserving the empty row's height gives the overlay a table to sit in. It does the same
   for "No matching records found", which had the same problem and nothing to fix it. */
tr.dt-empty > td,
table.dataTable > tbody > tr > td.dt-empty {
    /* TALL ENOUGH FOR THE MARK THAT SITS IN IT - logo over words over dots. It was 7rem,
       set when a loading table showed a one-line indicator without the logo. */
    height: 9.5rem;
    vertical-align: middle;
}

/* ---------------------------------------------------------------------------
   A SECTION CARD INSIDE A PERSON'S TAB HAS NO FRAME. A CONTENT CARD KEEPS ONE.

   The Profile, Azure and Precoro tabs each wrap their record in a headed card,
   and those sit inside the person panel's own card body - so the border was a
   box drawn just inside another box and the shadow was a second one lifting off
   it. The Finance tab is not that: its stat tiles are cards in their own right,
   sitting on the tab rather than framing it, and stripping their borders left
   four numbers floating on the page.

   THE HEADER IS WHAT TELLS THEM APART, and that is the application's own rule
   rather than a new one: "a stat tile is the one card with no header"
   (root CLAUDE.md). A card WITH a header is a section - it is titling something
   already inside a frame, so it needs none of its own. A card WITHOUT one is a
   thing on the page and keeps its edges.

   Measured before it was written: Azure 7 cards and 7 headers, Precoro 2 and 2,
   the Profile card 1 and 1 - all sections. The stat tiles and the Education
   tab's card carry none, and keep their frames.

   THE SHADOW IS ADMINLTE'S, NOT BOOTSTRAP'S. Bootstrap leaves
   --bs-card-box-shadow empty; AdminLTE sets a real box-shadow on .card, so
   overriding the variable does nothing and the declaration has to be beaten.

   WHERE :has() IS NOT SUPPORTED nothing breaks - every card keeps its frame,
   which is how all of them looked before.
   --------------------------------------------------------------------------- */
.card-body.tab-content .card:has(> .card-header) {
    border: 0;
    box-shadow: none;
}

/* ---------------------------------------------------------------------------
   A FACTS TABLE HAS NO RULES BETWEEN ITS ROWS.

   The label-and-value tables on a person's record - Azure, Precoro, the person
   and profile panels - are not lists. They are one record read top to bottom,
   and Bootstrap's line under every cell chopped four facts into four boxes.
   A LIST needs its rules, because a row is a separate thing; a record does not,
   because the rows are one thing.

   IT IS KEYED ON THE <th>, NOT ON A CLASS ANYBODY HAS TO REMEMBER. Every one of
   these tables already marks its label cell `th.fw-normal.ps-0` - a normal-weight
   heading flush to the left edge - and nothing else in the application does. So a
   facts table written tomorrow gets this by being written the same way, which is
   the point of a standard: no second class to add, and no way to add it wrongly.
   It is the same trick the column-alignment hook in _footer2.cfm uses, reading a
   list's alignment off its own <th> rather than a per-page columnDefs.

   BORDERED AND LIST TABLES ARE UNTOUCHED. They carry no such <th>, so every
   DataTable, every .table-bordered and the two report tables on migrate.cfm keep
   the rules they need.

   WHERE :has() IS NOT SUPPORTED nothing breaks - the borders simply stay, which
   is exactly how these tables looked before. A progressive improvement rather
   than a layout that depends on it.
   --------------------------------------------------------------------------- */
.table:has(th.fw-normal.ps-0) > :not(caption) > * > * {
    border-bottom-width: 0;
}


/* Long audit-log detail strings would otherwise force the table to scroll sideways. */
#audit-table td:last-child {
    max-width: 40rem;
    word-break: break-word;
}

/* AdminLTE still carries `.card-title { float: left }` from its pre-flex layout. Inside
   a flex card-header the float is ignored, but it makes header layout depend on whether
   a given header happens to be flex - so a "New" button lands right on one screen and
   beside the title on another. Make every card header flex and drop the float, so the
   layout is the same everywhere and `ms-auto` on the action button always works. */
.card-header {
    display: flex;
    align-items: center;
}
.card-header .card-title {
    float: none;
    margin: 0;
}

/* The title absorbs the free space, so everything after it in a card header sits hard
   right - whatever the button is, however many there are, including headers not written
   yet. Doing it here rather than with `ms-auto` on each button means there is one rule
   to know about instead of a class to remember. */
.card-header > .card-title {
    margin-right: auto;
}

/* The sidebar user panel is our own markup, so AdminLTE's collapse rules never reach
   it - without this it keeps its full width and name while everything around it
   shrinks to an icon rail.

   Mirrors what the theme does to the menu, including the `:not(:hover)` escape: hover
   the collapsed rail and the panel comes back, exactly like the nav labels do. */
.sidebar-mini.sidebar-collapse .app-sidebar:not(:hover) .sidebar-user-text {
    display: none;
}
.sidebar-mini.sidebar-collapse .app-sidebar:not(:hover) .sidebar-user > div {
    justify-content: center;
    padding-left: 0;
    padding-right: 0;
}

/* Toasts slide in from the right and back out the same way. Bootstrap only fades, and a
   notification that appears in the corner reads better if it arrives from the edge it is
   pinned to - the movement is what catches the eye, not the colour. */
#toast-container .ums-toast {
    transition: transform .4s ease, opacity .4s ease;
}
#toast-container .ums-toast:not(.show) {
    transform: translateX(120%);
    opacity: 0;
}
/* Added by the footer just before hiding, so the exit animates instead of snapping. */
#toast-container .ums-toast.ums-toast-out {
    transform: translateX(120%);
    opacity: 0;
}

/* ================================================================ BRANDBOOK ===
   "UoC Brandbook MARCH 2026" - integration/uoc-branding/ - which covers all four
   institutions: University of Cyberjaya, Cyberjaya College Central, Cyberjaya
   College Kuching and Cyberjaya College Kota Kinabalu.

   THIS BLOCK IS THE ONLY PLACE THE PALETTE IS WRITTEN DOWN. login.cfm reads these
   same variables - it loads this file - so the sign-in screen and the signed-in
   application cannot drift apart, and a value is checked against the book on one
   page rather than hunted through a stylesheet.

   Page 18 is the palette. The four primaries first, then the secondaries as the
   book pairs them (each primary with a light and a deep shade), then the two
   neutrals. Names follow the book so anybody holding it can check a value against
   the page rather than guess what "--accent-2" was meant to be.

   The book prints TEAL as #00B98F and as R0 G185 B173 on the same swatch, which are
   not the same colour. The hex is what is used here. */
:root {
    --uoc-purple:       #603F8E;
    --uoc-purple-deep:  #462378;
    --uoc-purple-light: #BEAAD7;
    --uoc-pink:         #E13F8F;
    --uoc-pink-deep:    #BE1E78;
    --uoc-pink-light:   #FFBEDC;
    --uoc-teal:         #00B98F;
    --uoc-teal-deep:    #00948A;
    --uoc-teal-light:   #99E3DE;
    --uoc-yellow:       #FFFA32;
    --uoc-yellow-light: #FFFAA5;
    --uoc-gold:         #FFD700;
    --uoc-grey:         #D2D2D2;

    /* Page 15/16. Raleway is the headline face and is the one remote asset in the
       application. Proxima Nova is the book's body face, is licensed, and is not
       held here - so body copy uses the book's OWN internal-use list from page 16,
       Aptos then Calibri, both of which ship with Windows and Office. Every stack
       ends in the system UI font, so an unreachable Google Fonts changes the
       typeface and nothing else. */
    --uoc-font-head: "Raleway", "Segoe UI", system-ui, sans-serif;
    --uoc-font-body: "Aptos", "Calibri", "Segoe UI", system-ui, -apple-system, Roboto, Arial, sans-serif;
}

/* -------------------------------------------------- Bootstrap's own colours ---
   Remapped where the book HAS an answer, and left alone where it does not.

   primary -> purple, info -> teal, warning -> the book's gold. success, danger and
   secondary keep Bootstrap's green, red and grey: those are not brand colours, they
   are STATUS colours - a failed import, a barred result, an inactive row - and the
   book has neither a red nor a green to offer. Recolouring them would cost meaning
   to gain consistency, which is the wrong way round.

   Bootstrap's utilities (.text-bg-*, .bg-*, .text-*, .border-*) all read the -rgb
   variables, so those follow from here. Components do NOT: .btn-primary and friends
   carry compiled hex values, which is what the component block below is for. */
:root {
    --bs-primary:              #603F8E;
    --bs-primary-rgb:          96, 63, 142;
    --bs-primary-text-emphasis:#462378;
    --bs-primary-bg-subtle:    #EFEAF5;
    --bs-primary-border-subtle:#D5C7E6;

    --bs-info:                 #00B98F;
    --bs-info-rgb:             0, 185, 143;
    --bs-info-text-emphasis:   #00594A;
    --bs-info-bg-subtle:       #E6F8F3;
    --bs-info-border-subtle:   #99E3DE;

    --bs-warning:              #FFD700;
    --bs-warning-rgb:          255, 215, 0;

    --bs-link-color:           #603F8E;
    --bs-link-color-rgb:       96, 63, 142;
    --bs-link-hover-color:     #462378;
    --bs-link-hover-color-rgb: 70, 35, 120;
    --bs-focus-ring-color:     rgba(96, 63, 142, .25);
}

/* Dark mode needs the LIGHT purple: #603F8E on a near-black page is a link nobody
   can read. Bootstrap sets these under its own [data-bs-theme="dark"] rule, which
   has the same specificity as the :root above - this file loads last, so without
   this block the dark theme would keep the light theme's link colour. */
[data-bs-theme="dark"] {
    --bs-link-color:           #BEAAD7;
    --bs-link-color-rgb:       190, 170, 215;
    --bs-link-hover-color:     #FFBEDC;
    --bs-link-hover-color-rgb: 255, 190, 220;
    --bs-primary-text-emphasis:#BEAAD7;
    --bs-primary-bg-subtle:    #26193A;
    --bs-primary-border-subtle:#462378;
}

/* --------------------------------------------------- components, one by one ---
   Everything here is a component that compiled Bootstrap's blue into itself rather
   than reading --bs-primary at run time. Each is stated as the component's own
   variables, which is how Bootstrap 5.3 expects to be themed - no !important, and
   no rule that has to be re-specified every time a button gains a state. */
.btn-primary {
    --bs-btn-bg: var(--uoc-purple);
    --bs-btn-border-color: var(--uoc-purple);
    --bs-btn-hover-bg: var(--uoc-purple-deep);
    --bs-btn-hover-border-color: var(--uoc-purple-deep);
    --bs-btn-active-bg: var(--uoc-purple-deep);
    --bs-btn-active-border-color: var(--uoc-purple-deep);
    --bs-btn-disabled-bg: var(--uoc-purple);
    --bs-btn-disabled-border-color: var(--uoc-purple);
    --bs-btn-focus-shadow-rgb: 96, 63, 142;
}
.btn-outline-primary {
    --bs-btn-color: var(--uoc-purple);
    --bs-btn-border-color: var(--uoc-purple);
    --bs-btn-hover-bg: var(--uoc-purple);
    --bs-btn-hover-border-color: var(--uoc-purple);
    --bs-btn-active-bg: var(--uoc-purple);
    --bs-btn-active-border-color: var(--uoc-purple);
    --bs-btn-disabled-color: var(--uoc-purple);
    --bs-btn-disabled-border-color: var(--uoc-purple);
    --bs-btn-focus-shadow-rgb: 96, 63, 142;
}
/* Black ink on both, as Bootstrap already does for info and warning - the book's
   teal and gold are both light enough that white would fail on them. */
.btn-info {
    --bs-btn-bg: var(--uoc-teal);
    --bs-btn-border-color: var(--uoc-teal);
    --bs-btn-hover-bg: var(--uoc-teal-deep);
    --bs-btn-hover-border-color: var(--uoc-teal-deep);
    --bs-btn-active-bg: var(--uoc-teal-deep);
    --bs-btn-active-border-color: var(--uoc-teal-deep);
    --bs-btn-disabled-bg: var(--uoc-teal);
    --bs-btn-disabled-border-color: var(--uoc-teal);
    --bs-btn-focus-shadow-rgb: 0, 185, 143;
}
.btn-warning {
    --bs-btn-bg: var(--uoc-gold);
    --bs-btn-border-color: var(--uoc-gold);
    --bs-btn-hover-bg: var(--uoc-yellow);
    --bs-btn-hover-border-color: var(--uoc-yellow);
    --bs-btn-active-bg: var(--uoc-yellow);
    --bs-btn-active-border-color: var(--uoc-yellow);
    --bs-btn-disabled-bg: var(--uoc-gold);
    --bs-btn-disabled-border-color: var(--uoc-gold);
    --bs-btn-focus-shadow-rgb: 255, 215, 0;
}

/* One more brand colour than Bootstrap has a slot for: the book's pink is a primary,
   and nothing in Bootstrap's six is it. Named for the book, not for a meaning - it
   is a brand colour and never a status. */
.text-bg-brand-pink {
    color: #fff !important;
    background-color: var(--uoc-pink) !important;
}

/* Forms: the focus ring is the most-seen blue in the application, on every input on
   every screen. */
.form-control:focus,
.form-select:focus {
    border-color: var(--uoc-purple-light);
    box-shadow: 0 0 0 .25rem rgba(96, 63, 142, .25);
}
.form-check-input:checked {
    background-color: var(--uoc-purple);
    border-color: var(--uoc-purple);
}
.form-check-input:focus {
    border-color: var(--uoc-purple-light);
    box-shadow: 0 0 0 .25rem rgba(96, 63, 142, .25);
}
/* The switch knob is an inline SVG in a data URI, with the colour baked in - so the
   only way to move it off Bootstrap's blue is to restate the image. */
.form-switch .form-check-input:focus {
    --bs-form-switch-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='%23BEAAD7'/></svg>");
}

/* DataTables draws its pager as Bootstrap pagination, so this is every table's
   page numbers. */
.pagination {
    --bs-pagination-color: var(--uoc-purple);
    --bs-pagination-hover-color: var(--uoc-purple-deep);
    --bs-pagination-focus-color: var(--uoc-purple-deep);
    --bs-pagination-focus-box-shadow: 0 0 0 .25rem rgba(96, 63, 142, .25);
    --bs-pagination-active-bg: var(--uoc-purple);
    --bs-pagination-active-border-color: var(--uoc-purple);
}
.nav-pills {
    --bs-nav-pills-link-active-bg: var(--uoc-purple);
}
.dropdown-menu {
    --bs-dropdown-link-active-bg: var(--uoc-purple);
}
.list-group {
    --bs-list-group-active-bg: var(--uoc-purple);
    --bs-list-group-active-border-color: var(--uoc-purple);
}
/* ----------------------------------------------------------------- Select2 ---
   THE THEME COMPILED LIGHT MODE INTO ITSELF. select2-bootstrap-5-theme states
   #fff and #212529 as literals on the control, the dropdown and the search box
   inside it, so every filter on the page stayed white when the rest of the
   application went dark - which is what this fixes.

   IT IS MAPPED TO BOOTSTRAP'S OWN VARIABLES rather than given a
   [data-bs-theme="dark"] block of its own. #fff and #212529 are exactly what
   --bs-body-bg and --bs-body-color resolve to in LIGHT mode, so this changes
   nothing there and follows the theme in both directions - including any third
   theme added later. The same call div.dt-processing already made above.

   AND IT MATCHES THE VENDOR'S SPECIFICITY, which the previous version did not:
   its selectors are five classes deep
   (.select2-container--bootstrap-5 .select2-dropdown .select2-results__options
    .select2-results__option.select2-results__option--highlighted), so a
   two-class override lost and the blue it compiled in was what showed. The
   highlighted row was blue on screen while this file said purple. */

.select2-container--bootstrap-5 .select2-selection,
.select2-container--bootstrap-5 .select2-dropdown,
.select2-container--bootstrap-5 .select2-dropdown .select2-search .select2-search__field {
    color: var(--bs-body-color);
    background-color: var(--bs-body-bg);
    border-color: var(--bs-border-color);
}

/* THE CHOSEN VALUE IS A CHILD, AND IT CARRIES ITS OWN COLOUR. Setting the
   control above is not enough: the theme puts #212529 on .select2-selection__rendered
   itself, which inherits nothing from its parent, so the text stayed near-black on
   the dark control and a chosen campus was invisible until you opened the list. */
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered {
    color: var(--bs-body-color);
}

/* The greys the theme hardcodes for text that is deliberately quiet. */
.select2-container--bootstrap-5 .select2-selection--single .select2-selection__rendered .select2-selection__placeholder,
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__message,
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__option--disabled,
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[role=group] .select2-results__group {
    color: var(--bs-secondary-color);
}

/* A chosen value in a multiple select is a tag, not body text: it needs to sit
   ON the control rather than disappear into it, so it takes the tertiary
   surface, which flips with the theme like everything else. */
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
    color: var(--bs-body-color);
    background-color: var(--bs-tertiary-bg);
    border-color: var(--bs-border-color);
}
/* Its remove X is an inline SVG with a dark grey baked into the data: URI, so it
   cannot be recoloured - it is inverted instead, and only where the ground is
   dark. */
[data-bs-theme="dark"] .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice .select2-selection__choice__remove {
    filter: invert(1) opacity(.75);
}

/* THE BRAND COLOURS, at the depth the theme states its own.
   Highlighted is the row under the cursor; selected is the one already chosen.
   Both were Bootstrap blue. White text on both, because the purple is dark
   enough that the theme's #000 on highlight was unreadable. */
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__option--highlighted,
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option.select2-results__option--selected,
.select2-container--bootstrap-5 .select2-dropdown .select2-results__options .select2-results__option[aria-selected=true]:not(.select2-results__option--highlighted) {
    color: #fff;
    background-color: var(--uoc-purple);
}

/* A DISABLED CONTROL IS A STATE, NOT A THEME, and the theme states it two classes
   deeper than the base rule - so it rightly wins, and it wins with #e9ecef, which is
   a bright grey box on a dark page. Bootstrap dresses its own disabled .form-select
   in --bs-secondary-bg, so a disabled picker now matches the disabled select beside
   it in either theme. */
.select2-container--bootstrap-5.select2-container--disabled .select2-selection,
.select2-container--bootstrap-5.select2-container--disabled.select2-container--focus .select2-selection {
    color: var(--bs-secondary-color);
    background-color: var(--bs-secondary-bg);
    border-color: var(--bs-border-color);
}

.select2-container--bootstrap-5.select2-container--focus .select2-selection,
.select2-container--bootstrap-5.select2-container--open .select2-selection {
    border-color: var(--uoc-purple-light);
    box-shadow: 0 0 0 .25rem rgba(96, 63, 142, .25);
}

/* ------------------------------------------------------- the shell in purple ---
   The sidebar is the one large field of colour in the signed-in application, and it
   was AdminLTE's near-black. It is the book's deep purple now, which is what the
   book offers for a dark ground (page 9: the logo's reverse variant is specified on
   purple, black or grey).

   Set as the Bootstrap variable the .bg-body-secondary utility on the sidebar reads,
   rather than as a background-color with !important - the utility keeps working, and
   nothing has to be removed from the markup. */
.app-sidebar {
    --bs-secondary-bg-rgb: 51, 26, 92;
}

/* AdminLTE's own sidebar variables, under the data-bs-theme="dark" the sidebar
   carries. The active item is white at low opacity rather than a second colour: on
   a purple ground, a purple highlight is invisible and a pink one shouts. */
.app-sidebar[data-bs-theme="dark"] {
    --lte-sidebar-hover-bg: rgba(255, 255, 255, .08);
    --lte-sidebar-menu-active-bg: rgba(255, 255, 255, .14);
    --lte-sidebar-submenu-hover-bg: rgba(255, 255, 255, .08);
    --lte-sidebar-submenu-active-bg: rgba(255, 255, 255, .14);
    --lte-sidebar-submenu-color: rgba(255, 255, 255, .68);
}

/* WHICH item is current, said in pink down the left edge - the one place the second
   primary appears in the shell. An inset shadow rather than a border, because a
   border would move the label by its own width and only on the active row. */
.app-sidebar .sidebar-menu .nav-link.active {
    box-shadow: inset .1875rem 0 0 var(--uoc-pink);
}

/* The four primaries as a hairline under the top bar - the same signature the
   sign-in screen carries across the top of its form column, so the two screens are
   recognisably one application. */
.app-header {
    position: relative;
}
.app-header::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 3px;
    background: linear-gradient(90deg,
        var(--uoc-purple) 0 25%,
        var(--uoc-pink) 25% 50%,
        var(--uoc-teal) 50% 75%,
        var(--uoc-yellow) 75% 100%);
}

/* ---------------------------------------------------------------- Typography ---
   Raleway for headings, the book's internal-use faces for everything else - both
   defined once in the brandbook block above.

   The 14px root is what actually makes the UI smaller. AdminLTE 4 sizes everything
   - padding, sidebar width, card gutters - in rem, so moving the root scales the
   whole layout down together. Setting only body{font-size} would shrink the text
   inside boxes that stayed large, which reads as a mistake rather than a density. */
:root {
    font-size: 14px;
}

body,
.select2-container,
.dataTables_wrapper {
    font-family: var(--uoc-font-body);
}

h1, h2, h3, h4, h5, h6,
.h1, .h2, .h3, .h4, .h5, .h6,
.card-title,
.modal-title,
.brand-text,
.btn-primary {
    font-family: var(--uoc-font-head);
}

/* Headings scale with the root, but AdminLTE's page H1 is still oversized against a
   14px base - CampusOne's page title sits at about this weight. */
.app-content-header h3,
.app-content-header .h3 { font-size: 1.5rem; font-weight: 600; }

.card-title { font-size: 1rem; font-weight: 600; }

/* Tabbed detail panels - the person and profile panels. The tab strip sits IN the card
   header, so the header's own padding would double the inset; that is what p-0 and the
   inset below are for.

   THE STRIP KEEPS ITS OWN BOTTOM BORDER, and removing it was a mistake corrected
   2026-09-12. The markup carries `border-bottom-0` on the header to avoid a double rule -
   the header's line sitting a pixel under the nav's - and this rule then took the nav's
   away as well, which left the tabs floating over the panel with nothing under them at
   all. The nav's is the one to keep: Bootstrap paints the ACTIVE tab's bottom edge in the
   card's own colour, so the line breaks where the open tab is and that break is what
   joins the tab to the panel below it.

   It is a flex item because .card-header is flex, so it needs to be told to take the
   width - otherwise the border stops at the last tab. */
.card-header.p-0 .nav-tabs {
    flex: 1 1 auto;
    padding: 0 .5rem;
}

/* --------------------------------------------------- collapsed sidebar rail ---
   Icon-only means icon-only. AdminLTE keeps the OPEN group's child list in the
   flow when collapsed, so the branch matching the current page rendered its
   children as a stack of bare carets under the real icons - which reads as broken
   icons rather than as a menu.

   Scoped to :not(:hover) throughout, so the whole menu comes back the moment the
   rail is hovered, which is how it is meant to be used. */
.sidebar-mini.sidebar-collapse .app-sidebar:not(:hover) .nav-treeview {
    display: none;
}

/* The label and the disclosure arrow have no room in a 4rem rail; the icon alone
   carries the meaning, and the title attribute covers the rest. */
.sidebar-mini.sidebar-collapse .app-sidebar:not(:hover) .sidebar-menu > .nav-item > .nav-link > p {
    display: none;
}

/* Centre what is left, instead of leaving each icon indented where its text used
   to start. */
.sidebar-mini.sidebar-collapse .app-sidebar:not(:hover) .sidebar-menu > .nav-item > .nav-link {
    justify-content: center;
    padding-left: 0;
    padding-right: 0;
}
.sidebar-mini.sidebar-collapse .app-sidebar:not(:hover) .sidebar-menu > .nav-item > .nav-link > .nav-icon {
    margin-right: 0;
}

/* ------------------------------------------------- hover overlays, not pushes ---
   AdminLTE 4 lays the shell out as a CSS grid: `grid-template-columns: auto 1fr`,
   so the first column TRACKS the sidebar. Its own hover rule widens the sidebar to
   250px, the column follows, and the whole page slides right - which is what made
   a mouse passing over the rail shove the content you were reading.

   The fix is to stop the column tracking. Pinned to the 4.6rem rail while collapsed,
   the hovered sidebar overflows its own grid area and floats over the content
   instead, which is how CampusOne behaves on AdminLTE 3 (fixed sidebar + a fixed
   content margin).

   Clicking the burger still pushes: that removes .sidebar-collapse entirely, so none
   of these rules apply and the grid goes back to tracking. Hover overlays, click
   commits - the distinction being asked for. */
.sidebar-mini.sidebar-collapse .app-wrapper {
    grid-template-columns: 4.6rem 1fr;
}

/* BUT NOT ON A PHONE. Below lg AdminLTE takes the sidebar out of the grid - position:
   fixed, slid off-canvas by a negative margin, overlaid when opened - so its own first
   column (auto) collapses to zero and the content runs full width. The rule above instead
   pins that column to the 4.6rem rail, which on mobile reserves an empty strip down the
   left with nothing in it: the sidebar is off-canvas, but the gap it used to fill stays.
   Hand the track back to auto at that width, which is AdminLTE's own mobile default. */
@media (max-width: 991.98px) {
    .sidebar-mini.sidebar-collapse .app-wrapper {
        grid-template-columns: auto 1fr;
    }
}

/* Above the content and the header's shadow, below modals (1055) and toasts (1090).
   Without this the expanded panel renders behind the page it is overlaying. */
.sidebar-mini.sidebar-collapse .app-sidebar {
    z-index: 1038;
}

/* Only while it is actually overlaying - a drop shadow on the flush rail would just
   look like a stray line. */
.sidebar-mini.sidebar-collapse .app-sidebar:hover {
    box-shadow: 0 0 1.5rem rgba(0, 0, 0, .35);
}

/* --------------------------------------------------------------- brand mark ---
   AdminLTE centres .sidebar-brand. That looks deliberate on the narrow rail and
   wrong the moment the sidebar opens: the logo floats in the middle of a 250px
   panel while every menu item below it starts at the left edge.

   So: left-aligned by default, and centred ONLY while the rail is collapsed and
   not hovered, where it lines up with the icon column instead. */
.sidebar-brand {
    justify-content: flex-start;
}

.sidebar-mini.sidebar-collapse .app-sidebar:not(:hover) .sidebar-brand {
    justify-content: center;
}
.sidebar-mini.sidebar-collapse .app-sidebar:not(:hover) .sidebar-brand .brand-image {
    margin-left: 0 !important;
    margin-right: 0 !important;
}
/* ====================================================== modal form layout ===
   THE house layout for every modal form. See CLAUDE.md; the markup is:

       <form class="form-modal" ...>
         <div class="row">
           <div class="col-lg-6">          left half - fields stack down it
             <div class="row mb-3">        one field
               <label class="col-3 col-form-label">Label <sup>*</sup></label>
               <div class="col-9">         control, then its .form-text
           <div class="col-lg-6">          right half

   Fields are dealt LEFT half first, then RIGHT, balanced so the dialog is about
   as tall on both sides. A form is READ, not scrolled: one stack of ten fields
   pushes Save below the fold, and the button is then the thing people miss.

   Real Bootstrap rows and columns, not a CSS grid pretending to be them - the
   markup says what it does, and `col-3` is a width anyone can change without
   reading this file. What lives HERE is only what would otherwise be repeated on
   every label in eleven forms. */

.form-modal .col-form-label {
    text-align: right;
}

/* Right-aligned only while the label is actually beside its control. Below lg the
   grid stacks them, and a right-aligned label above a full-width input reads as a
   mistake rather than as alignment. */
@media (max-width: 991.98px) {
    .form-modal .col-form-label {
        text-align: left;
        padding-bottom: 0;
    }
}

/* A switch has no baseline of its own, so it sits high against a label that does. */
.form-modal .form-check.form-switch {
    padding-top: .4rem;
}

/* The loading indicator - the organisation's mark over a spinner. The markup comes from
   loadingMark() in include/_functions.cfm, which is the ONE place it is written; this is
   the one place it is styled. It appears in the modal body before a form arrives and in
   the DataTables processing overlay, so it has to sit correctly in both.

   The mark is capped rather than sized: logo_small is whatever each organisation loaded
   and the files are not a uniform square, so a fixed height would stretch one deployment's
   logo to fit another's proportions. NEVER RECOLOURED - brandbook page 17 - so no filter
   or opacity is applied to make it sit better on a background. */
.app-loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .5rem;
}
/* THE GLOBAL STANDARD (2026-10-28): every wait for content is this one - uocLoading("words") in script, the
   uoc_core/_loading.html include on the server. On its own in a modal body it gets room to breathe. */
.modal-body > .app-loading { padding: 2rem 0; }
.app-loading-mark {
    max-height: 2.5rem;
    max-width: 8rem;
    width: auto;
}

/* THE FOUR DOTS, and they are ours now rather than DataTables'. DataTables appends its
   own set under whatever language.processing returns, so a loading TABLE had them and a
   loading pane, modal or tab did not - one indicator that looked like two things. The
   markup moved into loadingMark(), which every wait in the application already uses, and
   the geometry and the timings below are DataTables' own so that nothing on screen moved
   when it did.

   The vendor's set is hidden rather than reused: it is appended after the text and is the
   container's last child, and with ours in the markup a table would otherwise show eight
   dots. Safe because language.processing is set globally in _footer2.cfm, so every table
   here carries our indicator. */
div.dt-processing > div:last-child {
    display: none;
}

.app-loading-dots {
    position: relative;
    display: block;
    width: 80px;
    height: 13px;
}
.app-loading-dots > span {
    position: absolute;
    top: 0;
    width: 13px;
    height: 13px;
    border-radius: 50%;
    /* THE BRANDBOOK'S PURPLE, not Bootstrap's blue. This was rgb(13, 110, 253) - the
       literal DataTables ships, carried over with the geometry when the dots moved into
       loadingMark() and missed by the remap, which is exactly the trap CLAUDE.md names:
       a COMPONENT compiles the blue into itself and the --bs-*-rgb variables never reach
       it. The one blue thing left on a purple screen was the thing shown while somebody
       waited, which is the worst thing to have it be. */
    background: var(--bs-primary);
    animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.app-loading-dots > span:nth-child(1) { left:  8px; animation: ums-loader-1 .6s infinite; }
.app-loading-dots > span:nth-child(2) { left:  8px; animation: ums-loader-2 .6s infinite; }
.app-loading-dots > span:nth-child(3) { left: 32px; animation: ums-loader-2 .6s infinite; }
.app-loading-dots > span:nth-child(4) { left: 56px; animation: ums-loader-3 .6s infinite; }

/* THE COMPACT ONE - inside a table, where the card around it is already on screen. It
   is the same indicator with the organisation's mark left off (loadingMark(.., false)),
   so the only thing to say here is that it sits on one line rather than stacked. */
.app-loading-sm {
    flex-direction: row;
    align-items: center;
    gap: .75rem;
    /* IT WRAPPED, and "Loading, please / wait..." over a row of dots is the shape of a
       broken screen rather than a busy one - which is what somebody is looking at when
       they are already waiting. Two things caused it: the box below is narrower than the
       words, and the dots are a fixed 80px that flex was free to take the space from. */
    white-space: nowrap;
}
.app-loading-sm > span:first-child {
    flex: 0 1 auto;
}
.app-loading-sm > .app-loading-dots {
    flex: 0 0 auto;
}

@keyframes ums-loader-1 { 0% { transform: scale(0) }          100% { transform: scale(1) } }
@keyframes ums-loader-2 { 0% { transform: translate(0, 0) }   100% { transform: translate(24px, 0) } }
@keyframes ums-loader-3 { 0% { transform: scale(1) }          100% { transform: scale(0) } }

/* ------------------------------------------------- a section row inside a table ---
   The grouping rows in a long table - an academic session heading over its class
   groups, "Excluded by design" over the counts it covers - were Bootstrap's
   .table-light. That class PINS a light background: it sets --bs-table-bg to #f8f9fa
   and --bs-table-color to black outright, so in dark mode it painted a bright band
   across the table with black text on it, which is the one thing on the page that
   does not follow the theme.

   .table-group reads the theme's own variables instead. Bootstrap redefines
   --bs-secondary-bg and --bs-emphasis-color under [data-bs-theme="dark"], so the row
   sits one step off the table's ground in either mode without naming a colour here.

   The striped and hover values are set too, or a striped table repaints the row on
   alternate draws and a hover lightens the one thing meant to read as a heading. */
.table-group,
.table > :not(caption) > tr.table-group > * {
    --bs-table-color: var(--bs-emphasis-color);
    --bs-table-bg: var(--bs-secondary-bg);
    --bs-table-border-color: var(--bs-border-color);
    --bs-table-striped-bg: var(--bs-secondary-bg);
    --bs-table-striped-color: var(--bs-emphasis-color);
    --bs-table-hover-bg: var(--bs-secondary-bg);
    --bs-table-hover-color: var(--bs-emphasis-color);
    color: var(--bs-emphasis-color);
    background-color: var(--bs-secondary-bg);
}

/* ------------------------------------------------------------- a quiet badge ---
   The badge that says a thing is off, inactive, inherited, draft, retired - the
   one that has to read as "less than the others" - was Bootstrap's .text-bg-light.
   That class PINS its colours: --bs-light is #f8f9fa whatever the theme, and
   .text-bg-* computes black text against it. In dark mode that made the quietest
   badge on the page the brightest thing on it, a white chip on a near-black table.

   .text-bg-soft reads the theme's own variables instead. Bootstrap redefines
   --bs-secondary-bg and --bs-emphasis-color under [data-bs-theme="dark"], so the
   badge sits one step off the surface either way without naming a colour here.

   The border is what keeps it a badge rather than a smudge: at one step off the
   surface there is not enough contrast to read as a chip on its own. */
.text-bg-soft {
    color: var(--bs-emphasis-color) !important;
    background-color: var(--bs-secondary-bg) !important;
    border: 1px solid var(--bs-border-color);
}

/* --------------------------------------------- a note under a card ---
   Six of the person tabs end with a sentence explaining what the list above does
   NOT say - that the file is not held here, that an unpublished session is treated
   as deleted, that a re-sit with no mark is a registration. They were loose
   .form-text under the table, which reads as a stray line rather than as part of
   the panel.

   A real .card-footer cannot be used: the tabs render inside the card's BODY, and a
   footer has to be a sibling of it. So this takes the footer's own variables and
   bleeds to the card edges with negative margins matching the body's padding - the
   same treatment, in the only place it can go.

   Everything here is a Bootstrap card variable rather than a value, so it follows
   the theme and any change to card padding, in both modes. */
.card-note {
    margin: var(--bs-card-spacer-y) calc(-1 * var(--bs-card-spacer-x))
            calc(-1 * var(--bs-card-spacer-y));
    padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);
    background-color: var(--bs-card-cap-bg);
    border-top: var(--bs-card-border-width) solid var(--bs-card-border-color);
    border-radius: 0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius);
    font-size: .875em;
    color: var(--bs-secondary-color);
}

/* ------------------------------------------------------ the budget entry grid ---
   Accounts down, periods across, with the account column frozen at the left and the
   totals (and the remove button, where there is one) frozen at the right. It lives here
   rather than in a <style> on the page because TWO tables use it now: the grid somebody
   types into, and the read-only grid of what a closed revision held.

   SEPARATE BORDERS, DELIBERATELY. Under border-collapse the edge between two cells is
   ONE border painted by the TABLE, so it scrolls with the table and a frozen column
   cannot keep its own rule - the heavy line slid away with the months, and a box-shadow
   is not painted at all under collapse. Separate borders are painted by each cell, so
   they freeze with it. Each cell then draws its right and bottom edge only, or every
   line would be two cells thick; the table draws the top, and the frozen account column
   draws the left - if the TABLE drew it, it would scroll away.

   --fzx is the width of the action column at the right, and Total's sticky offset has to
   be exactly it. width on a table cell is a suggestion, so it is pinned with min and max
   as well; a strip left between them shows the months sliding past underneath. */
.bgt-grid {
    --fzx: 56px;
    border-collapse: separate;
    border-spacing: 0;
    border-top: var(--bs-border-width) solid var(--bs-border-color);
}
.bgt-grid > :not(caption) > * > * {
    border-width: 0 var(--bs-border-width) var(--bs-border-width) 0;
    border-style: solid;
    border-color: var(--bs-border-color);
}

/* THE ACCOUNT COLUMN DOES NOT WRAP. A long name broke over two lines and made its row
   twice as tall as the ones around it, so a column of identical boxes stopped lining up.
   The column widens to the longest name; the months scroll, so there is nothing for it to
   take space from. Its heavy right rule marks where the frozen part ends and the
   scrolling months begin - the ordinary border grey at twice the width, because
   near-black read as a line drawn round something rather than as an edge. */
.bgt-grid .fz-l {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--bs-body-bg);
    white-space: nowrap;
    border-left: var(--bs-border-width) solid var(--bs-border-color);
    border-right: 2px solid var(--bs-border-color);
}
/* ...and the same rule on the other side, where the months end and Total begins. */
.bgt-grid .fz-r {
    position: sticky;
    right: 0;
    z-index: 2;
    background: var(--bs-body-bg);
    border-left: 2px solid var(--bs-border-color);
}
/* A grid with a per-row action (the remove button) holds Total one column further in. */
.bgt-grid.bgt-grid-act .fz-r { right: var(--fzx); }
.bgt-grid .fz-x {
    position: sticky;
    right: 0;
    z-index: 2;
    background: var(--bs-body-bg);
    width: var(--fzx);
    min-width: var(--fzx);
    max-width: var(--fzx);
}

/* The corners sit INSIDE the header and footer bands, so they take the same ground
   .table-group gives those - a half-step lighter drew a pale block at each end. */
.bgt-grid thead .fz-l, .bgt-grid thead .fz-r, .bgt-grid thead .fz-x,
.bgt-grid tfoot .fz-l, .bgt-grid tfoot .fz-r, .bgt-grid tfoot .fz-x {
    z-index: 3;
    background: var(--bs-secondary-bg);
}
/* A heading row sets its own background on every cell, so the frozen ones must not
   repaint it with the body's. */
.bgt-grid .bgt-type .fz-l, .bgt-grid .bgt-type .fz-r, .bgt-grid .bgt-type .fz-x,
.bgt-grid .bgt-group .fz-l, .bgt-grid .bgt-group .fz-r, .bgt-grid .bgt-group .fz-x {
    background: inherit;
}

/* A CELL CHANGED AND NOT SAVED: the book's gold on the border and a wash behind, so a
   screen of identical boxes says at a glance which figures are in the browser only. Not
   a status - nothing is wrong with it - so never red, and it clears the moment the value
   is typed back to what was loaded. */
.bgt-grid input.bgt-changed {
    border-color: var(--bs-warning);
    box-shadow: 0 0 0 .15rem rgba(var(--bs-warning-rgb), .25);
    background-color: rgba(var(--bs-warning-rgb), .08);
    font-weight: 600;
}

/* COLLAPSE AND EXPAND IS + AND - (2026-10-26, the global standard) - like a folder tree: every <details> on every site
   has a small boxed + in front of its label while closed and a - while open, the browser's triangle hidden (as Azure's
   group tree, which draws its own and is left to it). A nested <details> indents under its parent. */
details > summary:not(.az-tree summary) { list-style: none; cursor: pointer; display: flex; align-items: center; gap: .5rem;
                                          padding: .2rem .3rem; border-radius: .25rem; }
details > summary:not(.az-tree summary):hover { background: var(--bs-tertiary-bg); }
details > summary:not(.az-tree summary)::-webkit-details-marker { display: none; }
details > summary:not(.az-tree summary)::before {
    content: "+"; flex: none; display: inline-block; width: .95rem; height: .95rem; line-height: .8rem; text-align: center;
    font-weight: 600; font-size: .8rem; color: var(--bs-secondary-color);
    border: 1px solid var(--bs-border-color); border-radius: .2rem;
}
details[open] > summary:not(.az-tree summary)::before { content: "\2212"; }
/* what a branch holds sits under its label, a tree line down from the + / - box */
details:not(.az-tree details) > :not(summary) { margin-left: .78rem; padding-left: calc(.67rem - 1px); border-left: 1px solid var(--bs-border-color); }   /* a child's + / - box sits under its parent's first item after the box */
details:not(.az-tree details) > table { width: calc(100% - 1.45rem); }   /* a full-width table keeps inside its indent */
/* a branch inside a branch is joined to that line by an elbow to its box; the last one ends the line there, as a corner */
details:not(.az-tree details) > details > summary { position: relative; }
details:not(.az-tree details) > details > summary::after {
    content: ""; position: absolute; left: -.67rem; top: 0; width: .97rem; height: 50%;
    border-bottom: 1px solid var(--bs-border-color); pointer-events: none;
}
details:not(.az-tree details) > details:last-child { border-left-color: transparent; }
details:not(.az-tree details) > details:last-child > summary::after { border-left: 1px solid var(--bs-border-color); }
/* LEAF ROWS ON THE TREE: <div class="uoc-tree-rows"><table> in a branch - each row's first cell (its checkbox) sits under
   the parent's first item after its box, joined to the tree line by an elbow; the last row ends the line */
details:not(.az-tree details) > .uoc-tree-rows { border-left-color: transparent; }
.uoc-tree-rows > table td:first-child, .uoc-tree-rows > table th:first-child { position: relative; padding-left: .3rem; }
.uoc-tree-rows > table td:first-child::before {
    content: ""; position: absolute; left: -.67rem; top: 0; width: .97rem; height: 50%; pointer-events: none;
    border-left: 1px solid var(--bs-border-color); border-bottom: 1px solid var(--bs-border-color);
}
.uoc-tree-rows > table tr:not(:last-child) td:first-child::after, .uoc-tree-rows > table th:first-child::after {
    content: ""; position: absolute; left: -.67rem; top: 50%; bottom: 0; pointer-events: none;
    border-left: 1px solid var(--bs-border-color);
}
.uoc-tree-rows > table th:first-child::after { top: 0; }
/* ...and every other fold wears the same box: an AdminLTE card's collapse button IS the box, in front of the card's
   title (its own + / - icons hidden); a Bootstrap collapse link marked .uoc-pm (start it .collapsed) has it in front
   of its text */
.card-header > [data-lte-toggle="card-collapse"] { order: -1; margin: 0 .5rem 0 0; padding: 0; border: 0; line-height: 1; }
[data-lte-toggle="card-collapse"] > i { display: none; }
.uoc-pm { display: inline-flex; align-items: flex-start; gap: .5rem; text-decoration: none; }   /* an underline would cross the box */
.uoc-pm::before { margin-top: .25em; }   /* on the first line of a long label */
[data-lte-toggle="card-collapse"]::before, .uoc-pm::before {
    content: "\2212"; flex: none; display: inline-block; width: .95rem; height: .95rem; line-height: .8rem; text-align: center;
    font-weight: 600; font-size: .8rem; color: var(--bs-secondary-color);
    border: 1px solid var(--bs-border-color); border-radius: .2rem;
}
.collapsed-card [data-lte-toggle="card-collapse"]::before, .uoc-pm.collapsed::before { content: "+"; }

/* TABS ON THE LEFT, THE PICKED ONE ON THE RIGHT (2026-10-26, the global standard) - a line between the two sides once they
   sit side by side (lg); stacked on a phone there is none. Found by itself: the left column is the one holding a vertical
   pill nav (.nav.flex-column.nav-pills) - or marked .uoc-split-end. In a card-body the line runs the body's height edge
   to edge: the column reaches over the body's top padding when its row comes first, and the bottom when it comes last. */
@media (min-width: 992px) {
    .row > :has(> .nav.flex-column.nav-pills), .uoc-split-end { border-right: 1px solid var(--bs-border-color); }
    .card-body > .row:first-child > :is(:has(> .nav.flex-column.nav-pills), .uoc-split-end) {
        margin-top: calc(-1 * var(--bs-card-spacer-y)); padding-top: var(--bs-card-spacer-y);
    }
    .card-body > .row:last-child > :is(:has(> .nav.flex-column.nav-pills), .uoc-split-end) {
        margin-bottom: calc(-1 * var(--bs-card-spacer-y)) !important; padding-bottom: var(--bs-card-spacer-y);   /* over mb-lg-0 */
    }
}
/* A WIDE TABLE WHOSE MIDDLE SCROLLS (2026-10-27, Staff Cost's months) - the table in <div class="uoc-fixcols">, one table
   (borders and all, not DataTables' split scrollX); a cell marked .fix-s stays on the left, .fix-s2 next to it (after a
   4rem first column - give that column style="width:4rem;min-width:4rem"), .fix-e on the right. Cells are already opaque
   (Bootstrap's --bs-table-bg), so the scrolled ones pass beneath. */
.uoc-fixcols { overflow-x: auto; }
.uoc-fixcols .fix-s, .uoc-fixcols .fix-s2, .uoc-fixcols .fix-e { position: sticky; z-index: 2; }
.uoc-fixcols .fix-s  { left: 0; }
/* the edge line is a box-shadow, and so is Bootstrap's hover and stripe colour - both kept, so a hovered row lights up its fixed cells too */
.uoc-fixcols .fix-s2 { left: 4rem; box-shadow: inset -1px 0 0 var(--bs-border-color), inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg))); }
.uoc-fixcols .fix-e  { right: 0; box-shadow: inset 1px 0 0 var(--bs-border-color), inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg))); }

/* THE TABS-LEFT CARD FILLS THE PAGE (2026-10-27, the global minimum) - side by side, the card with left tabs (a gap-free
   row holding .uoc-split-end) is at least as tall as the window leaves under the filters: the page's main area stretches
   its last card (or the form round it) down to the footer, and the row inside grows with it - so the list on the left
   has the whole height, however short the right side is. */
@media (min-width: 992px) {
    .app-main { display: flex; flex-direction: column; }
    .app-main > .app-content { flex: 1 0 auto; display: flex; flex-direction: column; }
    .app-main > .app-content > .container-fluid { flex: 1 0 auto; display: flex; flex-direction: column; }
    .app-content > .container-fluid > :last-child:has(.uoc-split-end) { flex: 1 0 auto; display: flex; flex-direction: column; }
    .card:has(> .row.g-0 > .uoc-split-end) { flex: 1 0 auto; }
    .card > .row.g-0:has(> .uoc-split-end) { flex: 1 0 auto; }
}

/* A TAB ON THE LEFT UNDER THE MOUSE (2026-10-27) - a light purple wash, so the card the pointer is on reads as one to
   click; the picked one keeps its solid purple */
.nav.flex-column.nav-pills .nav-link:not(.active):hover {
    background-color: rgba(96, 63, 142, .1);   /* --uoc-purple at 10% */
}

/* A ROW THAT IS A LINK (2026-10-27, uoc.js) - tr[data-href]: the pointer says so */
tr[data-href] { cursor: pointer; }

/* A LONG LIST OF TABS SCROLLS ON ITS OWN (2026-10-26) - side by side, the list stays in view as the page scrolls and
   is never taller than the window; uoc.js brings the picked one into view */
@media (min-width: 992px) {
    .row > * > .nav.flex-column.nav-pills { position: sticky; top: .75rem; max-height: calc(100vh - 1.5rem); overflow-y: auto; flex-wrap: nowrap;
                                            overscroll-behavior: contain; scrollbar-width: thin; }
    /* THE LIST IN ITS OWN CARD-BODY (2026-10-27, the tabs-left cards: a search card-body above it) - THE FULL HEIGHT OF THE
       COLUMN: the column stacks its card-bodies, the search's keeps its own height, the list's takes the rest - as tall as
       the right side - and the list fills it, scrolling inside when it is longer (never shorter than 12rem) */
    .row > :has(> .card-body > .nav.flex-column.nav-pills) { display: flex; flex-direction: column; }
    .row > :has(> .card-body > .nav.flex-column.nav-pills) > .card-body { flex: 0 0 auto; }
    .row > :has(> .card-body > .nav.flex-column.nav-pills) > .card-body:has(> .nav.flex-column.nav-pills) { flex: 1 1 0; min-height: 12rem; position: relative; }
    .row > * > .card-body > .nav.flex-column.nav-pills { position: absolute; inset: var(--bs-card-spacer-y) var(--bs-card-spacer-x);
                                                         overflow-y: auto; flex-wrap: nowrap; overscroll-behavior: contain; scrollbar-width: thin; }
}


/* A TREE IN A TABLE (2026-10-26) - rows that fold where columns must line up (a P&L): <table class="uoc-tree-table">,
   each row <tr data-tt="key" data-tt-parent="parent key" style="--tt-depth:n">, its first cell starting with
   <a href="#" class="uoc-pm tt-toggle"></a> (a branch) or <span class="uoc-pm tt-leaf"></span> (a leaf). Each level
   steps in by one + / - box; uoc.js folds the rows and remembers them, as it does a <details>. */
.uoc-tree-table > tbody > tr > :first-child { padding-left: calc(.4rem + var(--tt-depth, 0) * 1.45rem); }
.uoc-tree-table .tt-leaf::before { visibility: hidden; }
.uoc-tree-table .uoc-pm { vertical-align: middle; margin-right: .5rem; }
.uoc-tree-table .tt-toggle::before { background: var(--bs-body-bg); }   /* the box stands out on a shaded row */
.uoc-tree-table tr.tt-hidden { display: none; }
