﻿/*
 * Space ERP — application shell and shared components.
 * Tokens live in brand.css; this file is layout and component behaviour.
 */

/* ---------------------------------------------------------------------------
   Shell: fixed sidebar on desktop, off-canvas drawer below lg.
   The sidebar surface is --st-surface, never a navy fill, in either theme.
   --------------------------------------------------------------------------- */
.st-shell {
    display: flex;
    min-height: 100vh;
}

.st-sidebar {
    width: var(--st-sidebar-width);
    flex-shrink: 0;
    background: var(--st-surface);
    border-right: 1px solid var(--st-border);
    display: flex;
    flex-direction: column;
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    z-index: 1030;
    overflow: hidden;           /* the nav scrolls, not the sidebar — see .st-nav */
}

/* The brand is a header, so it holds its height and stays put: without flex-shrink it is the
   only item the flex line can squeeze once every nav group is expanded, and the logo clips. */
.st-sidebar-brand {
    height: var(--st-topbar-height);
    flex-shrink: 0;
    display: flex;
    align-items: center;
    padding-inline: 1rem;
    border-bottom: 1px solid var(--st-border);
}

.st-sidebar-brand img {
    height: 26px;
    width: auto;
}

/* The wrapper the pins form re-renders through (hx-target="#side-nav"). It sits between the sidebar
   and the nav, so it has to be a flex column that passes the height constraint through: as a plain
   block it grew to the menu's full height, the nav's own overflow rule never applied, and the
   sidebar's overflow:hidden clipped everything below the fold with no way to reach it. */
#side-nav {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
}

/* min-height:0 defeats the automatic minimum size of a flex item, which would otherwise be the
   full height of an expanded menu and push the overflow back onto the brand. */
.st-nav {
    padding: 0.75rem 0.5rem;
    flex: 1;
    min-height: 0;
    overflow-y: auto;

    /* A thin bar that only shows while the pointer is over the menu. The default bar sat as a
       grey column down the whole sidebar and read as part of the layout. */
    scrollbar-width: thin;
    scrollbar-color: transparent transparent;
    transition: scrollbar-color var(--st-transition);
}

.st-nav:hover,
.st-nav:focus-within {
    scrollbar-color: var(--st-border) transparent;
}

/* WebKit equivalents for browsers that ignore scrollbar-color. */
.st-nav::-webkit-scrollbar {
    width: 6px;
}

.st-nav::-webkit-scrollbar-thumb {
    background: transparent;
    border-radius: 3px;
}

.st-nav:hover::-webkit-scrollbar-thumb,
.st-nav:focus-within::-webkit-scrollbar-thumb {
    background: var(--st-border);
}

/* Group heading. It is a button so it can collapse its section, but it must read as a
   quiet label — no border, no fill, nothing that looks pressable except on hover. */
.st-nav-group-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    width: 100%;
    background: none;
    border: 0;
    text-align: start;
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--st-text-muted);
    padding: 0.75rem 0.75rem 0.35rem;
    font-weight: 600;
}

.st-nav-group-toggle:hover {
    color: var(--st-accent);
}

.st-nav-group-chevron {
    font-size: 0.75rem;
    transition: transform var(--st-transition);
}

.st-nav-group-toggle[aria-expanded="false"] .st-nav-group-chevron {
    transform: rotate(-90deg);
}

/* A heading that does not fold: the pinned list has nothing to put away. Same voice as the
   group toggles so the two read as one menu. */
.st-nav-group-label {
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--st-text-muted);
    padding: 0.75rem 0.75rem 0.35rem;
    font-weight: 600;
}

.st-nav-link {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.5rem 0.75rem;
    border-radius: var(--st-radius);
    color: var(--st-text);
    text-decoration: none;
    font-size: 0.875rem;
    border-left: 3px solid transparent;
    transition: background-color var(--st-transition);
}

/* A link and its pin side by side. The link takes the width; the pin only appears when the row
   is hovered or either of them has focus, so thirty rows do not wear thirty pins. A pinned
   entry's pin stays visible, because that is how you can tell it is pinned. */
.st-nav-row {
    display: flex;
    align-items: stretch;
}

.st-nav-row .st-nav-link {
    flex: 1;
    min-width: 0;
}

.st-nav-pin {
    flex-shrink: 0;
    width: 2rem;
    background: none;
    border: 0;
    border-radius: var(--st-radius);
    color: var(--st-text-muted);
    font-size: 0.8125rem;
    opacity: 0;
    transition: opacity var(--st-transition), background-color var(--st-transition);
}

.st-nav-row:hover .st-nav-pin,
.st-nav-row:focus-within .st-nav-pin,
.st-nav-pin.is-pinned {
    opacity: 1;
}

.st-nav-pin:hover,
.st-nav-pin:focus-visible {
    color: var(--st-accent);
    background: var(--st-pearl-light);
}

/* Coarse pointers have no hover, so the pin is always offered there. */
@media (pointer: coarse) {
    .st-nav-pin {
        opacity: 1;
    }
}

.st-nav-link:hover {
    background: var(--st-pearl-light);
    color: var(--st-accent);
}

/* Selected item: gold indicator bar — the one place gold does structural work. */
.st-nav-link.active {
    background: var(--st-pearl-light);
    color: var(--st-accent);
    font-weight: 500;
    border-left-color: var(--st-gold);
}

.st-main {
    flex: 1;
    min-width: 0;               /* lets wide tables scroll instead of stretching the page */
    margin-inline-start: var(--st-sidebar-width);
    display: flex;
    flex-direction: column;
}

.st-topbar {
    height: var(--st-topbar-height);
    background: var(--st-surface);
    border-bottom: 1px solid var(--st-border);
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding-inline: 1rem;
    position: sticky;
    top: 0;
    z-index: 1020;
}

/* The 44px touch minimum below md (see the touch-target rule further down) leaves an anchor's
   line box at the top of the taller box, while a <button> centres its own content — which is why
   the alert bell rode above the icons beside it on a phone. The topbar's only anchor button is
   the bell; centring its content puts every tool on one line. */
.st-topbar a.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.st-content {
    padding: 1.25rem 1.5rem 3rem;
    flex: 1;
}

/* Content width is deliberately unconstrained: ERP users have wide monitors and
   data-dense tables should use them. */

.st-footer {
    padding: 0.75rem 1.5rem;
    border-top: 1px solid var(--st-border);
    background: var(--st-surface);
    color: var(--st-text-muted);
    font-size: 0.8125rem;
    text-align: center;
}

@media (max-width: 991.98px) {
    .st-sidebar {
        transform: translateX(-100%);
        transition: transform var(--st-transition);
        box-shadow: var(--st-shadow-lg);
    }

    .st-sidebar.show {
        transform: translateX(0);
    }

    .st-main {
        margin-inline-start: 0;
    }

    .st-content {
        padding: 1rem;
    }
}

.st-sidebar-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: rgb(10 23 45 / 35%);
    z-index: 1029;
}

.st-sidebar-backdrop.show {
    display: block;
}

/* ---------------------------------------------------------------------------
   Global search / command palette trigger
   --------------------------------------------------------------------------- */
.st-search-trigger {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-width: 220px;
    padding: 0.35rem 0.6rem;
    background: var(--st-bg-subtle);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius);
    color: var(--st-text-muted);
    font-size: 0.8125rem;
    cursor: pointer;
    transition: border-color var(--st-transition);
}

.st-search-trigger:hover {
    border-color: var(--st-border-strong);
}

/* A phone has no room for a 220px box beside the drawer button and five tools, and the fixed
   minimum was what pushed the alert bell and the account menu off the right of the screen —
   taking the rest of the page with them, since the topbar then set the width of the document.
   The trigger takes what is left instead, and the keyboard hint goes: there is no Ctrl key. */
@media (max-width: 767.98px) {
    .st-search-trigger {
        min-width: 0;
        flex: 1 1 auto;
        overflow: hidden;
    }

    .st-search-trigger .st-kbd {
        display: none;
    }

    /* Truncates rather than forcing the button wider than its share. */
    .st-search-trigger > span:not(.st-kbd) {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
}

.st-kbd {
    font-family: var(--st-font-mono);
    font-size: 0.6875rem;
    padding: 0.05rem 0.3rem;
    border: 1px solid var(--st-border-strong);
    border-bottom-width: 2px;
    border-radius: 3px;
    background: var(--st-surface);
    color: var(--st-text-muted);
}

/* ---------------------------------------------------------------------------
   Page header — one primary action, breadcrumbs above, status beside the title.
   --------------------------------------------------------------------------- */
.st-page-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1.25rem;
}

/* ---------------------------------------------------------------------------
   Alert severity — red urgent, amber warning, blue information.

   Icon, colour AND word together, every time. The shapes differ too (octagon,
   triangle, circle), so the three levels survive a greyscale print-out and a
   reader who does not see red and amber as different colours. Colours are the
   darker end of each hue deliberately: they sit on a white row and have to pass
   contrast as text, not just register as a splash.
   --------------------------------------------------------------------------- */
.st-severity {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    white-space: nowrap;
    font-weight: 500;
}

.st-severity .bi {
    font-size: 0.9375rem;
    line-height: 1;
}

.st-severity-critical {
    color: var(--st-danger);
}

.st-severity-warning {
    color: var(--st-warning-text);
}

.st-severity-info {
    color: var(--st-severity-info);
}

/* A screen with a second, quieter action beside the primary one. Wraps rather than squashing,
   so the pair never collapses into overlapping buttons on a narrow window. */
.st-header-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.st-breadcrumb {
    font-size: 0.8125rem;
    color: var(--st-text-muted);
    margin-bottom: 0.25rem;
}

.st-breadcrumb a {
    color: var(--st-text-muted);
    text-decoration: none;
}

.st-breadcrumb a:hover {
    color: var(--st-accent);
    text-decoration: underline;
}

.st-page-title {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    margin: 0;
}

/* A long table inside a detail card. Stands in for Bootstrap's .table-responsive where the table runs
   to a hundred lines, which a real order does: the card keeps its size and the reader scrolls the rows
   instead of the whole page, which otherwise carries the totals and the buttons off the top. The
   header stays put, the same bargain .st-table-wrapper makes below.

   Scrolls both ways, which is why it replaces .table-responsive rather than joining it. Declared
   before .st-table so a table wearing both keeps the grid's own header styling. */
.st-scroll-table {
    overflow: auto;
    max-height: min(32rem, calc(100vh - 16rem));
}

/* Opaque, because rows pass underneath. A border-bottom would scroll away with the row it is painted
   against, so the rule under the header is an inset shadow. */
.st-scroll-table thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--st-surface);
    box-shadow: inset 0 -1px 0 var(--st-border-strong);
}

/* The pivot layout on Purchase analysis: breakdown rows down the page, periods across it. Wide on
   purpose - a year of months is twenty-four figure columns - so it lives inside .st-scroll-table and
   the name column is pinned while the figures pass under it. A row of numbers whose name has
   scrolled out of sight is a row about nothing.

   Opaque backgrounds because cells really do pass underneath, and a z-index above the sticky header
   cells for the same reason: the name column and the header row cross at one corner, and whichever
   loses that argument shows the other's text through it. */
.st-pivot .st-pivot-name {
    position: sticky;
    left: 0;
    z-index: 3;
    background: var(--st-surface);
    /* Wraps rather than running on. Overflowing text is not clipped by its cell, so a nowrap name
       long enough to leave the pinned column paints straight over the barcode beside it - and the
       longest SKU names here are exactly the ones that do. Bounded so the column cannot grow to a
       width that pushes every figure off the screen instead. */
    max-width: 20rem;
}

.st-pivot thead .st-pivot-name,
.st-pivot tfoot .st-pivot-name {
    z-index: 4;
    background: var(--st-pearl-light);
}

/* Two header rows, so the second cannot also stick to the top or it would sit on the first. The
   height is fixed rather than measured for the same reason: the offset below has to be a number, and
   a number that disagrees with the row above it is a gap or an overlap on every screen. */
.st-pivot thead tr:first-child th {
    height: 2rem;
}

.st-pivot thead tr:nth-child(2) th {
    top: 2rem;
}

/* One rule where each period begins, so a reader following a row can see where one month ends. On
   the left edge of the pair rather than between its two figures, which belong together. */
.st-pivot .st-pivot-period {
    border-left: 1px solid var(--st-border-strong);
}

/* The period totals. A foot that scrolled away would leave the columns above it unanchored, which is
   the same bargain the header makes at the other end of the table. */
.st-pivot tfoot td,
.st-pivot tfoot th {
    position: sticky;
    bottom: 0;
    background: var(--st-pearl-light);
    border-top: 1px solid var(--st-border-strong);
    padding: 0.5rem 0.75rem;
}

/* A grid of ticks: one row per class of article, one column per document (the supplier screen's
   coverage table). Narrow and dense on purpose — it sits beside a form in half a row, and reading it is
   a matter of scanning a column for a gap rather than reading any single cell. */
.st-coverage-table {
    font-size: 0.85rem;
}

.st-coverage-table th,
.st-coverage-table td {
    padding: 0.3rem 0.4rem;
    vertical-align: middle;
}

/* The document headings stack under a narrow card, so they are allowed to wrap while the article
   names beside them are not: a wrapped article name doubles the height of every row. */
.st-coverage-table thead th {
    font-size: 0.78rem;
    line-height: 1.15;
    white-space: normal;
}

.st-coverage-table tbody th {
    white-space: nowrap;
}

/* ---------------------------------------------------------------------------
   Data tables — sticky header, zebra rows, whole row clickable.
   --------------------------------------------------------------------------- */
.st-table-wrapper {
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius);
    overflow: auto;             /* wide tables scroll here, never the page body */
    max-height: calc(100vh - 260px);
}

.st-table {
    width: 100%;
    margin: 0;
    border-collapse: separate;
    border-spacing: 0;
    font-size: 0.875rem;
}

.st-table thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--st-pearl-light);
    border-bottom: 1px solid var(--st-border-strong);
    padding: 0.5rem 0.75rem;
    font-weight: 600;
    font-size: 0.8125rem;
    color: var(--st-accent);
    white-space: nowrap;
    text-align: left;
}

.st-table tbody td {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--st-border);
    vertical-align: middle;
}

.st-table tbody tr:hover {
    background: var(--st-pearl-light);
}

.st-table tbody tr[data-href] {
    cursor: pointer;
}

/* Zero matches: the header and filter row above stay visible, so this replaces only the data
   rows. Undo the ordinary cell padding/border and the row hover so it reads as one empty area,
   not a one-row table. */
.st-table tbody tr.st-empty-row:hover {
    background: none;
}

.st-table tbody .st-empty-cell {
    padding: 0;
    border-bottom: none;
}

/* Compact density for users who want more rows on screen. */
.st-table.st-density-compact tbody td,
.st-table.st-density-compact thead th {
    padding: 0.25rem 0.5rem;
    font-size: 0.8125rem;
}

/* Below md the same data renders as cards — a 12-column table is unusable on a phone. */
.st-card-list {
    display: none;
}

@media (max-width: 767.98px) {
    .st-table-wrapper {
        display: none;
    }

    .st-card-list {
        display: grid;
        gap: 0.5rem;
    }
}

.st-record-card {
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius);
    padding: 0.75rem;
    text-decoration: none;
    color: inherit;
    display: block;
}

.st-record-card:active {
    background: var(--st-pearl-light);
}

/* ---------------------------------------------------------------------------
   The five states every screen must handle.
   --------------------------------------------------------------------------- */

/* 1. Loading — a skeleton shaped like the real content, never a blank page. */
.st-skeleton {
    background: linear-gradient(90deg, var(--st-pearl-light) 25%, var(--st-pearl) 37%, var(--st-pearl-light) 63%);
    background-size: 400% 100%;
    animation: st-shimmer 1.4s ease-in-out infinite;
    border-radius: var(--st-radius);
}

@keyframes st-shimmer {
    0% { background-position: 100% 50%; }
    100% { background-position: 0 50%; }
}

