/* ─────────────────────────────────────────────────────────────────────────────
   ui-core.css — SPIDER shared design system

   Semantic UI tokens (plus legacy compatibility aliases), and the canonical
   ui- and u- prefixed component vocabulary: typography, page header, buttons,
   segmented controls, form fields, search, selects, file inputs,
   chips, pills, badges, cards, tables, empty and loading states, modals,
   sheets, overlays, toasts, session bar, restrained utilities, and global
   focus-visible and reduced-motion coverage.

   Load this file before index.css (app shell) or login.css (login page) —
   both link it so the design system exists in exactly one place. login.html
   links only ui-core.css and login.css, skipping the rest of index.css's
   view-specific rules (tables, loans, audit, settings, modals content, etc).
───────────────────────────────────────────────────────────────────────────── */

:root {
    /* === v58 SEMANTIC UI TOKENS ======================================= */
    /* Base surfaces */
    --ui-bg:              #060608;
    --ui-surface-1:       rgba(255,255,255,.05);
    --ui-surface-2:       rgba(255,255,255,.07);
    --ui-surface-3:       rgba(255,255,255,.10);
    --ui-surface-solid-1: #121214;
    --ui-surface-solid-2: #171719;
    --ui-surface-solid-3: #1F1F21; /* opaque twin of --ui-surface-3 (rgba(255,255,255,.10) over --ui-bg) */

    /* Borders and focus */
    --ui-border-subtle: rgba(255,255,255,.10);
    --ui-border-strong: rgba(255,255,255,.20);
    --ui-focus-ring:    rgba(255,255,255,.08);

    /* Text */
    --ui-text:         #ffffff;
    --ui-text-muted:   #a0a0a6;
    --ui-text-subtle:  #8b8b92;
    --ui-text-inverse: #000000;

    /* Actions and states */
    --ui-action:  #ffffff;
    --ui-success: #4ade80;
    --ui-warning: #fbbf24;
    --ui-danger:  #f87171;
    --ui-info:    #7c6af5;

    --ui-success-bg: rgba(74,222,128,.25);
    --ui-warning-bg: rgba(251,191,36,.25);
    --ui-danger-bg:  rgba(248,113,113,.25);
    --ui-info-bg:    rgba(124,106,245,.15);

    /* Shape */
    --ui-radius-xs:   6px;
    --ui-radius-sm:   10px;
    --ui-radius-md:   12px;
    --ui-radius-lg:   16px;
    --ui-radius-pill: 999px;

    /* Sizing */
    --ui-control-h-sm: 34px;
    --ui-control-h-md: 44px;
    --ui-control-h-lg: 48px;
    --ui-touch-target: 40px;
    --ui-table-row-h:  56px;
    --ui-control-w-compact: 90px;

    /* Spacing */
    --ui-space-1: 4px;
    --ui-space-2: 8px;
    --ui-space-3: 12px;
    --ui-space-4: 16px;
    --ui-space-5: 20px;
    --ui-space-6: 24px;

    /* Motion */
    --ui-ease:      cubic-bezier(.4,0,.2,1);
    --ui-ease-out:  cubic-bezier(.16,1,.3,1);
    --ui-transition: .2s var(--ui-ease);

    /* Layers */
    --ui-z-dropdown:       80;
    --ui-z-sticky:         90;
    --ui-z-modal-backdrop: 200;
    --ui-z-modal:          220;
    --ui-z-toast:          240;

    /* === Legacy tokens (mapped to semantic tokens) ==================== */
    --bg:           var(--ui-bg);
    --surface:      var(--ui-surface-1);
    --surface-2:    var(--ui-surface-2);
    --surface-3:    var(--ui-surface-3);
    --surface-opaque:   #121214; /* --surface flattened over --bg — for sticky/frozen cells that must hide scrolling content behind them */
    --surface-2-opaque: #171719; /* --surface-2 flattened over --bg, same purpose */
    --surface-3-opaque: #1f1f21; /* --surface-3 flattened over --bg, same purpose */
    --surface-4-opaque: #2b2b2d; /* rgba(255,255,255,.15) flattened over --bg — lighter hover highlight for frozen columns */
    --border:       var(--ui-border-subtle);
    --hi:       var(--ui-action);
    --hi-dim:   var(--ui-focus-ring);
    --hi-glow:  var(--ui-border-strong);
    --green:        var(--ui-success);
    --green-dim:    rgba(74,222,128,.25);
    --amber:        var(--ui-warning);
    --amber-dim:    rgba(251,191,36,.25);
    --purple:      var(--ui-info);
    --purple-dim:  rgba(124,106,245,.15);
    --purple-glow: rgba(124,106,245,.35);
    --red:          var(--ui-danger);
    --red-dim:      rgba(248,113,113,.25);
    --text:         var(--ui-text);
    --text-2:       var(--ui-text-muted);
    --text-3:       var(--ui-text-subtle);
    --accent:       var(--ui-info);
    --radius:       var(--ui-radius-lg);
    --radius-sm:    var(--ui-radius-sm);
    --radius-pill:  var(--ui-radius-pill);
    --nav-h:        56px;
    --nav-radius:   28px;
    --safe-top:     env(safe-area-inset-top, 0px);
    --safe-bottom:  env(safe-area-inset-bottom, 0px);
    --transition:   var(--ui-transition);
    --glass:            rgba(255,255,255,0.10);
    --glass-mid:        rgba(255,255,255,0.13);
    --glass-strong:     rgba(255,255,255,0.18);
    --glass-border:     rgba(255,255,255,0.18);
    --glass-rim:        rgba(255,255,255,0.25);
    --glass-blur:       blur(20px) saturate(180%) brightness(0.92);
    --glass-blur-heavy: blur(40px) saturate(200%) brightness(0.88);
    --shadow-glass:     0 8px 32px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.25);
    --shadow-glow:      0 0 0 1px var(--hi-glow), 0 8px 24px rgba(0,0,0,.35);
    --spring:           var(--ui-ease-out);
    --ease-out:         var(--ui-ease-out);
}

