/* =============================================
   WESTAY SRC — COMPONENT LIBRARY
   Every reusable UI element. Pages use these
   classes — they never define their own.
   ============================================= */


/* ===================
   BUTTONS
   =================== */

/* Button family — conformed to the approved report-chrome language
   (rep-export / rrf-*): compact, quiet by default, flat hovers. Navy fill
   is reserved for a page's single key action via .btn-primary. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 7px 13px;
    font-family: var(--src-font-body);
    font-size: var(--src-text-sm);
    font-weight: var(--src-weight-semibold);
    line-height: 1;
    border: 1px solid transparent;
    border-radius: var(--src-radius-interactive);
    cursor: pointer;
    transition: all var(--src-transition-state);
    white-space: nowrap;
    text-decoration: none;
    min-height: 32px;
}

.btn:focus-visible {
    outline: 2px solid var(--src-border-focus);
    outline-offset: 2px;
}

.btn:disabled,
.btn.disabled {
    opacity: 0.5;
    cursor: not-allowed;
    pointer-events: none;
}

.btn-primary {
    background: var(--src-primary);
    color: var(--src-text-on-dark);
    border-color: transparent;
}

.btn-primary:hover {
    background: var(--src-primary-hover);
}

/* Quiet routine-action button — the rep-export language. The default for
   any action that is not the page's key action. */
.btn-outline {
    background: var(--src-surface-solid);
    color: var(--src-text-secondary);
    border-color: var(--src-border-medium);
}

.btn-outline:hover {
    color: var(--src-text-heading);
    border-color: var(--src-border-strong);
}

.btn-secondary {
    background-color: var(--src-surface-card);
    color: var(--src-text-primary);
    border-color: var(--src-border-strong);
}

.btn-secondary:hover {
    background-color: var(--src-surface-card-hover);
    border-color: rgba(2, 36, 60, 0.18);
}

.btn-ghost {
    background-color: transparent;
    color: var(--src-ruby);
    border-color: transparent;
}

.btn-ghost:hover {
    background-color: var(--src-ruby-tint);
}

.btn-danger {
    background-color: var(--src-error);
    color: white;
    border-color: var(--src-error);
}

.btn-danger:hover {
    background-color: #b71c1c;
    border-color: #b71c1c;
}

.btn-sapphire {
    background-color: var(--src-sapphire);
    color: white;
    border-color: var(--src-sapphire);
}

.btn-sapphire:hover {
    background-color: var(--src-sapphire-hover);
    border-color: var(--src-sapphire-hover);
}

.btn-sm {
    padding: 5px 10px;
    font-size: var(--src-text-xs);
    min-height: 26px;
}

/* One step below .btn-sm, for controls that sit inside a row of content rather
   than in a toolbar (message-thread actions, no-JS submit buttons beside a
   select). It sat in approvals.css until 2026-08-11, where nothing on this tree
   emitted it while reservations and leadgen emitted it on 10 buttons that
   never loaded that sheet — so the size silently did not apply. */
.btn-xs {
    padding: 2px 6px;
    font-size: var(--src-text-xs);
    min-height: 20px;
}

.btn-lg {
    padding: var(--src-space-3) var(--src-space-6);
    font-size: var(--src-text-md);
    min-height: 44px;
}

.btn-icon {
    padding: var(--src-space-2);
    min-height: 32px;
    min-width: 32px;
}

.btn-icon.btn-sm {
    padding: var(--src-space-1);
    min-height: 26px;
    min-width: 26px;
}

.btn-block {
    width: 100%;
}


/* ===================
   CARDS
   =================== */

.card {
    background-color: var(--src-surface-card);
    border: 1px solid var(--src-border-medium);
    border-radius: var(--src-radius-medium);
    box-shadow: var(--src-shadow-1);
    overflow: hidden;
}

.card-header {
    padding: var(--src-space-4) var(--src-space-5);
    border-bottom: 1px solid var(--src-divider);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--src-space-4);
}

.card-header h3,
.card-header h4 {
    margin: 0;
}

.card-body {
    padding: var(--src-space-5);
}

.card-footer {
    padding: var(--src-space-4) var(--src-space-5);
    border-top: 1px solid var(--src-divider);
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--src-space-3);
}

.card-interactive {
    cursor: pointer;
    transition: all var(--src-transition-fast);
}

.card-interactive:hover {
    box-shadow: var(--src-shadow-2);
    border-color: var(--src-border-strong);
}

.card-flat {
    box-shadow: none;
}


/* An inline text link that reads a touch stronger than body text — a filename in
   a list, a record reference in a cell.

   TWO properties, deliberately. base.css's `a` rule already gives colour,
   text-decoration:none and the colour transition, and `a:hover` already gives
   the hover colour; re-declaring them here would add nothing and create a place
   the base can move without this following.

   The hover underline is declared privately at 12 sites across 8 stylesheets
   while base.css's a:hover changes colour only — so the app has two live
   link-hover behaviours and DESIGN.md settles only the link colour. Whether the
   underline belongs on the base a:hover (retiring all 12) is a design-language
   question for that whole population, not one this class may decide. Local
   until then. */

.attachment-link { font-weight: var(--src-weight-medium); }
.attachment-link:hover { text-decoration: underline; }


/* ===================
   SECTION PANEL
   ===================

   The self-padded content surface: a ticket header block, a "Thread" /
   "Attachments" / "History" section, a dashboard section. It holds its content
   DIRECTLY and owns its own padding — its heading is `.section-title--sub`.

   NOT `.card` and NOT `.panel-*`. Three surfaces, three situations; the radius
   scale in tokens.css is tiered BY ROLE (micro 6 / interactive 8 / medium 12 /
   secondary 16 / structural 24), so the radius difference below is a role
   statement, not drift:
     .card          medium 12px, no padding, slot-composed (-header/-body/
                    -footer) with overflow:hidden for flush media
     .section-panel secondary 16px, own padding, content sits directly inside
     .panel-*       the slide-in OVERLAY (see PANELS below) — unrelated surface

   Declared here, once. A module that re-declares this body under its own page
   scope (`.{module}-page .{module}-thing`) makes it unreachable from every
   other module, which is what forces the next copy. */

.section-panel {
    background: var(--src-surface-solid);
    border: 1px solid var(--src-border-medium);
    border-radius: var(--src-radius-secondary);
    box-shadow: var(--src-shadow-1);
    padding: var(--src-space-5);
}


/* ===================
   TABLES
   =================== */

.table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--src-text-base);
}

.table thead {
    border-bottom: 2px solid var(--src-border-strong);
}

.table th {
    padding: var(--src-space-3) var(--src-space-4);
    text-align: left;
    font-size: var(--src-text-sm);
    font-weight: var(--src-weight-semibold);
    color: var(--src-text-secondary);
    text-transform: uppercase;
    letter-spacing: var(--src-tracking-wide);
    white-space: nowrap;
}

.table td {
    padding: var(--src-space-3) var(--src-space-4);
    border-bottom: 1px solid var(--src-divider);
    vertical-align: middle;
}

.table tbody tr:last-child td {
    border-bottom: none;
}

.table-striped tbody tr:nth-child(even) {
    background-color: var(--src-surface-card-raised);
}

.table-hover tbody tr {
    transition: background-color var(--src-transition-fast);
}

.table-hover tbody tr:hover {
    background-color: var(--src-surface-card-hover);
}

.table-compact th,
.table-compact td {
    padding: var(--src-space-2) var(--src-space-3);
}

.table-clickable tbody tr {
    cursor: pointer;
}

.table td.text-right,
.table th.text-right {
    text-align: right;
}

/* Cell treatments. These used to be declared per module — .reservations-td--meta,
   .leadgen-td--meta and their icon twins, byte-identical under two page scopes —
   which is the shared table's own vocabulary owned by a leaf. They belong here,
   bound to .table, so every consumer reaches them at one specificity.
   NOTE: .roster-td--meta is NOT one of these. Same name, different declaration
   (a wrapping grid cell), so it stays where it is. */
.table td.cell-meta {
    color: var(--src-text-secondary);
    font-size: var(--src-text-xs);
    white-space: nowrap;
}

.table td.cell-icon {
    width: 32px;
    text-align: center;
    color: var(--src-text-tertiary);
}

.table td.text-center,
.table th.text-center {
    text-align: center;
}

/* ─── Shared table scroll container ────────────────────────────────
   THE canonical treatment for wide data tables across the whole app.
   Pattern: natural readable column widths + contained horizontal
   scroll. The scrollbar lives on this box; the page never overflows.

   Rules that make it work:
   1. overflow-x: auto  — horizontal scrollbar appears on this box
                          when its child table is wider than the box.
   2. No overflow:hidden on any ancestor — hidden kills the scrollbar
      before it can render. Keep overflow:hidden out of card/section
      parents; clip their border-radius on sub-elements instead.
   3. Child table: min-width:max-content — columns keep natural widths
      and never compress. width:100% satisfies wide viewports; the
      min-width floor satisfies narrow ones.

   Roster module uses .roster-table-wrap which follows the same rules.
   ──────────────────────────────────────────────────────────────── */
.table-wrapper {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    border: 1px solid var(--src-border);
    border-radius: var(--src-radius-lg);
    background: var(--src-surface-card);
}

.table-wrapper .table {
    margin: 0;
    min-width: max-content;  /* never squash columns — see comment above */
}


/* ===================
   BADGES
   =================== */

.badge {
    display: inline-flex;
    align-items: center;
    gap: var(--src-space-1);
    padding: 2px var(--src-space-2);
    font-size: var(--src-text-xs);
    font-weight: var(--src-weight-semibold);
    letter-spacing: var(--src-tracking-wide);
    text-transform: uppercase;
    border-radius: var(--src-radius-sm);
    white-space: nowrap;
    line-height: 1.4;
}

.badge-success {
    background-color: var(--src-success-bg);
    /* Text on the family's own tint takes the readable member: the raw hue reads
       1.81:1 here. Same repoint as .alert-success. [STATUS-HUE-ON-ITS-OWN-TINT] */
    color: var(--src-success-text);
}

.badge-error {
    background-color: var(--src-error-bg);
    color: var(--src-error);
}

.badge-warning {
    background-color: var(--src-warning-bg);
    /* 2.02:1 on the tint — readable member. [STATUS-HUE-ON-ITS-OWN-TINT] */
    color: var(--src-warning-text);
}

.badge-info {
    background-color: var(--src-info-bg);
    color: var(--src-info);
}

.badge-neutral {
    background-color: rgba(2, 36, 60, 0.06);
    color: var(--src-text-secondary);
}

.badge-ruby {
    background-color: var(--src-ruby-tint);
    color: var(--src-ruby);
}

.badge-sandstone {
    background-color: var(--src-sandstone-tint);
    color: var(--src-sandstone-hover);
}

.badge-sapphire {
    background-color: var(--src-sapphire-light);
    color: var(--src-sapphire);
}

.badge-dot::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: var(--src-radius-round);
    background-color: currentColor;
    flex-shrink: 0;
}


/* ===================
   FORMS
   =================== */

.form-group {
    margin-bottom: var(--src-space-5);
}

.form-label {
    display: flex;
    align-items: center;
    gap: var(--src-space-1);
    margin-bottom: var(--src-space-2);
    font-size: var(--src-text-sm);
    font-weight: var(--src-weight-medium);
    color: var(--src-text-secondary);
    text-transform: uppercase;
    letter-spacing: var(--src-tracking-wide);
}

.form-label .required {
    color: var(--src-ruby);
}

.form-input,
.form-select,
.form-textarea {
    width: 100%;
    padding: var(--src-space-2) var(--src-space-3);
    font-size: var(--src-text-base);
    background-color: var(--src-surface-input);
    border: 1px solid var(--src-border-input);
    border-radius: var(--src-radius-md);
    transition: border-color var(--src-transition-fast), box-shadow var(--src-transition-fast);
    min-height: 40px;
    color: var(--src-text-primary);
}

.form-input:hover,
.form-select:hover,
.form-textarea:hover {
    border-color: var(--src-border-strong);
}

.form-input:focus,
.form-select:focus,
.form-textarea:focus {
    border-color: var(--src-primary);
    box-shadow: 0 0 0 3px rgba(2, 36, 60, 0.1);
    background: var(--src-surface-solid);
    outline: none;
}

.form-input::placeholder,
.form-textarea::placeholder {
    color: var(--src-text-tertiary);
}

.form-input:disabled,
.form-select:disabled,
.form-textarea:disabled {
    background-color: var(--src-surface-input-disabled);
    color: var(--src-text-disabled);
    cursor: not-allowed;
}

.form-select {
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2302243c' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right var(--src-space-3) center;
    padding-right: var(--src-space-8);
}

.form-textarea {
    min-height: 100px;
    line-height: var(--src-leading-normal);
}

/* Field width steps. A field narrower than its row picks one of these — never an
   inline max-width. Three steps only; a fourth is drift, not a requirement. */
