/* =====================================================================
   Shared module styles.

   Everything the design spec calls common lives here, once: the action
   bar, toasts, the full screen stepped panel, the clearable search, the
   drag handle. A module stylesheet holds only what is its own.

   Load this before the module's own sheet.
   ===================================================================== */


/* ---------------------------------------------------------------------
   Action bar  ·  spec §3
   One bar for anything pending a decision.
   --------------------------------------------------------------------- */

.action-bar-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1035;
    background-color: rgba(0, 0, 0, .18);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;          /* the page underneath stays usable */
    transition: opacity .25s ease, visibility .25s ease;
}

.action-bar-backdrop.is-visible {
    opacity: 1;
    visibility: visible;
}

.action-bar {
    position: fixed;
    left: 50%;
    bottom: 24px;
    z-index: 1040;

    /* Wide enough for what it actually holds. At 780px a screen offering four actions and a
       Clear wrapped onto a second line, and a bar that changes height as you select is a bar
       that pushes its own buttons out from under the pointer. */
    width: min(940px, calc(100% - 32px));
    transform: translate(-50%, 120%);
    opacity: 0;
    visibility: hidden;
    transition: transform .25s ease, opacity .25s ease, visibility .25s ease;
}

.action-bar.is-visible {
    transform: translate(-50%, 0);
    opacity: 1;
    visibility: visible;
}

.action-bar-inner {
    display: flex;
    align-items: center;
    gap: .5rem;

    /* One line. The sentence gives up its width first, the buttons keep theirs -- they are
       what the bar is for. */
    flex-wrap: nowrap;
    padding: .75rem 1rem;
    border: 1px solid var(--bs-border-color);
    /* The theme rounds a card by 5px. The bar is a card that arrived from the bottom, not
       a different kind of object, and a second radius in the same screen is noticed even
       when it cannot be named. */
    border-radius: 5px;
    background-color: var(--bs-body-bg, #fff);
    box-shadow: var(--box-shadow-lg, 0 12px 32px rgba(0, 0, 0, .18)), 0 0 0 4px rgba(var(--primary-rgb), .12);
}

/* left indicator, one per flavour */
.action-bar-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 28px;
    height: 28px;
    padding: 0 .5rem;
    border-radius: 14px;
    background-color: var(--primary);
    color: #fff;
    font-size: 13px;
    font-weight: 600;
}

.action-bar-pulse {
    position: relative;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background-color: var(--bs-warning);
    flex-shrink: 0;
}

.action-bar-pulse::after {
    content: "";
    position: absolute;
    inset: -5px;
    border-radius: 50%;
    border: 1px solid var(--bs-warning);
    opacity: .5;
}

/* The half that gives way: the count and its sentence. `min-width: 0` is what lets a flex
   child shrink below its text at all. */
.action-bar-inner > .me-auto {
    min-width: 0;
}

.action-bar-inner > .me-auto h6,
.action-bar-inner > .me-auto p {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.action-bar-inner .btn {
    white-space: nowrap;
}

/* On a phone there is no width to give, so it wraps and the page reserves more room. */
@media (max-width: 575.98px) {
    .action-bar-inner { flex-wrap: wrap; }

    body.has-action-bar { padding-bottom: 148px; }
}

body.has-action-bar {
    padding-bottom: 96px;
}


/* ---------------------------------------------------------------------
   Clearable search  ·  spec §10
   The box carries its own ×; there is never a chip for a search.
   --------------------------------------------------------------------- */

.search-clearable {
    position: relative;
}

.search-clearable .form-control {
    padding-right: 2rem;
}

.search-clear {
    position: absolute;
    top: 50%;
    right: .5rem;
    transform: translateY(-50%);
    z-index: 4;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background-color: transparent;
    color: var(--bs-secondary-color);
    line-height: 1;
}

.search-clear:hover {
    background-color: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
}


/* ---------------------------------------------------------------------
   Toasts  ·  spec §4

   The template's own toast (`ui-toasts.html`): Bootstrap's `.toast` with
   a header — icon, title, arrival time, close — and a body. What is added
   is what a notification needs and a static page could not show: the kind
   said in colour, a bar counting the time down, and room for buttons.

   The colour appears three times and always the same three: the header's
   wash, the icon, and the bar. One glance answers "what kind is this?"
   from any of them.
   --------------------------------------------------------------------- */

/* The host a screen declares in its own markup. Positioned like any other
   toast container, so a screen's host and a kit-made one look identical. */
.app-toast-host {
    position: fixed;
    top: 84px;
    right: 24px;
    z-index: 1090;
    display: flex;
    flex-direction: column;
    gap: .625rem;
    pointer-events: none;
}

.app-toast-host > .toast,
.toast-container > .toast {
    pointer-events: auto;
}

.app-toast {
    --bs-toast-max-width: 380px;
    /* The theme's own card radius and the theme's own shadow token — a toast is a
       small card and has no business rounding differently from every other one. */
    --bs-toast-border-radius: 5px;
    --bs-toast-box-shadow: var(--box-shadow-lg, 0 4.4px 12px -1px rgba(222, 222, 222, .36));
    --bs-toast-padding-x: .875rem;

    /* Bootstrap's toast is deliberately translucent, which is fine over a
       hero image and wrong over a table: the rows read straight through
       the message. A notification is not decoration — it is opaque. */
    --bs-toast-bg: var(--bs-body-bg);
    --bs-toast-border-color: var(--bs-border-color);
    position: relative;
    overflow: hidden;
    backdrop-filter: none;
}

/* ---- the header, washed in the kind's own colour ---- */

.app-toast .toast-header {
    padding: .5rem .875rem;
    border-bottom-color: transparent;
    font-size: 13px;
}

.app-toast .toast-header strong {
    font-size: 13px;
    font-weight: 600;
    letter-spacing: -.01em;
}

.app-toast .toast-header small {
    font-size: 11px;
    opacity: .8;
}

.app-toast-icon {
    font-size: 15px;
}

.app-toast-success .toast-header {
    background-color: var(--bs-success-bg-subtle);
    color: var(--bs-success-text-emphasis);
}

.app-toast-danger .toast-header {
    background-color: var(--bs-danger-bg-subtle);
    color: var(--bs-danger-text-emphasis);
}

.app-toast-warning .toast-header {
    background-color: var(--bs-warning-bg-subtle);
    color: var(--bs-warning-text-emphasis);
}

.app-toast-info .toast-header {
    background-color: var(--bs-info-bg-subtle);
    color: var(--bs-info-text-emphasis);
}

.app-toast-success .app-toast-icon { color: var(--bs-success); }
.app-toast-danger .app-toast-icon { color: var(--bs-danger); }
.app-toast-warning .app-toast-icon { color: var(--bs-warning); }
.app-toast-info .app-toast-icon { color: var(--bs-info); }

/* The close button inherits the header's text colour rather than staying
   black on a tinted band. */
.app-toast .toast-header .btn-close {
    --bs-btn-close-opacity: .5;
    --bs-btn-close-hover-opacity: .9;
    width: .7em;
    height: .7em;
}

/* ---- the body ---- */

.app-toast .toast-body {
    padding: .625rem .875rem .75rem;
    font-size: 13px;
    line-height: 1.45;
    color: var(--bs-body-color);
}

.app-toast-text {
    display: block;
}

/* Buttons sit under the sentence, right-aligned, the way a dialog's do. */
.app-toast-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: .375rem;
    margin-top: .625rem;
}

.app-toast-actions .btn {
    --bs-btn-padding-y: .25rem;
    --bs-btn-padding-x: .625rem;
    --bs-btn-font-size: 12px;
    --bs-btn-font-weight: 500;
    --bs-btn-border-radius: 5px;
    display: inline-flex;
    align-items: center;
}

/* The quieter alternative beside it: a link, not a second filled button. */
.app-toast-actions .btn-link {
    --bs-btn-color: var(--bs-secondary-color);
    --bs-btn-hover-color: var(--bs-body-color);
    text-decoration: none;
}

.app-toast-actions .btn-link:hover {
    text-decoration: underline;
}

/* ---- the countdown ----

   A line along the bottom, draining left to right for exactly as long as
   the toast will live. It answers the question a timed message always
   raises — "how long have I got?" — and it holds while the cursor is on
   the card, because reading should not cost you the message. */
.app-toast-progress {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 3px;
    background-color: var(--bs-tertiary-bg);
}

.app-toast-progress span {
    display: block;
    height: 100%;
    width: 100%;
    transform-origin: left center;
    background-color: var(--bs-secondary-color);
    animation-name: app-toast-drain;
    animation-timing-function: linear;
    animation-fill-mode: forwards;
}

.app-toast-success .app-toast-progress span { background-color: var(--bs-success); }
.app-toast-danger .app-toast-progress span { background-color: var(--bs-danger); }
.app-toast-warning .app-toast-progress span { background-color: var(--bs-warning); }
.app-toast-info .app-toast-progress span { background-color: var(--bs-info); }

@keyframes app-toast-drain {
    from { transform: scaleX(1); }
    to { transform: scaleX(0); }
}

@media (max-width: 767.98px) {
    .app-toast-host {
        left: 16px;
        right: 16px;
    }

    .app-toast {
        --bs-toast-max-width: none;
    }
}

/* ---- the same message, twice ----

   Deduplication by `id` (spec §16.2): rather than stack two identical toasts, the one
   already on screen counts up. Small, quiet, and beside the title — it is a footnote to
   the message, not a second message. */
.app-toast-count {
    margin-right: .5rem;
    padding: 0 .375rem;
    border-radius: 999px;
    background-color: var(--bs-tertiary-bg);
    color: var(--bs-secondary-color);
    font-size: .6875rem;
    font-weight: 600;
    line-height: 1.35;
}

/* ---- somebody who asked for no motion ----

   `prefers-reduced-motion` is not a preference about decoration; for some people movement
   causes nausea or migraine. So the fade and the draining bar go, and the toast simply
   appears and simply leaves. It still counts down — the TIMING is not the problem, the
   moving pixels are (spec §16.2). */