.st-skeleton-row {
    height: 2.25rem;
    margin-bottom: 0.4rem;
}

/* 2. Empty — an explanation and the action that fixes it. Empty states are onboarding. */
.st-empty {
    text-align: center;
    padding: 3rem 1.5rem;
    color: var(--st-text-muted);
}

.st-empty-icon {
    font-size: 2rem;
    color: var(--st-border-strong);
    margin-bottom: 0.75rem;
}

.st-empty-title {
    font-family: var(--st-font-heading);
    color: var(--st-accent);
    margin-bottom: 0.25rem;
}

/* 3. Error — what went wrong plus a way to retry. */
.st-error-panel {
    border: 1px solid var(--st-danger);
    background: var(--st-danger-bg);
    border-radius: var(--st-radius);
    padding: 1rem 1.25rem;
    color: var(--st-danger);
}

/* 4. Stale — keep showing data, say it may be out of date. Never wipe the screen. */
.st-stale-banner {
    background: var(--st-warning-bg);
    color: var(--st-warning);
    border-bottom: 1px solid currentcolor;
    padding: 0.4rem 1rem;
    font-size: 0.8125rem;
    text-align: center;
}

/* 5. Success — a toast, bottom-right, with undo where the action was destructive. */
.st-toast-container {
    position: fixed;
    bottom: 1rem;
    right: 1rem;
    z-index: 1080;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    max-width: min(24rem, calc(100vw - 2rem));
}

.st-toast {
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-left: 3px solid var(--st-accent);
    border-radius: var(--st-radius);
    box-shadow: var(--st-shadow-lg);
    padding: 0.75rem 1rem;
    display: flex;
    align-items: flex-start;
    gap: 0.625rem;
    font-size: 0.875rem;
}

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

/* ---------------------------------------------------------------------------
   Forms — labels above inputs, errors under the field they belong to.
   --------------------------------------------------------------------------- */
.st-form-section {
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius);
    padding: 1.25rem;
    margin-bottom: 1rem;
}

.st-form-section-title {
    font-family: var(--st-font-heading);
    font-size: 1rem;
    color: var(--st-accent);
    margin-bottom: 1rem;
    padding-bottom: 0.5rem;
    border-bottom: 1px solid var(--st-border);
}

.st-label {
    display: block;
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--st-text);
    margin-bottom: 0.25rem;
}

.st-required::after {
    content: " *";
    color: var(--st-danger);
}

.form-control,
.form-select {
    border-color: var(--st-border-strong);
    border-radius: var(--st-radius);
    font-size: 0.875rem;
    padding: 0.4rem 0.6rem;
}

.form-control:focus,
.form-select:focus {
    border-color: var(--st-accent);
    box-shadow: 0 0 0 3px rgb(10 23 45 / 8%);
}

/* The "Choose file" half of a file input. Bootstrap fills it with --bs-tertiary-bg, which is a
   fixed light grey we do not override, and colours its label with --bs-body-color, which we do —
   so on the dark theme it came out near-white text on near-white grey. Both sides are set from
   our own tokens here so the pair moves together with the theme. Styled on the button rather than
   by redefining --bs-tertiary-bg, which also backs table striping and other Bootstrap parts. */
.form-control::file-selector-button {
    background: var(--st-bg-subtle);
    color: var(--st-accent);
    border: 0;
    border-inline-end: 1px solid var(--st-border-strong);
    font-family: inherit;
    font-weight: 500;
    transition: background-color var(--st-transition);
}

.form-control:hover::file-selector-button {
    background: var(--st-pearl);
}

.st-field-error {
    color: var(--st-danger);
    font-size: 0.8125rem;
    margin-top: 0.25rem;
}

/* Touch targets stay finger-friendly on phones (warehouse staff use tablets). */
@media (max-width: 767.98px) {
    .form-control,
    .form-select,
    .btn {
        min-height: 44px;
    }
}

/* ---------------------------------------------------------------------------
   Login — brand panel beside the sign-in card, stacking on small screens.
   --------------------------------------------------------------------------- */
.st-login {
    min-height: 100vh;
    display: grid;
    grid-template-columns: 1.1fr 1fr;
    background: var(--st-bg);
}

.st-login-brand {
    background: var(--st-pearl-light);
    border-right: 1px solid var(--st-border);
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 3rem clamp(2rem, 6vw, 6rem);
}

.st-login-brand img {
    max-width: 320px;
    width: 100%;
    margin-bottom: 2rem;
}

/* This mark (logo-tagline-navy.png) has no separate light-on-dark export the way the sidebar
   logo does — it is single-colour navy artwork, so recolouring it to white with a filter is the
   same treatment the brand kit already sanctions elsewhere (logo-horizontal-white.png), just
   applied at render time instead of as a second exported file. */
[data-theme="dark"] .st-login-brand img {
    filter: brightness(0) invert(1);
}

.st-login-lede {
    font-family: var(--st-font-display);
    font-size: clamp(1.5rem, 2.6vw, 2.25rem);
    line-height: 1.25;
    color: var(--st-accent);
    margin-bottom: 1rem;
}

/*
 * Value points. Separated from the paragraph above by a hairline rule so they read as a
 * considered list rather than marketing filler, with the ticks in brand gold.
 */
.st-login-points {
    list-style: none;
    padding: 0;
    margin: 2rem 0 0;
    padding-top: 1.5rem;
    border-top: 1px solid var(--st-border);
    color: var(--st-text-muted);
    font-size: 0.9375rem;
}

.st-login-points li {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    padding: 0.35rem 0;
}

.st-login-points .bi {
    color: var(--st-gold);
    font-size: 0.875rem;
}

.st-login-form {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2rem 1.5rem;
}

.st-login-card {
    width: 100%;
    max-width: 24rem;
}

.st-login-card h1 {
    font-size: 1.375rem;
    margin-bottom: 1.25rem;
}

.st-login-divider {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin: 1.25rem 0;
    color: var(--st-text-muted);
    font-size: 0.8125rem;
}

.st-login-divider::before,
.st-login-divider::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--st-border);
}

/* Language switch on the anonymous screens: a quiet pill, top-right, above the two panels. */
.st-login-lang {
    position: absolute;
    top: 1rem;
    right: 1rem;
    z-index: 2;
    display: inline-flex;
    gap: 0.25rem;
    padding: 0.2rem;
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-radius: 999px;
}

.st-login-lang-btn {
    border: 0;
    background: transparent;
    color: var(--st-text-muted);
    font-size: 0.8125rem;
    line-height: 1;
    padding: 0.35rem 0.75rem;
    border-radius: 999px;
    cursor: pointer;
}

.st-login-lang-btn:hover {
    color: var(--st-accent);
}

.st-login-lang-btn.is-active {
    background: var(--st-navy);
    color: var(--st-text-inverse);
    font-weight: 600;
}

/* The login page honours the theme cookie too, and a navy pill on the dark page background has
   almost no silhouette — same inversion as .btn-primary. */
:root[data-theme="dark"] .st-login-lang-btn.is-active {
    background: var(--st-accent);
    color: var(--st-navy);
}

/* Theme switch on the anonymous screens: mirrors .st-login-lang but sits top-left, since the
   language pill already owns top-right. */
.st-login-theme {
    position: absolute;
    top: 1rem;
    left: 1rem;
    z-index: 2;
}

.st-login-theme-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border: 1px solid var(--st-border);
    border-radius: 999px;
    background: var(--st-surface);
    color: var(--st-text-muted);
    cursor: pointer;
}

.st-login-theme-btn:hover {
    color: var(--st-accent);
    border-color: var(--st-accent);
}

@media (max-width: 991.98px) {
    .st-login {
        grid-template-columns: 1fr;
    }

    .st-login-brand {
        border-right: 0;
        border-bottom: 1px solid var(--st-border);
        padding: 2rem 1.5rem;
        text-align: center;
        align-items: center;
    }

    .st-login-brand img {
        max-width: 220px;
        margin-bottom: 1rem;
    }

    .st-login-points {
        display: none;      /* on a phone the sign-in card is what matters */
    }
}

/* ---------------------------------------------------------------------------
   Help drawer — opens the guide page for the CURRENT screen, not a generic index.
   --------------------------------------------------------------------------- */
.st-help-drawer {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: min(30rem, 100vw);
    background: var(--st-surface);
    border-left: 1px solid var(--st-border);
    box-shadow: var(--st-shadow-lg);
    z-index: 1070;
    transform: translateX(100%);
    transition: transform var(--st-transition);
    display: flex;
    flex-direction: column;
}

.st-help-drawer.show {
    transform: translateX(0);
}

.st-help-header {
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--st-border);
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.st-help-body {
    padding: 1rem 1.25rem 2rem;
    overflow-y: auto;
    font-size: 0.9375rem;
}

.st-help-body h1,
.st-help-body h2 {
    font-size: 1.0625rem;
    margin-top: 1.25rem;
}

.st-help-body kbd {
    font-family: var(--st-font-mono);
    font-size: 0.75rem;
    background: var(--st-pearl-light);
    border: 1px solid var(--st-border-strong);
    border-radius: 3px;
    padding: 0.05rem 0.3rem;
}

/* ---------------------------------------------------------------------------
   Assistant drawer — the same mechanics as the help drawer beside it, and deliberately not the
   same width. An answer carries a table of the records it leant on, and 30rem leaves about 444px
   of content for five columns. This is the one thing the two drawers do not share.
   --------------------------------------------------------------------------- */
.st-ai-drawer {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: min(46rem, 100vw);
    background: var(--st-surface);
    border-left: 1px solid var(--st-border);
    box-shadow: var(--st-shadow-lg);
    z-index: 1070;
    transform: translateX(100%);
    transition: transform var(--st-transition);
    display: flex;
    flex-direction: column;
}

.st-ai-drawer.show {
    transform: translateX(0);
}

.st-ai-header {
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--st-border);
    display: flex;
    align-items: center;
    justify-content: space-between;
}

/* The whole body scrolls, question box included: the question that produced an answer should still
   be readable beside it rather than pinned out of the way. */
.st-ai-body {
    padding: 1rem 1.25rem 2rem;
    overflow-y: auto;
}

/* ---------------------------------------------------------------------------
   The assistant's mark — the brand gold running into the brand navy, in exactly two places: the
   topbar button that opens the drawer, and beside every answer it gives. It appears nowhere else
   in the app on purpose. One gradient used twice reads as "this is the assistant speaking"; the
   same gradient sprinkled around the app would read as decoration and mean nothing.

   Gold into navy rather than the purple every AI feature reaches for: purple belongs to no part
   of this brand, and on a screen a distributor looks at all day it reads as something bolted on.
   --------------------------------------------------------------------------- */
.st-ai-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: var(--st-radius);
    background: linear-gradient(135deg, var(--st-gold) 0%, var(--st-navy) 100%);
    /* Fixed white, not --st-text-inverse: the glyph sits on the gradient, which is the same on
       both themes, so it must not follow the surface. */
    color: #fff;
    font-size: 0.8125rem;
    line-height: 1;
}

/* On dark the navy end is darker than the surface it sits on (#0A172D against --st-surface
   #171d26), so half the mark sinks into the topbar and it reads as a gold smudge. The lighter
   navy step keeps both ends of the gradient above the background. */
:root[data-theme="dark"] .st-ai-mark {
    background: linear-gradient(135deg, var(--st-gold) 0%, var(--st-navy-600) 100%);
}

/* The topbar trigger is the mark and nothing else — no button chrome around it, because the
   gradient is already the thing the eye goes to. */
.st-ai-trigger {
    background: none;
    border: 0;
    padding: 0.125rem;
    line-height: 1;
    border-radius: var(--st-radius);
}

.st-ai-trigger:hover .st-ai-mark {
    filter: brightness(1.08);
}

/* ---------------------------------------------------------------------------
   One exchange: the question bubbled on the right, the answer unbubbled on the left beside the
   mark.

   Only the question is bubbled, and that asymmetry is deliberate. An answer carries a five-column
   table of the records it leant on, and the 46rem drawer exists so that table fits; a bubble's
   padding and inset are width the table cannot spare.

   One exchange, not a scrollback: no conversation state is stored and nothing above is sent with
   the next question, so a visible history would imply the assistant can see what it cannot.
   --------------------------------------------------------------------------- */
.st-ai-turn {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.st-ai-question {
    align-self: flex-end;
    max-width: 80%;
    background: var(--st-pearl);
    /* The fill alone carries the bubble on light. On dark, pearl (#232a35) against the drawer
       surface (#171d26) is too near a match at this size and the shape stops reading, so the
       edge is drawn as well. One rule for both: in light the border is a shade off the fill and
       simply does not show. */
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-lg);
    padding: 0.5rem 0.75rem;
}

.st-ai-answer {
    display: flex;
    align-items: flex-start;
    gap: 0.625rem;
}

.st-ai-answer > .st-ai-mark {
    /* Sits on the answer's first line rather than above it. */
    margin-top: 0.125rem;
}

/* min-width:0 is what lets the answer body scroll a wide table instead of stretching this flex
   item to the table's width and pushing the drawer's own content sideways. */
.st-ai-answer-content {
    flex: 1 1 auto;
    min-width: 0;
}

/* Each block of a delivered answer fades in on the same timing, with no cascade down the page. A
   stagger would imply the parts arrived in order; they did not, the whole answer lands at once.
   Sequence becomes real in 3b, when the answer is streamed, and can be shown then.

   Reduced motion needs no rule here: brand.css flattens every animation in the app under that
   preference, this one included. */
.st-ai-turn > *,
.st-ai-answer-content > * {
    animation: st-ai-fade 180ms ease-out both;
}

@keyframes st-ai-fade {
    from { opacity: 0; transform: translateY(2px); }
    to { opacity: 1; transform: none; }
}

/* ---------------------------------------------------------------------------
   Suggested questions, offered until the first one is asked. An empty box answers nothing,
   because nobody knows what it can be asked — and unlike a placeholder, which shows one example
   and vanishes as soon as you type, these show the range and can be pressed.
   --------------------------------------------------------------------------- */
.st-ai-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin-top: 0.625rem;
}

/* display:flex above beats the browser's own [hidden] rule, so hiding these from script needs
   saying here or the chips stay on screen after the first ask. */
.st-ai-chips[hidden] {
    display: none;
}

.st-ai-chip {
    background: var(--st-surface);
    border: 1px solid var(--st-border-strong);
    border-radius: 999px;
    padding: 0.25rem 0.75rem;
    font-size: 0.8125rem;
    color: var(--st-text);
    text-align: start;
}

.st-ai-chip:hover {
    border-color: var(--st-gold);
    background: var(--st-pearl-light);
}

/* ---------------------------------------------------------------------------
   Recent questions, restored from this browser's localStorage across reloads and tabs. Plain rows
   rather than chips on purpose: chips are suggestions worth trying, this is history worth
   revisiting, and the two should not look interchangeable.
   --------------------------------------------------------------------------- */
.st-ai-history {
    display: flex;
    flex-direction: column;
    margin-top: 0.625rem;
    padding: 0.5rem 0.625rem;
    border: 1px solid var(--st-border);
    border-radius: 0.375rem;
}

.st-ai-history[hidden] {
    display: none;
}

.st-ai-history-label {
    margin: 0 0 0.25rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--st-text-muted);
}

.st-ai-history-list {
    display: flex;
    flex-direction: column;
}

.st-ai-history-item {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    background: none;
    border: none;
    border-radius: 0.25rem;
    padding: 0.25rem 0.375rem;
    font-size: 0.8125rem;
    color: var(--st-text);
    text-align: start;
}

.st-ai-history-item .bi {
    color: var(--st-text-muted);
}

.st-ai-history-item:hover {
    background: var(--st-pearl-light);
    border-color: var(--st-gold);
}

