/* ============================================================
   UX component layer — the parts that make the system *feel* fast.

   Each block below exists because of a specific problem in the old
   build; the comment says which one. Load after tokens.css + aero.css.
   ============================================================ */

/* ------------------------------------------------------------
   1. Command palette  (Ctrl/Cmd-K)

   Problem it solves: to open job file GML-0912 you had to guess which
   of five log lists it lived in, load that page, type into its filter,
   press Search, then click the row. Four page loads to reach a record
   you could already name. The palette is one keystroke to anywhere.
   ------------------------------------------------------------ */

.ux-palette {
    position: fixed;
    inset: 0;
    z-index: var(--z-palette);
    display: none;
    padding: 10vh var(--sp-5) var(--sp-5);
    background: rgba(16, 32, 56, 0.34);
    backdrop-filter: blur(3px);
}
.ux-palette[data-open="true"] { display: block; }

.ux-palette__box {
    max-width: 680px;
    margin: 0 auto;
    background: rgba(250, 252, 254, 0.99);
    border: 1px solid var(--aero-glass-border);
    border-radius: var(--r-lg);
    box-shadow: var(--el-4);
    overflow: hidden;
    animation: uxPop var(--dur) var(--ease);
}
@keyframes uxPop {
    from { opacity: 0; transform: translateY(-10px) scale(0.985); }
    to   { opacity: 1; transform: none; }
}