.app-toast-still {
    opacity: 1;
    transition: none;
}

@media (prefers-reduced-motion: reduce) {
    .app-toast,
    .app-toast.fade {
        transition: none;
    }

    .app-toast-progress span {
        animation: none;
    }
}

/* THE PER-PERSON HALF OF THE SAME ANSWER (22 Sep 2026).

   `reduce_motion` is a preference on the Account page, and from 22 Sep 2026
   `<html>` carries `data-reduce-motion` when it is on. A media query cannot
   be driven by an attribute, so every `prefers-reduced-motion` block in this
   file and in `layout.css` is now followed by a companion carrying the same
   declarations under `html[data-reduce-motion]`.

   Duplicated rather than merged into one selector list, because a browser
   drops a whole rule it cannot parse: put `@media` and an attribute selector
   in one group and an engine that dislikes either loses BOTH, which would
   silently take away the OS setting that works today. Four extra rules
   against that risk is the right trade.

   The companion is a COPY, never an extension. If these two ever say
   different things, the switch means something different from the OS
   setting, and nobody reading the Account page would guess it. */
html[data-reduce-motion] .app-toast,
html[data-reduce-motion] .app-toast.fade {
    transition: none;
}

html[data-reduce-motion] .app-toast-progress span {
    animation: none;
}


/* ---------------------------------------------------------------------
   Full screen stepped panel  ·  spec §5 and §6
   Covers the content area only; the sidebar and topbar stay usable.
   --------------------------------------------------------------------- */

.uf-overlay {
    position: fixed;
    inset: var(--topbar-height, 56px) 0 0 var(--sidenav-width, 240px);
    z-index: 1045;
    visibility: hidden;
    pointer-events: none;
}

.uf-overlay.is-open {
    visibility: visible;
    pointer-events: auto;
}

.uf-scrim {
    position: absolute;
    inset: 0;
    background-color: rgba(0, 0, 0, .35);
    opacity: 0;
    transition: opacity .28s ease;
}

.uf-overlay.is-open .uf-scrim {
    opacity: 1;
}

.uf-shell {
    position: absolute;
    inset: 12px;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    opacity: 0;
    transform: translateY(18px) scale(.985);
    transition: opacity .28s ease, transform .28s ease;
}

.uf-overlay.is-open .uf-shell {
    opacity: 1;
    transform: none;
}

/* the middle band is the only part that scrolls */
.uf-scrollable {
    flex: 1 1 auto;
    overflow-y: auto;
}

/* a column that keeps pace with the form beside it while that form scrolls */
.uf-sticky {
    position: sticky;
    top: 0;
}

@media (max-width: 1199.98px) {
    .uf-sticky { position: static; }
}

.border-dashed {
    border-style: dashed !important;
}

@media (prefers-reduced-motion: reduce) {
    .action-bar,
    .action-bar-backdrop,
    .app-toast,
    .uf-shell,
    .uf-scrim {
        transition: none;
    }
}

/* The per-person half — see the note beside the toast block above. */
html[data-reduce-motion] .action-bar,
html[data-reduce-motion] .action-bar-backdrop,
html[data-reduce-motion] .app-toast,
html[data-reduce-motion] .uf-shell,
html[data-reduce-motion] .uf-scrim {
    transition: none;
}

@media (max-width: 991.98px) {
    .uf-overlay { inset: var(--topbar-height, 56px) 0 0 0; }
    .uf-shell { inset: 0; border-radius: 0; }
}


/* ---------------------------------------------------------------------
   The stepper in a panel header  ·  spec §6
   --------------------------------------------------------------------- */

.uf-stepper { display: flex; align-items: center; gap: 0; list-style: none; padding: 0; }
.uf-stepper li { display: flex; align-items: center; }

.uf-stepper li + li::before {
    content: "";
    width: 16px;
    height: 1px;
    background: var(--bs-border-color);
}

.uf-step {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 3px 8px;
    border-radius: 20px;
    text-decoration: none;
    color: var(--bs-secondary-color);
    transition: background .15s ease;
}

.uf-step:hover { background: var(--bs-tertiary-bg); }

.uf-step-dot {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 11px;
    font-weight: 600;
    line-height: 1;
    border: 1px solid var(--bs-border-color);
    background: var(--bs-body-bg);
}

.uf-step-text { font-size: 12px; }

.uf-step.is-active .uf-step-dot {
    background: var(--primary);
    border-color: var(--primary);
    color: #fff;
}

.uf-step.is-active .uf-step-text { color: var(--bs-body-color); font-weight: 500; }

.uf-step.is-done .uf-step-dot {
    background: var(--bs-success);
    border-color: var(--bs-success);
    color: #fff;
}

.uf-step.is-done .uf-step-text { color: var(--bs-body-color); }

@media (max-width: 1199.98px) { .uf-step-text { display: none; } }


/* ---------------------------------------------------------------------
   Field groups inside a stage  ·  spec §6
   --------------------------------------------------------------------- */

.uf-group + .uf-group {
    margin-top: 22px;
    padding-top: 22px;
    border-top: 1px dashed var(--bs-border-color);
}

.uf-group-title {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--bs-secondary-color);
    margin-bottom: 12px;
}


/* ---------------------------------------------------------------------
   Drag handle  ·  spec §8c
   Beside the record's own actions, quiet at rest, full on hover.
   --------------------------------------------------------------------- */

.drag-handle {
    display: inline-flex;
    align-items: center;
    padding: 4px 0;
    line-height: 1;
    color: #000;
    cursor: grab;
    transition: opacity .15s ease;
}

.is-dragging .drag-handle { cursor: grabbing; }

/* The handle refuses the browser's own gestures BEFORE one starts. On touch, whether a
   drag scrolls the page or moves the row is decided at first contact, from the CSS -- by
   the time a move event arrives it is too late to say. */
[data-drag] { touch-action: none; }

/* While a row is being carried: no text selected across the rows it passes over. */
.is-reordering { user-select: none; touch-action: none; }

/* The whole page is in a drag: the cursor says so wherever it goes, not only over the grip. */
.is-reordering * { cursor: grabbing !important; }


/* ---------------------------------------------------------------------
   A row being moved

   The row itself, marked while it travels -- no floating copy of it, and
   no gap left where it was. It sits between the rows it is passing, in
   the platform's own colour, which means it is always exactly where it
   would land if you let go.

   The same treatment wherever a drag exists: the types rail, the sections
   table, and anything a module makes draggable with `orderUrl`.
   --------------------------------------------------------------------- */

.is-dragging {
    background-color: rgba(var(--primary-rgb), .07) !important;
    box-shadow: 0 2px 10px rgba(var(--primary-rgb), .18);
    outline: 1px solid rgba(var(--primary-rgb), .45);
    outline-offset: -1px;
    border-radius: 5px;
    position: relative;
    z-index: 2;
}

/* A table row cannot carry a background of its own in every theme -- the cells paint over
   it -- so the cells take it too. */
.is-dragging > td { background-color: rgba(var(--primary-rgb), .07) !important; }

/* The whole page is in a drag: the cursor says so wherever it goes, not only over the
   grip. */
.is-reordering * { cursor: grabbing !important; }

/* Sorted by something other than its own order: the grips say so before anybody pulls one.
   They stay in place rather than disappearing -- a column of handles that comes and goes as
   you sort is a list that changes shape for no reason a reader can see. */
[data-reorder="off"] [data-drag] {
    /* !important because a module may raise its grip on hover, and "you cannot use this"
       must win over "the pointer is near it". */
    opacity: .3 !important;
    cursor: not-allowed;
}

/* ---------------------------------------------------------------------
   The theme's phone control  ·  spec §8
   Its country list needs flag, name and dial code on one line.
   --------------------------------------------------------------------- */

.iti { width: 100%; }

.iti__country-list {
    max-height: 260px;
    z-index: 1090;
    text-align: left;
    background: var(--bs-body-bg, #fff);
    border: 1px solid var(--bs-border-color, #e5e7eb);
    border-radius: 6px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, .12);
}

.iti__country-list .iti__flag-box,
.iti__country-list .iti__country-name,
.iti__country-list .iti__dial-code {
    display: inline-block;
    vertical-align: middle;
}

.iti__country-list .iti__country { white-space: nowrap; }
.iti__country-list .iti__country-name { margin: 0 6px; }


/* ---------------------------------------------------------------------
   The map inside a form panel  ·  spec §6b
   --------------------------------------------------------------------- */

.af-map {
    height: 340px;
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius, .375rem);
    z-index: 0;
}

.af-map .leaflet-control-attribution {
    font-size: 10px;
}

@media (max-width: 767.98px) {
    .af-map { height: 260px; }
}


/* ---------------------------------------------------------------------
   A record read through  ·  spec §12
   Label beside value, two to a row. Used wherever a record is shown
   rather than edited.
   --------------------------------------------------------------------- */

.rec-facts {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0 28px;
}

@media (max-width: 767.98px) {
    .rec-facts { grid-template-columns: minmax(0, 1fr); }
}

.rec-row {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 9px 0;
    border-bottom: 1px solid var(--bs-border-color);
    min-width: 0;
}

.rec-row:last-child,
.rec-row.is-last {
    border-bottom: 0;
}

/* a value that spans both columns - an address, a biography */
.rec-row.is-wide {
    grid-column: 1 / -1;
}

.rec-label {
    flex: 0 0 132px;
    font-size: 12px;
    color: var(--bs-secondary-color);
    padding-top: 1px;
}

.rec-value {
    flex: 1 1 auto;
    font-size: 13px;
    font-weight: 500;
    min-width: 0;
    overflow-wrap: anywhere;
}

