/* ==========================================================================
   Q-Mgr Component Library
   Custom UI components with CSS variable theming
   ========================================================================== */

/* ==========================================================================
   CSS Variables - Component specific tokens
   ========================================================================== */
:root {
    /* Button */
    --q-btn-padding-y: 0.5rem;
    --q-btn-padding-x: 1rem;
    --q-btn-font-size: var(--qm-text-base);
    --q-btn-font-weight: 500;
    --q-btn-border-radius: var(--qm-radius-md);
    --q-btn-transition: all 0.2s ease;

    /* Card */
    /* 1.25rem (20px) until 2026-09-19. A card is a container for data, not a frame around it. */
    --q-card-padding: 0.875rem;
    --q-card-border-radius: var(--qm-radius-lg);
    --q-card-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    --q-card-shadow-elevated: 0 4px 12px rgba(0, 0, 0, 0.15);

    /* Input */
    --q-input-padding-y: 0.625rem;
    --q-input-padding-x: 0.875rem;
    --q-input-border-radius: var(--qm-radius-md);
    --q-input-font-size: var(--qm-text-base);

    /* Modal */
    --q-modal-backdrop-bg: rgba(0, 0, 0, 0.5);
    --q-modal-border-radius: var(--qm-radius-lg);
    --q-modal-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);

    /* Toast */
    --q-toast-border-radius: var(--qm-radius-md);
    --q-toast-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

/* ==========================================================================
   QButton Component
   ========================================================================== */
.q-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: var(--q-btn-padding-y) var(--q-btn-padding-x);
    font-size: var(--q-btn-font-size);
    font-weight: var(--q-btn-font-weight);
    line-height: 1.5;
    border: 1px solid transparent;
    border-radius: var(--q-btn-border-radius);
    cursor: pointer;
    transition: var(--q-btn-transition);
    text-decoration: none;
    white-space: nowrap;
}

/* :focus-visible, not :focus — a tap on a phone focuses the button and the ring then stayed on it
   as if it were still pressed. The keyboard still gets the ring. */
.q-btn:focus { outline: none; }
.q-btn:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px rgba(var(--qm-primary-rgb), 0.25);
}

.q-btn--block {
    width: 100%;
}

.q-btn--disabled {
    opacity: 0.65;
    cursor: not-allowed;
    pointer-events: none;
}

/* Button Sizes */
.q-btn--sm {
    padding: 0.375rem 0.75rem;
    font-size: var(--qm-text-base);
}

.q-btn--md {
    padding: var(--q-btn-padding-y) var(--q-btn-padding-x);
}

.q-btn--lg {
    padding: 0.75rem 1.5rem;
    font-size: var(--qm-text-md);
}

/* Button Variants */
/* A BUTTON OWNS ITS LABEL'S COLOUR AND SIZE (2026-10-04). A page rule like ".list li span { color: muted }" also reached the label
   (a <span>), so a primary button read as grey on the brand colour — Today's "Take register" looked disabled. Two classes beat any
   page's class-plus-elements rule; a page that genuinely restyles a button's words does it on the button. */
.q-btn .q-btn__text { color: inherit; font-size: inherit; }
.q-btn .q-btn__icon { color: inherit; }

/* A button with a shorter label for a phone (2026-10-05): <span class="q-btn__text q-btn__long">Save as draft</span><span
   class="q-btn__text q-btn__short">Draft</span>, with the full words as its aria-label. Used where a dialog footer's three
   buttons would otherwise take two rows at 390px (the welfare log form). */
.q-btn .q-btn__short { display: none; }
@media (max-width: 576px) {
    .q-btn .q-btn__long { display: none; }
    .q-btn .q-btn__short { display: inline; }
}

.q-btn--primary {
    background: var(--qm-primary);
    color: var(--qm-text-on-primary);
    border-color: var(--qm-primary);
}

.q-btn--primary:hover:not(.q-btn--disabled) {
    background: var(--qm-primary-dark);
    border-color: var(--qm-primary-dark);
}

.q-btn--secondary {
    background: var(--qm-bg-elevated);
    color: var(--qm-text-primary);
    border-color: var(--qm-border);
}

.q-btn--secondary:hover:not(.q-btn--disabled) {
    background: var(--qm-border);
}

.q-btn--success {
    background: var(--qm-success);
    color: var(--qm-text-on-primary);
    border-color: var(--qm-success);
}

.q-btn--success:hover:not(.q-btn--disabled) {
    background: var(--qm-success);
    filter: brightness(0.92);
    border-color: var(--qm-success);
}

.q-btn--danger {
    background: var(--qm-danger);
    color: var(--qm-text-on-primary);
    border-color: var(--qm-danger);
}

.q-btn--danger:hover:not(.q-btn--disabled) {
    background: var(--qm-danger);
    border-color: var(--qm-danger);
}

.q-btn--warning {
    background: var(--qm-warning);
    /* NOT --qm-text-on-primary: that one follows the tenant's brand, and this fill is amber. */
    color: var(--qm-text-on-warning);
    border-color: var(--qm-warning);
}

.q-btn--warning:hover:not(.q-btn--disabled) {
    background: var(--qm-accent-orange);
    border-color: var(--qm-accent-orange);
}

.q-btn--info {
    background: var(--qm-info);
    color: var(--qm-text-on-primary);
    border-color: var(--qm-info);
}

.q-btn--info:hover:not(.q-btn--disabled) {
    background: var(--qm-info);
    filter: brightness(0.92);
    border-color: var(--qm-info);
}

.q-btn--outline-primary {
    background: transparent;
    color: var(--qm-primary);
    border-color: var(--qm-primary);
}

.q-btn--outline-primary:hover:not(.q-btn--disabled) {
    background: var(--qm-primary);
    color: var(--qm-text-on-primary);
}

.q-btn--outline-secondary {
    background: transparent;
    color: var(--qm-text-secondary);
    border-color: var(--qm-border);
}

.q-btn--outline-secondary:hover:not(.q-btn--disabled) {
    background: var(--qm-bg-elevated);
}

.q-btn--outline-danger {
    background: transparent;
    color: var(--qm-danger);
    border-color: var(--qm-danger);
}

.q-btn--outline-danger:hover:not(.q-btn--disabled) {
    background: var(--qm-danger);
    color: var(--qm-text-on-primary);
}

.q-btn--ghost {
    background: transparent;
    color: var(--qm-text-primary);
    border-color: transparent;
}

.q-btn--ghost:hover:not(.q-btn--disabled) {
    background: var(--qm-bg-elevated);
}

.q-btn--link {
    background: transparent;
    color: var(--qm-primary);
    border-color: transparent;
    text-decoration: underline;
}

.q-btn--link:hover:not(.q-btn--disabled) {
    color: var(--qm-primary-dark);
}

/* Button Spinner */
.q-btn__spinner {
    width: 1em;
    height: 1em;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: q-spin 0.75s linear infinite;
}

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

/* ==========================================================================
   QCard Component
   ========================================================================== */
.q-card {
    background: var(--qm-bg-card);
    border-radius: var(--q-card-border-radius);
    overflow: hidden;
}

/* A card clips its children so its rounded corners stay clean. The date picker's popover and the
   (i) balloon are both placed FIXED by q-select.js now, so a scroller no longer clips them; this
   rule stays for cards only, as a fallback until the script has placed one.
   NEVER add `.q-modal__body` here again (removed 2026-10-05): a dialog body that stops scrolling
   while a picker is open pushes the footer — Save, Cancel — off the bottom of a phone. */
.q-card:has(.q-datepicker__popover),
.filter-card:has(.q-datepicker__popover),
.q-card:has(.q-info__pop),
.filter-card:has(.q-info__pop) {
    overflow: visible;
}

/* ---------------------------------------------------------------------------------------------
   QInfo — the explanation that used to sit permanently on the page. See QInfo.razor for why this
   is a popover and not a `title` tooltip.
   --------------------------------------------------------------------------------------------- */
.q-info {
    position: relative;
    display: inline-flex;
    vertical-align: middle;
    line-height: 1;
}

.q-info__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 24px, not 20: WCAG 2.2 SC 2.5.8 (Level AA) is a 24x24 floor, and this button sits on
       most page titles — it was the ONLY thing failing the mobile readiness pass, on ten of
       fourteen pages. The glyph is unchanged; only the hit area grew. */
    width: 24px;
    height: 24px;
    padding: 0;
    border: none;
    border-radius: var(--qm-radius-sm, 3px);
    background: none;
    color: var(--qm-text-muted);
    cursor: pointer;
    font-size: var(--qm-text-base);
    transition: color 0.15s, background 0.15s;
}

.q-info__btn:hover { color: var(--qm-primary); background: var(--qm-bg-secondary); }
.q-info__btn--on { color: var(--qm-primary); background: var(--qm-bg-secondary); }
.q-info__btn:focus-visible { outline: 2px solid var(--qm-primary); outline-offset: 1px; }

.q-info__pop {
    position: absolute;
    top: calc(100% + 6px);
    left: -8px;
    z-index: 1075; /* fixed at its (i) by q-select.js placeInfo (2026-10-05): above a dialog (1050), a dropdown (1060) and a row menu (1071), below toasts */
    display: block;
    width: max-content;
    max-width: min(320px, 70vw);
    padding: 10px 12px;
    border: 1px solid var(--qm-border);
    border-radius: var(--qm-radius-md, 4px);
    background: var(--qm-bg-card);
    box-shadow: var(--qm-shadow-md);
    color: var(--qm-text-secondary);
    font-size: var(--qm-text-sm);
    font-weight: 400;
    line-height: 1.5;
    text-align: left;
    white-space: normal;
}

.q-info__pop--right { left: auto; right: -8px; }
.q-info__head { display: block; margin-bottom: 3px; color: var(--qm-text-primary); font-weight: 600; }
.q-info__body { display: block; }
.q-info__body a { color: var(--qm-primary); }
/* A Hint link (QInfo Href): its own line, a 44px-tall target on a phone. */
.q-info__link { display: inline-flex; align-items: center; gap: 2px; margin-top: 6px; color: var(--qm-primary); font-weight: 600; text-decoration: none; }
.q-info__link:hover, .q-info__link:focus-visible { text-decoration: underline; }
@media (max-width: 640px) { .q-info__link { min-height: 44px; } }

/* On a phone the popover is centred rather than anchored — 320px hanging off a control near the
   right edge would run off the screen. Same treatment as the date picker's own popover. */
@media (max-width: 560px) {
    .q-info__pop,
    .q-info__pop--right {
        position: fixed;
        left: 16px;
        right: 16px;
        top: auto;
        width: auto;
        max-width: none;
    }
}

.q-card--default {
    border: 1px solid var(--qm-border);
}

.q-card--elevated {
    box-shadow: var(--q-card-shadow-elevated);
}

.q-card--outlined {
    border: 1px solid var(--qm-border);
    background: transparent;
}

.q-card--flat {
    background: var(--qm-bg-elevated);
}

.q-card--clickable {
    cursor: pointer;
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.q-card--clickable:hover {
    transform: translateY(-2px);
    box-shadow: var(--q-card-shadow-elevated);
}

/* A LONG HeaderActions MUST NOT SQUEEZE THE TITLE TO NOTHING (2026-09-19).

   `.q-card__header-text` was `flex: 1` — that is `1 1 0%`, a base size of zero — beside actions
   sized by their own content. When those actions are long (Billing Overview puts the tenant's
   whole module list in the header), the free space goes negative, both shrink, and the title has
   `min-width: 0` so it collapses to a single character column: "Current Plan" rendered as one
   letter per line, straight down the card. Seen in production.

   The header wraps now, and the title block carries a real basis, so the ACTIONS drop to a second
   line rather than the title being crushed. */
.q-card__header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 0.75rem;
    padding: var(--q-card-padding);
    border-bottom: 1px solid var(--qm-border);
}

.q-card__icon {
    font-size: var(--qm-text-xl);
    color: var(--qm-primary);
}

.q-card__header-text {
    /* A real basis, not 0%: the title keeps room for a few words before anything else gets any. */
    flex: 1 1 11rem;
    min-width: 0;
}

.q-card__title {
    margin: 0;
    font-size: var(--qm-text-md);
    font-weight: 600;
    color: var(--qm-text-primary);
}

.q-card__subtitle {
    margin: 0.25rem 0 0;
    font-size: var(--qm-text-base);
    color: var(--qm-text-muted);
}

.q-card__header-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    align-items: center;
    gap: 0.5rem;
    min-width: 0;
    max-width: 100%;
}

.q-card__body {
    padding: var(--q-card-padding);
}

.q-card--no-padding .q-card__body {
    padding: 0;
}

.q-card__footer {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: var(--q-card-padding);
    border-top: 1px solid var(--qm-border);
    background: var(--qm-bg-elevated);
}

/* ==========================================================================
   QModal Component
   ========================================================================== */
.q-modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1050;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    background: var(--q-modal-backdrop-bg);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.2s ease, visibility 0.2s ease;
}

.q-modal-backdrop--visible {
    opacity: 1;
    visibility: visible;
}

.q-modal {
    position: relative;
    width: 100%;
    /* dvh: a phone's browser bars come and go and 100vh is the TALLEST viewport, so a 100vh dialog
       ran its footer under the address bar. The vh line is the fallback for an older browser. */
    max-height: calc(100vh - 2rem);
    max-height: calc(100dvh - 2rem);
    background: var(--qm-bg-card);
    border-radius: var(--q-modal-border-radius);
    box-shadow: var(--q-modal-shadow);
    overflow: hidden;
    /* clip, not hidden (2026-10-16, calendar film): an element hidden can still be SCROLLED by script — a press or scrollIntoView
       inside a long dialog scrolled the dialog's frame, lifted its footer and left a white band. Only the body scrolls. */
    overflow: clip;
    display: flex;
    flex-direction: column;
    animation: q-modal-enter 0.2s ease;
}

@keyframes q-modal-enter {
    from {
        opacity: 0;
        transform: scale(0.95) translateY(-10px);
    }
    to {
        opacity: 1;
        transform: scale(1) translateY(0);
    }
}

.q-modal--sm { max-width: 400px; }
.q-modal--md { max-width: 560px; }
.q-modal--lg { max-width: 800px; }
.q-modal--xl { max-width: 1140px; }
.q-modal--full { max-width: calc(100vw - 2rem); max-height: calc(100vh - 2rem); max-height: calc(100dvh - 2rem); }

/* A DIALOG NEVER HIDES ITS BUTTONS (2026-10-05, Android end-user testing). The header and the footer
   keep their size and only the body scrolls. On a phone the dialog stops above the phone bar and clear
   of the home indicator, so the footer is always on screen and never level with the bar. Buttons belong
   in FooterContent, never at the foot of the body, where they scroll away (scripts/e2e/dialog-check.mjs). */
.q-modal__header,
.q-modal__footer { flex-shrink: 0; }
.q-modal__body { min-height: 0; }

@media (max-width: 768px) {
    .q-modal-backdrop { padding-bottom: max(1rem, env(safe-area-inset-bottom, 0px)); }
    body:has(.qm-mobilenav) .q-modal-backdrop { padding-bottom: calc(0.5rem + var(--qm-mobilenav-total)); }
    body:has(.qm-mobilenav) .q-modal,
    body:has(.qm-mobilenav) .q-modal--full {
        max-height: calc(100vh - 1.5rem - var(--qm-mobilenav-total));
        max-height: calc(100dvh - 1.5rem - var(--qm-mobilenav-total));
    }
}

.q-modal__header {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 1rem 1.25rem;
    border-bottom: 1px solid var(--qm-border);
}

.q-modal__icon {
    font-size: var(--qm-text-xl);
    color: var(--qm-primary);
}

.q-modal__title {
    flex: 1;
    margin: 0;
    font-size: var(--qm-text-lg);
    font-weight: 600;
    color: var(--qm-text-primary);
}

/* The dialog's own explanation (QModal Info): beside the title, read at body weight. The dialog clips its
   overflow, so a long explanation scrolls inside its popup rather than being cut off. */
.q-modal__info { margin-left: 6px; vertical-align: middle; font-weight: 400; }
.q-modal__info .q-info__pop { max-height: 60vh; overflow-y: auto; }

.q-modal__close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    background: transparent;
    border: none;
    border-radius: var(--qm-radius-sm);
    color: var(--qm-text-muted);
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease;
}

.q-modal__close:hover {
    background: var(--qm-bg-elevated);
    color: var(--qm-text-primary);
}

.q-modal__body {
    flex: 1;
    padding: 1.25rem;
    overflow-y: auto;
}

.q-modal__body--no-padding {
    padding: 0;
}

.q-modal__footer {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    /* Wrap, don't overflow. Without this a footer whose buttons are wider than the modal (an "sm"
       modal with two descriptive labels, e.g. the duplicate-visitor confirmation) pushed the
       left-most button out past the modal's edge and clipped its label — the button was still
       clickable but you could not read what it did. */
    flex-wrap: wrap;
    gap: 0.75rem;
    padding: 1rem 1.25rem;
    border-top: 1px solid var(--qm-border);
    background: var(--qm-bg-elevated);
}

/* ==========================================================================
   QInput Component
   ========================================================================== */
.q-input-wrapper {
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
}

/* The label and its QInfo on one line. The button is a SIBLING of the <label>, never a child:
   a click inside a label is re-dispatched to its first control. */
.q-field__label-row {
    display: flex;
    align-items: center;
    gap: 2px;
    min-width: 0;
}

/* A checkbox row is the control plus its info button; the label itself keeps its own gap. */
.q-checkbox-row { gap: 4px; }

.q-input__label {
    font-size: var(--qm-text-base);
    font-weight: 500;
    color: var(--qm-text-primary);
}

/* A FIELD LABEL IS SENTENCE CASE, DARK, WHEREVER THE FIELD SITS (2026-10-05). The doubled class is the .q-checkbox fix
   again: app.css (loaded last) and a dozen page blocks carry `.form-group label { text-transform: uppercase; color: muted;
   letter-spacing; display: block; margin-bottom: 8px }` at 0,1,1, which beat a bare .q-input__label / .q-select__label, so the
   same QInput read "CASE TYPE" in grey inside a .form-group and "Case type" outside one — reported on the welfare log form.
   0,2,0 settles it for every QInput, QDatePicker, QFileUpload, QSelect and QMultiSelect label; a raw <label> in a .form-group
   is left as it was. A rule that genuinely restyles one of these labels names its context (0,2,0 or more, later). */
.q-input__label.q-input__label,
.q-select__label.q-select__label {
    display: inline;
    margin-bottom: 0;
    font-size: var(--qm-text-base);
    font-weight: 500;
    color: var(--qm-text-primary);
    text-transform: none;
    letter-spacing: normal;
}

.q-input__required {
    color: var(--qm-danger);
    margin-left: 0.125rem;
}

.q-input__container {
    position: relative;
    display: flex;
    align-items: center;
}