/* Compact control size. Shared vocabulary: emitted by leadgen, reservations,
   learning, approvals and dept-roster sidebars and filter rows. It lived in
   roster.css until 2026-08-13, where it resolved only on pages that linked that
   sheet — every other module emitted the class and got the full-size control.
   Moved verbatim, so the roster pages that already had it are unchanged.
   These are standalone control declarations rather than size deltas on
   .form-select / .form-input; measuring them back against the base is a
   separate act. uiux vault → U2, shared-vocabulary clause. */

.form-select-sm {
    padding: var(--src-space-1) var(--src-space-8) var(--src-space-1) var(--src-space-3);
    min-height: 32px;
    font-size: var(--src-text-sm);
    border: 1px solid var(--src-border-medium);
    border-radius: var(--src-radius-interactive);
    background: var(--src-surface-solid);
    color: var(--src-text-primary);
    font-family: var(--src-font-body);
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2302243c' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right var(--src-space-3) center;
    cursor: pointer;
    transition: border-color var(--src-transition-state);
}

.form-select-sm:focus {
    border-color: var(--src-border-focus);
    box-shadow: 0 0 0 3px var(--src-primary-tint);
    outline: none;
}

.form-input-sm {
    padding: var(--src-space-1) var(--src-space-3);
    min-height: 32px;
    font-size: var(--src-text-sm);
}

.form-input-w-sm { max-width: var(--src-field-w-sm); }
.form-input-w-md { max-width: var(--src-field-w-md); }
.form-input-w-lg { max-width: var(--src-field-w-lg); }

/* Native colour picker. The browser draws the swatch, so the control needs an
   explicit box to be consistent across browsers — no JS. Shared vocabulary: every
   module that lets an operator pick a display colour renders the same control
   (hr-roster shift codes, leave types), so it lives here rather than in either
   module's sheet. Note the modifier grammar is `--color` where the neighbours
   above read `-error` / `-w-sm`: the name came with the rule when it moved out of
   roster.css and renaming it would touch emitters outside the templates. */
.form-input--color {
    width: 48px;
    height: 36px;
    padding: 2px;
    border: 1px solid var(--src-border);
    border-radius: var(--src-radius-sm);
    cursor: pointer;
    background: var(--src-surface-solid);
}

.form-input--color:focus {
    outline: 2px solid var(--src-border-focus);
    outline-offset: 2px;
}

/* The colour control's row: the swatch, and the stored value as readable text.
   The browser exposes the hex to nobody, so without the label an operator can
   see the colour and cannot read or copy it. Emitted by the shared form theme's
   color_widget block (templates/form/src_form_theme.html.twig), never by a page
   template — which is why it lives beside .form-input--color rather than in a
   module sheet: one control, one row, every module that picks a colour. */
.form-color-control {
    display: flex;
    align-items: center;
    gap: var(--src-space-2);
}

.form-color-value {
    font-family: var(--src-font-mono);
    font-size: var(--src-text-xs);
    color: var(--src-text-secondary);
}

/* Validation-error affordance on the CONTROL. Emitted by the shared form theme's
   widget_attributes block (templates/form/src_form_theme.html.twig), which is the
   block EVERY control renders through — so this class lands on inputs, textareas,
   selects, checkboxes and radios alike, and it owes each of them something visible.
   One declaration per rendering family, which is why there are two blocks and not
   one: see the uiux vault's STATE-CHANNEL approach. */
.form-input-error {
    border-color: var(--src-border-error) !important;
}

.form-input-error:focus {
    box-shadow: 0 0 0 3px var(--src-error-bg) !important;
}

/* Natively-rendered controls. .form-checkbox input / .form-radio input keep the
   UA's own painting (accent-color, no appearance:none), and a natively-painted
   checkbox DISCARDS border-color entirely — the rule above matches, wins the
   cascade, and draws nothing. Outline is the channel a native control does honour,
   so the state reaches the eye on the one control family the border cannot.
   Values are the design system's own focus vocabulary (base.css :focus-visible),
   re-pointed at the error token — not a second focus treatment.

   :not(:focus-visible) is load-bearing, not tidiness. Outline is the SAME channel
   the global focus ring uses, and this selector outranks base.css's bare
   :focus-visible — so without the guard a focused errored checkbox would wear the
   error outline INSTEAD of its focus ring, silently costing a keyboard user their
   position to state a fact the error message already states. The border half above
   has no such collision: border-color and outline are different channels and
   coexist, which is why only this block carries the guard. */
input[type="checkbox"].form-input-error:not(:focus-visible),
input[type="radio"].form-input-error:not(:focus-visible) {
    outline: 2px solid var(--src-border-error);
    outline-offset: 2px;
}

.form-error {
    display: flex;
    align-items: center;
    gap: var(--src-space-1);
    margin-top: var(--src-space-1);
    font-size: var(--src-text-sm);
    color: var(--src-error);
}

.form-hint {
    margin-top: var(--src-space-1);
    font-size: var(--src-text-sm);
    color: var(--src-text-tertiary);
}

.form-row {
    display: flex;
    gap: var(--src-space-4);
}

.form-row > .form-group {
    flex: 1;
}

.form-checkbox,
.form-radio {
    display: flex;
    align-items: center;
    gap: var(--src-space-2);
    cursor: pointer;
    font-size: var(--src-text-base);
}

.form-checkbox input,
.form-radio input {
    width: 18px;
    height: 18px;
    accent-color: var(--src-sapphire);
    cursor: pointer;
}

.form-actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--src-space-3);
    padding-top: var(--src-space-5);
    border-top: 1px solid var(--src-divider);
    margin-top: var(--src-space-5);
}

.form-action-delete { margin-right: auto; }


/* ===================
   MODAL
   =================== */

.modal-overlay {
    position: fixed;
    inset: 0;
    background-color: var(--src-surface-overlay);
    z-index: var(--src-z-modal-overlay);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--src-floating-gap);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--src-transition-base), visibility var(--src-transition-base);
}

.modal-overlay.active {
    opacity: 1;
    visibility: visible;
}

.modal {
    background-color: var(--src-surface-card);
    border: 1px solid var(--src-border);
    border-radius: var(--src-radius-lg);
    box-shadow: var(--src-shadow-modal);
    z-index: var(--src-z-modal);
    width: 100%;
    max-width: var(--src-modal-width);
    max-height: 100%;
    display: flex;
    flex-direction: column;
    transform: scale(0.95) translateY(10px);
    transition: transform var(--src-transition-base);
}

.modal-overlay.active .modal {
    transform: scale(1) translateY(0);
}

/* A form wrapping header/body/footer must itself be the bounded flex column,
   or the body never scrolls and the footer spills past max-height. */
.modal > form {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--src-space-5);
    border-bottom: 1px solid var(--src-divider);
    flex-shrink: 0;
}

.modal-header h3 {
    margin: 0;
}

.modal-close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: var(--src-radius-md);
    color: var(--src-text-secondary);
    transition: all var(--src-transition-fast);
}

.modal-close:hover {
    background-color: rgba(2, 36, 60, 0.06);
    color: var(--src-text-primary);
}

.modal-body {
    padding: var(--src-space-5);
    overflow-y: auto;
    flex: 1;
}

.modal-footer {
    display: flex;
    justify-content: flex-end;
    gap: var(--src-space-3);
    padding: var(--src-space-4) var(--src-space-5);
    border-top: 1px solid var(--src-divider);
    flex-shrink: 0;
}

.modal-lg {
    max-width: var(--src-modal-width-lg);
}

.modal-sm {
    max-width: var(--src-modal-width-sm);
}

/* A sheet whose body is written into rather than read off — it owns a tall,
   stable box instead of resizing with its content. Bounded by .modal's cap. */
.modal--tall {
    height: var(--src-modal-height-tall);
}


/* ===================
   SLIDE PANEL
   =================== */

.panel-overlay {
    position: fixed;
    top: calc(var(--src-topbar-height) + var(--src-floating-gap) * 2);
    left: calc(var(--src-sidebar-width) + var(--src-floating-gap) * 2);
    right: var(--src-floating-gap);
    bottom: var(--src-floating-gap);
    background-color: rgba(255, 255, 255, 0.15);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    border-radius: var(--src-radius-structural);
    z-index: var(--src-z-panel);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--src-transition-base), visibility var(--src-transition-base);
}

.panel-overlay.active {
    opacity: 1;
    visibility: visible;
}

.slide-panel {
    position: fixed;
    top: calc(var(--src-topbar-height) + var(--src-floating-gap) + var(--src-floating-gap));
    right: var(--src-floating-gap);
    bottom: var(--src-floating-gap);
    width: var(--src-panel-width);
    max-width: 90vw;
    background: linear-gradient(180deg, #fafbfc 0%, #ffffff 100px);
    border-radius: var(--src-radius-structural);
    border: 1px solid rgba(2, 36, 60, 0.08);
    box-shadow: var(--src-shadow-panel);
    z-index: calc(var(--src-z-panel) + 1);
    display: flex;
    flex-direction: column;
    transform: translateX(calc(100% + var(--src-floating-gap) * 2));
    transition: transform var(--src-transition-panel);
    overflow: hidden;
}

.slide-panel.active {
    transform: translateX(0);
}

.panel-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 24px 24px 20px;
    background: linear-gradient(180deg, rgba(2, 36, 60, 0.02) 0%, transparent 100%);
    border-bottom: 1px solid rgba(2, 36, 60, 0.06);
    flex-shrink: 0;
}

.panel-header h3 {
    margin: 0;
    font-size: var(--src-text-lg);
    font-weight: var(--src-weight-semibold);
    color: var(--src-text-primary);
    max-width: 320px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.panel-close-btn {
    width: 36px;
    height: 36px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: transparent;
    color: var(--src-text-secondary);
    font-size: 24px;
    cursor: pointer;
    border-radius: var(--src-radius-interactive);
    transition: background-color var(--src-transition-fast), color var(--src-transition-fast);
    flex-shrink: 0;
}

.panel-close-btn:hover {
    background-color: rgba(2, 36, 60, 0.06);
    color: var(--src-text-primary);
}

.panel-body {
    padding: 20px 24px 24px;
    overflow-y: auto;
    flex: 1;
}

.panel-body::-webkit-scrollbar {
    width: 6px;
}

.panel-body::-webkit-scrollbar-track {
    background: transparent;
}

.panel-body::-webkit-scrollbar-thumb {
    background-color: var(--src-gray-300);
    border-radius: var(--src-radius-pill);
}

.panel-body::-webkit-scrollbar-thumb:hover {
    background-color: var(--src-gray-400);
}

.panel-footer {
    display: flex;
    justify-content: flex-end;
    gap: var(--src-space-3);
    padding: 16px 24px;
    border-top: 1px solid rgba(2, 36, 60, 0.06);
    flex-shrink: 0;
}


/* ─── Panel Content Components ────────────────────────── */

.panel-section {
    margin-bottom: var(--src-space-4);
    padding: var(--src-space-4);
    background: var(--src-surface-card);
    border-radius: var(--src-radius-lg);
    border: 1px solid rgba(2, 36, 60, 0.06);
    box-shadow: 0 1px 3px rgba(2, 36, 60, 0.02);
    transition: border-color var(--src-transition-fast), box-shadow var(--src-transition-fast);
}

.panel-section:hover {
    border-color: rgba(2, 36, 60, 0.1);
    box-shadow: 0 2px 8px rgba(2, 36, 60, 0.06);
}

.panel-section-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: var(--src-space-3);
}

.panel-section-label {
    display: flex;
    align-items: center;
    gap: var(--src-space-2);
    font-size: var(--src-text-xs);
    font-weight: var(--src-weight-bold);
    color: var(--src-text-secondary);
    text-transform: uppercase;
    letter-spacing: var(--src-tracking-wide);
    margin-bottom: var(--src-space-3);
}

.panel-section-header .panel-section-label {
    margin-bottom: 0;
}

.panel-section-label i {
    font-size: 14px;
    opacity: 0.6;
}

.panel-fields-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: var(--src-space-3);
    margin-bottom: var(--src-space-4);
}

.panel-field {
    display: flex;
    flex-direction: column;
    gap: var(--src-space-1);
    padding: var(--src-space-3);
    background: var(--src-surface-card);
    border: 1px solid rgba(2, 36, 60, 0.08);
    border-radius: var(--src-radius-md);
    transition: border-color var(--src-transition-fast), box-shadow var(--src-transition-fast);
}

.panel-field:hover {
    border-color: rgba(2, 36, 60, 0.12);
    box-shadow: 0 2px 8px rgba(2, 36, 60, 0.06);
}

.panel-field .form-label {
    margin-bottom: 0;
}

.panel-field .form-input,
.panel-field .form-select {
    border: none;
    background: transparent;
    padding: var(--src-space-1) 0;
    font-weight: var(--src-weight-medium);
}

.panel-field .form-input:focus,
.panel-field .form-select:focus {
    box-shadow: none;
    outline: none;
    background: rgba(2, 36, 60, 0.02);
    border-radius: var(--src-radius-sm);
    padding: var(--src-space-1) var(--src-space-2);
}

.panel-list {
    display: flex;
    flex-direction: column;
    gap: var(--src-space-2);
}