/* === v58 UI STANDARD COMPONENTS ===================================== */

/* --- Typography --- */
.ui-title-page {
    font-size: 26px;
    line-height: 1.15;
    font-weight: 700;
    letter-spacing: 0;
    color: var(--ui-text);
}

.ui-title-panel {
    font-size: 18px;
    line-height: 1.25;
    font-weight: 700;
    letter-spacing: 0;
    color: var(--ui-text);
}

.ui-title-section {
    font-size: 15px;
    line-height: 1.3;
    font-weight: 700;
    letter-spacing: 0;
    color: var(--ui-text);
    margin: 0 0 var(--ui-space-3);
}

.settings-section-title {
    display: flex;
    align-items: center;
    gap: var(--ui-space-2);
    font-size: 15px;
    line-height: 1.3;
    font-weight: 700;
    letter-spacing: 0;
    color: var(--ui-text);
    margin: 0 0 var(--ui-space-3);
}

.settings-section-title i {
    font-size: 16px;
    color: var(--ui-text-muted);
}

.ui-label-section,
.ui-label-field {
    display: block;
    font-size: 12px;
    line-height: 1.25;
    font-weight: 600;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--ui-text-subtle);
}

.ui-text-body {
    font-size: 14px;
    line-height: 1.5;
    color: var(--ui-text);
}

.ui-text-muted {
    font-size: 12.5px;
    line-height: 1.45;
    color: var(--ui-text-subtle);
}

.ui-text-error {
    min-height: 18px;
    font-size: 12.5px;
    line-height: 1.4;
    color: var(--ui-danger);
}

/* --- Page header --- */
.ui-page-header {
    /* Clears the fixed top-right session pill (top: safe-top + 10px, 44px tall). */
    padding: calc(var(--safe-top) + 56px) var(--ui-space-5) var(--ui-space-5);
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--ui-space-3);
}

.ui-page-actions {
    display: inline-flex;
    align-items: center;
    gap: var(--ui-space-2);
}

/* --- Buttons --- */
.ui-button,
.ui-button-icon {
    font: inherit;
    font-weight: 600;
    border: 1px solid transparent;
    cursor: pointer;
    transition:
        background var(--ui-transition),
        border-color var(--ui-transition),
        color var(--ui-transition),
        opacity var(--ui-transition),
        transform var(--ui-transition);
}

.ui-button {
    min-height: var(--ui-control-h-md);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--ui-space-2);
    padding: 0 var(--ui-space-4);
    border-radius: var(--ui-radius-sm);
    white-space: nowrap;
}

.ui-button--primary {
    background: var(--ui-action);
    color: var(--ui-text-inverse);
}

.ui-button--secondary {
    background: var(--ui-surface-2);
    color: var(--ui-text-muted);
    border-color: var(--ui-border-subtle);
}

.ui-button--ghost {
    background: transparent;
    color: var(--ui-text-muted);
    border-color: var(--ui-border-subtle);
}

.ui-button--danger {
    background: var(--ui-danger-bg);
    color: var(--ui-danger);
    border-color: var(--ui-danger);
}

.ui-button--sm {
    min-height: var(--ui-control-h-sm);
    padding: 0 var(--ui-space-3);
    font-size: 12.5px;
}