.q-input {
    width: 100%;
    padding: var(--q-input-padding-y) var(--q-input-padding-x);
    font-size: var(--q-input-font-size);
    color: var(--qm-text-primary);
    background: var(--qm-bg-card);
    border: 1px solid var(--qm-border);
    border-radius: var(--q-input-border-radius);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.q-input::placeholder {
    color: var(--qm-text-muted);
}

.q-input:focus {
    outline: none;
    border-color: var(--qm-primary);
    box-shadow: 0 0 0 3px rgba(var(--qm-primary-rgb), 0.15);
}

.q-input--textarea {
    resize: vertical;
    min-height: 80px;
}

.q-input__container--focused .q-input {
    border-color: var(--qm-primary);
}

.q-input__icon {
    position: absolute;
    color: var(--qm-text-muted);
    pointer-events: none;
}

.q-input__icon--left {
    left: 0.875rem;
}

.q-input__icon--right {
    right: 0.875rem;
}

.q-input__container:has(.q-input__icon--left) .q-input {
    padding-left: 2.5rem;
}

.q-input__container:has(.q-input__icon--right) .q-input,
.q-input__container:has(.q-input__toggle) .q-input,
.q-input__container:has(.q-input__clear) .q-input {
    padding-right: 2.5rem;
}

.q-input__toggle,
.q-input__clear {
    position: absolute;
    right: 0.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    background: transparent;
    border: none;
    border-radius: var(--qm-radius-sm);
    color: var(--qm-text-muted);
    cursor: pointer;
}

.q-input__toggle:hover,
.q-input__clear:hover {
    background: var(--qm-bg-elevated);
    color: var(--qm-text-primary);
}

.q-input__helper {
    font-size: var(--qm-text-sm);
    color: var(--qm-text-muted);
}

/* QInput ShowCount: "312 / 2,000" under a box with a limit, at the right. */
.q-input__count {
    align-self: flex-end;
    font-size: var(--qm-text-xs);
    color: var(--qm-text-muted);
    font-variant-numeric: tabular-nums;
}
.q-input__count--full { color: var(--qm-danger); font-weight: 600; }

.q-input__error {
    font-size: var(--qm-text-sm);
    color: var(--qm-danger);
}

.q-input-wrapper--error .q-input {
    border-color: var(--qm-danger);
}

.q-input-wrapper--error .q-input:focus {
    box-shadow: 0 0 0 3px rgba(var(--qm-danger-rgb), 0.15);
}

/* Standalone error state for bare .q-input usage without the wrapper (e.g. QDatePicker) */
.q-input--error {
    border-color: var(--qm-danger);
}

.q-input--error:focus {
    box-shadow: 0 0 0 3px rgba(var(--qm-danger-rgb), 0.15);
}

.q-input-wrapper--disabled .q-input {
    background: var(--qm-bg-elevated);
    cursor: not-allowed;
    opacity: 0.7;
}

/* ==========================================================================
   QSelect Component
   ========================================================================== */
.q-select-wrapper {
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
}

.q-select__label {
    font-size: var(--qm-text-base);
    font-weight: 500;
    color: var(--qm-text-primary);
}

.q-select__required {
    color: var(--qm-danger);
    margin-left: 0.125rem;
}

.q-checkbox__required {
    color: var(--qm-danger);
    margin-left: 0.125rem;
}

.q-select__container {
    position: relative;
}

.q-select {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: var(--q-input-padding-y) var(--q-input-padding-x);
    font-size: var(--q-input-font-size);
    color: var(--qm-text-primary);
    background: var(--qm-bg-card);
    border: 1px solid var(--qm-border);
    border-radius: var(--q-input-border-radius);
    cursor: pointer;
    transition: border-color 0.15s ease;
    text-align: left;
}

.q-select:focus {
    outline: none;
    border-color: var(--qm-primary);
    box-shadow: 0 0 0 3px rgba(var(--qm-primary-rgb), 0.15);
}

.q-select__icon {
    color: var(--qm-text-muted);
}

.q-select__value {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.q-select__value--placeholder {
    color: var(--qm-text-muted);
}

.q-select__chevron {
    color: var(--qm-text-muted);
    transition: transform 0.2s ease;
}

.q-select__container--open .q-select__chevron {
    transform: rotate(180deg);
}

.q-select__dropdown {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    /* Above the modal overlay (z-index 1050 below) so the fixed-position dropdown
       (see QSelect.razor's ToggleDropdown / qSelectInterop.getDropdownPosition) still
       renders on top when it escapes a QModal's clipping overflow; below toasts (1100),
       which should stay visible above everything else. */
    z-index: 1060;
    max-height: 250px;
    background: var(--qm-bg-card);
    border: 1px solid var(--qm-border);
    border-radius: var(--q-input-border-radius);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    overflow-y: auto;
    animation: q-select-dropdown 0.15s ease;
}

@keyframes q-select-dropdown {
    from {
        opacity: 0;
        transform: translateY(-8px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.q-select__search {
    position: sticky;
    top: 0;
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.875rem;
    background: var(--qm-bg-card);
    border-bottom: 1px solid var(--qm-border);
    z-index: 1;
}

.q-select__search-icon {
    color: var(--qm-text-muted);
    font-size: var(--qm-text-base);
    flex-shrink: 0;
}

.q-select__search-input {
    flex: 1;
    min-width: 0;
    border: none;
    outline: none;
    background: transparent;
    color: var(--qm-text-primary);
    font-size: var(--qm-text-base);
    font-family: inherit;
}

.q-select__search-input::placeholder {
    color: var(--qm-text-muted);
}

.q-select__option {
    display: flex;
    align-items: center;
    /* LEFT-ALIGNED, and the trailing tick is what moves instead (see .q-select__check).
       This was `space-between`, which reads correctly for the single-select — label, then the
       tick pushed to the right edge — and wrongly for the MULTI-select, where the checkbox comes
       FIRST: the two items are the box and the label, so space-between threw the label to the far
       right and left it floating in the middle of the list. Reported 2026-09-20 against the
       timetable's teacher picker, and it was every QMultiSelect in the app. */
    justify-content: flex-start;
    padding: 0.625rem 0.875rem;
    cursor: pointer;
    transition: background 0.15s ease;
}

.q-select__option:hover {
    background: var(--qm-bg-elevated);
}

.q-select__option--selected {
    background: rgba(var(--qm-primary-rgb), 0.1);
    color: var(--qm-primary);
}

/* An option shown but not choosable (ItemDisabled, staff accounts plan phase 4b): greyed, no hover, its reason under it. */
.q-select__option-body {
    display: block;
    min-width: 0;
}

.q-select__option--disabled {
    cursor: not-allowed;
    color: var(--qm-text-muted);
}

.q-select__option--disabled:hover {
    background: transparent;
}

.q-select__option-reason {
    display: block;
    font-size: var(--qm-text-sm);
    color: var(--qm-text-muted);
    white-space: normal;
}

.q-select__option--clear {
    color: var(--qm-text-muted);
    font-size: var(--qm-text-base);
    border-bottom: 1px solid var(--qm-border);
}

/* The single-select's tick keeps its place at the right edge, now by taking the free space
   itself rather than by the row's alignment doing it. One rule, and it cannot affect a row that
   has no tick in it. */
.q-select__check {
    color: var(--qm-primary);
    margin-left: auto;
    padding-left: 10px;
}

.q-select__empty {
    padding: 1rem;
    text-align: center;
    color: var(--qm-text-muted);
    font-size: var(--qm-text-base);
}

.q-select__helper,
.q-select__error {
    font-size: var(--qm-text-sm);
}

.q-select__helper {
    color: var(--qm-text-muted);
}

.q-select__error {
    color: var(--qm-danger);
}

.q-select-wrapper--error .q-select {
    border-color: var(--qm-danger);
}

.q-select-wrapper--disabled .q-select {
    background: var(--qm-bg-elevated);
    cursor: not-allowed;
    opacity: 0.7;
}

/* ==========================================================================
   QMultiSelect Component
   ========================================================================== */
.q-multiselect {
    min-height: 42px;
    height: auto;
    flex-wrap: wrap;
}

.q-multiselect__chips {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    flex: 1;
    padding: 2px 0;
}

.q-multiselect__chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    background: rgba(var(--qm-primary-rgb), 0.1);
    color: var(--qm-primary);
    border-radius: var(--qm-radius-sm);
    font-size: var(--qm-text-base);
    max-width: 150px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.q-multiselect__chip--more {
    background: var(--qm-bg-elevated);
    color: var(--qm-text-muted);
}

.q-multiselect__chip-remove {
    cursor: pointer;
    opacity: 0.7;
    transition: opacity 0.15s ease;
    flex-shrink: 0;
}

.q-multiselect__chip-remove:hover {
    opacity: 1;
}

.q-multiselect__dropdown {
    min-width: 100%;
}

.q-multiselect__filter {
    padding: 8px;
    border-bottom: 1px solid var(--qm-border);
}

.q-multiselect__filter-input {
    width: 100%;
    padding: 8px 12px;
    border: 1px solid var(--qm-border);
    border-radius: var(--q-input-border-radius);
    font-size: var(--qm-text-base);
    outline: none;
    background: var(--qm-bg-card);
    color: var(--qm-text-primary);
}

.q-multiselect__filter-input:focus {
    border-color: var(--qm-primary);
}

.q-multiselect__options {
    max-height: 200px;
    overflow-y: auto;
}

.q-multiselect__checkbox {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    border: 2px solid var(--qm-border);
    border-radius: var(--qm-radius-sm);
    margin-right: 10px;
    flex-shrink: 0;
    transition: all 0.15s ease;
}

.q-multiselect__checkbox--checked {
    background: var(--qm-primary);
    border-color: var(--qm-primary);
    color: var(--qm-text-on-primary);
}

.q-multiselect__checkbox i {
    font-size: var(--qm-text-sm);
}

/* ==========================================================================
   QCheckbox Component
   ========================================================================== */
/* The doubled class is deliberate, not a typo.
   QCheckbox renders a <label>, and app.css plus roughly a dozen page style blocks all carry a
   generic `.form-group label { display: block; font-weight: 600; text-transform: uppercase; ... }`
   at specificity 0,1,1. A bare `.q-checkbox` is 0,1,0 and loses, so every checkbox inside a form
   group was laid out as a block — the box on one line, its label stacked underneath in bold
   uppercase, as if the person's name were a field heading. `label.q-checkbox` only ties, and a
   page's own <style> block loads after this file, so a tie still loses. Repeating the class
   reaches 0,2,0 and settles it without needing !important or a :not() chase through every page
   that has ever written a label rule. */
.q-checkbox.q-checkbox {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    cursor: pointer;
    user-select: none;

    /* Undo what those generic label rules inject. A checkbox label is a sentence the user reads,
       not a field heading. */
    margin-bottom: 0;
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    color: var(--qm-text-primary);

    /* WCAG 2.2 SC 2.5.8: a 24x24 target even for a row's label-less tick box. The box still draws at
       20px; the label around it is what is pressed. Moved here 2026-09-23 from StudentRoster, which
       carried it for its two raw checkboxes — every checkbox is this one now. */
    min-height: 1.5rem;
    min-width: 1.5rem;
}

.q-checkbox--disabled {
    cursor: not-allowed;
    opacity: 0.6;
}

.q-checkbox__input {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}

.q-checkbox__box {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.25rem;
    height: 1.25rem;
    background: var(--qm-bg-card);
    border: 2px solid var(--qm-border);
    border-radius: var(--qm-radius-sm);
    transition: all 0.15s ease;
    flex-shrink: 0;
}

.q-checkbox:hover:not(.q-checkbox--disabled) .q-checkbox__box {
    border-color: var(--qm-primary);
}

.q-checkbox--checked .q-checkbox__box {
    background: var(--qm-primary);
    border-color: var(--qm-primary);
}

.q-checkbox__icon {
    color: var(--qm-text-on-primary);
    font-size: var(--qm-text-base);
}

/* Doubled for the same reason as .q-checkbox above — the span inherits from the <label>, so the
   generic rules' uppercase and bold reach it too. */
.q-checkbox__label.q-checkbox__label {
    font-size: var(--qm-text-base);
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    color: var(--qm-text-primary);
    line-height: 1.4;
    margin-bottom: 0;
}

/* The box must not be squeezed by a long label, and must not stretch when the label wraps. */
.q-checkbox__box {
    flex: 0 0 auto;
}

/* A label that is a paragraph, not a phrase: the box sits beside the FIRST line rather than
   floating at the middle of three. */
.q-checkbox-row--top .q-checkbox.q-checkbox { align-items: flex-start; }
.q-checkbox-row--top .q-checkbox__box { margin-top: 0.1rem; }

/* The input is visually hidden, so the focus has to be drawn on what IS visible, or a keyboard user
   tabbing through a list of rows cannot see which one they are on (added 2026-09-23 with QSwitch). */
.q-checkbox__input:focus-visible + .q-checkbox__box {
    outline: 2px solid var(--qm-primary);
    outline-offset: 2px;
}

/* ==========================================================================
   QRadioGroup Component (2026-09-23) — one choice out of a few, all visible
   ========================================================================== */
.q-radio-group {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin: 0;
    padding: 0;
    border: 0;
    min-width: 0;
}
.q-radio-group--inline { flex-direction: row; flex-wrap: wrap; gap: 6px 18px; }
.q-radio-group__legend {
    padding: 0;
    margin-bottom: 4px;
    font-size: var(--qm-text-sm);
    font-weight: 600;
    color: var(--qm-text-secondary);
}
/* Doubled class for the reason given at .q-checkbox: generic `.form-group label` rules would make each
   option a bold uppercase block. */
.q-radio.q-radio {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    cursor: pointer;
    user-select: none;
    margin-bottom: 0;
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    color: var(--qm-text-primary);
    min-height: 1.5rem;          /* WCAG 2.2 SC 2.5.8, as QCheckbox */
}
/* An option that carries a sentence: the dot sits by the FIRST line, not the middle of three. */
.q-radio--described.q-radio--described { align-items: flex-start; }
.q-radio--described .q-radio__dot { margin-top: 0.15rem; }
.q-radio--disabled { cursor: not-allowed; opacity: 0.6; }
.q-radio__input { position: absolute; opacity: 0; width: 0; height: 0; }
.q-radio__dot {
    flex: 0 0 auto;
    width: 1.1rem;
    height: 1.1rem;
    border-radius: 50%;
    border: 2px solid var(--qm-border);
    background: var(--qm-bg-card);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.q-radio:hover:not(.q-radio--disabled) .q-radio__dot { border-color: var(--qm-primary); }
.q-radio--checked .q-radio__dot {
    border-color: var(--qm-primary);
    box-shadow: inset 0 0 0 3px var(--qm-bg-card);
    background: var(--qm-primary);
}
.q-radio__input:focus-visible + .q-radio__dot { outline: 2px solid var(--qm-primary); outline-offset: 2px; }
.q-radio__label.q-radio__label {
    font-size: var(--qm-text-base);
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    color: var(--qm-text-primary);
    line-height: 1.4;
    margin-bottom: 0;
}
.q-radio__label strong { font-weight: 600; }
.q-radio__desc { color: var(--qm-text-secondary); }

/* ==========================================================================
   QSwitch Component (2026-09-23) — the on/off control for a setting
   ========================================================================== */
/* Doubled class for the same reason as .q-checkbox: the generic `.form-group label` rules would
   otherwise make it a bold uppercase block. */
.q-switch.q-switch {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    cursor: pointer;
    user-select: none;
    margin-bottom: 0;
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    color: var(--qm-text-primary);
}

.q-switch--disabled { cursor: not-allowed; opacity: 0.6; }

.q-switch__input { position: absolute; opacity: 0; width: 0; height: 0; }

.q-switch__track {
    position: relative;
    flex: 0 0 auto;
    width: 2.25rem;
    height: 1.25rem;
    border-radius: 999px;
    background: var(--qm-border);
    transition: background 0.15s ease;
}

.q-switch__knob {
    position: absolute;
    top: 2px;
    left: 2px;
    width: calc(1.25rem - 4px);
    height: calc(1.25rem - 4px);
    border-radius: 50%;
    background: var(--qm-bg-card);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
    transition: transform 0.15s ease;
}

.q-switch--on .q-switch__track { background: var(--qm-primary); }
.q-switch--on .q-switch__knob { transform: translateX(1rem); }
.q-switch:hover:not(.q-switch--disabled):not(.q-switch--on) .q-switch__track { background: var(--qm-text-muted); }

.q-switch__input:focus-visible + .q-switch__track {
    outline: 2px solid var(--qm-primary);
    outline-offset: 2px;
}

.q-switch__label.q-switch__label {
    font-size: var(--qm-text-base);
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    color: var(--qm-text-primary);
    line-height: 1.4;
    margin-bottom: 0;
}

@media (prefers-reduced-motion: reduce) {
    .q-switch__track, .q-switch__knob { transition: none; }
}

/* A finger, not a pointer: on a phone the switch itself is at least the 40px floor wide and the
   whole label is the target. */
@media (max-width: 767px) {
    .q-switch.q-switch { min-height: 40px; }
}

/* ==========================================================================
   QBadge Component
   ========================================================================== */
.q-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-weight: 500;
    line-height: 1;
}

.q-badge--sm {
    padding: 0.125rem 0.5rem;
    font-size: var(--qm-text-xs);
}

.q-badge--md {
    padding: 0.25rem 0.625rem;
    font-size: var(--qm-text-sm);
}

.q-badge--lg {
    padding: 0.375rem 0.75rem;
    font-size: var(--qm-text-base);
}

.q-badge--pill {
    border-radius: 9999px;
}

.q-badge:not(.q-badge--pill) {
    border-radius: var(--qm-radius-sm);
}

.q-badge--dot {
    width: 0.5rem;
    height: 0.5rem;
    padding: 0;
    border-radius: 50%;
}

.q-badge__icon {
    font-size: 0.875em;
}

.q-badge--primary {
    background: rgba(var(--qm-primary-rgb), 0.15);
    color: var(--qm-primary);
}

.q-badge--secondary {
    background: var(--qm-bg-elevated);
    color: var(--qm-text-secondary);
}

.q-badge--success {
    background: rgba(var(--qm-success-rgb), 0.12);
    color: var(--qm-success);
}

.q-badge--danger {
    background: rgba(var(--qm-danger-rgb), 0.12);
    color: var(--qm-danger);
}

.q-badge--warning {
    background: rgba(var(--qm-warning-rgb), 0.12);
    color: var(--qm-warning);
}

.q-badge--info {
    background: rgba(var(--qm-info-rgb), 0.12);
    color: var(--qm-info);
}

.q-badge--light {
    background: var(--qm-bg-card);
    color: var(--qm-text-primary);
    border: 1px solid var(--qm-border);
}

.q-badge--dark {
    background: var(--qm-text-primary);
    color: var(--qm-bg-card);
}

/* ==========================================================================
   QSpinner Component
   ========================================================================== */
.q-spinner {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
}

.q-spinner--sm .q-spinner__border,
.q-spinner--sm .q-spinner__pulse {
    width: 1rem;
    height: 1rem;
}

.q-spinner--md .q-spinner__border,
.q-spinner--md .q-spinner__pulse {
    width: 1.5rem;
    height: 1.5rem;
}

.q-spinner--lg .q-spinner__border,
.q-spinner--lg .q-spinner__pulse {
    width: 2rem;
    height: 2rem;
}

.q-spinner--xl .q-spinner__border,
.q-spinner--xl .q-spinner__pulse {
    width: 3rem;
    height: 3rem;
}

.q-spinner__border {
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: q-spin 0.75s linear infinite;
}

.q-spinner__pulse {
    border-radius: 50%;
    background: currentColor;
    animation: q-pulse 1.5s ease-in-out infinite;
}

@keyframes q-pulse {
    0%, 100% { transform: scale(0.5); opacity: 0.5; }
    50% { transform: scale(1); opacity: 1; }
}

.q-spinner__dots {
    display: flex;
    gap: 0.25rem;
}

.q-spinner__dots span {
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
    background: currentColor;
    animation: q-dots 1.4s ease-in-out infinite both;
}

.q-spinner__dots span:nth-child(1) { animation-delay: -0.32s; }
.q-spinner__dots span:nth-child(2) { animation-delay: -0.16s; }

@keyframes q-dots {
    0%, 80%, 100% { transform: scale(0); }
    40% { transform: scale(1); }
}

.q-spinner__text {
    font-size: var(--qm-text-base);
    color: var(--qm-text-muted);
}

.q-spinner--primary { color: var(--qm-primary); }
.q-spinner--secondary { color: var(--qm-text-secondary); }
.q-spinner--light { color: var(--qm-text-on-primary); }
.q-spinner--dark { color: var(--qm-text-primary); }

/* ==========================================================================
   QIcon Component
   ========================================================================== */
.q-icon--xs { font-size: var(--qm-text-sm); }
.q-icon--sm { font-size: var(--qm-text-base); }
.q-icon--md { font-size: var(--qm-text-md); }
.q-icon--lg { font-size: var(--qm-text-xl); }
.q-icon--xl { font-size: var(--qm-text-stat); }
.q-icon--2xl { font-size: 2rem; }

/* ==========================================================================
   QToast Component
   ========================================================================== */
.q-toast-container {
    position: fixed;
    z-index: 1100;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: 1rem;
    pointer-events: none;
}

.q-toast-container--topright {
    top: 0;
    right: 0;
}

.q-toast-container--topleft {
    top: 0;
    left: 0;
}

.q-toast-container--topcenter {
    top: 0;
    left: 50%;
    transform: translateX(-50%);
}

.q-toast-container--bottomright {
    bottom: 0;
    right: 0;
}

.q-toast-container--bottomleft {
    bottom: 0;
    left: 0;
}

.q-toast-container--bottomcenter {
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
}

.q-toast {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    min-width: 300px;
    max-width: 400px;
    padding: 1rem;
    background: var(--qm-bg-card);
    border-radius: var(--q-toast-border-radius);
    box-shadow: var(--q-toast-shadow);
    pointer-events: auto;
    overflow: hidden;
    animation: q-toast-enter 0.3s ease;
}

@keyframes q-toast-enter {
    from {
        opacity: 0;
        transform: translateX(100%);
    }
    to {
        opacity: 1;
        transform: translateX(0);
    }
}

.q-toast__icon {
    flex-shrink: 0;
    font-size: var(--qm-text-xl);
}

.q-toast--success .q-toast__icon { color: var(--qm-success); }
.q-toast--error .q-toast__icon { color: var(--qm-danger); }
.q-toast--warning .q-toast__icon { color: var(--qm-warning); }
.q-toast--info .q-toast__icon { color: var(--qm-info); }

.q-toast__content {
    flex: 1;
    min-width: 0;
}

.q-toast__title {
    font-weight: 600;
    font-size: var(--qm-text-base);
    color: var(--qm-text-primary);
    margin-bottom: 0.125rem;
}

.q-toast__message {
    font-size: var(--qm-text-base);
    color: var(--qm-text-secondary);
    line-height: 1.4;
}

.q-toast__close {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    background: transparent;
    border: none;
    border-radius: var(--qm-radius-sm);
    color: var(--qm-text-muted);
    cursor: pointer;
    transition: background 0.15s ease;
}

.q-toast__close:hover {
    background: var(--qm-bg-elevated);
}

.q-toast__progress {
    position: absolute;
    bottom: 0;
    left: 0;
    height: 3px;
    background: currentColor;
    opacity: 0.3;
    animation: q-toast-progress linear forwards;
}

@keyframes q-toast-progress {
    from { width: 100%; }
    to { width: 0%; }
}

.q-toast--success .q-toast__progress { background: var(--qm-success); }
.q-toast--error .q-toast__progress { background: var(--qm-danger); }
.q-toast--warning .q-toast__progress { background: var(--qm-warning); }
.q-toast--info .q-toast__progress { background: var(--qm-info); }

/* ==========================================================================
   Pager
   ========================================================================== */
.q-pager {
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

.q-pager__btn {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 2rem;
    height: 2rem;
    padding: 0 0.5rem;
    background: var(--qm-bg-card);
    border: 1px solid var(--qm-border);
    border-radius: var(--qm-radius-md, 0.25rem);
    color: var(--qm-text-secondary);
    font-size: var(--qm-text-base);
    cursor: pointer;
    transition: all 0.15s ease;
}

.q-pager__btn:hover:not(:disabled) {
    border-color: var(--qm-primary);
    color: var(--qm-primary);
}

.q-pager__btn--active {
    background: var(--qm-primary);
    border-color: var(--qm-primary);
    color: var(--qm-text-on-primary);
}

.q-pager__btn:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

/* ==========================================================================
   Breadcrumb — inherits link color from the app's own .page-header .breadcrumb a
   rule in qm-theme.css; this just adds the separator/current-item chrome.
   ========================================================================== */
.q-breadcrumb {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}

.q-breadcrumb__separator {
    color: var(--qm-text-muted);
}

.q-breadcrumb__current {
    color: var(--qm-text-primary);
    font-weight: 500;
}

/* ==========================================================================
   QFileUpload — shared evidence/attachment widget (Student Welfare Ledger and
   any future feature needing "attach a file to a record")
   ========================================================================== */
.q-file-upload {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.q-file-upload__dropzone {
    position: relative;
    border: 2px dashed var(--qm-border);
    border-radius: var(--qm-radius-lg);
    background: var(--qm-bg-elevated);
    transition: var(--qm-transition-fast);
    min-height: 96px;
}

.q-file-upload__dropzone:hover {
    border-color: var(--qm-primary);
    background: var(--qm-primary-light);
}

.q-file-upload--error .q-file-upload__dropzone {
    border-color: var(--qm-danger);
}

.q-file-upload__input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
    z-index: 1;
}

.q-file-upload__prompt {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
    padding: 1.25rem;
    text-align: center;
    cursor: pointer;
    min-height: 96px;
}

.q-file-upload__icon {
    font-size: var(--qm-text-stat);
    color: var(--qm-primary);
}

.q-file-upload__text {
    font-size: var(--qm-text-base);
    color: var(--qm-text-primary);
}

.q-file-upload__hint {
    font-size: var(--qm-text-sm);
    color: var(--qm-text-muted);
}

.q-file-upload__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
}

.q-file-upload__item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.75rem;
    border-radius: var(--qm-radius-md);
    background: var(--qm-bg-elevated);
    font-size: var(--qm-text-base);
}

.q-file-upload__item--uploading { color: var(--qm-text-secondary); }
.q-file-upload__item--done { color: var(--qm-success); }
.q-file-upload__item--error { color: var(--qm-danger); }

.q-file-upload__filename {
    color: var(--qm-text-primary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    flex: 1;
}

.q-file-upload__status {
    flex-shrink: 0;
}

.q-file-upload__spin {
    animation: q-file-upload-spin 1s linear infinite;
}

@keyframes q-file-upload-spin {
    to { transform: rotate(360deg); }
}

/* ==========================================================================
   Utility: Visually Hidden (for accessibility)
   ========================================================================== */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ==========================================================================
   QDatePicker — the project's own calendar
   --------------------------------------------------------------------------
   Lives here rather than in a component <style> block because QDatePicker is
   used 23 times across 10 files and its popover is absolutely positioned: a
   per-component copy of these rules would ship once per page that used it.
   Everything is a --qm-* token, so both themes come for free.
   ========================================================================== */
.q-datepicker {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
    min-width: 0;
}

.q-datepicker__field {
    position: relative;
    display: flex;
    align-items: center;
    min-width: 0;
}

/* The trigger is a <button> wearing .q-input, so it inherits the exact height,
   padding, radius and focus ring every other field in the app has. */
.q-datepicker__trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    text-align: left;
    font: inherit;
    font-size: var(--q-input-font-size);
    cursor: pointer;
    min-height: 38px;
}

.q-datepicker__trigger:disabled { cursor: not-allowed; opacity: 0.6; }
.q-datepicker__trigger:hover:not(:disabled) { border-color: var(--qm-primary); }
.q-datepicker__trigger:focus-visible {
    outline: 2px solid var(--qm-primary);
    outline-offset: 1px;
}

.q-datepicker__value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.q-datepicker__value--empty { color: var(--qm-text-muted); }
.q-datepicker__icon { color: var(--qm-text-muted); flex-shrink: 0; }

.q-datepicker__clear {
    position: absolute;
    right: 30px;
    background: none;
    border: 0;
    padding: 2px;
    line-height: 1;
    cursor: pointer;
    color: var(--qm-text-muted);
    font-size: var(--qm-text-xs);
}
.q-datepicker__clear:hover { color: var(--qm-danger); }

.q-datepicker__popover {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    z-index: 1200;
    width: 268px;
    max-width: calc(100vw - 24px);
    background: var(--qm-bg-card);
    border: 1px solid var(--qm-border);
    border-radius: var(--qm-radius-lg);
    box-shadow: var(--qm-shadow-lg, 0 8px 28px rgba(0, 0, 0, 0.18));
    padding: 10px;
}

.q-datepicker__nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 4px;
    margin-bottom: 8px;
}

.q-datepicker__navbtn,
.q-datepicker__monthlabel {
    background: none;
    border: 1px solid transparent;
    border-radius: var(--qm-radius-sm);
    color: var(--qm-text-primary);
    font: inherit;
    font-size: var(--qm-text-base);
    cursor: pointer;
    padding: 5px 8px;
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.q-datepicker__monthlabel { font-weight: 600; flex: 1; justify-content: center; }
.q-datepicker__navbtn:hover,
.q-datepicker__monthlabel:hover { background: var(--qm-bg-secondary); border-color: var(--qm-border); }

.q-datepicker__grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 2px;
}

.q-datepicker__dow {
    text-align: center;
    font-size: var(--qm-text-xs);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--qm-text-muted);
    padding-bottom: 4px;
}

.q-datepicker__day {
    /* 34px is a deliberate floor: this is tapped with a thumb on a phone at a
       front desk, and a 24px cell is a mis-tap waiting to happen. */
    min-height: 34px;
    border: 1px solid transparent;
    border-radius: var(--qm-radius-sm);
    background: none;
    color: var(--qm-text-primary);
    font: inherit;
    font-size: var(--qm-text-base);
    font-variant-numeric: tabular-nums;
    cursor: pointer;
}
.q-datepicker__day:hover:not(:disabled) { background: var(--qm-bg-secondary); }
.q-datepicker__day.is-outside { color: var(--qm-text-muted); opacity: 0.55; }
.q-datepicker__day.is-today { border-color: var(--qm-primary); font-weight: 600; }
.q-datepicker__day.is-selected {
    background: var(--qm-primary);
    border-color: var(--qm-primary);
    color: var(--qm-text-on-primary);
    font-weight: 600;
}
.q-datepicker__day:disabled { opacity: 0.28; cursor: not-allowed; }
.q-datepicker__day:focus-visible { outline: 2px solid var(--qm-primary); outline-offset: -1px; }

.q-datepicker__years {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 4px;
}
.q-datepicker__year {
    padding: 8px 4px;
    border: 1px solid var(--qm-border);
    border-radius: var(--qm-radius-sm);
    background: var(--qm-bg-card);
    color: var(--qm-text-primary);
    font: inherit;
    font-size: var(--qm-text-base);
    font-variant-numeric: tabular-nums;
    cursor: pointer;
}
.q-datepicker__year:hover { border-color: var(--qm-primary); }
.q-datepicker__year.is-current {
    background: var(--qm-primary);
    border-color: var(--qm-primary);
    color: var(--qm-text-on-primary);
    font-weight: 600;
}

.q-datepicker__foot {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px solid var(--qm-border);
}
.q-datepicker__quick {
    background: none;
    border: 1px solid var(--qm-border);
    border-radius: var(--qm-radius-sm);
    color: var(--qm-text-muted);
    font: inherit;
    font-size: var(--qm-text-sm);
    padding: 4px 10px;
    cursor: pointer;
}
.q-datepicker__quick:hover:not(:disabled) { border-color: var(--qm-primary); color: var(--qm-text-primary); }
.q-datepicker__quick:disabled { opacity: 0.45; cursor: not-allowed; }
.q-datepicker__quick--close { margin-left: auto; }

/* Where the popover sits is decided by q-select.js (placePicker): fixed at its field, inside the
   window, above the phone bar and never over the footer of the dialog it is in. This block is only
   the look before the script has placed it, on a phone. */
@media (max-width: 480px) {
    .q-datepicker__popover {
        position: fixed;
        top: auto;
        bottom: 12px;
        left: 12px;
        right: 12px;
        width: auto;
        max-width: none;
    }
}

/* ==========================================================================
   QDateRangePicker
   ========================================================================== */
.q-daterange {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 12px;
    min-width: 0;
}

.q-daterange__fields {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 10px;
    min-width: 0;
}
.q-daterange__field { min-width: 140px; flex: 0 1 150px; }

/* ONE ROW, NOT TWO (2026-09-19). "From" and "To" sat as labels above the boxes, which made every
   range filter on twelve pages two rows tall to say what a dash between two dates already says.
   The labels stay in the markup — screen readers still announce "From" and "To" — and are hidden
   visually only. The presets and boxes share the row, aligned on their centres. */
.q-daterange,
.q-daterange__fields { align-items: center; }

.q-daterange__field .q-input__label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}

.q-daterange__sep { color: var(--qm-text-muted); }

/* The pair never splits (2026-09-19). Beside the presets, the fields box shrank to its min-content
   width and the second date wrapped UNDER the first — "From" on one line and "To" on the next,
   reported from the dashboard. The pair keeps its natural width and stays on one line; the PRESETS
   are what wrap. On a phone there is no room for both boxes side by side, so they may stack there. */
.q-daterange__fields { flex: 0 0 auto; flex-wrap: nowrap; }
.q-daterange__presets { flex: 1 1 260px; min-width: 0; }

@media (max-width: 576px) {
    .q-daterange__fields { flex: 1 1 100%; flex-wrap: wrap; }
}

.q-daterange__presets {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    min-width: 0;
}

.q-daterange__preset {
    padding: 5px 11px;
    border-radius: 999px;
    border: 1px solid var(--qm-border);
    background: var(--qm-bg-card);
    color: var(--qm-text-muted);
    font: inherit;
    font-size: var(--qm-text-sm);
    cursor: pointer;
    white-space: nowrap;
}
.q-daterange__preset:hover { border-color: var(--qm-primary); color: var(--qm-text-primary); }
.q-daterange__preset.is-active {
    background: var(--qm-primary);
    border-color: var(--qm-primary);
    color: var(--qm-text-on-primary);
    font-weight: 600;
}

.q-daterange__caption {
    font-size: var(--qm-text-sm);
    color: var(--qm-text-muted);
    margin: 0;
    flex-basis: 100%;
}
.q-daterange__caption strong { color: var(--qm-text-primary); font-weight: 600; }
.q-daterange__warning { color: var(--qm-warning); }

/* ==========================================================================
   Pager caption — "Showing 21–40 of 57" beside the buttons
   ========================================================================== */
.q-pager-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 14px 0 0;
    margin-top: 12px;
    border-top: 1px solid var(--qm-border);
}