.panel-dynamic-row {
    display: flex;
    align-items: flex-start;
    gap: var(--src-space-2);
    padding: var(--src-space-3);
    background: rgba(2, 36, 60, 0.02);
    border-radius: var(--src-radius-md);
    border: 1px solid rgba(2, 36, 60, 0.04);
    margin-bottom: var(--src-space-2);
}
.panel-dynamic-row-fields {
    display: flex;
    flex-wrap: wrap;
    gap: var(--src-space-3);
    flex: 1;
    min-width: 0;
}
.panel-dynamic-row-fields .form-group { flex: 1; min-width: 100px; }
.panel-dynamic-row-remove {
    flex-shrink: 0;
    margin-top: 24px;
}

.panel-list-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--src-space-3);
    padding: var(--src-space-3);
    background: rgba(2, 36, 60, 0.02);
    border-radius: var(--src-radius-md);
    border: 1px solid rgba(2, 36, 60, 0.04);
    transition: background-color var(--src-transition-fast);
}

/* Flex children default to min-width:auto and refuse to shrink below their
   min-content width — with nowrap badges and buttons inside, the row grows
   past the panel and .slide-panel's overflow:hidden clips the actions. */
.panel-list-item > * {
    min-width: 0;
}

/* Action cluster keeps its full width and stays right-aligned; when the label
   side leaves it no room, it takes its own line instead of leaving the panel. */
.panel-list-item > :last-child:not(:only-child) {
    flex-shrink: 0;
    margin-left: auto;
}

/* Safety valve: a badge is a short token, but callers pass lists into it.
   Wrap rather than overflow — only engages when the badge cannot fit. */
.panel-list-item .badge {
    max-width: 100%;
    white-space: normal;
}

.panel-list-item:hover {
    background: rgba(2, 36, 60, 0.04);
}

.panel-list-empty {
    display: flex;
    align-items: center;
    gap: var(--src-space-2);
    padding: var(--src-space-3);
    color: var(--src-text-tertiary);
    font-size: var(--src-text-sm);
    font-style: italic;
}

.panel-list-empty i {
    opacity: 0.4;
}

.panel-footer-meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--src-space-3) 0;
    margin-top: var(--src-space-4);
    border-top: 1px solid rgba(2, 36, 60, 0.06);
    font-size: var(--src-text-sm);
    color: var(--src-text-tertiary);
}

.panel-footer-meta i {
    margin-right: var(--src-space-1);
    opacity: 0.5;
}


/* ===================
   SIDEBAR — C2 Contextual Swap
   =================== */

/* ── Shell ── */
.sidebar {
    position: fixed;
    top: var(--src-floating-gap);
    left: var(--src-floating-gap);
    bottom: var(--src-floating-gap);
    width: var(--src-sidebar-width);
    background: var(--src-surface-sidebar-shell);
    border-radius: var(--src-radius-structural);
    border: 1px solid rgba(255, 255, 255, 0.95);
    outline: 1px solid rgba(0, 0, 0, 0.04);
    outline-offset: -1px;
    box-shadow:
        0 8px 24px rgba(2, 36, 60, 0.12),
        0 2px 4px rgba(2, 36, 60, 0.15),
        inset 0 1px 0 rgba(255, 255, 255, 1);
    z-index: var(--src-z-sidebar);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* ── Header — centered brand crest, sealed zone (H2) ──
   The crest is centered in symmetric padding and the zone is closed by a
   full-bleed hairline (.sb-zone-seal). The axis change from centered crest
   to left-aligned nav is structural: the seal marks where brand ends and
   navigation begins. The « back affordance floats left below the seal;
   the module title rejoins the nav's left axis. */
.sb-header {
    padding: 16px 0 0;
    flex-shrink: 0;
}

.sb-brand-zone {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 0 var(--src-space-5) 13px;
    text-decoration: none;
}

.sb-brand-zone img {
    max-width: 160px;
    height: auto;
}

/* Zone seal: full-bleed hairline closes the crest region */
.sb-zone-seal {
    height: 1px;
    background: rgba(30,41,59,0.08);
    width: 100%;
}

/* Context row: « floats left below the seal, title on the nav axis.
   Vertical balance: gap above (12) = row pad-below (4) + body pad-top (8)
   + divider margin-top (0) — title sits centered between the two lines. */
.sb-context-row {
    display: flex;
    align-items: center;
    padding: 12px var(--src-space-5) 4px;
}

/* ── Scroll body ── */
.sb-body {
    flex: 1;
    overflow-y: auto;
    overflow-x: hidden;
    padding: var(--src-space-2) 0 0;
}

.sb-body::-webkit-scrollbar { width: 3px; }
.sb-body::-webkit-scrollbar-thumb { background: rgba(30,41,59,0.08); border-radius: 2px; }

/* ── Separator ── */
.sb-sep {
    height: 1px;
    background: rgba(30,41,59,0.06);
    margin: var(--src-space-2) var(--src-space-5);
}

/* ── Footer ── */
.sb-footer {
    padding: 11px var(--src-space-5) 13px;
    border-top: 1px solid rgba(30,41,59,0.06);
    font-size: 8px;
    font-weight: 700;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: rgba(2,36,60,0.16);
    flex-shrink: 0;
    background: var(--src-surface-sidebar-shell);
}

/* Plane badge — WHICH plane you are on, in the footer that already carried the
   letter (G = served, B = under review). The letter alone was invisible: it
   inherited the wordmark's 8px / rgba(2,36,60,0.16), which measures 1.38:1, and
   testers reported not knowing which plane they were on. The colour is the
   glanceable channel; the LETTER STAYS, because colour alone fails a
   colour-blind reader and a mono screenshot — the two together are one fact
   said once, in two channels that survive different failures. Same tokens the
   topbar cup uses, from the same single plane_role() read, so the two ends of
   the shell cannot disagree. An unknown role emits `sb-plane--`, which matches
   neither rule and renders as an uncoloured letter on the neutral tint. */
.sb-plane {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 14px;
    height: 14px;
    margin-left: 3px;
    padding: 0 4px;
    border-radius: 3px;
    letter-spacing: 0;
    background: var(--src-gray-100, #f3f4f6);
    color: rgba(2,36,60,0.55);
}

.sb-plane--live {
    background: var(--src-plane-live-bg, rgba(21,128,61,0.10));
    color: var(--src-plane-live, #15803d);
}

.sb-plane--next {
    background: var(--src-plane-next-bg, rgba(29,78,216,0.10));
    color: var(--src-plane-next, #1d4ed8);
}

/* ── Root nav rows — Dashboard + module list ── */
.sb-root-section {
    padding: var(--src-space-1) 0;
}

.sb-root-label {
    font-size: 8px;
    font-weight: 700;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: rgba(2,36,60,0.18);
    padding: 12px var(--src-space-5) var(--src-space-1);
}

/* Inset pill row — shared geometry for root items and module page rows */
.sb-row {
    display: flex;
    align-items: center;
    gap: var(--src-space-2);
    margin: 0 8px;
    padding: 7px var(--src-space-3) 7px calc(var(--src-space-5) - 8px);
    border-radius: 8px;
    font-size: 13px;
    font-weight: 400;
    color: rgba(2,36,60,0.48);
    text-decoration: none;
    cursor: pointer;
    position: relative;
    transition:
        color var(--src-sidebar-dur-wake) var(--src-sidebar-ease),
        background var(--src-sidebar-dur-wake) var(--src-sidebar-ease);
}

.sb-row .sb-row-icon {
    font-size: 12px;
    width: 14px;
    text-align: center;
    flex-shrink: 0;
    color: rgba(2,36,60,0.24);
    transition: color var(--src-sidebar-dur-wake) var(--src-sidebar-ease);
}

.sb-row:hover {
    color: rgba(2,36,60,0.85);
}

.sb-row:hover .sb-row-icon { color: rgba(2,36,60,0.42); }

/* Active leaf — sandstone bar marker + warm tan tint. */
.sb-row.active {
    color: var(--src-primary);
    font-weight: 600;
    background: var(--src-sidebar-tan-ghost);
}

.sb-row.active .sb-row-icon { color: rgba(2,36,60,0.52); }

.sb-row.active::before {
    content: '';
    position: absolute;
    left: 6px;
    top: 50%;
    transform: translateY(-50%);
    width: 3px;
    height: 60%;
    min-height: 12px;
    border-radius: 2px;
    background: var(--src-sandstone);
}

/* ── Back affordance (standalone « in the context row) ── */
.sb-bc-back {
    font-size: 10px;
    color: rgba(2,36,60,0.28);
    width: 20px;
    height: 20px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 6px;
    flex-shrink: 0;
    text-decoration: none;
    transition:
        color var(--src-sidebar-dur-wake) var(--src-sidebar-ease),
        background var(--src-sidebar-dur-wake) var(--src-sidebar-ease);
}

.sb-bc-back:hover {
    color: rgba(2,36,60,0.60);
    background: rgba(2,36,60,0.05);
}

.sb-bc-current {
    font-size: 13px;
    font-weight: 700;
    color: rgba(2,36,60,0.82);
    letter-spacing: -0.01em;
    line-height: 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    /* Nudge to sit on the nav text column */
    padding-left: 2px;
}

/* ── Context divider — sits after the context row ── */
.sb-c2-divider {
    height: 1px;
    background: rgba(30,41,59,0.07);
    margin: 0 var(--src-space-5) 6px;
}

/* ── Section groups (module context) ── */
.sb-group-label {
    font-size: 8px;
    font-weight: 700;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: rgba(2,36,60,0.18);
    padding: 12px var(--src-space-5) var(--src-space-1);
}

/* Receded (off-path) group.

   Recede means DE-EMPHASISED, not hidden. These values were 0.10-0.16 alpha,
   which is roughly a 1.25:1 contrast ratio on the sidebar — below the 3:1 floor
   for non-text UI and far below the 4.5:1 for body text. The rows were on
   screen, navigable and completely unreadable, so the group read as decoration
   or as disabled controls rather than as links.

   It went unnoticed because nothing exercised it: no live module had page
   groups until Learning Centre, so `.receded` had never rendered to a user.
   Martin hit it within a minute of the first one shipping — he could not find
   Centre Admin while looking straight at it.

   The active group sits at 0.88 (11.15:1). The receded set does NOT clear the
   AA floors — MEASURED 2026-09-06 against --src-surface-sidebar-shell #ffffff,
   the surface .sidebar paints: receded-text 0.60 = 4.37:1 (floor 4.5, misses
   by 0.13), receded-icon 0.40 = 2.45:1 and receded-label 0.30 = 1.91:1 (floor
   3.0 non-text). The ON-path .sb-row above, rgba(2,36,60,0.48), is a FOURTH
   miss: 3.05:1 at 13px/400 against the same 4.5 floor — and it is lighter than
   the band that is meant to recede from it, so the hierarchy reads inverted.
   The values stay as they are because which side moves is an intent-vs-
   standard fork (tokens.css, the receded block; LeoMemoryDB ns 26640/28088);
   the sentence that used to sit here claimed ~4.4:1 "clears" the floor and is
   what stopped anyone measuring. Hover now lands ABOVE the resting state
   rather than below it, which is the other half of the bug: at 0.50 hover the
   old rule made a row that was already illegible get dimmer relative to
   nothing, and pointing at a link has to brighten it. */
.sb-group.receded .sb-group-label {
    color: var(--src-sidebar-receded-label);
}

.sb-group.receded .sb-row {
    color: var(--src-sidebar-receded-text) !important;
    background: transparent !important;
}

.sb-group.receded .sb-row .sb-row-icon {
    color: var(--src-sidebar-receded-icon) !important;
}

.sb-group.receded .sb-row:hover {
    color: var(--src-sidebar-receded-hover-text) !important;
    background: var(--src-sidebar-receded-hover-fill) !important;
}

.sb-group.receded .sb-row:hover .sb-row-icon {
    color: var(--src-sidebar-receded-hover-icon) !important;
}

/* Active group — no extra class needed; absence of .receded is the state */

/* ── Presence — tabbed Online / Offline ── */

/* Tab bar: pill-style segmented control spanning the presence zone */
.sb-presence-tabbar {
    display: flex;
    align-items: center;
    gap: 2px;
    padding: var(--src-space-2) var(--src-space-3) var(--src-space-1);
    background: rgba(2,36,60,0.03);
    border-radius: var(--src-radius-interactive);
    margin: var(--src-space-1) var(--src-space-3) var(--src-space-1);
}

/* Individual tab buttons */
.sb-presence-tab {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 8px;
    font-size: 10px;
    font-weight: var(--src-weight-semibold);
    color: var(--src-text-tertiary);
    background: transparent;
    border: none;
    border-radius: var(--src-radius-micro);
    cursor: pointer;
    line-height: 1;
    transition: color var(--src-sidebar-dur-wake) var(--src-sidebar-ease),
                background var(--src-sidebar-dur-wake) var(--src-sidebar-ease);
    white-space: nowrap;
    letter-spacing: 0.01em;
}

.sb-presence-tab:hover {
    color: var(--src-sidebar-wake-text);
    background: rgba(2,36,60,0.05);
}

.sb-presence-tab.is-active {
    color: var(--src-text-primary);
    background: var(--src-surface-solid);
    box-shadow: var(--src-shadow-1);
}

/* Count badge inside each tab */
.sb-presence-tab-count {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: var(--src-text-xs);
    font-weight: var(--src-weight-semibold);
    border-radius: 8px;
    padding: 1px 5px;
}

.sb-presence-tab-count--online {
    color: rgba(22,163,74,0.85);
    background: rgba(34,197,94,0.12);
}

/* Online count only visible when Online tab is active */
.sb-presence-tab--online:not(.is-active) .sb-presence-tab-count--online {
    display: none;
}

.sb-presence-tab-count--offline {
    color: rgba(2,36,60,0.40);
    background: rgba(2,36,60,0.06);
}

/* Offline count only visible when Offline tab is active */
.sb-presence-tab--offline:not(.is-active) .sb-presence-tab-count--offline {
    display: none;
}

/* Dot inside count badges — reuse existing dot classes */
.sidebar-online-count-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--src-sidebar-presence-green);
    flex-shrink: 0;
}

.sidebar-offline-count-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--src-gray-400);
    flex-shrink: 0;
}