.ui-button--block {
    width: 100%;
}

.ui-button:hover,
.ui-button-icon:hover {
    border-color: var(--ui-border-strong);
}

.ui-button:disabled,
.ui-button-icon:disabled {
    opacity: .45;
    cursor: not-allowed;
    transform: none;
}

.ui-button-icon {
    width: var(--ui-touch-target);
    height: var(--ui-touch-target);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--ui-radius-sm);
    background: var(--ui-surface-2);
    border-color: var(--ui-border-subtle);
    color: var(--ui-text-muted);
}

.ui-button-icon.is-active {
    background: var(--ui-action);
    border-color: var(--ui-action);
    color: var(--ui-text-inverse);
}

/* Round icon button — generalises the former #btn-db-compact / #btn-loans-compact
   ID overrides. 38px circle with a slightly larger glyph. Doubled class raises
   specificity above the legacy .icon-btn rules in index.css (which load after
   this file and would otherwise win on radius/size at equal specificity). */
.ui-button-icon.ui-button-icon--round {
    width: 38px;
    height: 38px;
    border-radius: var(--ui-radius-pill);
}
.ui-button-icon--round i { font-size: 20px; }

/* Opaque icon button — for buttons that sit over scrolling content (e.g. the
   collapsible search trigger over the chip rail). Uses the solid surfaces so it
   composites identically to a normal .ui-button-icon on --ui-bg, but nothing
   shows through. Doubled class beats .icon-btn's background for the same reason. */
.ui-button-icon.ui-button-icon--opaque { background: var(--ui-surface-solid-2); }
.ui-button-icon.ui-button-icon--opaque:hover { background: var(--ui-surface-solid-3); }

/* --- Segmented controls --- */
.ui-segmented {
    display: inline-flex;
    align-items: center;
}

.ui-segment {
    min-height: 30px;
    padding: 0 12px;
    font: inherit;
    font-size: 12px;
    font-weight: 600;
    border: 1px solid var(--ui-border-subtle);
    background: var(--ui-surface-2);
    color: var(--ui-text-subtle);
    cursor: pointer;
}

.ui-segment:first-child {
    border-radius: var(--ui-radius-pill) 0 0 var(--ui-radius-pill);
}

.ui-segment:last-child {
    border-radius: 0 var(--ui-radius-pill) var(--ui-radius-pill) 0;
}

.ui-segment.is-active {
    background: var(--ui-focus-ring);
    border-color: var(--ui-border-strong);
    color: var(--ui-text);
}

/* --- Form fields --- */
.ui-field {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-bottom: var(--ui-space-4);
}

.ui-input,
.ui-select,
.ui-textarea {
    width: 100%;
    min-height: var(--ui-control-h-md);
    padding: 10px 14px;
    border: 1px solid var(--ui-border-subtle);
    border-radius: var(--ui-radius-sm);
    background: var(--ui-surface-2);
    color: var(--ui-text);
    font: inherit;
    font-size: 14px;
    outline: none;
    transition: border-color var(--ui-transition), box-shadow var(--ui-transition);
}

.ui-input::placeholder,
.ui-textarea::placeholder {
    color: var(--ui-text-subtle);
}

.ui-input:focus,
.ui-select:focus,
.ui-textarea:focus {
    border-color: var(--ui-border-strong);
    box-shadow: 0 0 0 3px var(--ui-focus-ring);
}

.ui-field-help {
    font-size: 12px;
    line-height: 1.4;
    color: var(--ui-text-subtle);
}

.ui-field.is-invalid .ui-input,
.ui-field.is-invalid .ui-select,
.ui-field.is-invalid .ui-textarea {
    border-color: var(--ui-danger);
}

/* --- Search bars --- */
.ui-search {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--ui-space-2);
    padding: 0 var(--ui-space-5);
    margin-bottom: var(--ui-space-4);
}

.ui-search--flush {
    padding-left: 0;
    padding-right: 0;
}

.ui-search > .ti-search {
    position: absolute;
    left: 34px;
    color: var(--ui-text-subtle);
    font-size: 15px;
    pointer-events: none;
    transition: color var(--ui-transition);
}

.ui-search--flush > .ti-search {
    left: 14px;
}

.ui-search-input {
    flex: 1;
    min-width: 0;
    min-height: 38px;
    width: 100%;
    padding: 0 16px 0 39px;
    border: none;
    border-radius: var(--ui-radius-pill);
    background: var(--ui-surface-2);
    color: var(--ui-text);
    font: inherit;
    font-size: 13.5px;
    outline: none;
    transition: background var(--ui-transition), color var(--ui-transition), box-shadow var(--ui-transition);
}