.ux-palette__input {
    width: 100%;
    border: 0;
    border-bottom: 1px solid rgba(0, 0, 0, 0.10);
    padding: var(--sp-6) var(--sp-7);
    font-family: inherit;
    font-size: var(--fs-h3);
    color: var(--text);
    background: transparent;
}
.ux-palette__input:focus { outline: none; box-shadow: none; }
.ux-palette__input::placeholder { color: #8496ad; }

.ux-palette__results { max-height: 56vh; overflow-y: auto; padding: var(--sp-3); }

.ux-palette__group {
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: 0.6px;
    font-weight: var(--fw-semi);
    color: var(--text-muted);
    padding: var(--sp-5) var(--sp-5) var(--sp-2);
}

.ux-palette__item {
    display: flex;
    align-items: center;
    gap: var(--sp-5);
    width: 100%;
    padding: var(--sp-4) var(--sp-5);
    border: 0;
    background: none;
    border-radius: var(--r-md);
    text-align: left;
    font: inherit;
    color: var(--text);
    cursor: pointer;
    text-decoration: none;
}
/* Highlight follows the keyboard, not the mouse: the arrow keys are the
   primary input here, so hover must not fight the selection. */
.ux-palette__item[aria-selected="true"] { background: rgba(31, 111, 235, 0.14); }

.ux-palette__icon {
    flex: none;
    width: 26px; height: 26px;
    display: grid; place-items: center;
    border-radius: var(--r-sm);
    background: var(--neutral-tint);
    font-size: var(--fs-small);
}
.ux-palette__main { min-width: 0; flex: 1; }
.ux-palette__title {
    font-weight: var(--fw-semi);
    font-size: var(--fs-body);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ux-palette__sub {
    font-size: var(--fs-small);
    color: var(--text-muted);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ux-palette__meta { flex: none; font-size: var(--fs-small); color: var(--text-muted); }
.ux-palette__hit { background: #ffe9a8; border-radius: 2px; font-weight: var(--fw-bold); }

.ux-palette__foot {
    display: flex; gap: var(--sp-6);
    padding: var(--sp-4) var(--sp-7);
    border-top: 1px solid rgba(0, 0, 0, 0.08);
    background: rgba(238, 243, 249, 0.9);
    font-size: var(--fs-micro);
    color: var(--text-muted);
}
.ux-palette__empty { padding: var(--sp-9) var(--sp-7); text-align: center; color: var(--text-muted); }

kbd {
    display: inline-block;
    font-family: var(--font-ui);
    font-size: var(--fs-micro);
    font-weight: var(--fw-semi);
    line-height: 1;
    padding: 3px 6px;
    color: var(--text-soft);
    background: linear-gradient(to bottom, #fff 0%, #e8edf3 100%);
    border: 1px solid #b3c0d1;
    border-bottom-width: 2px;
    border-radius: var(--r-sm);
    white-space: nowrap;
}

/* ------------------------------------------------------------
   2. Toasts + Undo

   Problem: Django messages rendered at the top of the *next* page.
   After clicking Approve you got a full reload and had to look up to
   find out whether it worked. And there was no undo anywhere — a
   mis-clicked Delete was final (recoverable only by a manager digging
   through Settings > Trashcan).

   Toasts are announced via aria-live so screen readers get them too.
   ------------------------------------------------------------ */

.ux-toasts {
    position: fixed;
    right: var(--sp-6);
    bottom: var(--sp-6);
    z-index: var(--z-toast);
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
    max-width: min(420px, calc(100vw - var(--sp-9)));
    pointer-events: none;
}

.ux-toast {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: var(--sp-4);
    padding: var(--sp-5) var(--sp-6);
    border-radius: var(--r-md);
    border: 1px solid;
    border-left-width: 4px;
    background: rgba(252, 253, 255, 0.99);
    box-shadow: var(--el-3);
    font-size: var(--fs-body);
    animation: uxToastIn var(--dur-slow) var(--ease);
}
.ux-toast[data-leaving="true"] { animation: uxToastOut var(--dur) var(--ease) forwards; }

@keyframes uxToastIn  { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@keyframes uxToastOut { to   { opacity: 0; transform: translateX(20px); } }

.ux-toast--success { border-color: var(--ok-border);   background: linear-gradient(to bottom, #f2fbf6 0%, #e6f5ec 100%); color: var(--ok-fg); }
.ux-toast--error   { border-color: var(--bad-border);  background: linear-gradient(to bottom, #fff6f7 0%, #fdeaec 100%); color: var(--bad-fg); }
.ux-toast--warning { border-color: var(--warn-border); background: linear-gradient(to bottom, #fffbef 0%, #fff5da 100%); color: var(--warn-fg); }
.ux-toast--info    { border-color: var(--info-border); background: linear-gradient(to bottom, #f4f8ff 0%, #e8f1ff 100%); color: var(--info-fg); }

.ux-toast__icon { flex: none; font-size: var(--fs-lead); line-height: 1.3; }
.ux-toast__body { flex: 1; min-width: 0; }
.ux-toast__title { font-weight: var(--fw-semi); }
.ux-toast__detail { font-size: var(--fs-small); opacity: 0.85; margin-top: var(--sp-1); }

.ux-toast__undo {
    flex: none;
    align-self: center;
    border: 1px solid currentColor;
    background: rgba(255, 255, 255, 0.7);
    color: inherit;
    font: inherit;
    font-size: var(--fs-small);
    font-weight: var(--fw-semi);
    padding: var(--sp-2) var(--sp-5);
    border-radius: var(--r-sm);
    cursor: pointer;
}
.ux-toast__undo:hover { background: #fff; }

.ux-toast__close {
    flex: none; align-self: flex-start;
    border: 0; background: none; color: inherit;
    opacity: 0.5; cursor: pointer;
    font-size: var(--fs-lead); line-height: 1; padding: 0 var(--sp-1);
}
.ux-toast__close:hover { opacity: 1; }

/* A thin bar drains to show how long undo stays available, so "how long
   do I have?" is answered visually rather than by anxiety. */
.ux-toast__timer { position: absolute; left: 0; bottom: 0; height: 2px; background: currentColor; opacity: 0.35; }
.ux-toast { position: relative; overflow: hidden; }

/* ------------------------------------------------------------
   3. Dialogs

   Problem: rejection reasons were collected with window.prompt() and
   deletions with confirm(). Both are unstyleable, unvalidatable,
   suppressible by the browser ("don't show more dialogs"), useless on
   touch, and — worst for an approvals workflow — they hide the document
   you are deciding about behind a modal grey box.

   These are real dialogs: they name the record, focus-trap, restore
   focus to the trigger on close, validate before allowing submit, and
   make the destructive button the *non*-default action.
   ------------------------------------------------------------ */

.ux-dialog-backdrop {
    position: fixed; inset: 0;
    z-index: var(--z-dialog);
    display: none;
    place-items: center;
    padding: var(--sp-6);
    background: rgba(16, 32, 56, 0.42);
    backdrop-filter: blur(3px);
}
.ux-dialog-backdrop[data-open="true"] { display: grid; }

.ux-dialog {
    width: min(520px, 100%);
    max-height: 88vh;
    overflow: auto;
    background: rgba(251, 252, 254, 0.99);
    border: 1px solid var(--aero-glass-border);
    border-radius: var(--r-lg);
    box-shadow: var(--el-4);
    animation: uxPop var(--dur) var(--ease);
}
.ux-dialog--wide { width: min(860px, 100%); }

.ux-dialog__head {
    display: flex; align-items: flex-start; gap: var(--sp-5);
    padding: var(--sp-7) var(--sp-7) var(--sp-5);
}
.ux-dialog__glyph {
    flex: none; width: 36px; height: 36px;
    display: grid; place-items: center;
    border-radius: var(--r-pill);
    font-size: var(--fs-h3);
}
.ux-dialog--danger  .ux-dialog__glyph { background: var(--bad-tint);  color: var(--bad-border); }
.ux-dialog--warning .ux-dialog__glyph { background: var(--warn-tint); color: #8a6100; }
.ux-dialog--info    .ux-dialog__glyph { background: var(--info-tint); color: var(--info-border); }

.ux-dialog__title { font-size: var(--fs-h3); font-weight: var(--fw-semi); }
.ux-dialog__body { padding: 0 var(--sp-7) var(--sp-6); color: var(--text-soft); }
.ux-dialog__body p { margin: 0 0 var(--sp-4); }

/* Names the exact record being acted on. "Delete Invoice INV-1043 —
   Acme Trading — 12,400.00 USD" is a decidable question;
   "Are you sure?" is not. */
.ux-dialog__subject {
    background: var(--neutral-tint);
    border: 1px solid rgba(0, 0, 0, 0.08);
    border-radius: var(--r-md);
    padding: var(--sp-4) var(--sp-5);
    margin-bottom: var(--sp-5);
    font-size: var(--fs-small);
}
.ux-dialog__subject dl { display: grid; grid-template-columns: auto 1fr; gap: var(--sp-2) var(--sp-5); margin: 0; }
.ux-dialog__subject dt { color: var(--text-muted); }
.ux-dialog__subject dd { margin: 0; font-weight: var(--fw-semi); }

.ux-dialog__foot {
    display: flex; justify-content: flex-end; gap: var(--sp-4);
    padding: var(--sp-5) var(--sp-7) var(--sp-7);
}
.ux-dialog__foot .spacer { margin-right: auto; }
.ux-dialog__counter { font-size: var(--fs-small); color: var(--text-muted); text-align: right; margin-top: var(--sp-2); }

/* ------------------------------------------------------------
   4. Page header + breadcrumbs

   Problem: "where am I / how do I get back" was answered differently on
   every screen — some had "Back to list", some `history.back()`, some a
   Cancel button, some nothing. Deep screens (Match, Record Payment,
   Attachments) were the worst: you arrived from three possible places
   and only one of them had a way back.
   ------------------------------------------------------------ */

.ux-pagehead {
    display: flex; flex-wrap: wrap;
    align-items: flex-end; justify-content: space-between;
    gap: var(--sp-5);
    margin-bottom: var(--sp-6);
}
.ux-pagehead__titles { min-width: 0; }
.ux-pagehead__actions { display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: center; }
.ux-pagehead__sub { font-size: var(--fs-small); color: var(--text-muted); margin-top: var(--sp-2); }

.ux-crumbs {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: var(--sp-2);
    font-size: var(--fs-small);
    margin-bottom: var(--sp-3);
    padding: 0; list-style: none;
}
.ux-crumbs li { display: flex; align-items: center; gap: var(--sp-2); }
.ux-crumbs li + li::before { content: "/"; color: var(--text-muted); opacity: 0.7; }
.ux-crumbs a { color: var(--text-link); text-decoration: none; }
.ux-crumbs a:hover { text-decoration: underline; }
.ux-crumbs [aria-current="page"] { color: var(--text-muted); }

/* ------------------------------------------------------------
   5. Filter bar: active-filter chips + saved views

   Problem: after searching, the only evidence of *what* you filtered by
   was the values still sitting in the (collapsible, often scrolled-off)
   form. Two people reading the same screen would disagree about whether
   a number was "everything" or "just Basra, last month". And clearing
   one filter meant emptying a field and re-submitting.

   Chips make the filter state a visible, removable object.
   ------------------------------------------------------------ */

.ux-filterbar {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: var(--sp-4);
    margin-bottom: var(--sp-5);
}

.ux-chip {
    display: inline-flex; align-items: center; gap: var(--sp-3);
    background: rgba(255, 255, 255, 0.78);
    border: 1px solid rgba(255, 255, 255, 0.9);
    box-shadow: var(--el-1);
    border-radius: var(--r-pill);
    padding: var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-5);
    font-size: var(--fs-small);
    color: var(--text-soft);
}
.ux-chip__key { color: var(--text-muted); }
.ux-chip__val { font-weight: var(--fw-semi); color: var(--text); }
.ux-chip__x {
    display: grid; place-items: center;
    width: 18px; height: 18px;
    border: 0; border-radius: var(--r-pill);
    background: rgba(0, 0, 0, 0.07);
    color: var(--text-soft);
    font-size: 11px; line-height: 1; cursor: pointer;
    text-decoration: none;
}
.ux-chip__x:hover { background: var(--bad-border); color: #fff; }

.ux-chip--clear { border-style: dashed; background: transparent; cursor: pointer; text-decoration: none; }
.ux-chip--clear:hover { background: rgba(255, 255, 255, 0.7); }

.ux-savedviews { display: flex; align-items: center; gap: var(--sp-3); margin-left: auto; }

/* Collapsible advanced filters. Default-collapsed keeps the data table
   above the fold; the chip row shows what's applied while it's shut. */
.ux-filters[hidden] { display: none; }
.ux-filters {
    background: rgba(255, 255, 255, 0.4);
    border: 1px solid rgba(255, 255, 255, 0.68);
    border-radius: var(--r-md);
    padding: var(--sp-5) var(--sp-6);
    margin-bottom: var(--sp-5);
}
/* Filter bars only. Bottom alignment is right here: the Search button has to
   line up with the inputs, and every cell is a bare label+input of equal
   height. It is WRONG for data-entry forms — see .ux-formgrid below. */
.ux-filters__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(165px, 1fr));
    gap: var(--sp-5);
    align-items: end;
}
.ux-filters__actions { display: flex; gap: var(--sp-4); align-items: end; }

/* ------------------------------------------------------------
   Data-entry form grid.

   Fixes a real layout bug: the forms were reusing .ux-filters__grid, whose
   `align-items: end` bottom-aligns every cell. In a filter bar each cell is
   the same height so that is invisible. In a form, some fields carry help
   text and some do not — so the taller cells pushed their inputs up while
   their neighbours stayed pinned to the bottom, and a row of inputs came out
   staggered at two or three different heights. On a 25-field job form that
   reads as fields sliding up and down at random.

   The fix is structural rather than a nudge. Each field is a 3-row grid
   (label / control / notes) and, where the browser supports subgrid, all
   fields share those rows — so every label aligns with every other label and
   every input with every other input, no matter how much help text sits
   underneath or whether a label wraps to two lines. Without subgrid it falls
   back to top alignment, which is still correct for the common case.
   ------------------------------------------------------------ */
.ux-formgrid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
    gap: var(--sp-6) var(--sp-5);
    align-items: start;
}

/* One field: label, then the control, then help text and errors. */
.ux-formgrid > * {
    display: grid;
    grid-template-rows: auto auto auto;
    align-content: start;
    row-gap: 0;
    min-width: 0;          /* let long values ellipsis instead of overflowing */
}

@supports (grid-template-rows: subgrid) {
    .ux-formgrid > * {
        grid-row: span 3;
        grid-template-rows: subgrid;
    }
}

/* A field that should run the full width of the form (long remarks, a
   tracking number with a preview underneath). */
.ux-formgrid > .span-2 { grid-column: span 2; }
.ux-formgrid > .span-all { grid-column: 1 / -1; }

@media (max-width: 700px) {
    /* One column: spans would overflow the grid. */
    .ux-formgrid > .span-2 { grid-column: auto; }
}

/* ------------------------------------------------------------
   6. Sortable columns

   Problem: no list in the app could be sorted. To find the biggest
   overdue receivable you exported to CSV and sorted in Excel.
   ------------------------------------------------------------ */

th.ux-sort { padding: 0; }
th.ux-sort > a {
    display: flex; align-items: center; gap: var(--sp-3);
    justify-content: inherit;
    padding: var(--sp-4) var(--cell-pad-x);
    color: inherit; text-decoration: none;
    font-weight: inherit;
}
th.ux-sort > a:hover { background: rgba(31, 111, 235, 0.10); }
th.ux-sort--num > a { justify-content: flex-end; }
th.ux-sort__arrow { opacity: 0.28; font-size: 9px; }
th.ux-sort[aria-sort] .ux-sort__arrow { opacity: 1; color: var(--text-link); }
th.ux-sort[aria-sort] > a { background: rgba(31, 111, 235, 0.08); }

/* ------------------------------------------------------------
   7. Empty states

   Problem: every empty table said "No documents found." — a dead end
   that does not distinguish "you have no invoices yet" (create one)
   from "your filters match nothing" (clear them). Different problem,
   different next action.
   ------------------------------------------------------------ */

.ux-empty { padding: var(--sp-10) var(--sp-7); text-align: center; }
.ux-empty__glyph { font-size: 30px; opacity: 0.35; margin-bottom: var(--sp-4); }
.ux-empty__title { font-size: var(--fs-lead); font-weight: var(--fw-semi); color: var(--text-soft); }
.ux-empty__hint { font-size: var(--fs-small); color: var(--text-muted); margin: var(--sp-3) auto var(--sp-6); max-width: 46ch; }
.ux-empty__actions { display: flex; gap: var(--sp-4); justify-content: center; flex-wrap: wrap; }

/* ------------------------------------------------------------
   8. Bulk action bar

   Problem: the approvals queue made you approve one document at a
   time, each one a full page reload. A supervisor clearing 30 documents
   on Monday morning did 30 round trips.
   ------------------------------------------------------------ */

.ux-bulkbar {
    position: sticky;
    bottom: var(--sp-5);
    z-index: var(--z-sticky);
    display: none;
    align-items: center;
    gap: var(--sp-5);
    margin: var(--sp-5) 0 0;
    padding: var(--sp-4) var(--sp-6);
    background: rgba(23, 42, 71, 0.96);
    color: #fff;
    border-radius: var(--r-md);
    box-shadow: var(--el-3);
    font-size: var(--fs-body);
}
.ux-bulkbar[data-visible="true"] { display: flex; animation: uxToastIn var(--dur) var(--ease); }
.ux-bulkbar__count { font-weight: var(--fw-semi); }
.ux-bulkbar__spacer { margin-left: auto; }
.ux-bulkbar .btn-secondary { background: rgba(255, 255, 255, 0.15); border-color: rgba(255, 255, 255, 0.35); color: #fff; text-shadow: none; }
.ux-bulkbar .btn-secondary:hover { background: rgba(255, 255, 255, 0.26); color: #fff; }

/* ------------------------------------------------------------
   9. Pending / loading feedback

   Problem: clicking Approve produced no visible change until the new
   page painted. On a slow link people clicked twice, which is how you
   get duplicate payments. Now every in-flight request marks its trigger
   busy and disables it.
   ------------------------------------------------------------ */

.htmx-request.ux-busy,
[aria-busy="true"] { position: relative; pointer-events: none; opacity: 0.72; }

.htmx-request .ux-spin,
.ux-busy .ux-spin { display: inline-block; }
.ux-spin {
    display: none;
    width: 12px; height: 12px;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: uxSpin 640ms linear infinite;
    vertical-align: -1px;
}
@keyframes uxSpin { to { transform: rotate(360deg); } }

/* Row-level progress while a workflow action is in flight. */
tr.htmx-request > td { opacity: 0.55; }
tr.htmx-request > td:first-child { box-shadow: inset 3px 0 0 var(--text-link); }

/* Top-of-page progress bar for any HTMX navigation. */
.ux-progress {
    position: fixed; top: 0; left: 0;
    height: 2px; width: 0;
    background: var(--text-link);
    z-index: var(--z-palette);
    transition: width 200ms var(--ease), opacity var(--dur);
    opacity: 0;
}
.ux-progress[data-active="true"] { opacity: 1; }

/* Skeleton rows: reserve the layout so refreshing a KPI block does not
   make the page jump under the pointer. */
.ux-skeleton {
    background: linear-gradient(90deg, rgba(255,255,255,0.35) 25%, rgba(255,255,255,0.7) 37%, rgba(255,255,255,0.35) 63%);
    background-size: 400% 100%;
    animation: uxShimmer 1.3s ease infinite;
    border-radius: var(--r-sm);
    color: transparent !important;
}
@keyframes uxShimmer { from { background-position: 100% 0; } to { background-position: 0 0; } }

/* ------------------------------------------------------------
   10. Sticky action bar for long forms

   Problem: the invoice/bill form put Save at the bottom, below a
   line-item table that can run past a screen. You scrolled to save,
   then scrolled back to check what you saved.
   ------------------------------------------------------------ */

.ux-actionbar {
    position: sticky;
    bottom: 0;
    z-index: var(--z-sticky);
    display: flex; flex-wrap: wrap;
    align-items: center; gap: var(--sp-5);
    margin: 0;
    padding: var(--sp-5) var(--sp-6);
    background: rgba(244, 248, 252, 0.97);
    backdrop-filter: blur(12px);
    border-top: 1px solid var(--aero-glass-border);
    box-shadow: 0 -6px 18px rgba(16, 32, 56, 0.10);
    border-radius: 0 0 var(--r-lg) var(--r-lg);
}
/* When the bar sits *inside* a padded card body rather than as a direct
   child of the card, it has to break out of that padding to reach the
   card's edges. */
.card-body > .ux-actionbar,
.ux-section__body > .ux-actionbar {
    margin: var(--sp-6) calc(-1 * var(--sp-6)) calc(-1 * var(--sp-6));
}
.ux-actionbar__spacer { margin-left: auto; }

/* Running document total, live as you type. This number is the whole
   reason the screen exists; it should not require a save to see. */
.ux-total { display: flex; align-items: baseline; gap: var(--sp-4); }
.ux-total__label { font-size: var(--fs-small); color: var(--text-muted); }
.ux-total__value { font-size: var(--fs-h2); font-weight: var(--fw-semi); font-variant-numeric: tabular-nums; }
.ux-total__cur { font-size: var(--fs-small); color: var(--text-muted); }
.ux-total--flash { animation: uxFlash 500ms var(--ease); }
@keyframes uxFlash { 0% { background: var(--warn-tint); } 100% { background: transparent; } }

/* Unsaved-changes marker in the action bar. */
.ux-dirty {
    display: none; align-items: center; gap: var(--sp-3);
    font-size: var(--fs-small); color: #8a6100; font-weight: var(--fw-medium);
}
form[data-dirty="true"] .ux-dirty { display: inline-flex; }

/* ------------------------------------------------------------
   11. Combobox (type-ahead over a <select>)

   Problem: Customer/Supplier/Service were native <select>s with
   hundreds of options. Native selects only jump by first letter, so
   picking "Zubair Petroleum Services" meant scrolling. Progressive
   enhancement: the real <select> stays in the DOM as the form value, so
   with JS off the form still works exactly as before.
   ------------------------------------------------------------ */

.ux-combo { position: relative; }
.ux-combo__list {
    position: absolute; left: 0; right: 0; top: calc(100% + 2px);
    z-index: var(--z-dropdown);
    max-height: 260px; overflow-y: auto;
    background: #fff;
    border: 1px solid #9fb0c8;
    border-radius: var(--r-md);
    box-shadow: var(--el-3);
    padding: var(--sp-2);
    display: none;
}
.ux-combo__list[data-open="true"] { display: block; }
.ux-combo__opt {
    padding: var(--sp-3) var(--sp-4);
    border-radius: var(--r-sm);
    font-size: var(--fs-body);
    cursor: pointer;
}
.ux-combo__opt[aria-selected="true"] { background: rgba(31, 111, 235, 0.14); }
.ux-combo__opt mark { background: #ffe9a8; border-radius: 2px; }
.ux-combo__none { padding: var(--sp-4); color: var(--text-muted); font-size: var(--fs-small); }

/* ------------------------------------------------------------
   12. KPI cards / stat blocks

   Problem: dashboard tiles were clickable <div onclick> — not focusable,
   not announced as links, no keyboard access. They are <a> now; this
   only styles them.
   ------------------------------------------------------------ */

.ux-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 0;
    padding: var(--sp-4) var(--sp-5);
}
.ux-stat {
    position: relative;
    display: block;
    padding: var(--sp-5) var(--sp-6);
    text-align: center;
    text-decoration: none;
    color: inherit;
    border-radius: var(--r-md);
    transition: background var(--dur) var(--ease);
}
.ux-stat + .ux-stat { box-shadow: inset 1px 0 0 rgba(11, 61, 145, 0.12); }
.ux-stat:hover { background: rgba(255, 255, 255, 0.5); }
.ux-stat:hover .ux-stat__drill { opacity: 1; }

.ux-stat__value { font-size: var(--fs-kpi); font-weight: var(--fw-semi); color: #1e293b; font-variant-numeric: tabular-nums; line-height: 1.15; }
.ux-stat__second { font-size: var(--fs-body); font-weight: var(--fw-semi); color: var(--text-soft); font-variant-numeric: tabular-nums; }
.ux-stat__label { font-size: var(--fs-small); color: var(--text-muted); font-weight: var(--fw-medium); margin-top: var(--sp-2); }
.ux-stat__sub { font-size: var(--fs-micro); color: var(--text-muted); margin-top: var(--sp-3); }
.ux-stat__drill { position: absolute; top: var(--sp-3); right: var(--sp-4); font-size: var(--fs-micro); color: var(--text-link); opacity: 0; transition: opacity var(--dur); }

.ux-stat--money .ux-stat__value { color: var(--money-in); }
.ux-stat--neg   .ux-stat__value { color: var(--money-out); }
.ux-stat--alert .ux-stat__value { color: var(--money-out); }
.ux-stat--zero  .ux-stat__value { color: var(--text-muted); font-weight: var(--fw-medium); }

/* Period-over-period delta: a number without a trend is hard to judge. */
.ux-delta { display: inline-flex; align-items: center; gap: 3px; font-size: var(--fs-micro); font-weight: var(--fw-semi); }
.ux-delta--up   { color: var(--money-in); }
.ux-delta--down { color: var(--money-out); }
.ux-delta--flat { color: var(--text-muted); }

/* Wrapping variant. `.ux-stats` draws its dividers with `.ux-stat + .ux-stat`,
   which is fine on one row and wrong the moment tiles wrap: the first tile of
   every new row gets a left divider hanging off nothing. In the narrower
   dashboard columns tiles always wrap, so here the divider is replaced by a
   gap and a faint surface, which survives any number of rows. */
.ux-stats--grid {
    /* Narrower track than the default 180px: in a half-width column four
       money tiles at 180px wrap 3 + 1, leaving the fourth alone beside a
       gap. 132px fits all four on one row and still fits six across the
       wider column. */
    grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
    gap: var(--sp-4); padding: var(--sp-5);
}
/* Money tiles need their own track width. A figure like $25,150,000.00 at
   --fs-kpi wants ~200px, and in a 132px track it simply runs out of the
   tile. Two-up is the honest fit for four of them in a half-width column. */
.ux-stats--money { grid-template-columns: repeat(auto-fit, minmax(228px, 1fr)); }
.ux-stats--grid .ux-stat { background: rgba(255, 255, 255, 0.40); border: 1px solid rgba(255, 255, 255, 0.66); }
.ux-stats--grid .ux-stat + .ux-stat { box-shadow: none; }
.ux-stats--grid .ux-stat:hover { background: rgba(255, 255, 255, 0.72); }

/* ------------------------------------------------------------
   12b. Charts

   The SVG comes from `bar_chart` / `stack_bar` in ux_tags.py and carries
   tone classes, never fills — so the palette stays in tokens.css and the
   high-contrast override at the bottom of that file keeps working.

   One custom property drives both the SVG `fill` and the legend swatch's
   `background`, so a tone is defined once rather than twice in two
   different properties that can drift apart.
   ------------------------------------------------------------ */

.ux-tone--in      { --chart-tone: var(--money-in); }
.ux-tone--out     { --chart-tone: var(--money-out); }
.ux-tone--accent  { --chart-tone: var(--text-link); }
.ux-tone--neutral { --chart-tone: var(--neutral-border); }
.ux-tone--warn    { --chart-tone: var(--warn-border); }
.ux-tone--bad     { --chart-tone: var(--bad-border); }

/* Height is set here, not by the viewBox: a chart that keeps its aspect
   ratio gets taller every time the column gets wider, which is how a trend
   ends up as 250px of empty plot. */
.ux-chart { display: block; }
.ux-chart__plot { display: block; width: 100%; height: var(--chart-h, 116px); }
.ux-chart--short { --chart-h: 84px; }
.ux-chart__bar, .ux-chart__seg { fill: var(--chart-tone, var(--neutral-border)); }
.ux-chart__axis { stroke: rgba(11, 61, 145, 0.28); stroke-width: 1; }

/* One equal cell per bar, so a label sits under the group it names. */
.ux-chart__ticks {
    display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
    margin-top: var(--sp-3);
}
/* Labels overflow their cell rather than being clipped by it. With twenty
   buckets a cell is ~25px and "Jul 25" would be cut to "Jul :" — but the
   cells either side are blank (the labels are thinned), so there is nothing
   to collide with. */
.ux-chart__ticks span {
    font-size: var(--fs-micro); color: var(--text-muted); text-align: center;
    white-space: nowrap; overflow: visible;
}
/* The mix bar is a proportion, not a shape: it may stretch freely. */
.ux-chart--stack {
    display: block; width: 100%; height: 14px; border-radius: var(--r-pill);
    overflow: hidden; box-shadow: inset 0 0 0 1px rgba(11, 61, 145, 0.10);
}

/* Two trends abreast where the column allows it. Stacking one chart per
   currency made the trend column twice as tall as the tiles beside it,
   which put the empty space back — just in the other column. */
.ux-chartgrid {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
    gap: var(--sp-5) var(--sp-7);
}

.ux-chartcard { padding: var(--sp-5) var(--sp-7) var(--sp-6); }
.ux-chartcard__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-3) var(--sp-5); margin-bottom: var(--sp-5); }
.ux-chartcard__fig { font-size: var(--fs-h2); font-weight: var(--fw-semi); font-variant-numeric: tabular-nums; }
.ux-chartcard__meta { font-size: var(--fs-small); color: var(--text-muted); }
/* A second, smaller figure under the main one — the mix bar under the trend.
   Needs its own rule because a <figcaption> butted straight up against the
   tick labels above it reads as part of the same axis. */
.ux-chartcard__aside {
    margin-top: var(--sp-7); padding-top: var(--sp-5);
    border-top: 1px solid rgba(11, 61, 145, 0.10);
}
.ux-chartcard__aside .ux-chart--stack { margin-top: var(--sp-3); }

.ux-chart-legend {
    display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-6);
    margin-top: var(--sp-5); font-size: var(--fs-small); color: var(--text-soft);
}
.ux-chart-legend__item { display: inline-flex; align-items: center; gap: var(--sp-3); }
.ux-chart-legend__item b { font-variant-numeric: tabular-nums; font-weight: var(--fw-semi); }
.ux-key {
    display: inline-block; width: 10px; height: 10px; flex: none;
    border-radius: 2px; background: var(--chart-tone, var(--neutral-border));
}

/* Said out loud rather than drawn: a chart of two data points is a lie
   with axes on it. */
.ux-chart-empty {
    display: flex; align-items: center; justify-content: center;
    min-height: 96px; padding: var(--sp-6);
    font-size: var(--fs-small); color: var(--text-muted); text-align: center;
}

/* ------------------------------------------------------------
   12c. Dashboard column layout

   A single full-width column was the real source of the dashboard's empty
   space: `.ux-stats` uses `auto-fit`, so empty tracks collapse and three
   tiles stretch to fill a 1600px page — a 500px-wide box around one digit.
   Pairing each tile group with something that wants the width (a trend, a
   queue) fixes the cause rather than shrinking the symptom.
   ------------------------------------------------------------ */

/* Narrow column first: the headline figures are what someone came for, and
   the trend that explains them reads second. Vertical spacing stays on the
   sections' own margin so the rhythm is unchanged when this collapses. */
.ux-dashgrid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
    gap: 0 var(--sp-7);
    align-items: start;
}
.ux-dashgrid--even { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
/* Below this the two columns are narrower than a comfortable reading
   measure, and stacking beats cramming. */
@media (max-width: 1180px) {
    .ux-dashgrid, .ux-dashgrid--even { grid-template-columns: minmax(0, 1fr); }
}

/* ------------------------------------------------------------
   13. Section shell (used by dashboard + detail screens)
   ------------------------------------------------------------ */

.ux-section {
    background: rgba(255, 255, 255, 0.6);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border: 1px solid var(--aero-glass-border);
    border-radius: var(--r-lg);
    margin-bottom: var(--sp-7);
    box-shadow: var(--el-2), var(--el-inset-top);
}
.ux-section__head {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-4);
    padding: var(--sp-5) var(--sp-7);
    background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0.4) 100%);
    border-bottom: 1px solid rgba(0, 0, 0, 0.05);
    border-radius: var(--r-lg) var(--r-lg) 0 0;
}
.ux-section__head h2 { font-size: var(--fs-lead); font-weight: var(--fw-semi); }
.ux-section__hint { font-size: var(--fs-small); color: var(--text-muted); font-weight: var(--fw-normal); }
.ux-section__spacer { margin-left: auto; }
.ux-section__body { padding: var(--sp-6) var(--sp-7); }
.ux-section__body--flush { padding: 0; }

/* ------------------------------------------------------------
   14. Peek panel — read a document without leaving the queue

   Problem: the only way to see an invoice's line items from the
   approvals queue was to open the print view, which navigated away and
   lost your place in the list. Approvers were opening tabs to cope.
   ------------------------------------------------------------ */

.ux-peek { border-top: 2px solid var(--text-link); background: rgba(244, 248, 253, 0.96); }
.ux-peek__inner { padding: var(--sp-6) var(--sp-7); }
.ux-peek__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: var(--sp-5); margin-bottom: var(--sp-5); }
.ux-peek__k { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.4px; color: var(--text-muted); }
.ux-peek__v { font-size: var(--fs-body); font-weight: var(--fw-semi); }
.ux-peek__close { margin-left: auto; }

/* Audit trail as a vertical timeline: who did what, in order. */
.ux-trail { list-style: none; margin: 0; padding: 0 0 0 var(--sp-6); border-left: 2px solid rgba(11, 61, 145, 0.18); }
.ux-trail li { position: relative; padding: 0 0 var(--sp-5); font-size: var(--fs-small); }
.ux-trail li::before {
    content: ""; position: absolute; left: calc(-1 * var(--sp-6) - 5px); top: 5px;
    width: 8px; height: 8px; border-radius: 50%;
    background: var(--text-link); box-shadow: 0 0 0 2px #fff;
}
.ux-trail li[data-kind="reject"]::before { background: var(--bad-border); }
.ux-trail li[data-kind="approve"]::before { background: var(--ok-border); }
.ux-trail__who { font-weight: var(--fw-semi); }
.ux-trail__when { color: var(--text-muted); }

/* ------------------------------------------------------------
   15. Ageing / SLA signal

   Problem: the queue showed "3 days ago" as plain grey text, so an
   item sitting for three weeks looked exactly like one from lunchtime.
   ------------------------------------------------------------ */

.ux-age { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-small); }
.ux-age__dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.ux-age--fresh .ux-age__dot { background: var(--ok-border); }
.ux-age--warn  .ux-age__dot { background: var(--warn-border); }
.ux-age--late  .ux-age__dot { background: var(--bad-border); }
.ux-age--late  { color: var(--bad-fg); font-weight: var(--fw-semi); }

/* ------------------------------------------------------------
   16. Keyboard shortcut help sheet (?)
   ------------------------------------------------------------ */

.ux-keys { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--sp-7); }
.ux-keys h3 { font-size: var(--fs-small); text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: var(--sp-4); }
.ux-keys dl { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-3) var(--sp-5); margin: 0; align-items: center; }
.ux-keys dt { font-size: var(--fs-small); color: var(--text-soft); }
.ux-keys dd { margin: 0; text-align: right; }

/* ------------------------------------------------------------
   17. Misc utilities
   ------------------------------------------------------------ */

.ux-sr {
    position: absolute !important;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0);
    white-space: nowrap; border: 0;
}

/* A dotted underline that signals "there is more detail on hover/focus"
   — replaces bare title="" attributes, which touch users never see. */
.ux-hint {
    border-bottom: 1px dotted var(--text-muted);
    cursor: help;
}

.ux-stack { display: flex; flex-direction: column; gap: var(--sp-5); }
.ux-row   { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-4); }
.ux-grow  { flex: 1; min-width: 0; }
.ux-nowrap { white-space: nowrap; }
.ux-mono { font-variant-numeric: tabular-nums; }

/* Split a long screen into a scrollable data area with a fixed header. */
.ux-tablewrap { overflow: auto; max-height: min(68vh, 760px); }
.ux-tablewrap--tall { max-height: min(78vh, 1000px); }

@media (max-width: 992px) {
    .ux-toasts { left: var(--sp-5); right: var(--sp-5); max-width: none; }
    .ux-palette { padding-top: var(--sp-6); }
    .ux-actionbar { gap: var(--sp-4); }
    .ux-stat + .ux-stat { box-shadow: none; border-top: 1px solid rgba(11, 61, 145, 0.12); }
}