/* ---------------------------------------------------------------------------
   The wait before anything has arrived, shaped like the answer that is coming: three lines of
   text and a block where the table of records will be. A spinner spends that time saying "wait"
   where this says what for. Built from .st-skeleton so it shimmers with every other loading state
   in the app, and stands still under reduced motion.

   It never names a lookup, and it does not have to: the moment the first lookup or the first word
   arrives this goes, and what replaces it is the answer saying what it is doing. Guessing here
   would be inventing activity.
   --------------------------------------------------------------------------- */
.st-ai-ghost {
    display: none;
    align-items: flex-start;
    gap: 0.625rem;
}

.st-ai-ghost.is-busy {
    display: flex;
}

.st-ai-ghost-lines {
    flex: 1 1 auto;
    min-width: 0;
}

.st-ai-ghost-line {
    height: 0.75rem;
    margin-bottom: 0.5rem;
}

.st-ai-ghost-line:nth-child(2) { width: 92%; }
.st-ai-ghost-line:nth-child(3) { width: 64%; }

.st-ai-ghost-table {
    height: 5.5rem;
    margin-top: 1rem;
}

/* The previous answer goes while the next is on its way. Two answers on screen, one of them
   stale and neither labelled, is worse than the wait. */
.st-ai-ghost.is-busy ~ .st-ai-answer-slot {
    display: none;
}

/* ---------------------------------------------------------------------------
   The answer while it is still being written. Same column, same mark and same position as the
   card that replaces it, so nothing moves when it settles.

   The words are Markdown source at this point, so the line breaks the model wrote are the only
   layout there is: pre-wrap keeps them. What it must not do is look finished, which is what the
   caret is for.
   --------------------------------------------------------------------------- */
.st-ai-live-text {
    white-space: pre-wrap;
    margin-bottom: 0.75rem;
}

.st-ai-live-text:last-child {
    margin-bottom: 0;
}

.st-ai-live-text.is-writing::after {
    content: "";
    display: inline-block;
    width: 0.5rem;
    height: 0.9em;
    margin-left: 0.125rem;
    vertical-align: text-bottom;
    background: var(--st-gold);
    animation: st-ai-caret 1s step-end infinite;
}

@keyframes st-ai-caret {
    0%, 100% { opacity: 1; }
    50% { opacity: 0; }
}

/* A lookup, named as the model reaches for it. Quiet on purpose: it is the reason the answer is
   taking a moment, not part of the answer. */
.st-ai-lookup {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
    color: var(--st-text-muted);
    font-size: 0.8125rem;
}

.st-ai-lookup::before {
    content: "";
    flex-shrink: 0;
    width: 0.375rem;
    height: 0.375rem;
    border-radius: 50%;
    background: var(--st-gold);
}

/* What kind of record a citation is, carried on the record rather than in a column of its own. */
.st-answer-kind {
    display: block;
    font-size: 0.75rem;
    color: var(--st-text-muted);
}

/* The line above the record table: what the rows are, and the button that takes them away as a
   workbook. Wraps rather than crushes, because the label is a sentence in French and the drawer is
   46rem on a desktop and the whole screen on a phone. */
.st-ai-records-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.25rem;
}

/* ---------------------------------------------------------------------------
   Answer body — Markdown a model wrote, rendered inside a card.

   The prompt asks for no headings and short tables, but a prompt is a request and not a guarantee,
   so every element Markdown can produce is given a size that fits inside a card. Without this, one
   answer that opens with a heading puts a 32px title in the middle of the page.
   --------------------------------------------------------------------------- */
.st-answer-body {
    font-size: 0.9375rem;
    /* A table wider than the card scrolls rather than pushing the card out. Paragraphs wrap, so
       nothing else can reach this. */
    overflow-x: auto;
}

.st-answer-body > :last-child {
    margin-bottom: 0;
}

.st-answer-body p {
    margin-bottom: 0.5rem;
}

/* Flattened to card-sized text. Markdown headings are not expected here — see the prompt — but an
   answer that produces one should still look like a line of the answer. */
.st-answer-body h1,
.st-answer-body h2,
.st-answer-body h3,
.st-answer-body h4,
.st-answer-body h5,
.st-answer-body h6 {
    font-family: var(--st-font-body);
    font-size: 0.9375rem;
    font-weight: 600;
    margin: 0.75rem 0 0.35rem;
}

.st-answer-body ul,
.st-answer-body ol {
    margin-bottom: 0.5rem;
    padding-inline-start: 1.25rem;
}

.st-answer-body li {
    margin-bottom: 0.15rem;
}

/* Matched to .table-sm so a Markdown table and a Razor one read as the same object. */
.st-answer-body table {
    width: 100%;
    margin-bottom: 0.5rem;
    border-collapse: collapse;
    font-size: 0.875rem;
    font-variant-numeric: tabular-nums;
}

.st-answer-body th,
.st-answer-body td {
    padding: 0.35rem 0.5rem;
    border-bottom: 1px solid var(--st-border-subtle);
    text-align: start;
    vertical-align: top;
    white-space: nowrap;
}

.st-answer-body thead th {
    border-bottom: 1px solid var(--st-border-strong);
    font-weight: 600;
    color: var(--st-text-muted);
    font-size: 0.8125rem;
}

.st-answer-body tbody tr:last-child th,
.st-answer-body tbody tr:last-child td {
    border-bottom: 0;
}

.st-answer-body code {
    font-family: var(--st-font-mono);
    font-size: 0.8125rem;
    background: var(--st-pearl-light);
    border-radius: 3px;
    padding: 0.05rem 0.25rem;
}

.st-answer-body pre {
    background: var(--st-pearl-light);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius);
    padding: 0.6rem 0.75rem;
    margin-bottom: 0.5rem;
    overflow-x: auto;
}

.st-answer-body pre code {
    background: none;
    padding: 0;
}

.st-answer-body blockquote {
    margin: 0 0 0.5rem;
    padding-inline-start: 0.75rem;
    border-inline-start: 2px solid var(--st-border-strong);
    color: var(--st-text-muted);
}

/* ---------------------------------------------------------------------------
   Grid toolbar, filters and selection — the controls around the table.
   --------------------------------------------------------------------------- */
.st-grid-toolbar {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
    flex-wrap: wrap;
}

.st-grid-search {
    position: relative;
    display: flex;
    align-items: center;
    flex: 1 1 18rem;
    max-width: 26rem;
}

.st-grid-search .bi-search {
    position: absolute;
    left: 0.625rem;
    color: var(--st-text-muted);
    font-size: 0.875rem;
    pointer-events: none;
}

/* color and font-family are set explicitly, not left to inherit: a search input is one of the
   controls browsers style themselves, so without these the text renders in the UA's own black
   and default face — invisible against the dark theme's surface. Same reasoning as
   .st-filter-input below, which is why the two carry the same declarations. */
.st-grid-search input {
    width: 100%;
    padding: 0.375rem 0.75rem 0.375rem 2rem;
    border: 1px solid var(--st-border-strong);
    border-radius: var(--st-radius);
    background: var(--st-surface);
    color: var(--st-text);
    font-family: inherit;
    font-size: 0.875rem;
}

.st-grid-search input::placeholder {
    color: var(--st-text-muted);
}

.st-grid-search input:focus {
    outline: 2px solid var(--st-accent);
    outline-offset: -1px;
    border-color: var(--st-accent);
}

.st-grid-toolbar-actions {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    margin-left: auto;
}

.st-column-menu {
    max-height: 20rem;
    overflow-y: auto;
}

.st-column-menu label {
    cursor: pointer;
    display: flex;
    align-items: center;
}

/* Sorting: the whole header is the target, and the chevron shows direction. */
.st-sort-link {
    color: inherit;
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    white-space: nowrap;
}

.st-sort-link:hover {
    color: var(--st-gold);
}

/* Numbers right-aligned with tabular figures so digits line up down the column and a
   wrong order of magnitude is visible rather than something you have to read for. */
.st-cell-end {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.st-cell-center {
    text-align: center;
}

.st-cell-select {
    width: 2.5rem;
    text-align: center;
}

/* Active filters, each removable. A short list should always explain itself. */
.st-filter-chips {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    flex-wrap: wrap;
    margin-bottom: 0.75rem;
}

.st-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.1875rem 0.5rem;
    border: 1px solid var(--st-border-strong);
    border-radius: 999px;
    background: var(--st-pearl-light);
    font-size: 0.8125rem;
    color: var(--st-accent);
    text-decoration: none;
}

.st-chip:hover {
    border-color: var(--st-accent);
    color: var(--st-accent);
}

.st-chip-label {
    color: var(--st-text-muted);
}

.st-chip-clear {
    font-size: 0.8125rem;
    color: var(--st-text-muted);
}

/* Appears only when rows are selected — chrome the rest of the time would be in the way. */
.st-bulk-bar {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.5rem 0.875rem;
    margin-bottom: 0.75rem;
    border: 1px solid var(--st-accent);
    border-radius: var(--st-radius);
    background: var(--st-pearl-light);
    animation: st-slide-in 140ms ease-out;
}

@keyframes st-slide-in {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: none; }
}

.st-bulk-count {
    font-weight: 600;
    font-size: 0.875rem;
    color: var(--st-accent);
}

.st-bulk-actions {
    display: flex;
    gap: 0.375rem;
    flex-wrap: wrap;
}

.st-grid-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    margin-top: 0.75rem;
    flex-wrap: wrap;
}

.st-grid-count {
    font-size: 0.8125rem;
    color: var(--st-text-muted);
    font-variant-numeric: tabular-nums;
}

.st-pager {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.st-pager-position {
    font-size: 0.8125rem;
    color: var(--st-text-muted);
    font-variant-numeric: tabular-nums;
}

/* Card layout below md: label and value per line, so a record stays readable one-handed. */
.st-card-field {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.125rem 0;
    font-size: 0.875rem;
}

.st-card-label {
    color: var(--st-text-muted);
    flex-shrink: 0;
}

/* The value takes what the label leaves and wraps inside it. A flex item will not shrink below
   its own content without min-width:0, so a value carrying no break opportunity made the whole
   card wider than the screen — measured at 487px inside a 390px phone on the document register,
   where names run "0010485588_Space TR D_0898937477_FINAL-AIR-FCA-G2-.PDF" and an underscore is
   not a place the browser will break. overflow-wrap lets those break anywhere; ordinary values
   with spaces in them are unaffected.

   Scoped to the list's own cards on purpose: .st-card-field is also the detail screens' fact row
   (activity, alert and user detail), which is a dt/dd pair that renders at every width and was
   not measured as overflowing. Right-aligning a wrapped sentence there would be a change to
   desktop that nothing asked for. */
.st-card-list .st-card-field > :not(.st-card-label) {
    min-width: 0;
    text-align: end;
    overflow-wrap: anywhere;
}

@media (prefers-reduced-motion: reduce) {
    .st-bulk-bar { animation: none; }
}

/* ---------------------------------------------------------------------------
   Grid loading state. htmx sets .htmx-request on the element it is replacing,
   so the table dims and the cursor changes while new rows are on their way —
   feedback within 100ms, without the screen ever going blank.
   --------------------------------------------------------------------------- */
.st-grid.htmx-request {
    opacity: 0.55;
    pointer-events: none;
    transition: opacity 120ms ease-out;
}

.st-grid.htmx-request .st-table-wrapper {
    cursor: progress;
}

/* ---------------------------------------------------------------------------
   Busy line. For work with nowhere to dim: an upload replaces an empty panel,
   so until the answer arrives there is nothing on screen to show as pending and
   a person is left wondering whether the button took. Named by hx-indicator, so
   htmx puts .htmx-request on it for the life of the request.
   --------------------------------------------------------------------------- */
.st-busy {
    display: none;
    align-items: center;
    gap: 0.5rem;
    color: var(--st-text-muted);
    font-size: 0.875rem;
}

.st-busy.htmx-request {
    display: flex;
}

.st-busy .st-btn-spinner {
    display: inline-block;
    margin-right: 0;
}

/* ---------------------------------------------------------------------------
   A panel filled by htmx, where what was in it last time is about to be wrong.
   The busy line sits OUTSIDE the swap target so it survives every swap, and
   while it is running the stale panel beside it is hidden rather than left on
   screen under a spinner - a modal that shows the previous brand's heading for
   a second reads as the wrong answer rather than as a slow one.

   Done in CSS because the content security policy is script-src 'self' with no
   unsafe-eval: htmx's own hx-on attributes are compiled with new Function and
   are dead here, and there is no inline script to hang a listener on.
   --------------------------------------------------------------------------- */
.st-swap-busy.htmx-request + .st-swap-target {
    display: none;
}

/* ---------------------------------------------------------------------------
   A modal carrying a panel of data rather than a chart.

   Bootstrap's own .modal-content is white, which is right on a white page and
   wrong here: on the dark theme a plain modal arrives as a sheet of paper over
   a dark screen. The enlarged chart has carried the surface treatment for a
   while under .st-chart-modal; this is the same three declarations for a modal
   whose content is a table.
   --------------------------------------------------------------------------- */
.st-panel-modal .modal-content {
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-lg);
}

.st-panel-modal .modal-header,
.st-panel-modal .modal-footer {
    border-color: var(--st-border);
}

/* The panel while it is being read. It stands in for .modal-content rather than
   sitting inside it (see the shell's own note), so it carries the same surface,
   or the spinner would arrive floating on the backdrop with no panel around it. */
.st-panel-busy {
    width: 100%;
    justify-content: center;
    padding: 3rem 1rem;
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-lg);
}

/* Saved views: name on the left, a quiet delete on the right. */
.st-views-menu {
    min-width: 17rem;
}

.st-view-item {
    display: flex;
    align-items: center;
}

.st-view-item .dropdown-item {
    flex: 1 1 auto;
}

.st-view-item form {
    margin: 0;
    padding-right: 0.5rem;
}

.st-view-delete {
    color: var(--st-text-muted);
    padding: 0.125rem 0.25rem;
    line-height: 1;
}

.st-view-delete:hover {
    color: var(--st-danger);
}

.st-view-save .st-label {
    font-size: 0.75rem;
    color: var(--st-text-muted);
    margin-bottom: 0.25rem;
}

@media (prefers-reduced-motion: reduce) {
    .st-grid.htmx-request { transition: none; }
}

/* ---------------------------------------------------------------------------
   Column separators. Lighter than the row rules on purpose: rows are what the
   eye follows down a list, so vertical lines organise without competing.
   --------------------------------------------------------------------------- */
.st-table thead th + th,
.st-table tbody td + td {
    border-left: 1px solid var(--st-border-subtle);
}

/* ---------------------------------------------------------------------------
   Per-column filter row, directly beneath the headers.

   The sticky offset is set from JS (--st-header-height) rather than guessed in
   rem: the header's real height changes with density, font loading and zoom,
   and a hardcoded value leaves the filter row detached or overlapping.
   --------------------------------------------------------------------------- */
.st-filter-row th {
    position: sticky;
    top: var(--st-header-height, 2.15rem);
    z-index: 1;
    background: var(--st-surface);
    border-bottom: 1px solid var(--st-border-strong);
    padding: 0.3125rem 0.375rem;
}

.st-table.st-density-compact .st-filter-row th {
    padding: 0.1875rem 0.25rem;
}

/* Text boxes and dropdowns must be the same object at a glance: same box, same
   height, same width. Native select chrome differs enough between browsers that
   matching it by hand is a losing game, so the arrow is drawn here instead. */
.st-filter-input {
    display: block;
    width: 100%;
    min-width: 0;
    height: 1.875rem;
    padding: 0 0.5rem;
    border: 1px solid var(--st-border-strong);
    border-radius: 4px;
    background: var(--st-surface);
    font-family: inherit;
    font-size: 0.8125rem;
    font-weight: 400;
    line-height: 1.75rem;
    color: var(--st-text);
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
}

select.st-filter-input {
    /* Room for the chevron, which is drawn as a background image so every browser
       renders the same control at the same height. */
    padding-right: 1.5rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23777777'%3E%3Cpath d='M4.5 6.5 8 10l3.5-3.5z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.375rem center;
    background-size: 0.875rem;
    cursor: pointer;
}

.st-table.st-density-compact .st-filter-input {
    height: 1.625rem;
    line-height: 1.5rem;
}