.ui-search-input:hover {
    background: var(--ui-surface-3);
}

.ui-search-input:focus {
    background: var(--ui-action);
    color: var(--ui-text-inverse);
    box-shadow: 0 0 0 3px var(--ui-focus-ring);
}

.ui-search-input::placeholder {
    color: var(--ui-text-subtle);
}

.ui-search-input:focus::placeholder {
    color: rgba(0,0,0,.55);
}

.ui-search:focus-within > .ti-search {
    color: var(--ui-text-inverse);
}

/* --- Selects --- */
.ui-select {
    appearance: none;
    padding-right: 32px;
    cursor: pointer;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238b8b92' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 12px center;
}

.ui-select--compact {
    min-height: var(--ui-control-h-sm);
    padding: 0 28px 0 12px;
    font-size: 12px;
    border-radius: var(--ui-radius-pill);
    color: var(--ui-text-muted);
}

.ui-select option {
    background: #1e1e24;
    color: #fff;
}

/* --- File inputs --- */
.ui-file {
    display: block;
    width: 100%;
    min-height: var(--ui-control-h-md);
    padding: 7px 10px;
    border: 1px solid var(--ui-border-subtle);
    border-radius: var(--ui-radius-sm);
    background: var(--ui-surface-2);
    color: var(--ui-text-muted);
    font: inherit;
    font-size: 12px;
    cursor: pointer;
}

.ui-file:focus {
    outline: none;
    border-color: var(--ui-border-strong);
    box-shadow: 0 0 0 3px var(--ui-focus-ring);
}

.ui-file::file-selector-button {
    min-height: 28px;
    margin-right: 12px;
    padding: 0 12px;
    border: 0;
    border-right: 1px solid var(--ui-border-subtle);
    border-radius: var(--ui-radius-sm);
    background: var(--ui-surface-3);
    color: var(--ui-text);
    font: inherit;
    font-size: 11px;
    font-weight: 600;
    cursor: pointer;
}

/* --- Chips, pills, badges --- */
.ui-chip {
    /* DEC-20 landed at slice 8 (DEC-79). Was a hardcoded 32px, which knowingly
       violated the --ui-touch-target floor for eight slices. */
    min-height: var(--ui-touch-target);
    display: inline-flex;
    align-items: center;
    gap: var(--ui-space-1);
    padding: 0 11px;
    border: none;
    border-radius: var(--ui-radius-pill);
    background: var(--ui-surface-2);
    color: var(--ui-text-muted);
    font: inherit;
    font-size: 12.5px;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
    transition: background var(--ui-transition), color var(--ui-transition), transform 0.12s var(--ui-ease-out), box-shadow var(--ui-transition);
}

.ui-chip:hover {
    background: var(--ui-surface-2);
    color: var(--ui-text);
}

.ui-chip.is-active {
    background: var(--ui-action);
    color: var(--ui-text-inverse);
}

.ui-pill,
.ui-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--ui-space-1);
    border-radius: var(--ui-radius-pill);
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
}

.ui-pill {
    padding: 4px 11px;
    font-size: 12.5px;
}

.ui-badge {
    padding: 3px 10px;
    font-size: 12px;
}

.ui-pill--success { background: var(--ui-success-bg); color: var(--ui-success); }
.ui-pill--warning { background: var(--ui-warning-bg); color: var(--ui-warning); }
.ui-pill--danger { background: var(--ui-danger-bg); color: var(--ui-danger); }
.ui-pill--info { background: var(--ui-info-bg); color: var(--ui-info); }
.ui-pill--neutral,
.ui-badge--neutral {
    background: var(--ui-surface-3);
    color: var(--ui-text-muted);
}

/* --- Cards and panels --- */
.ui-card {
    margin: 0 var(--ui-space-5) var(--ui-space-3);
    overflow: hidden;
    border: 1px solid var(--ui-border-subtle);
    border-radius: var(--ui-radius-lg);
    background: var(--ui-surface-1);
}

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

.ui-card-body {
    padding: var(--ui-space-4);
}

/* --- Navigation lists --- */
.ui-list {
    display: flex;
    flex-direction: column;
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border-subtle);
    border-radius: var(--ui-radius-md);
    overflow: hidden;
}

.ui-list-row {
    display: flex;
    align-items: center;
    gap: var(--ui-space-3);
    width: 100%;
    min-height: 52px;
    padding: 0 var(--ui-space-4);
    background: none;
    border: 0;
    border-bottom: 1px solid var(--ui-border-subtle);
    color: var(--ui-text);
    font: inherit;
    text-align: left;
    cursor: pointer;
    transition: background var(--ui-transition);
}