/* Sound toggle — pushed to end of the tab bar */
.sidebar-online-sound-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    padding: 0;
    margin-left: auto;
    border: none;
    background: transparent;
    border-radius: var(--src-radius-micro);
    color: var(--src-text-tertiary);
    font-size: 12px;
    line-height: 1;
    cursor: pointer;
    opacity: 0.7;
    flex-shrink: 0;
    transition: opacity var(--src-transition-state), background var(--src-transition-state), color var(--src-transition-state);
}
.sidebar-online-sound-btn:hover {
    opacity: 1;
    background: var(--src-hover-bg);
    color: var(--src-text-secondary);
}

/* Panels — only the active panel is visible (controlled by JS via hidden attr) */
.sb-presence-panel {
    padding-bottom: var(--src-space-1);
}

.sb-presence-panel[hidden] {
    display: none;
}

/* ── Online person rows ── */
.sidebar-online-person,
button.sidebar-online-person--chat {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 5px var(--src-space-3);
    background: none;
    border: none;
    width: 100%;
    cursor: default;
    text-align: left;
}

button.sidebar-online-person--chat { cursor: pointer; }

.sidebar-online-avatar {
    position: relative;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    font-size: 9px;
    font-weight: 700;
    color: rgba(255, 255, 255, 0.85);
    letter-spacing: 0.02em;
    border: 1.5px solid rgba(30,41,59,0.14);
}

.sidebar-online-dot {
    position: absolute;
    right: -1px;
    bottom: -1px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--src-sidebar-presence-green);
    border: 1.5px solid var(--src-surface-sidebar-shell);
}

.sidebar-online-meta {
    display: flex;
    flex-direction: column;
    line-height: 1.25;
    min-width: 0;
    flex: 1;
}

.sidebar-online-name {
    font-size: 11px;
    font-weight: 600;
    color: rgba(2,36,60,0.78);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.sidebar-online-dept {
    font-size: 9px;
    color: rgba(2,36,60,0.50);
    margin-top: 1px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.sidebar-online-unread {
    font-size: 9px;
    font-weight: 600;
    color: var(--src-ruby);
    background: var(--src-sidebar-active-fill);
    border-radius: 8px;
    padding: 2px 7px;
    flex-shrink: 0;
}

/* ── Offline person rows ── */
.sidebar-offline-chat-form {
    margin: 0;
    padding: 0;
    display: block;
}

.sidebar-offline-person,
button.sidebar-offline-person--chat {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 5px var(--src-space-3);
    background: none;
    border: none;
    width: 100%;
    cursor: default;
    text-align: left;
    opacity: 0.7;
}

button.sidebar-offline-person--chat { cursor: pointer; }

button.sidebar-offline-person--chat:hover { opacity: 1; }

.sidebar-offline-avatar {
    position: relative;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    font-size: 9px;
    font-weight: 700;
    color: rgba(255, 255, 255, 0.85);
    letter-spacing: 0.02em;
    border: 1.5px solid rgba(30,41,59,0.14);
    filter: grayscale(0.6);
}

.sidebar-offline-dot {
    position: absolute;
    right: -1px;
    bottom: -1px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--src-gray-400);
    border: 1.5px solid var(--src-surface-sidebar-shell);
}

.sidebar-offline-meta {
    display: flex;
    flex-direction: column;
    line-height: 1.25;
    min-width: 0;
    flex: 1;
}

.sidebar-offline-name {
    font-size: 11px;
    font-weight: 600;
    color: rgba(2,36,60,0.55);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.sidebar-offline-dept {
    font-size: 9px;
    color: rgba(2,36,60,0.38);
    margin-top: 1px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.sidebar-offline-unread {
    font-size: 9px;
    font-weight: 600;
    color: var(--src-ruby);
    background: var(--src-sidebar-active-fill);
    border-radius: 8px;
    padding: 2px 7px;
    flex-shrink: 0;
}

/* ── Presence card — the presence zone floats as its own panel in the rail.
   The live region (.sb-presence) lives INSIDE the card, so live
   refreshes replace people rows without touching card chrome or the slug. */
.sb-presence-card {
    margin: var(--src-space-3) 10px var(--src-space-2);
    background: var(--src-surface-solid);
    border: 1px solid rgba(30,41,59,0.09);
    border-radius: 16px;
    box-shadow: 0 1px 3px rgba(2,36,60,0.05);
    padding: 2px 0 4px;
    position: relative;
    overflow: hidden;
}

/* ── Liquid highlight — a glass capsule that flows between rows.
   Stacking inside each [data-liquid-scope]: scope surface < capsule (z1)
   < row content (z2). The capsule refracts and brightens whatever row it
   covers (shade-shift), with a lit rim and lift shadow; sidebar-liquid.js
   positions it and upgrades the backdrop to an SVG lens on Chromium. */
[data-liquid-scope] { position: relative; }

[data-liquid-scope] .sb-row,
[data-liquid-scope] .sidebar-online-person,
[data-liquid-scope] .sidebar-offline-person,
[data-liquid-scope] .sb-presence-tabbar {
    position: relative;
    z-index: 2;
}

/* Any capsule target keeps its content above the gliding capsule — generic
   form of the row list above, so new scopes (e.g. the travel command rail)
   need no per-class registration here. */
[data-liquid-scope] [data-hl] {
    position: relative;
    z-index: 2;
}

.sb-slug {
    position: absolute;
    left: 0;
    top: 0;
    width: 100px;
    height: 30px;
    border-radius: 999px;
    pointer-events: none;
    opacity: 0;
    z-index: 1;
    background: rgba(255, 255, 255, 0.25);
    box-shadow:
        inset 1.2px 1.2px 1px -0.5px rgba(255,255,255,1),
        inset -0.8px -0.8px 1px -0.5px rgba(255,255,255,0.6),
        inset 0 0 10px rgba(255,255,255,0.25),
        0 6px 16px rgba(2,36,60,0.14),
        0 1.5px 4px rgba(2,36,60,0.10);
    backdrop-filter: blur(1.5px) saturate(1.45) brightness(1.12);
    -webkit-backdrop-filter: blur(1.5px) saturate(1.45) brightness(1.12);
    transition:
        left 300ms cubic-bezier(0.3, 1.15, 0.35, 1),
        top 300ms cubic-bezier(0.3, 1.15, 0.35, 1),
        width 300ms cubic-bezier(0.3, 1.15, 0.35, 1),
        height 300ms cubic-bezier(0.3, 1.15, 0.35, 1),
        border-radius 300ms cubic-bezier(0.3, 1.15, 0.35, 1),
        opacity 200ms ease;
}

.sb-slug.lensed { backdrop-filter: var(--lens-filter); }
.sb-slug.on { opacity: 1; }
.sb-slug.jump { transition: opacity 200ms ease; }


/* ===================
   TOPBAR
   =================== */

.topbar {
    position: sticky;
    top: 0;
    height: var(--src-topbar-height);
    /* Solid — topbar carries breadcrumbs/controls; only the content-area is glass */
    background: var(--src-surface-solid);
    border-radius: var(--src-radius-structural);
    border: 1px solid rgba(255, 255, 255, 0.8);
    box-shadow: 0 4px 16px rgba(2, 36, 60, 0.12);
    z-index: var(--src-z-topbar);
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 var(--src-space-6);
}

.topbar-left {
    display: flex;
    align-items: center;
    gap: var(--src-space-4);
}

.topbar-logo {
    display: flex;
    align-items: center;
    height: 28px;
}

.topbar-logo img {
    height: 100%;
    width: auto;
}

.topbar-breadcrumb {
    display: flex;
    align-items: center;
    gap: var(--src-space-2);
    font-size: var(--src-text-base);
    color: var(--src-text-secondary);
}

.topbar-breadcrumb a {
    color: var(--src-text-secondary);
    text-decoration: none;
    transition: color var(--src-transition-fast);
}

.topbar-breadcrumb a:hover {
    color: var(--src-text-primary);
}

.topbar-breadcrumb .separator {
    color: var(--src-text-tertiary);
    font-size: var(--src-text-sm);
}

.topbar-breadcrumb .current {
    color: var(--src-text-primary);
    font-weight: var(--src-weight-medium);
}

.topbar-right {
    display: flex;
    align-items: center;
    gap: var(--src-space-4);
}

/* ===================
   BLUE-GREEN COLOR SWITCH — Blue-Green/03-ui-color-switch.md §3
   =================== */

.color-switch {
    display: inline-flex;
}

.color-switch-btn {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 6px 14px;
    border: 1px solid var(--src-sandstone);   /* tan outline = invitation, not alarm */
    border-radius: var(--src-radius-md);
    background: var(--src-sandstone-tint);
    color: var(--src-primary);                 /* navy text — never white on light bg */
    font-family: var(--src-font-body);
    font-size: var(--src-text-sm);
    font-weight: var(--src-weight-medium);
    line-height: 1;
    cursor: pointer;
    transition: background-color var(--src-transition-fast), border-color var(--src-transition-fast);
    white-space: nowrap;
}

.color-switch-btn:hover {
    background: var(--src-sandstone-light);
}

.color-switch-btn:focus-visible {
    outline: 2px solid var(--src-border-focus);
    outline-offset: 2px;
}

.color-switch-label {
    font-weight: var(--src-weight-medium);
}

.color-switch-days {
    color: var(--src-text-secondary);
    font-size: var(--src-text-xs);
}

/* Last day — draws the eye without reading as an error (never ruby). */
/* Zero emitters tree-wide — [U9-UNSTYLED] disposition, not a contrast repoint. */
.color-switch-days--warning {
    color: var(--src-warning);
    font-weight: var(--src-weight-semibold);
}

/* Return variant (user is on the new color): quieter — plain outline, no tint. */
.color-switch-btn--return {
    border-color: var(--src-border-strong);
    background: transparent;
}

.color-switch-btn--return:hover {
    background: rgba(2, 36, 60, 0.04);
}


/* ===================
   BLUE-GREEN LOGIN POPUP — Blue-Green/03-ui-color-switch.md §4d
   =================== */

.color-announce-overlay {
    position: fixed;
    inset: 0;
    background-color: var(--src-surface-overlay);
    z-index: var(--src-z-modal-overlay);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--src-space-5);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--src-transition-base), visibility var(--src-transition-base);
}

.color-announce-overlay.active {
    opacity: 1;
    visibility: visible;
}

.color-announce-card {
    background-color: var(--src-surface-card);
    border: 1px solid var(--src-border);
    border-radius: var(--src-radius-xl);
    box-shadow: var(--src-shadow-modal);
    width: 100%;
    max-width: 420px;
    padding: var(--src-space-6);
    text-align: center;
}

.color-announce-title {
    margin: 0 0 var(--src-space-3);
    font-size: var(--src-text-lg);
    font-weight: var(--src-weight-semibold);
    color: var(--src-text-primary);
}

.color-announce-body {
    margin: 0 0 var(--src-space-5);
    font-size: var(--src-text-base);
    color: var(--src-text-secondary);
    line-height: var(--src-leading-normal);
}

.color-announce-actions {
    display: flex;
    flex-direction: column;
    gap: var(--src-space-3);
}

.color-announce-try,
.color-announce-stay {
    width: 100%;
}

.topbar-action {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: var(--src-radius-lg);
    color: var(--src-text-secondary);
    background: rgba(2, 36, 60, 0.04);
    border: none;
    transition: all var(--src-transition-fast);
    position: relative;
}

.topbar-action:hover {
    background-color: rgba(2, 36, 60, 0.08);
    color: var(--src-text-primary);
}

.topbar-action .notification-dot {
    position: absolute;
    top: 6px;
    right: 6px;
    width: 8px;
    height: 8px;
    background-color: var(--src-ruby);
    border-radius: var(--src-radius-round);
    border: 2px solid var(--src-surface-card);
}

/* ── Topbar launcher (releases' announce door) ─────────────────── */
/* Inherits .topbar-action for size, hit area and hover — it must read as a
   sibling of the bell, not as a new species of control. Only the sandstone tint
   is added, which is what distinguishes "an announcement is available" from the
   bell's neutral state without adding a second badge next to a badge.

   It lives HERE, not in releases.css, because it decorates a shared shell
   component and is emitted by a SHELL-REGION FILL (App\Service\Releases\
   ReleasesLauncherFill -> topbar_actions). A fill is rendered by the layout in
   <body>, long after {% block stylesheets %} has closed, so it has no route
   into the head and its module sheet is linked on no page it appears on. In
   releases.css these two rules were inert everywhere except the four releases
   pages. chat/_presence.html.twig is the conformant precedent and says so in
   its own docblock: "All presence styles are already in components.css — no
   extra stylesheet."

   Same specificity as .topbar-action / .topbar-action:hover (0,1,0 and 0,2,0),
   so it must stay BELOW them in this file — order is what makes it win. */

.rel-launcher {
    color: var(--src-sandstone);
}

.rel-launcher:hover {
    color: var(--src-sandstone-hover);
}

/* The launcher's live region: present on every page so it can gain the
   launcher when a release is pinned, and boxless, so an empty one takes no
   slot in .topbar-right's gap and a full one lays the launcher out as the
   bell's direct sibling. */
.rel-launcher-region {
    display: contents;
}

/* ── Notification count badge ──────────────────────────────────── */

.notif-badge {
    position: absolute;
    top: 4px;
    right: 4px;
    min-width: 18px;
    height: 18px;
    padding: 0 4px;
    background-color: var(--src-warning);
    color: var(--src-primary);
    font-size: 10px;
    font-weight: var(--src-weight-bold);
    line-height: 18px;
    text-align: center;
    border-radius: var(--src-radius-pill);
    border: 2px solid var(--src-surface-topbar);
    pointer-events: none;
    transition: opacity var(--src-transition-fast), transform var(--src-transition-fast);
}

.notif-badge--hidden {
    opacity: 0;
    transform: scale(0.5);
    pointer-events: none;
}

/* ── Notifications dropdown panel ──────────────────────────────── */

.notif-dropdown-panel {
    min-width: 320px;
    max-width: 360px;
    padding: 0;
    overflow: hidden;
    /* Inherits .dropdown-menu position/shadow/transition */
}

.notif-dropdown-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--src-space-3) var(--src-space-4);
    border-bottom: 1px solid var(--src-divider);
    background-color: var(--src-surface-raised);
}