.st-filter-input::placeholder {
    color: var(--st-cloud);
}

.st-filter-input:focus {
    outline: 2px solid var(--st-accent);
    outline-offset: -1px;
    border-color: var(--st-accent);
}

/* An active filter is marked by weight and a navy edge, not a colour wash: the
   table already carries the brand's few colours, and tinting inputs cheapens it.
   The "Clear filters" button in the toolbar carries the actual count. */
.st-filter-input[data-active="true"] {
    border-color: var(--st-accent);
    font-weight: 500;
}

.st-clear-filters {
    white-space: nowrap;
}

/* Filters sit under their column, so they follow its alignment — except selects,
   where text-align is unreliable across browsers and the chevron anchors right. */
.st-cell-end input.st-filter-input {
    text-align: right;
}

.st-cell-center input.st-filter-input {
    text-align: center;
}

/* A date column filters with two pickers instead of a text box, stacked because two
   native date controls side by side need more width than a date column has. */
.st-filter-daterange {
    display: flex;
    flex-direction: column;
    gap: 0.1875rem;
}

/* Native date inputs bring their own line-height and a picker button; normalised so
   the filter row still reads as one consistent set of controls. */
input.st-filter-date {
    line-height: normal;
    padding-right: 0.25rem;
}

input.st-filter-date::-webkit-calendar-picker-indicator {
    padding: 0;
    margin-left: 0.125rem;
    opacity: 0.55;
    cursor: pointer;
}

input.st-filter-date::-webkit-calendar-picker-indicator:hover {
    opacity: 1;
}

/* An empty date input still prints "dd/mm/yyyy" at full text colour, which reads as a
   value already set rather than as a prompt. Muted until it holds a real date. */
input.st-filter-date[data-active="false"] {
    color: var(--st-text-muted);
}

/* Dates sit under a right-aligned column, but right-aligning the field itself pushes
   the digits away from the picker button they belong to. */
.st-cell-end input.st-filter-date {
    text-align: left;
}

/* ---------------------------------------------------------------------------
   Command palette — Ctrl+K. A dialog pinned near the top of the viewport, the
   way people expect a launcher to appear, rather than centred like a modal.
   --------------------------------------------------------------------------- */
.st-palette-overlay {
    position: fixed;
    inset: 0;
    z-index: 1082;
    display: none;
}

.st-palette-overlay.show {
    display: block;
}

.st-palette-backdrop {
    position: absolute;
    inset: 0;
    background: rgb(10 23 45 / 32%);
}

.st-palette {
    position: relative;
    margin: 10vh auto 0;
    width: min(40rem, calc(100vw - 2rem));
    max-height: 70vh;
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-lg);
    box-shadow: var(--st-shadow-lg);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.st-palette-input {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--st-border-subtle);
    color: var(--st-text-muted);
}

.st-palette-input input {
    flex: 1 1 auto;
    border: none;
    outline: none;
    background: transparent;
    font-size: 1rem;
    color: var(--st-text);
}

.st-palette-input input::placeholder {
    color: var(--st-text-muted);
}

.st-palette-results {
    overflow-y: auto;
    padding: 0.35rem 0;
}

.st-palette-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.st-palette-group {
    padding: 0.5rem 1rem 0.25rem;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--st-text-muted);
}

.st-palette-item {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    padding: 0.5rem 1rem;
    color: var(--st-text);
    text-decoration: none;
    cursor: pointer;
}

/* Highlight follows both the pointer and the arrow keys, so the two never disagree. */
.st-palette-item:hover,
.st-palette-item.active {
    background: var(--st-bg-subtle);
}

.st-palette-item.active {
    box-shadow: inset 3px 0 0 var(--st-gold);
}

/* Muted navy against white keeps the icon supporting its label rather than competing with it.
   --st-navy-300 is a raw swatch the dark block does not redefine, though, and on the palette's
   dark surface it reads as barely-there grey — so dark mode takes the same near-white as the
   titles instead. This is the only place that swatch is used as a foreground colour. */
.st-palette-item-icon {
    color: var(--st-navy-300);
    flex: 0 0 auto;
}

:root[data-theme="dark"] .st-palette-item-icon {
    color: var(--st-text);
}

.st-palette-item-title {
    flex: 1 1 auto;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.st-palette-item-meta {
    flex: 0 0 auto;
    font-size: 0.8125rem;
    color: var(--st-text-muted);
    max-width: 45%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.st-palette-empty {
    padding: 2rem 1rem;
    text-align: center;
    color: var(--st-text-muted);
}

.st-palette-empty .bi {
    font-size: 1.25rem;
    display: block;
    margin-bottom: 0.5rem;
}

/* Full /search page fallback (JavaScript off): the same results, laid out on the page. */
.st-search-page-form {
    display: flex;
    gap: 0.5rem;
    max-width: 40rem;
    margin-bottom: 1.25rem;
}

.st-search-page-results {
    max-width: 40rem;
    border: 1px solid var(--st-border-subtle);
    border-radius: var(--st-radius);
}

/* ---------------------------------------------------------------------------
   User detail — the account, its status, details and roles. Two columns on a
   wide screen, stacked on a narrow one (Bootstrap grid handles the reflow).
   --------------------------------------------------------------------------- */
.st-detail-card {
    border: 1px solid var(--st-border-subtle);
    border-radius: var(--st-radius);
    box-shadow: none;
}

.st-detail-heading {
    font-family: var(--st-font-heading);
    color: var(--st-accent);
    margin-bottom: 1rem;
}

.st-detail-status {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-weight: 600;
    margin-bottom: 1rem;
}

.st-detail-facts {
    margin: 0;
}

/* dt/dd inside the shared card-field rows read like the grid's card layout. */
.st-detail-facts dd {
    margin: 0;
    text-align: right;
    word-break: break-word;
}

.st-detail-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 1.25rem;
    padding-top: 1rem;
    border-top: 1px solid var(--st-border-subtle);
}

.st-detail-hint {
    font-size: 0.8125rem;
    margin-bottom: 1rem;
}

/* ---------------------------------------------------------------------------
   Settings (/admin/settings) — a card per group, and inside it one row per
   setting: what it is on the left at a readable measure, the control and its
   Save on the right.

   Every kind draws that same shape. Before this, a switch and a money box were
   laid out by different rules and read as two different screens; the switch in
   particular sat on .st-finding-note, a flex line meant for a one-line note,
   which put a three-sentence description and the Save button on one 1600px row.
   --------------------------------------------------------------------------- */
.st-settings-lead,
.st-settings-group {
    margin-bottom: 1rem;
}

/* The topbar is fixed, so a card jumped to from the index would open with its
   heading underneath it. */
.st-settings-group {
    scroll-margin-top: calc(var(--st-topbar-height) + 1rem);
}

.st-settings-find {
    max-width: 24rem;
    margin-top: 1.25rem;
}

.st-settings-find input {
    margin-bottom: 0.35rem;
}

.st-settings-index {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--st-border-subtle);
}

.st-settings-index-link {
    display: inline-flex;
    align-items: baseline;
    gap: 0.4rem;
    padding: 0.25rem 0.7rem;
    border: 1px solid var(--st-border);
    border-radius: 999px;
    font-size: 0.8125rem;
    color: var(--st-text);
    text-decoration: none;
    transition: border-color var(--st-transition), color var(--st-transition);
}

.st-settings-index-link:hover {
    border-color: var(--st-gold);
    color: var(--st-accent);
}

.st-settings-index-count {
    font-size: 0.6875rem;
    color: var(--st-text-muted);
    font-variant-numeric: tabular-nums;
}

/* The control column is sized to its content and sits directly beside the prose,
   rather than being pushed to the card's right edge. On a wide monitor that edge
   is 600px of nothing away from the words the control belongs to, and the eye has
   to cross it on every row to find out what it is setting. */
.st-setting {
    display: grid;
    grid-template-columns: minmax(0, 38rem) max-content;
    align-items: start;
    gap: 1rem 2rem;
    padding: 1rem 0;
    border-top: 1px solid var(--st-border-subtle);
}

/* The heading is not a form, so :first-of-type is the first setting in the card. */
.st-setting:first-of-type {
    padding-top: 0;
    border-top: 0;
}

.st-setting:last-of-type {
    padding-bottom: 0;
}

/* The filter hides rows, whole groups and index links with the hidden attribute,
   and every one of the three carries a display of its own that would otherwise
   win over it — grid here, flex on a Bootstrap card, inline-flex on a chip. */
.st-setting[hidden],
.st-settings-group[hidden],
.st-settings-index-link[hidden] {
    display: none;
}

.st-setting-name {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.3rem;
}

.st-setting-label {
    margin: 0;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--st-text);
}

.st-setting-help {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    font-size: 0.8125rem;
    color: var(--st-text-muted);
}

.st-setting-bounds {
    font-variant-numeric: tabular-nums;
}

.st-setting-control {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.st-setting-box {
    width: 9rem;
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* The switch carries no label of its own — the row's label is in the left column
   and points at it — so Bootstrap's indent for one would push it off the edge of
   the control column. */
.st-setting-switch {
    min-height: 0;
    padding-left: 0;
    margin-bottom: 0;
}

.st-setting-switch .form-check-input {
    float: none;
    margin-left: 0;
    width: 2.25rem;
    height: 1.15rem;
    cursor: pointer;
}

@media (max-width: 767.98px) {
    .st-setting {
        grid-template-columns: minmax(0, 1fr);
        gap: 0.75rem;
    }
}

/* ---------------------------------------------------------------------------
   Check schedules (/admin/email/schedules) — the numbers a check reads.

   A check's numbers are a sub-row of the check above them and have to read that
   way. They sat on .st-finding-note before this: a one-line flex strip with no
   container of its own, so a heading, a shared-key warning and two-part forms
   flattened onto one 1600px line and the block looked like loose text that had
   escaped the table. That is the same misuse the settings screen above was
   rebuilt to undo, on a second screen.

   .st-finding-note itself is untouched — six other places across four views use
   it for the one-line note it was written for.

   A number is drawn on .st-setting, the shape /admin/settings already uses,
   because it is the same object: a bounded number with a description and a Save.
   Two screens presenting it as two different things is what made this one read
   as half a table. The narrow-screen collapse above is inherited with it.
   --------------------------------------------------------------------------- */

/* The check and its numbers close once, not twice. Left alone, the check's own
   bottom rule cuts between a row and the thing that belongs to it. */
.st-check-table tbody tr.st-check-has-numbers > td {
    border-bottom: 0;
    padding-bottom: 0.35rem;
}

/* Hover tracks the eye along a row of data. These rows are forms, not links, and
   a check lighting up while its own numbers stay dark splits the block in two at
   exactly the seam these rules exist to close. */
.st-check-table tbody tr:hover {
    background: none;
}

.st-check-numbers > td {
    padding-top: 0;
}

/* Indented and ruled down its left edge: the panel is read as belonging to the
   check above it rather than as a further check. */
.st-check-numbers-panel {
    margin-left: 1.5rem;
    padding: 0.75rem 1rem;
    background: var(--st-pearl-light);
    border: 1px solid var(--st-border);
    border-left: 3px solid var(--st-border-strong);
    border-radius: var(--st-radius);
}

.st-check-numbers-heading {
    margin-bottom: 0.6rem;
    font-weight: 600;
    color: var(--st-text-muted);
}

.st-role-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
    gap: 0.5rem 1rem;
}

/* ---------------------------------------------------------------------------
   Roles screen — a card per role, and a permission checklist per role grouped
   by the module that owns each permission.
   --------------------------------------------------------------------------- */
.st-role-card {
    transition: border-color var(--st-transition), box-shadow var(--st-transition);
}

.st-role-card:hover {
    border-color: var(--st-gold);
    box-shadow: var(--st-shadow-lg);
}

.st-role-flag {
    color: var(--st-text-muted);
}

.st-permission-list {
    display: grid;
    gap: 0.75rem;
}

/* Human description first, the Module.Entity.Action code below it for the administrator who wants precision. */
.st-permission-row .form-check-label {
    display: flex;
    flex-direction: column;
    line-height: 1.3;
}

.st-permission-name {
    color: var(--st-text);
}

.st-permission-code {
    font-family: var(--st-font-mono);
    font-size: 0.75rem;
    color: var(--st-text-muted);
}

/* System logs -------------------------------------------------------------- */

.st-page-actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* Month picker (exchange rates) ------------------------------------------- */

/* A button naming the month on screen, and under it a year with arrows and the twelve months in
   a grid. One click on a month loads it: there is nothing to confirm and only one month to choose,
   because the screen shows one month. The panel is drawn by month-picker.js. */
.st-month-picker {
    position: relative;
}

.st-month-picker-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    min-width: 11rem;
    justify-content: center;
}

.st-month-picker-panel {
    position: absolute;
    top: calc(100% + 0.375rem);
    right: 0;
    z-index: 1030;
    width: 21rem;
    max-width: calc(100vw - 2rem);
    padding: 0.75rem;
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-lg);
    box-shadow: var(--st-shadow-lg);
}

.st-month-picker-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 0.625rem;
}

.st-month-picker-year {
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--st-text);
}

.st-month-picker-step {
    width: 2rem;
    height: 2rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid transparent;
    border-radius: var(--st-radius);
    background: transparent;
    color: var(--st-text);
    transition: background-color var(--st-transition);
}

.st-month-picker-step:hover:not(:disabled),
.st-month-picker-step:focus-visible {
    background: var(--st-bg-subtle);
    border-color: var(--st-border);
}

/* The floor year: the arrow stays in place so the head does not shift, but reads as off. */
.st-month-picker-step:disabled {
    color: var(--st-text-muted);
    opacity: 0.45;
    cursor: default;
}

.st-month-picker-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 0.375rem;
}

.st-month-picker-month {
    display: block;
    padding: 0.5rem 0.25rem;
    text-align: center;
    font-size: 0.875rem;
    color: var(--st-text);
    text-decoration: none;
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius);
    background: var(--st-surface);
    transition: background-color var(--st-transition), border-color var(--st-transition);
}

.st-month-picker-month:hover,
.st-month-picker-month:focus-visible {
    background: var(--st-bg-subtle);
    border-color: var(--st-border-strong);
    color: var(--st-text);
}

/* The month already on screen. Navy as an outline and weight rather than a fill, as everywhere
   else the accent marks the current thing. */
.st-month-picker-month.is-current {
    border-color: var(--st-accent);
    box-shadow: inset 0 0 0 1px var(--st-accent);
    font-weight: 600;
}

/* Shrink a column to its content so a neighbour (here, Date) absorbs the slack. Without this a
   sparse table dumps all leftover width into the last column, leaving a gap before its content. */
.st-table .st-col-shrink {
    width: 1%;
    white-space: nowrap;
}

/* Let one heading wrap. Headings are nowrap by default so a column name always reads on one line,
   but a long one then claims that whole width from its neighbours: "Cnf (Carry and forwarding)" over
   a column of short figures pushed the product names on the landed cost report onto two lines each.
   The heading wraps instead, and the width goes back to the columns whose content needs it. */
.st-table thead th.st-col-wrap {
    white-space: normal;
}

.st-log-row-actions {
    display: flex;
    gap: 0.5rem;
    justify-content: flex-end;
}

.st-log-kind {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    font-weight: 500;
}

.st-log-kind-error {
    color: var(--st-danger);
}

.st-log-kind-activity {
    color: var(--st-accent);
}

.st-log-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0.75rem;
    padding: 0.875rem 1rem;
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-radius: 0.5rem;
}