.ui-list-row:last-child { border-bottom: 0; }
.ui-list-row:hover { background: var(--ui-surface-2); }

.ui-list-row-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    font-size: 19px;
    color: var(--ui-text-muted);
    flex-shrink: 0;
}

.ui-list-row-text { flex: 1; min-width: 0; }
.ui-list-row-label { display: block; font-size: 14px; font-weight: 500; line-height: 1.3; }
.ui-list-row-sub { display: block; margin-top: 1px; font-size: 11px; color: var(--ui-text-subtle); line-height: 1.3; }
.ui-list-row-arrow { font-size: 16px; color: var(--ui-text-subtle); flex-shrink: 0; }

/* Leaf-view hero: the icon/label/sub of a .ui-list-row, restacked as a
   left-aligned card at the top of the destination page. Title uses
   .ui-title-panel, sub uses .ui-text-muted (both existing ui-core classes);
   only the icon needs a size bump scoped to this component, matching the
   26px .ui-title-page scale already defined above. */
.ui-list-hero {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--ui-space-1);
    padding: var(--ui-space-4);
    margin-bottom: var(--ui-space-4);
    background: var(--ui-surface-1);
    border: 1px solid var(--ui-border-subtle);
    border-radius: var(--ui-radius-md);
}
.ui-list-hero .ui-list-row-icon { font-size: 26px; width: auto; }

/* --- Tables --- */
.ui-table-scroll {
    overflow-x: auto;
}

.ui-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 14px;
}

.ui-table th {
    padding: 9px 14px;
    border-bottom: 1px solid var(--ui-border-subtle);
    color: var(--ui-text-subtle);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: .05em;
    text-align: left;
    text-transform: uppercase;
    white-space: nowrap;
}

.ui-table td {
    height: var(--ui-table-row-h);
    padding: 10px 14px;
    border-bottom: 1px solid var(--ui-border-subtle);
    color: var(--ui-text);
    vertical-align: middle;
    white-space: nowrap;
}

.ui-table tr:hover td {
    background: var(--ui-surface-2);
}

/* --- Empty and loading states --- */
.ui-empty {
    min-height: 160px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--ui-space-3);
    padding: var(--ui-space-6);
    color: var(--ui-text-subtle);
}

.ui-empty i {
    font-size: 40px;
}

.ui-empty p {
    max-width: 42ch;
    font-size: 14px;
    line-height: 1.45;
    text-align: center;
}

.ui-spin {
    animation: ui-spin .8s linear infinite;
}

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

/* --- Modals, sheets, overlays ---
   Shown state is opacity/visibility-driven, not display-driven, so both the
   open AND close transitions can play. Visibility flips only after the fade
   completes (transition-delay on the closed side) so the overlay stops
   intercepting clicks/tab order once fully hidden, and pointer-events is
   dropped immediately on close so it can't be clicked mid-fade-out. */
.ui-overlay {
    position: fixed;
    inset: 0;
    z-index: var(--ui-z-modal-backdrop);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--ui-space-5);
    background: rgba(0,0,0,.72);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    /* visibility delay matches the modal/sheet content's own exit transform
       duration (.28s) below so the close animation gets to finish before the
       overlay disappears — not the backdrop's own (shorter) fade time. */
    transition: opacity var(--ui-transition), visibility 0s linear .28s;
}

.ui-overlay.open {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity var(--ui-transition);
}

/* Sheets are full-bleed: they anchor to the true bottom edge AND run to the
   left/right screen edges, so the overlay's own padding must not inset them on
   any side (only .ui-modal wants that inset). The 480px cap on .ui-sheet below
   still centres it on desktop; this only removes the gutter that kept it off
   the edges on mobile. Scoped by :has() so .ui-modal is untouched. */
.ui-overlay:has(> .ui-sheet) {
    padding: 0;
}

.ui-modal,
.ui-sheet {
    width: min(100%, 480px);
    max-height: 92dvh;
    overflow-y: auto;
    border: 1px solid var(--ui-border-subtle);
    color: var(--ui-text);
    opacity: 0;
    transition: transform .28s var(--ui-ease-out), opacity .22s var(--ui-ease);
}