.q-pager__caption {
    font-size: var(--qm-text-base);
    color: var(--qm-text-secondary);
    font-variant-numeric: tabular-nums;
}

/* Rows per page, the buttons and "Go to page" share one wrapping row on the right (2026-09-23). */
.q-pager__controls {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--q-row-gap);
    margin-left: auto;
    min-width: 0;
}

/* A QSelect in the bar reads its label beside the control, not above it. */
.q-pager-bar .q-pager__size,
.q-pager-bar .q-pager__jump {
    flex-direction: row;
    align-items: center;
    gap: 6px;
}
.q-pager-bar .q-pager__size .q-select__label,
.q-pager-bar .q-pager__jump .q-select__label {
    font-weight: 400;
    color: var(--qm-text-secondary);
    white-space: nowrap;
}
.q-pager-bar .q-pager__size .q-select__container { width: 5.25rem; }
.q-pager-bar .q-pager__jump .q-select__container { width: 5.25rem; }

/* "Page 2 of 9" replaces the page numbers on a phone only. */
.q-pager__summary {
    display: none;
    padding: 0 6px;
    font-size: var(--qm-text-base);
    color: var(--qm-text-secondary);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

@media (max-width: 640px) {
    .q-pager__controls { margin-left: 0; width: 100%; justify-content: space-between; }
    .q-pager__btn--number,
    .q-pager__btn--edge,
    .q-pager-bar .q-pager__jump { display: none; }
    .q-pager__summary { display: inline; }
    /* A finger, not a pointer: the phone floor for the pager is 44px. */
    .q-pager__btn { min-width: 44px; height: 44px; }
    .q-pager-bar .q-pager__size .q-select { min-height: 44px; }
}

/* ==========================================================================
   Timeline — lifted from StudentWelfareTimeline
   ========================================================================== */
.q-timeline {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}

.q-timeline__day {
    position: relative;
    padding: 4px 0 14px 56px;
    font-family: var(--qm-font-display);
    font-size: var(--qm-text-sm);
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--qm-text-muted);
}

.q-timeline__day::before {
    content: '';
    position: absolute;
    left: 19px;
    top: 0;
    bottom: 0;
    width: 2px;
    background: var(--qm-border);
}

.q-timeline__item {
    display: flex;
    gap: 16px;
    position: relative;
    padding-bottom: 24px;
    min-width: 0;
}

/* The connector rail runs from under the marker to the next item. */
.q-timeline__item::before {
    content: '';
    position: absolute;
    left: 19px;
    top: 40px;
    bottom: 0;
    width: 2px;
    background: var(--qm-border);
}

.q-timeline__item:last-child::before { display: none; }

.q-timeline__marker {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--qm-primary);
    color: var(--qm-text-on-primary);
    flex-shrink: 0;
    z-index: 1;
}

.q-timeline__card {
    flex: 1;
    min-width: 0;
    background: var(--qm-bg-card);
    border: 1px solid var(--qm-border);
    border-radius: var(--qm-radius-lg);
    padding: 16px;
}

.q-timeline__card--draft { border-style: dashed; }
/* An annulled record (2026-10-05): muted by COLOUR, never opacity — opacity makes the card its own layer, which trapped its
   own ⋯ menu under the phone bar. Only the record's name is struck; its chips, its (i) and its controls stay readable. */
.q-timeline__card--annulled { background: var(--qm-bg-secondary); border-style: dashed; }
.q-timeline__card--annulled .q-timeline__title > strong { text-decoration: line-through; color: var(--qm-text-muted); }
/* An annulled record's words (welfare audit U11: this rule lived in a page's own style block). */
.q-timeline__card--annulled .q-timeline__text { text-decoration: line-through; color: var(--qm-text-muted); }
.q-timeline__card--final { border-color: var(--qm-border-active, var(--qm-border)); }

.q-timeline__head {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 8px;
}

.q-timeline__title {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    min-width: 0;
    color: var(--qm-text-primary);
}

.q-timeline__date {
    font-size: var(--qm-text-sm);
    color: var(--qm-text-muted);
    white-space: nowrap;
}

.q-timeline__text {
    margin: 8px 0;
    color: var(--qm-text-primary);
    font-size: var(--qm-text-base);
    line-height: 1.5;
}

.q-timeline__meta > * {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 4px 0;
    font-size: var(--qm-text-sm);
    color: var(--qm-text-muted);
}

/* flex-wrap is the fix for the reported mobile bug: four buttons were wider than a phone, and
   because a flex child will not shrink below its content by default, the overflow widened the
   whole PAGE rather than this row (see layout.css). */
.q-timeline__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid var(--qm-border);
}

.q-timeline__more {
    display: flex;
    justify-content: center;
    padding: 4px 0 12px;
}

@media (max-width: 700px) {
    .q-timeline__item { gap: 10px; }
    .q-timeline__marker { width: 32px; height: 32px; }
    .q-timeline__item::before { left: 15px; top: 32px; }
    .q-timeline__day { padding-left: 42px; }
    .q-timeline__day::before { left: 15px; }
}

/* ==========================================================================
   Stat tiles — lifted from WelfareReports .stat-card, sized by --qm-statcard-*
   ========================================================================== */
/* `align-content: start` since 2026-09-19. A stat row sitting beside a taller card in an outer
   grid was stretched to that card's height, and its own rows stretched with it — four tiles with
   a number at the top and 200px of nothing under it, which is what Billing Overview showed. A
   tile is sized by what it says. */
.q-stat-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(var(--qm-statcard-min), 1fr));
    align-content: start;
    gap: var(--qm-statcard-gap);
    margin-bottom: var(--qm-block-gap, 12px);
}

.q-stat-row > * { min-width: 0; }

/* A NARROW TILE GIVES ITS FIGURE THE ROOM (UI pass, 2026-10-03). Six tiles across a desktop row are ~170px each, and the 32px icon
   badge beside the figure broke "417 of 417" and "79 h 45 min" over two lines, so the row's tiles stood at three heights. A tile in a
   stat row measures itself (a container) and, under 200px, drops the decorative icon; the figure is never broken mid-value. Only
   inside a row: there each tile is a 1fr grid cell, so measuring itself cannot collapse it. */
.q-stat-row > .q-stat { container-type: inline-size; }
@container (max-width: 200px) {
    .q-stat .q-stat__icon { display: none; }
    .q-stat .q-stat__value { white-space: nowrap; }
}

/* A row of tiles that SCROLLS SIDEWAYS on a phone instead of stacking into a screen of its own (the staffing overview,
   departments plan Phase 2): one row, each tile a fixed share of the width, snapping. The page itself never scrolls
   sideways — only this row does, inside its own box. */
@media (max-width: 640px) {
    .q-stat-row--scroll {
        grid-template-columns: none;
        grid-auto-flow: column;
        grid-auto-columns: minmax(150px, 44%);
        overflow-x: auto;
        scroll-snap-type: x proximity;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: thin;
        padding-bottom: 4px;
    }
    .q-stat-row--scroll > * { scroll-snap-align: start; }
}

.q-stat {
    display: block;
    padding: var(--qm-statcard-pad);
    background: var(--qm-bg-card);
    border: 1px solid var(--qm-border);
    border-radius: var(--qm-radius-lg);
    color: inherit;
    text-decoration: none;
    text-align: left;
    font: inherit;
    min-width: 0;
    transition: var(--qm-transition-fast, all 0.15s ease);
}

.q-stat--link { cursor: pointer; }

.q-stat--link:hover {
    border-color: var(--qm-primary);
    box-shadow: var(--qm-shadow-sm);
    color: inherit;
    text-decoration: none;
}

.q-stat--link:focus-visible {
    outline: 3px solid var(--qm-primary);
    outline-offset: 3px;
}

.q-stat__body {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 10px;
}

