/* ==========================================================================
   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;
}

.q-btn:focus {
    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 */
.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 — which also cut the date
   picker's popover off halfway down, because the calendar is absolutely positioned inside the
   filter card it sits in. Lift the clip only while a picker is actually open: :has() keeps this
   to the moment it matters instead of making every card in the app non-clipping. Browsers
   without :has() simply do not match this rule and get the old clipped behaviour, which is a
   degraded calendar rather than a broken card. */
.q-card:has(.q-datepicker__popover),
.filter-card:has(.q-datepicker__popover),
.q-modal__body:has(.q-datepicker__popover),
.q-card:has(.q-info__pop),
.filter-card:has(.q-info__pop),
.q-modal__body: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;
    width: 20px;
    height: 20px;
    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: 60;
    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); }

/* 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%;
    max-height: calc(100vh - 2rem);
    background: var(--qm-bg-card);
    border-radius: var(--q-modal-border-radius);
    box-shadow: var(--q-modal-shadow);
    overflow: hidden;
    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); }

.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);
}

.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;
}

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

.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);
}

.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);
}

.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);
}

.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;
}

/* ==========================================================================
   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; }

/* On a phone the popover is centred rather than anchored: 268px hanging off a
   field near the right edge of a 360px screen is exactly the overflow this
   release is fixing everywhere else. */
@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;
}

/* ==========================================================================
   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; opacity: 0.85; }
.q-timeline__card--annulled { opacity: 0.7; }
.q-timeline__card--annulled .q-timeline__title { text-decoration: line-through; }
.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; }

.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;
}

.q-stat__label {
    font-size: var(--qm-statcard-label);
    color: var(--qm-text-muted);
    text-transform: uppercase;
    letter-spacing: .03em;
}

.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);
}

.q-stat__sub--neutral { color: var(--qm-text-muted); background: var(--qm-bg-elevated); }
.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;
}

.q-bars__label a { color: inherit; text-decoration: none; }
.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);
}

.q-tabs__badge {
    min-width: 20px;
    padding: 1px 7px;
    border-radius: var(--qm-radius-full);
    background: var(--qm-primary);
    color: var(--qm-text-on-primary);
    font-size: var(--qm-text-sm);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    text-align: center;
}

.q-tabs__badge--danger { background: var(--qm-danger); }

@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); }

.q-chip--mono { font-family: var(--qm-font-mono); }

/* 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 {
    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); }

.q-avatar__img { 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);
    }

    table.q-stack > tbody > tr > td {
        display: flex;
        align-items: flex-start;
        justify-content: space-between;
        gap: 12px;
        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);
        flex: 0 0 38%;
        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=""] {
        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; }

    /* 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 phone button is at least 40px tall. */
    .qm-main .q-btn--sm { min-height: 40px; }
}

/* QBarList Stacked: label and figure share a line, the bar spans beneath (a narrow card's layout). */
.q-bars--stacked .q-bars__row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 4px; }
.q-bars--stacked .q-bars__label { grid-row: 1; grid-column: 1; }
.q-bars--stacked .q-bars__count { grid-row: 1; grid-column: 2; font-weight: 500; color: var(--qm-text-secondary); }
.q-bars--stacked .q-bars__track { grid-row: 2; grid-column: 1 / -1; }
.q-bars--stacked .q-bars__label { white-space: normal; }
.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; }
.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; }
.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 keeps a 40px tap target on every control. */
    .qm-main .q-btn,
    .qm-main .q-select,
    .qm-main .q-datepicker__trigger { min-height: 40px; }
    .qm-main input.q-input { height: 40px; }
    .qm-main .q-btn:has(> .q-btn__icon:only-child) { width: 40px; }
}

/* 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;
}