/* The transform/opacity transition above only plays when the surface was
   already in the DOM in its closed state and `.open` is then toggled on — the
   legacy vanilla path. React's <Overlay> returns null while closed and mounts
   the node ALREADY carrying `.open`, so there is no previous frame to
   transition from and the entry animation silently never ran on any React
   sheet. Keyframe animations do not have that limitation: they run whenever an
   element matching the selector is inserted, and again when the selector starts
   matching an existing element. So the animation drives the entry for both
   paths, the transition is left in place to drive the legacy EXIT (React
   overlays unmount, so they cannot animate out at all), and both settle on the
   identical end state — opacity 1, no transform — so there is no jump when the
   animation's `both` fill hands back to the cascade. */
.ui-overlay.open .ui-modal,
.ui-overlay.open .ui-sheet {
    opacity: 1;
}

.ui-overlay.open .ui-modal {
    animation: ui-modal-in .28s var(--ui-ease-out) both;
}

.ui-overlay.open .ui-sheet {
    animation: ui-sheet-in .28s var(--ui-ease-out) both;
}

@keyframes ui-modal-in {
    from { opacity: 0; transform: scale(.92) translateY(14px); }
    to   { opacity: 1; transform: none; }
}

@keyframes ui-sheet-in {
    from { opacity: 0; transform: translateY(100%); }
    to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
    .ui-overlay.open .ui-modal,
    .ui-overlay.open .ui-sheet {
        animation-duration: .01ms;
    }
}

.ui-modal {
    background: rgba(13,15,20,.92);
    border-radius: var(--ui-radius-lg);
    padding: var(--ui-space-5);
    transform: scale(.92) translateY(14px);
}

.ui-overlay.open .ui-modal {
    transform: none;
}

.ui-sheet {
    align-self: flex-end;
    background: rgba(13,15,20,.75);
    backdrop-filter: var(--glass-blur-heavy);
    -webkit-backdrop-filter: var(--glass-blur-heavy);
    border-top-color: var(--glass-border);
    border-radius: 28px 28px 0 0;
    padding: 0 var(--ui-space-5) calc(36px + var(--safe-bottom));
    transform: translateY(100%);
}

.ui-overlay.open .ui-sheet {
    transform: translateY(0);
}

.ui-sheet::-webkit-scrollbar {
    display: none;
}

/* Fullscreen sheet: fills the viewport instead of sizing to its content, and
   becomes a flex column so a paged/scrolling body can inherit a DEFINITE
   height. That flex column is load-bearing, not cosmetic — the loan builder's
   `.ml-page` → `#ml-tracked-panel` → `#ml-serial-scroll` chain is a stack of
   `flex:1; min-height:0` rules, and every one of them is inert unless its
   parent is a flex container, which is why an inner list previously grew the
   whole sheet instead of scrolling inside its own box. `overflow-x: hidden`
   contains horizontally-translated page tracks. */
.ui-sheet--full {
    min-height: 100dvh;
    max-height: 100dvh;
    display: flex;
    flex-direction: column;
    overflow-x: hidden;
}

/* Tighter gutters where 20px is too much of a narrow viewport. Deliberately
   scoped to the fullscreen variant rather than all sheets: it preserves the
   loan builder's existing ≤390px behaviour exactly while leaving the
   content-sized sheets untouched. Worth generalising to `.ui-sheet` later, but
   only as a change reviewed on its own. */
@media (max-width: 390px) {
    .ui-sheet--full {
        padding-inline: var(--ui-space-4);
        padding-bottom: calc(28px + var(--safe-bottom));
    }

    .ui-sheet--full .ui-sheet-handle {
        margin-bottom: var(--ui-space-4);
    }
}

.ui-sheet-handle {
    width: 36px;
    height: 4px;
    margin: 14px auto 20px;
    border-radius: var(--ui-radius-pill);
    background: var(--ui-border-strong);
}

.ui-sheet-title {
    font-size: 16px;
    font-weight: 700;
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 4px;
}

.ui-sheet-title i {
    color: var(--ui-action);
    font-size: 20px;
}

.ui-sheet-sub,
.ui-sheet-meta {
    font-size: 12px;
    color: var(--ui-text-subtle);
    margin-bottom: 20px;
}

.ui-modal-header,
.ui-modal-actions {
    display: flex;
    align-items: center;
    gap: var(--ui-space-3);
}

.ui-modal-header {
    justify-content: space-between;
    margin-bottom: var(--ui-space-4);
}

.ui-modal-body {
    display: flex;
    flex-direction: column;
}

.ui-modal-actions {
    justify-content: flex-end;
    margin-top: var(--ui-space-4);
}

/* --- Toasts --- */
.ui-toast {
    display: inline-flex;
    align-items: center;
    gap: var(--ui-space-2);
    min-height: var(--ui-control-h-md);
    max-width: min(420px, calc(100vw - 32px));
    padding: 12px 16px;
    border: 1px solid var(--ui-border-subtle);
    border-radius: var(--ui-radius-md);
    background: var(--ui-surface-solid-2);
    color: var(--ui-text);
    box-shadow: 0 8px 24px rgba(0,0,0,.35);
}