.q-stat__info {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.q-stat__value {
    font-family: var(--qm-font-display);
    font-size: var(--qm-statcard-value);
    line-height: 1.1;
    font-weight: 700;
    color: var(--qm-text-primary);
    font-variant-numeric: tabular-nums;
}

.q-stat__unit {
    font-size: var(--qm-text-base);
    font-weight: 500;
    margin-left: 4px;
}

/* The label's TEXT wraps; its (i) and arrow stay on the label's line beside it (2026-10-05: a long label dropped them
   onto a second line, unlike its neighbours). */
.q-stat__label { display: flex; align-items: center; gap: 0; min-width: 0; }
.q-stat__labeltext { min-width: 0; overflow-wrap: anywhere; }
.q-stat__label > .q-info, .q-stat__label > .q-stat__go { flex: none; }
.q-stat__label {
    font-size: var(--qm-statcard-label);
    color: var(--qm-text-muted);
    text-transform: uppercase;
    letter-spacing: .03em;
}

/* A linked tile's arrow (2026-10-04): it says the tile opens the records behind its figure; it moves on hover. */
.q-stat__go { margin-left: 2px; font-size: 1.25em; line-height: 1; vertical-align: middle; color: var(--qm-text-muted); transition: transform .15s ease; }
.q-stat--link:hover .q-stat__go, .q-stat--link:focus-within .q-stat__go { color: var(--qm-primary); transform: translateX(2px); }
@media (prefers-reduced-motion: reduce) { .q-stat__go { transition: none; } }

/* A tile's Hint: the QInfo balloon sits on the label's line, not in capitals. On a stretched tile it sits above the link. */
.q-stat__hint { margin-left: 4px; text-transform: none; letter-spacing: normal; vertical-align: middle; position: relative; z-index: 2; }

/* A linked tile with a Hint (2026-10-03): the figure is the link, stretched over the card, so the whole tile still opens. */
.q-stat--stretch { position: relative; }
.q-stat__stretch { color: inherit; text-decoration: none; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.q-stat__stretch::after { content: ""; position: absolute; inset: 0; z-index: 1; border-radius: inherit; }
.q-stat__stretch:hover, .q-stat__stretch:focus { color: inherit; text-decoration: none; }
.q-stat__stretch:focus-visible { outline: none; }
.q-stat--stretch:focus-within:has(.q-stat__stretch:focus-visible) { outline: 3px solid var(--qm-primary); outline-offset: 3px; }

.q-stat__sub {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    align-self: flex-start;
    margin-top: 6px;
    padding: 3px 9px;
    border-radius: var(--qm-radius-full);
    font-size: var(--qm-text-sm);
    max-width: 100%;
    min-width: 0;
    white-space: nowrap;
}
/* ONE line (2026-10-03): a caption that wraps makes its tile, and the row, taller than its neighbours. The sentence behind it
   belongs in the tile's Hint. */
.q-stat__subtext { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }

/* A neutral note is information, not a status: plain muted text under the figure, never a pill that reads like a
   button (2026-10-01). Only a real rise or fall (positive/negative) keeps its tinted chip. */
.q-stat__sub--neutral { color: var(--qm-text-muted); background: none; padding-left: 0; padding-right: 0; border-radius: 0; }
.q-stat__sub--positive { color: var(--qm-success); background: rgba(var(--qm-success-rgb), 0.1); }
.q-stat__sub--negative { color: var(--qm-danger); background: rgba(var(--qm-danger-rgb), 0.1); }

.q-stat__icon {
    width: var(--qm-statcard-icon);
    height: var(--qm-statcard-icon);
    border-radius: var(--qm-radius-lg);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    font-size: var(--qm-statcard-icon-font);
    background: var(--qm-bg-elevated);
    color: var(--qm-text-secondary);
}

/* A variant colours the figure and tints the icon badge. default/secondary stay neutral. */
.q-stat--primary .q-stat__value { color: var(--qm-primary); }
.q-stat--primary .q-stat__icon { background: rgba(var(--qm-primary-rgb), 0.12); color: var(--qm-primary); }
.q-stat--success .q-stat__value { color: var(--qm-success); }
.q-stat--success .q-stat__icon { background: rgba(var(--qm-success-rgb), 0.12); color: var(--qm-success); }
.q-stat--warning .q-stat__value { color: var(--qm-warning); }
.q-stat--warning .q-stat__icon { background: rgba(var(--qm-warning-rgb), 0.12); color: var(--qm-warning); }
.q-stat--danger .q-stat__value { color: var(--qm-danger); }
.q-stat--danger .q-stat__icon { background: rgba(var(--qm-danger-rgb), 0.12); color: var(--qm-danger); }
.q-stat--info .q-stat__value { color: var(--qm-info); }
.q-stat--info .q-stat__icon { background: rgba(var(--qm-info-rgb), 0.12); color: var(--qm-info); }

/* On paper a tile is a ruled box with black figures: no card fill, no tinted icon badge, never split
   across a page. A tint prints as an indistinguishable grey on an office mono printer. */
@media print {
    .q-stat { background: none !important; box-shadow: none !important; border: 1px solid #999; break-inside: avoid; page-break-inside: avoid; }
    .q-stat__value, .q-stat__label { color: #000 !important; }
    .q-stat__sub { background: none !important; color: #333 !important; padding-left: 0; }
    .q-stat__icon { display: none; }
}

/* ==========================================================================
   Bar list — lifted from WelfareReports .bar-list
   ========================================================================== */
.q-bars {
    display: flex;
    flex-direction: column;
    gap: 10px;
    min-width: 0;
}

.q-bars__row {
    display: grid;
    grid-template-columns: 1fr 2fr auto;
    align-items: center;
    gap: 10px;
    min-width: 0;
}

.q-bars--suffixed .q-bars__row { grid-template-columns: 1fr 2fr auto auto; }

.q-bars__label {
    font-size: var(--qm-text-base);
    color: var(--qm-text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

/* The link is never wider than the label it sits in (2026-10-14): a long name is cut short with "…", and the box a press
   lands on is the visible text — it ran past the clipped label, so a press on its middle hit the row instead. */
.q-bars__label a { color: inherit; text-decoration: none; display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: bottom; }
.q-bars__label a:hover { color: var(--qm-primary); text-decoration: underline; }
.q-bars__label em { color: var(--qm-text-muted); font-style: normal; margin-left: 4px; }

.q-bars__track {
    height: 8px;
    background: var(--qm-bg-secondary);
    border-radius: var(--qm-radius-full);
    overflow: hidden;
    min-width: 0;
}

.q-bars__fill {
    height: 100%;
    background: var(--qm-primary);
    border-radius: var(--qm-radius-full);
    transition: width 0.3s ease;
}

.q-bars__count {
    font-size: var(--qm-text-base);
    font-weight: 700;
    color: var(--qm-text-primary);
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.q-bars__suffix {
    font-size: var(--qm-text-xs);
    color: var(--qm-text-muted);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

.q-bars__empty {
    font-size: var(--qm-text-sm);
    color: var(--qm-text-muted);
    margin: 0 0 10px;
}

@media (max-width: 480px) {
    .q-bars__row { grid-template-columns: minmax(0, 1fr) auto; }
    .q-bars__track { grid-column: 1 / -1; }
    .q-bars--suffixed .q-bars__row { grid-template-columns: minmax(0, 1fr) auto auto; }
}

/* ==========================================================================
   Tabs — lifted from WelfareReports .tab-strip. Scrolls sideways, never wraps.
   ========================================================================== */
/* The tab strip is a CONTROL and sits on the control scale like every other one. It did not
   until 2026-09-19: 48px-tall buttons (10px/18px padding, 14px) under a 20px margin came to 69px,
   the single largest fixed cost on a hub page — and since the hubs, that is eleven pages. Tabs
   scroll, they never wrap: a wrapped tab strip reads as broken in a way a wrapped button row
   does not. */
.q-tabs {
    display: flex;
    flex-wrap: nowrap;
    gap: 2px;
    border-bottom: 1px solid var(--qm-border);
    margin-bottom: var(--qm-block-gap, 12px);
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
    min-width: 0;
}

.q-tabs > * { flex-shrink: 0; }

.q-tabs__tab {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: var(--q-control-h-sm, 30px);
    padding: 6px 13px;
    border: none;
    border-bottom: 2px solid transparent;
    background: none;
    color: var(--qm-text-muted);
    font: inherit;
    font-size: var(--q-control-font-sm, 13px);
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
    transition: var(--qm-transition-fast, color 0.15s ease);
}

.q-tabs__tab:hover { color: var(--qm-text-primary); }
.q-tabs__tab:focus-visible { outline: 2px solid var(--qm-primary); outline-offset: -2px; }

.q-tabs__tab.active {
    color: var(--qm-primary);
    border-bottom-color: var(--qm-primary);
}

/* ==========================================================================
   QCount — the ONE count pill (MOBILE_AND_SHELL_AUDIT §6.1). Replaced .nav-badge,
   .notification-badge, .qm-mobilenav__badge, .q-tabs__badge, the bulk bar's count and the
   dashboard's .queue-badge: five hand-rolled pills that each centred (or failed to) differently.
   The text is centred by the box, never by line-height or padding, so only a rule that changes
   its `display` can break it — which is why no stylesheet may select `.nav-link span` any more
   (nav-label-check.mjs), and why a component <style> may not restyle it (style-leak-check.mjs).
   ========================================================================== */
.q-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    height: var(--q-count-h, 18px);
    min-width: var(--q-count-h, 18px);
    padding: 0 .4em;
    margin: 0;
    border-radius: 999px;
    font: 700 var(--qm-text-xs)/1 var(--qm-font-primary);
    font-variant-numeric: tabular-nums lining-nums;
    letter-spacing: 0;
    white-space: nowrap;
    vertical-align: middle;
    text-align: center;
    flex: 0 0 auto;
    background: var(--qm-primary);
    color: var(--qm-text-on-primary);
}

.q-count--sm { --q-count-h: 16px; }

/* Red is dark in both themes, so the digits are white in both — never the brand's foreground. */
.q-count--danger { background: var(--qm-danger); color: var(--qm-text-on-danger); }

.q-count--muted { background: var(--qm-bg-secondary); color: var(--qm-text-secondary); }

/* Pinned to a positioned parent's corner (the bell, a phone-bar slot). */
.q-count--float { position: absolute; top: 2px; right: 2px; }

/* A new number bumps ONCE (QCount re-keys the element on change). Never an endless pulse, and
   nothing at all for somebody who asked for less motion. */
@media (prefers-reduced-motion: no-preference) {
    .q-count--bump { animation: q-count-bump .32s ease-out 1; }
}

@keyframes q-count-bump {
    0% { transform: scale(1); }
    40% { transform: scale(1.25); }
    100% { transform: scale(1); }
}

@media (max-width: 576px) {
    .q-tabs__tab { padding: 12px 14px; min-height: 44px; }
}

/* ==========================================================================
   Chip — status / removable filter / attachment link
   ========================================================================== */
.q-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    max-width: 100%;
    border-radius: var(--qm-radius-full);
    background: var(--qm-bg-secondary);
    color: var(--qm-text-muted);
    text-decoration: none;
    line-height: 1.4;
    vertical-align: middle;
}

.q-chip__text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.q-chip--md { font-size: var(--qm-text-sm); padding: 3px 10px; }

.q-chip--sm {
    font-size: var(--qm-text-xs);
    padding: 2px 7px;
    text-transform: uppercase;
    letter-spacing: .03em;
    font-weight: 600;
}

.q-chip--muted { background: var(--qm-bg-secondary); color: var(--qm-text-muted); }
.q-chip--primary { background: rgba(var(--qm-primary-rgb), 0.12); color: var(--qm-primary); }
.q-chip--success { background: rgba(var(--qm-success-rgb), 0.12); color: var(--qm-success); }
.q-chip--warning { background: rgba(var(--qm-warning-rgb), 0.12); color: var(--qm-warning); }
.q-chip--danger  { background: rgba(var(--qm-danger-rgb), 0.12); color: var(--qm-danger); }
.q-chip--info    { background: rgba(var(--qm-info-rgb), 0.12); color: var(--qm-info); }

/* A code chip (a subject, department or reference code): the row's own font, bold, with aligned digits — never monospace (2026-10-04). */
.q-chip--mono { font-weight: 600; font-variant-numeric: tabular-nums; }

/* An attachment or other link: bordered, and lights up on hover like a control. */
.q-chip--link {
    border-radius: var(--qm-radius-md);
    background: var(--qm-bg-elevated);
    border: 1px solid var(--qm-border);
    color: var(--qm-text-secondary);
    padding: 4px 10px;
}

.q-chip--link:hover {
    border-color: var(--qm-primary);
    color: var(--qm-primary);
}

/* A removable filter chip reads as a small statement with a dismiss control. */
.q-chip--removable {
    background: var(--qm-primary-light);
    border: 1px solid var(--qm-border);
    color: var(--qm-text-secondary);
    border-radius: var(--qm-radius-md);
    padding: 6px 8px 6px 12px;
    font-size: var(--qm-text-base);
    text-transform: none;
    letter-spacing: 0;
    font-weight: 400;
}

.q-chip--removable strong { color: var(--qm-text-primary); }

.q-chip__remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    margin-left: 2px;
    border: none;
    border-radius: var(--qm-radius-full);
    background: transparent;
    color: var(--qm-primary);
    cursor: pointer;
    flex-shrink: 0;
}

.q-chip__remove:hover { background: rgba(var(--qm-primary-rgb), 0.12); }
.q-chip__remove:focus-visible { outline: 2px solid var(--qm-primary); outline-offset: 1px; }

/* ==========================================================================
   Empty state — lifted from app.css .empty-state
   ========================================================================== */
/* AN EMPTY STATE IS ONE LINE OF NEWS, NOT A POSTER (2026-09-19, "those empty states are
   unnecessarily big. compact them"). It was 60px of padding round a 64px icon and a 20px title —
   about 230px to say "nothing here yet", often inside a card whose header already said what would be
   here. The sizes are tokens (layout.css, --qm-empty-*) shared with the older `.empty-state` class
   in app.css, so both families match — and a page never sets its own: 50 components used to, which
   is how every empty state in the app came to be a different size. */
.q-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 4px;
    padding: var(--qm-empty-pad);
    background: var(--qm-bg-card);
    border: 1px solid var(--qm-border);
    border-radius: var(--qm-radius-lg);
    color: var(--qm-text-muted);
}

.q-empty .q-empty__icon {
    font-size: var(--qm-empty-icon);
    line-height: 1;
    color: var(--qm-text-muted);
    opacity: 0.5;
    margin-bottom: 2px;
}

.q-empty__title {
    margin: 0;
    font-size: var(--qm-text-md);
    font-family: var(--qm-font-display);
    color: var(--qm-text-primary);
}

.q-empty__text {
    margin: 0;
    font-size: var(--qm-text-sm);
    color: var(--qm-text-muted);
    max-width: 90ch;
}

.q-empty__action {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--q-row-gap, 8px);
    margin-top: 8px;
}


/* THE ICON SITS BESIDE THE HEADING, NOT ABOVE IT. Stacked, an empty state was four rows tall — icon,
   title, sentence, button — and a sentence that wrapped made it taller still (157–178px measured).
   Beside, the icon spans the title and the sentence, so the whole thing is the height of its text.
   A grid rather than a flex row because the button, when there is one, spans the full width below. */
.q-empty {
    display: grid;
    grid-template-columns: auto minmax(0, max-content);
    justify-content: center;
    align-items: center;
    column-gap: 12px;
    row-gap: 2px;
    text-align: left;
}

.q-empty .q-empty__icon {
    grid-row: span 2;
    align-self: center;
    margin: 0;
}

.q-empty__title,
.q-empty__text {
    grid-column: 2;
}

/* The button sits on the SAME row, to the right — a button underneath added a whole row. A phone
   has no room beside, so there it goes underneath. */
.q-empty__action {
    grid-column: 3;
    grid-row: 1 / span 2;
    margin: 0 0 0 8px;
}

@media (max-width: 576px) {
    .q-empty__action { grid-column: 1 / -1; grid-row: auto; justify-self: center; margin: 6px 0 0; }
}

/* Inside a card the card already frames it: no second border, and less padding. */
.q-card .q-empty { border: 0; background: transparent; padding: 12px 8px; }

.q-empty--compact { padding: 12px 8px; }
.q-empty--compact .q-empty__icon { font-size: var(--qm-text-xl); }

/* ==========================================================================
   Activity log — generalised from the Document Library's activity modal
   ========================================================================== */
.q-activity { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.q-activity__tiles { margin-bottom: 0; }

.q-activity__note {
    margin: 0;
    font-size: var(--qm-text-base);
    color: var(--qm-text-secondary);
    line-height: 1.5;
}

.q-activity__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}

.q-activity__title {
    margin: 0;
    font-family: var(--qm-font-display);
    font-size: var(--qm-text-md);
    color: var(--qm-text-primary);
}

.q-activity__loading { display: flex; justify-content: center; padding: 2rem; }

.q-activity__table-wrap { overflow-x: auto; }

.q-activity__table { width: 100%; border-collapse: collapse; font-size: var(--qm-text-base); }

.q-activity__table th,
.q-activity__table td {
    text-align: left;
    padding: .4rem .5rem;
    border-bottom: 1px solid var(--qm-border);
    white-space: nowrap;
    color: var(--qm-text-primary);
}

.q-activity__table th { color: var(--qm-text-secondary); font-weight: 600; }
.q-activity__when { font-variant-numeric: tabular-nums; }
.q-activity__where { color: var(--qm-text-secondary); max-width: 320px; overflow: hidden; text-overflow: ellipsis; }
.q-activity__row--fail td { color: var(--qm-danger); }

/* ==========================================================================
   Rating — lifted from FeedbackPage .star-rating
   ========================================================================== */
.q-rating { display: flex; flex-direction: column; align-items: center; gap: 12px; }

.q-rating__stars {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 12px;
}

.q-rating--sm .q-rating__stars { gap: 8px; }

.q-rating__star {
    width: 64px;
    height: 64px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--qm-bg-elevated);
    border: 2px solid var(--qm-border);
    border-radius: var(--qm-radius-xl);
    cursor: pointer;
    padding: 0;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.q-rating__star .q-icon {
    font-size: 36px;
    color: var(--qm-text-muted);
    transition: all 0.3s ease;
}

.q-rating--sm .q-rating__star { width: 48px; height: 48px; }
.q-rating--sm .q-rating__star .q-icon { font-size: var(--qm-text-stat); }

.q-rating__star:hover,
.q-rating__star.hover {
    background: rgba(var(--qm-warning-rgb), 0.1);
    border-color: rgba(var(--qm-warning-rgb), 0.3);
    transform: scale(1.1);
}

.q-rating__star:hover .q-icon,
.q-rating__star.hover .q-icon { color: var(--qm-warning); }

.q-rating__star.active {
    background: rgba(var(--qm-warning-rgb), 0.15);
    border-color: var(--qm-warning);
}

/* Kept on purpose: the conventional "star lights up" affordance on a rating widget (CLAUDE.md). */
.q-rating__star.active .q-icon {
    color: var(--qm-warning);
    text-shadow: 0 0 20px rgba(var(--qm-warning-rgb), 0.5);
}

.q-rating__star:focus-visible { outline: 3px solid var(--qm-primary); outline-offset: 2px; }

.q-rating--readonly .q-rating__star { cursor: default; }
.q-rating--readonly .q-rating__star:hover { transform: none; background: var(--qm-bg-elevated); border-color: var(--qm-border); }
.q-rating--readonly .q-rating__star:hover .q-icon { color: var(--qm-text-muted); }
.q-rating--readonly .q-rating__star.active:hover { background: rgba(var(--qm-warning-rgb), 0.15); border-color: var(--qm-warning); }
.q-rating--readonly .q-rating__star.active:hover .q-icon { color: var(--qm-warning); }

.q-rating__label {
    text-align: center;
    font-size: var(--qm-text-md);
    font-weight: 600;
    color: var(--qm-primary);
    min-height: 24px;
}

@media (max-width: 576px) {
    .q-rating__star { width: 52px; height: 52px; }
    .q-rating__star .q-icon { font-size: var(--qm-text-stat); }
    .q-rating--sm .q-rating__star { width: 42px; height: 42px; }
    .q-rating--sm .q-rating__star .q-icon { font-size: var(--qm-text-xl); }
}

/* ==========================================================================
   Filter bar — lifted from WelfareReports .filter-bar
   ========================================================================== */
/* Density (2026-09-19): gap 14 and margin 20 put a one-row filter bar at 58px for 36px of
   control. The controls were already on the scale; it was the space around them that cost. */
/* `.filter-row` is the same object under an older name: five report pages write the markup by
   hand rather than using QFilterBar, and each carried its own `display:flex; gap:20px` copy. One
   rule for both, so the two shapes cannot drift; new filter rows use QFilterBar. */
.q-filter-bar,
.filter-row {
    display: flex;
    align-items: flex-end;
    flex-wrap: wrap;
    gap: var(--q-row-gap, 8px) 10px;
    margin-bottom: var(--qm-block-gap, 12px);
    min-width: 0;
}

.q-filter-bar--wrap { align-items: center; }

.q-filter-bar__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-left: auto;
}

/* ==========================================================================
   Avatar — initials on a colour from the name, or a photograph
   ========================================================================== */
.q-avatar {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    overflow: hidden;
    border-radius: var(--qm-radius-lg);
    background: var(--qm-primary);
    color: var(--qm-text-on-primary);
    font-family: var(--qm-font-display);
    font-weight: 600;
    line-height: 1;
    user-select: none;
}

.q-avatar--sm { width: 28px; height: 28px; font-size: var(--qm-text-xs); border-radius: var(--qm-radius-md); }
.q-avatar--md { width: 40px; height: 40px; font-size: var(--qm-text-base); }
.q-avatar--lg { width: 56px; height: 56px; font-size: var(--qm-text-xl); }

/* Over the initials, not beside them: the initials are the fallback an expired signed link
   drops back to when onerror removes this element. */
.q-avatar__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }

/* ==========================================================================
   Stacked tables (2026-09-16) — a `table.q-stack` becomes a list of cards on a
   phone. Labels come from the table's own header via js/qStackTable.js, which
   stamps data-label on each cell. The first cell leads the card unlabelled;
   an empty header (an actions column) gets no label and its buttons wrap.
   Desktop is untouched.
   ========================================================================== */
@media (max-width: 640px) {
    table.q-stack,
    table.q-stack > tbody,
    table.q-stack > tbody > tr,
    table.q-stack > tbody > tr > td {
        display: block;
        width: 100%;
    }

    table.q-stack > thead { display: none; }

    table.q-stack > tbody > tr {
        margin: 0 0 10px;
        padding: 10px 12px;
        border: 1px solid var(--qm-border);
        border-radius: var(--qm-radius-lg);
        background: var(--qm-bg-card);
    }

    /* A labelled cell is a BLOCK with its label floated left, never a flex row (2026-10-02): a cell
       holding text AND chips ("S2D · Information and Communication Technology" + "Double" + "Timetabled")
       made every text run and chip its own flex item, and the text was squeezed to one letter a line.
       Floated, the cell's content flows inline to the right of the label and wraps under it. */
    table.q-stack > tbody > tr > td {
        display: flow-root;
        padding: 5px 0 !important;
        border: 0 !important;
        text-align: right;
        min-width: 0;
        overflow-wrap: anywhere;
    }

    table.q-stack > tbody > tr > td::before {
        content: attr(data-label);
        float: left;
        width: 38%;
        margin-right: 12px;
        text-align: left;
        font-size: var(--qm-text-xs);
        font-weight: 600;
        letter-spacing: .04em;
        text-transform: uppercase;
        color: var(--qm-text-muted);
        padding-top: 2px;
    }

    table.q-stack > tbody > tr > td:first-child {
        display: block;
        text-align: left;
        padding-bottom: 8px !important;
        margin-bottom: 4px;
        border-bottom: 1px solid var(--qm-border) !important;
        font-size: var(--qm-text-md);
    }

    table.q-stack > tbody > tr > td:first-child::before,
    table.q-stack > tbody > tr > td[data-label=""]::before { display: none; }

    table.q-stack > tbody > tr > td[data-label=""] {
        display: flex;
        gap: 12px;
        justify-content: flex-start;
        flex-wrap: wrap;
        text-align: left;
    }

    table.q-stack > tbody > tr > td:last-child:not(:first-child) { padding-bottom: 0 !important; }

    /* .data-table .row-actions is `width: 1%` so the column hugs its buttons on a desktop. That
       rule is two classes deep and beats the stacked `width: 100%` above, so on a phone the cell
       collapsed to about 3px and its buttons with it — found by the mobile readiness pass, at 3px
       against a 24px floor. Stacked, the actions are a full-width row of their own. */
    table.q-stack > tbody > tr > td.row-actions {
        width: 100% !important;
        display: flex;
        justify-content: flex-start;
        flex-wrap: wrap;
        gap: 6px;
        text-align: left;
    }

    /* The card list sits on the page; the table's own card and scroller are not needed. */
    .table-wrap:has(> table.q-stack),
    .q-card:has(table.q-stack) > .q-card__body { overflow: visible; }
    .q-card--no-padding:has(table.q-stack),
    .q-card:has(> .q-card__body > .table-wrap > table.q-stack) { background: transparent; border: 0; box-shadow: none; }
}

/* ==========================================================================
   Phone polish for filters and tabs (2026-09-16)
   A filter bar WRAPS on a phone — two controls a row, a date range full width —
   rather than scrolling sideways, which clipped the last preset and put a
   dropdown inside a scroller. Tabs still scroll (a wrapped tab strip reads as
   broken) but without a visible scrollbar, with a fade to say there is more.
   ========================================================================== */
@media (max-width: 640px) {
    .q-filter-bar.filter-bar {
        flex-wrap: wrap !important;
        overflow-x: visible !important;
        gap: 10px;
        align-items: stretch;
    }
    .q-filter-bar.filter-bar > * {
        flex: 1 1 calc(50% - 5px) !important;
        min-width: 0 !important;
        max-width: 100%;
    }
    .q-filter-bar.filter-bar > .q-daterange,
    .q-filter-bar.filter-bar > .q-input-wrapper:has(input[type="search"]),
    .q-filter-bar.filter-bar > [class*="search"] {
        flex-basis: 100% !important;
    }
    /* A tab strip sharing a bar with a select was squeezed to half a phone ("By t…", found on the timetable
       2026-09-17): the tabs take their own row, and still scroll inside it when there are more. */
    .q-filter-bar.filter-bar > .q-tabs {
        flex-basis: 100% !important;
    }

    .q-daterange__fields { flex-wrap: wrap; }
    .q-daterange__presets {
        flex-wrap: nowrap;
        overflow-x: auto;
        scrollbar-width: none;
        -webkit-mask-image: linear-gradient(to right, #000 88%, transparent);
        mask-image: linear-gradient(to right, #000 88%, transparent);
        padding-right: 24px;
    }
    .q-daterange__presets::-webkit-scrollbar { display: none; }
    .q-daterange__presets > * { flex-shrink: 0; }

    .q-tabs {
        scrollbar-width: none;
        -webkit-mask-image: linear-gradient(to right, #000 90%, transparent);
        mask-image: linear-gradient(to right, #000 90%, transparent);
    }
    .q-tabs::-webkit-scrollbar { display: none; }

    /* Tap targets: a small phone button is drawn at 32px like the rest (see the control scale below). */
    .qm-main .q-btn--sm { min-height: 32px; }
}

/* QBarList Stacked: label and figure share a line, the bar spans beneath (a narrow card's layout). A FLEX row that wraps, never a
   grid: in a grid the figure's column grew to its whole length and left the label one letter wide, broken mid-word on a phone
   ("Le/ss/on/At/te/nd/an/ce" beside "100 / 100 · 1 period (2 not counted yet) · weight 3", 2026-10-09). The label keeps a readable
   width and breaks only between words; a figure that no longer fits beside it goes under it, right-aligned, then the bar. */
.q-bars--stacked .q-bars__row { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 10px; row-gap: 4px; }
.q-bars--stacked .q-bars__label { order: 1; flex: 1 1 auto; min-width: min(100%, 9rem); white-space: normal; overflow-wrap: normal; word-break: normal; }
.q-bars--stacked .q-bars__count { order: 2; flex: 0 1 auto; margin-left: auto; text-align: right; font-weight: 500; color: var(--qm-text-secondary); }
.q-bars--stacked .q-bars__suffix { order: 2; }
.q-bars--stacked .q-bars__track { order: 3; flex: 1 0 100%; }
.q-bars--stacked .q-bars__row + .q-bars__row { margin-top: 6px; }

/* ---- Date + time on one field ----------------------------------------------------------------------
   QDatePicker is either a calendar (date) or a native time input (ShowTime), never both. A field that
   needs a moment pairs the two, bound to the same DateTime? — each half keeps the other's part
   (PickDay keeps the time, HandleTimeChanged keeps the date). Found 2026-09-17: five Staff Performance
   fields used ShowTime alone, so a duty could only be scheduled today and a record never backdated. */
.q-datetime-pair { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 8px; align-items: end; min-width: 0; }

/* ==========================================================================
   QWeekGrid — rows × columns (duty rota plan §9)
   The grid scrolls inside its own box with the day header and period column sticky; below 640px the
   table is hidden and the per-day list shown. State is a border plus an icon as well as a tint, so a
   conflict still reads on a mono printer.
   ========================================================================== */
.q-weekgrid { min-width: 0; position: relative; }
/* "There is more this way": layout.js marks a grid whose scroller has more to the right or left (2026-10-05), and a fade
   on that edge says so. On the grid, not the scroller, so it stays put while the grid scrolls. */
/* Only the right edge: the period column on the left is sticky, and a fade over it would hide the labels. */
.q-weekgrid[data-more-right]::after {
    content: ""; position: absolute; top: 1px; bottom: 1px; right: 1px; width: 32px; pointer-events: none; z-index: 4;
    border-radius: 0 var(--qm-radius-md) var(--qm-radius-md) 0;
    background: linear-gradient(to left, rgba(var(--qm-primary-rgb), .22), transparent);
}
@media print { .q-weekgrid::after { display: none !important; } }
.q-weekgrid__scroll {
    overflow: auto;
    border: 1px solid var(--qm-border);
    border-radius: var(--qm-radius-md);
    background: var(--qm-bg-card);
    max-width: 100%;
}
.q-weekgrid__table { border-collapse: separate; border-spacing: 0; width: 100%; min-width: max-content; font-size: var(--qm-text-base); }
.q-weekgrid__table th, .q-weekgrid__table td { border-right: 1px solid var(--qm-border); border-bottom: 1px solid var(--qm-border); }
.q-weekgrid__table tr > :last-child { border-right: none; }
.q-weekgrid__table tbody tr:last-child > * { border-bottom: none; }

.q-weekgrid__colhead, .q-weekgrid__corner {
    position: sticky; top: 0; z-index: 2;
    background: var(--qm-bg-elevated);
    padding: 8px 10px; text-align: left; white-space: nowrap;
    font-family: var(--qm-font-display); font-size: var(--qm-text-sm); font-weight: 600; color: var(--qm-text-primary);
}
.q-weekgrid__corner { left: 0; z-index: 3; color: var(--qm-text-muted); font-weight: 500; }
/* A row heading holding an open menu sits above the rows after it (2026-10-02): each sticky heading is its own stacking
   context, so the rota menu opened UNDER the next rota's heading and its first item could not be pressed. */
.q-weekgrid__rowhead:has(.q-rowmenu__panel) { z-index: 20; }
.q-weekgrid__colhead--today { box-shadow: inset 0 -2px 0 var(--qm-primary); color: var(--qm-primary); }
.q-weekgrid__rowhead {
    position: sticky; left: 0; z-index: 1;
    background: var(--qm-bg-elevated);
    padding: 6px 10px; text-align: left; white-space: nowrap; vertical-align: top;
    font-size: var(--qm-text-sm); font-weight: 600; color: var(--qm-text-primary);
}
.q-weekgrid__head-label { display: block; }
.q-weekgrid__head-sub { display: block; font-size: var(--qm-text-xs); font-weight: 400; color: var(--qm-text-muted); font-variant-numeric: tabular-nums; }

.q-weekgrid__cell {
    position: relative;
    min-width: 120px; height: 56px; padding: 5px 7px;
    vertical-align: top;
    background: var(--qm-bg-card);
}
.q-weekgrid__cell--today { background: rgba(var(--qm-primary-rgb), .03); }
.q-weekgrid__cell--interactive { cursor: pointer; }
.q-weekgrid__cell--interactive:hover { background: var(--qm-bg-elevated); }
.q-weekgrid__cell:focus-visible { outline: 2px solid var(--qm-primary); outline-offset: -2px; }
.q-weekgrid__cell--selected { box-shadow: inset 0 0 0 2px var(--qm-primary); }

.q-weekgrid--highlight { background: rgba(var(--qm-primary-rgb), .08); }
.q-weekgrid--warning { background: rgba(var(--qm-warning-rgb), .10); box-shadow: inset 3px 0 0 var(--qm-warning); }
.q-weekgrid--conflict { background: rgba(var(--qm-danger-rgb), .10); box-shadow: inset 3px 0 0 var(--qm-danger); }
.q-weekgrid--unavailable {
    background: repeating-linear-gradient(135deg, transparent 0 6px, rgba(var(--qm-primary-rgb), .06) 6px 12px);
    color: var(--qm-text-muted);
}
.q-weekgrid__cell--selected.q-weekgrid--conflict { box-shadow: inset 0 0 0 2px var(--qm-primary), inset 5px 0 0 var(--qm-danger); }
.q-weekgrid__state-icon { float: right; margin-left: 4px; font-size: var(--qm-text-sm); line-height: 1; }
.q-weekgrid--warning .q-weekgrid__state-icon { color: var(--qm-warning); }
.q-weekgrid--conflict .q-weekgrid__state-icon { color: var(--qm-danger); }
.q-weekgrid--unavailable .q-weekgrid__state-icon { color: var(--qm-text-muted); }
.q-weekgrid__count { font-weight: 600; font-variant-numeric: tabular-nums; }

.q-weekgrid__break .q-weekgrid__rowhead { font-weight: 500; color: var(--qm-text-muted); }
.q-weekgrid__break-band {
    padding: 4px 10px; height: auto;
    background: var(--qm-bg-elevated);
    font-size: var(--qm-text-xs); text-transform: uppercase; letter-spacing: .06em; color: var(--qm-text-muted); text-align: center;
}

/* The phone: a list per day. */
.q-weekgrid__list { display: none; }
.q-weekgrid__day { margin-bottom: 14px; }
.q-weekgrid__day-head {
    display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 8px;
    margin: 0 0 6px; font-family: var(--qm-font-display); font-size: var(--qm-text-base); color: var(--qm-text-primary);
}
.q-weekgrid__day-head .q-weekgrid__head-sub { display: inline; }
.q-weekgrid__today-tag { font-size: var(--qm-text-xs); font-weight: 600; color: var(--qm-primary); text-transform: uppercase; letter-spacing: .05em; }
.q-weekgrid__day-empty { margin: 0; font-size: var(--qm-text-base); color: var(--qm-text-muted); }
.q-weekgrid__day-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.q-weekgrid__item {
    display: flex; align-items: flex-start; gap: 10px; width: 100%; min-height: 44px;
    padding: 8px 10px; text-align: left;
    border: 1px solid var(--qm-border); border-radius: var(--qm-radius-md);
    background: var(--qm-bg-card); color: var(--qm-text-primary); font: inherit;
}
.q-weekgrid__item--selected { box-shadow: inset 0 0 0 2px var(--qm-primary); }
.q-weekgrid__item:focus-visible { outline: 2px solid var(--qm-primary); outline-offset: 1px; }
.q-weekgrid__item-when { flex: 0 0 64px; display: flex; flex-direction: column; font-size: var(--qm-text-sm); font-variant-numeric: tabular-nums; }
.q-weekgrid__item-when span { color: var(--qm-text-muted); font-size: var(--qm-text-xs); }
.q-weekgrid__item-body { flex: 1 1 auto; min-width: 0; font-size: var(--qm-text-base); }

@media (max-width: 640px) {
    .q-weekgrid__scroll { display: none; }
    .q-weekgrid__list { display: block; }
}

@media print {
    /* On paper the whole week, never the phone list, and no scroll box to clip it. */
    .q-weekgrid__scroll { display: block !important; overflow: visible; max-height: none !important; border-color: #000; }
    .q-weekgrid__list { display: none !important; }
    .q-weekgrid__colhead, .q-weekgrid__corner, .q-weekgrid__rowhead { position: static; background: none; }
    .q-weekgrid--conflict { box-shadow: inset 3px 0 0 #000; background: none; }
    .q-weekgrid__table tr { break-inside: avoid; }
}

/* ==========================================================================
   QMonthList — a month as a list of Monday-to-Sunday weeks (duty rota plan §9)
   ========================================================================== */
.q-monthlist { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.q-monthlist__week { border: 1px solid var(--qm-border); border-radius: var(--qm-radius-md); background: var(--qm-bg-card); padding: 10px 12px; min-width: 0; }
.q-monthlist__week--current { border-left: 3px solid var(--qm-primary); }
.q-monthlist__head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; margin: 0 0 8px; font: 600 13px var(--qm-font-display); color: var(--qm-text-primary); }
.q-monthlist__tag { font-size: var(--qm-text-xs); text-transform: uppercase; letter-spacing: .05em; color: var(--qm-primary); }
.q-monthlist__empty { margin: 0; font-size: var(--qm-text-base); color: var(--qm-text-muted); }
.q-monthlist__items { display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; }

/* =====================================================================================================
   ONE control scale (2026-09-17). An audit of 80 pages measured buttons at 34 and 39px, dropdowns at 44px
   and text and date inputs at 48px, so a filter bar showed three heights in one row. Every button, input,
   dropdown and date picker is one of three heights, and text in a control is one of two sizes.
   Change the tokens, never a page: a page rule that sets a control's height, padding or font-size puts
   the drift back.

   TIGHTENED 2026-09-19 — 36/30/44 at 14px became 32/28/38 at 13px. Reported as "reduce the button size,
   the buttons are still large", and on a data-driven screen the header row is as tall as the buttons in
   it: four pixels off a control is four pixels off every band and filter row in the app.

   The phone rule below is NOT scaled with it. 40px stays the floor there, because that is a finger
   rather than a pointer, and it is the one place where a smaller control would be a worse one.
   ===================================================================================================== */
:root {
    --q-control-h: 32px;
    --q-control-h-sm: 28px;
    --q-control-h-lg: 38px;
    --q-control-font: var(--qm-text-base);
    --q-control-font-sm: var(--qm-text-sm);
    --q-control-pad-x: 10px;
    --q-row-gap: 8px;
}

.q-btn {
    min-height: var(--q-control-h);
    padding: 0 12px;
    font-size: var(--q-control-font);
    line-height: 1.2;
}

.q-btn--sm {
    min-height: var(--q-control-h-sm);
    padding: 0 10px;
    font-size: var(--q-control-font-sm);
}

.q-btn--lg {
    min-height: var(--q-control-h-lg);
    padding: 0 20px;
    font-size: var(--qm-text-md);
}

/* An icon-only button is square. */
.q-btn:has(> .q-btn__icon:only-child) { padding: 0; width: var(--q-control-h); }
.q-btn--sm:has(> .q-btn__icon:only-child) { width: var(--q-control-h-sm); }

input.q-input {
    height: var(--q-control-h);
    padding-top: 0;
    padding-bottom: 0;
    padding-left: var(--q-control-pad-x);
    padding-right: var(--q-control-pad-x);
    font-size: var(--q-control-font);
}

textarea.q-input { font-size: var(--q-control-font); padding: 8px var(--q-control-pad-x); }

.q-select,
.q-datepicker__trigger {
    min-height: var(--q-control-h);
    padding-top: 0;
    padding-bottom: 0;
    padding-left: var(--q-control-pad-x);
    padding-right: var(--q-control-pad-x);
    font-size: var(--q-control-font);
}

/* Chips can wrap, so a multi-select grows by its own padding rather than clipping. */
.q-multiselect { padding-top: 3px; padding-bottom: 3px; }

.q-daterange__presets { gap: var(--q-row-gap); }

@media (max-width: 640px) {
    /* A PHONE'S CONTROLS ARE 32px — the desktop's size — AND A BUTTON'S TAP AREA IS 44px (2026-10-05: "reduce the size of
       buttons globally"; it was 36px from 2026-09-28: "reduce and standardise the size of
       all buttons … industry standard of a modern data driven platform"). The floor was 40px drawn, which read as
       oversized beside the 13px type. Stripe, Linear and Notion draw 32–36px on a phone and make the TOUCH area
       larger than what is drawn; so here: 32px on screen, and an invisible 6px margin above and below each button
       that still takes the tap — 44px, Apple's and WCAG 2.5.5's target. A page never sets a control's size. */
    .qm-main .q-btn,
    .qm-main .q-select,
    .qm-main .q-datepicker__trigger,
    .q-modal .q-btn { min-height: 32px; }
    .qm-main input.q-input { height: 32px; }
    .qm-main .q-btn:has(> .q-btn__icon:only-child) { width: 32px; }
    .qm-main .q-btn,
    .q-modal .q-btn { position: relative; }
    .qm-main .q-btn::before,
    .q-modal .q-btn::before { content: ""; position: absolute; left: 0; right: 0; top: -6px; bottom: -6px; }
}

/* The one Radzen grid left (Users) paginates with Radzen's own buttons: same small control size as QPager. */
.rz-pager .rz-pager-page,
.rz-pager .rz-pager-element {
    min-width: var(--q-control-h-sm);
    min-height: 0;
    min-block-size: 0;
    block-size: var(--q-control-h-sm);
    height: var(--q-control-h-sm);
    line-height: var(--q-control-h-sm);
    font-size: var(--q-control-font-sm);
    padding: 0 8px;
}

/* Date-range preset chips are small controls. */
.q-daterange__preset { min-height: var(--q-control-h-sm); padding: 0 11px; font-size: var(--q-control-font-sm); }

/* ---- Card density (user direction 2026-09-18) ------------------------------------------------
   A card header spent 64px to say one word: 20px of padding above and below a 24px row, the same
   generous padding as the card's BODY. A header is a label, not a section — it does not need the
   body's breathing room, and on a page of four cards that difference is a screenful.

   The body keeps its padding; only the header and footer tighten. --q-card-padding stays the token
   a page must not override. */
.q-card__header {
    padding: 0.7rem var(--q-card-padding);
    gap: 0.6rem;
}

.q-card__footer {
    padding: 0.7rem var(--q-card-padding);
}

/* The icon is a marker beside the title, not a tile above it. */
.q-card__icon {
    font-size: var(--qm-text-lg);
    line-height: 1;
}

.q-card__title {
    font-size: var(--qm-text-md);
    line-height: 1.3;
}

/* ==========================================================================
   QProductMark — the app's name set in type (rebrand 2026-09-24)
   ========================================================================== */
.q-product-name { font-family: var(--qm-font-display); white-space: nowrap; }
.q-product-name__word { font-weight: 800; }
.q-product-name__desc { font-weight: 500; opacity: .78; }

/* ==========================================================================
   QPhoneSheet and QRowMenu (compact lists plan, 2026-09-29). One bottom sheet on a phone, with every item's
   words; inline (sheet) or beside the button (row menu) on a desktop. Above modals (1050) and the phone bar
   (1025), below toasts (1100).
   ========================================================================== */

.q-psheet, .q-psheet__body, .q-psheet__items { display: contents; }
.q-psheet__toggle, .q-psheet__backdrop, .q-psheet__head { display: none; }

@media (max-width: 640px) {
    /* A sheet only when there are two or more ACTIONS in it: one action stays a labelled button. A STATUS (a chip, a badge,
       a "Saved" note — always a <span>) is not an action: it stays on the title row, beside the Actions button, and is never
       folded away. Found 2026-09-29: a register's "Taken 21:12" chip vanished into the sheet, so a save looked as if it
       had not happened. */
    .q-psheet:has(.q-psheet__items > :not(span) ~ :not(span)) { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--q-row-gap); }
    .q-psheet:has(.q-psheet__items > :not(span) ~ :not(span)) > .q-psheet__toggle { display: inline-flex; gap: 6px; }
    .q-psheet:where(:has(.q-psheet__items > :not(span) ~ :not(span))):not(.is-open) .q-psheet__items > :not(span) { display: none; }
    /* `Always` (a register's header, 2026-09-30): even ONE action goes behind Actions, so the title and its status keep the line. */
    .q-psheet.q-psheet--always:has(.q-psheet__items > :not(span)) { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--q-row-gap); }
    .q-psheet.q-psheet--always:has(.q-psheet__items > :not(span)) > .q-psheet__toggle { display: inline-flex; gap: 6px; }
    .q-psheet.q-psheet--always:not(.is-open) .q-psheet__items > :not(span) { display: none; }
    .q-psheet.is-open > .q-psheet__backdrop { display: block; position: fixed; inset: 0; z-index: 1070; background: rgba(0, 0, 0, .45); }
    .q-psheet.is-open > .q-psheet__body,
    .q-rowmenu__panel {
        display: block;
        position: fixed; left: 0; right: 0; bottom: 0; z-index: 1071;
        max-height: 80vh; overflow-y: auto;
        padding: 6px 0 calc(12px + env(safe-area-inset-bottom, 0px));
        background: var(--qm-bg-card); color: var(--qm-text-primary);
        border-radius: var(--qm-radius-lg) var(--qm-radius-lg) 0 0;
        box-shadow: 0 -6px 24px rgba(0, 0, 0, .18);
        text-align: left;
    }
    .q-psheet.is-open .q-psheet__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 6px 16px 8px; border-bottom: 1px solid var(--qm-border); }
    .q-psheet__title { font-weight: 600; font-size: var(--qm-text-md); }
    .q-psheet__done { border: 0; background: none; color: var(--qm-primary); font: inherit; font-weight: 600; min-height: 44px; padding: 0 4px; cursor: pointer; }
    .q-psheet.is-open .q-psheet__items { display: flex; flex-direction: column; align-items: stretch; gap: 8px; padding: 12px 16px; }
    .q-psheet.is-open .q-psheet__items > * { width: 100%; max-width: none; margin: 0; flex: none; }
    /* An action in the sheet is a full-width row that says what it does — icon AND words. */
    .q-psheet.is-open .q-psheet__items .q-btn { width: 100%; justify-content: flex-start; gap: 12px; min-height: 44px; }
    .q-psheet.is-open .q-psheet__items .q-btn > .q-btn__text { position: static; width: auto; height: auto; overflow: visible; clip: auto; }
    .q-psheet.is-open .q-psheet__items .dropdown-menu { z-index: 1072; }
    html.qps-open { overflow: hidden; }
}

.q-rowmenu { display: inline-flex; position: relative; }
.q-rowmenu__trigger { width: var(--q-control-h-sm); min-width: var(--q-control-h-sm); padding: 0; justify-content: center; }
.q-rowmenu__trigger.is-open { background: var(--qm-primary-light); }
/* A trigger with its own face (a register's status pill) sizes itself; the page styles it. */
.q-rowmenu__trigger.q-rowmenu__trigger--custom { width: auto; min-width: 0; font: inherit; cursor: pointer; }
.q-rowmenu__backdrop { position: fixed; inset: 0; z-index: 1070; background: transparent; }
.q-rowmenu__panel {
    position: fixed; z-index: 1071; min-width: 220px; max-width: min(320px, calc(100vw - 16px));
    /* It lives inside a cell, so it must not inherit the cell's type: a code cell is bold and nowrap. */
    font-family: var(--qm-font-primary); font-weight: 400; font-style: normal; letter-spacing: normal; text-transform: none;
    white-space: normal; text-align: left; line-height: 1.4;
    padding: 6px; background: var(--qm-bg-card); color: var(--qm-text-primary);
    border: 1px solid var(--qm-border); border-radius: var(--qm-radius-md); box-shadow: var(--qm-shadow-md);
}
.q-rowmenu__heading { padding: 6px 10px 8px; font-size: var(--qm-text-sm); font-weight: 600; color: var(--qm-text-muted);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.q-rowmenu__item { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 36px; padding: 6px 10px;
    border: 0; border-radius: var(--qm-radius-sm); background: none; color: var(--qm-text-primary);
    font: inherit; font-size: var(--qm-text-base); text-align: left; cursor: pointer; }
.q-rowmenu__item i { width: 18px; text-align: center; color: var(--qm-text-muted); flex: none; }
.q-rowmenu__item:hover, .q-rowmenu__item:focus-visible { background: var(--qm-primary-light); outline: none; }
.q-rowmenu__item--danger, .q-rowmenu__item--danger i { color: var(--qm-danger); }
.q-rowmenu__divider { height: 1px; margin: 6px 4px; background: var(--qm-border); }

/* The row's CODE as the trigger (2026-10-02): reads as a code you can press — in the ROW's own font and size, only bold
   (user, 2026-10-04: "the same font and size as the rest of the rows, only it can be bold"), with a small caret; never a
   filled button, never monospace or underlined. A 24px target at least (WCAG 2.5.8); the cell's padding is the rest. */
.q-rowmenu__trigger.q-rowmenu__trigger--code {
    display: inline-flex; align-items: center; gap: 4px; min-height: 24px;
    padding: 2px 4px; margin: -2px -4px; border: 0; border-radius: var(--qm-radius-sm); background: none;
    color: var(--qm-text-primary); font: inherit; font-size: var(--qm-text-base); cursor: pointer; text-align: left;
}
/* The code keeps its whole width — a table column sizes from it. A page that caps the cell (a phone row) adds the
   .q-rowmenu--fit class to QRowMenu, and the code then shrinks with an ellipsis. */
.q-rowmenu__code { font-family: inherit; font-size: inherit; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Phone only (2026-10-02): on a desktop a table column sizes from its content's MIN width, and an ellipsis code has none,
   so the Staff no column collapsed to its caret and every number vanished. */
@media (max-width: 767.98px) {
    .q-rowmenu--fit { max-width: 100%; min-width: 0; width: 100%; }
    .q-rowmenu--fit > .q-rowmenu__trigger--code { max-width: 100%; min-width: 0; width: 100%; justify-content: flex-end; }
    .q-rowmenu--fit .q-rowmenu__code { flex: 0 1 auto; }
    .q-rowmenu--fit .q-rowmenu__code { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
}
.q-rowmenu__code--empty { font-weight: 400; color: var(--qm-text-muted); }
.q-rowmenu__caret { font-size: 9px; color: var(--qm-text-muted); flex: none; }
.q-rowmenu__trigger--code:hover .q-rowmenu__code, .q-rowmenu__trigger--code.is-open .q-rowmenu__code { color: var(--qm-primary); text-decoration-color: var(--qm-primary); }
.q-rowmenu__trigger--code:hover .q-rowmenu__caret, .q-rowmenu__trigger--code.is-open .q-rowmenu__caret { color: var(--qm-primary); }
.q-rowmenu__trigger--code:focus-visible { outline: 2px solid var(--qm-primary); outline-offset: 1px; }
.q-rowmenu__trigger--code.is-open { background: var(--qm-primary-light); }
/* The row's NAME as the trigger (2026-10-04): the code trigger's look on a name — bold, the row's font, a caret; a long
   name wraps rather than running under the next column. */
/* A NAME is not a ⋯: it sizes to its text and starts at the left. The base trigger is a fixed 28px square with its content
   centred, which squeezed a name into 28px and pushed it out on both sides ("aching staff", a rota name one letter a line,
   2026-10-04). */
.q-rowmenu__trigger.q-rowmenu__trigger--name { width: auto; min-width: 0; max-width: 100%; justify-content: flex-start; }
.q-rowmenu__name { font-weight: 600; white-space: normal; overflow-wrap: break-word; }
/* In a week grid's row heading the name reads like the plain label it replaced: one line. "anywhere" let a table column shrink to
   ONE CHARACTER and print the rota name down the page letter by letter (2026-10-04). */
.q-weekgrid__rowhead .q-rowmenu__name { white-space: nowrap; }
.q-rowmenu__trigger--name:hover .q-rowmenu__name, .q-rowmenu__trigger--name.is-open .q-rowmenu__name { color: var(--qm-primary); }
/* The .q-btn 44px finger rule does not reach this button, so a phone gets its own. */
@media (max-width: 640px) { .q-rowmenu__trigger.q-rowmenu__trigger--code { min-height: 32px; } }

/* ===== QTableFrame: a wide list's own scroll box (2026-10-02) ============================================
   Reported from Staff Directory (185 rows, ten columns): the horizontal scrollbar sat at the FOOT of the list, so
   reaching anything on the right meant scrolling down, across and back up. `.q-table-frame` (opt-in, on the
   element that wraps the table or the grid) gets a height bounded by the viewport — measured by
   wwwroot/js/qTableFrame.js as the window height less the frame's top on the page, so with the page at its top the
   frame's own horizontal scrollbar is on screen — and scrolls in both directions itself. The header row stays at
   the top of the frame and the first `data-sticky-cols` columns (tick, code, name) stay at its left, on a solid
   ground, with an edge once the frame is scrolled across. Desktop only (≥ 768px): a phone's lists are rows.
   A table row is `tr`; a CSS-grid list marks its rows `.q-frame-row` and its header `.q-frame-head`.
   A selected row tints its sticky cells through `--qtf-row-tint` (set on the row): a translucent background on a
   sticky cell would show the columns scrolling under it. */
@media (min-width: 768px) {
    /* Defaults; qTableFrame.js writes the measured height and offsets inline, which wins. */
    .q-table-frame { --qtf-max-h: calc(100dvh - 240px); --qtf-l1: 0px; --qtf-l2: 0px; --qtf-l3: 0px; --qtf-l4: 0px; --qtf-cell-bg: var(--qm-bg-card);
        max-height: var(--qtf-max-h); overflow: auto; }
    .q-table-frame table { border-collapse: separate; border-spacing: 0; }
    .q-table-frame thead th,
    .q-table-frame .q-frame-head > * { position: sticky; top: 0; z-index: 2; background: var(--qtf-head-bg, var(--qm-bg-card)); box-shadow: inset 0 -1px 0 var(--qm-border); }
    .q-table-frame[data-sticky-cols="1"] :is(tr, .q-frame-row) > :nth-child(-n+1),
    .q-table-frame[data-sticky-cols="2"] :is(tr, .q-frame-row) > :nth-child(-n+2),
    .q-table-frame[data-sticky-cols="3"] :is(tr, .q-frame-row) > :nth-child(-n+3),
    .q-table-frame[data-sticky-cols="4"] :is(tr, .q-frame-row) > :nth-child(-n+4) {
        position: sticky;
        background: linear-gradient(var(--qtf-row-tint, transparent), var(--qtf-row-tint, transparent)), var(--qtf-cell-bg);
        /* A pinned cell never spills into its neighbour (2026-10-02): a right-aligned staff number wider than its column ran
           left over the tick box and took every press meant for it, so bulk selection did nothing. Menus are fixed, so a
           clipped cell does not clip its menu. */
        overflow: hidden;
    }
    .q-table-frame[data-sticky-cols] :is(tr, .q-frame-row) > :nth-child(1) { left: var(--qtf-l1); }
    .q-table-frame[data-sticky-cols] :is(tr, .q-frame-row) > :nth-child(2) { left: var(--qtf-l2); }
    .q-table-frame[data-sticky-cols] :is(tr, .q-frame-row) > :nth-child(3) { left: var(--qtf-l3); }
    .q-table-frame[data-sticky-cols] :is(tr, .q-frame-row) > :nth-child(4) { left: var(--qtf-l4); }
    /* A header cell that is also a sticky column sits above both the header and the body. */
    .q-table-frame[data-sticky-cols] :is(thead tr, .q-frame-head) > :nth-child(-n+4) { z-index: 3; background: var(--qtf-head-bg, var(--qm-bg-card)); }
    .q-table-frame.is-scrolled-x[data-sticky-cols="1"] :is(tr, .q-frame-row) > :nth-child(1),
    .q-table-frame.is-scrolled-x[data-sticky-cols="2"] :is(tr, .q-frame-row) > :nth-child(2),
    .q-table-frame.is-scrolled-x[data-sticky-cols="3"] :is(tr, .q-frame-row) > :nth-child(3),
    .q-table-frame.is-scrolled-x[data-sticky-cols="4"] :is(tr, .q-frame-row) > :nth-child(4) {
        box-shadow: inset -1px 0 0 var(--qm-border), 6px 0 6px -6px rgba(0, 0, 0, .22);
    }
    .q-table-frame.is-scrolled-x[data-sticky-cols] :is(thead tr, .q-frame-head) > :nth-child(-n+4) { box-shadow: inset 0 -1px 0 var(--qm-border); }
    /* A sticky cell is its own stacking context, so a QRowMenu opened from it (its panel and backdrop are fixed, but they
       paint INSIDE that context) would sit under the next row's sticky cells. The cell holding an open menu goes on top. */
    .q-table-frame :is(tr, .q-frame-row) > :has(.q-rowmenu__panel) { z-index: 20; }
    /* A code too long for its pinned column ends in "…" rather than being cut at its START (2026-10-02): the cell clips
       now, and a right-aligned code lost its first characters ("IJ249-DEPTBR"). The whole code is in the tooltip. */
    .q-table-frame .q-rowmenu:has(> .q-rowmenu__trigger--code) { max-width: 100%; min-width: 0; }
    .q-table-frame .q-rowmenu__trigger--code { max-width: 100%; min-width: 0; justify-content: flex-start; }
    /* A table column sizes from its content's MIN width and ignores min-width on a cell, so the code itself carries the floor:
       9ch shows a whole staff or student number (700001, S/2024/0153); a longer code ends in "…" (2026-10-02 — at min-width 0
       the column collapsed to its caret and every number vanished in production). */
    .q-table-frame .q-rowmenu__code { overflow: hidden; text-overflow: ellipsis; min-width: 9ch; max-width: 16ch; }
    .q-table-frame.is-scrolled-x[data-sticky-cols="1"] :is(thead tr, .q-frame-head) > :nth-child(1),
    .q-table-frame.is-scrolled-x[data-sticky-cols="2"] :is(thead tr, .q-frame-head) > :nth-child(2),
    .q-table-frame.is-scrolled-x[data-sticky-cols="3"] :is(thead tr, .q-frame-head) > :nth-child(3),
    .q-table-frame.is-scrolled-x[data-sticky-cols="4"] :is(thead tr, .q-frame-head) > :nth-child(4) {
        box-shadow: inset 0 -1px 0 var(--qm-border), inset -1px 0 0 var(--qm-border), 6px 0 6px -6px rgba(0, 0, 0, .22);
    }
}

@media (max-width: 640px) {
    .q-rowmenu__backdrop { background: rgba(0, 0, 0, .45); }
    .q-rowmenu__panel { max-width: none; min-width: 0; border: 0; padding-left: 8px; padding-right: 8px; }
    .q-rowmenu__heading { padding: 6px 12px 10px; border-bottom: 1px solid var(--qm-border); margin-bottom: 6px; font-size: var(--qm-text-base); color: var(--qm-text-primary); }
    .q-rowmenu__item { min-height: 48px; padding: 10px 12px; font-size: var(--qm-text-md); }
}

/* ===== Motion and loading, the shared kit (2026-10-01) =====================================================
   Flat by the design rules: no glow, no gradient. Short, and declared ONLY under prefers-reduced-motion:
   no-preference — with less motion asked for, every change is instant. A page uses these classes; it never
   writes its own keyframes for the same job.
     .q-anim-enter   a line arriving: a short fade and rise. Stagger with style="--q-anim-i: N".
     .q-anim-leave   a line done: it collapses out.
     .q-anim-check   a small check that appears on a completed line.
     .q-skeleton     a placeholder line while a group loads (QSkeleton). */
:root {
    --q-anim-enter-ms: 150ms;
    --q-anim-stagger-ms: 35ms;
    --q-anim-leave-ms: 320ms;
}

.q-skeleton-group { display: flex; flex-direction: column; gap: 8px; padding: 6px 0; }
.q-skeleton-group--inline { display: inline-flex; vertical-align: middle; padding: 0; }
.q-skeleton { display: block; height: 0.85em; min-height: 10px; border-radius: var(--qm-radius-sm); background: var(--qm-bg-elevated); }
.q-skeleton--inline { display: inline-block; width: 10em; }

.q-anim-check { color: var(--qm-success); display: inline-flex; align-items: center; }

@media (prefers-reduced-motion: no-preference) {
    .q-skeleton { animation: q-skeleton-breathe 1.4s ease-in-out infinite; }
    /* BACKWARDS, never both/forwards (2026-10-03): an animation that keeps filling a transform after it ends leaves the element a
       containing block for position: fixed, so every QModal backdrop and QSelect panel inside it (the portal's folds) was placed
       relative to the fold, off the screen: a press on an option landed on nothing and "Ask for cover" could not be sent.
       Backwards still holds the "from" state during the stagger delay, and nothing after the end. */
    .q-anim-enter {
        animation: q-anim-enter var(--q-anim-enter-ms) ease-out backwards;
        animation-delay: calc(min(var(--q-anim-i, 0), 12) * var(--q-anim-stagger-ms));
    }
    .q-anim-leave { animation: q-anim-leave var(--q-anim-leave-ms) ease-in forwards; overflow: hidden; }
    .q-anim-check { animation: q-anim-check 220ms ease-out backwards; }
}

@keyframes q-skeleton-breathe { 50% { opacity: 0.5; } }
@keyframes q-anim-enter { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
@keyframes q-anim-leave {
    0% { opacity: 1; max-height: 160px; }
    50% { opacity: 0.4; }
    100% { opacity: 0; max-height: 0; padding-top: 0; padding-bottom: 0; margin-top: 0; margin-bottom: 0; border-width: 0; }
}
@keyframes q-anim-check { from { opacity: 0; transform: scale(0.6); } to { opacity: 1; transform: scale(1); } }
/* "All done": the calm empty state of a list that was waiting on somebody — a check in the tenant's own colour. */
.q-empty--done .q-empty__icon { color: var(--qm-primary); }

/* ---- QLessonCounts and QWeekStrip (2026-10-02): the one home for lesson figures — planned · so far, the week strip, the compact
   line. Colours are tokens only (the week states use the status tokens; future weeks are empty boxes). The strip is a grid of equal
   columns, at most 16px wide, never below 8px, wrapping, so a 15-week term fits a 390px phone without the page scrolling sideways. */
.q-lc-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: var(--q-row-gap, 8px);
    margin: 0 0 var(--qm-block-gap, 12px);
}
.q-lc-grid > * { min-width: 0; }
.q-lc {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 10px 12px;
    background: var(--qm-bg-card);
    border: 1px solid var(--qm-border);
    border-radius: var(--qm-radius-lg);
    min-width: 0;
}
.q-lc__head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; min-width: 0; }
.q-lc__title {
    margin: 0;
    font-size: var(--qm-text-md);
    font-weight: 600;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.q-lc__title a { color: inherit; }
.q-lc__rate { flex: 0 0 auto; font-size: var(--qm-text-xs); color: var(--qm-text-muted); white-space: nowrap; }
.q-lc__cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 8px; }
.q-lc__col { display: flex; flex-direction: column; min-width: 0; }
.q-lc__label { font-size: var(--qm-text-xs); color: var(--qm-text-muted); text-transform: uppercase; letter-spacing: 0.03em; }
.q-lc__num { font-size: var(--qm-text-stat); font-weight: 700; line-height: 1.2; font-variant-numeric: tabular-nums; }
.q-lc__sub { font-size: var(--qm-text-xs); color: var(--qm-text-secondary); }
.q-lc__line {
    display: flex;
    flex-wrap: wrap;
    gap: 2px 0;
    margin: 0;
    font-size: var(--qm-text-sm);
    color: var(--qm-text-secondary);
    font-variant-numeric: tabular-nums;
}
.q-lc__line > span { white-space: nowrap; }
.q-lc__line > span + span::before { content: "·"; margin: 0 6px; color: var(--qm-text-muted); }
.q-lc__fig { color: var(--qm-primary); font-weight: 600; text-decoration: none; }
.q-lc__fig:hover, .q-lc__fig:focus-visible { text-decoration: underline; }
.q-lc__fig--btn {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    font-weight: 600;
    color: var(--qm-primary);
    cursor: pointer;
}
.q-lc__empty { margin: 8px 12px; font-size: var(--qm-text-base); color: var(--qm-text-muted); }
.q-lc__nowrap { white-space: nowrap; }
.q-lc__strip-cell { min-width: 120px; }
.q-lc-table--print { width: 100%; border-collapse: collapse; }
.q-lc-table--print .num { text-align: right; font-variant-numeric: tabular-nums; }
/* Y4 (2026-10-03): a heading is its words plus a QInfo balloon; headings wrap so the default table fits 1366px without
   sideways scroll. A balloon open inside the scrolling wrapper must not be clipped by it. */
/* `th.num` too: app.css's `.data-table .num { white-space: nowrap }` (two classes, loaded last) otherwise beats this rule and
   every heading stays on one line — "Awaiting verification" alone took 261px and the default table ran to 1,494px at 1366. */
.q-lc-table thead th, .q-lc-table thead th.num { white-space: normal; vertical-align: bottom; }
@media (min-width: 641px) {
    .q-lc-table th, .q-lc-table td { padding-left: 8px; padding-right: 8px; }
    .q-lc-table thead th.num { min-width: 64px; }
}
.q-lc__th { overflow-wrap: anywhere; }
.q-lc__th-info, .q-lc__cell-info { margin-left: 2px; }
.q-lc-table thead th .q-info__btn { width: 20px; height: 20px; font-size: var(--qm-text-xs); }
.table-wrap.q-lc-wrap:has(.q-info__pop) { overflow: visible; }
/* Z2: a rate not drawn because too little is recorded, and time lost's "in N recorded periods". */
.q-lc__gated { color: var(--qm-text-muted); font-style: italic; white-space: nowrap; }
.q-lc__cellsub { display: block; font-size: var(--qm-text-xs); color: var(--qm-text-muted); white-space: nowrap; }
/* Z1 + Z8: the printed table — compact, headings wrap, no fixed widths, black on white, so eight columns fit A4 landscape.
   Every figure is one string ("48 of 1,163 (4.1%)"); a sub-line prints on its own line, never run into the figure before it. */
.q-lc-table--print { table-layout: auto; color: #000; font-size: 8.5pt; line-height: 1.25; }
.q-lc-table--print th, .q-lc-table--print td { padding: 2px 4px; border: 1px solid #555; color: #000; vertical-align: top; min-width: 0; }
/* A printed heading breaks BETWEEN words only (2026-10-04): `overflow-wrap: anywhere` fitted the table by printing "Timetabl/ed",
   "Rec/over/ed" and "Ow/ed". The columns shrink to their longest word instead. */
.q-lc-table--print thead th { white-space: normal; font-size: 7.5pt; font-weight: 700; text-align: right; overflow-wrap: normal; word-break: normal; hyphens: manual; }
.q-lc-table--print thead th .q-lc__th { overflow-wrap: normal; }
.q-lc-table--print thead th[data-col="name"] { text-align: left; }
.q-lc-table--print td[data-col="name"] { white-space: normal; }
.q-lc-table--print .cell-sub, .q-lc-table--print .q-lc__cellsub { display: block; color: #000; font-size: 7pt; }
.q-lc-table--print .q-lc__gated { color: #000; }
.q-lc-table--print tr { break-inside: avoid; page-break-inside: avoid; }
/* 2026-10-04: the screen rule above (`th.num { min-width: 64px }` from 641px) reached the printed table too — sixteen columns
   at 64px ran 178px past an A4 landscape sheet and eight past half of one. A printed column is as narrow as its words. */
.q-lc-table--print thead th, .q-lc-table--print thead th.num { min-width: 0; }
/* A printed FIGURE stays on one line ("0 of 1 · 0%" split as "0 of 1 ·" over "0%"); a sentence under it may wrap. */
.q-lc-table--print td.num { white-space: nowrap; }
.q-lc-table--print .q-lc__gated, .q-lc-table--print .q-lc__cellsub, .q-lc-table--print .q-lc__nowrap { white-space: normal; }

.q-weekstrip { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px; min-width: 0; }
.q-weekstrip__boxes {
    display: grid;
    grid-template-columns: repeat(var(--q-weeks, 13), minmax(8px, 16px));
    gap: 3px;
    flex: 1 1 auto;
    min-width: 0;
    max-width: 100%;
}
.q-weekstrip__cell { display: block; min-width: 0; }
.q-weekstrip__wk {
    display: block;
    width: 100%;
    aspect-ratio: 1;
    min-height: 8px;
    padding: 0;
    border: 1px solid var(--qm-border);
    border-radius: var(--qm-radius-sm);
    background: transparent;
    cursor: default;
}
button.q-weekstrip__wk, a.q-weekstrip__wk { cursor: pointer; }
button.q-weekstrip__wk:focus-visible, a.q-weekstrip__wk:focus-visible { outline: 2px solid var(--qm-primary); outline-offset: 1px; }
.q-weekstrip__wk--taught { background: var(--qm-success); border-color: var(--qm-success); }
.q-weekstrip__wk--recovered { background: var(--qm-warning); border-color: var(--qm-warning); }
.q-weekstrip__wk--missed { background: var(--qm-danger); border-color: var(--qm-danger); }
.q-weekstrip__wk--unrecorded { background: var(--qm-text-muted); border-color: var(--qm-text-muted); opacity: 0.55; }
/* Taught on the teacher's own report, awaiting verification against the class's form (E8): the info tone, outlined (a claim, not yet a fact). */
.q-weekstrip__wk--awaiting { background: rgba(var(--qm-info-rgb), 0.25); border: 2px solid var(--qm-info); }
.q-weekstrip__wk--future { background: transparent; }
.q-weekstrip__wk--none { background: transparent; border-style: dashed; opacity: 0.6; }
.q-weekstrip__wk.is-current { outline: 2px solid var(--qm-primary); outline-offset: 1px; }
.q-weekstrip__count { font-size: var(--qm-text-xs); color: var(--qm-text-muted); white-space: nowrap; }
.q-weekstrip--compact .q-weekstrip__boxes { grid-template-columns: repeat(var(--q-weeks, 13), minmax(6px, 10px)); gap: 2px; }
.q-weekstrip--compact .q-weekstrip__wk { min-height: 6px; }

@media (max-width: 640px) {
    .q-lc-grid { grid-template-columns: 1fr; }
    /* A tap target on a phone: the box stays small, its button reaches 24px tall around it (WCAG 2.5.8 minimum). */
    .q-weekstrip:not(.q-weekstrip--compact) .q-weekstrip__boxes { grid-template-columns: repeat(var(--q-weeks, 13), minmax(10px, 1fr)); max-width: 360px; }
    .q-weekstrip:not(.q-weekstrip--compact) .q-weekstrip__wk { aspect-ratio: auto; height: 22px; }
}

@media print {
    .q-weekstrip { display: none; }
    .q-lc { break-inside: avoid; border-color: #000; }
}

/* ---- QFoldSection (2026-10-02): a folding section of a page — a card whose header is one full-width button. The body grows from
   0fr to 1fr rows and fades in, the chevron turns, a changed status pulses once; all of it ONLY under prefers-reduced-motion:
   no-preference, 150–250ms, flat. Without that preference it simply opens and shuts. --------------------------------------------- */
.q-fold { display: flex; flex-direction: column; min-width: 0; border: 1px solid var(--qm-border); }
.q-fold__head { display: flex; align-items: center; gap: 4px; min-width: 0; padding-right: 8px; }
.q-fold__toggle {
    flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 10px;
    min-height: 48px; padding: 8px 4px 8px var(--q-card-padding);
    background: none; border: 0; margin: 0; color: inherit; font: inherit; text-align: left; cursor: pointer;
    border-radius: var(--qm-radius-lg);
}
.q-fold__toggle:hover { background: var(--qm-bg-card-hover); }
.q-fold__toggle:focus-visible { outline: 2px solid var(--qm-primary); outline-offset: -2px; }
.q-fold__icon { flex: none; color: var(--qm-primary); font-size: var(--qm-text-md); }
.q-fold__text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.q-fold__title { font-family: var(--qm-font-display); font-weight: 600; font-size: var(--qm-text-md); line-height: 1.25;
                 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.q-fold__status { font-size: var(--qm-text-sm); color: var(--qm-text-muted); line-height: 1.3; border-radius: var(--qm-radius-sm);
                  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; align-self: flex-start; max-width: 100%; }
.q-fold--action .q-fold__status { color: var(--qm-text-primary); font-weight: 600; }
.q-fold__count { flex: none; }
.q-fold__chevron { flex: none; color: var(--qm-text-muted); font-size: var(--qm-text-sm); }
.q-fold.is-open .q-fold__chevron { transform: rotate(180deg); }
.q-fold__info { flex: none; }
.q-fold__panel { display: grid; grid-template-rows: 1fr; min-width: 0; }
.q-fold__panel[hidden], .q-fold__panel.is-closing { display: none; }
.q-fold__inner { min-height: 0; min-width: 0; }
.q-fold__body { padding: 4px var(--q-card-padding) var(--q-card-padding); border-top: 1px solid var(--qm-border); min-width: 0; }
.q-card-plain { min-width: 0; }
.q-card-plain__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--q-row-gap); margin: 6px 0; }
.q-card-plain__footer { margin-top: var(--qm-block-gap); }

@media (prefers-reduced-motion: no-preference) {
    .q-fold__chevron { transition: transform 200ms ease; }
    .q-fold__toggle { transition: background-color 150ms ease; }
    .q-fold__panel:not([hidden]):not(.is-closing) { animation: q-fold-open 220ms ease-out; }
    .q-fold__panel:not([hidden]):not(.is-closing) > .q-fold__inner { animation: q-fold-clip 220ms; }
    .q-fold__panel.is-closing { display: grid; animation: q-fold-close 180ms ease-in forwards; }
    .q-fold__panel.is-closing > .q-fold__inner { overflow: hidden; }
    .q-fold__status--changed { animation: q-fold-pulse 600ms ease-out; }
}
@keyframes q-fold-open { from { grid-template-rows: 0fr; opacity: 0; } to { grid-template-rows: 1fr; opacity: 1; } }
@keyframes q-fold-close { from { grid-template-rows: 1fr; opacity: 1; } to { grid-template-rows: 0fr; opacity: 0; } }
/* Clip the content only while it grows: kept, overflow:hidden would cut off a popover inside an open section. */
@keyframes q-fold-clip { from, to { overflow: hidden; } }
@keyframes q-fold-pulse { 0% { background: rgba(var(--qm-primary-rgb), 0.22); } 100% { background: transparent; } }

/* QLessonCounts Compact (2026-10-02): one line per class, a tap opens its card. */
.q-lc-compact { display: flex; flex-direction: column; border: 1px solid var(--qm-border); border-radius: var(--qm-radius-lg); background: var(--qm-bg-card); }
.q-lc-line + .q-lc-line { border-top: 1px solid var(--qm-border); }
.q-lc-line__sum { list-style: none; display: grid; grid-template-columns: minmax(3.5em, auto) auto minmax(0, 1fr) auto auto; align-items: center; column-gap: 10px;
    min-height: 44px; padding: 6px 12px; cursor: pointer; font-size: var(--qm-text-base); }
.q-lc-line__sum::-webkit-details-marker { display: none; }
.q-lc-line__sum:focus-visible { outline: 2px solid var(--qm-primary); outline-offset: -2px; }
.q-lc-line__name { font-weight: 600; white-space: nowrap; }
.q-lc-line__fig { font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--qm-text-secondary); }
.q-lc-line__strip { min-width: 0; overflow: hidden; }
.q-lc-line__pct { color: var(--qm-text-muted); font-variant-numeric: tabular-nums; }
.q-lc-line__chev { color: var(--qm-text-muted); transition: transform .18s ease; }
.q-lc-line[open] .q-lc-line__chev { transform: rotate(180deg); }
.q-lc-line__body { padding: 0 12px 12px; }
.q-lc-line__body .q-lc--card { border: 0; box-shadow: none; padding: 0; }
@media (max-width: 640px) {
    .q-lc-line__sum { grid-template-columns: auto auto minmax(0, 1fr) auto; row-gap: 4px; }
    .q-lc-line__strip { grid-column: 1 / -1; grid-row: 2; }
}
@media (prefers-reduced-motion: reduce) { .q-lc-line__chev { transition: none; } }
/* An opened line does not repeat itself: the summary already names the class, so the card drops its title, and the
   summary's strip hides while the card shows its own (2026-10-02). The figures stay close together on a wide screen. */
.q-lc-line__body .q-lc__title { display: none; }
.q-lc-line__body .q-lc__head { justify-content: flex-start; }
.q-lc-line[open] > .q-lc-line__sum .q-lc-line__strip { visibility: hidden; }
.q-lc-line__body .q-lc__cols { max-width: 420px; }

/* A week-grid flag that explains itself (QWeekGrid CellStateText, 2026-10-02). */
.q-weekgrid__state-icon--info { pointer-events: auto; }
.q-weekgrid__flag .q-info__btn { color: var(--qm-warning); }
.q-weekgrid--conflict .q-weekgrid__flag .q-info__btn { color: var(--qm-danger); }

/* ===== QListRow · QListHead · QListGroup — one list row on every list (lesson integrity plan, Phase 4, 2026-10-02) =====
   tick · code▾ · [avatar] name / one sub-line + markers · wide cells (desk only) · end (meta, or ⋯ when there is no code).
   A <table class="q-list"> on a desk (a QTableFrame keeps its header and its tick, code and name columns in view); under
   768px — the breakpoint QTableFrame uses, --q-list-break — every row is ONE grid line of four tracks. A page never sets
   a row's height, tint, breakpoint, avatar or sub-line size: these tokens are the one home. The code keeps its TAIL
   (D6): the ellipsis is at the START, with the characters in their own order (an isolated LTR run inside an RTL box). */
:root {
    --q-list-break: 767.98px;      /* documentation: a media query cannot read a custom property */
    --q-list-row-h: 52px;
    --q-list-sub: var(--qm-text-sm);
    --q-list-code-w: clamp(7ch, 22vw, 11ch);
}
/* Re-declared on the list, so a tenant's brand set below :root is the one the tint is made from. */
:root, .q-list { --q-list-sel: rgba(var(--qm-primary-rgb), .07); }

.q-list { width: 100%; }
.q-list-wrap { overflow-x: auto; }
/* A table in a card (2026-10-03): it fits its card or scrolls inside this wrapper — never clipped by the card. One home; a page
   no longer writes its own `.table-wrap` rule. On a phone a q-stack table is cards and does not scroll (below). */
.table-wrap { overflow-x: auto; max-width: 100%; }
.table-wrap:has(.q-info__pop) { overflow: visible; } /* an open balloon in a heading is not clipped by the scroller */
/* QTable's column kinds (2026-10-03, plan Phase 5.4; QColKind / QCol). A long status chip ("Not taught — with permission")
   is cut short with its words in its title, and a text column keeps a reading width, so a chip can no longer squeeze a
   reason into a tall strip (the My teaching record case). Desktop only: under 641px a q-stack table is cards. */
.data-table .q-col-nowrap { white-space: nowrap; }
@media (min-width: 641px) {
    .data-table .q-col-status { width: 1%; white-space: nowrap; }
    .data-table .q-col-status .q-chip { max-width: 13rem; }
    .data-table .q-col-status .q-chip__text { min-width: 0; }
    .data-table .q-col-text { min-width: 14rem; }
}
/* The page's loading block (2026-10-03, plan Phase 5.5): one home. Forty pages each wrote their own padding (60px, 80px),
   gap, colour and sometimes a card frame, so no two loading states matched. A page uses the name and never restyles it
   (style-leak-check); one that genuinely needs a different block gives it a class of its own. */
.loading-container { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 48px 20px; color: var(--qm-text-muted); text-align: center; }
.q-list-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 48px 20px; color: var(--qm-text-muted); }

.q-list > tbody > .q-list-row > td { height: var(--q-list-row-h); vertical-align: middle; }
.q-list-row__tick, .q-list-head__tick { width: 34px; }
.q-list-row__tickbox { display: inline-flex; align-items: center; }
.q-list-row__code, .q-list-head__code { white-space: nowrap; }
.q-list-row__end, .q-list-head__end { white-space: nowrap; text-align: right; }
.q-list-row__end--empty, .q-list-head__end--empty { padding: 0 !important; width: 0; }
.q-list-row__person { display: flex; align-items: center; gap: 10px; min-width: 0; }
.q-list-row__text { display: flex; flex-direction: column; min-width: 0; }
.q-list-row__line { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 6px; min-width: 0; }
.q-list-row__name { font-weight: 600; color: var(--qm-text-primary); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
a.q-list-row__name { display: inline-block; max-width: 100%; min-height: 24px; line-height: 24px; vertical-align: middle; }
a.q-list-row__name:hover { color: var(--qm-primary); text-decoration: underline; }
a.q-list-row__name:focus-visible, .q-list-row__name--button:focus-visible { outline: 2px solid var(--qm-primary); outline-offset: 2px; border-radius: var(--qm-radius-sm); }
.q-list-row__name--button { border: 0; background: none; padding: 0; font: inherit; font-weight: 600; cursor: pointer; text-align: left; min-height: 24px; }
.q-list-row__name--button:hover { color: var(--qm-primary); text-decoration: underline; }
/* A row with no code: its name is the menu (QRowMenu NameTrigger) and keeps the row's one line, cut short with "…". */
.q-list-row__line > .q-list-row__menu { min-width: 0; max-width: 100%; }
/* The name trigger carries the code trigger's -4px side margins, so its menu's max-content is 8px LESS than the trigger needs, and
   "max-width: 100%" of that cut every short name by 8px ("Foyer scr…" with half the row empty, 2026-10-16 front-office film).
   The margins are given back here; a long name still ends in "…" at the line's width. */
.q-list-row__line .q-rowmenu__trigger--name { min-width: 0; max-width: calc(100% + 8px); }
.q-list-row__line .q-rowmenu__trigger--name .q-rowmenu__name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.q-list-row__sub { font-size: var(--q-list-sub); color: var(--qm-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; font-weight: 400; }
.q-list-row__sub--phone { display: none; }
.q-list-row__meta { display: inline-flex; align-items: center; gap: 6px; }

/* The code: the row's menu, the same size as its cell. Its START is kept and it ends in "…" when cut (2026-10-03: a staff
   number or username is read from the left; "…3bcvy-S9" lost what identifies it). An empty value ("No code") is words, so
   it wraps rather than being cut. */
.q-list-row__code { font-size: var(--qm-text-base); }
.q-list-row__code .q-list-row__menu { max-width: 100%; min-width: 0; }
.q-list-row__code .q-rowmenu__trigger--code { max-width: 100%; min-width: 0; justify-content: flex-start; }
.q-list-row__codeval { display: block; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.q-list-row__codeval.q-rowmenu__code--empty { white-space: normal; text-overflow: clip; line-height: 1.25; }
.q-list-row__codetext { display: inline-flex; max-width: 100%; min-width: 0; font-weight: 600; font-variant-numeric: tabular-nums; }
/* On a desk a table column sizes from its content's MIN width, and an ellipsis code has none: the code carries a floor
   (9ch shows a whole staff or student number) and a ceiling past which it ends in "…" (the QTableFrame rule). */
@media (min-width: 768px) {
    .q-list-row__codeval { min-width: 9ch; max-width: 16ch; }
}

/* Selected and muted. A sticky cell takes the tint through --qtf-row-tint (a translucent cell background would show the
   columns scrolling under it); muted dims the cell's CONTENT, never its ground, for the same reason. */
.q-list-row--selected { --qtf-row-tint: var(--q-list-sel); }
.q-list-row--selected > td { background: var(--q-list-sel); }
.q-list-row--muted > td > * { opacity: .55; }
.q-list-row--muted > td { color: var(--qm-text-muted); }

/* The row's own end controls (End) sit in a line; a line under the row (Below) sits under the name on a desk and across the
   whole row on a phone (Phase 5.3: the register's note and trail, the Lesson log's cover line). */
.q-list-row__end > * { vertical-align: middle; }
.q-list-row__below { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; min-width: 0; }
.q-list-row__below:empty { display: none; }

/* A group heading: one cell across the list. */
.q-list-group > td { padding: 6px 12px; background: var(--qm-bg-secondary); border-bottom: 1px solid var(--qm-border); }
.q-list-group__in { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; font-size: var(--qm-text-sm); color: var(--qm-text-secondary); min-width: 0; }
.q-list-group__title { color: var(--qm-text-primary); font-size: var(--qm-text-base); }
.q-list-group__count { font-variant-numeric: tabular-nums; }

/* A phone: one grid line per row. */
@media (max-width: 767.98px) {
    table.q-list, table.q-list > tbody, table.q-list > thead { display: block; width: 100%; }
    /* Virtualize's spacer rows. */
    table.q-list > tbody > tr:not(.q-list-row):not(.q-list-group) { display: block; }
    .q-list-row, .q-list-head {
        display: grid; grid-template-columns: 24px var(--q-list-code-w) minmax(0, 1fr) auto; grid-template-areas: "tick code main end";
        column-gap: 10px; align-items: center; padding: 4px 10px 4px 12px; border-bottom: 1px solid var(--qm-border);
    }
    .q-list-row { min-height: var(--q-list-row-h); }
    .q-list-row--notick, .q-list-head--notick { grid-template-columns: var(--q-list-code-w) minmax(0, 1fr) auto; grid-template-areas: "code main end"; }
    .q-list-row--nocode, .q-list-head--nocode { grid-template-columns: 24px minmax(0, 1fr) auto; grid-template-areas: "tick main end"; }
    .q-list-row--notick.q-list-row--nocode, .q-list-head--notick.q-list-head--nocode { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "main end"; }
    .q-list > tbody > .q-list-row > td, .q-list-head > th { padding: 0; border: 0; height: auto; min-width: 0; width: auto; background: none; }
    .q-list-row__tick, .q-list-head__tick { grid-area: tick; }
    .q-list-row__code, .q-list-head__code { grid-area: code; overflow: hidden; }
    .q-list-row__main, .q-list-head__main { grid-area: main; }
    .q-list-row__end, .q-list-head__end { grid-area: end; justify-self: end; }
    .q-list-row > td:not(.q-list-row__tick):not(.q-list-row__code):not(.q-list-row__main):not(.q-list-row__end),
    .q-list-head > th:not(.q-list-head__tick):not(.q-list-head__code):not(.q-list-head__main):not(.q-list-head__end) { display: none; }
    .q-list-head { min-height: 32px; padding-top: 2px; padding-bottom: 2px; background: var(--qm-bg-secondary); }
    .q-list-row--selected { background: var(--q-list-sel); }
    .q-list-row--selected > td { background: none; }
    .q-list-row__line { flex-wrap: nowrap; overflow: hidden; }
    .q-list-row__line > * { flex: none; }
    .q-list-row__line > .q-list-row__name, .q-list-row__line > .q-list-row__menu { flex: 0 1 auto; min-width: 0; }
    /* The end never takes the name's room (2026-10-06): a long status ("Taught, awaiting verification · No class record") grew
       the end track until the name got 0px. It is capped and its chip ends in "…", the whole status in its title. */
    .q-list-row__end { max-width: 34vw; min-width: 0; overflow: hidden; }
    /* Every box between the end and the chip takes the cap too (2026-10-07): a page that wraps its row menu (the Lesson log's
       .ll-phone) left the menu as wide as its whole status, so the pill ran past the clipped end and off the screen — its
       middle, where a finger lands, was outside the window, and the chip was cut without its "…". */
    .q-list-row__end > *, .q-list-row__end .q-rowmenu { max-width: 100%; min-width: 0; }
    .q-list-row__end .q-chip, .q-list-row__end .q-rowmenu__trigger--custom { max-width: 100%; min-width: 0; }
    .q-list-row__end .q-chip__text { min-width: 0; }
    .q-list-row__sub--desk { display: none; }
    .q-list-row__sub--phone { display: block; }
    .q-list-row__code .q-rowmenu__trigger--code { width: 100%; }
    .q-list-group, .q-list-group > td { display: block; }

    /* Below: the name cell steps aside (display: contents), so the person keeps the main track and the line under it
       takes a second grid row across every track. */
    .q-list-row--below { row-gap: 4px; }
    .q-list-row--below > .q-list-row__main { display: contents; }
    .q-list-row--below .q-list-row__person { grid-area: main; min-width: 0; }
    .q-list-row--below .q-list-row__below { grid-column: 1 / -1; grid-row: 2; margin-top: 0; }

    /* WrapName: a name read by a person (a register) takes two lines before it is cut. */
    .q-list-row--wrapname .q-list-row__name { white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
                                              line-height: 1.25; overflow-wrap: anywhere; }

    /* A list whose tick column shows on a phone only while selecting (a register, the Lesson log). */
    .q-list--tick-on-demand:not(.q-list--ticking) .q-list-row__tick,
    .q-list--tick-on-demand:not(.q-list--ticking) .q-list-head__tick { display: none; }
    .q-list--tick-on-demand:not(.q-list--ticking) .q-list-row,
    .q-list--tick-on-demand:not(.q-list--ticking) .q-list-head { grid-template-columns: var(--q-list-code-w) minmax(0, 1fr) auto; grid-template-areas: "code main end"; }
    .q-list--tick-on-demand:not(.q-list--ticking) .q-list-row--nocode,
    .q-list--tick-on-demand:not(.q-list--ticking) .q-list-head--nocode { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "main end"; }
}

/* ---- A register page's header (StudentRegister, StaffRegister), moved here from QRegister on 2026-10-04 so it holds when
   the list does not draw (a register with no students showed both words of its status chip). ---- */
/* THE REGISTER'S HEADER ON A PHONE IS ONE LINE: back, the title (cut short, never overlapping), the status beside it,
   and the actions behind one Actions button. A long meeting title used to run under the status chip. */
@media (max-width: 767px) {
    .reg-page .page-header { flex-wrap: nowrap; align-items: center; gap: 6px; }
    .reg-page .page-header .header-content { flex: 1 1 0; min-width: 0; gap: 4px; }
    .reg-page .page-header h1 { flex: 1 1 auto; min-width: 0; margin: 0; }
    /* The title needs the room; on a phone the sheet and the pill explain themselves, so the (i) steps aside. */
    .reg-page .page-header h1 .q-info { display: none; }
    .reg-page .page-header .header-actions { flex: 0 1 auto; min-width: 0; flex-wrap: nowrap; }
    .reg-page .page-header .q-psheet { flex-wrap: nowrap; }
    .reg-page .page-header .q-psheet__toggle { order: 2; }
    .reg-page .reg-back { margin: 0; flex: none; }
    .reg-page .reg-back .q-btn__text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
    .reg-page .reg-status { max-width: 128px; min-width: 0; overflow: hidden; flex: none; }
    .reg-page .reg-status, .reg-page .reg-status * { white-space: nowrap; text-overflow: ellipsis; }
}
.reg-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
/* The status in full at a desk ("Taken 30 Sep 2026, 21:12 by …"), in two words on a phone ("Taken 21:12"); the whole
   sentence is the chip's title. */
.reg-status__short { display: none; }
@media (max-width: 767px) { .reg-page .reg-status__long { display: none; } .reg-page .reg-status__short { display: inline; } }


/* ATTENTION: a button that calls the reader to press it (Workspace's "Open Today", 2026-10-05: "a more prominent colour … calling the
   teacher to click it"). A ring in the school's colour grows out of the button and fades, three times, then rests. A transform and
   an opacity — no glow, no flashing (WCAG 2.3.1: well under three a second) — and nothing at all under prefers-reduced-motion. */
.q-attention { position: relative; isolation: isolate; }
@media (prefers-reduced-motion: no-preference) {
    .q-attention::after { content: ""; position: absolute; inset: 0; border-radius: inherit; border: 2px solid var(--qm-primary);
                          pointer-events: none; z-index: -1; animation: q-attention-ring 1.6s ease-out 0.6s 3 both; }
    @keyframes q-attention-ring { 0% { transform: scale(1); opacity: .7; } 100% { transform: scale(1.35, 1.7); opacity: 0; } }
}

/* A code is never cut to make room for its menu's arrow (2026-10-05, seen filming the teacher demo: six-digit student codes read
   "504…" in a 86px column while the button took 62px). The code keeps its own width; only a code longer than the whole column
   is clipped, by the column. */
.q-rowmenu__trigger--code .q-rowmenu__code, .q-list-row__codeval { flex-shrink: 0; }
/* A warning on a form: a confirmation the form waits for, or a fact that changes what somebody does (2026-10-05).
   One look for every page: a warning-tinted box, the text in the theme's own text colour (readable in both themes),
   the icon in the warning colour. Two pages styled their own, one in --qm-accent-orange — a token the tenant's
   palette rewrites, so on a blue-branded school the welfare log's warning was blue text on an amber box. */
.warning-banner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 10px;
    padding: 10px 12px;
    margin-bottom: var(--qm-field-gap);
    border: 1px solid rgba(var(--qm-warning-rgb), 0.45);
    border-left: 3px solid var(--qm-warning);
    border-radius: var(--qm-radius-md);
    background: rgba(var(--qm-warning-rgb), 0.1);
    font-size: var(--qm-text-base);
    color: var(--qm-text-primary);
}
.warning-banner > i, .warning-banner > .bi { color: var(--qm-warning); }
.warning-banner > span { flex: 1 1 16rem; min-width: 0; }

/* QHScroll (2026-10-07): wide content that scrolls sideways in its own box, with a rail pinned to the bottom of the window while
   the content is in view — ‹ › and a second scroll bar, so a mouse never has to go to the table's own bar, far below. */
.q-hscroll { position: relative; min-width: 0; }
.q-hscroll__view { overflow-x: auto; min-width: 0; }
.q-hscroll__rail {
    position: sticky; bottom: 0; z-index: 3;
    display: flex; align-items: center; gap: var(--q-row-gap);
    padding: 4px 6px; margin-top: 4px;
    background: var(--qm-bg-card); border: 1px solid var(--qm-border); border-radius: var(--qm-radius-md);
    box-shadow: var(--qm-shadow-sm);
}
.q-hscroll__rail[hidden] { display: none; }
.q-hscroll__btn {
    flex: none; width: var(--q-control-h-sm); height: var(--q-control-h-sm);
    display: inline-flex; align-items: center; justify-content: center;
    border: 1px solid var(--qm-border); border-radius: var(--qm-radius-md);
    background: var(--qm-bg-card); color: var(--qm-text-primary); cursor: pointer;
}
.q-hscroll__btn:hover:not(:disabled) { background: var(--qm-primary-light); }
.q-hscroll__btn:focus-visible { outline: 2px solid var(--qm-primary); outline-offset: 1px; }
.q-hscroll__btn:disabled { opacity: .4; cursor: default; }
.q-hscroll__bar { flex: 1; min-width: 0; overflow-x: auto; overflow-y: hidden; height: 16px; }
.q-hscroll__spacer { height: 1px; }
@media (max-width: 767.98px) { .q-hscroll__rail { display: none !important; } }
@media print { .q-hscroll__rail { display: none !important; } .q-hscroll__view { overflow: visible; } }

/* QSortButton: a table heading that sorts (2026-10-07). It reads like the heading it sits in; the arrow says the order. */
.q-sort { all: unset; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; font: inherit; color: inherit;
    text-transform: inherit; letter-spacing: inherit; border-radius: var(--qm-radius-sm); }
.q-sort:hover, .q-sort.on { color: var(--qm-text-primary); }
.q-sort:focus-visible { outline: 2px solid var(--qm-primary); outline-offset: 2px; }
.q-sort__icon { font-size: var(--qm-text-xs); opacity: .55; }
.q-sort.on .q-sort__icon { opacity: 1; }

/* ---- What's new (PLAN §31) ------------------------------------------------------------------------------------------
   QNewBadge is a QChip (sm, primary); these rules only space it where it sits. The panel's list and the To do / Today line
   are shared, module-neutral, tokens only. */
.q-new-badge { margin-left: var(--q-row-gap); flex: none; vertical-align: middle; }
.q-whatsnew__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.q-whatsnew__item {
    display: flex; align-items: center; gap: var(--q-row-gap);
    padding: var(--q-row-gap) 0; border-bottom: 1px solid var(--qm-border);
}
.q-whatsnew__item:last-child { border-bottom: 0; }
.q-whatsnew__text { flex: 1 1 auto; min-width: 0; }
.q-whatsnew__title { display: flex; align-items: center; flex-wrap: wrap; font-weight: 600; color: var(--qm-text-primary); }
.q-whatsnew__title .q-new-badge { margin-left: var(--q-row-gap); }
.q-whatsnew__sentence { color: var(--qm-text-secondary); font-size: var(--qm-text-sm); }
.q-whatsnew__date { color: var(--qm-text-muted); font-size: var(--qm-text-xs); }
.q-whatsnew__go { flex: none; }
.q-whatsnew-line {
    display: flex; align-items: center; gap: var(--q-row-gap);
    padding: 6px 12px; margin-bottom: var(--qm-block-gap);
    border: 1px solid var(--qm-border); border-radius: var(--qm-radius-md);
    background: var(--qm-bg-card); color: var(--qm-text-primary); font-size: var(--qm-text-base);
}
.q-whatsnew-line__text { flex: 1 1 auto; min-width: 0; }
.q-whatsnew-line__open {
    border: 0; background: none; padding: 0; font: inherit; color: var(--qm-primary); text-decoration: underline; cursor: pointer;
    min-height: 24px; display: inline-flex; align-items: center; /* a 24px target (WCAG 2.5.8) */
}
.q-whatsnew-line__dismiss { flex: none; }
@media (max-width: 767.98px) {
    .q-whatsnew__item { flex-wrap: wrap; }
    .q-whatsnew__go { margin-left: auto; }
}

/* ---- MY CALENDAR'S LAYERS (PLAN §32 C1) --------------------------------------------------------------------------------
   Six FIXED hues, one per layer — never derived from --qm-primary, so an exam looks like an exam whatever the school's crest.
   Each has a strong value (the stripe, the icon, the legend's swatch) and a -soft tint (the block's ground). Text on a tint is
   always --qm-text-primary, which reads on both. Colour is never the only cue: every item also carries its layer's icon and word.
   Dark is the default (:root); the light theme redefines them darker so the stripe keeps its contrast on white. */
:root {
    --cal-layer-lessons: #94a3b8;
    --cal-layer-lessons-soft: rgba(148, 163, 184, 0.16);
    --cal-layer-duty: #fb923c;
    --cal-layer-duty-soft: rgba(251, 146, 60, 0.22);
    --cal-layer-invigilation: #c084fc;
    --cal-layer-invigilation-soft: rgba(192, 132, 252, 0.22);
    --cal-layer-meetings: #60a5fa;
    --cal-layer-meetings-soft: rgba(96, 165, 250, 0.22);
    --cal-layer-events: #34d399;
    --cal-layer-events-soft: rgba(52, 211, 153, 0.20);
    --cal-layer-suspensions: #22d3ee;
    --cal-layer-suspensions-soft: rgba(34, 211, 238, 0.16);
    --cal-break-soft: rgba(148, 163, 184, 0.10);
}
[data-theme="light"] {
    --cal-layer-lessons: #64748b;
    --cal-layer-lessons-soft: rgba(100, 116, 139, 0.10);
    --cal-layer-duty: #c2410c;
    --cal-layer-duty-soft: rgba(234, 88, 12, 0.13);
    --cal-layer-invigilation: #7e22ce;
    --cal-layer-invigilation-soft: rgba(126, 34, 206, 0.11);
    --cal-layer-meetings: #1d4ed8;
    --cal-layer-meetings-soft: rgba(29, 78, 216, 0.10);
    --cal-layer-events: #047857;
    --cal-layer-events-soft: rgba(4, 120, 87, 0.11);
    --cal-layer-suspensions: #0e7490;
    --cal-layer-suspensions-soft: rgba(14, 116, 144, 0.10);
    --cal-break-soft: rgba(100, 116, 139, 0.08);
}
/* An item on a layer: its colour comes in through --cal-c / --cal-c-soft, set by the layer class. */
.cal-layer--lessons { --cal-c: var(--cal-layer-lessons); --cal-c-soft: var(--cal-layer-lessons-soft); }
.cal-layer--duty { --cal-c: var(--cal-layer-duty); --cal-c-soft: var(--cal-layer-duty-soft); }
.cal-layer--invigilation { --cal-c: var(--cal-layer-invigilation); --cal-c-soft: var(--cal-layer-invigilation-soft); }
.cal-layer--meetings { --cal-c: var(--cal-layer-meetings); --cal-c-soft: var(--cal-layer-meetings-soft); }
.cal-layer--events { --cal-c: var(--cal-layer-events); --cal-c-soft: var(--cal-layer-events-soft); }
.cal-layer--suspensions { --cal-c: var(--cal-layer-suspensions); --cal-c-soft: var(--cal-layer-suspensions-soft); }

/* ================= Welfare: the student header strip and the tier chip (welfare audit U05, U10, 2026-10-11) ================= */
/* Picture · Timeline · Attendance as real links, Back as a link outside any phone sheet, the two actions at the end. */
.q-student-strip { display: flex; flex-wrap: wrap; align-items: center; gap: var(--q-row-gap); margin: 0 0 var(--qm-block-gap); }
.q-student-strip__back { display: inline-flex; align-items: center; gap: 6px; min-height: var(--q-control-h-sm); padding: 0 8px;
    color: var(--qm-text-secondary); font-size: var(--qm-text-base); text-decoration: none; border-radius: var(--qm-radius-md); }
.q-student-strip__back:hover { color: var(--qm-primary); background: var(--qm-bg-elevated); }
.q-student-strip__views { display: inline-flex; flex-wrap: wrap; gap: 4px; padding: 2px; border: 1px solid var(--qm-border);
    border-radius: var(--qm-radius-md); background: var(--qm-bg-card); }
.q-student-strip__view { display: inline-flex; align-items: center; gap: 6px; min-height: var(--q-control-h-sm); padding: 0 12px;
    font-size: var(--qm-text-base); color: var(--qm-text-secondary); text-decoration: none; border-radius: var(--qm-radius-sm); }
.q-student-strip__view:hover { color: var(--qm-text-primary); background: var(--qm-bg-elevated); }
.q-student-strip__view.is-current { background: var(--qm-primary-light); color: var(--qm-primary); font-weight: 600; }
.q-student-strip__view:focus-visible, .q-student-strip__back:focus-visible { outline: 2px solid var(--qm-primary); outline-offset: 2px; }
.q-student-strip__actions { display: inline-flex; flex-wrap: wrap; gap: var(--q-row-gap); margin-left: auto; }
@media (max-width: 767.98px) {
    .q-student-strip__views { flex: 1 1 100%; order: 2; }
    .q-student-strip__view { flex: 1 1 0; justify-content: center; min-height: 40px; }
}

/* A muted line of help under a field or in a dialog. Defined ONCE here (U11: two pages each defined it in their own block,
   which restyled every .form-hint in the app while either was open). */
.form-hint { font-size: var(--qm-text-sm); color: var(--qm-text-muted); margin: 0 0 10px; }

/* The batch actions menu (a Bootstrap dropdown) on the Student List and Welfare Reports — one style, defined once (U11). */
.batch-menu { min-width: 220px; padding: 6px; border: 1px solid var(--qm-border); border-radius: var(--qm-radius-md);
    background: var(--qm-bg-card); box-shadow: var(--qm-shadow-md); }
.batch-menu .dropdown-item { display: flex; align-items: center; gap: 9px; width: 100%; padding: 8px 10px; border: none; background: none;
    border-radius: var(--qm-radius-sm); font-size: var(--qm-text-base); text-align: left; color: var(--qm-text-primary); cursor: pointer; }
.batch-menu .dropdown-item:hover, .batch-menu .dropdown-item:focus-visible { background: var(--qm-primary-light); }
.batch-menu .dropdown-item .q-icon { color: var(--qm-text-muted); flex: none; }
.batch-menu .dropdown-item.batch-danger { color: var(--qm-danger); }
.batch-menu .dropdown-item.batch-danger .q-icon { color: var(--qm-danger); }
.batch-menu .dropdown-divider { margin: 6px 4px; border-top-color: var(--qm-border); }

/* The import history log, shared by the Student List and Welfare Reports (U11/U14: both pages carried a copy). */
.import-log { max-height: 360px; overflow-y: auto; border: 1px solid var(--qm-border); border-radius: var(--qm-radius-md); margin-top: 12px; }
.import-log-row { display: grid; grid-template-columns: 100px 110px 1fr 2fr; gap: 10px; padding: 8px 12px; font-size: var(--qm-text-sm);
    border-bottom: 1px solid var(--qm-border); align-items: center; color: var(--qm-text-primary); }
.import-log-row:last-child { border-bottom: none; }
.import-log-rownum { font-variant-numeric: tabular-nums; color: var(--qm-text-muted); white-space: nowrap; }
.import-log-outcome { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.outcome-created .import-log-outcome { color: var(--qm-success); }
.outcome-updated .import-log-outcome { color: var(--qm-primary); }
.outcome-duplicateinfile .import-log-outcome { color: var(--qm-text-primary); }
.outcome-alreadyexists .import-log-outcome { color: var(--qm-text-secondary); }
.outcome-failed .import-log-outcome { color: var(--qm-danger); }
.import-log-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.import-log-message { color: var(--qm-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 900px) { .import-log-row { grid-template-columns: 1fr; } }

/* One tier chip: the tier in WORDS ("High tier") on tokens. Medium keeps the theme's text colour on an amber tint with a
   border, because amber text on an amber tint at this size is about 3:1 (WCAG 1.4.3). */
.q-tier { display: inline-flex; align-items: center; gap: 4px; font-size: var(--qm-text-xs); font-weight: 600; line-height: 1.4;
    padding: 1px 8px; border-radius: 999px; white-space: nowrap; border: 1px solid var(--qm-border);
    background: var(--qm-bg-secondary); color: var(--qm-text-secondary); }
.q-tier--high { background: rgba(var(--qm-danger-rgb), 0.12); color: var(--qm-danger); border-color: rgba(var(--qm-danger-rgb), 0.35); }
.q-tier--medium { background: rgba(var(--qm-warning-rgb), 0.16); color: var(--qm-text-primary); border-color: rgba(var(--qm-warning-rgb), 0.5); }
@media print {
    .q-tier { background: none !important; color: #000 !important; border: 1px solid #000; }
    .q-tier--high { font-weight: 700; border-width: 2px; }
    .q-student-strip { display: none; }
}

/* QFactList / QFact (2026-10-12): read-only facts, a label beside its value on a desk and above it on a phone. */
.q-facts {
    display: grid;
    grid-template-columns: minmax(7rem, 36%) minmax(0, 1fr);
    gap: 8px 14px;
    margin: 0;
}
.q-fact { display: contents; }
.q-fact__label {
    display: flex;
    align-items: center;
    gap: 4px;
    margin: 0;
    color: var(--qm-text-muted);
    font-size: var(--qm-text-sm);
    font-weight: 500;
}
.q-fact__value {
    margin: 0;
    min-width: 0;
    overflow-wrap: anywhere;
    color: var(--qm-text-primary);
    font-size: var(--qm-text-base);
}
.q-fact__empty { color: var(--qm-text-muted); }
@media (max-width: 480px) {
    .q-facts { grid-template-columns: minmax(0, 1fr); gap: 2px 0; }
    .q-fact__value { margin-bottom: 8px; }
}