.st-log-filter-field {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

.st-log-filter-field label {
    font-size: 0.75rem;
    color: var(--st-text-muted);
}

.st-log-filter-actions {
    display: flex;
    gap: 0.5rem;
    margin-left: auto;
}

/* The line table reads like a terminal: monospace, tight, colour-coded by level. */
.st-log-lines {
    font-family: var(--st-font-mono);
    font-size: 0.78125rem;
}

.st-log-time,
.st-log-user {
    white-space: nowrap;
    color: var(--st-text-muted);
}

.st-log-message {
    white-space: pre-wrap;
    word-break: break-word;
}

.st-log-request {
    font-size: 0.7rem;
}

.st-log-badge {
    display: inline-block;
    padding: 0.05rem 0.4rem;
    border-radius: 0.25rem;
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    background: var(--st-bg-subtle);
    color: var(--st-text-muted);
}

/* Level is signalled by more than colour: a left border weight and the badge tint both change,
   so the distinction survives for colour-blind readers and in print. */
.st-log-level-warning {
    background: var(--st-warning-bg);
}

.st-log-level-warning .st-log-badge {
    background: var(--st-warning);
    color: var(--st-text-inverse);
}

.st-log-level-error,
.st-log-level-fatal {
    background: var(--st-danger-bg);
}

.st-log-level-error .st-log-badge,
.st-log-level-fatal .st-log-badge {
    background: var(--st-danger);
    color: var(--st-text-inverse);
}

/* Both log badges fill with a status token, so both go pale on dark and need the navy label. */
:root[data-theme="dark"] .st-log-level-warning .st-log-badge,
:root[data-theme="dark"] .st-log-level-error .st-log-badge,
:root[data-theme="dark"] .st-log-level-fatal .st-log-badge {
    color: var(--st-navy);
}

.st-log-exception {
    margin-top: 0.375rem;
}

.st-log-exception summary {
    cursor: pointer;
    color: var(--st-accent);
    font-size: 0.72rem;
}

.st-log-exception pre {
    margin: 0.375rem 0 0;
    padding: 0.625rem 0.75rem;
    background: var(--st-bg-subtle);
    border: 1px solid var(--st-border);
    border-radius: 0.375rem;
    font-size: 0.72rem;
    white-space: pre-wrap;
    word-break: break-word;
    max-height: 22rem;
    overflow: auto;
}

/* Activity drill-in ------------------------------------------------------- */

.st-audit-timeline {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.st-audit-entry {
    border: 1px solid var(--st-border-subtle);
    border-radius: var(--st-radius);
    padding: 0.875rem 1rem;
}

.st-audit-entry-head {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    margin-bottom: 0.5rem;
}

.st-audit-action {
    display: inline-block;
    padding: 0.05rem 0.45rem;
    border-radius: 0.25rem;
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    background: var(--st-bg-subtle);
    color: var(--st-text-muted);
}

.st-audit-action-created {
    background: var(--st-success-bg, #e6f4ea);
    color: var(--st-success, #1e7e46);
}

.st-audit-action-deleted {
    background: var(--st-danger-bg);
    color: var(--st-danger);
}

.st-audit-changes {
    font-size: 0.82rem;
}

.st-audit-field {
    font-weight: 500;
    white-space: nowrap;
}

.st-audit-from {
    color: var(--st-text-muted);
}

.st-audit-to {
    color: var(--st-accent);
    font-weight: 500;
}

/* Cascading links between master-data screens: a link inside a grid cell (the row still
   navigates to its own record — the row-click handler ignores clicks on anchors), and a small
   drill link under a form field. */
.st-cell-link {
    color: var(--st-accent);
    /* A dotted underline at rest is the cue that this leads somewhere — visible without hovering,
       and not colour-only, so it reads for everyone. It firms up to solid gold on hover/focus. */
    text-decoration: underline;
    text-decoration-style: dotted;
    text-decoration-color: var(--st-gold);
    text-decoration-thickness: 1px;
    text-underline-offset: 3px;
    cursor: pointer;
}

.st-cell-link:hover,
.st-cell-link:focus-visible {
    color: var(--st-gold);
    text-decoration-style: solid;
}

/* ---------------------------------------------------------------------------
   Activity feed cells. The feed is scanned rather than read row by row, so each
   column carries one shape the eye can lock onto: a time, a face, an area, a
   verb. Nothing here is colour-only — every badge keeps its word and its icon.
   --------------------------------------------------------------------------- */
.st-activity-when {
    display: inline-flex;
    flex-direction: column;
    gap: 0.0625rem;
    line-height: 1.25;
}

.st-activity-ago {
    font-weight: 500;
    white-space: nowrap;
}

.st-activity-exact {
    font-size: 0.75rem;
    color: var(--st-text-muted);
    white-space: nowrap;
}

/* Compact density asked for more rows on screen, so the second line goes. The exact
   stamp is still on the cell's tooltip, to the second. */
.st-table.st-density-compact .st-activity-exact {
    display: none;
}

.st-actor {
    display: inline-flex;
    align-items: center;
    gap: 0.4375rem;
    min-width: 0;
}

/* An initials mark makes one person's run of changes visible as a block when
   scanning, which a column of similar-length names does not. */
.st-actor-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 1.5rem;
    height: 1.5rem;
    border-radius: 50%;
    background: var(--st-info-bg);
    color: var(--st-accent);
    font-size: 0.6875rem;
    font-weight: 600;
}

/* A recurring job, not a person: a cog, so it is never read as somebody's initials. */
.st-actor-mark-system {
    background: var(--st-pearl-light);
    color: var(--st-text-muted);
    font-size: 0.75rem;
}

.st-actor-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The area a row belongs to. Quiet by design: it groups rows, it does not rank them. */
.st-module-chip {
    display: inline-block;
    padding: 0.125em 0.5em;
    border: 1px solid var(--st-border);
    border-radius: 999px;
    background: var(--st-bg-subtle);
    color: var(--st-text-muted);
    font-size: 0.75rem;
    white-space: nowrap;
}

.st-activity-action {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.125rem;
}

.st-activity-subject {
    font-size: 0.75rem;
    color: var(--st-text-muted);
}

.st-field-link {
    display: inline-block;
    margin-top: 0.35rem;
    font-size: 0.85rem;
    color: var(--st-accent);
    text-decoration: none;
}

.st-field-link::after {
    content: " \2192"; /* → : this leads somewhere */
}

.st-field-link:hover,
.st-field-link:focus {
    color: var(--st-gold);
    text-decoration: underline;
}

/* --- Purchasing: order lines panel --------------------------------------------------------- */
.st-add-line {
    padding: 0.75rem;
    background: var(--st-pearl, #e9e5e3);
    border-radius: 0.375rem;
}

/* --- Article typeahead on the add-a-line forms ---------------------------------------------
   The list floats over the form rather than pushing it down: the add row sits directly above the
   lines table, and growing the row by eight articles would move the button out from under the
   pointer mid-click. The positioning wrapper carries no chrome of its own, so an empty response
   leaves nothing behind whether it cleared to empty markup or to whitespace.
   ------------------------------------------------------------------------------------------- */
.st-typeahead {
    position: relative;
}

.st-typeahead-results {
    position: absolute;
    inset-inline: 0;
    top: calc(100% + 0.25rem);
    z-index: 1050;
}

.st-typeahead-panel {
    max-height: 17rem;
    overflow-y: auto;
    background: var(--st-surface, #fff);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius, 0.375rem);
    box-shadow: var(--st-shadow-lg);
}

.st-typeahead-list {
    list-style: none;
    margin: 0;
    padding: 0.25rem 0;
}

.st-typeahead-item {
    display: block;
    width: 100%;
    padding: 0.35rem 0.75rem;
    border: none;
    background: transparent;
    text-align: left;
    color: var(--st-text);
    cursor: pointer;
}

/* Pointer and arrow keys share one highlight, so the two can never disagree about what Enter takes. */
.st-typeahead-item:hover,
.st-typeahead-item.active {
    background: var(--st-bg-subtle);
}

.st-typeahead-item.active {
    box-shadow: inset 3px 0 0 var(--st-gold);
}

.st-typeahead-name {
    display: block;
    font-size: 0.875rem;
}

.st-typeahead-meta {
    display: block;
    font-size: 0.75rem;
    color: var(--st-text-muted, #6c757d);
    font-variant-numeric: tabular-nums;
}

.st-typeahead-empty {
    margin: 0;
    padding: 0.6rem 0.75rem;
    font-size: 0.8rem;
    color: var(--st-text-muted, #6c757d);
}

/* Same shrink-to-content trick as .st-table, for the lines table's remove-button column. */
.st-lines-table .st-col-shrink {
    width: 1%;
    white-space: nowrap;
}

.st-line-product {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

.st-line-ean {
    font-variant-numeric: tabular-nums;
    color: var(--st-accent);
}

.st-line-desc {
    font-size: 0.875rem;
}

.st-tester-badge {
    background: var(--st-navy);
    color: var(--st-text-inverse);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    font-size: 0.65rem;
}

/* What class of article a line is, where it is not the ordinary one. Outlined rather than filled,
   unlike the tester badge beside it: this is a note about the article, not a state of the line, and
   two solid badges on one row compete for the same glance. */
.st-line-type-badge {
    background: transparent;
    color: var(--st-text-muted);
    border: 1px solid var(--st-border-subtle);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    font-size: 0.65rem;
}

/* A line that matched no known product: flagged, never rejected, so a human can reconcile it. */
.st-unmatched-badge {
    background: var(--st-warning);
    color: var(--st-text-inverse);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    font-size: 0.65rem;
}

.st-unmatched-text {
    color: var(--st-warning-text);
    font-weight: 600;
}

.st-line-unmatched {
    background: var(--st-warning-bg);
}

/* Articles an unmatched line might name. Offered under the line, never applied on their own. */
.st-suggestions {
    margin-top: 0.5rem;
    padding: 0.5rem 0.75rem;
    border-left: 2px solid var(--st-warning);
    background: var(--st-surface, #fff);
    border-radius: 0 4px 4px 0;
}

.st-suggestions-lead {
    margin: 0 0 0.35rem;
    font-size: 0.8rem;
    font-weight: 600;
}

.st-suggestion-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.st-suggestion {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
    flex-wrap: wrap;
    padding: 0.3rem 0;
}

.st-suggestion + .st-suggestion {
    border-top: 1px solid var(--st-border-subtle, #efebe6);
}

.st-suggestion-article {
    flex: 1 1 16rem;
}

.st-suggestion-name {
    font-weight: 500;
}

.st-suggestion-ean,
.st-suggestion-brand {
    margin-left: 0.5rem;
    font-size: 0.75rem;
    color: var(--st-text-muted, #6c757d);
}

/* The reason a suggestion is being made. Never hidden behind a tooltip: a suggestion whose grounds
   are not on screen gets accepted on faith, which is the guess we refused to make automatically. */
.st-suggestion-why {
    font-size: 0.75rem;
    color: var(--st-text-muted, #6c757d);
    flex: 0 1 auto;
}

.st-suggestions-note {
    margin: 0.4rem 0 0;
    font-size: 0.7rem;
    color: var(--st-text-muted, #6c757d);
}

/* Documents-received panel: proforma / delivery note / final invoice, grouped by match leg. */
.st-doc-group + .st-doc-group {
    margin-top: 1.25rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--st-border-subtle, #efebe6);
}

.st-doc-group-title {
    color: var(--st-accent);
}

.st-doc-list li + li {
    margin-top: 0.4rem;
}

.st-doc-row {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
    padding: 0.5rem 0.65rem;
    border: 1px solid var(--st-border-subtle, #efebe6);
    border-radius: 0.375rem;
    text-decoration: none;
    color: inherit;
}

.st-doc-row:hover {
    border-color: var(--st-gold, #bf8e36);
    background: var(--st-bg-subtle);
}

.st-doc-primary {
    font-weight: 600;
    color: var(--st-accent);
    font-variant-numeric: tabular-nums;
}

.st-doc-meta {
    margin-left: auto;
    font-size: 0.8rem;
    white-space: nowrap;
}

/* A small separation for a destructive action tucked under the main form. */
.st-danger-zone {
    border-top: 1px solid var(--st-border-subtle, #efebe6);
    padding-top: 0.75rem;
}

/* Attached-file rows in the documents panel. */
.st-file-row {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
    padding: 0.5rem 0.65rem;
    border: 1px solid var(--st-border-subtle, #efebe6);
    border-radius: 0.375rem;
}

.st-file-row + .st-file-row {
    margin-top: 0.4rem;
}

.st-file-name {
    display: inline-flex;
    align-items: baseline;
    gap: 0.4rem;
    font-weight: 600;
    color: var(--st-accent);
    text-decoration: none;
    word-break: break-all;
}

.st-file-name:hover {
    color: var(--st-gold, #bf8e36);
    text-decoration: underline;
}

.st-file-meta {
    margin-left: auto;
    font-size: 0.8rem;
    white-space: nowrap;
}

.st-file-remove {
    margin: 0;
}

/* The files table sits in a half-width panel and supplier file names run to fifty characters.
   Left to the table's own layout the name column is squeezed to a character a line, because
   .st-file-name may break anywhere. So the name keeps a floor, wraps at separators before it
   wraps inside a number, and the wrapper scrolls sideways when even that is not enough; the
   narrow columns wrap rather than push it. */
.st-file-table td:first-child,
.st-file-table th:first-child {
    min-width: 14rem;
}

.st-file-table .st-file-name {
    word-break: normal;
    overflow-wrap: anywhere;
}

.st-file-table tbody td {
    white-space: normal;
    vertical-align: top;
}

.st-file-table .st-cell-actions {
    width: 3rem;
}

/* --- Reading a document -------------------------------------------------------------------
   The review screen puts the original beside what we read off it. The document must be big
   enough to actually read at a glance, or the reviewer opens it in another tab and the whole
   side-by-side idea is wasted. */

.st-review-document {
    width: 100%;
    flex: 1 1 auto;
    min-height: 34rem;
    border: 1px solid var(--st-border-subtle, #efebe6);
    border-radius: 0.375rem;
    background: var(--st-pearl-light, #f7f5f3);
}

/* A neutral status strip: reading in progress, already posted, nothing read yet. */
.st-review-notice {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.65rem 0.9rem;
    margin-bottom: 1rem;
    border-left: 3px solid var(--st-gold, #bf8e36);
    background: var(--st-pearl-light, #f7f5f3);
    color: var(--st-accent);
    font-size: 0.9rem;
}

/* Everything the checks found, gathered above the lines so the reviewer reads it before editing. */
.st-review-flags {
    border: 1px solid var(--st-warning-border);
    border-radius: 0.375rem;
    background: var(--st-warning-bg);
    padding: 0.6rem 0.8rem;
}

/* Same panel-sized tint, same problem on dark — see the note on .st-finding. The flag rows keep
   --st-warning-text, which reads 8:1 on this lift. */
:root[data-theme="dark"] .st-review-flags {
    background: var(--st-pearl);
    border-color: var(--st-border);
}

.st-review-flag {
    display: flex;
    align-items: baseline;
    gap: 0.45rem;
    font-size: 0.85rem;
    color: var(--st-warning-text);
}

.st-review-flag + .st-review-flag {
    margin-top: 0.35rem;
}

/* Flagged rows are tinted, never hidden or blocked — the reviewer decides, not us. */
.st-review-row-flagged {
    background: var(--st-warning-bg);
}

/* A line the document says it is not sending. Set apart from a flagged row on purpose: nothing was
   misread, so it gets the neutral tint rather than the warning one. */
.st-review-row-unavailable {
    background: var(--st-pearl-light);
}

/* Keeps the text and number boxes from collapsing in a cramped table. Checkboxes are excluded
   deliberately: 4rem stretches one into a 64px rectangle, so an empty tick box reads as an empty
   text field and its label reads as the field's caption. On the invoice review that made every
   line of a 119-line document look like the supplier had marked it out of stock. */
.st-review-table input:not([type="checkbox"]) {
    min-width: 4rem;
}

.st-review-table .st-qty-input,
.st-review-table .st-cost-input {
    width: 6rem;
}

/* A quiet note under a field: what was read, why a row is flagged. */
.st-field-hint {
    display: block;
    margin-top: 0.15rem;
    font-size: 0.75rem;
    color: var(--st-warning-text);
}

/* Reading state on the order's document list. Colour is never the only signal — each badge
   carries an icon and a word, so it still reads correctly in greyscale. */
.st-reading-badge {
    background: var(--st-navy, #0a172d);
    color: var(--st-text-inverse);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    font-size: 0.65rem;
}

.st-failed-badge {
    background: var(--st-danger);
    color: var(--st-text-inverse);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    font-size: 0.65rem;
}

.st-reviewed-badge {
    background: var(--st-success);
    color: var(--st-text-inverse);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    font-size: 0.65rem;
}

/* Solid-fill badges invert on the dark theme, the same move .btn-primary makes in brand.css and for
   the same reason: the dark status tokens are the PALE end of each hue, so keeping white text on
   them leaves the label barely there. Fill with the pale token and set navy as the label instead,
   which reads 9:1 or better on all four. Light stays exactly as designed — dark-surface fills are
   the only case that needed rescuing. */
:root[data-theme="dark"] .st-tester-badge,
:root[data-theme="dark"] .st-reading-badge {
    background: var(--st-accent);
    color: var(--st-navy);
}

:root[data-theme="dark"] .st-unmatched-badge {
    background: var(--st-warning);
    color: var(--st-navy);
}

:root[data-theme="dark"] .st-failed-badge {
    background: var(--st-danger);
    color: var(--st-navy);
}

:root[data-theme="dark"] .st-reviewed-badge {
    background: var(--st-success);
    color: var(--st-navy);
}

/* --- Checking the four documents against each other ----------------------------------------
   The exception screen reproduces the purchasing team's REVIEW FILE. Two rules carried over
   from the rest of the interface: colour is never the only signal (a difference also carries a
   sign and a word), and a shortfall reads differently from an overage because they are not the
   same problem. */

/* The way in, from the order's documents panel. */
.st-match-link {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.6rem 0.75rem;
    margin-bottom: 1rem;
    border: 1px solid var(--st-border-subtle, #efebe6);
    border-radius: 0.375rem;
    text-decoration: none;
    color: inherit;
}

.st-match-link:hover {
    border-color: var(--st-gold, #bf8e36);
    background: var(--st-bg-subtle);
}

.st-match-link-text {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    font-weight: 600;
    color: var(--st-accent);
}

.st-match-link .badge {
    margin-left: auto;
}

/* Which of the four documents the check could actually see. */
.st-match-legs {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 1.25rem;
}

.st-match-leg {
    flex: 1 1 10rem;
    padding: 0.5rem 0.7rem;
    border: 1px solid var(--st-border-subtle, #efebe6);
    border-radius: 0.375rem;
}

.st-match-leg.is-missing {
    background: var(--st-pearl-light, #f7f5f3);
    color: var(--st-text-muted);
}

.st-match-leg-label {
    display: block;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--st-text-muted);
}

.st-match-leg-state {
    display: block;
    font-weight: 600;
    color: var(--st-accent);
}

.st-match-leg.is-missing .st-match-leg-state {
    color: var(--st-text-muted);
    font-weight: 500;
}

/* Which proformas the confirmed column was summed over, listed only when an order holds more than one.
   A replaced revision is dimmed rather than hidden: it is still on the order, and somebody comparing
   the screen against the document list has to be able to find it. */
.st-proforma-use {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem 1rem;
    padding-top: 0.9rem;
    font-size: 0.875rem;
}

.st-proforma-use-label {
    font-weight: 500;
}

.st-proforma-use li {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.4rem;
    padding: 0.15rem 0;
}

.st-proforma-use li.is-superseded {
    opacity: 0.65;
}

/* Ordered against invoiced, in units and in money. */
.st-match-headline {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
    padding: 0.9rem 0;
    border-top: 1px solid var(--st-border-subtle, #efebe6);
    border-bottom: 1px solid var(--st-border-subtle, #efebe6);
}

.st-match-figure-label {
    display: block;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--st-text-muted);
}

.st-match-figure {
    display: block;
    font-size: 1.15rem;
    font-weight: 600;
    color: var(--st-accent);
    font-variant-numeric: tabular-nums;
}

.st-match-verdict {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    margin-top: 0.9rem;
    font-weight: 600;
}

.st-match-verdict.is-clean,
.st-match-verdict.is-settled {
    color: var(--st-success);
}

.st-match-verdict.is-discrepant {
    color: var(--st-warning-text);
}

/* A difference we owe somebody an answer about. */

/* The list is as long as the order, which on a real PO carries the verdict, the totals and the page
   header off the top. Cap it and scroll the differences inside the card instead, the same bargain
   .st-scroll-table makes above — and the same height, rather than a second number to keep in step.
   Vertical only: a finding wraps, it never needs to scroll sideways. */
.st-finding-list {
    overflow-y: auto;
    max-height: min(32rem, calc(100vh - 16rem));
    padding-right: 0.35rem;   /* keeps the scrollbar off the Settle button */
}

.st-finding-list li + li {
    margin-top: 0.6rem;
}

.st-finding {
    padding: 0.65rem 0.8rem;
    border: 1px solid var(--st-warning-border);
    border-left: 3px solid var(--st-warning);
    border-radius: 0.375rem;
    background: var(--st-warning-bg);
}

/* The warm amber tint is a light-theme device and does not survive being scaled up on dark: at card
   size, #3a2c10 is a muddy olive block, and it is warm sitting on a cool blue-grey surface, so the
   two hues fight. On dark the card lifts in the SURFACE's own family instead, and the warning is
   carried entirely by the amber left rule — which is how .is-resolved below already works, so this
   is the existing structure rather than a new one. --st-pearl is reused rather than a new hex: it
   is already the theme's "one step above surface" neutral.
   Scoped to the cards deliberately. --st-warning-bg stays exactly as designed everywhere it is used
   small: .st-badge-warning, the flagged table rows, the log-level rows. */
:root[data-theme="dark"] .st-finding {
    background: var(--st-pearl);
    border-color: var(--st-border);
    border-left-color: var(--st-warning);
}

.st-finding.is-resolved {
    border-color: var(--st-border-subtle, #efebe6);
    border-left-color: var(--st-success);
    background: var(--st-surface);
}

.st-finding-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.3rem;
}

.st-finding-type {
    background: var(--st-warning);
    color: var(--st-text-inverse);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    font-size: 0.65rem;
}

.st-finding.is-resolved .st-finding-type {
    background: var(--st-text-muted);
}

/* Both fills go pale on dark, so the label flips to navy — see the badge block above. */
:root[data-theme="dark"] .st-finding-type,
:root[data-theme="dark"] .st-finding.is-resolved .st-finding-type {
    color: var(--st-navy);
}

.st-finding-where {
    font-size: 0.8rem;
}

.st-finding-body {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.6rem;
}

.st-finding-product {
    font-weight: 600;
    color: var(--st-accent);
}

.st-finding-code {
    font-family: var(--st-font-mono);
    font-size: 0.8rem;
}

.st-finding-numbers {
    margin-left: auto;
    font-size: 0.9rem;
    font-variant-numeric: tabular-nums;
}

.st-finding-settle {
    display: flex;
    gap: 0.5rem;
    margin-top: 0.55rem;
    max-width: 34rem;
}

/* The note field takes the slack; the buttons keep their own width. Without this the row's flex
   basis squeezes "Write off" into two lines and the input still ends up wider than it needs. */
.st-finding-settle .form-control {
    flex: 1 1 auto;
    min-width: 0;
}

.st-finding-settle .btn {
    flex: 0 0 auto;
    white-space: nowrap;
}

.st-finding-note {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem;
    margin-top: 0.45rem;
    font-size: 0.9rem;
}

.st-finding-note .btn-link {
    padding: 0;
    margin-left: auto;
}

/* The REVIEW FILE table. */
.st-match-table {
    font-variant-numeric: tabular-nums;
}

/* One order's back-orders. Left to itself the table spreads the row evenly, which spends its width on
   three-digit quantities and a date while the product name wraps to eight lines and the note box ends
   up narrower than one word of what somebody typed in it. So the figures, the date and the buttons
   shrink to their content (.st-col-shrink above) and the slack goes to the product and the note.

   The floor for the two controls sits on the control, not on the cell: a cell's min-width is only
   advisory once the table starts squeezing columns, an input's is not. */
.st-backorder-table {
    font-variant-numeric: tabular-nums;
}

/* Shrink to content but allow the wrap — these hold a badge or two, not a number. */
.st-backorder-table .st-col-tight {
    width: 1%;
}

/* Save and Write off stack, so the actions column is one button wide rather than two. That width
   comes back to the product name, which is the column people actually read down. */
.st-backorder-table .st-col-product {
    min-width: 20rem;
}

.st-backorder-table .st-col-decision .form-select {
    min-width: 8.5rem;
}

.st-backorder-table .st-col-note .form-control {
    min-width: 13rem;
}

/* Marks a price cell whose leg held more than one price. Deliberately small: the finding above the
   table is where the two prices are named, this only stops the cell reading as the whole story. */
.st-price-conflict {
    color: var(--st-gold);
    font-weight: 700;
    cursor: help;
}

/* The list price a document discounted to reach what it charges. Muted, and stated on the row rather
   than raised as a difference: the invoice explains itself, and suppressing the finding without
   showing the explanation would leave a price that looks changed with nothing to say why. */
.st-price-list {
    display: block;
    margin-top: 0.1rem;
    font-size: 0.75rem;
    font-weight: 400;
    color: var(--st-text-muted);
}

.st-match-table th[scope="row"] {
    font-weight: 500;
}

/* Which document a difference column reads against. Sits under the heading rather than beside it,
   so naming the leg never widens an already wide table. */
/* Reading the chain a step at a time (17 Aug backlog #13). The chosen step is filled rather
   than outlined, because the picker is a place you are rather than a filter you applied. */
.st-stage-picker {
    display: flex;
    flex-wrap: wrap;
    gap: 0.375rem;
    margin: 0.875rem 0 0.5rem;
}

.st-stage-chip {
    display: inline-flex;
    align-items: center;
    padding: 0.1875rem 0.625rem;
    border: 1px solid var(--st-border-strong);
    border-radius: 999px;
    background: var(--st-pearl-light);
    font-size: 0.8125rem;
    color: var(--st-text-muted);
    text-decoration: none;
    white-space: nowrap;
}

.st-stage-chip:hover {
    border-color: var(--st-accent);
    color: var(--st-accent);
}

.st-stage-chip.is-current {
    border-color: var(--st-accent);
    background: var(--st-accent);
    color: #fff;
}

.st-stage-chip.is-current:hover {
    color: #fff;
}

/* A step with nothing to report says so, rather than leaving an empty table to be read as a fault. */
.st-stage-quiet {
    color: var(--st-text-muted);
    font-size: 0.875rem;
}

.st-stage-folded,
.st-stage-hidden {
    color: var(--st-text-muted);
}

.st-th-basis {
    display: block;
    font-size: 0.7rem;
    font-weight: 400;
    text-transform: none;
    letter-spacing: 0;
    color: var(--st-text-muted);
}

.st-match-figure-basis {
    font-size: 0.75rem;
    font-weight: 400;
    color: var(--st-text-muted);
}

.st-match-product {
    display: block;
    font-weight: 600;
    color: var(--st-accent);
}

.st-match-code {
    font-family: var(--st-font-mono);
    font-size: 0.75rem;
}

.st-match-row-flagged {
    background: var(--st-warning-bg);
}

/* Short is the one that costs money and needs chasing; over is unexpected but not a loss. */
.st-diff-short {
    color: var(--st-danger);
    font-weight: 600;
}

.st-diff-over {
    color: var(--st-warning-text);
    font-weight: 600;
}

.st-diff-none {
    color: var(--st-text-muted);
}

/* --- Order tracking timeline (PUR-03) -------------------------------------------------------
   Sixteen steps, advanced by hand. The design job is that a long list still answers "where is
   this order?" at a glance, so the state of each step is carried by an icon and a word as well
   as by colour, and the one step the order is actually waiting on is marked out from the rest. */

.st-stage-summary {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}

.st-stage-waiting {
    font-size: 1.05rem;
    color: var(--st-accent);
}

.st-stage-progress {
    flex: 1 1 12rem;
    max-width: 20rem;
    height: 0.5rem;
    border-radius: 999px;
    background: var(--st-pearl-light, #f7f5f3);
    border: 1px solid var(--st-border-subtle, #efebe6);
    overflow: hidden;
}

.st-stage-progress-fill {
    display: block;
    height: 100%;
    background: var(--st-gold, #bf8e36);
}

.st-stage-list {
    margin: 0;
    padding: 0;
}

/* The spine the markers sit on. Drawn on the item rather than the list so the last step's line
   can be cut short of the page edge. */
.st-stage {
    position: relative;
    display: flex;
    gap: 0.9rem;
    padding: 0 0 1.25rem 0;
}

.st-stage::before {
    content: "";
    position: absolute;
    left: 0.6rem;
    top: 1.5rem;
    bottom: 0;
    width: 1px;
    background: var(--st-border-subtle, #efebe6);
}

.st-stage:last-child::before {
    display: none;
}

.st-stage-marker {
    position: relative;
    z-index: 1;
    flex: 0 0 auto;
    width: 1.2rem;
    line-height: 1.4;
    text-align: center;
    background: var(--st-surface);
    color: var(--st-text-muted);
}

.st-stage.is-done .st-stage-marker {
    color: var(--st-success);
}

.st-stage.is-active .st-stage-marker {
    color: var(--st-gold, #bf8e36);
}

.st-stage.is-skipped .st-stage-marker {
    color: var(--st-text-muted);
}

.st-stage-body {
    flex: 1 1 auto;
    min-width: 0;
}

.st-stage-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

.st-stage-name {
    font-weight: 600;
    color: var(--st-accent);
}

/* A step ruled out is still readable, just visibly out of the running. */
.st-stage.is-skipped .st-stage-name {
    text-decoration: line-through;
    color: var(--st-text-muted);
}

/* The one step the order is waiting on. A left rule rather than a fill, so it reads as emphasis
   and not as a warning. */
.st-stage.is-next .st-stage-body {
    border-left: 3px solid var(--st-gold, #bf8e36);
    padding-left: 0.7rem;
    margin-left: -0.7rem;
}

.st-stage-status {
    background: var(--st-pearl-light, #f7f5f3);
    color: var(--st-accent);
    border: 1px solid var(--st-border-subtle, #efebe6);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    font-size: 0.65rem;
}

.st-stage.is-done .st-stage-status {
    background: var(--st-success);
    border-color: var(--st-success);
    color: var(--st-text-inverse);
}

/* Pale fill on dark, so the label flips to navy — see the badge block further up. */
:root[data-theme="dark"] .st-stage.is-done .st-stage-status {
    color: var(--st-navy);
}

.st-stage-meta {
    margin-top: 0.15rem;
}

.st-stage-form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.6rem;
}

.st-stage-form .form-select {
    max-width: 9rem;
}

.st-stage-form input[type="date"] {
    max-width: 10rem;
}

.st-stage-form input[type="text"] {
    flex: 1 1 12rem;
    min-width: 8rem;
}

.st-stage-docs {
    margin-top: 0.6rem;
}

.st-stage-docs > summary {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    cursor: pointer;
    font-size: 0.85rem;
    color: var(--st-text-muted);
}

.st-stage-docs > summary:hover {
    color: var(--st-accent);
}

/* The way into the timeline from the order screen. */
.st-track-link .text-muted {
    margin-left: auto;
}

/* The same strip when the order is stopped by a proforma somebody sent back. The accent and the
   icon carry it rather than the whole strip turning red: this is a step to go and redo, not an
   error on the screen. */
.st-track-link.is-sent-back {
    border-color: var(--st-danger);
    border-left-width: 3px;
}

.st-track-link.is-sent-back .bi,
.st-track-link.is-sent-back strong {
    color: var(--st-danger);
}

/* Whatever the approver typed. Quieter than the sentence above it, and never in place of it: a
   rejection with no reason still has to read as a rejection. */
.st-sent-back-why {
    margin-left: 0.4rem;
    color: var(--st-text-muted);
}

/* ------------------------------------------------------------------------------------------
   Landed cost — the headline strip, and the heads that write it.

   The strip heads the order screen because what an order costs to have here is the figure
   people open it for; it wears the gold edge and a figure a size larger than anything else on
   the screen so it reads before the tracking line under it. The heads themselves sit in the
   narrow column beside the order's own fields, one per row: six across a half-width card wrap
   into something nobody can read down.
   ------------------------------------------------------------------------------------------ */

.st-landed-strip {
    align-items: baseline;
    border-left: 3px solid var(--st-gold, #bf8e36);
}

.st-landed-strip-body {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 0;
}

.st-landed-strip-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem;
}

.st-landed-figure {
    font-size: 1.3rem;
    line-height: 1.2;
}

/* Pushed to the far end of the headline, the way the tracking counter is. */
.st-landed-strip-aside {
    margin-left: auto;
    text-align: right;
}

/* The either-or above the heads: recorded bills, or a percentage of the invoice. A fieldset because
   the two radios are one question, and the legend is what a screen reader reads before either of
   them. Kept off Bootstrap's own legend sizing, which sets it full width and a size larger. */
.st-landed-basis {
    margin-bottom: 0.75rem;
}

.st-landed-basis > legend {
    float: none;
    width: auto;
    margin-bottom: 0.35rem;
    font-size: inherit;
}

/* Side by side while there is room. They are two words each, and stacked they read as a list of
   settings rather than as one choice with two answers. */
.st-landed-basis-choices {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 1.5rem;
}

.st-landed-heads {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

/* The block that is not in force is hidden outright rather than dimmed: its figures are about to be
   cleared by the next save, and showing them greyed would read as money the order still carries. */
.st-landed-heads[hidden] {
    display: none;
}

/* Name, amount, currency. The currency box is narrow on purpose: it holds three letters. */
.st-landed-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 7.5rem 4.5rem;
    gap: 0.5rem;
    align-items: center;
}

.st-landed-amount {
    text-align: right;
}

.st-landed-total {
    margin-top: 0.25rem;
    padding-top: 0.6rem;
    border-top: 1px solid var(--st-border-subtle, #efebe6);
}

/* The subtotal that closes the freight block. A lighter rule than the total's, and no top margin, so
   it reads as a line drawn under four rows rather than as a second total competing with the real one. */
.st-landed-subtotal {
    padding-top: 0.5rem;
    padding-bottom: 0.35rem;
    border-top: 1px solid var(--st-border-subtle, #efebe6);
}

/* The two named charges. The caption is a text box on those rows, and left at full input weight it
   pulls the eye away from the figures, which are what the panel is for. */
.st-landed-head > input[type="text"]:first-child {
    font-size: 0.8125rem;
}

/* One line per currency, so the total spans both boxes rather than pretending to be one. */
.st-landed-total-figures {
    grid-column: 2 / -1;
    text-align: right;
}

/* On a phone the label takes its own line rather than squeezing to three characters. */
@media (max-width: 30rem) {
    .st-landed-head {
        grid-template-columns: minmax(0, 1fr) 4.5rem;
    }

    .st-landed-head > .st-label,
    .st-landed-head > input[type="text"]:first-child {
        grid-column: 1 / -1;
    }
}

/* ------------------------------------------------------------------------------------------
   Purchasing document repository (PUR-04) — a navigation tree beside the result list.

   The tree narrows; the list answers. On a phone the two stack, tree first, because someone
   reaching for a brand on a small screen is doing so to shorten the list they scroll next.
   ------------------------------------------------------------------------------------------ */

.st-doc-repository {
    display: grid;
    grid-template-columns: minmax(13rem, 17rem) minmax(0, 1fr);
    gap: 1.25rem;
    align-items: start;
}

@media (max-width: 62rem) {
    .st-doc-repository {
        grid-template-columns: minmax(0, 1fr);
    }
}

.st-doc-tree {
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius);
    padding: 0.85rem;
    /* Follows the reader down a long result list rather than scrolling away at the first page. */
    position: sticky;
    top: calc(var(--st-header-height, 4rem) + 0.75rem);
    max-height: calc(100vh - var(--st-header-height, 4rem) - 2rem);
    overflow-y: auto;
}

@media (max-width: 62rem) {
    .st-doc-tree {
        position: static;
        max-height: none;
    }
}

.st-doc-tree-title {
    font-family: var(--st-font-heading);
    font-size: 0.95rem;
    color: var(--st-accent);
    margin: 0 0 0.5rem;
}

.st-doc-tree-clear {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    font-size: 0.85rem;
    margin-bottom: 0.6rem;
}

.st-doc-tree-brands,
.st-doc-tree-orders {
    list-style: none;
    margin: 0;
    padding: 0;
}

.st-doc-tree-orders {
    /* A rule down the left says these belong to the brand above without indenting them off-screen. */
    margin: 0.2rem 0 0.4rem 0.45rem;
    padding-left: 0.55rem;
    border-left: 1px solid var(--st-border-subtle);
}

.st-doc-tree-brand > summary {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    cursor: pointer;
    padding: 0.35rem 0.25rem;
    border-radius: var(--st-radius);
    color: var(--st-text);
}

.st-doc-tree-brand > summary:hover {
    background: var(--st-bg-subtle);
    color: var(--st-accent);
}

.st-doc-tree-brand[open] > summary {
    color: var(--st-accent);
    font-weight: 600;
}

.st-doc-tree-name {
    overflow-wrap: anywhere;
}

.st-doc-tree-order {
    display: block;
    padding: 0.3rem 0.4rem;
    border-radius: var(--st-radius);
    text-decoration: none;
    color: var(--st-text);
    font-size: 0.88rem;
}

.st-doc-tree-order:hover {
    background: var(--st-bg-subtle);
    color: var(--st-accent);
}

/* The active branch is marked by weight and a gold edge, never by colour alone. */
.st-doc-tree-order.is-active {
    font-weight: 600;
    color: var(--st-accent);
    box-shadow: inset 2px 0 0 var(--st-gold);
}

.st-doc-tree-any {
    font-style: italic;
}

.st-doc-tree-ref {
    display: block;
    overflow-wrap: anywhere;
}

.st-doc-tree-meta {
    display: block;
    font-size: 0.78rem;
    color: var(--st-text-muted);
}

.st-doc-tree-empty {
    font-size: 0.82rem;
    color: var(--st-text-muted);
    margin: 0.3rem 0.4rem;
}

.st-doc-results {
    min-width: 0;
}

/* ---------------------------------------------------------------------------
   Dashboard panels

   Every module's panel is rendered by the same partial, so the look is defined
   once here rather than by whoever contributed it. Colour for the plots
   themselves lives in brand.css with the rest of the palette; nothing in this
   block names a series hue.
   --------------------------------------------------------------------------- */
/* One tab per department, plus the Overview. Underlined rather than boxed: the strip sits directly
   above a grid of bordered tiles, and a second row of boxes would compete with them for the eye.
   It scrolls sideways instead of wrapping, so a narrow screen never pushes the panels down a row
   the moment a fifth department ships. */
.st-dashboard-tabs {
    display: flex;
    gap: 0.25rem;
    margin: -0.5rem 0 1.25rem;
    border-bottom: 1px solid var(--st-border);
    overflow-x: auto;
}

.st-dashboard-tab {
    flex: 0 0 auto;
    padding: 0.5rem 0.85rem;
    color: var(--st-text-muted);
    text-decoration: none;
    font-weight: 500;
    white-space: nowrap;
    /* Transparent rather than absent, so gaining the accent underline does not move the label. */
    border-bottom: 2px solid transparent;
    transition: color var(--st-transition), border-color var(--st-transition);
}

.st-dashboard-tab:hover {
    color: var(--st-text);
    border-bottom-color: var(--st-border-strong);
}

/* Weight and colour carry the current tab as well as the rule does, so it still reads where a
   border is dropped — forced-colours mode, or a printout. */
.st-dashboard-tab.is-current {
    color: var(--st-text);
    font-weight: 600;
    border-bottom-color: var(--st-accent);
}

.st-dashboard-tab:focus-visible {
    outline: 2px solid var(--st-accent);
    outline-offset: -2px;
    border-radius: var(--st-radius) var(--st-radius) 0 0;
}

/* The window every panel reports on. One bar for the whole screen rather than a control per panel:
   the range is one time context, and two panels answering for two different periods is a wrong
   reading that nothing on the page would give away.

   It wraps rather than scrolling, unlike the tab strip above it: these are inputs, and an input that
   has scrolled out of sight is worse than one that has moved to a second line. */
/* The date bar and the panel picker on one line. The bar carries the spacing below, not the form
   inside it, so the two controls cannot drift apart vertically. */
.st-dashboard-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 1.25rem;
}

/* Takes the row and leaves the picker at the end of it. Its own controls are sized to their content,
   so growing the form is what puts the window text and the picker button against the right edge
   rather than in the middle of the line. */
.st-dashboard-range {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    flex: 1 1 20rem;
    margin: 0;
}

/* Whitespace in the markup is not enough of a gap between an icon and its label at this size. */
.st-dashboard-manage > .bi {
    margin-right: 0.35rem;
}

.st-dashboard-range-label {
    margin: 0;
    color: var(--st-text-muted);
    font-size: 0.85rem;
}

/* Sized to their content rather than stretching: a period name and a date are both short, and a
   full-width select above a grid of tiles reads as the page's main control, which it is not. */
.st-dashboard-range-preset {
    width: auto;
    min-width: 11rem;
}

.st-dashboard-range-date {
    width: auto;
}

/* Pushed to the end of the line, so the window in force is stated where the eye leaves the controls
   rather than between them. */
.st-dashboard-range-window {
    margin: 0 0 0 auto;
    color: var(--st-text-muted);
    font-size: 0.85rem;
}

/* A full-width flex item, so the reason a range was refused sits under the boxes it is about rather
   than squeezing them onto another line. */
.st-dashboard-range-error {
    flex: 1 0 100%;
    margin: 0;
    color: var(--st-danger);
    font-size: 0.85rem;
}

/* The panel picker's list. A plain list rather than the dropdown the column chooser uses: a panel is
   identified by a title and a sentence of subtitle, which is more than a dropdown item can hold
   without turning into a wall. */
.st-panel-picker-intro {
    margin-bottom: 1rem;
    color: var(--st-text-muted);
    font-size: 0.9rem;
}

.st-panel-picker {
    margin: 0;
    padding: 0;
    list-style: none;
}

/* The box and the label side by side, the label's two lines stacked. Aligned to the top rather than
   centred: against a two-line label a centred checkbox sits beside the gap between the lines. */
.st-panel-choice {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    padding: 0.6rem 0;
    border-top: 1px solid var(--st-border);
}

.st-panel-choice:first-child {
    border-top: 0;
}

/* The checkbox is nudged onto the title's line; Bootstrap's own top margin lines it up with a single
   line of body text, which the title is not. */
.st-panel-choice > .form-check-input {
    flex: none;
    margin-top: 0.2rem;
}

/* The whole label is the hit target, so the subtitle is part of what can be clicked to tick.
   It takes the spare width as well, which is what holds the arrows against the right edge without
   an auto margin of their own. */
.st-panel-choice .form-check-label {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    gap: 0.15rem;
    cursor: pointer;
}

/* The two arrows stack, so a row is no taller for having them and the pair reads as one control.
   Top-aligned with the checkbox and the title, not centred against a two-line label. */
.st-panel-choice-move {
    display: flex;
    flex: none;
    flex-direction: column;
    gap: 0.15rem;
}

.st-panel-move {
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius);
    background: var(--st-surface);
    color: var(--st-text-muted);
    padding: 0 0.4rem;
    font-size: 0.7rem;
    line-height: 1.5;
    cursor: pointer;
    transition: background var(--st-transition), color var(--st-transition);
}

.st-panel-move:hover:not(:disabled) {
    background: var(--st-bg-subtle);
    color: var(--st-text);
}

.st-panel-move:focus-visible {
    outline: 2px solid var(--st-accent);
    outline-offset: -2px;
}

/* A panel at the top of the list has nowhere up to go. Shown as unavailable rather than removed,
   so the pair does not change shape as rows move past the ends. */
.st-panel-move:disabled {
    opacity: 0.45;
    cursor: default;
}

.st-panel-choice-title {
    font-weight: 600;
}

.st-panel-choice-subtitle {
    color: var(--st-text-muted);
    font-size: 0.85rem;
}

/* How many panels sit across is one number, declared once. Going from two up to three is then a
   class on the grid rather than a new set of column classes on every panel — which matters because
   the panels are contributed by modules and the Host is the only place that may decide the layout.
   minmax(0, 1fr) rather than 1fr: a track sized to its content lets a canvas push the column wider
   than the screen, and a chart is exactly the element that does it. */
.st-dashboard {
    --st-dashboard-columns: 1;
    display: grid;
    grid-template-columns: repeat(var(--st-dashboard-columns), minmax(0, 1fr));
    gap: 1rem;
}

@media (min-width: 992px) {
    .st-dashboard {
        --st-dashboard-columns: 2;
    }
}

/* The denser setting, applied by adding one class. Nothing applies it yet: two across is the agreed
   layout until there are enough panels to want a third, at which point this is the whole change.
   Held back to very wide screens because three tiles across a laptop leaves a chart too narrow to
   label. */
@media (min-width: 1600px) {
    .st-dashboard.st-dashboard-dense {
        --st-dashboard-columns: 3;
    }
}

.st-widget {
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-lg);
    padding: 1.1rem 1.25rem 1.25rem;
    height: 100%;
    display: flex;
    flex-direction: column;
    /* Nothing inside a tile may set its width: without this a long axis label or a wide canvas
       makes the track grow and the whole grid drifts off the right of the page. */
    min-width: 0;
}

/* The modal's heading block, where there is room for the subtitle to sit under the title. */
.st-widget-heading {
    min-width: 0;
}

.st-widget-heading .st-widget-subtitle {
    margin-top: 0.2rem;
}

/* Wraps rather than squeezes. The controls are a fixed width, so on a narrow tile an unwrapped row
   would take the difference out of the title and leave it two words a line. */
.st-widget-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem 1rem;
    margin-bottom: 0.9rem;
}

.st-widget-title {
    font-family: var(--st-font-heading);
    font-size: 1.05rem;
    margin: 0;
    /* Takes the room the controls leave, down to a floor at which they wrap below instead. */
    flex: 1 1 11rem;
    min-width: 0;
}

/* A whole line to itself, whatever happened above it: a sentence broken into a four-word column is
   most of a tile's height spent on the caption. margin-left keeps the controls to the right when
   they are the ones that wrapped. */
.st-widget-subtitle {
    flex: 0 0 100%;
    font-size: 0.82rem;
    color: var(--st-text-muted);
    margin: 0;
}

.st-widget-actions {
    flex: none;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    /* Stays at the right edge on its own line too, for when the title took the first one. */
    margin-left: auto;
}

.st-widget-more {
    flex: none;
    font-size: 0.82rem;
    white-space: nowrap;
    text-decoration: none;
}

/* Chart form switch. A segmented control rather than a dropdown: there are two or three
   choices, and seeing which one is in force matters more than saving the space. */
.st-chart-kinds {
    display: inline-flex;
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius);
    overflow: hidden;
}

.st-chart-kind {
    border: 0;
    background: var(--st-surface);
    color: var(--st-text-muted);
    padding: 0.2rem 0.5rem;
    font-size: 0.85rem;
    line-height: 1.4;
    cursor: pointer;
    transition: background var(--st-transition), color var(--st-transition);
}

.st-chart-kind + .st-chart-kind {
    border-left: 1px solid var(--st-border);
}

.st-chart-kind:hover {
    background: var(--st-bg-subtle);
    color: var(--st-text);
}

/* The selected form is filled, not merely tinted: this control sits among muted chrome and a
   faint state would be missed. */
.st-chart-kind.is-active {
    background: var(--st-accent);
    color: var(--st-text-inverse);
}

/* Same inversion the primary button makes: in dark the accent is a pale blue, so a white label on
   it would be barely readable. The fill stays the accent and the label goes navy. */
:root[data-theme="dark"] .st-chart-kind.is-active {
    color: var(--st-navy);
}

.st-chart-kind:focus-visible {
    outline: 2px solid var(--st-accent);
    outline-offset: -2px;
}

/* The same segmented control carrying words instead of icons, for choosing which reading of a report
   is drawn. There is no glyph for "over time" against "by brand", so these segments are text and take
   the room text needs. */
.st-chart-sources .st-chart-kind {
    padding-inline: 0.65rem;
    white-space: nowrap;
}

/* Quiet until wanted: on a tile the chart is the content and the chrome around it should not
   compete. Same footprint as one segment of the form switch beside it. */
.st-widget-expand,
.st-widget-data {
    flex: none;
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius);
    background: var(--st-surface);
    color: var(--st-text-muted);
    padding: 0.2rem 0.5rem;
    font-size: 0.85rem;
    line-height: 1.4;
    cursor: pointer;
    transition: background var(--st-transition), color var(--st-transition);
}

.st-widget-expand:hover,
.st-widget-data:hover {
    background: var(--st-bg-subtle);
    color: var(--st-text);
}

.st-widget-expand:focus-visible,
.st-widget-data:focus-visible {
    outline: 2px solid var(--st-accent);
    outline-offset: -2px;
}

/* Filled while the figures are showing, matching the form switch: this is a state someone is
   currently in, not an action waiting to be taken. */
.st-widget-data[aria-expanded="true"] {
    background: var(--st-accent);
    border-color: var(--st-accent);
    color: var(--st-text-inverse);
}

/* The dark theme's accent is a pale blue, so white on it would barely read. Same inversion the
   primary button and the form switch make. */
:root[data-theme="dark"] .st-widget-data[aria-expanded="true"] {
    color: var(--st-navy);
}

.st-widget-more i {
    font-size: 0.72rem;
}

.st-widget-empty {
    color: var(--st-text-muted);
    margin: 0.4rem 0 0;
}

/* Figures sit above the plot. auto-fit rather than a fixed count because the number of
   tiles is data: one per currency in play, plus the counts. */
.st-widget-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
    gap: 0.85rem 1.5rem;
    margin-bottom: 1.1rem;
}

.st-stat-label {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--st-text-muted);
}

.st-stat-value {
    font-family: var(--st-font-numeric);
    font-size: 1.55rem;
    line-height: 1.2;
    color: var(--st-text);
}

.st-stat-unit {
    font-size: 0.8rem;
    color: var(--st-text-muted);
    margin-left: 0.15rem;
}

.st-stat-caption {
    font-size: 0.78rem;
    color: var(--st-text-muted);
}

/* The same tiles as a widget's, on a report's summary card. auto-fit for the reason
   .st-widget-stats uses it: the number of tiles is data, not layout. This card carries four
   or five depending on whether the window has a landed cost, and a fixed twelve-column split
   stranded the fifth on a row of its own underneath the first. */
.st-summary-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
    gap: 1.25rem 2rem;
}

/* The caption qualifies the figure; it is never a paragraph competing with it. It sits tight
   under the number and wraps at a readable width rather than running the tile's full span,
   because a caveat set louder than its own figure reads as a warning rather than a total. */
.st-summary-stats .st-stat-caption {
    margin-top: 0.35rem;
    line-height: 1.4;
    max-width: 22rem;
}

.st-summary-stats .st-stat-caption + .st-stat-caption {
    margin-top: 0.2rem;
}

/* The way into the lines a caption is talking about, given its own line at the end of that
   caption. Inline, it ended a sentence that the next sentence opened with a figure, and the
   link text ran into that figure as one phrase. */
.st-summary-stats .st-stat-caption-link {
    display: block;
    margin-top: 0.15rem;
}

/* A report's summary tiles name their figures in sentence case, unlike the dashboard widgets
   these tiles otherwise share. The label sits above a figure the reader is here to read, and
   setting it in caps gave a heading more presence than the number under it. */
.st-summary-stats .st-stat-label {
    text-transform: none;
    letter-spacing: 0;
}

/* The height belongs to the wrapper, not the canvas: Chart.js sizes the canvas itself,
   and a canvas carrying its own CSS height fights the resize instead of following it. */
/* The plot takes whatever the tile has left, never less than this. Tiles in a row are the same
   height whatever is above their charts, so a panel with no headline figures beside one that has
   three would otherwise carry the difference as blank space under its own heading. Growing into it
   spends that space on the chart instead — which is the point of the tile. */
.st-chart {
    position: relative;
    flex: 1 1 210px;
    min-height: 210px;
}

/* The same figures as a table. It replaces the plot rather than sitting under it, so the panel
   keeps its height and the grid does not reflow around whoever is reading. */
.st-widget.is-showing-data .st-chart {
    display: none;
}

/* Takes the tile's spare room exactly as the plot it replaces does, so switching between them does
   not move anything, and a table never opens against the bottom edge with the panel empty above it. */
.st-widget.is-showing-data .st-chart-data {
    flex: 1 1 210px;
    min-height: 210px;
    overflow: auto;
}

.st-chart-table {
    font-size: 0.85rem;
}

/* A row's own heading is what it is, not a second emphasis: the figures are the point. */
.st-chart-table tbody th {
    font-weight: 500;
    text-align: left;
    white-space: nowrap;
}

/* Figures line up under each other so they can be compared down the column, which is most of
   the reason to read a chart as a table at all. */
.st-col-numeric {
    text-align: right;
    font-family: var(--st-font-numeric);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.st-chart-table thead th.st-col-numeric {
    font-family: var(--st-font-body);
}

/* ---------------------------------------------------------------------------
   Enlarged chart

   The tile is a summary; this is where a dense axis becomes readable. The chart
   is the same data drawn bigger, never a different query.
   --------------------------------------------------------------------------- */
.st-chart-modal .modal-content {
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-lg);
}

.st-chart-modal .modal-header {
    align-items: flex-start;
    gap: 1rem;
    border-bottom: 1px solid var(--st-border);
}

/* Bootstrap's .modal-header does not set justify-content: it right-aligns by putting
   margin-left:auto on .btn-close alone. Ours is nested inside the actions group, so that
   margin only pushed the close button away from the form switch and left the whole group
   floating beside the title. The auto margin belongs on the group instead. */
.st-chart-modal .modal-header .st-widget-actions {
    margin-left: auto;
}

.st-chart-modal .modal-header .btn-close {
    margin-left: 0;
}

.st-chart-modal .st-widget-title {
    font-size: 1.15rem;
}

/* Tall enough to be worth opening, but bounded by the viewport: a modal that scrolls
   its own body puts half the chart out of sight, which defeats enlarging it. */
.st-chart-large {
    height: min(60vh, 30rem);
    min-height: 18rem;
    margin-top: 0;
}

/* ---------------------------------------------------------------------------
   Cross-order board (plan §6.8.1) — one row per order, one column per stage.

   The stage columns scroll inside their own container so the page body never
   scrolls sideways, and the three identifying columns are sticky, because a row
   twenty columns wide is unreadable once you cannot see which order it is.
   --------------------------------------------------------------------------- */

.st-board-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0.75rem;
    padding: 0.9rem 1rem;
    margin-bottom: 0.75rem;
    background: var(--st-surface, #fff);
    border: 1px solid var(--st-border-subtle, #efebe6);
    border-radius: 0.5rem;
}

.st-board-filter {
    flex: 1 1 11rem;
    min-width: 0;
}

.st-board-filter .form-label {
    margin-bottom: 0.15rem;
    font-size: 0.75rem;
    color: var(--st-text-muted);
}

.st-board-filter-check,
.st-board-filter-actions {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.st-board-phases {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-bottom: 0.75rem;
}

.st-board-phase {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.3rem 0.7rem;
    border: 1px solid var(--st-border-subtle, #efebe6);
    border-radius: 2rem;
    background: var(--st-surface, #fff);
    color: var(--st-text);
    text-decoration: none;
    font-size: 0.85rem;
}

.st-board-phase .badge {
    background: var(--st-pearl-light, #f7f5f3);
    color: var(--st-accent);
    font-weight: 600;
}

/* Selected reads as weight and a rule, not as a colour fill alone. */
.st-board-phase.is-active {
    border-color: var(--st-accent);
    font-weight: 600;
}

.st-board-scroll {
    overflow-x: auto;
    border: 1px solid var(--st-border-subtle, #efebe6);
    border-radius: 0.5rem;
    background: var(--st-surface, #fff);
}

.st-board {
    border-collapse: separate;
    border-spacing: 0;
    width: max-content;
    min-width: 100%;
    font-size: 0.85rem;
}

.st-board th,
.st-board td {
    padding: 0.5rem 0.6rem;
    border-bottom: 1px solid var(--st-border-subtle, #efebe6);
    vertical-align: top;
    white-space: nowrap;
    background: var(--st-surface, #fff);
}

.st-board thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--st-pearl-light, #f7f5f3);
    font-weight: 600;
    color: var(--st-accent);
    border-bottom: 1px solid var(--st-border, #e2ddd7);
}

/* The stage headings are the long ones and there are sixteen of them, so they are given a fixed
   narrow column and allowed to wrap rather than setting the width of the whole board. */
.st-board-stage {
    width: 4.5rem;
    text-align: center;
}

.st-board-stage span {
    display: block;
    white-space: normal;
    font-size: 0.7rem;
    line-height: 1.15;
}

.st-board-sticky {
    position: sticky;
    z-index: 1;
}

.st-board thead .st-board-sticky {
    z-index: 3;
}

.st-board-brand { left: 0; width: 10rem; }
.st-board-ref { left: 10rem; width: 12rem; }
.st-board-status { left: 22rem; width: 13rem; border-right: 1px solid var(--st-border, #e2ddd7); }

.st-board-brand,
.st-board-ref,
.st-board-status {
    white-space: normal;
}

/* The board sizes itself to its content, so a width on the cell is only a floor. The brand names
   need a block of their own to hold the column at ten rems and wrap the rest onto a second line.
   The wrapping is set here rather than on the cell: `.st-board td` is the more specific selector
   and keeps every cell on one line, which is what widened this column in the first place. */
.st-board-brands {
    display: block;
    width: 10rem;
    white-space: normal;
    overflow-wrap: break-word;
}

.st-board-sub {
    display: block;
    font-size: 0.75rem;
}

/* The way to the order record itself, sat under the reference that opens the timeline. Inline-flex
   rather than block so the underline runs beneath the words and not beneath the icon. */
.st-board-open {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    margin-top: 0.2rem;
    font-size: 0.75rem;
}

.st-board-open i {
    text-decoration: none;
}

.st-board-number {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

.st-board-phase-badge {
    display: inline-block;
    padding: 0.1rem 0.45rem;
    border: 1px solid var(--st-border-subtle, #efebe6);
    border-radius: 0.3rem;
    background: var(--st-pearl-light, #f7f5f3);
    color: var(--st-accent);
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

.st-board-cell {
    text-align: center;
    padding-left: 0.2rem;
    padding-right: 0.2rem;
}

/* Shape carries the state; colour only reinforces it. In greyscale the four marks are still a
   filled circle, an arrow, a slash and an outline. */
.st-board-cell a {
    color: var(--st-text-muted);
    text-decoration: none;
}

.st-board-cell.is-done a { color: var(--st-success); }
.st-board-cell.is-active a { color: var(--st-gold, #bf8e36); }
.st-board-cell.is-skipped a { color: var(--st-text-muted); opacity: 0.6; }

/* The step an order is late for. A filled exclamation rather than an outline circle, so the column
   that stopped is findable in greyscale and by a reader who is only skimming shapes. */
.st-board-cell.is-overdue a { color: var(--st-warning-text); }

/* A proforma somebody sent back. Red rather than the overdue amber: a date that has passed is a
   promise slipping, while this is a step that was refused and needs somebody to act. The two never
   land on the same cell - a rejection stops the order on the step before this one. */
.st-board-cell.is-sent-back a { color: var(--st-danger); }

.st-board-wait {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.3rem;
    margin-top: 0.15rem;
}

/* Deliberately quieter than the badge beside it: an order nobody has touched is worth noticing, not
   worth the same alarm as a date that has been and gone. */
.st-quiet-badge {
    background: var(--st-pearl-light, #f7f5f3);
    color: var(--st-text-muted);
    border: 1px solid var(--st-border, #e2ddd7);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    font-size: 0.65rem;
}

.st-board-attention.is-active {
    border-color: var(--st-warning);
}

.st-board tbody tr:hover td {
    background: var(--st-pearl-light, #f7f5f3);
}

.st-board-paging {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin-top: 0.75rem;
}

.st-board-paging-links {
    display: flex;
    gap: 0.4rem;
}

/* ---------------------------------------------------------------------------
   Alert detail — "What it says" renders the email's own markup verbatim, so
   everything inside carries the inline styles the alert services wrote for an
   inbox. That is right in mail and wrong here for one thing: the call to
   action is filled navy, and navy on the dark theme's page background is the
   same vanishing silhouette .btn-primary already inverts to avoid (see the
   note at the top of brand.css). Inverted here to match, and matched on the
   inline fill every alert service writes rather than a class each of them
   would have to remember. !important is unavoidable: the rule is competing
   with an inline style.
   --------------------------------------------------------------------------- */
.st-alert-body a[style*="background:#0A172D"] {
    display: inline-block;
}

:root[data-theme="dark"] .st-alert-body a[style*="background:#0A172D"] {
    background: var(--st-accent) !important;
    color: var(--st-navy) !important;
}

:root[data-theme="dark"] .st-alert-body a[style*="background:#0A172D"]:hover,
:root[data-theme="dark"] .st-alert-body a[style*="background:#0A172D"]:focus-visible {
    background: var(--st-accent-hover) !important;
    color: var(--st-navy) !important;
}

/* ---------------------------------------------------------------------------
   Skip to main content (WCAG 2.4.1)

   Off-screen until it is focused, which for this link means a keyboard user has
   just pressed Tab on a fresh page. Every screen's sidebar carries the links of
   every module the reader may open, so without this the first Tab press starts a
   walk through the whole navigation before the screen itself is reachable.

   Sits above every other layer (topbar 1030, palette 1080, help drawer 1070):
   when it is showing, nothing else on the page holds focus.
   --------------------------------------------------------------------------- */
.st-skip-link {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 1090;
    padding: 0.5rem 1rem;
    background: var(--st-surface);
    color: var(--st-accent);
    border: 1px solid var(--st-border-strong);
    border-radius: 0 0 var(--st-radius) 0;
    box-shadow: var(--st-shadow);
    font-weight: 500;
    text-decoration: none;
    transform: translateY(-120%);
}

.st-skip-link:focus,
.st-skip-link:focus-visible {
    transform: translateY(0);
}

/* ---------------------------------------------------------------------------
   The mark on the error pages.

   Both dimensions are set, as attributes on the tag as well as here, so the box
   is reserved before the image arrives and keeps its shape if it never does.
   With only a height, a mark that fails to load collapses to no width at all and
   takes its own alt text off the screen with it, so a missing image reads as a
   blank page rather than as a missing image. object-fit keeps the artwork
   undistorted if it is ever re-exported at a different ratio.
   --------------------------------------------------------------------------- */
.st-error-brand {
    display: block;
    width: 127px;   /* the artwork is 507x128, which is this wide at 32px tall */
    height: 32px;
    object-fit: contain;
    margin-bottom: 1.5rem;
}

/* A badge that carries no status, only a note: the active product filter, and the tag saying a period
   rests on a proforma. Bootstrap's .text-bg-light was doing this job and is a fixed light fill, so on
   the dark theme both rendered as a white pill with black text - the one thing on the page that had
   not been told the theme changed. Built from the surface tokens instead, so it follows both. */
.st-soft-badge {
    background: var(--st-pearl-light);
    color: var(--st-text);
    border: 1px solid var(--st-border);
    font-weight: 500;
}