.ui-toast--success {
    border-color: rgba(74,222,128,.4);
    background: rgba(74,222,128,.12);
}

.ui-toast--error {
    border-color: rgba(248,113,113,.45);
    background: rgba(248,113,113,.12);
}

/* --- Navigation and session bar --- */
.ui-session-bar {
    min-height: 36px;
    display: flex;
    align-items: center;
    gap: var(--ui-space-2);
    padding: 0 var(--ui-space-3);
}

.ui-status-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
}

.ui-status-dot--success {
    background: var(--ui-success);
    box-shadow: 0 0 6px var(--ui-success);
}

/* --- Restrained utility classes --- */
.u-hidden { display: none !important; }
/* Off-screen but still read aloud. For live regions that narrate something the
   sighted user learns from the layout itself — the catalogue reorder is the
   first: the DOM changes and nothing otherwise announces it. `clip-path`
   rather than `display:none`, which removes the node from the a11y tree. */
.u-visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}
.u-row { display: flex; gap: var(--ui-space-2); }
.u-row--center { align-items: center; }
.u-row--gap-sm { gap: var(--ui-space-2); }
.u-block { width: 100%; }
.u-flex-1 { flex: 1 1 0; min-width: 0; }
.u-mt-2 { margin-top: var(--ui-space-2); }
.u-mt-3 { margin-top: var(--ui-space-3); }

/* --- Boot / splash screen ---
   Painted by the SERVER, not by React. Both Jinja shells include
   templates/_boot.html as a sibling of the React mount, so this screen is on
   screen from the browser's first paint — before the bundle has downloaded and
   long before React mounts. That is the entire point: it exists to fill the
   blank gap a standalone PWA shows on cold launch, and React cannot fill a gap
   that occurs before React exists.

   It therefore lives HERE and not in frontend/src/components.css. ui-core.css
   is loaded from <head> by both templates/index.html and templates/login.html;
   the React island's CSS is linked at the end of <body> from the Vite manifest,
   so styling this screen there would leave it unstyled for exactly the window
   it is meant to cover.

   Dismissal and the progress bar are driven by
   frontend/src/components/shell/bootScreen.ts — read that file's header before
   changing any class name or id below; it queries them by name.

   REDUCED MOTION is already handled globally at the bottom of this file
   (animation-duration: .01ms !important + iteration-count: 1). Combined with
   `both` fill mode the entrance snaps straight to its end state: visible, no
   movement. Do not add a bespoke reduced-motion block here; it would be a
   second implementation of a rule this file already owns. */

.ui-boot {
    position: fixed;
    inset: 0;
    z-index: var(--ui-z-modal);
    display: flex;
    align-items: center;
    justify-content: center;
    /* Safe-area insets, not a bare gutter: this paints edge-to-edge in a
       standalone PWA, so the notch and the home indicator both overlap it. */
    padding: calc(var(--safe-top) + var(--ui-space-6)) var(--ui-space-5)
             calc(var(--safe-bottom) + var(--ui-space-6));
    background: var(--ui-bg);
    color: var(--ui-text);
    font-family: 'Poppins', system-ui, sans-serif;
    /* NO entrance animation on the ground itself — see the note above
       @keyframes ui-boot-rise. It was a visual no-op that cost a permanently
       promoted compositing layer, which left a stale tile across the top of
       the screen after the splash was removed (b1.2.3). */
}

/* Removed by bootScreen.ts once the app is ready. Kept as a class rather than
   an immediate node removal so the exit can fade; `visibility` flips only
   after the fade so the screen stops swallowing taps the instant it starts
   leaving — the same pattern .ui-overlay above uses, for the same reason. */
.ui-boot.is-leaving {
    opacity: 0;
    pointer-events: none;
    transition: opacity .22s var(--ui-ease);
}

.ui-boot-inner {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
    max-width: 300px;
}

/* --- Brand lockup, shared by the boot screen and the login card ---
   ONE implementation under two names. The design premise is that the logo and
   wordmark do not move when the splash hands off to the login card, which only
   holds if a single rule sizes both. These used to live in login.css; they were
   moved here (not copied) when the boot screen was added, so login.css no
   longer defines them. Changing a value here changes both surfaces together,
   which is the point. */
.ui-boot-logo,
.login-logo {
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 0 auto 24px;
}

.ui-boot-logo img,
.login-logo img {
    width: 96px;
    height: 96px;
}