.rec-value.is-empty {
    font-weight: 400;
    color: var(--bs-tertiary-color, #adb5bd);
}

/* ---------------------------------------------------------------------
   A row a bulk action refused.

   The kit adds `.is-bulk-failed` and a `title` saying why, to the row AND
   the card, so the reason sits next to the record it belongs to rather
   than in a toast that has already gone. Deliberately quiet: it marks,
   it does not shout, because the ones that worked are gone from the
   selection and only these are left.
   --------------------------------------------------------------------- */

.is-bulk-failed {
    position: relative;
    background-color: rgba(var(--bs-danger-rgb), .04);
}

.is-bulk-failed > td:first-child,
.is-bulk-failed .card {
    box-shadow: inset 3px 0 0 0 var(--bs-danger);
}

/* ---------------------------------------------------------------------
   A row's action menu must not be clipped by the table's own scroller.

   `.table-responsive` is `overflow: auto`, which clips its children as
   well as scrolling them -- so a dropdown opened on the last visible row
   is cut off at the edge of the table. It is invisible on a full page and
   obvious on a short one, which is exactly the wrong way round for a bug
   to behave.

   The clip is lifted only while a menu is actually open, so horizontal
   scrolling on a wide table still works the rest of the time.
   --------------------------------------------------------------------- */

.table-responsive:has(.dropdown-menu.show) {
    overflow: visible;
}


/* ---------------------------------------------------------------------
   Responsive collapse  ·  K.7f

   What the table could not fit, stacked under the row's first column and
   labelled -- a value with no name is a puzzle, and "1 day ago" only
   means something beside the word "Last Activity".

   The treatment is the one the design already uses for the second line
   of the Name cell: same muted colour, same 13px, same spacing. It is
   meant to read as more of that row, not as a panel bolted underneath.
   --------------------------------------------------------------------- */

.list-stack {
    display: flex;
    flex-direction: column;
    gap: .15rem;
    margin-top: .35rem;
}

.list-stack-item {
    display: flex;
    align-items: baseline;
    gap: .4rem;
    font-size: .8125rem;
    font-weight: 400;
    color: var(--bs-secondary-color, #6b7280);
    white-space: normal;
}

.list-stack-label {
    flex: 0 0 auto;
    min-width: 5.5rem;
    font-size: .75rem;
    color: var(--bs-tertiary-color, #9aa3b2);

    /* The label names the value; wrapping it over two lines turns a caption
       into a paragraph. The VALUE is what may wrap -- `.table-nowrap` is
       lifted here for the same reason. */
    white-space: nowrap;
}

.list-stack-item > :not(.list-stack-label) {
    white-space: normal;
}

/* Anything the row renderer put in a cell comes across as-is -- a status
   badge stays a status badge. It just should not carry the cell's own
   block spacing into a line half its height. */
.list-stack-item .badge {
    vertical-align: baseline;
}

.list-stack-item > .form-check,
.list-stack-item > .dropdown {
    display: none;
}


/* ---------------------------------------------------------------------
   Keyboard focus  ·  K.7f

   A row is reachable and operable from the keyboard, so it must also be
   VISIBLE when it has focus -- an outline nobody can see is the same as
   no outline at all.

   `:focus-visible`, not `:focus`: a mouse click should not leave a ring
   behind it. The ring is inset so it reads as the row's own edge rather
   than a box drawn on top of the table.
   --------------------------------------------------------------------- */

/* `outline`, not an inset shadow: a shadow on a `<tr>` is painted by the
   row box, which under `border-collapse` is only the left edge -- the
   ring appeared as a stripe down one side and nowhere else. An outline
   is drawn around the element itself and works on both. */
/* The rows are focusable (`tabindex`), so the browser draws its OWN ring
   on whichever element has focus. On a grid item that element is the
   COLUMN WRAPPER, and a wrapper's ring is drawn at its border box --
   outside the `g-3` gutter padding -- so it appeared as a second blue
   line about eight pixels clear of the card's own border. That was the
   double border, and it was the browser's, not ours.
   
   Suppress it everywhere and put the ring back deliberately below, on
   the element that should carry it. */
[data-id]:focus,
[data-id]:focus-visible,
th[data-col]:focus,
th[data-col]:focus-visible {
    outline: none;
}

/* A table row wears the ring itself. */
tr[data-id]:focus-visible,
tr[data-id].is-current {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
}

/* A row that is NOT a table row and holds no card -- the types rail --
   wears it itself too. The `:not(:has(> .card))` is the whole point: a
   grid item is a bare column wrapper AROUND a card, and ringing both the
   wrapper and the card drew two borders, one just outside the other. */
[data-id]:not(tr):not(:has(> .card)):focus-visible,
[data-id]:not(tr):not(:has(> .card)).is-current {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
}

/* `.is-current` is the row somebody is ON -- clicked, or arrived at with
   the arrow keys. It is NOT selection: selection is the checkbox, the
   tint and the bulk bar, and it takes Space or the box itself. Pointing
   at a row should never arm a Delete button.

   The two can be true at once, and then the tint and the border are both
   there, which is exactly right: this is the row I am on, and it is one
   of the ones I have chosen. */

/* A card already has a border and a radius, so the ring follows it. */
/* A card already HAS a border, and the design already turns that border
   primary when the card is chosen. Adding an outline on top drew a second
   blue line a pixel outside the first -- the double border.
   
   So a card says "current" the way a card should: its own border goes
   primary and a soft halo sits behind it. One line, not two. */
/* INSET, not a halo outside the card.

   The halo was `0 0 0 3px` — a ring sitting immediately outside a 1px
   border. On a card that is only current that reads as one edge with a
   glow. On a card that is BOTH current and selected the border is already
   primary, so the ring became a second primary line a pixel outside the
   first: the double border again, in a new place (owner, 2 Sep 2026).

   Inset thickens the edge the card already has instead of adding one
   beside it. Current and selected then compose — a heavier border and the
   selected tint — rather than stacking two outlines. */
/* ONE line, always.

   A card carries its own 1px border, and `.is-selected` already turns that
   border primary. Anything the CURRENT state adds on top of a selected
   card — a halo outside it, a ring inset within it — lands immediately
   beside that border and reads as a second line. Both were tried and both
   doubled (owner, 2 Sep 2026, twice).

   So current and selected never draw at the same time. Selected wins,
   because it is the state with consequences: the bulk bar is armed and a
   Delete button is pointing at this record. Current on an unselected card
   still says where you are, with the same single primary edge. */
[data-id]:focus-visible:not(.is-selected) > .card,
[data-id].is-current:not(.is-selected) > .card {
    border-color: var(--primary) !important;
    box-shadow: none !important;
}



th[data-col]:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: -2px;
}

/* Sortable headings are buttons in everything but name; say so to the
   pointer as well. */
th[data-sort] {
    cursor: pointer;
}


/* ---------------------------------------------------------------------
   Row detail  ·  K.7

   Your template has no disclosure treatment, so this is built from what
   it does have: the same muted body text, the same border colour, and
   the chevron from Tabler that the rest of the UI already uses. The
   panel is part of the row, not a card dropped on top of one -- so it
   shares the row's left edge and carries no shadow of its own.
   --------------------------------------------------------------------- */

/* The disclosure column holds a 22px button and nothing else, so it takes
   the width of that button and no more. `width: 1%` is the table idiom
   for "as narrow as your content" -- left to itself the column claimed
   48px and took it from Status, which then truncated to "S...". */
/* An explicit width, not `1%`. The idiom shrinks a column to its content
   only when the rest of the table is willing to take the space, and this
   table is not -- every heading now carries a drag grip, so they are all
   asking for a few more pixels than their words need. Left at `auto` the
   disclosure column claimed 48px for a 22px button and took the
   difference out of Status, which truncated to "S...". */
.table > :not(caption) > * > [data-col="expand"] {
    width: 34px;
    min-width: 34px;
    max-width: 34px;
    padding-left: .375rem;
    padding-right: .25rem;
    text-align: center;
}

.list-expander {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--bs-secondary-color, #6b7280);
    line-height: 1;
    transition: transform .15s ease-in-out, background-color .15s ease-in-out;
}

.list-expander:hover {
    background-color: var(--bs-tertiary-bg, #f1f3f5);
    color: var(--bs-body-color);
}

/* The chevron turns rather than swapping to a different glyph: the
   rotation IS the statement that this is the same control in another
   state, and a swap reads as two different buttons. */
.list-expander.is-open {
    transform: rotate(90deg);
}

.list-detail > td {
    padding: 0;
    border-top: 0;
    background-color: var(--bs-tertiary-bg, #f8f9fa);
}

.list-detail-body {
    padding: .875rem 1rem .875rem 3rem;
    font-size: .8125rem;
    color: var(--bs-secondary-color, #6b7280);

    /* Indented to the row's first real column, so the panel reads as
       belonging to the row above it rather than to the table. */
    box-shadow: inset 3px 0 0 0 var(--bs-border-color);
}

/* A grid of small labelled facts is what one of these almost always
   holds, so the kit offers the shape and a module can ignore it. */
.list-detail-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: .5rem 1.5rem;
}

.list-detail-grid > div {
    display: flex;
    flex-direction: column;
    gap: .1rem;
    min-width: 0;
}

.list-detail-label {
    font-size: .6875rem;
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--bs-tertiary-color, #9aa3b2);
}

.list-detail-value {
    color: var(--bs-body-color);
    overflow-wrap: anywhere;
}


/* ---------------------------------------------------------------------
   The totals line  ·  K.7

   A summary of the table, not another row of it. Your template has no
   `<tfoot>`, so this is built to read as the quietest thing on the
   screen: 12px, muted, one rule above, and no hover or cursor -- a
   footer styled like data invites somebody to try to click it.
   --------------------------------------------------------------------- */

.table-foot td {
    padding-top: .625rem;
    padding-bottom: .625rem;
    border-top: 1px solid var(--bs-border-color);
    border-bottom: 0;
    background-color: transparent;
    font-size: .75rem;
    color: var(--bs-tertiary-color, #9aa3b2);
    white-space: nowrap;
}

/* The numbers themselves carry the weight; the words around them are
   there to say what the number is. */
.table-foot [data-agg],
.table-foot #manage-users-foot-count {
    font-weight: 500;
    color: var(--bs-secondary-color, #6b7280);
    font-variant-numeric: tabular-nums;
}

/* A footer cell whose column is hidden or folded goes with it -- the kit
   sets `display` per `[data-col]`, and these are `[data-col]` cells like
   any other, which is the whole reason they are marked that way. */


/* ---------------------------------------------------------------------
   Sortable headings  ·  the only thing on a header

   The theme already styles a stacked chevron pair for sortable columns
   -- tabler-icons, 14px, right-aligned -- but scoped to
   `table.dataTable`, which stopped matching the day DataTables was
   dropped. Sortable headings have looked identical to fixed ones ever
   since. This is that same treatment, re-pointed at `[data-sort]`.

   Both chevrons show faintly at rest, meaning "this can be sorted"; the
   one that is active goes solid, meaning "and it is, this way".
   --------------------------------------------------------------------- */

th[data-sort] {
    position: relative;
    padding-right: 1.75rem !important;
    cursor: pointer;
    user-select: none;
}

th[data-sort]::before,
th[data-sort]::after {
    position: absolute;
    right: .625rem;
    display: block;
    font-family: "tabler-icons";
    font-size: 14px;
    line-height: 7px;
    color: var(--bs-body-color);
    opacity: .28;
    transition: opacity .15s ease-in-out;
}

th[data-sort]::before {
    top: 50%;
    margin-top: -8px;
    content: "\ea62";   /* chevron-up, per tabler-icons.min.css */
}

th[data-sort]::after {
    top: 50%;
    margin-top: 1px;
    content: "\ea5f";   /* chevron-down, per tabler-icons.min.css */
}

th[data-sort]:hover::before,
th[data-sort]:hover::after {
    opacity: .5;
}

th[data-sort].is-sorted-asc::before,
th[data-sort].is-sorted-desc::after {
    opacity: 1;
    color: var(--primary);
}

th[data-sort].is-sorted-asc::after,
th[data-sort].is-sorted-desc::before {
    opacity: .2;
}

/* ---------------------------------------------------------------------
   The confirm dialog  ·  K.4

   The theme's own modal, small and centred, with the tinted round icon
   the design already uses for an empty state. Nothing invented: the
   colours come from the tone (`danger`, `warning`) so one dialog serves
   "delete this" and "this cannot be undone" without a second stylesheet.
   --------------------------------------------------------------------- */

.app-confirm .modal-content {
    border: 0;
    border-radius: .75rem;
}

.app-confirm .app-confirm-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/* Above the offcanvas panels, which sit at 1045 -- a confirm raised from
   inside a panel must not open behind it. */
.app-confirm {
    z-index: 1080;
}

/* ---------------------------------------------------------------------
   "Filtered by" -- one strip saying everything currently narrowing the
   list, whatever put it there: the search box, a tab, the date range, the
   sort dropdown. Each chip clears just itself; "Clear all" clears the lot.

   The design is the screen's own: the pill chips that used to sit in the
   tab row, kept exactly -- 20px radius, 12px text, hairline border on the
   body background -- because that strip was already the design's answer to
   "what is narrowing this list". What changed is only what it can say.
   --------------------------------------------------------------------- */
.list-chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem;
    margin-bottom: .75rem;
}

.list-chips-lead {
    font-size: 12px;
    color: var(--bs-secondary-color);
}

.list-chip {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .2rem .5rem;
    border: 1px solid var(--bs-border-color);
    border-radius: 20px;
    background-color: var(--bs-body-bg, #fff);
    font-size: 12px;
}

.list-chip-label {
    color: var(--bs-secondary-color);
}

.list-chip-value {
    font-weight: 500;
    max-width: 16rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.list-chip-clear {
    padding: 0;
    border: 0;
    background: none;
    line-height: 1;
    color: var(--bs-secondary-color, #6c757d);
}

.list-chip-clear:hover,
.list-chip-clear:focus-visible {
    color: var(--bs-danger);
}

.list-chips-reset {
    font-size: 12px;
}
/* ---------------------------------------------------------------------
   The row under the list -- page size, the count, the pager.

   It used to sit hard against the table: the last row's border, the
   horizontal scrollbar and the pager were all touching, which read as one
   crowded block rather than "the list, and then the controls for it".

   A rule and a gap separate them, matching the space the toolbar already
   leaves ABOVE the table so the list sits evenly between the two.
   --------------------------------------------------------------------- */
.list-footer {
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--bs-border-color);
    row-gap: .75rem;
}

/* Stacked on a narrow screen, the two halves need room between them, and
   the pager centres rather than hanging off the right edge. */
@media (max-width: 767.98px) {
    .list-footer .pagination {
        margin-top: .25rem;
    }
}

/* The theme gives `.dataTables_length` a 20px top padding and a float, both of
   which existed to seat it under a DataTables wrapper. Inside the footer row the
   row itself already provides the space, and the float would drop it out of the
   flex line. */
.list-footer .dataTables_length {
    float: none;
    padding-top: 0;
}


/* ---------------------------------------------------------------------
   The waiting state  ·  liquid dots

   Every placeholder is a grid of small dots rather than a solid bar, and
   colour runs THROUGH that grid like liquid through a mesh: a slow band
   of the platform's own accent, drifting diagonally, brightening each dot
   as it passes and leaving it settled behind.

   How it is made — two layers and a mask:

     1. the dots are a `mask`, not a drawing: one radial gradient tiled at
        6px, so everything painted underneath can only show THROUGH the
        dots;
     2. under the mask, a wide multi-stop gradient drifts across the block
        — that is the liquid;
     3. a dim grey copy sits beneath both, so a dot the liquid has not
        reached is still visible and the block keeps its shape.

   Nothing here is an image and nothing is a canvas: it is one element per
   placeholder, so a page of forty costs nothing to animate.
   --------------------------------------------------------------------- */

.skeleton {
    position: relative;
    display: inline-block;
    height: 12px;
    border-radius: 5px;
    overflow: hidden;

    /* Layer 3: the resting grid. Dots the liquid has not reached. */
    background-image: radial-gradient(
        circle at center,
        var(--bs-secondary-bg) 1.15px,
        transparent 1.35px
    );
    background-size: 6px 6px;
    background-position: center;
}

/* Layers 1 + 2: the liquid, seen only through the dots. */
.skeleton::after {
    content: "";
    position: absolute;
    inset: 0;
    /* The platform's own two colours -- `--primary` and `--secondary` from the
       theme, not Bootstrap's stock blue -- flowing one into the other. */
    background-image: linear-gradient(
        115deg,
        transparent 8%,
        rgba(var(--primary-rgb, 228, 31, 7), .30) 24%,
        rgba(var(--secondary-rgb, 255, 162, 1), .85) 36%,
        rgba(var(--primary-rgb, 228, 31, 7), 1) 46%,
        rgba(var(--secondary-rgb, 255, 162, 1), .55) 56%,
        rgba(var(--primary-rgb, 228, 31, 7), .30) 66%,
        transparent 82%
    );
    background-size: 320% 100%;
    background-repeat: no-repeat;

    /* The mesh. Same tile as the resting grid, so the liquid lands exactly
       on the dots rather than between them. */
    -webkit-mask-image: radial-gradient(circle at center, #000 1.15px, transparent 1.35px);
    mask-image: radial-gradient(circle at center, #000 1.15px, transparent 1.35px);
    -webkit-mask-size: 6px 6px;
    mask-size: 6px 6px;
    -webkit-mask-position: center;
    mask-position: center;

    animation: dots-flow 2.4s cubic-bezier(.45, 0, .35, 1) infinite;
}

.skeleton-line { width: 100%; }
.skeleton-line-sm { height: 9px; opacity: .85; }

/* A round placeholder gets a round mesh; the liquid still runs through it. */
.skeleton-avatar {
    width: 30px;
    height: 30px;
    border-radius: 50%;
    flex: 0 0 auto;
}

.skeleton-avatar-lg {
    width: 42px;
    height: 42px;
}

.skeleton-dot {
    width: 18px;
    height: 18px;
    border-radius: 5px;
}

/* ---- the cascade ----

   Each row starts its flow a beat after the one above, so the colour
   travels down the list and it reads as one surface filling rather than
   forty blocks blinking out of step. */
.skeleton-row td { vertical-align: middle; }
.skeleton-row:nth-child(2) .skeleton::after { animation-delay: .1s; }
.skeleton-row:nth-child(3) .skeleton::after { animation-delay: .2s; }
.skeleton-row:nth-child(4) .skeleton::after { animation-delay: .3s; }
.skeleton-row:nth-child(5) .skeleton::after { animation-delay: .4s; }
.skeleton-row:nth-child(6) .skeleton::after { animation-delay: .5s; }
.skeleton-row:nth-child(n+7) .skeleton::after { animation-delay: .6s; }

/* Cards fill across the row, then down it. */
[data-list-skeleton]:nth-child(4n+2) .skeleton::after { animation-delay: .1s; }
[data-list-skeleton]:nth-child(4n+3) .skeleton::after { animation-delay: .2s; }
[data-list-skeleton]:nth-child(4n+4) .skeleton::after { animation-delay: .3s; }

/* ---- the caret ----

   On the first line of the first row only: one blinking bar for "still
   being written". More than one would be a light show. */
.skeleton-row:first-child .skeleton-caret::before {
    content: "";
    position: absolute;
    top: 50%;
    right: -7px;
    width: 2px;
    height: 13px;
    transform: translateY(-50%);
    border-radius: 1px;
    background-color: rgba(var(--primary-rgb, 228, 31, 7), .9);
    animation: skeleton-caret 1.1s steps(1, end) infinite;
    z-index: 2;
}

.skeleton-caret { overflow: visible; }
.skeleton-card { margin-bottom: 0; }

/* The rows already on screen step back and stay put, so nothing jumps
   when the real ones arrive. */
.is-waiting > :not([data-list-skeleton]) {
    opacity: .35;
    filter: saturate(.45);
    transition: opacity .18s ease, filter .18s ease;
    pointer-events: none;
}

@keyframes dots-flow {
    from { background-position: -60% 0; }
    to { background-position: 160% 0; }
}

@keyframes skeleton-caret {
    0%, 45% { opacity: 1; }
    50%, 100% { opacity: 0; }
}

/* With motion switched off the mesh holds still: the shape and the height
   are the useful part, and they are still there. */
@media (prefers-reduced-motion: reduce) {
    .skeleton::after,
    .skeleton-row:first-child .skeleton-caret::before {
        animation: none;
    }

    .skeleton::after {
        background-image: linear-gradient(
            115deg,
            rgba(var(--primary-rgb, 228, 31, 7), .22),
            rgba(var(--secondary-rgb, 255, 162, 1), .22)
        );
    }
}

/* The per-person half — see the note beside the toast block above. */
html[data-reduce-motion] .skeleton::after,
html[data-reduce-motion] .skeleton-row:first-child .skeleton-caret::before {
    animation: none;
}

html[data-reduce-motion] .skeleton::after {
    background-image: linear-gradient(
        115deg,
        rgba(var(--primary-rgb, 228, 31, 7), .22),
        rgba(var(--secondary-rgb, 255, 162, 1), .22)
    );
}


/* ---------------------------------------------------------------------
   The other waiting state  ·  the orbit

   For a screen that would rather show one device than a shape of the
   list: `loader: 'thinking'`. A core that breathes and three points
   travelling round it at different radii and speeds, each with a trail.
   --------------------------------------------------------------------- */

.is-thinking {
    position: relative;
    min-height: 220px;
}

.think {
    position: absolute;
    inset: 0;
    z-index: 3;
    display: flex;
    flex-direction: column;
    align-items: center;

    /* Near the top, not dead centre: a long list makes a tall container, and a
       loader centred in 1,500px of table is a loader nobody can see. */
    justify-content: flex-start;
    padding-top: clamp(28px, 10vh, 96px);
    gap: 1rem;
    background: radial-gradient(
        ellipse 60% 55% at 50% 45%,
        rgba(var(--bs-body-bg-rgb, 255, 255, 255), .96) 40%,
        rgba(var(--bs-body-bg-rgb, 255, 255, 255), .72) 100%
    );
    animation: think-in .25s ease both;
}

.think-stage {
    position: relative;
    width: 78px;
    height: 78px;
}

.think-core {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 13px;
    height: 13px;
    margin: -6.5px 0 0 -6.5px;
    border-radius: 50%;
    background: radial-gradient(
        circle at 35% 35%,
        rgba(255, 255, 255, .95),
        rgba(var(--primary-rgb, 228, 31, 7), 1) 60%
    );
    box-shadow: 0 0 0 0 rgba(var(--primary-rgb, 228, 31, 7), .35);
    animation: think-breathe 2.6s ease-in-out infinite;
}

.think-orbit {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: 1px solid rgba(var(--primary-rgb, 228, 31, 7), .14);
    animation: think-turn linear infinite;
}

.think-orbit i {
    position: absolute;
    top: -3px;
    left: 50%;
    width: 6px;
    height: 6px;
    margin-left: -3px;
    border-radius: 50%;
    background-color: rgb(var(--primary-rgb, 228, 31, 7));
    box-shadow:
        0 0 6px 1px rgba(var(--primary-rgb, 228, 31, 7), .55),
        -5px 4px 5px -2px rgba(var(--primary-rgb, 228, 31, 7), .28),
        -9px 9px 6px -3px rgba(var(--primary-rgb, 228, 31, 7), .14);
}

.think-orbit-1 { animation-duration: 2.1s; transform: rotateX(58deg); }
.think-orbit-2 {
    inset: 13px;
    animation-duration: 1.5s;
    animation-direction: reverse;
    transform: rotateY(62deg);
    border-color: rgba(var(--secondary-rgb, 255, 162, 1), .22);
}
.think-orbit-3 { inset: 25px; animation-duration: 2.9s; transform: rotate(28deg); }

/* The middle ring carries the secondary, so both of the platform's colours are
   travelling and the three points never read as one repeated dot. */
.think-orbit-2 i {
    width: 5px;
    height: 5px;
    margin-left: -2.5px;
    top: -2.5px;
    background-color: rgb(var(--secondary-rgb, 255, 162, 1));
    box-shadow:
        0 0 6px 1px rgba(var(--secondary-rgb, 255, 162, 1), .55),
        -5px 4px 5px -2px rgba(var(--secondary-rgb, 255, 162, 1), .28),
        -9px 9px 6px -3px rgba(var(--secondary-rgb, 255, 162, 1), .14);
}
.think-orbit-3 i { width: 4px; height: 4px; margin-left: -2px; top: -2px; opacity: .9; }

.think-said {
    margin: 0;
    font-size: 13px;
    font-weight: 500;
    color: var(--bs-secondary-color);
    letter-spacing: .01em;
    display: inline-flex;
    align-items: baseline;
    gap: 1px;
}

.think-dots {
    display: inline-flex;
    gap: 2px;
    margin-left: 1px;
}

.think-dots i {
    width: 3px;
    height: 3px;
    border-radius: 50%;
    background-color: currentColor;
    opacity: .25;
    animation: think-dot 1.4s ease-in-out infinite;
}

.think-dots i:nth-child(2) { animation-delay: .18s; }
.think-dots i:nth-child(3) { animation-delay: .36s; }

@keyframes think-turn {
    to { transform: rotate(360deg); }
}

@keyframes think-breathe {
    0%, 100% {
        transform: scale(1);
        box-shadow: 0 0 0 0 rgba(var(--primary-rgb, 228, 31, 7), .32);
    }
    50% {
        transform: scale(1.18);
        box-shadow: 0 0 0 9px rgba(var(--primary-rgb, 228, 31, 7), 0);
    }
}

@keyframes think-dot {
    0%, 100% { opacity: .25; transform: translateY(0); }
    40% { opacity: 1; transform: translateY(-2px); }
}

@keyframes think-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .think,
    .think-core,
    .think-orbit,
    .think-dots i {
        animation: none;
    }
}

/* The per-person half — see the note beside the toast block above. */
html[data-reduce-motion] .think,
html[data-reduce-motion] .think-core,
html[data-reduce-motion] .think-orbit,
html[data-reduce-motion] .think-dots i {
    animation: none;
}


/* ---------------------------------------------------------------------
   Empty is two different sentences  ·  spec §K.4.9

   The kit stamps `data-empty="filtered"` or `data-empty="none"` on a
   screen's empty state. A module writes both halves, marks each with
   `data-when`, and these two rules show the right one.

   "Nothing matches what you asked for" and "there is nothing here yet"
   need different words and different buttons — a Clear filters button is
   an insult to somebody who has not set one.
   --------------------------------------------------------------------- */

[data-empty] > [data-when] {
    display: none;
}

[data-empty="filtered"] > [data-when="filtered"],
[data-empty="none"] > [data-when="none"] {
    display: block;
}




/* ---------------------------------------------------------------------
   Sorted by more than one column

   The arrow says which way; this says in what order. It appears only when
   a second column is part of the sort, so an ordinary single sort looks
   exactly as it always has.
   --------------------------------------------------------------------- */

.list-sort-rank {
    display: inline-block;
    min-width: 14px;
    height: 14px;
    margin-right: 5px;
    border-radius: 7px;
    background-color: rgba(var(--primary-rgb), .12);
    color: var(--primary);
    font-size: 10px;
    font-weight: 700;
    line-height: 14px;
    text-align: center;
    vertical-align: middle;
}


/* ---------------------------------------------------------------------
   A field that cannot be changed

   `readonly` is invisible in this theme: the box looked exactly like the
   editable one beside it, so somebody clicked into it, typed, and nothing
   happened. The value is still part of the record and still travels with
   the save -- locked means "you may not change this", never "this is not
   here".
   --------------------------------------------------------------------- */

.form-control.is-locked,
.form-control[readonly] {
    background-color: var(--bs-tertiary-bg);
    color: var(--bs-secondary-color);
    cursor: not-allowed;
    border-style: dashed;
}

.form-control.is-locked:focus,
.form-control[readonly]:focus {
    box-shadow: none;
    border-color: var(--bs-border-color);
}


/* ---------------------------------------------------------------------
   The pager

   First, back, where you are, forward, last. The page itself opens a
   short list to jump with -- see `renderPager`.
   --------------------------------------------------------------------- */

.list-page-jump .page-link {
    min-width: 3.5rem;
    text-align: center;
    cursor: pointer;
}

.list-page-menu {
    max-height: 16rem;
    overflow-y: auto;
    min-width: 8rem;
}

/* =====================================================================
   Focus, seen  ·  the project's ONE focus ring

   LOCKED and UNIVERSAL by the owner's decision (1 Sep 2026): every
   focusable thing in OMPanel says where the keyboard is in exactly one
   way, and no screen defines its own.

   ## What it looks like, and why it is this and not something else

   A thin 2px solid line in the platform's primary, held 2px off the
   control. That is NOT a new invention — it is what the product already
   did on the Add User form's status control (`manage-users.css`), so the
   whole product now matches the part of it that was already right. A soft
   wide glow was tried first and rejected by the owner for exactly that
   reason: it did not match the theme.

   ## Why it lives here

   The template REMOVES focus outright — `style.css` sets
   `outline: 0; box-shadow: none` on `.form-control:focus` (6665) and
   `.form-check-input:focus` (7364). Somebody driving the product by
   keyboard had no way to tell which box they were in: not on login, not
   on Add User, not anywhere. That is not a styling preference, it is
   whether the product can be used without a mouse. `module.css` is loaded
   last by every shell, so this is the one place that can answer it once.

   ## `:focus-visible`, never `:focus`

   The browser raises focus-visible for keyboard navigation and withholds
   it for a mouse click, which is exactly the distinction wanted: the ring
   appears when somebody is tabbing, and a mouse click leaves the
   template's design exactly as drawn.
   ===================================================================== */

:root {
    --app-focus-width: 2px;
    --app-focus-offset: 2px;
    --app-focus-color: var(--primary, #e41f07);
}

.form-control:focus-visible,
.form-select:focus-visible,
.form-check-input:focus-visible,
textarea:focus-visible,
input:focus-visible,
select:focus-visible,
button:focus-visible,
.btn:focus-visible,
.btn-close:focus-visible,
.toggle-password:focus-visible,
a:focus-visible,
[role="button"]:focus-visible,
[tabindex]:focus-visible:not([data-id]) {
    /*
     * `!important`, and it is not laziness. It is answering one.
     *
     * `style.css` line 9317 declares, for the whole product:
     *
     *     button, a { outline: none !important; }
     *
     * Nothing without `!important` can beat that at any specificity, so until
     * 16 Sep 2026 EVERY BUTTON, LINK AND TAB ON THE PLATFORM HAD NO KEYBOARD
     * FOCUS INDICATOR AT ALL. Form fields did — they are neither a `button`
     * nor an `a` — which is what the owner saw as focus behaving two different
     * ways, and it is the more serious half of that report: a product you can
     * tab through with nothing on screen saying where you are cannot be used
     * without a mouse.
     *
     * The template is vendored and is not edited here; overriding it in this
     * file is the established route (this whole section exists for that), and
     * `!important` against `!important` is the only instrument that reaches.
     *
     * `box-shadow` stays unflagged on purpose. It is only clearing Bootstrap's
     * blue glow, nothing declares that `!important`, and a blanket
     * `box-shadow: none !important` here would strip the shadow off every
     * focused card and button the theme draws deliberately.
     */
    outline: var(--app-focus-width) solid var(--app-focus-color) !important;
    outline-offset: var(--app-focus-offset) !important;
    box-shadow: none;
}

/* "ALWAYS SHOW FOCUS" — the same ring, on plain `:focus` (22 Sep 2026).

   The section above argues for `:focus-visible` and against `:focus`, and
   that argument still stands as the DEFAULT. It rests on the browser
   guessing correctly about intent, though, and the guess is wrong for real
   people: a switch-access or head-pointer user drives the product through
   what the browser reports as pointer input and is given no ring at all.

   `always_show_focus` is that person's override. It does not change the
   default for anybody else, and it is the identical ring rather than a
   second design — the same three tokens, so raising the width raises both.

   The `[tabindex]:not([data-id])` exclusion is carried over for the reason
   the note directly below gives: a list row already says "you are here".

   `:is()` here and not a hand-written list of thirteen. The list above is
   written out because it predates nothing in particular; repeating it in
   full would be twenty-six selectors to keep in step forever. */
html[data-always-show-focus] :is(
    .form-control,
    .form-select,
    .form-check-input,
    textarea,
    input,
    select,
    button,
    .btn,
    .btn-close,
    .toggle-password,
    a,
    [role="button"]
):focus,
html[data-always-show-focus] [tabindex]:focus:not([data-id]) {
    outline: var(--app-focus-width) solid var(--app-focus-color) !important;
    outline-offset: var(--app-focus-offset) !important;
    box-shadow: none;
}

/* A LIST ROW is excluded above, and this is why.

   Rows and grid cards carry `tabindex` so they can be reached by keyboard,
   so the generic `[tabindex]:focus-visible` ring applied to them too — an
   `outline-offset: 2px` line sitting OUTSIDE a card that already draws a
   primary border when it is current or selected. Two rings, the outer one
   larger and detached: the double border, appearing only when the card was
   reached by keyboard, which is why clicking it never showed the fault
   (owner, 2 Sep 2026).

   A row already says "you are here" in the kit's own vocabulary — the
   rules further up this file — so it needs no second ring. Every other
   focusable thing keeps the platform's one focus ring, unchanged. */
/* ONE FOCUS RING, ON THE CONTROL ITSELF — and the exception that earns itself.

   `.input-icon` used to be listed here beside `.input-group` and it was
   removed on 16 Sep 2026: *"form fields focus 2 different, remove one and
   keep only one everywhere"* (owner).

   The two were real. A BARE field drew the ring on itself; a field inside
   `.input-icon` drew nothing and let the WRAPPER carry it. Two rules, two
   elements, and on a form holding both — Add User has a plain First name
   beside a Username with an `@` addon — two different focus geometries a few
   pixels apart.

   The measurement that settles it, taken on the live page:

       .input-icon    wrapper 237x39   field 237x39   difference 0 x 0
       .input-group   wrapper 451x39   field 422x39   difference 29 x 0

   `.input-icon` places its icon INSIDE the field's own box — the addon is
   positioned over the padding, so wrapper and field are the same rectangle to
   the pixel. Ringing the wrapper there was drawing the same rectangle by a
   second mechanism. Dropping it changes nothing on screen and removes one of
   the two behaviours entirely.

   `.input-group` is different and keeps the rule: its segments are genuinely
   separate boxes, and 29px of button sits outside the field. Outlining only
   the field half draws a line down the middle of something that reads as one
   control. That is not a second focus STYLE — it is the same ring around the
   thing a reader actually sees as the control, which is the principle the
   whole file follows.

   `:has(:focus-visible)` rather than `:focus-within`, so a mouse click does
   not raise it either. */
.input-group:has(:focus-visible) {
    outline: var(--app-focus-width) solid var(--app-focus-color);
    outline-offset: var(--app-focus-offset);
    border-radius: var(--bs-border-radius, 5px);
}

.input-group:has(:focus-visible) .form-control:focus-visible {
    outline: 0 !important;
}

/* The two SEARCH boxes the template draws itself — the one in the top bar
   and the one over the sidebar menu — are the same shape as the groups
   above, with one difference: the template already draws their focus.

       .app-search-box:focus-within   border → primary, ring 4px  (layout.css)
       .sidebar-search .form-control  border → primary, ring 3px  (style.css)

   So the group needs nothing from us, and the generic `input:focus-visible`
   ring above was landing INSIDE it — a hard 2px line at 2px offset within a
   box that already had a 1px border and a soft ring. Three concentric red
   lines where the template has two (owner, 6 Sep 2026: the focus ring
   should look as the template draws it).

   The input goes plain, exactly as it does inside `.input-group`. Nothing
   is lost for somebody tabbing: the box itself lights up either way. */
.app-search-input:focus-visible,
.sidebar-search .form-control:focus-visible {
    outline: 0 !important;
    outline-offset: 0 !important;
}

/* An invalid field keeps its own colour while focused: the line says
   "you are here", the red border says "this is wrong", and one must not
   be mistaken for the other. */
.is-invalid:focus-visible,
.input-group:has(.is-invalid:focus-visible) {
    outline-color: var(--bs-danger, #dc3545) !important;
}

/* CHIP FIELDS — ported from the design's own `shared/css/module.css`, 9 Sep 2026.

   The `chips` field type was missing from our `form-panel.js`, so a field declaring one
   fell through to a plain text box — the tenants edit form asks for two. Porting the type
   without its rules would only have moved the problem, so both halves land together.

   The chips ARE the control, not a picture of one: each is a checkbox wearing a badge, and
   one that is off still says so rather than disappearing. */
.af-chips {
    display: flex;
    flex-wrap: wrap;
    gap: .375rem;
}

.af-chip {
    cursor: pointer;
    margin: 0;
}

.af-chip input {
    position: absolute;
    opacity: 0;
    pointer-events: none;
}

.af-chip > span {
    display: inline-block;
    padding: .25rem .625rem;
    border: 1px solid var(--bs-border-color);
    border-radius: 50rem;
    font-size: .75rem;
    line-height: 1.4;
    color: var(--bs-secondary-color);
    transition: background-color .15s, border-color .15s, color .15s;
}

.af-chip:hover > span {
    border-color: var(--primary);
}

.af-chip.is-on > span {
    background-color: var(--primary);
    border-color: var(--primary);
    color: #fff;
}

.af-chip.is-away > span {
    text-decoration: line-through;
}

.af-chip input:focus-visible + span {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

.af-chip input:disabled + span {
    opacity: .55;
    cursor: not-allowed;
}


/* =====================================================================
   BOOTSTRAP'S COLOURS, POINTED AT THE THEME'S
   =====================================================================

   Added 16 Sep 2026: *"Default primary color are still blue, it visible when
   use keyboard focus. make it one primary and secondary and all as per
   theme"* (owner).

   ## The fault

   This theme declares its own palette — `--primary`, `--secondary`,
   `--border-color` and the rest — and NEVER overrides Bootstrap's. Both sets
   are live on every page, and the `--bs-*` half is stock:

       --primary            #E41F07      the brand
       --bs-primary         #0d6efd      Bootstrap's default blue
       --bs-secondary       #6c757d      Bootstrap's default grey
       --bs-focus-ring-color  rgba(13, 110, 253, .25)
       --bs-link-color      #0d6efd

   Anything reaching for a `--bs-*` colour therefore painted BLUE on a red
   product. It is not visible everywhere, because the template restyles most
   components by hand — which is exactly why it survived this long and why it
   surfaced on keyboard focus, one of the few paths the template never redrew.

   The trap is worth stating for whoever comes next: on this platform a
   `--bs-*` token is NOT "the theme's" anything. It is Bootstrap's, and
   reaching for one is how a colour from outside the palette reaches the
   screen while every file involved still contains no literal.

   ## Why this is the right place

   `module.css` is loaded last by every shell, so it is the one file that can
   answer this once for the whole product. The alternative — each module
   restating the palette — is how six copies drift.

   ## The channel lists, and the one literal that is left

   `--bs-primary-rgb` and `--bs-secondary-rgb` are CHANNEL LISTS, not colours:
   Bootstrap composes them as `rgba(var(--bs-primary-rgb), .25)`, and CSS
   cannot take a hex custom property apart. The theme already publishes
   `--primary-rgb` and `--secondary-rgb` in exactly that shape, so the primary
   is POINTED AT ITS OWN rather than copied.

   That stopped being a tidiness argument on 22 Sep 2026. `accent` is a
   per-person preference now, injected as `--primary` and `--primary-rgb` onto
   the `style` attribute of `<html>` by `App\Core\Support\Appearance`. Three
   literal `228, 31, 7`s stood in this block until that day and they did NOT
   follow a changed accent: somebody who picked blue got blue buttons with a
   red focus ring, red links and a red Bootstrap accent. A literal here is now
   a BUG rather than a duplication.

   `--bs-secondary-rgb` keeps its literal because `--secondary` is not a
   preference: no swatch offers it and nothing changes it.

   ## What is NOT mapped, deliberately

   `--bs-body-bg`, `--bs-body-color` and the surface tokens are left alone.
   The theme drives those through its own `--white` / `--heading-color` and
   flips them per theme; pointing Bootstrap's at them as well would give two
   owners to one surface and a race on every theme switch. This block is
   limited to the ACCENTS, which had no owner at all.
   ===================================================================== */

:root {
    --bs-primary: var(--primary);
    --bs-primary-rgb: var(--primary-rgb);

    --bs-secondary: var(--secondary);
    --bs-secondary-rgb: 255, 162, 1;

    /* Follows the theme's own border, so it inverts in dark mode for free —
       `--border-color` is #E2E8F0 light and #161641 dark. */
    --bs-border-color: var(--border-color);

    /* Bootstrap's own ring, for the handful of components that draw it
       themselves rather than going through the rule below. Same red, same
       weight it always had. */
    --bs-focus-ring-color: rgba(var(--primary-rgb), .25);

    --bs-link-color: var(--primary);
    --bs-link-color-rgb: var(--primary-rgb);
    --bs-link-hover-color: var(--primary);
}


/* =====================================================================
   THE BLUES BOOTSTRAP HARD-CODES, WHICH NO VARIABLE CAN REACH
   ===================================================================== */

/*
   Four rules in `bootstrap.min.css` write `#86b7fe` and `rgba(13,110,253,.25)`
   as LITERALS rather than through a token, so the block above cannot touch
   them. Each is restated here at matching specificity — and matching matters:
   the platform rule further down this file is `select:focus-visible`, one
   class less specific than Bootstrap's `.form-select:focus`, so it loses on
   the properties Bootstrap sets even though it loads later.

   `:focus` and not `:focus-visible` on the field rules, because that is the
   selector Bootstrap used: a MOUSE click raises `:focus` alone, and the blue
   was appearing there too.
*/

/* A select kept a blue BORDER on focus. `style.css` neutralises the shadow at
   line 6719 and stops there, so the border came through on every select in
   the product. */
.form-select:focus {
    border-color: var(--border-color);
}

/* The switch's knob is an inline SVG with the blue baked into the data URI,
   so it cannot be recoloured — only replaced. It also cannot be written here
   in terms of `var(--primary)`: CSS has no `url(var(--x))`, and a custom
   property cannot be interpolated into a URL. This stood as a literal
   `%23E41F07` until 22 Sep 2026 and was then the LAST accent on the platform
   that ignored the preference — a person who chose teal got a teal switch
   with a red knob the moment they focused it.

   `App\Core\Support\Appearance` now assembles the whole `url()` from the
   chosen accent and puts it on `<html>` as `--primary-knob`. The fallback is
   the brand's own circle, for the two shells that do not call `Appearance`
   (`public` and `print`) and for a document rendered before it is set. */
.form-check-input:focus {
    --bs-form-switch-bg: var(--primary-knob, url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23E41F07'/%3e%3c/svg%3e"));
}

/*
   A NAV LINK HAD NO VISIBLE FOCUS AT ALL, which is worse than the wrong
   colour and is the real find here.

   `bootstrap.min.css` declares `.nav-link:focus-visible { outline: 0;
   box-shadow: 0 0 0 .25rem rgba(13,110,253,.25) }`. That selector is
   (0,2,0); the platform rule above is `a:focus-visible` at (0,1,1), so
   Bootstrap wins on specificity no matter which file loads last — its
   `outline: 0` removed the platform ring, and its blue shadow was then
   suppressed by something else downstream. The result was a tab strip you
   could move through by keyboard with nothing on screen saying where you
   were. Measured on Manage Users: `outline: none`, `box-shadow: none`.

   Restated at (0,2,0) so it wins, with the platform's own ring.
*/
.nav-link:focus-visible {
    outline: var(--app-focus-width) solid var(--app-focus-color) !important;
    outline-offset: var(--app-focus-offset) !important;
    box-shadow: none;
}

/* The range thumb, which takes its glow from a literal in both engines. */
.form-range:focus::-webkit-slider-thumb {
    box-shadow: 0 0 0 1px var(--white, #fff), 0 0 0 .25rem var(--bs-focus-ring-color);
}

.form-range:focus::-moz-range-thumb {
    box-shadow: 0 0 0 1px var(--white, #fff), 0 0 0 .25rem var(--bs-focus-ring-color);
}


/* =====================================================================
   A WHOLE ROW THAT PICKS ITS OWN RADIO
   =====================================================================

   Owner, 17 Sep 2026: *"i need anywhere click to select radio, no need to
   click on specific radio button"*.

   The markup does the work, not a script. Each row is a `<label for="…">`
   pointing at the radio inside it, so the browser's own behaviour selects
   that radio when any part of the row is clicked — the icon, the heading,
   the explanation, the empty space beside it.

   That matters more than saving a line of JavaScript. A native label also
   enlarges the ACCESSIBLE name and the hit target that assistive technology
   and touch both use, which a click handler on a div does not: a handler
   makes a div respond to a mouse and leaves it invisible to everything else.

   `cursor: pointer` because the row is now a control and has to look like
   one. Without it the only thing that appears clickable is the 16px circle
   the owner is trying to stop having to aim at.

   `user-select: none` so a quick double-click picks the option twice rather
   than selecting the sentence — which is what a row of text does by default
   and looks like a fault the moment somebody tries it.
   ===================================================================== */

.up-line-pick {
    cursor: pointer;
    user-select: none;
}

/* The radio inside keeps its own pointer. It is still directly clickable —
   this only ADDS the rest of the row as a target, it does not move it. */
.up-line-pick .form-check-input { cursor: pointer; }

/* =========================================================================
   A PERSON, DRAWN — the platform's avatar, 17 Sep 2026
   =========================================================================

   Every avatar on the platform used to be `ui/img/users/user-40.jpg`: one stock photograph of a
   stranger, hardcoded in three places, shown for whoever was signed in. This is what replaced
   it, and it is here in the shared `Core/Ui` stylesheet rather than in a module or a theme
   because both panels, the account page, the user list and the user detail all draw it — a copy
   per caller is a palette that drifts.

   A photograph and a set of initials get the SAME classes and therefore the same box, so a list
   where some people have uploaded a picture and most have not still lines up.

   ## THE TONE IS CHOSEN IN PHP, RESOLVED HERE

   `App\Core\Support\Avatar` returns a NAME — `teal`, `purple` — and never a colour. The ten
   rules at the bottom turn a name into the theme's own `--x` / `--x-transparent` pair, so the
   palette follows the theme into dark mode and a rebrand is a change here rather than a sweep
   through PHP. The order of the tones in that class is what the hash indexes and must not be
   reordered; the order here is cosmetic.
   ========================================================================= */

.om-avatar {
    /* Set per tone at the bottom; the defaults keep an unknown tone legible rather than
       invisible, which is what an unset `currentColor` on a transparent ground would be. */
    --om-avatar-ink: var(--bs-secondary-color);
    --om-avatar-tint: var(--light-300);

    display: inline-grid;
    place-items: center;
    flex-shrink: 0;
    overflow: hidden;
    box-sizing: border-box;

    /* The border is the tone at a third of its strength, which is what makes the initials read
       as a deliberate token rather than as a coloured rectangle. `color-mix` degrades to the
       flat colour on a browser that cannot do it, which is the right failure. */
    border: 1px solid color-mix(in srgb, var(--om-avatar-ink) 35%, transparent);
    border-radius: 8px;

    background: var(--om-avatar-tint);
    color: var(--om-avatar-ink);

    font-weight: 600;
    line-height: 1;
    letter-spacing: .3px;
    text-transform: uppercase;
    text-decoration: none;

    /* A photograph fills the box and is cropped to it rather than squashed. Harmless on the
       span, load-bearing on the img — a portrait and a landscape photo must end the same shape
       or a list of faces is a list of different rectangles. */
    object-fit: cover;
}

/* Sizes. The font is ~40% of the box at every step, which is what keeps two letters inside a
   circle that is half the size of the one above it. */
.om-avatar-xs { width: 24px; height: 24px; font-size: 10px;   border-radius: 6px; }
.om-avatar-sm { width: 32px; height: 32px; font-size: 12px;   border-radius: 7px; }
.om-avatar-md { width: 38px; height: 38px; font-size: 14px; }
.om-avatar-lg { width: 42px; height: 42px; font-size: 15px; }
.om-avatar-xl { width: 96px; height: 96px; font-size: 34px;   border-radius: 16px; }

/* A round variant for the places the template draws a circle. The shape is the caller's
   decision, not the component's — the account header is round, a list row is not. */
.om-avatar-circle { border-radius: 50%; }

/* The ten tones. Both halves of each pair exist in the theme; a tone whose `--x-transparent`
   were missing would draw coloured letters on no ground at all. */
.om-avatar-primary { --om-avatar-ink: var(--primary); --om-avatar-tint: var(--primary-transparent); }
.om-avatar-info    { --om-avatar-ink: var(--info);    --om-avatar-tint: var(--info-transparent); }
.om-avatar-success { --om-avatar-ink: var(--success); --om-avatar-tint: var(--success-transparent); }
.om-avatar-warning { --om-avatar-ink: var(--warning); --om-avatar-tint: var(--warning-transparent); }
.om-avatar-purple  { --om-avatar-ink: var(--purple);  --om-avatar-tint: var(--purple-transparent); }
.om-avatar-teal    { --om-avatar-ink: var(--teal);    --om-avatar-tint: var(--teal-transparent); }
.om-avatar-indigo  { --om-avatar-ink: var(--indigo);  --om-avatar-tint: var(--indigo-transparent); }
.om-avatar-pink    { --om-avatar-ink: var(--pink);    --om-avatar-tint: var(--pink-transparent); }
.om-avatar-cyan    { --om-avatar-ink: var(--cyan);    --om-avatar-tint: var(--cyan-transparent); }
.om-avatar-orange  { --om-avatar-ink: var(--orange);  --om-avatar-tint: var(--orange-transparent); }

/*
 * THE TONE CLASSES ON THE THEME'S OWN AVATAR SLOT.
 *
 * The user list draws `.avatar > .avatar-title`, which is the theme's component and owns its own
 * box, ring and size. Only the COLOUR is ours there, so the tone class is applied on its own and
 * this is what consumes it — one rule rather than ten, because every `.om-avatar-<tone>` above
 * does nothing but set the same two custom properties.
 *
 * The attribute selector is deliberate: it matches whichever of the ten was applied without
 * naming any of them, so a tone added to `Avatar::TONES` needs no change here.
 */
.avatar-title[class*="om-avatar-"] {
    background: var(--om-avatar-tint);
    color: var(--om-avatar-ink);
}

/*
 * THE PRESENCE DOT — sized here, by the component, and nowhere else.
 *
 * Both shells used to write this themselves and lean on `style.css`'s
 * `.dropdown .online { font-size: 8px }`. The markup was byte-identical in the two files and the
 * dot still measured 10x10 on the control panel and 20x20 on the tenant panel: whether that rule
 * wins depends on which ancestors each shell wraps the menu in, and neither shell was written to
 * guarantee them. Two copies of the same markup, each at the mercy of a stylesheet neither owns.
 *
 * So the size is stated here in pixels, on a class no theme rule targets, and `avatar_tag()` is
 * the only thing that emits it. There is no second place left to change, which is the only way
 * two panels can be relied on to agree.
 *
 * Absolutely positioned against `.om-avatar-shell`, which exists solely to be the containing
 * block — the avatar tile itself must not become `position: relative`, or a photograph's
 * `object-fit` cropping would be measured against a different box.
 */
.om-avatar-shell {
    position: relative;
    display: inline-flex;
    flex-shrink: 0;
    line-height: 0;
}

.om-avatar-dot {
    position: absolute;
    right: -2px;
    bottom: -2px;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--success);

    /* The ring is the page behind it, not white: on a dark topbar a white ring is a bright speck
       and on a light one it is invisible. `--white` is the theme's own surface token and flips
       with the theme, which a literal cannot. */
    border: 2px solid var(--white);
}

/*
 * THE TENANT SIDEBAR HEADER STAYS VISIBLE ABOVE 992px.
 *
 * `layout.css` turns the menu panel into a hover-out flyout on desktop and hides
 * `.sidebar-right .sidebar-logo` with it. On the CONTROL panel that header holds two window
 * controls and hiding them in a flyout is correct. On the TENANT panel it also holds the tenant
 * switcher — the only way to move between tenants — so the same rule is a navigation dead end.
 *
 * ONE override rather than six copies. The alternative was moving the switcher out of that
 * header, which was tried first and quietly broke it: every `.project-switch-*` rule in
 * `style.css` is scoped `… .sidebar-right .sidebar-logo .project-switch-…`, so outside it the
 * label, the name and its ellipsis all stopped applying and a long tenant name overflowed. The
 * second alternative was restating those six rules here — the theme's own styling duplicated
 * into our file, which is the habit being stamped out.
 *
 * `:has()` scopes this to the shell that actually has a switcher, so the control panel keeps the
 * theme's behaviour untouched and no guard or body class needs to be invented to tell them apart.
 */
body.twocol-layout .sidebar-twocol.sidebar .sidebar-right .sidebar-logo:has(.project-switch-info) {
    display: block;
}


/* =========================================================================
   ONE TONE TABLE FOR "WHAT KIND OF THING IS THIS" — 17 Sep 2026
   =========================================================================

   Three families of marks say the same six things in the same six colours:

       .lg-mark-*      the device tile on a Login history card
       .ac-branch-*    the labelled branch of the Activities tree
       .ac-point-*     one activity chip hanging off that branch

   Until this block existed the table was written out THREE times — once in
   `account-logins.css` and twice in `account-activity.css` — as eighteen pairs
   of hex literals. That is not merely repetition. Three copies of a palette are
   three chances to drift, and they HAD drifted: the sibling sheets disagreed on
   whether the neutral tone was `#f3f4f6` or `#f3f6f9`, and a reader moving from
   Login history to Activities was being taught two codes for one idea.

   ## WHY THE DEFINITION LIVES HERE AND NOT IN EITHER SHEET

   `module.css` is the shared `Core/Ui` stylesheet and is loaded by every shell
   before a module's own. Putting the table in one of the two panes would have
   made that pane the owner of a palette the other pane depends on — the module
   boundary would then be a lie, and deleting the Login history screen would
   silently repaint the Activities tree.

   ## WHY THE THREE FAMILIES ARE NAMED IN ONE SELECTOR LIST

   The class names are built by string concatenation — `account-logins.js` line
   525 writes `'lg-mark lg-mark-' + markTint`, and `account/index.php` writes
   `ac-branch-<?= $acKey ?>` and `ac-point-<?= $acKey ?>`. They are LIVE even
   though a grep for the literal class name finds nothing but the concatenation.
   They therefore cannot be renamed to a single shared `.om-kind-<tone>` without
   editing PHP and JS, which is out of this file's scope and would have been the
   wrong trade anyway: a device is a `desktop`, not an `info`, and collapsing the
   two would throw away the only place the domain word survives.

   So the MAPPING stays where the names are and the VALUES are stated once, by
   listing each family's class beside its siblings on the same row. A new device
   kind or a new activity kind is one selector added to one line here.

   ## WHY EVERY VALUE IS A THEME TOKEN

   Same rule as `.om-avatar-<tone>` above, and for the same two reasons. A hex
   literal ignores the theme switcher — `--primary` has ten different values
   because the product ships ten colour themes — and `style.css` REDEFINES every
   `*-transparent` token under `[data-bs-theme=dark]` (`--info-transparent` is
   `#eaf2fd` light and `#032830` dark). A literal therefore stays LIGHT on a dark
   page, which is exactly the bug `account-activity.css` was carrying a
   seventeen-line `[data-bs-theme="dark"]` restatement to paper over. Written as
   tokens the tints invert on their own and that block was deleted outright.

   Both halves of every pair below exist in `style.css` in both themes; they were
   checked before use, because a tone whose `--x-transparent` were missing would
   draw a coloured glyph on no ground at all.

   ## THE CONSUMERS SET NO COLOUR OF THEIR OWN

   Each family reads `--kind-tint` and `--kind-ink` and nothing else. The two
   names are deliberately generic: the tile, the branch label and the dot are
   three different shapes that agree only about colour, and a shared property
   named after any one of them would read as a borrowing rather than a contract.
   ========================================================================= */

/* BLUE — a sign-in, and the desktop machine it came from. */
.lg-mark-desktop,
.ac-branch-session,
.ac-point-session { --kind-tint: var(--info-transparent); --kind-ink: var(--info); }

/* THE BRAND RED — anything that touched security. Deliberately `--primary` and
   not `--danger`: changing a permission is not a failure, and red-as-error is
   the one meaning this tone must not carry here. */
.ac-branch-security,
.ac-point-security { --kind-tint: var(--primary-transparent); --kind-ink: var(--primary); }

/* GREEN — a record was written. */
.ac-branch-record,
.ac-point-record { --kind-tint: var(--success-transparent); --kind-ink: var(--success); }

/* PURPLE — access granted or taken away; and the phone, which is the device
   kind furthest from a desk. */
.lg-mark-mobile,
.ac-branch-access,
.ac-point-access { --kind-tint: var(--purple-transparent); --kind-ink: var(--purple); }

/* AMBER — money; and the tablet. `--warning`, not the theme's `--secondary`,
   which is ORANGE here rather than grey and is reserved for the platform's own
   second accent. */
.lg-mark-tablet,
.ac-branch-billing,
.ac-point-billing { --kind-tint: var(--warning-transparent); --kind-ink: var(--warning); }

/* TEAL — the laptop. It has no activity-kind counterpart, which is why it sits
   alone: the device table has five kinds and the activity table has six, and
   forcing them to the same five would have meant either a duplicate colour or a
   device kind with no tone at all. */
.lg-mark-laptop { --kind-tint: var(--teal-transparent); --kind-ink: var(--teal); }

/* NEUTRAL — genuinely unknown, and it must stay genuinely grey.
   `--light-300` and `--bs-secondary-color` rather than `--secondary`, because
   `--secondary` in this theme is orange: reaching for it as a neutral is how a
   "we do not know" chip ends up looking like a warning. Both of these follow the
   theme into dark mode (`--light-300` is `#F5F8FA` light, `#172128` dark). */
.lg-mark-unknown,
.ac-branch-unknown,
.ac-point-unknown { --kind-tint: var(--light-300); --kind-ink: var(--bs-secondary-color); }