.notif-dropdown-title {
    font-size: var(--src-text-sm);
    font-weight: var(--src-weight-semibold);
    color: var(--src-text-secondary);
    text-transform: uppercase;
    letter-spacing: var(--src-tracking-wide);
}

.notif-mark-all-btn {
    background: none;
    border: none;
    padding: 0;
    font-size: var(--src-text-sm);
    font-weight: var(--src-weight-medium);
    color: var(--src-text-link);
    cursor: pointer;
    transition: color var(--src-transition-fast);
}

.notif-mark-all-btn:hover {
    color: var(--src-text-link-hover);
}

/* Empty state */

.notif-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--src-space-2);
    padding: var(--src-space-8) var(--src-space-4);
    color: var(--src-text-tertiary);
    text-align: center;
}

.notif-empty p {
    margin: 0;
    font-size: var(--src-text-sm);
}

/* List */

.notif-list {
    list-style: none;
    margin: 0;
    padding: var(--src-space-1) 0;
    max-height: 400px;
    overflow-y: auto;
}

/* The ROW owns the separator and the unread tint; the item link owns hover.
   Both moved up off .notif-item when the per-item read-state form landed beside
   it — a row is now two controls, and a border or tint on one of them stops at
   its own edge, leaving a seam next to the button. */

.notif-row {
    display: flex;
    align-items: stretch;
    border-bottom: 1px solid var(--src-border-subtle);
}

.notif-row:last-child {
    border-bottom: none;
}

.notif-row--unread {
    background-color: rgba(245, 158, 11, 0.06);
}

.notif-item {
    display: flex;
    flex: 1;
    min-width: 0;
    align-items: flex-start;
    gap: var(--src-space-3);
    padding: var(--src-space-3) var(--src-space-4);
    cursor: pointer;
    transition: background-color var(--src-transition-fast);
    text-decoration: none;
    color: inherit;
}

.notif-item:hover {
    background-color: var(--src-surface-card-hover);
}

.notif-item--unread:hover {
    background-color: rgba(245, 158, 11, 0.12);
}

/* Per-item read-state slot — a form, so it sits beside the link rather than
   inside it. ONE slot on EVERY row, carrying mark-read or mark-unread off the
   row's own state; it is never absent, so the link's width does not change
   between a read row and an unread one and the list reads as a column.

   Tertiary, not link-coloured: the slot is now on every row rather than on the
   unread ones, and presence at that density is paid for with weight, never with
   hover-concealment (unreachable on touch). --src-text-tertiary clears the
   WCAG-AA floor on every light surface, so quiet here is not faint. */

.notif-item-toggle {
    display: flex;
    align-items: flex-start;
    padding: var(--src-space-3) var(--src-space-4) var(--src-space-3) 0;
}

.notif-item-toggle-btn {
    background: none;
    border: none;
    padding: 0;
    font-size: var(--src-text-xs);
    font-weight: var(--src-weight-medium);
    color: var(--src-text-tertiary);
    cursor: pointer;
    white-space: nowrap;
    transition: color var(--src-transition-fast);
}

.notif-item-toggle-btn:hover {
    color: var(--src-text-link-hover);
}

/* Unread dot */

.notif-unread-dot {
    flex-shrink: 0;
    margin-top: 5px;
    width: 7px;
    height: 7px;
    border-radius: var(--src-radius-round);
    background-color: var(--src-warning);
}

.notif-unread-dot--read {
    background-color: transparent;
}

/* Item body */

.notif-item-body {
    flex: 1;
    min-width: 0;
}