.ui-boot-title,
.login-title {
    text-align: center;
    font-size: 22px;
    font-weight: 700;
    letter-spacing: -.3px;
    margin-bottom: 4px;
}

.ui-boot-version,
.login-footer {
    margin-top: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    color: var(--ui-text-muted);
    font-size: 13px;
}

/* The boot screen carries no subtitle, so the wordmark's 4px bottom margin
   (sized for the login card, where .login-sub supplies the rest of the gap)
   would leave the bar sitting on top of it. Restored on the container rather
   than by weakening the shared rule above. */
.ui-boot .ui-boot-title { margin-bottom: 0; }

/* Short and centred, deliberately NOT the full-width Summary bar. A bar that
   spans the column reads as a measurement of something and invites the eye to
   study it; at boot it should read as a breath mark. Only the BOX is set here
   — .progress-track / .progress-fill in index.css still own the height,
   radius, track colour and fill contract. */
.ui-boot-progress {
    width: 120px;
    margin-top: var(--ui-space-6);
}

.ui-boot-progress .progress-track { margin-bottom: 0; }

/* Bootstrap can fail (no key material on this device, a revoked session, a
   500). Nothing else in the app renders SessionState.status === 'error', so
   without this the screen would simply never leave and the user would be
   locked on a splash. Hidden until bootScreen.ts fills it. */
.ui-boot-error {
    display: none;
    margin-top: var(--ui-space-5);
    max-width: 34ch;
    text-align: center;
    font-size: 13px;
    line-height: 1.5;
    color: var(--ui-danger);
}

.ui-boot.has-error .ui-boot-error { display: block; }
.ui-boot.has-error .ui-boot-progress { display: none; }

.ui-boot-error a {
    display: inline-block;
    margin-top: var(--ui-space-3);
    color: var(--ui-text);
}

/* --- Background lattice ---
   Was `body::before` in login.css, where it could only ever dress the login
   document. The boot screen needs the same ground on the app shell too, so it
   became a class applied to <body> on login and to .ui-boot everywhere. One
   definition, two call sites. */
.ui-lattice::before {
    content: '';
    position: fixed;
    inset: 0;
    background-image:
        linear-gradient(rgba(255,255,255,.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(255,255,255,.03) 1px, transparent 1px);
    background-size: 48px 48px;
    pointer-events: none;
    z-index: 0;
}

/* The screen arrives rather than appearing: each element rises in a short
   stagger. The logo leads because it is the only thing on screen that
   identifies the app; the version trails because it is the least important
   thing on it.

   `both` fill mode is load-bearing HERE. Without it every element paints at
   its final opacity for one frame before its delay elapses, which is precisely
   the flash this animation exists to remove. The children are small, and the
   whole subtree is removed on dismissal, so their promoted layers go with it.

   THE GROUND ITSELF IS NOT ANIMATED, and must not be. `.ui-boot` used to carry
   `animation: ui-boot-fade .28s both`, which was wrong twice over:

     1. It was invisible. `.ui-boot`'s background is --ui-bg (#060608), which is
        exactly manifest.json's `background_color` and the `theme-color`. The
        platform's own launch screen is already painted that colour, so fading
        the splash background in from transparent faded #060608 over #060608.

     2. It cost a full-viewport compositing layer for the ENTIRE boot. A filled
        animation leaves the element holding an animated `opacity` for its whole
        life, so browsers keep it GPU-promoted indefinitely; the exit transition
        then removed the node while that layer was still being torn down. On
        mobile that left a stale tile as a full-width blurred band across the
        top of the app, which only cleared when a route change forced a repaint
        (reported against b1.2.3).

   If an entrance for the background is ever wanted, it needs a colour that
   actually differs from the launch screen — otherwise it is paying a real
   rendering cost for nothing. */
@keyframes ui-boot-rise {
    from { opacity: 0; transform: translateY(10px) scale(.97); }
    to   { opacity: 1; transform: none; }
}

.ui-boot-inner > * {
    animation: ui-boot-rise .52s var(--ui-ease-out) both;
}

.ui-boot-inner > *:nth-child(1) { animation-delay: .06s; }
.ui-boot-inner > *:nth-child(2) { animation-delay: .14s; }
.ui-boot-inner > *:nth-child(3) { animation-delay: .22s; }
.ui-boot-inner > *:nth-child(4) { animation-delay: .30s; }

/* --- Global focus-visible and reduced-motion coverage --- */
:where(button, a, input, select, textarea, [tabindex]):focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px var(--ui-focus-ring);
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        scroll-behavior: auto !important;
        transition-duration: .01ms !important;
    }
}