.notif-item-title {
    margin: 0 0 2px 0;
    font-size: var(--src-text-base);
    font-weight: var(--src-weight-medium);
    color: var(--src-text-primary);
    line-height: var(--src-leading-tight);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.notif-item--unread .notif-item-title {
    font-weight: var(--src-weight-semibold);
}

.notif-item-preview {
    margin: 0 0 4px 0;
    font-size: var(--src-text-sm);
    color: var(--src-text-secondary);
    line-height: var(--src-leading-normal);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.notif-item-time {
    display: block;
    font-size: var(--src-text-xs);
    color: var(--src-text-tertiary);
}

.topbar-user {
    display: flex;
    align-items: center;
    gap: var(--src-space-2);
    padding: var(--src-space-1) var(--src-space-2);
    border-radius: var(--src-radius-md);
    cursor: pointer;
    transition: background-color var(--src-transition-fast);
}

.topbar-user:hover {
    background-color: rgba(2, 36, 60, 0.04);
}

.topbar-user-name {
    font-size: var(--src-text-base);
    font-weight: var(--src-weight-medium);
    color: var(--src-text-primary);
}


/* ===================
   TABS
   =================== */

.tabs {
    display: flex;
    align-items: center;
    gap: var(--src-space-1);
    border-bottom: 1px solid var(--src-divider);
}

.tab-item {
    display: flex;
    align-items: center;
    gap: var(--src-space-2);
    padding: var(--src-space-3) var(--src-space-4);
    font-size: var(--src-text-base);
    font-weight: var(--src-weight-medium);
    color: var(--src-text-secondary);
    text-decoration: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    transition: all var(--src-transition-fast);
    white-space: nowrap;
    cursor: pointer;
}

.tab-item:hover {
    color: var(--src-text-primary);
    background-color: var(--src-surface-card-hover);
}

.tab-item.active {
    color: var(--src-sapphire);
    border-bottom-color: var(--src-sapphire);
}


/* ===================
   BREADCRUMB
   =================== */

.breadcrumb {
    display: flex;
    align-items: center;
    gap: var(--src-space-2);
    font-size: var(--src-text-sm);
    color: var(--src-text-secondary);
}

.breadcrumb-item a {
    color: var(--src-text-secondary);
    text-decoration: none;
}

.breadcrumb-item a:hover {
    color: var(--src-text-link);
}

.breadcrumb-item.active {
    color: var(--src-text-primary);
    font-weight: var(--src-weight-medium);
}

.breadcrumb-separator::after {
    content: '\203A';
    color: var(--src-text-tertiary);
    margin: 0 var(--src-space-1);
}


/* ===================
   TOAST NOTIFICATIONS
   =================== */

.toast-container {
    position: fixed;
    top: var(--src-space-6);
    right: var(--src-space-6);
    z-index: var(--src-z-toast);
    display: flex;
    flex-direction: column;
    gap: var(--src-space-3);
    pointer-events: none;
}

.toast {
    display: flex;
    align-items: flex-start;
    gap: var(--src-space-3);
    padding: var(--src-space-4);
    background-color: var(--src-surface-card);
    border-radius: var(--src-radius-lg);
    box-shadow: var(--src-shadow-toast);
    border-left: 4px solid var(--src-border);
    min-width: 320px;
    max-width: 420px;
    pointer-events: auto;
    transform: translateX(120%);
    transition: transform var(--src-transition-panel);
}

.toast.active {
    transform: translateX(0);
}

.toast-success { border-left-color: var(--src-success); }
.toast-error { border-left-color: var(--src-error); }
.toast-warning { border-left-color: var(--src-warning); }
.toast-info { border-left-color: var(--src-info); }

.toast-icon {
    flex-shrink: 0;
    width: 20px;
    height: 20px;
}

/* The ICONS take the readable member: on the toast's neutral surface the raw
   hues read 1.93:1 (success) and 2.15:1 (warning), under even the 3:1 graphical
   floor. error (8.14:1) and info (6.74:1) pass and are left on the hue.
   The border-left stripes above are NOT part of this — a border is neither text
   nor glyph, and --src-*-border is the low-alpha box outline, not an accent
   stripe. [STATUS-HUE-ON-ITS-OWN-TINT] */
.toast-success .toast-icon { color: var(--src-success-text); }
.toast-error .toast-icon { color: var(--src-error); }
.toast-warning .toast-icon { color: var(--src-warning-text); }
.toast-info .toast-icon { color: var(--src-info); }

.toast-content {
    flex: 1;
}

.toast-title {
    font-weight: var(--src-weight-semibold);
    font-size: var(--src-text-base);
    margin-bottom: var(--src-space-1);
}

.toast-message {
    font-size: var(--src-text-sm);
    color: var(--src-text-secondary);
}

.toast-dismiss {
    flex-shrink: 0;
    color: var(--src-text-tertiary);
    padding: var(--src-space-1);
    border-radius: var(--src-radius-sm);
}

.toast-dismiss:hover {
    color: var(--src-text-primary);
    background-color: rgba(2, 36, 60, 0.06);
}


/* ===================
   PRG TOAST  (.lg-toast)
   ===================
   The SERVER-RENDERED confirmation: a page that redirects after a write prints
   this from the notice/error query param, and CSS alone fades it out. No JS at
   all — distinct from the .toast family above, which exists only as markup
   src-toast.js builds at runtime and has no template emitter.

   Re-homed here 2026-08-12 (uiux vault → U2-SHARED-VOCAB) from a byte-identical
   pair of copies in modules/leadgen.css and modules/reservations.css, including
   both @keyframes — a duplicated keyframe NAME is resolved globally by whichever
   sheet the page loaded last, so the pair was one edit away from two module
   pages animating differently with no diff to show for it. A third emitter,
   templates/whatsapp_connect/hub.html.twig, declared nothing of its own and was
   rendering through whichever copy happened to be in reach.

   The `lg-` prefix is leadgen's and survives the move deliberately: re-homing is
   a provable no-op, re-homing plus rename is a migration wearing a move's commit
   message.

   The two names beside each other were left as the open question. It is ANSWERED
   (uiux vault, U2 TOAST-STACK-IS-SHARED, 2026-08-13) and the answer is that they
   are two situations rather than one, so do NOT collapse them: the test is the
   TRIGGER, not the appearance. A message that exists at render time is the PRG
   toast below, printed by templates/_toast_stack.html.twig with zero JS. A
   message that arrives with NO navigation — chat over Mercure, a fetch result —
   is .toast, built by SRC.toast (public/js/src-toast.js). Neither can do the
   other's job: there is no response for the push case to render into, and
   putting the PRG case behind a script hides a confirmation the server already
   has (U10). The markup for the PRG half is owned by that partial and by
   nothing else — a page that hand-rolls .lg-toast-wrap has left the primitive. */
.lg-toast-wrap {
    position: fixed;
    top: 72px;
    right: 22px;
    z-index: 4000;
    display: flex;
    flex-direction: column;
    gap: 8px;
    pointer-events: none;
}
.lg-toast {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 10px 15px;
    border-radius: 10px;
    font-size: 0.78rem;
    font-weight: 600;
    color: #fff;
    box-shadow: 0 10px 30px rgba(2,36,60,0.20);
    opacity: 0;
    animation: lgToastIn 0.28s cubic-bezier(0.16,1,0.3,1) forwards,
               lgToastOut 0.5s ease 3.6s forwards;
}
.lg-toast-success { background: #02243c; }
.lg-toast-error   { background: var(--src-ruby); }
.lg-toast .bi { font-size: 0.95rem; }
@keyframes lgToastIn  { from { opacity: 0; transform: translateX(24px); } to { opacity: 1; transform: none; } }
@keyframes lgToastOut { to  { opacity: 0; transform: translateX(24px); visibility: hidden; } }



/* ===================
   BANNER ALERTS
   =================== */

.banner {
    display: flex;
    align-items: center;
    gap: var(--src-space-3);
    padding: var(--src-space-3) var(--src-space-5);
    font-size: var(--src-text-base);
    border-bottom: 1px solid transparent;
}

/* NOTE: .banner-* has ZERO emitters tree-wide (no template, no JS builds one).
   The success/warning bodies measure 1.81:1 / 2.02:1 and are deliberately left
   on the raw hue: repointing an unrendered rule corrects no contrast and removes
   the only signal that it is dead. Disposition is a [U9-UNSTYLED] question.
   [STATUS-HUE-ON-ITS-OWN-TINT] zero-emitter clause. */
.banner-success {
    background-color: var(--src-success-bg);
    border-bottom-color: var(--src-success);
    color: var(--src-success);
}

.banner-error {
    background-color: var(--src-error-bg);
    border-bottom-color: var(--src-error);
    color: var(--src-error);
}

.banner-warning {
    background-color: var(--src-warning-bg);
    border-bottom-color: var(--src-warning);
    color: var(--src-warning);
}

.banner-info {
    background-color: var(--src-info-bg);
    border-bottom-color: var(--src-info);
    color: var(--src-info);
}

.banner-content {
    flex: 1;
}

.banner-dismiss {
    flex-shrink: 0;
    padding: var(--src-space-1);
    border-radius: var(--src-radius-sm);
    color: inherit;
    opacity: 0.7;
}

.banner-dismiss:hover {
    opacity: 1;
}


/* ===================
   INLINE ALERTS
   =================== */

.alert {
    /* The icon row is declared HERE because it is the house pattern, not a
       leadgen/reservations habit: 18 of the 43 alert emitters put a `bi-*` icon
       before the message, across hr-roster, dept-roster, reservations, leadgen,
       admin, whatsapp-connect, approvals, travel and leave. Only two modules
       could render it as a row, each through a page-scoped copy of this whole
       body; everywhere else the icon sat inline on an ordinary word-space.
       [U2-SHARED-VOCAB]

       flex-start, not the copies' `center`: those were authored for one-line
       flash banners, and the population promoting them is mostly multi-line —
       a centred icon beside a four-line notice (leave/_detail) or a `<ul>`
       (hr-roster/leave_policy_detail) floats in the middle of the block. On a
       single line the two are within a pixel of each other.

       For an alert whose content is PURE TEXT this is a no-op: the whole run
       becomes one anonymous flex item and lays out exactly as the block did.

       That sentence read "for an alert with no icon" until 2026-08-12, and the
       missing qualifier hid a third population (Quality QUA-20260812-006). A
       flex container blockifies every in-flow ELEMENT child — `<strong>`, `<a>`,
       `<span>` included — and wraps each contiguous text run in its own
       anonymous item, so `text <strong>and</strong> text` is THREE flex items in
       a row with `gap` between them: a sentence broken into columns. Icon-less
       is not the test; childless is. See .alert-prose below. */
    display: flex;
    align-items: flex-start;
    gap: var(--src-space-3);
    padding: var(--src-space-3) var(--src-space-4);
    border-radius: var(--src-radius-md);
    font-size: var(--src-text-sm);
    border: 1px solid transparent;
    margin-bottom: var(--src-space-5);
}

/* The cross-axis member of the base above. The base's axis was promoted for the
   icon row, counted against the 18-of-43 emitters that render one — but a base
   that declares an axis lays out EVERY emitter's children, including the ones
   that render a lead plus a list and were relying on the block behaviour the
   base used to have. Those become flex items side by side, silently.

   `align-items: stretch` is not decoration: the base carries `flex-start`,
   which on the cross axis shrink-wraps each block child to its content width,
   so `flex-direction` alone does not restore what a block child had. Stretch is
   that prior state, stated.

   Spacing comes from the base's own `gap` — an emitter's child that carried a
   `margin-top` to separate itself from a sibling has that margin zeroed at the
   same time, or the two double-count.
   uiux vault → FLEX-AXIS-CONTRACT. */
.alert-stack {
    flex-direction: column;
    align-items: stretch;
}

/* The NO-axis member of the base, and the reason it is a third rule rather than
   a use of .alert-stack: this population has no arrangement to declare. Its
   content is one prose run — a sentence that happens to carry an inline
   `<strong>` or `<a>` — and the axis is wrong for it in BOTH directions. On the
   base's `row` the sentence is laid out as side-by-side columns with `gap`
   between the fragments; under .alert-stack it would stack those same fragments
   onto separate lines. Neither is a sentence.

   `display: block` is not a re-declaration of the whole body, it is the prior
   state stated — exactly as .alert-stack's `align-items: stretch` is. Inline
   flow resumes, `gap` and `align-items` go inert, and the padding/colour/border
   the base owns are untouched.

   An emitter takes .alert-stack or .alert-prose, never both: stack is for
   children that stacked, prose is for content that was never children.
   uiux vault → FLEX-AXIS-CONTRACT. */
.alert-prose {
    display: block;
}

/* Two independent decisions meet in these four rules, and they must be read
   separately or the family re-splits:

   COLOUR — success and warning take the family's READABLE member, not the
   signalling hue: the hue on its own tint measures 1.81:1 and 2.02:1, both well
   under AA. error and info are NOT changed — 7.01:1 and 5.99:1 on their own
   tints, so they owe no -text member. That asymmetry is measured.

   BORDER — all four take their own family's -border member, at α 0.22. This is
   the base's `border: 1px solid transparent` box outline, a container edge, and
   it is owed by the component's grammar rather than by any contrast floor (a
   border is neither text nor glyph, so no floor applies to it). Until
   2026-08-12 error and info bordered in the raw hue while success and warning
   bordered soft — not a severity grammar (info is the quietest variant and it
   was on the loud side) but the footprint of the 2026-08-11 contrast pass,
   which minted -text and -border as a pair and repointed both declarations for
   the two families it touched. Do not re-derive the border from the colour
   split above; they answer different questions.
   uiux vault → NOTICE-BOX-OUTLINE, and STATUS-HUE-ON-ITS-OWN-TINT's border
   clause for why an accent stripe (.toast-*, .banner-*) stays on the raw hue. */
.alert-error {
    background-color: var(--src-error-bg);
    border-color: var(--src-error-border);
    color: var(--src-error);
}

.alert-success {
    background-color: var(--src-success-bg);
    border-color: var(--src-success-border);
    color: var(--src-success-text);
}

.alert-warning {
    background-color: var(--src-warning-bg);
    border-color: var(--src-warning-border);
    color: var(--src-warning-text);
}

.alert-info {
    background-color: var(--src-info-bg);
    border-color: var(--src-info-border);
    color: var(--src-info);
}


/* ===================
   CHIPS (Filter Chips)
   =================== */

.chip {
    display: inline-flex;
    align-items: center;
    gap: var(--src-space-1);
    padding: var(--src-space-1) var(--src-space-3);
    font-size: var(--src-text-sm);
    font-weight: var(--src-weight-medium);
    background-color: var(--src-surface-card);
    border: 1px solid var(--src-border);
    border-radius: 20px;
    color: var(--src-text-primary);
    cursor: default;
}

.chip-active {
    background-color: var(--src-sapphire-light);
    border-color: var(--src-sapphire);
    color: var(--src-sapphire);
}

.chip-remove {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    border-radius: var(--src-radius-round);
    font-size: 10px;
    color: var(--src-text-tertiary);
    cursor: pointer;
    transition: all var(--src-transition-fast);
}

.chip-remove:hover {
    background-color: var(--src-error-bg);
    color: var(--src-error);
}

/* A filter chip that IS the <select>. The chip above is a static token (it
   states a selection); this one is the CONTROL that sets it — same pill
   grammar, so a toolbar of filters reads as one family whether a value is
   set or not.
   The caret is not a label: it says "this opens", which the word cannot
   (icon-or-word ruling, 2026-08-04). Native select = no JS beyond the
   existing onchange submit, and the no-JS Apply path stays intact. */
.chip-select {
    appearance: none;
    display: inline-flex;
    align-items: center;
    width: auto;
    max-width: 100%;
    text-overflow: ellipsis;
    min-height: 0;
    padding: 8px 32px 8px 15px;
    font-size: var(--src-text-sm);
    font-weight: var(--src-weight-medium);
    color: var(--src-text-secondary);
    background-color: var(--src-surface-solid);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%2302243c' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round' opacity='0.55' d='M2 5l6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 13px center;
    border: 1px solid var(--src-border-medium);
    border-radius: var(--src-radius-pill);
    cursor: pointer;
    white-space: nowrap;
    transition: border-color var(--src-transition-state),
                color var(--src-transition-state);
}

.chip-select:hover {
    border-color: var(--src-border-strong);
    color: var(--src-text-heading);
}

.chip-select:focus-visible {
    outline: 2px solid var(--src-primary);
    outline-offset: 2px;
}

/* A filter that is ON states it by inversion — the toolbar can be read at a
   glance for "what is narrowing this list", with no second badge to pair up. */
.chip-select--set {
    background-color: var(--src-primary);
    border-color: var(--src-primary);
    color: var(--src-surface-solid);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23ffffff' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round' opacity='0.7' d='M2 5l6 6 6-6'/%3E%3C/svg%3E");
}

.chip-select--set:hover {
    border-color: var(--src-primary);
    color: var(--src-surface-solid);
}


/* ===================
   PERSON ROW
   The one way this app lists PEOPLE. A person row is a CARD, not a table
   row: air between rows is what makes a dense list read modern, and it is
   already the house rhythm ("the air IS the design") simply never applied
   to a list. A table would win a few pixels and lose card-or-page.

   The primitive owns the shell — card, status edge, avatar, identity block,
   labelled cell, hover actions. The COLUMN TEMPLATE is composition: each
   surface declares its own grid-template-columns on .person-row, because
   which facts a roster shows is that roster's business.
   =================== */

.roster-list {
    display: flex;
    flex-direction: column;
    gap: var(--src-space-2);
}

.person-row {
    position: relative;
    display: grid;
    align-items: center;
    gap: var(--src-space-4);
    background: var(--src-surface-card);
    border: 1px solid var(--src-border);
    border-radius: var(--src-radius-secondary);
    box-shadow: var(--src-shadow-1);
    /* left padding clears the status edge (3px) and then gives it air */
    padding: var(--src-space-3) var(--src-space-4) var(--src-space-3) 19px;
    overflow: hidden;
    text-decoration: none;
    color: inherit;
    transition: box-shadow var(--src-transition-transform),
                transform var(--src-transition-transform),
                border-color var(--src-transition-state);
}

.person-row:hover {
    box-shadow: var(--src-shadow-2);
    border-color: var(--src-border-strong);
    transform: translateY(-1px);
    text-decoration: none;
    color: inherit;
}

.person-row:focus-visible {
    outline: 2px solid var(--src-primary);
    outline-offset: 2px;
}

/* The status edge carries colour from the module's own status vocabulary
   (e.g. .stripe-{code}); this element only owns the geometry. Positioned, not
   a grid column: a status band that stops short of the card's own corners
   reads as a stray tick, and this way no roster has to spend a column on it. */
.person-row-edge {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
}

.person-row-avatar {
    width: 44px;
    height: 44px;
    border-radius: var(--src-radius-circle);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    flex-shrink: 0;
    font-size: var(--src-text-base);
    font-weight: var(--src-weight-semibold);
    color: var(--src-surface-solid);
    background: linear-gradient(135deg, var(--src-primary) 0%, var(--src-sandstone) 100%);
}

/* Carries .avatar-stack in its template (staff/directory.html.twig) — the
   img positioning/sizing (initials-underneath, image-on-top) comes from the
   shared .avatar-stack img rule above; no second copy of that grammar here. */

/* Identity block — the one column that must never be dropped. min-width:0 so
   a long name ellipsises instead of blowing the grid open. */
.person-identity {
    min-width: 0;
}

.person-name {
    display: block;
    font-size: var(--src-text-base);
    font-weight: var(--src-weight-semibold);
    color: var(--src-text-heading);
    line-height: var(--src-leading-tight);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.person-sub {
    display: block;
    font-size: var(--src-text-xs);
    color: var(--src-text-secondary);
    margin-top: 2px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A labelled fact. The label is what lets a row lose its table header
   without losing what the value means. */
.person-cell {
    font-size: var(--src-text-sm);
    color: var(--src-text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.person-cell-label {
    display: block;
    font-size: 0.625rem;
    font-weight: var(--src-weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--src-text-tertiary);
    margin-bottom: 2px;
}

@media (prefers-reduced-motion: reduce) {
    .person-row:hover { transform: none; }
}


/* ===================
   AVATAR
   =================== */

.avatar {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--src-radius-round);
    background-color: var(--src-sapphire);
    color: white;
    font-weight: var(--src-weight-semibold);
    flex-shrink: 0;
    overflow: hidden;
}

/* Avatar stacking — initials render underneath (normal flow, centered by
   the flex above); an <img>, when present, layers on top via absolute
   positioning, so a missing/broken/slow/404ing image reveals the initials
   instead of an empty circle (templates/_avatar.html.twig). .avatar-stack
   extends the identical mechanic to a bespoke skin that isn't .avatar
   itself — its current consumers are chat's .msg-avatar / .chat-header-avatar
   (modules/chat.css), staff's .org-avatar (modules/staff.css) and
   .person-row-avatar (staff/directory.html.twig), all three already
   flex-centered, sized and overflow:hidden, so this class only adds the
   positioning context. staff_profile/view.html.twig's .sp-photo-wrap is the
   one exception — it carries no shape of its own (that lives on the child
   .sp-photo/.sp-photo-initial pair, each self-clipping via its own
   border-radius), so .avatar-stack there contributes ONLY the
   position:relative the absolutely-positioned <img> needs. */
.avatar-stack { position: relative; }

.avatar img,
.avatar-stack img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.avatar-sm { width: 28px; height: 28px; font-size: var(--src-text-xs); }
.avatar-md { width: 36px; height: 36px; font-size: var(--src-text-sm); }
.avatar-lg { width: 48px; height: 48px; font-size: var(--src-text-md); }


/* ===================
   DROPDOWN
   =================== */

.dropdown {
    position: relative;
    display: inline-flex;
}

.dropdown-menu {
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: var(--src-space-1);
    min-width: 200px;
    background-color: var(--src-surface-card);
    border: 1px solid var(--src-border);
    border-radius: var(--src-radius-lg);
    box-shadow: var(--src-shadow-dropdown);
    z-index: var(--src-z-dropdown);
    padding: var(--src-space-1) 0;
    opacity: 0;
    visibility: hidden;
    transform: translateY(-4px);
    transition: all var(--src-transition-fast);
}

.dropdown-menu.active {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

.dropdown-item {
    display: flex;
    align-items: center;
    gap: var(--src-space-3);
    padding: var(--src-space-2) var(--src-space-4);
    font-size: var(--src-text-base);
    color: var(--src-text-primary);
    text-decoration: none;
    transition: background-color var(--src-transition-fast);
    cursor: pointer;
}

.dropdown-item:hover {
    background-color: rgba(2, 36, 60, 0.04);
}

/* A dropdown item is a <button> wherever it acts rather than navigates (dropdown.js's
   documented form). base.css already drops its border/background; these two are the
   remainder of the UA button box that a flex row still shows — a stretched hit area
   and a label that reads left when it wraps. */
button.dropdown-item {
    width: 100%;
    text-align: left;
}

.dropdown-item-danger {
    color: var(--src-error);
}

.dropdown-divider {
    height: 1px;
    background-color: var(--src-divider);
    margin: var(--src-space-1) 0;
}


/* ===================
   PAGINATION
   =================== */

/* The ONE pager vocabulary. Every paged surface emits
   <nav class="pagination" aria-label="Pagination"> with .pagination-item
   controls and one .pagination-info. Module stylesheets declare NOTHING here —
   a module-prefixed pagination stem is a fork, not a skin, and it silently opts
   that surface out of every global rule keyed to .pagination (the print-hide in
   layouts.css being the one that already bit). Vault: PAGINATION.

   .pagination-item is load-bearing BEHAVIOUR as well as style — staff/pages/
   index.php dispatches its click listener on classList.contains('pagination-item').
   Grep the JS before renaming anything here. */

.pagination {
    display: flex;
    align-items: center;
    gap: var(--src-space-1);
    flex-wrap: wrap;
}

.pagination-item {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 36px;
    height: 36px;
    padding: 0 var(--src-space-2);
    font-size: var(--src-text-base);
    color: var(--src-text-secondary);
    border-radius: var(--src-radius-md);
    transition: all var(--src-transition-fast);
    cursor: pointer;
    text-decoration: none;
}

.pagination-item:hover {
    background-color: var(--src-surface-card-hover);
    color: var(--src-text-primary);
}

/* The current page is not a destination and must not advertise itself as one —
   .pagination-item sets cursor:pointer for the navigable chips, which is wrong
   the moment the chip is the page you are already on. (.disabled covers the
   other non-destination case, the dead Prev/Next ends.) */
.pagination-item.active {
    background-color: var(--src-sapphire);
    color: white;
    cursor: default;
}

.pagination-item.disabled {
    opacity: 0.4;
    cursor: not-allowed;
    pointer-events: none;
}

.pagination-info {
    font-size: var(--src-text-sm);
    color: var(--src-text-secondary);
    margin-left: var(--src-space-4);
    font-variant-numeric: tabular-nums;
}

/* ── Bar treatment ──────────────────────────────────────────────────────
   Opt-in modifier for the STRUCTURAL case where the pager is the footer of a
   table or panel rather than an inline row under one: it gets a surface, a
   border, and space-between (info left, controls right). This is a widening of
   the primitive, not a fork — a footer attached to a card is a different
   structure from a row floating beneath it. Everything cosmetic still converges
   on the bare default above.

   Bare surfaces are untouched by all of this: they never carry .pagination-bar. */

.pagination-bar {
    justify-content: space-between;
    gap: var(--src-space-3);
    padding: var(--src-space-3) var(--src-space-4);
    background: var(--src-surface-raised);
    border: 1px solid var(--src-border-subtle);
    border-radius: var(--src-radius-interactive);
    margin-top: var(--src-space-2);
}

/* In bar mode the info leads instead of trailing, so the bare row's left
   margin (which exists to push it away from the controls) must not apply. */
.pagination-bar .pagination-info {
    margin-left: 0;
    white-space: nowrap;
}

/* The control group, so space-between has exactly two children to separate. */
.pagination-links {
    display: flex;
    align-items: center;
    gap: var(--src-space-1);
    flex-wrap: wrap;
}

/* ── Flush treatment ────────────────────────────────────────────────────
   The other structural footer case: the pager is pinned INSIDE a panel that
   already owns the surface and the rounding (`overflow:hidden` + a radius on
   the panel, a `flex:1` scroll region above it). It contributes a separator and
   nothing else — the bar's background would be a card inside a card, and its
   radius/margin-top would open a dead gap against an edge the panel clips.

   Sibling of `.pagination-bar`, not an override of it: a modifier that had to
   undo half of bar's declarations would be evidence they are different
   structures, not a variant. Shares bar's arrangement (info left, controls
   right) — only surface ownership differs. Vault: PAGINATION (flush). */

.pagination-flush {
    justify-content: space-between;
    gap: var(--src-space-3);
    padding: var(--src-space-3) var(--src-space-4);
    border-top: 1px solid var(--src-border-subtle);
    flex-shrink: 0;
}

/* Same reason as the bar: in footer mode the info leads, so the bare row's
   left margin (which exists to push it off the controls) must not apply. */
.pagination-flush .pagination-info {
    margin-left: 0;
    white-space: nowrap;
}

@media (max-width: 768px) {
    .pagination-bar {
        flex-direction: column;
        align-items: flex-start;
    }
}


/* ===================
   SEARCH INPUT
   =================== */

.search-input-wrapper {
    position: relative;
    display: flex;
    align-items: center;
}

.search-input-wrapper .search-icon {
    position: absolute;
    left: var(--src-space-3);
    color: var(--src-text-tertiary);
    pointer-events: none;
    width: 16px;
    height: 16px;
}

.search-input {
    width: 100%;
    padding: var(--src-space-2) var(--src-space-3) var(--src-space-2) var(--src-space-8);
    font-size: var(--src-text-base);
    background-color: var(--src-surface-input);
    border: 1px solid var(--src-border-input);
    border-radius: var(--src-radius-md);
    transition: border-color var(--src-transition-fast), box-shadow var(--src-transition-fast);
    min-height: 40px;
    color: var(--src-text-primary);
}

.search-input:focus {
    border-color: var(--src-primary);
    box-shadow: 0 0 0 3px rgba(2, 36, 60, 0.1);
    background: var(--src-surface-solid);
    outline: none;
}

.search-input::placeholder {
    color: var(--src-text-tertiary);
}


/* ===================
   STAT CARD
   =================== */

.stat-card {
    background-color: var(--src-surface-card);
    border: 1px solid var(--src-border);
    border-radius: var(--src-radius-lg);
    padding: var(--src-space-5);
    box-shadow: var(--src-shadow-1);
}

.stat-card-label {
    font-size: var(--src-text-sm);
    font-weight: var(--src-weight-medium);
    color: var(--src-text-secondary);
    text-transform: uppercase;
    letter-spacing: var(--src-tracking-wide);
    margin-bottom: var(--src-space-2);
}

.stat-card-value {
    font-family: var(--src-font-display);
    font-size: var(--src-text-3xl);
    font-weight: var(--src-weight-regular);
    color: var(--src-text-primary);
    line-height: 1.1;
}

.stat-card-delta {
    display: inline-flex;
    align-items: center;
    gap: var(--src-space-1);
    margin-top: var(--src-space-2);
    font-size: var(--src-text-sm);
    font-weight: var(--src-weight-medium);
}

/* Zero emitters tree-wide — see the .banner-* note above; [U9-UNSTYLED], not a
   live contrast defect. */
.stat-card-delta.positive { color: var(--src-success); }
.stat-card-delta.negative { color: var(--src-error); }


/* ===================
   STAT PILLS
   =================== */

.stat-bar {
    display: flex;
    align-items: center;
    gap: var(--src-space-2);
    margin-bottom: var(--src-space-4);
    flex-wrap: wrap;
}

.stat-bar-divider {
    width: 1px;
    height: 28px;
    background-color: var(--src-border);
    flex-shrink: 0;
}

.stat-pill {
    display: inline-flex;
    align-items: center;
    gap: var(--src-space-2);
    padding: var(--src-space-2) var(--src-space-4);
    background: var(--src-surface-solid);
    border: 1px solid var(--src-border);
    border-radius: var(--src-radius-pill);
    text-decoration: none;
    transition: all var(--src-transition-fast);
    white-space: nowrap;
    cursor: pointer;
}

.stat-pill:hover {
    border-color: var(--src-border-strong);
    background: var(--src-surface-card-hover);
}

.stat-pill.active {
    background: var(--src-primary);
    border-color: var(--src-primary);
}

.stat-pill.active .stat-pill-value,
.stat-pill.active .stat-pill-label {
    color: var(--src-text-on-dark);
}

.stat-pill.active .stat-pill-dot {
    background: var(--src-text-on-dark);
}

.stat-pill-dot {
    width: 8px;
    height: 8px;
    border-radius: var(--src-radius-round);
    flex-shrink: 0;
}

.stat-pill-value {
    font-size: var(--src-text-lg);
    font-weight: var(--src-weight-bold);
    color: var(--src-text-heading);
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

.stat-pill-label {
    font-size: var(--src-text-xs);
    font-weight: var(--src-weight-semibold);
    color: var(--src-text-secondary);
    text-transform: uppercase;
    letter-spacing: var(--src-tracking-wide);
    line-height: 1;
}

.stat-dot-positive  { background: var(--src-success); }
.stat-dot-attention { background: var(--src-warning); }
.stat-dot-caution   { background: var(--src-sandstone); }
.stat-dot-inform    { background: var(--src-sapphire); }
.stat-dot-negative  { background: var(--src-error); }
.stat-dot-neutral   { background: var(--src-text-tertiary); }


/* ===================
   EMPTY STATE
   =================== */

.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: var(--src-space-16) var(--src-space-8);
    text-align: center;
}

/* The box AND the glyph inside it. font-size is declared here, on the class —
   never as a descendant selector (.empty-state-icon i), which would out-specify
   every module's same-class variant and break it. Modules that render a
   different treatment (travel's 40px circle chip) re-declare this class at equal
   specificity and still win by load order, exactly as before. 48px is what the
   three independent implementations of the DESIGN.md empty-state component all
   chose (leadgen, reservations, admin-email — all 3rem; admin-email.css deleted 2026-08-15); consumers must not
   inline their own. */
.empty-state-icon {
    width: 64px;
    height: 64px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 48px;
    color: var(--src-text-tertiary);
    margin-bottom: var(--src-space-5);
}

.empty-state-title {
    font-size: var(--src-text-lg);
    font-weight: var(--src-weight-semibold);
    color: var(--src-text-primary);
    margin-bottom: var(--src-space-2);
}

.empty-state-message {
    font-size: var(--src-text-base);
    color: var(--src-text-secondary);
    max-width: 360px;
    margin-bottom: var(--src-space-6);
}

/* ── The two COMPACT empty renders ──────────────────────────────────────────
   .empty-state above is the page-level render: a 64px glyph and generous
   padding, correct in place of a page's main collection. It is wrong in a
   200px rail or a drawer section, which is why two module-private families
   grew instead of consuming it. They are NOT one situation and must not be
   collapsed into each other — the difference is provable from their consumers,
   not a preference:

     .empty-state-inline       a short list is empty inside a compact slot —
                               a filter rail, a drawer section, a small admin
                               sub-panel. One muted line, no box, no action,
                               because there is nothing for the reader to do
                               here. "No members yet."

     .empty-state-placeholder  a CONTENT PANE is empty and the reader fills it
                               from that pane. A dashed box that occupies the
                               pane and states the pane's own primary verb:
                               "Add the first slide", "Tick people on the left
                               and assign them."

   Bodies are verbatim from the module sheets they came from (whatsapp-connect
   .wac-empty-inline and learning .lrn-empty-drop) — this is a re-home, not a
   re-style, so nothing rendered changes. Which of the two a surface wants is
   decided by whether the reader can act in that container. */

.empty-state-inline {
    font-size: var(--src-text-sm);
    color: var(--src-text-tertiary);
    margin: 0;
    padding: var(--src-space-3) 0;
}

.empty-state-placeholder {
    border: 1px dashed var(--src-border-strong);
    border-radius: 8px;
    padding: 22px;
    text-align: center;
    font-size: 13px;
    color: var(--src-text-tertiary);
}

/* The conversation canvas of a client shell with no messages in it. Fills the
   pane (margin:auto inside a flex parent) rather than sitting at its top, which
   is what separates it from --placeholder: there is no action to offer here, the
   thread is simply empty.

   Converged from three byte-identical private declarations — leadgen and
   reservations both spelled it .lcc-thread-empty (the SAME selector in two
   sheets) and whatsapp-connect spelled it .wac-thread-empty. All four consumers
   emit the same shape: div > i + p. The `p` rule is the part that was NOT
   unanimous: two of the three carried it and the third did not, so one module's
   caption rendered at inherited size with default paragraph margins inside a
   gapped flex column. Converging on the COMPLETE body repairs that; converging
   on the intersection would have preserved it. */

.empty-state-canvas {
    margin: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    padding: 40px;
    color: var(--src-text-tertiary);
    text-align: center;
}
.empty-state-canvas i { font-size: 32px; opacity: 0.25; }
.empty-state-canvas p { font-size: 0.75rem; color: var(--src-text-tertiary); margin: 0; }


/* ===================
   DISTRIBUTION BAR
   Horizontal proportional fill bar used by any report grain to show
   a segmented breakdown (aging buckets, quality split, tier mix, etc.).
   Segment widths are data-driven via inline flex: or width:% — a bar length
   IS the datum; that idiom is established and allowed.

   Usage:
     <div class="ds-distbar">
       <div class="ds-distbar-seg" style="flex:42;"></div>
       <div class="ds-distbar-seg" style="flex:18;"></div>
     </div>

   Color and opacity are set by the grain's own classes placed alongside
   .ds-distbar-seg (e.g. .aging-seg.d0, .wq-split-seg.clean).
   =================== */

.ds-distbar {
    display: flex;
    height: 8px;
    border-radius: var(--src-radius-pill);
    overflow: hidden;
    gap: 2px;
}

.ds-distbar-seg {
    height: 100%;
    border-radius: 2px;
    min-width: 2px;
}


/* ===================
   LOADING
   =================== */

.spinner {
    width: 24px;
    height: 24px;
    border: 3px solid var(--src-border);
    border-top-color: var(--src-sapphire);
    border-radius: var(--src-radius-round);
    animation: spin 0.6s linear infinite;
}

.spinner-sm { width: 16px; height: 16px; border-width: 2px; }
.spinner-lg { width: 40px; height: 40px; border-width: 4px; }

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

.loading-overlay {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--src-space-12);
}

/* A form that is sending (public/js/src-busy.js). The button keeps its
   place and its colour — faded to the .btn:disabled 50% it would read as
   "unavailable", not "working" — and its spinner takes the button's own
   text colour so it shows on a dark primary and a light outline alike. */
.src-busy .btn:disabled { opacity: 0.9; cursor: progress; }
.src-busy-label {
    display: inline-flex;
    align-items: center;
    gap: var(--src-space-2);
}
.src-busy-label .spinner {
    border-color: currentColor;
    border-top-color: transparent;
    opacity: 0.85;
}
.src-busy-line {
    display: flex;
    margin-top: var(--src-space-2);
    font-size: var(--src-text-sm);
    color: var(--src-text-secondary);
}
.src-busy input[type="file"] { opacity: 0.6; pointer-events: none; }



/* ===================
   QUOTED-HISTORY FOLD
   =================== */

/* The ONE quoted-history vocabulary. The markup is server-authored by a CORE
   service — \App\Core\Services\EmailQuoteSplitter::renderEmailBody() emits
   <details class="email-quote-fold"><summary class="email-quote-summary">
   … <div class="email-quote-body"> — so every module that renders an inbound
   email body through that service emits these three classes and none of them
   owns the look. Module stylesheets declare NOTHING here; a module-prefixed
   quote-fold stem would be a fork of a class the module does not author.
   Vault: U2-SHARED-VOCAB. Placement: root-covers-all.

   Lived in modules/reservations.css until 2026-07-23, when leadgen became the
   service's second caller and the single-caller home stopped being tenable.
   Bodies moved unchanged — the frame head partial links components.css BEFORE
   any module stylesheet (templates/_head_assets.html.twig, and that link order
   is the contract), so a module extension can still layer on top.

   No client JS: the fold is a native <details>. Do not add a toggle script. */

.email-quote-fold {
    margin-top: 12px; border-top: 1px solid var(--src-border-medium); padding-top: 8px;
}
.email-quote-summary {
    cursor: pointer; font-size: var(--src-text-xs); color: var(--src-text-tertiary);
    user-select: none; list-style-position: inside;
}
.email-quote-summary:hover { color: var(--src-text-secondary); }
.email-quote-body {
    margin-top: 10px; padding-left: 12px; border-left: 2px solid rgba(2,36,60,0.12);
    color: var(--src-text-tertiary);
}

/* ===================
   THREAD WINDOW CONTROL
   =================== */

/* The ONE thread-pager vocabulary. Two modules render a conversation thread one
   window at a time and both emit this markup from their own copy of the block —
   reservations/includes/_thread-pager.inc.php and
   leadgen/includes/_thread-pager.inc.php. The renderers are per-module because
   the module boundary forbids one requiring the other; the LOOK is not, so it is
   authored once here rather than forked into two module stylesheets that would
   drift. Same placement and the same reasoning as QUOTED-HISTORY FOLD above:
   root-covers-all, U2-SHARED-VOCAB.

   The classes carry no module prefix ON PURPOSE. A module-prefixed stem would
   assert the module authors the look, and neither does — a third surface that
   pages a thread inherits this by emitting the same block, with no stylesheet
   edit anywhere.

   No client JS: the control is two ordinary links carrying a cursor in the query
   string. Do not add a fetch-and-append script — an accumulating window is what
   the pagination exists to prevent (LeoMemory ns 24372). */

.thread-pager {
    display: flex; align-items: center; justify-content: center; gap: 10px;
    flex-wrap: wrap; padding: 10px 12px;
}
.thread-pager-btn {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 5px 12px; border-radius: 999px;
    border: 1px solid var(--src-border-medium);
    background: var(--src-surface-solid);
    font-size: var(--src-text-xs); color: var(--src-text-secondary);
    text-decoration: none; line-height: 1.4;
    transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.thread-pager-btn:hover {
    background: var(--src-sapphire-tint);
    border-color: var(--src-sapphire-light);
    color: var(--src-sapphire);
}
.thread-pager-btn:focus-visible {
    outline: 2px solid var(--src-border-focus); outline-offset: 2px;
}
.thread-pager-btn i { font-size: 0.85em; opacity: 0.75; }
.thread-pager-note {
    font-size: var(--src-text-xs); color: var(--src-text-tertiary);
    letter-spacing: 0.04em;
}

/* ===================
   PEOPLE PICKER
   The shared search-and-choose control: text input, listbox of server-rendered
   rows, hidden field carrying the chosen id. Behaviour: public/js/src-picker.js.
   Two hosts: overlay (default, never pushes page content) and --panel, which
   fills its container and is the one scroll region in it.
   =================== */
.src-picker { position: relative; }

.src-picker-input { width: 100%; }

.src-picker-results {
    position: absolute;
    left: 0;
    right: 0;
    top: 100%;
    z-index: 5;
    margin-top: 4px;
    background: var(--src-surface-solid);
    border: 1px solid var(--src-border);
    border-radius: var(--src-radius-medium);
    box-shadow: var(--src-shadow-dropdown);
    max-height: 320px;
    overflow-y: auto;
}

/* Nothing rendered = nothing shown. The primitive empties this element to
   close, so :empty is the closed state and needs no second class. */
.src-picker-results:empty { display: none; }

/* Panel host: the results region FILLS what its host gives it and is the one
   scroll region there — the host sizes itself, the list never sizes the host.
   Height comes down a flex chain, so every layer between needs
   flex:1/min-height:0 (see .org-dialog--picker in modules/staff.css). */
.src-picker--panel { flex: 1; min-height: 0; }
.src-picker--panel .src-picker-input { flex: 0 0 auto; }

.src-picker--panel .src-picker-results {
    position: static;
    flex: 1;
    min-height: 0;
    max-height: none;
    margin-top: var(--src-space-2);
    box-shadow: none;
}

/* A short line pinned top-left in a tall empty box reads as broken, so panel
   messages centre in the region instead. */
.src-picker--panel .src-picker-message {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    padding: var(--src-space-6);
    text-align: center;
}

/* The panel host keeps its box whether or not it holds rows — that is the
   scroll runway, and a box that disappears when the query is empty is the
   jumping the panel shape exists to prevent. */
.src-picker--panel .src-picker-results:empty { display: block; }

.src-picker-option {
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: 10px 12px;
    cursor: pointer;
    font-size: var(--src-text-sm);
    border-bottom: 1px solid var(--src-border-subtle);
}

.src-picker-option:last-child { border-bottom: 0; }

.src-picker-option:hover,
.src-picker-option.kb-active { background: var(--src-surface-card-hover); }

.src-picker-option-name {
    color: var(--src-text-heading);
    font-weight: var(--src-weight-medium);
}

.src-picker-option-meta {
    color: var(--src-text-tertiary);
    font-size: var(--src-text-xs);
}

/* One muted line for every non-result state — no matches, no permission, a
   degraded staff seam. The endpoint sends its own sentence; this only sets
   how it reads. */
.src-picker-message {
    margin: 0;
    padding: 10px 12px;
    font-size: var(--src-text-sm);
    color: var(--src-text-tertiary);
}

/* ===================
   FILE DROP
   The file-selection control: a drop / browse target, never the raw native
   input. Markup: templates/_file_drop.html.twig. The input is stretched
   invisibly over the target, so a click opens the chooser and a dropped file
   lands in it with no script; public/js/src-file-drop.js only adds the
   chosen-file list and the drag highlight. --compact is the narrow-rail form.
   =================== */
.file-drop-field { min-width: 0; }

.file-drop {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--src-space-1);
    padding: var(--src-space-6);
    background: var(--src-surface-solid);
    border: 1.5px dashed var(--src-border-strong);
    border-radius: var(--src-radius-medium);
    cursor: pointer;
    transition: border-color var(--src-transition-fast), background-color var(--src-transition-fast);
}

/* Specific enough to outrank `.src-busy input[type="file"]` (LOADING), which
   would otherwise fade the invisible input INTO view while a form sends. */
.file-drop input.file-drop-input[type="file"] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
    z-index: 1;
}

.file-drop:hover,
.file-drop.is-dragover { border-color: var(--src-sapphire); background: var(--src-sapphire-tint); }
.file-drop:has(.file-drop-input:focus-visible) { outline: 2px solid var(--src-border-focus); outline-offset: 2px; }

.file-drop-icon { font-size: 2rem; color: var(--src-sapphire); line-height: 1; margin-bottom: var(--src-space-1); }
.file-drop-title { font-size: var(--src-text-md); font-weight: var(--src-weight-semibold); color: var(--src-text-heading); }
.file-drop-sub { font-size: var(--src-text-sm); color: var(--src-text-tertiary); }

.file-drop--compact { padding: var(--src-space-3); gap: 2px; }
.file-drop--compact .file-drop-icon { font-size: 1.25rem; margin-bottom: 2px; }
.file-drop--compact .file-drop-title { font-size: var(--src-text-sm); }
.file-drop--compact .file-drop-sub { font-size: var(--src-text-xs); }

/* Sending: the whole target reads as held, not just the input. */
.src-busy .file-drop { opacity: 0.6; pointer-events: none; }

.file-drop-list {
    list-style: none;
    margin: var(--src-space-3) 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--src-space-2);
}
.file-drop-list[hidden] { display: none; }

.file-drop-chip {
    display: flex;
    align-items: center;
    gap: var(--src-space-3);
    min-width: 0;
    padding: var(--src-space-2) var(--src-space-3);
    background: var(--src-surface-solid);
    border: 1px solid var(--src-border-medium);
    border-radius: var(--src-radius-interactive);
}
.file-drop-chip > .bi { color: var(--src-text-tertiary); }
.file-drop-chip-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--src-text-sm); font-weight: var(--src-weight-semibold); color: var(--src-text-heading); }
.file-drop-chip-size { flex: 0 0 auto; font-size: var(--src-text-xs); color: var(--src-text-tertiary); }
