/* ==========================================================================
   SETU Shared Components
   Cards, buttons, pills, forms, messages, tables.
   All colours reference tokens.css custom properties.

   Type-scale discipline:
   - Normal page UI (forms, buttons, pipeline cards) stays at --s-font-size-base.
   - Data-dense list/table views use .data-table (sm/xs tokens, tight rows).
   ========================================================================== */

/* ---- Utility ---- */
.muted {
    color: var(--s-color-text-secondary);
}

/* ---- Cards ---- */
.card {
    background-color: var(--s-color-surface);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-lg);
    box-shadow: var(--s-shadow-sm);
    padding: var(--s-space-4) var(--s-space-5);
    margin-bottom: var(--s-space-4);
    transition: background-color var(--s-transition-normal),
                border-color var(--s-transition-normal);
}

/* ---- Summary tiles: a handful of headline figures at a glance ----
   For a card whose whole job is "glance and know the number" (accounting
   home, dashboards). Each tile is a link when the figure has a page behind
   it; the value is the thing your eye lands on, the label sits above it in
   the reading order but should read as quiet as a caption. */
.summary-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: var(--s-space-4);
}
.summary-tile {
    display: block;
    background: var(--s-color-bg-alt);
    border: 1px solid transparent;
    border-radius: var(--s-radius-lg);
    padding: var(--s-space-3) var(--s-space-4);
}
a.summary-tile:hover {
    border-color: var(--s-color-border-strong);
    background: var(--s-color-surface-hover);
}
.summary-tile-label {
    font-size: var(--s-font-size-sm);
    color: var(--s-color-text-secondary);
}
.summary-tile-value {
    font-size: var(--s-font-size-3xl);
    font-weight: var(--s-font-weight-semibold);
    color: var(--s-color-text);
    margin: var(--s-space-1) 0;
    line-height: var(--s-line-height-tight);
}
.summary-tile-note {
    font-size: var(--s-font-size-xs);
    color: var(--s-color-text-secondary);
}
.summary-tile-note.warning {
    color: var(--s-color-warning-text);
}

/* ---- Modal (Alpine, x-show + x-cloak) ----
   Generic shell in templates/common/components/modal.html. Backdrop dims
   the page and closes on click; the panel is centered and the backdrop
   scrolls if the panel is taller than the viewport. */
.modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: var(--s-space-5);
    overflow-y: auto;
    background: rgba(0, 0, 0, 0.45);
}
/* The shared form modal opened from a drawer (the + that adds a missing material, in the item's Material
   drawer) sits over it: both are 1000, and the drawer is drawn later, so without this it would hide the modal. */
#form-modal .modal-backdrop {
    z-index: 1010;
}
/* The quick add from a dropdown opens over the form modal too. */
#form-quick .modal-backdrop {
    z-index: 1020;
}
/* A confirmation question is asked from anywhere: over the page, a drawer, the form modal or the
   quick add. It must be the topmost thing, or the person cannot see what they are asked to confirm
   (it hid behind the operation drawer until 2026-10-14). */
.confirm-dialog .modal-backdrop {
    z-index: 1030;
}
.modal-panel {
    background: var(--s-color-surface);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-lg);
    box-shadow: var(--s-shadow-md);
    width: 100%;
    max-width: 34rem;
    margin: auto;
}
/* A dialog holding a form rather than a confirmation (2026-09-20). 34rem is
   right for "are you sure" and squeezes a two-column form into one column.
   The viewport cap keeps a long form scrollable inside the panel instead of
   pushing its buttons off the bottom of a laptop screen. */
.modal-panel--wide {
    max-width: 58rem;
}
.modal-panel--wide .modal-body {
    max-height: calc(100vh - 12rem);
    overflow-y: auto;
}
/* A form in the shared modal that asks for room (render_form wide=True,
   such as an import list): the one shared panel widens for it alone. */
.modal-panel:has(.modal-body--wide) {
    max-width: 58rem;
}
.modal-body--wide {
    max-height: calc(100vh - 12rem);
    overflow-y: auto;
}
.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-space-3);
    padding: var(--s-space-4) var(--s-space-5);
    border-bottom: 1px solid var(--s-color-border);
}
.modal-header h2 {
    margin: 0;
    font-size: var(--s-font-size-lg);
}
.modal-header__actions {
    display: flex;
    align-items: center;
    gap: var(--s-space-2);
}
/* The open-in-a-new-tab icon is a link, so it needs the underline off. */
.modal-header__actions a.btn-icon {
    text-decoration: none;
}
.modal-body {
    padding: var(--s-space-4) var(--s-space-5);
}

/* ---- Buttons ---- */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s-space-1);
    padding: 0.4rem 0.85rem;
    border: 1px solid var(--s-color-accent);
    border-radius: var(--s-radius-md);
    background: var(--s-color-accent);
    color: #fff;
    font-family: var(--s-font-family);
    font-size: var(--s-font-size-md);
    font-weight: var(--s-font-weight-medium);
    line-height: var(--s-line-height-control);
    cursor: pointer;
    text-decoration: none;
    transition: all var(--s-transition-fast);
}
.btn:hover {
    background: var(--s-color-accent-hover);
    border-color: var(--s-color-accent-hover);
    color: #fff;
}
.btn.secondary {
    background: var(--s-color-surface);
    color: var(--s-color-accent-text);
    border-color: var(--s-color-border-strong);
}
.btn.secondary:hover {
    background: var(--s-color-surface-hover);
    border-color: var(--s-color-accent);
    color: var(--s-color-accent-text);
}
.btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* ---- Icon buttons: compact, icon-only actions (BOM line grid's remove/add/
   flag-PR controls) — a Material Icons ligature standing in for a .btn.
   secondary, with the action name conveyed via the title attribute (native
   tooltip) instead of visible text. */
.btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    padding: 0;
    border: 1px solid var(--s-color-border-strong);
    border-radius: var(--s-radius-md);
    background: var(--s-color-surface);
    color: var(--s-color-accent-text);
    cursor: pointer;
    transition: all var(--s-transition-fast);
}
.btn-icon:hover {
    background: var(--s-color-surface-hover);
    border-color: var(--s-color-accent);
}
.btn-icon:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}
.btn-icon .material-icons-outlined {
    font-size: 18px;
}
/* A pill, text and an icon button side by side in one table cell sit on one
   centre line (an inline icon button is taller than the text and drifts off
   it otherwise). */
.row-actions {
    display: inline-flex;
    align-items: center;
    gap: var(--s-space-2);
}

/* ---- HTMX loading indicator (hx-indicator) ---- */
/* Hidden by default; htmx toggles .htmx-request on the indicator element
   itself for the duration of the request. Used by anything that can take a
   noticeable amount of time (e.g. an AI call) so a slow response doesn't
   look like a dead button. */
.htmx-indicator {
    display: none;
}
.htmx-request.htmx-indicator,
.htmx-request .htmx-indicator {
    display: inline;
}

/* ---- Detail-page badge row (above the first card) ---- */
.detail-badges {
    display: flex;
    align-items: center;
    gap: var(--s-space-2);
    margin-bottom: var(--s-space-4);
    flex-wrap: wrap;
}

/* ---- Info hint: inline tooltip icon (2026-08-27) ----
   Explanatory/background text belongs in a hover tooltip next to the
   heading, not as a full sentence in the page body — keeps the page itself
   to-the-point. Native `title` attribute (same tooltip mechanism .btn-icon
   already uses above), no JS. Usage:
   <span class="info-hint material-icons-outlined" title="...">info</span> */
/* The little (i) that carries a tooltip. */
.info-hint {
    color: var(--s-color-text-tertiary);
    cursor: help;
    vertical-align: middle;
}
/* Sized here rather than in the rule above, and matching both class names,
   because css/material-icons.css loads after this file and sets every icon
   to 24px/1. At that size the glyph is twice the 12px text of a field label:
   it made the label carrying it taller than its neighbour, and the two
   fields in a .field-pair stopped lining up. 1rem is the readable size for a
   hint, and `line-height: 0` takes it out of the height calculation
   altogether, so it still paints in full but can never push anything around,
   in a label or in a heading. */
.info-hint.material-icons-outlined,
.info-hint.material-icons {
    font-size: 1rem;
    line-height: 0;
}
.info-hint:hover,
.info-hint:focus-visible {
    color: var(--s-color-text-secondary);
}

/* ---- Permission hint (2026-10-19) ----
   The small icon beside something that is shown reduced because the person lacks a permission
   (templates/common/components/permission_hint.html). It opens a bubble on tap or click, so it
   works on a phone. The hit area grows on touch screens without moving anything around it.
   Sized like .info-hint: both icon classes, and `line-height: 0`, so it never changes a line's height. */
.permission-hint {
    display: inline-flex;
    vertical-align: middle;
    margin-left: 0.25rem;
}
.permission-hint__button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    border: 0;
    border-radius: var(--s-radius-sm);
    background: none;
    color: var(--s-color-info);
    cursor: pointer;
}
.permission-hint__button:hover {
    background: var(--s-color-surface-hover);
}
.permission-hint__button:focus-visible {
    outline: 2px solid var(--s-color-info);
    outline-offset: 1px;
}
.permission-hint__icon.material-icons-outlined {
    font-size: 1.15rem;
    line-height: 0;
}
@media (pointer: coarse) {
    .permission-hint__button {
        width: 2.75rem;
        height: 2.75rem;
        margin: -0.5rem;
    }
}
.permission-hint__bubble {
    max-width: min(24rem, calc(100vw - 16px));
}
.permission-hint__line {
    margin: 0;
}
.permission-hint__line + .permission-hint__line,
.permission-hint__actions {
    margin-top: var(--s-space-2);
}
.permission-hint__note {
    font-size: var(--s-font-size-sm);
    color: var(--s-color-text-secondary);
}

/* ---- Pills / status badges ---- */
.pill {
    display: inline-block;
    padding: 0.1rem 0.6rem;
    border-radius: 999px;
    font-size: var(--s-font-size-sm);
    font-weight: var(--s-font-weight-medium);
    background: var(--s-color-bg-alt);
    color: var(--s-color-text-secondary);
    border: 1px solid transparent;
}
.pill.ok {
    background: var(--s-color-success-bg);
    color: var(--s-color-success-text);
}
.pill.warn {
    background: var(--s-color-warning-bg);
    color: var(--s-color-warning-text);
}
/* A link inside a warning (the books-not-open notice) keeps the warning's
   colour, underlined, so it reads on the tinted background. */
.pill.warn a {
    color: inherit;
    text-decoration: underline;
}
.pill.danger {
    background: var(--s-color-danger-bg);
    color: var(--s-color-danger-text);
}
.pill.info {
    background: var(--s-color-info-bg);
    color: var(--s-color-info-text);
}
a.pill:hover {
    border-color: var(--s-color-border-strong);
    color: var(--s-color-text);
}
a.pill.ok:hover {
    color: var(--s-color-success-text);
}
/* A pill that opens something, such as the Stock status cell on the item
   list. Same hover as a linked pill; keeps its own colour when hovered. */
button.pill {
    font-family: inherit;
    line-height: inherit;
    cursor: pointer;
}
button.pill:hover {
    border-color: var(--s-color-border-strong);
}

/* ---- Tables: normal scale (default) ---- */
table {
    width: 100%;
    border-collapse: collapse;
    background: transparent;
}
th, td {
    text-align: left;
    padding: var(--s-space-2) var(--s-space-3);
    border-bottom: 1px solid var(--s-color-border);
    font-size: var(--s-font-size-md);
}
th {
    color: var(--s-color-text-secondary);
    font-weight: var(--s-font-weight-semibold);
    font-size: var(--s-font-size-sm);
    text-transform: uppercase;
    letter-spacing: 0.03em;
    white-space: nowrap;
}
tbody tr:hover {
    background-color: var(--s-color-surface-hover);
}
tbody tr:last-child td {
    border-bottom: none;
}

/* ---- Tables: dense data view (.data-table) ----
   For genuinely data-heavy screens: item list, stock bins, enquiry list,
   customer list, BOM lines. Small text, tight rows, tabular numerals —
   AlignBooks/Tally information density without the cluttered chrome. */
.data-table {
    font-variant-numeric: tabular-nums;
}
.data-table th {
    font-size: var(--s-font-size-xs);
    padding: var(--s-space-1) var(--s-space-2);
    letter-spacing: 0.04em;
}
.data-table th.num {
    text-align: right;
}
.data-table td {
    font-size: var(--s-font-size-sm);
    padding: 0.3rem var(--s-space-2);
    line-height: 1.45;
}
.data-table .pill {
    font-size: var(--s-font-size-xs);
    padding: 0.05rem 0.5rem;
}
.data-table .btn {
    font-size: var(--s-font-size-xs);
    padding: 0.1rem 0.4rem;
}
.data-table .btn-icon {
    width: 1.6rem;
    height: 1.6rem;
}
.data-table .btn-icon .material-icons-outlined {
    font-size: 16px;
}

/* Wrap wide tables so they scroll inside the card, not the page */
.table-scroll {
    overflow-x: auto;
}

/* ---- Material Issue row (the multi-item/multi-user issue grid, 2026-09-23)
   ----
   One row is one item+user+project combination. A literal table row clipped
   the Item and Issued-to boxes at normal widths, so each row is instead
   three stacked strips — who it's for, the data, and notes that only show
   when relevant — inside its own bordered block. Alternating background
   tells one row's three strips apart from the next row's, since a plain
   border got lost between this many wrapped inputs. */
.issue-row {
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md);
    padding: var(--s-space-3);
    margin-bottom: var(--s-space-3);
}
.issue-row:nth-of-type(odd) {
    background-color: var(--s-color-surface);
}
.issue-row:nth-of-type(even) {
    background-color: var(--s-color-bg-alt);
}
.issue-row__top {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-4);
}
.issue-row__remove {
    margin-left: auto;
}
.issue-row__fields {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--s-space-3);
    margin: var(--s-space-2) 0;
}
.issue-row__fields > div {
    flex: 1 1 12rem;
    min-width: 12rem;
}
/* Quantity never needs a 12rem box to hold a handful of digits — left at
   the general rule it stretched to match Item/Issued to and read as an
   oversized, empty-looking field (caught 2026-09-23). */
.issue-row__fields > .issue-row__qty {
    flex: 0 1 6rem;
    min-width: 5rem;
}
.issue-row__fields input[type="text"],
.issue-row__fields input[type="number"],
.issue-row__fields select {
    width: 100%;
}
/* The quantity box's unit of measure (Item.unit, read off the same catalog
   the item picker already loaded — no second field, no second query),
   beside the number rather than guessed at. */
.qty-with-unit {
    display: flex;
    align-items: center;
    gap: var(--s-space-1);
}
.issue-row__fields .qty-with-unit input[type="number"],
.issue-row__fields .qty-with-unit input[type="text"] {
    width: auto;
    min-width: 0;
    flex: 1 1 auto;
}
/* The buttons that end a card (Add line, Save changes), at its right. */
.issue-row__actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--s-space-2);
    margin-top: var(--s-space-3);
}
.issue-row__notes {
    padding-top: var(--s-space-2);
    border-top: 1px dashed var(--s-color-border);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-3);
}
/* ---- Purchase order line cards (2026-09-25) ----
   The same striped card as the issue rows, for a PO line. The top strip is
   goods/service beside the job, the job taking the rest of the width. The
   figures sit on a ten-column grid so the widths are shares of the card, not
   of whatever fits: item 6, quantity 1, unit 1, price 2 on the first line;
   discount 2, tax code 4, tax rate 1, the keep-this-rate tick 3 on the second. */
.po-line__top {
    flex-wrap: nowrap;
    align-items: flex-end;
}
.po-line__type {
    display: flex;
    gap: var(--s-space-4);
    padding-bottom: 0.45rem;
    white-space: nowrap;
}
/* The stacked-form label margin would put a gap above the first thing in the
   card; the card's own padding is the only space around it. */
.po-line__type label.checkline,
.po-line__job label {
    margin-top: 0;
}
.po-line__job {
    flex: 1 1 auto;
    min-width: 0;
}
.issue-row__fields.po-line__fields {
    display: grid;
    grid-template-columns: repeat(10, minmax(0, 1fr));
    align-items: start;
}
.po-line__fields > div {
    min-width: 0;
}
.po-col-1 { grid-column: span 1; }
.po-col-2 { grid-column: span 2; }
.po-col-3 { grid-column: span 3; }
.po-col-4 { grid-column: span 4; }
.po-col-6 { grid-column: span 6; }
.po-line__keeprate {
    align-self: end;
}
.po-line__amount {
    margin-left: auto;
    align-self: flex-end;
    text-align: right;
    white-space: nowrap;
}
.po-line__amount label {
    margin-top: 0;
}
@media (max-width: 40rem) {
    .po-line__top {
        flex-wrap: wrap;
    }
    .issue-row__fields.po-line__fields {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    .po-line__fields > div {
        grid-column: span 1;
    }
    .po-line__fields > .po-col-6,
    .po-line__fields > .po-col-4,
    .po-line__fields > .po-col-3 {
        grid-column: 1 / -1;
    }
}
.issue-row__results {
    position: absolute;
    z-index: 5;
    left: 0;
    right: 0;
    margin: 0;
    padding: 0;
    list-style: none;
    max-height: 14rem;
    overflow-y: auto;
    background-color: var(--s-color-surface);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md);
}
.issue-row__results li {
    padding: var(--s-space-2) var(--s-space-3);
    cursor: pointer;
}
.issue-row__results li:hover {
    background-color: var(--s-color-bg-alt);
}
/* The order total stays in view while lines are added above it. */
.po-summary {
    position: sticky;
    bottom: 0;
    z-index: 4;
    padding: var(--s-space-3);
    background-color: var(--s-color-surface);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md);
}
/* Subtotal, tax and total one under another at the right, the figures
   lined up in a column of their own. */
.po-summary__figures {
    display: grid;
    grid-template-columns: auto auto;
    justify-content: end;
    column-gap: var(--s-space-5);
    row-gap: var(--s-space-1);
}
.po-summary__figures > strong {
    text-align: right;
}
/* A summary that sits under a table: a gap above it, and a shadow so it
   reads as floating over the rows when it sticks. */
.po-summary--below-table {
    margin-top: var(--s-space-4);
    box-shadow: var(--s-shadow-sm);
}
/* ---- Quick Issue dialog header fields (2026-09-23) ----
   Plain stacked <div>s with no spacing of their own read as one cramped
   block, most noticeably right under "Take everything from". */
.quick-issue-field {
    margin-bottom: var(--s-space-3);
}

/* ---- Forms ---- */
/* `.line-cards` is a set of line cards that sits outside a form, such as the
   quotation's lines, which save one card at a time. Its fields take the same
   look as a stacked form's, so the same card reads the same on both pages. */
form.stacked label,
.line-cards label {
    display: block;
    margin-top: var(--s-space-3);
    margin-bottom: var(--s-space-1);
    font-size: var(--s-font-size-sm);
    font-weight: var(--s-font-weight-medium);
    color: var(--s-color-text-secondary);
}
form.stacked input,
form.stacked select,
form.stacked textarea,
.line-cards input,
.line-cards select,
select.form-select {
    width: 100%;
    padding: 0.45rem 0.65rem;
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md);
    font: inherit;
    font-size: var(--s-font-size-md);
    background-color: var(--s-color-input-bg);
    color: var(--s-color-input-text);
    transition: border-color var(--s-transition-fast),
                box-shadow var(--s-transition-fast);
}
form.stacked input::placeholder,
form.stacked textarea::placeholder,
.line-cards input::placeholder {
    color: var(--s-color-input-placeholder);
}
form.stacked input:focus,
form.stacked select:focus,
form.stacked textarea:focus,
.line-cards input:focus,
.line-cards select:focus,
select.form-select:focus {
    outline: none;
    border-color: var(--s-color-accent);
    box-shadow: 0 0 0 3px var(--s-color-accent-light);
}
/* Read-only forms (e.g. Organization Details for an admin who can view but
   not edit — OrganizationForm(editable=False), 2026-08-27): themed instead
   of left to the browser's own disabled-input look, which is inconsistent
   across browsers/dark mode and easy to mistake for "broken" rather than
   "not yours to change". */
form.stacked input:disabled,
form.stacked select:disabled,
form.stacked textarea:disabled,
.line-cards input:disabled,
.line-cards select:disabled,
select.form-select:disabled {
    background-color: var(--s-color-surface);
    color: var(--s-color-text-secondary);
    cursor: not-allowed;
}
/* Locked textarea (the direct-PO page's delivery address): your address,
   shown but not editable until its toggle is ticked. The same muted
   treatment as disabled above, so locked reads settled rather than broken,
   except the cursor stays default, since the text can still be selected and
   copied. Toggled via the readonly attribute, never the disabled one, so
   the shown address still submits with the order. */
form.stacked textarea[readonly] {
    background-color: var(--s-color-surface);
    color: var(--s-color-text-secondary);
}
/* Inline toggle between a field's label and the field itself (the same
   page's "Different delivery address?"): a control, not a second field
   label, so it drops the label's semibold weight and sits close to the
   field it governs. */
form.stacked label.checkline {
    display: flex;
    align-items: center;
    gap: var(--s-space-1);
    margin-top: var(--s-space-1);
    margin-bottom: var(--s-space-1);
    font-weight: var(--s-font-weight-normal);
    cursor: pointer;
}
/* Radios/checkboxes are inline controls, not full-width fields — without
   this the generic `form.stacked input` width:100% stretches the control's
   box and shoves its label text onto the next line. */
form.stacked input[type="radio"],
form.stacked input[type="checkbox"] {
    width: auto;
    margin: 0 var(--s-space-1) 0 0;
    padding: 0;
    accent-color: var(--s-color-accent);
    vertical-align: middle;
}
/* Spreadsheet row inputs (the PR create page's inline entry row) keep
   their borderless-until-interaction grid look even when their form gains
   the .stacked class — form.stacked input would otherwise force a solid
   field box on them. Reproduces .bom-grid-input's states at higher
   specificity so hover/focus behave exactly the same. */
form.stacked input.bom-grid-input {
    padding: 0.35rem 0.5rem;
    border: 1px solid transparent;
    background: transparent;
    font-size: var(--s-font-size-sm);
}
form.stacked input.bom-grid-input:hover {
    border-color: var(--s-color-border);
    background: var(--s-color-surface);
}
form.stacked input.bom-grid-input:focus {
    outline: none;
    border-color: var(--s-color-accent);
    background: var(--s-color-surface);
    box-shadow: 0 0 0 3px var(--s-color-accent-light);
}

/* Two fields that belong together on one line inside a .stacked form — the
   amount and the currency it is in, the date and how the money moved. They
   drop to one under each other when the column is too narrow to read them
   side by side, so nothing is ever squeezed. */
.field-pair {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: var(--s-space-4);
}

/* ---- Callout: a speech bubble about one field (2026-10-12) ----
   Says what is wrong with a single box, with its tail pointing at the box, and
   never covers it. static/js/common/callout.js places it (fixed to the window,
   below the box or above it) and sets the side and where the tail goes.
   Hidden until it has been placed, so it never flashes in the wrong spot.
   Clicks pass through the bubble to whatever is under it, except on a link or
   button in it. The tint is laid over the plain surface colour because the
   dark theme's tint is see-through, and a bubble over other text must not be. */
.callout {
    --callout-tint: var(--s-color-warning-bg);
    --callout-edge: var(--s-color-warning);
    --callout-text: var(--s-color-warning-text);
    position: fixed;
    z-index: var(--s-z-dropdown);
    visibility: hidden;
    box-sizing: border-box;
    width: max-content;
    max-width: min(22rem, calc(100vw - 16px));
    padding: 0.45rem 0.7rem;
    border: 1px solid var(--callout-edge);
    border-radius: var(--s-radius-md);
    background-color: var(--s-color-surface);
    background-image: linear-gradient(var(--callout-tint), var(--callout-tint));
    box-shadow: var(--s-shadow-lg);
    color: var(--callout-text);
    font-size: var(--s-font-size-sm);
    font-weight: var(--s-font-weight-medium);
    line-height: 1.4;
    text-align: left;
    pointer-events: none;
}
.callout a,
.callout button,
.callout label,
.callout input {
    pointer-events: auto;
}
/* A bubble that can be closed has its button in the top right corner. */
.callout--closable {
    padding-right: 1.9rem;
}
.callout__close {
    position: absolute;
    top: 3px;
    right: 3px;
    width: 20px;
    height: 20px;
    padding: 0;
    border: 0;
    border-radius: var(--s-radius-sm);
    background: none;
    color: inherit;
    line-height: 0;
    cursor: pointer;
}
.callout__close:hover,
.callout__close:focus-visible {
    background: rgba(0, 0, 0, 0.08);
}
.callout .callout__close .material-icons-outlined {
    font-size: 16px;
    line-height: 0;
}
/* A tick box with its words inside a bubble reads as part of it. */
.callout label {
    margin: 0;
    color: inherit;
    font-weight: inherit;
}
.callout input[type="checkbox"] {
    flex: none;
}
.callout a {
    color: inherit;
    text-decoration: underline;
}
.callout--danger {
    --callout-tint: var(--s-color-danger-bg);
    --callout-edge: var(--s-color-danger);
    --callout-text: var(--s-color-danger-text);
}
.callout--info {
    --callout-tint: var(--s-color-info-bg);
    --callout-edge: var(--s-color-info);
    --callout-text: var(--s-color-info-text);
}
/* The tail: a turned square whose two outer sides are the visible point. */
.callout::before {
    content: "";
    position: absolute;
    left: var(--callout-tail-x, 1rem);
    width: 10px;
    height: 10px;
    border: 1px solid var(--callout-edge);
    background: inherit;
    transform: translateX(-50%) rotate(45deg);
}
.callout[data-side="below"]::before {
    top: -6px;
    border-right-color: transparent;
    border-bottom-color: transparent;
}
.callout[data-side="above"]::before {
    bottom: -6px;
    border-left-color: transparent;
    border-top-color: transparent;
}

/* A tick box that sits beside a field in a .field-pair (a bank account's "Main
   account" next to its currency). Its cell copies the shape of the field's:
   an empty label line above, then a box as tall as the field's own. The tick
   box then lines up with the control next to it, and keeps lining up when an
   error message makes the neighbour taller, which a fixed top margin did not. */
form.stacked .field-pair__check {
    display: flex;
    align-items: center;
    box-sizing: border-box;
    padding: 0.45rem 0;
    border: 1px solid transparent;
    font-size: var(--s-font-size-md);
}
form.stacked .field-pair__check label.checkline {
    margin: 0;
}

/* An item, its quantity with the unit beside it, and a read-only fact about
   the item (2026-09-25). The item gets the room; the two short boxes share the
   rest. One column when the page is too narrow to read three side by side. */
.field-pair--item {
    grid-template-columns: minmax(14rem, 2fr) minmax(9rem, 1fr) minmax(9rem, 1fr);
}
@media (max-width: 40rem) {
    .field-pair--item {
        grid-template-columns: 1fr;
    }
}
.field-with-unit {
    display: flex;
    align-items: center;
    gap: var(--s-space-2);
}
.field-with-unit input {
    flex: 1 1 auto;
    min-width: 0;
}
.unit-label {
    min-width: 2.5rem;
    color: var(--s-color-text-secondary);
}
form.stacked input[readonly] {
    background-color: var(--s-color-bg-alt);
}

/* ---- Form field (common/components/form_field.html) ----
   One label and its box. Nothing here by default: inside a plain .stacked
   form it lays out exactly as the bare label and input always did. The
   classes exist for the layouts below to arrange. */

/* ---- Form grid (the item form, 2026-09-16) ----
   A long form's fields in two columns, so a record fits on a screen rather
   than in a scroll. One column when the page is too narrow to read two side
   by side. A field marked wide spans the whole row. */
.form-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-space-2) var(--s-space-6);
}
.form-grid .form-field--wide {
    grid-column: 1 / -1;
}
@media (max-width: 760px) {
    .form-grid {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* Rows: the label sits left of its box instead of above it, so a field
   costs one line of height rather than two. An error goes under the box, in
   the box's column, so it reads as belonging to that box. */
.form-grid--rows .form-field {
    display: grid;
    grid-template-columns: 9.5rem minmax(0, 1fr);
    align-items: center;
    column-gap: var(--s-space-3);
}
form.stacked .form-grid--rows .form-field > label {
    margin: 0;
}
.form-grid--rows .form-field > :not(label) {
    grid-column: 2;
}
/* A tall box (the description) keeps its label level with its first line,
   not floating halfway down it. */
.form-grid--rows .form-field--wide {
    align-items: start;
}
.form-grid--rows .form-field--wide > label {
    padding-top: 0.5rem;
}
/* A field with no label (the item form's Description accordion, alone under
   a heading that already names it) drops the reserved 9.5rem label column
   entirely, rather than leaving it as blank space beside the box. */
.form-grid--rows .form-field--no-label {
    grid-template-columns: minmax(0, 1fr);
}
.form-grid--rows .form-field--no-label > :not(label) {
    grid-column: 1;
}
/* At phone width a 9.5rem label column leaves no box to type in. */
@media (max-width: 480px) {
    .form-grid--rows .form-field {
        grid-template-columns: minmax(0, 1fr);
    }
    .form-grid--rows .form-field > :not(label) {
        grid-column: 1;
    }
}

/* ---- Item name matches (2026-09-24 follow-up to D-44) ----
   The "already in your items" dropdown under the New item dialog's Name
   field. Reuses common.widgets.SearchableSelect's own ss-list/ss-option/
   ss-note classes (static/css/searchable_select.css) so it looks like the
   category picker's own dropdown. These two rules only right the ss-note
   title row, which that widget writes for a trailing note with its border
   on top, and lay each row's code/name left of its unit and proposed pill
   — the one thing a plain choice list never needed. */
.item-name-matches {
    position: relative;
}
.ss-list--item-matches .ss-note:first-child {
    border-top: 0;
    margin-top: 0;
}
.ss-list--item-matches .ss-option {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-space-2);
}

/* One field per row, the label above its box: for a form in a drawer (the item's Material drawer), where
   the item form's two columns with labels beside the boxes leave each box too narrow to use. */
.form-column {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-space-4);
}
/* What SETRA makes of the raw-material boxes (the name, the weight, what is missing) is read, not
   typed: its lines wrap instead of running off the edge, as a pill otherwise never does. */
.item-form-preview {
    display: grid;
    gap: var(--s-space-2);
}
.item-form-preview p {
    margin: 0;
}
.item-form-preview:not(:empty) {
    margin: var(--s-space-3) 0 var(--s-space-4);
}
/* A callout in it is fixed to the window, so it takes no room of its own: keep one bubble's height free, or
   it sits over the buttons under the boxes. */
.item-form-preview:has(.callout) {
    min-height: 5.5rem;
}
.item-form-preview .pill {
    white-space: normal;
}

/* A box with a small button to its right, such as the + that adds a
   category without leaving the item form. The box takes the width; the
   button keeps its own and matches the box's height, so the row stays one
   line tall. */
.field-with-action {
    display: flex;
    align-items: stretch;
    gap: var(--s-space-2);
}
.field-with-action > :first-child {
    flex: 1 1 auto;
    min-width: 0;
}
.field-with-action > .btn-icon {
    flex: 0 0 auto;
    height: auto;
    min-height: 2rem;
}
/* The + a unit dropdown gets (static/js/common/unit_quick_add.js) hides
   with its dropdown when the dropdown is hidden (x-show). */
.field-with-action > select[style*="display: none"] + .unit-quick-add {
    display: none;
}

/* ---- Form sections ----
   A titled group of fields. A plain <section> cannot be folded; a <details>
   one can, with its chevron turning as it opens. The form decides on the
   server which groups may fold and whether one starts open. */
.form-section + .form-section {
    margin-top: var(--s-space-4);
    padding-top: var(--s-space-4);
    border-top: 1px solid var(--s-color-border);
}
.form-section > h2,
.form-section > summary > h2 {
    margin: 0 0 var(--s-space-3);
    font-size: var(--s-font-size-md);
}
details.form-section > summary {
    cursor: pointer;
    list-style: none;
}
details.form-section > summary::-webkit-details-marker {
    display: none;
}
details.form-section > summary > h2 {
    display: inline-flex;
    align-items: center;
    gap: var(--s-space-2);
}
details.form-section:not([open]) > summary > h2 {
    margin-bottom: 0;
}
details.form-section > summary > h2::before {
    content: "";
    width: 0.4rem;
    height: 0.4rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(-45deg);
    transition: transform var(--s-transition-fast);
}
details.form-section[open] > summary > h2::before {
    transform: rotate(45deg);
}

/* ---- Item page: Summary beside the photo (2026-09-16) ----
   The photo must never be taller than the Summary card, and must keep its
   own shape. So the photo column takes no part in deciding the row's
   height: `container-type: size` makes the cell ignore its contents when
   sized, the row grows to fit the Summary alone, and the cell is stretched
   to match. The frame inside is then a square as large as the cell allows,
   measured with container units, and the picture is fitted into it whole
   rather than cropped.

   The cards lose their own bottom margin inside the grid, or the stretched
   cell would be taller than the card by that margin. */
.summary-photo-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) clamp(9rem, 28%, 18rem);
    gap: var(--s-space-4);
    margin-bottom: var(--s-space-4);
}
.summary-photo-grid > .card {
    margin-bottom: 0;
    min-width: 0;
}
.item-photo-cell {
    container-type: size;
    display: flex;
    justify-content: flex-end;
    align-items: flex-start;
}
.photo-placeholder {
    position: relative;
    width: min(100cqw, 100cqh);
    height: min(100cqw, 100cqh);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-lg);
    background-color: var(--s-color-bg-alt);
}
/* One column on a narrow screen. The cell then has no row height to borrow,
   so the frame sets its own: full width up to a limit, square. */
@media (max-width: 760px) {
    .summary-photo-grid {
        grid-template-columns: minmax(0, 1fr);
    }
    .item-photo-cell {
        container-type: normal;
        justify-content: flex-start;
    }
    .photo-placeholder {
        width: 100%;
        max-width: 20rem;
        height: auto;
        aspect-ratio: 1 / 1;
    }
}
.photo-placeholder img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
}
.photo-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-space-1);
    color: var(--s-color-text-tertiary);
    font-size: var(--s-font-size-sm);
}
.photo-empty .photo-empty-icon.material-icons-outlined {
    font-size: 40px;
}
.photo-edit-form {
    position: absolute;
    inset: 0;
    margin: 0;
}
/* Same scrim as .modal-backdrop, so the icon reads over any photo. With no
   photo there is nothing to cover, so the whole frame is simply clickable
   and the "Add photo" words already show. */
.photo-edit-overlay {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 0;
    background: rgba(0, 0, 0, 0.45);
    opacity: 0;
    cursor: pointer;
    transition: opacity var(--s-transition-fast);
}
.photo-placeholder.is-empty .photo-edit-overlay {
    background: transparent;
    opacity: 1;
}
.photo-placeholder.is-empty .photo-edit-overlay .material-icons-outlined {
    display: none;
}
.photo-placeholder.is-empty:hover {
    border-color: var(--s-color-border-strong);
}
.photo-placeholder:not(.is-empty):hover .photo-edit-overlay,
.photo-placeholder:not(.is-empty):focus-within .photo-edit-overlay {
    opacity: 1;
}
.photo-edit-overlay .material-icons-outlined {
    font-size: 28px;
    color: #fff;
}
.photo-edit-overlay input[type="file"] {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
}
/* Above the edit overlay so a click in the corner reaches this button
   rather than the label spanning the frame beneath it. */
.photo-remove {
    position: absolute;
    top: 0.4rem;
    right: 0.4rem;
    z-index: 2;
    opacity: 0;
    transition: opacity var(--s-transition-fast);
}
.photo-placeholder:hover .photo-remove,
.photo-placeholder:focus-within .photo-remove {
    opacity: 1;
}
.photo-remove .btn-icon {
    background: var(--s-color-surface);
}
.photo-error {
    position: absolute;
    left: 0.4rem;
    right: 0.4rem;
    bottom: 0.4rem;
    z-index: 2;
    margin: 0;
    white-space: normal;
}

/* ---- Detail page sections ----
   Page actions in one row above the Summary. A card that folds, with the
   same turning chevron as a folding form section, and a quiet count beside
   its title so a closed section still says whether anything is inside. */
.detail-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-2);
    margin: 0 0 var(--s-space-4);
}

/* ---- Overflow menu ----
   The actions that must not be one stray click away. Archiving an item sat
   beside Edit until 2026-09-20 and was being hit by accident, so it moved
   behind the ellipsis at the top right of the page. The sidebar's own
   .popup-menu is a flyout anchored to a nav item and cannot serve here.
   Alpine idiom matches the multiselect in list_filter_bar.html: `open` in
   x-data, closed by a click outside. */
.overflow-menu {
    position: relative;
    display: inline-block;
}
.overflow-menu-panel {
    position: absolute;
    right: 0;
    top: calc(100% + var(--s-space-1));
    background-color: var(--s-color-surface);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-lg);
    box-shadow: var(--s-shadow-xl);
    padding: var(--s-space-2);
    min-width: 12rem;
    z-index: var(--s-z-popover);
}
/* The menu holds forms (archive and restore both POST), so the row has to
   style a button as readily as a link, and the form around it must not
   introduce a line of its own. */
.overflow-menu-panel form {
    display: block;
    margin: 0;
}
/* A line under the menu's rows, such as who can give the module the rows need. */
.overflow-menu-panel .menu-note {
    padding: var(--s-space-2) var(--s-space-3) var(--s-space-1);
    color: var(--s-color-text-secondary);
    font-size: var(--s-font-size-sm);
}
.overflow-menu-panel .menu-item[disabled] {
    color: var(--s-color-text-tertiary);
    cursor: not-allowed;
}
.overflow-menu-panel .menu-item {
    display: flex;
    align-items: center;
    gap: var(--s-space-3);
    width: 100%;
    padding: var(--s-space-2) var(--s-space-3);
    border: none;
    border-radius: var(--s-radius-md);
    background: none;
    color: var(--s-color-text);
    font-size: var(--s-font-size-base);
    text-align: left;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
}
.overflow-menu-panel .menu-item:hover {
    background-color: var(--s-color-accent-light);
    color: var(--s-color-accent-text);
}
/* material-icons.css loads last and sets every icon to 24px; a menu row is
   not a heading, so the icon is sized down here and its line box zeroed so
   the row keeps one height (CLAUDE.md §4, icons). */
.overflow-menu-panel .menu-item .material-icons-outlined {
    font-size: 18px;
    line-height: 0;
    flex-shrink: 0;
    color: var(--s-color-text-secondary);
}
.overflow-menu-panel .menu-item:hover .material-icons-outlined {
    color: var(--s-color-accent-text);
}
/* A row that cannot be used now: greyed out, with the reason on the span
   around it (a disabled button shows no tooltip of its own, see
   common/components/remove_button.html). */
.overflow-menu-panel .menu-item-blocked {
    display: block;
    cursor: not-allowed;
}
.overflow-menu-panel .menu-item[disabled] {
    opacity: 0.5;
}
/* A figure that shows more when the pointer rests on it (a tooltip with a
   list in it, 2026-10-14): the dotted underline says there is more, and it
   takes the keyboard focus so the list is not for the mouse alone. */
.hover-peek {
    text-decoration: underline dotted;
    text-underline-offset: 0.2em;
    cursor: help;
}
.supplier-peek .tooltip-inner {
    max-width: 22rem;
    text-align: left;
}
.supplier-peek__row + .supplier-peek__row {
    margin-top: var(--s-space-2);
}
details.detail-section > summary {
    cursor: pointer;
    list-style: none;
}
details.detail-section > summary::-webkit-details-marker {
    display: none;
}
details.detail-section > summary > h2 {
    display: inline-flex;
    align-items: center;
    gap: var(--s-space-2);
}
details.detail-section:not([open]) > summary > h2 {
    margin-bottom: 0;
}
details.detail-section > summary > h2::before {
    content: "";
    width: 0.4rem;
    height: 0.4rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(-45deg);
    transition: transform var(--s-transition-fast);
}
details.detail-section[open] > summary > h2::before {
    transform: rotate(45deg);
}
.detail-section-count {
    font-size: var(--s-font-size-sm);
    font-weight: var(--s-font-weight-normal);
}
/* A closed section's heading carries a small icon and a count in a pill, so it
   still says what is inside it (order page Attachments and Lines due earlier,
   2026-10-06). The pill is quiet when it counts nothing and picks up the
   accent when there is something to open. `.detail-section--rich` gives the
   summary row the same hover as the other toggles, so the whole row reads as
   one control. */
.detail-section-icon.material-icons-outlined {
    font-size: 1.2rem;
    line-height: 0;
    vertical-align: middle;
    color: var(--s-color-text-secondary);
}
.detail-section-count.detail-section-badge {
    display: inline-block;
    min-width: 1.5rem;
    padding: 0 var(--s-space-2);
    border-radius: 999px;
    text-align: center;
    font-weight: var(--s-font-weight-medium);
    background: var(--s-color-accent-light);
    color: var(--s-color-accent-text);
}
.detail-section-count.detail-section-badge--empty {
    background: var(--s-color-bg-alt);
    color: var(--s-color-text-tertiary);
}
/* A date changed where it is shown (order page, Lines due earlier): the picker,
   the reason when a date is being moved, and Save, on one line. The reason
   takes the room that is left. */
.inline-date-edit {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-2);
}
.inline-date-edit input[type="date"] {
    width: auto;
    flex: 0 0 auto;
}
.inline-date-edit input[type="text"] {
    flex: 1 1 12rem;
    min-width: 10rem;
    width: auto;
}
details.detail-section--rich > summary {
    border-radius: var(--s-radius-lg);
}
details.detail-section--rich > summary:hover {
    background-color: var(--s-color-surface-hover);
}
/* "It came back as something else" (2026-09-24, inventory/_what_came_back_
   fields.html): closed by default so What came back is not mistaken for a
   field to fill in every time. A plain .detail-section already gives the
   bordered box and the chevron; this only adds a hover state, so the whole
   row reads as a clickable control rather than a line of text, and fixes
   the swap icon's line height (an icon in a heading, CLAUDE.md §5). */
.what-came-back-toggle > summary {
    border-radius: var(--s-radius-lg);
}
.what-came-back-toggle > summary:hover {
    background-color: var(--s-color-surface-hover);
}
.what-came-back-icon.material-icons-outlined {
    font-size: 1.1rem;
    line-height: 0;
    vertical-align: middle;
    color: var(--s-color-text-secondary);
}
/* A small inline disclosure for an optional field inside a line card (PO and
   quotation lines, 2026-09-27): the same bordered-row-with-chevron affordance
   as .detail-section, scaled to one line with no heading, so a rare field
   (a note, an item under a service line) never reads as one to fill in every
   time (CLAUDE.md, "User productivity is a requirement, not polish"). */
details.line-field-toggle {
    display: inline-block;
}
details.line-field-toggle > summary {
    cursor: pointer;
    list-style: none;
    display: inline-flex;
    align-items: center;
    gap: var(--s-space-1);
    padding: var(--s-space-1) var(--s-space-2);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md);
    font-size: var(--s-font-size-sm);
    color: var(--s-color-text-secondary);
}
details.line-field-toggle > summary:hover {
    background-color: var(--s-color-surface-hover);
}
details.line-field-toggle > summary::-webkit-details-marker {
    display: none;
}
details.line-field-toggle > summary::before {
    content: "";
    width: 0.35rem;
    height: 0.35rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(-45deg);
    transition: transform var(--s-transition-fast);
}
details.line-field-toggle[open] > summary::before {
    transform: rotate(45deg);
}
details.line-field-toggle > summary .material-icons-outlined {
    font-size: 1rem;
    line-height: 0;
}
details.line-field-toggle .line-field-body {
    margin-top: var(--s-space-2);
}
/* A fold that holds whole fields rather than one small box: the whole section
   has the border, with the clickable row as its heading, and it takes the
   width of its card. */
details.line-field-toggle--wide {
    display: block;
    margin-top: var(--s-space-3);
    margin-bottom: var(--s-space-3);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md);
}
details.line-field-toggle--wide > summary {
    display: flex;
    border: 0;
    border-radius: var(--s-radius-md);
    padding: var(--s-space-2) var(--s-space-3);
    font-size: var(--s-font-size-md);
    color: var(--s-color-text);
}
details.line-field-toggle--wide[open] > summary {
    border-bottom: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md) var(--s-radius-md) 0 0;
}
details.line-field-toggle--wide > .line-field-body {
    margin-top: 0;
    padding: 0 var(--s-space-3) var(--s-space-3);
}

/* Fields side by side on one line, with the button that saves them at the
   end, wrapping onto further lines on a narrow screen (2026-10-02). */
.field-row {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0 var(--s-space-3);
}
.field-row__field {
    flex: 1 1 12rem;
    min-width: 0;
}
.field-row__field--wide {
    flex-grow: 2;
}
.field-row__action {
    flex: none;
}
/* As tall as the fields beside it, so the row lines up. */
.field-row__action .btn-icon {
    box-sizing: border-box;
    width: calc(var(--s-font-size-md) * 1.2 + 0.9rem + 2px);
    height: calc(var(--s-font-size-md) * 1.2 + 0.9rem + 2px);
}

/* A long note cut to one line in a table cell; its title shows it all. */
.cell-clip {
    display: block;
    max-width: 20rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* A flyout's button stays lit while its flyout is open. */
.line-scope .btn-icon.is-on {
    color: var(--s-color-accent);
    border-color: var(--s-color-accent);
}

/* A card title with something on its right, such as a status pill. */
.card-title-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-space-3);
}
.card-title-row > .pill,
.card-title-row > .muted {
    font-size: var(--s-font-size-sm);
    font-weight: var(--s-font-weight-normal);
}
.card-subheading {
    margin: var(--s-space-4) 0 var(--s-space-2);
    font-size: var(--s-font-size-sm);
    color: var(--s-color-text-secondary);
}
/* Tiles on a detail page, where they sit among other cards rather than
   being the whole page. */
.summary-tiles--compact .summary-tile-value {
    font-size: var(--s-font-size-xl);
}

/* ---- Form actions ----
   A form's buttons in one row. The sticky variant pins the row to the
   bottom of the window while its form is taller than the window, so Save is
   in reach from any field. It stops at the bottom of its own card, so it
   never floats over whatever follows the form. */
.form-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-2);
    margin-top: var(--s-space-5);
}
.form-actions__legend {
    margin-left: auto;
    font-size: var(--s-font-size-sm);
}
.form-actions--sticky {
    position: sticky;
    bottom: 0;
    z-index: 5;
    padding: var(--s-space-3) 0;
    background-color: var(--s-color-surface);
    border-top: 1px solid var(--s-color-border);
}

/* A figure the form works out rather than asks for (the bill total, which is
   the net plus the tax). Sized and weighted like a field's own value so it
   reads as part of the form, not as a sentence about it. */
.stated-value {
    margin: 0;
    padding: 0.45rem 0;
    font-size: var(--s-font-size-md);
    font-weight: var(--s-font-weight-semibold);
    color: var(--s-color-text);
}

/* ---- Input group: a fixed box attached to the left of a field ----
   Used for the currency symbol beside an amount, so the figure being typed
   reads as money rather than as a number. The two halves share one border
   line and one focus ring, so they read as a single control rather than as a
   label that happens to sit nearby. */
.input-group {
    display: flex;
    align-items: stretch;
}
.input-group-prefix {
    display: flex;
    align-items: center;
    padding: 0 0.65rem;
    border: 1px solid var(--s-color-border);
    border-right: 0;
    border-radius: var(--s-radius-md) 0 0 var(--s-radius-md);
    background: var(--s-color-surface);
    color: var(--s-color-text-secondary);
    font-size: var(--s-font-size-md);
    white-space: nowrap;
    transition: border-color var(--s-transition-fast);
}
/* Square off the joined edge and let the input take the row's width. */
form.stacked .input-group input,
.line-cards .input-group input {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
}
.input-group:focus-within .input-group-prefix {
    border-color: var(--s-color-accent);
}
/* An empty prefix would otherwise draw as a thin sliver of border: a row
   whose currency cannot be told shows the plain box it always did. */
.input-group-prefix:empty {
    display: none;
}
.input-group-prefix:empty + input {
    border-radius: var(--s-radius-md);
}

/* Bare inputs outside .stacked (e.g. valuation qty peek) */
input[type="number"]:not(form.stacked input) {
    padding: 0.4rem 0.65rem;
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md);
    font: inherit;
    font-size: var(--s-font-size-md);
    background-color: var(--s-color-input-bg);
    color: var(--s-color-input-text);
}

/* Dark mode: invert the native calendar picker icon (WebKit/Blink) */
[data-theme="dark"] input[type="date"]::-webkit-calendar-picker-indicator {
    filter: invert(1) brightness(2) contrast(1);
}

/* ---- Filter bars (list pages, org approval oversight) ----
   `.filters` is the plain-GET form used to filter a list (item list,
   organization pending requests). Its controls get the same field look as
   .stacked forms — padding, border, radius, focus ring — but arranged as a
   compact wrapping flex row with labels left of their field, so the bar
   reads as one control cluster instead of a row of bare browser widgets. */
.filters {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s-space-2) var(--s-space-3);
}
.filters label {
    font-size: var(--s-font-size-sm);
    font-weight: var(--s-font-weight-medium);
    color: var(--s-color-text-secondary);
}
.filters input,
.filters select {
    padding: 0.45rem 0.65rem;
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md);
    font: inherit;
    font-size: var(--s-font-size-md);
    background-color: var(--s-color-input-bg);
    color: var(--s-color-input-text);
    transition: border-color var(--s-transition-fast),
                box-shadow var(--s-transition-fast);
}
.filters input::placeholder {
    color: var(--s-color-input-placeholder);
}
/* A tick box in the bar ("Include parts not sold to customers"): the box
   before its words, without the text-field padding above. */
.filters label.checkline {
    display: flex;
    align-items: center;
    gap: var(--s-space-1);
    cursor: pointer;
}
.filters label.checkline input {
    padding: 0;
    margin: 0;
}
.filters input:focus,
.filters select:focus {
    outline: none;
    border-color: var(--s-color-accent);
    box-shadow: 0 0 0 3px var(--s-color-accent-light);
}
.filters input[type="search"] {
    min-width: 14rem;
}
/* The shared list filter bar (common/components/list_filter_bar.html): the
   search box and each dropdown share the bar's width left to right; the
   search icon and Clear keep their own size. A bar with more dropdowns
   gives each the same share; on a narrow screen they wrap onto rows. */
.list-filters input[type="search"] {
    flex: 4 1 14rem;
}
.list-filters .multiselect,
.list-filters select {
    flex: 1 1 10rem;
}
.list-filters .multiselect-toggle {
    width: 100%;
    min-width: 0;
}
.list-filters > .btn-icon {
    flex: 0 0 auto;
    align-self: stretch;
    width: 2.5rem;
    height: auto;
}
.list-filters > .btn {
    flex: 0 0 auto;
}

/* The grid version of the bar (filter_layout="grid", the Item list): search
   and its buttons on the top row, then the dropdowns in a grid, each with its
   label above it in one .filter-field, so a label never ends a line while its
   dropdown starts the next. Three or four columns on a wide screen, one on a
   phone; every gap is the same 16px. */
.list-filters--grid {
    flex-direction: column;
    align-items: stretch;
    flex-wrap: nowrap;
    gap: var(--s-space-4);
}
.list-filters__top {
    display: flex;
    align-items: stretch;
    gap: var(--s-space-3);
}
.list-filters__top input[type="search"] {
    flex: 1 1 auto;
    min-width: 0;
}
.list-filters__top > .btn-icon {
    flex: 0 0 auto;
    align-self: stretch;
    width: 2.625rem;
    height: auto;
}
.list-filters__top > .btn {
    flex: 0 0 auto;
    align-self: center;
}
.list-filters__fields {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
    align-content: start;
    align-items: start;
    gap: var(--s-space-4);
}
.list-filters--grid .filter-field {
    display: flex;
    flex-direction: column;
    gap: var(--s-space-1);
    min-width: 0;
}
.list-filters--grid .filter-field select {
    flex: none;
    width: 100%;
    min-width: 0;
}

/* ---- Filter bar: checkbox-dropdown multi-select ----
   A native `<select multiple>` needs Ctrl/Cmd+click to pick more than one
   option, which almost nobody discovers on their own. This renders as a
   button that opens a checkbox panel instead; the checkboxes are ordinary
   `name="status" value="…"` inputs, so the submitted GET params — and the
   FilterSet reading them — need no changes. Alpine-driven (open/x-show),
   the same click-to-open/click-outside-to-close shape as the sidebar's
   .popup-menu, but scoped to its own `x-data` rather than $store.sidebar
   since a page can have more than one of these. */
.multiselect {
    position: relative;
}
.multiselect-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-space-2);
    min-width: 12rem;
    padding: 0.45rem 0.65rem;
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md);
    font: inherit;
    font-size: var(--s-font-size-md);
    background-color: var(--s-color-input-bg);
    color: var(--s-color-input-text);
    cursor: pointer;
}
.multiselect-toggle:focus {
    outline: none;
    border-color: var(--s-color-accent);
    box-shadow: 0 0 0 3px var(--s-color-accent-light);
}
.multiselect-toggle svg {
    width: 16px;
    height: 16px;
    flex-shrink: 0;
    color: var(--s-color-text-secondary);
}
.multiselect-panel {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    z-index: var(--s-z-popover);
    min-width: 100%;
    background-color: var(--s-color-surface);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-lg);
    box-shadow: var(--s-shadow-xl);
    padding: var(--s-space-2);
}
.multiselect-option {
    display: flex;
    align-items: center;
    gap: var(--s-space-2);
    padding: var(--s-space-2) var(--s-space-3);
    border-radius: var(--s-radius-md);
    font-size: var(--s-font-size-base);
    font-weight: var(--s-font-weight-normal);
    white-space: nowrap;
    cursor: pointer;
}
.multiselect-option:hover {
    background-color: var(--s-color-accent-light);
}

/* ---- List page header: title + meta on the left, actions on the right ----
   Used by data-dense list screens (item list, and any future list page that
   wants the same title/count/actions row instead of a bare <h1>). */
.list-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-space-4);
    flex-wrap: wrap;
    margin-bottom: var(--s-space-3);
}
.list-header h1 {
    margin: 0 0 0.2rem;
}
.list-header .list-header-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s-space-2);
}

/* ---- Sortable table heading link (common/components/list_sort_head.html) ----
   The whole heading is the sort toggle; it must read as a heading, not as a
   link, so it inherits the <th>'s colour and drops the underline. */
.data-table th .sort-link {
    color: inherit;
    text-decoration: none;
}
.data-table th .sort-link:hover {
    text-decoration: underline;
}

/* ---- Table footer: pagination row under a .card table ---- */
.table-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--s-space-3) var(--s-space-4);
    border-top: 1px solid var(--s-color-border);
    background: var(--s-color-bg-alt);
    font-size: var(--s-font-size-sm);
    color: var(--s-color-text-secondary);
}
.table-footer .table-footer-pages {
    display: flex;
    gap: var(--s-space-2);
}

/* ---- Detail page header: breadcrumb + title + meta ----
   Used above a pipeline/hub partial (enquiry detail, BOM page). */
.detail-header {
    margin-bottom: var(--s-space-3);
}
/* A header that carries the overflow menu puts the menu hard right, level with the
   heading's first line, which is where the notification bell is centred
   (static/js/common/shell-bell.js). The crumb, heading and meta line are named
   areas, so the menu stays on the heading's row whether or not there is a crumb,
   and a long name wraps under itself rather than pushing the menu off the page.
   The menu's box is one heading line tall and centres its button in it. */
.detail-header.has-menu {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
        "crumb crumb"
        "title menu"
        "meta meta";
    column-gap: var(--s-space-3);
    align-items: start;
}
.detail-header.has-menu > .detail-header-titles {
    display: contents;
}
.detail-header.has-menu .breadcrumb {
    grid-area: crumb;
}
.detail-header.has-menu h1 {
    grid-area: title;
}
.detail-header.has-menu .detail-meta {
    grid-area: meta;
}
.detail-header.has-menu > .overflow-menu {
    grid-area: menu;
    display: flex;
    align-items: center;
    height: calc(var(--s-font-size-3xl) * var(--s-line-height-tight));
}
.detail-header .breadcrumb {
    display: block;
    margin: 0 0 0.3rem;
    font-size: var(--s-font-size-sm);
}
.detail-header h1 {
    margin: 0 0 0.2rem;
}
.detail-header .detail-meta {
    margin: 0;
}

/* ---- A slim page bar with a click-open summary (the BOM page) ----
   One line: crumb, reference (the h1, drawn at crumb size), what it is, its status and an (i) button.
   The (i) opens a card over the page (.info-popover__panel) instead of a Summary card taking room. */
.bom-bar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s-space-2);
    font-size: var(--s-font-size-md);
}
.bom-bar a {
    text-decoration: none;
}
.bom-bar__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-2);
    margin-left: auto;
}
.detail-header.bom-bar h1 {
    margin: 0;
    font-size: inherit;
    font-weight: var(--s-font-weight-semibold);
}
.info-popover {
    position: relative;
    display: inline-flex;
    align-items: center;
}
.info-popover__button.info-hint {
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
}
.info-popover__panel {
    position: absolute;
    left: 0;
    top: calc(100% + var(--s-space-2));
    width: min(32rem, 90vw);
    padding: var(--s-space-3) var(--s-space-4);
    background-color: var(--s-color-surface);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-lg);
    box-shadow: var(--s-shadow-xl);
    font-size: var(--s-font-size-sm);
    z-index: var(--s-z-popover);
}
.info-popover__panel .card {
    margin: 0;
    padding: 0;
    border: 0;
    box-shadow: none;
}

/* A job with several BOMs: the BOM selector's row ends with the info button. The tabs scroll sideways inside
   their own strip, so the button sits beside it (a card opened inside would be cut off), and the row's
   underline carries on under the button. */
.bom-switcher {
    display: flex;
    align-items: stretch;
    margin-bottom: var(--s-space-5);
}
.bom-switcher > .tab-strip {
    flex: 1 1 auto;
    min-width: 0;
    margin-bottom: 0;
}
.bom-switcher__info {
    display: flex;
    align-items: center;
    padding-left: var(--s-space-3);
    border-bottom: 1px solid var(--s-color-border);
}
.bom-switcher__info .info-popover__panel {
    left: auto;
    right: 0;
}

/* What a summary card lets you do, set apart under a rule: full-width buttons, icon first. */
.info-popover__actions {
    display: flex;
    flex-direction: column;
    gap: var(--s-space-2);
    margin-top: var(--s-space-3);
    padding-top: var(--s-space-3);
    border-top: 1px solid var(--s-color-border);
}
.info-popover__actions > form {
    margin: 0;
}
.info-popover__actions .btn {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: var(--s-space-2);
    width: 100%;
}
.info-popover__actions .btn .material-icons-outlined {
    font-size: 1.1rem;
    line-height: 0;
}
.info-popover__actions .btn .info-popover__chevron {
    margin-left: auto;
    transition: transform var(--s-transition-fast);
}
.info-popover__actions .btn[disabled] {
    cursor: not-allowed;
}
.info-popover__actions form.stacked .btn {
    width: auto;
}

/* ---- Summary card: the first card on every detail page ----
   One label/value table per record, drawn by the {% summary_card %} tag
   (common/templatetags/summary.py). Fixed columns so that swapping a pill
   for its dropdown, or read text for an input, never reflows the table:
   the widths come from here, not from what is in the cells. A label wraps
   inside its column: the global th is nowrap, which let a long one (Tax
   classification code) run into the value beside it. */
.summary-card .summary-table {
    table-layout: fixed;
    width: 100%;
}
.summary-card .summary-table > tr > th,
.summary-card .summary-table > tbody > tr > th {
    width: 11rem;
    white-space: normal;
    overflow-wrap: anywhere;
}

/* A control that edits a Summary row in place (a draft quotation's currency
   and validity date). It takes the shared field look from form.stacked, but
   sits beside its info icon at a normal width instead of stretching across
   the whole row. */
.summary-card form.stacked.summary-field {
    display: flex;
    align-items: center;
    gap: var(--s-space-2);
    margin: 0;
}
.summary-card form.stacked.summary-field select,
.summary-card form.stacked.summary-field input {
    width: auto;
    min-width: 12rem;
    max-width: 100%;
    /* As tall as the status pill and the plain text in the rows around it,
       so choosing a control does not make its row taller than the others. */
    padding: 0.1rem 0.5rem;
    font-size: var(--s-font-size-sm);
    line-height: 1.5;
}

/* A card that a same-page link jumps to (the order's Open projects cell goes to its Projects card)
   is ringed for a moment so the eye finds it. The ring fades to the card's own look; with reduced
   motion it is not drawn. */
.card:target {
    scroll-margin-top: var(--s-space-4);
    animation: jump-target 2s ease-out;
}
@keyframes jump-target {
    0%,
    35% {
        box-shadow: 0 0 0 3px var(--s-color-accent), 0 0 0 9px var(--s-color-accent-light);
    }
}
@media (prefers-reduced-motion: reduce) {
    .card:target {
        animation: none;
    }
}

/* ---- Detail columns (2026-09-29) ----
   Two cards side by side on a detail page (the order's Summary beside its
   Delivery, its Jobs beside its Invoicing), so a record reads on one screen
   rather than down a long column. Cards lose their own bottom margin inside
   the grid, or the stretched cell would be taller than the card. One column
   when the page is too narrow to read two. */
.detail-columns {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-space-4);
    margin-bottom: var(--s-space-4);
    align-items: start;
}
.detail-columns > .card {
    margin-bottom: 0;
    min-width: 0;
}
@media (max-width: 900px) {
    .detail-columns {
        grid-template-columns: minmax(0, 1fr);
    }
}
/* A row of buttons that each open a panel below them (the order's "add
   lines" choices). The one open reads as the primary button. */
.panel-toggles {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-2);
    margin-top: var(--s-space-4);
}
.panel-toggles .btn {
    display: inline-flex;
    align-items: center;
    gap: var(--s-space-2);
}
.panel-toggles .btn .material-icons-outlined {
    line-height: 0;
}

/* ---- Process pipeline: step cards ----
   Each step of a hub/pipeline screen (Enquiry pipeline today) renders as its
   own .card. These modifiers give that card the complete/current/pending
   states the Process Pipeline redesign calls for, without changing which
   Django conditional renders which card. */
.step-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-space-3);
    flex-wrap: wrap;
    margin-bottom: var(--s-space-2);
}
.step-header-left {
    display: flex;
    align-items: center;
    gap: var(--s-space-2);
}
.step-header-left h2 {
    margin: 0;
}
.step-badge {
    width: 1.5rem;
    height: 1.5rem;
    border-radius: var(--s-radius-full);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    font-size: var(--s-font-size-xs);
    font-weight: var(--s-font-weight-bold);
}
.step-badge.complete {
    background: var(--s-color-success);
    color: #fff;
}
.step-badge.current {
    background: var(--s-color-accent);
    color: #fff;
}
.step-badge.pending {
    background: var(--s-color-surface);
    border: 1px solid var(--s-color-border-strong);
    color: var(--s-color-text-tertiary);
}
.step-body {
    padding-left: calc(1.5rem + var(--s-space-2));
}
.step-body > *:last-child {
    margin-bottom: 0;
}
.card.step-current {
    border-color: var(--s-color-accent);
    box-shadow: 0 0 0 3px var(--s-color-accent-light);
}
.card.step-pending {
    background: var(--s-color-bg-alt);
    opacity: 0.65;
}
.card.step-pending .step-header-left h2 {
    color: var(--s-color-text-secondary);
}

/* ---- BOM Line Grid: spreadsheet-style row entry ----
   Borderless until hovered/focused, so a dense grid of rows doesn't read as
   a wall of boxes — border and background appear only on interaction. */
.bom-grid-input {
    width: 100%;
    padding: 0.35rem 0.5rem;
    border: 1px solid transparent;
    border-radius: var(--s-radius-sm);
    font: inherit;
    font-size: var(--s-font-size-sm);
    background: transparent;
    color: var(--s-color-input-text);
}
.bom-grid-input:hover {
    border-color: var(--s-color-border);
    background: var(--s-color-surface);
}
.bom-grid-input:focus {
    outline: none;
    border-color: var(--s-color-accent);
    background: var(--s-color-surface);
    box-shadow: 0 0 0 3px var(--s-color-accent-light);
}
.bom-grid-row-new {
    background: var(--s-color-surface-hover);
}
/* The row being changed. The typing row moves into the line's own place, so
   this tint is what says "this is the line you are changing" rather than a
   new one being added at the bottom. */
.bom-grid-row-editing > td {
    background: var(--s-color-accent-light);
}

/* ---- Line grid: one compact row per line (RFQ lines, purchase order lines) ----
   The density of the BOM grid (css/bom_grid.css): 28px rows, 13px text, 11px
   headings, and the same borderless-until-hovered boxes as above. Headings
   are written once, not above every box. A warning or hint under a box makes
   its row taller, and only that row. The table scrolls sideways inside its
   card on a narrow screen. */
.line-grid {
    width: 100%;
    table-layout: fixed;
    border-collapse: collapse;
}
.line-grid th {
    height: 32px;
    padding: 0 6px;
    background: var(--s-color-surface-hover);
    color: var(--s-color-text-secondary);
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    white-space: nowrap;
}
.line-grid th .info-hint.material-icons-outlined {
    margin-left: 4px;
    font-size: 14px;
    text-transform: none;
}
.line-grid td {
    padding: 0 6px;
    border-bottom: 1px solid var(--s-color-border);
    font-size: 13px;
    line-height: 28px;
    /* Centred in the row, so a row made taller by a note or a warning keeps
       its other cells level with the middle of it, not stuck at the top. */
    vertical-align: middle;
}
.line-grid td.num {
    text-align: right;
}
.line-grid tbody tr:hover {
    background: var(--s-color-surface-hover);
}
.line-grid .line-gutter {
    width: 2.25rem;
    background: var(--s-color-surface-hover);
    color: var(--s-color-text-tertiary);
    font-family: var(--s-font-mono);
    font-size: 11px;
    text-align: right;
}
:is(form.stacked, .line-scope) .line-grid .bom-grid-input {
    display: block;
    height: 27px;
    line-height: normal;
    padding: 0 6px;
    font-size: 13px;
}
:is(form.stacked, .line-scope) .line-grid td.num .bom-grid-input {
    text-align: right;
}
:is(form.stacked, .line-scope) .line-grid .bom-grid-input:disabled {
    background: transparent;
    color: var(--s-color-text-secondary);
    cursor: not-allowed;
}
.line-grid .btn-icon.is-primary {
    background: var(--s-color-accent);
    border-color: var(--s-color-accent);
    color: #fff;
}
.line-grid .pill a {
    color: inherit;
    text-decoration: underline;
}
.line-grid td p {
    margin: 0 0 2px;
    line-height: 1.35;
}
/* A line that has a note shows it under the product, and its button stays lit. */
.line-grid .line-note-mark {
    display: flex;
    align-items: center;
    gap: 4px;
    overflow: hidden;
    white-space: nowrap;
}
.line-grid .line-note-mark span:last-child {
    overflow: hidden;
    text-overflow: ellipsis;
}
.line-grid .line-note-mark .material-icons-outlined {
    flex: none;
    font-size: 15px;
    line-height: 0;
    color: var(--s-color-text-tertiary);
}
/* The note flyout. Fixed to the window, so the table's sideways scroll box cannot
   clip it; the page places it beside its button, as the BOM editor's popup is. */
.line-pop {
    position: fixed;
    z-index: 1000;
    display: grid;
    gap: 6px;
    width: 22rem;
    max-width: calc(100vw - 32px);
    padding: 8px;
    background: var(--s-color-surface);
    border: 1px solid var(--s-color-border-strong);
    border-radius: 6px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
    font-size: 12.5px;
    line-height: 1.4;
}
/* A flyout field's caption sits above its box. A block, not a grid: as a grid
   item the caption's text and its info icon each took a row of their own, so
   the icon dropped under the words (2026-10-04). The box goes onto its own
   line by being a block itself. */
:is(form.stacked, .line-scope) .line-pop label {
    margin: 0;
    display: block;
    font-size: var(--s-font-size-sm);
    font-weight: var(--s-font-weight-medium);
    color: var(--s-color-text-secondary);
}
:is(form.stacked, .line-scope) .line-pop label:not(.checkline) > :is(input, select, textarea) {
    display: block;
    width: 100%;
    margin-top: 2px;
}
:is(form.stacked, .line-scope) .line-pop .bom-grid-input {
    border-color: var(--s-color-border);
    background: var(--s-color-surface);
}
/* A tick box in a flyout reads as one line: the box, its words, then its
   info icon, not the grid a field's caption and box use. */
:is(form.stacked, .line-scope) .line-pop label.checkline {
    display: flex;
    align-items: center;
    gap: var(--s-space-1);
    font-weight: var(--s-font-weight-normal);
    color: var(--s-color-text);
    cursor: pointer;
}
:is(form.stacked, .line-scope) .line-pop label.checkline input[type="checkbox"] {
    width: auto;
    margin: 0;
}
.line-pop label.checkline .info-hint.material-icons-outlined {
    font-size: 1rem;
    line-height: 0;
}
/* A flyout holding more than one form (a drawing's edit and its Remove
   file) lays each out like a single-form flyout, the second set apart. */
.line-pop > form {
    display: grid;
    gap: 6px;
}
.line-pop > form.line-pop__remove {
    padding-top: 6px;
    border-top: 1px solid var(--s-color-border);
}
/* The search list inside a flyout stays short, so it fits under the field. */
.line-pop .ss-list {
    max-height: 10rem;
}
.line-pop__title {
    display: flex;
    align-items: center;
    gap: 4px;
}
.line-pop__actions {
    display: flex;
    justify-content: flex-end;
}
.line-grid .line-acts .btn-icon.is-on {
    color: var(--s-color-accent);
    border-color: var(--s-color-accent);
}
.line-grid .line-acts {
    display: flex;
    gap: 1px;
    justify-content: flex-end;
    white-space: nowrap;
}
.line-grid .line-acts .btn-icon {
    width: 24px;
    height: 24px;
    margin: 1px 0;
}

/* Outlined where the box must be seen before it is hovered: a required reason. */
:is(form.stacked, .line-scope) .line-grid .bom-grid-input.is-outlined {
    border-color: var(--s-color-border);
    background: var(--s-color-surface);
}
.line-grid label.checkline {
    line-height: 1.35;
    margin: 2px 0;
}
/* A price with its currency symbol beside it, without a box of its own. */
.line-grid .input-group {
    align-items: center;
}
.line-grid .input-group-prefix {
    padding: 0 2px 0 0;
    border: 0;
    background: none;
    font-size: 13px;
}
:is(form.stacked, .line-scope) .line-grid .input-group input {
    border-radius: var(--s-radius-sm);
}
.line-grid td.line-total {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Column widths are set from the longest value a box has to show, so a number
   is never cut off (10 read as 1). They were measured in a browser on the
   modern look, where the boxes have 14px text and 12px of padding each side
   (ui.css), and the cell has 12px each side: a digit takes up to 8px, and
   --cell-edge is the cell padding, the box padding and the border together
   (50px) with a little to spare. A grid names its columns with these
   (.po-lines, .quote-lines). Change the look of a box and measure again. */
.line-grid {
    --cell-char: 0.55rem;
    --cell-edge: 3.3rem;
    /* 1250000.500 */
    --col-qty: calc(11 * var(--cell-char) + var(--cell-edge));
    /* A short drop-down: its longest option (Service, a unit code) is 7
       characters, and the arrow has its own strip beside the text. */
    --col-select: calc(7 * var(--cell-char) + 3.6rem);
    /* 12345678.123, and the currency sign in front of the box. */
    --col-money: calc(12 * var(--cell-char) + var(--cell-edge) + 2.8rem);
    /* 100.00 */
    --col-pct: calc(6 * var(--cell-char) + var(--cell-edge));
    /* A total is plain text with no box: 15 characters, such as
       ₹1,23,45,678.00, and the cell padding. */
    --col-total: calc(15 * var(--cell-char) + 1.6rem);
}
/* The up and down arrows take about 15px of a narrow box and the mouse wheel
   changes a value by accident. The arrow keys still step it. */
.line-grid input[type="number"] {
    -moz-appearance: textfield;
    appearance: textfield;
    font-variant-numeric: tabular-nums;
}
.line-grid input[type="number"]::-webkit-inner-spin-button,
.line-grid input[type="number"]::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}
/* A drop-down's arrow gets its own strip on the right, so it never draws over
   the text. The shared select style reserves one, but the grid's compact
   padding above replaces it. The arrow is also smaller here, to suit the row. */
:is(form.stacked, .line-scope) .line-grid select.bom-grid-input {
    padding-right: 1.4rem;
    background-position: right 0.4rem center;
    background-size: 14px 14px;
}
/* Typed words that still do not fit end in dots, not a cut letter. Boxes that
   can run long carry the full text in their title. A drop-down is never cut:
   its column is sized to its longest option instead. */
.line-grid input.bom-grid-input[type="text"] {
    text-overflow: ellipsis;
}

/* RFQ lines: the product takes whatever the other columns leave. */
.rfq-lines { min-width: 64rem; }
.rfq-lines th:nth-child(1) { width: 2.25rem; }
.rfq-lines th:nth-child(3) { width: 5rem; }
.rfq-lines th:nth-child(4) { width: 6rem; }
.rfq-lines th:nth-child(5) { width: 11rem; }
.rfq-lines th:nth-child(6) { width: 9.5rem; }
.rfq-lines th:nth-child(7) { width: 8rem; }
.rfq-lines th:nth-child(8) { width: 8.5rem; }
.rfq-lines .rfq-internal {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Purchase order lines: the item or work takes whatever the others leave. */
.po-lines { min-width: 100rem; }
.po-lines th:nth-child(1) { width: 2.25rem; }
.po-lines th:nth-child(2) { width: var(--col-select); }
.po-lines th:nth-child(4) { width: var(--col-qty); }
.po-lines th:nth-child(5) { width: var(--col-select); }
.po-lines th:nth-child(6) { width: var(--col-money); }
.po-lines th:nth-child(7) { width: var(--col-pct); }
.po-lines th:nth-child(8) { width: 9rem; }
.po-lines th:nth-child(9) { width: var(--col-pct); }
.po-lines th:nth-child(10) { width: var(--col-total); }
.po-lines th:nth-child(11) { width: 5.5rem; }
/* Purchase request lines: the item or description takes whatever the others leave. */
.pr-lines { min-width: 76rem; }
.pr-lines th:nth-child(1) { width: 2.25rem; }
.pr-lines th:nth-child(3) { width: var(--col-qty); }
.pr-lines th:nth-child(4) { width: var(--col-select); }
.pr-lines th:nth-child(5) { width: var(--col-money); }
.pr-lines th:nth-child(6) { width: var(--col-total); }
.pr-lines th:nth-child(7) { width: 16rem; }
.pr-lines th:nth-child(8) { width: 3.5rem; }
/* Send work out lines: the item takes whatever the others leave. A row stays one
   line high: the quantity carries its unit, and the store carries its shelf
   beside it. */
.send-lines { min-width: 84rem; }
.send-lines th.line-gutter { width: 2.25rem; }
.send-lines th.c-qty { width: calc(var(--col-qty) + 3rem); }
.send-lines th.c-work { width: 11rem; }
.send-lines th.c-step { width: 13rem; }
.send-lines th.c-from { width: 22rem; }
.send-lines th.c-acts { width: 5.5rem; }
.send-lines .send-qty,
.send-lines .send-from {
    display: flex;
    align-items: center;
    gap: 6px;
}
.send-lines .send-qty .bom-grid-input {
    flex: 1 1 auto;
    min-width: 0;
}
.send-lines .send-qty__unit {
    flex: none;
    min-width: 2rem;
    color: var(--s-color-text-secondary);
    font-size: 12px;
    text-align: left;
}
/* Every box of a row starts on the same line. The item's cell grows taller
   when it carries notes or the drawing chip under its box, and the other
   boxes stay level with the item's box instead of floating down to the
   middle of the taller cell. The row number and the buttons follow. */
.send-lines tbody td {
    vertical-align: top;
    padding-top: 6px;
    padding-bottom: 6px;
}
.send-lines td.line-gutter {
    line-height: 27px;
}
.send-lines .line-acts {
    min-height: 27px;
    align-items: center;
}
.send-lines .line-chip-row {
    margin: 6px 0 0;
}
.send-lines tr.has-drawing-row > td {
    border-bottom-color: transparent;
}
.send-lines .send-from .bom-grid-input {
    flex: 1 1 0;
    min-width: 0;
}
/* The drawing, as a small quiet chip under the item that opens its own row. */
.send-lines .drawing-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    height: 24px;
    padding: 0 10px;
    border: 1px solid var(--s-color-border);
    border-radius: 999px;
    background: var(--s-color-surface);
    color: var(--s-color-text-secondary);
    font: inherit;
    font-size: 12px;
    line-height: 1;
    cursor: pointer;
}
.send-lines .drawing-chip:hover {
    border-color: var(--s-color-border-strong);
    color: var(--s-color-text);
}
.send-lines .drawing-chip:focus-visible {
    outline: 2px solid var(--s-color-accent);
    outline-offset: 2px;
}
.send-lines .drawing-chip .material-icons-outlined {
    font-size: 14px;
    line-height: 0;
}
/* The drawing, in a row of its own under the line: one boxed, softly shaded
   panel (as .record-history boxes information), with the fields in two
   columns, the question on one line and the buttons in one row. */
.send-lines td.drawing-cell {
    padding: 4px 6px 12px;
    line-height: 1.4;
}
.drawing-panel {
    display: grid;
    gap: 12px;
    padding: 12px 16px;
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md, 8px);
    background: var(--s-color-surface-hover);
    font-size: 13px;
}
.drawing-panel__section {
    display: grid;
    gap: 8px;
}
.drawing-panel__section .drawing-panel__section {
    gap: 12px;
}
.drawing-panel p,
.drawing-panel ul {
    margin: 0;
}
.drawing-panel__title {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: var(--s-color-text-secondary);
}
.drawing-panel__title .info-hint.material-icons-outlined {
    margin-left: 4px;
    font-size: 14px;
    text-transform: none;
}
.drawing-panel__files {
    padding-left: 1.1rem;
}
.drawing-panel__fields {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px 16px;
}
.drawing-panel label {
    display: block;
    margin: 0 0 4px;
    font-size: 12px;
    font-weight: 500;
    color: var(--s-color-text-secondary);
}
.drawing-panel label.checkline {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin: 0;
    font-size: 13px;
    font-weight: 400;
    color: var(--s-color-text);
}
.drawing-panel label.checkline input {
    width: auto;
    margin: 0;
}
.drawing-panel input[type="text"],
.drawing-panel select {
    width: 100%;
    margin: 0;
}
.drawing-panel__question {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 20px;
}
.drawing-panel__question > span {
    font-weight: 500;
}
@media (max-width: 40rem) {
    .drawing-panel__fields {
        grid-template-columns: minmax(0, 1fr);
    }
}
/* Quotation lines. Columns vary with who is looking and whether the quotation is
   locked (an Ordered column, and no buttons column), so they are named, not
   counted. The product takes whatever the others leave, so the minimum width is
   what keeps it from being squeezed to a few letters: about 16rem is left for
   it, and a narrower screen scrolls the table sideways instead. */
.quote-lines { min-width: 100rem; }
.quote-lines th.c-gutter { width: 2.25rem; }
.quote-lines th.c-qty { width: var(--col-qty); }
.quote-lines th.c-unit { width: var(--col-select); }
.quote-lines th.c-price { width: var(--col-money); }
.quote-lines th.c-disc { width: var(--col-pct); }
.quote-lines th.c-taxcode { width: 8rem; }
.quote-lines th.c-tax { width: var(--col-pct); }
.quote-lines th.c-lead { width: 8rem; }
.quote-lines th.c-ordered { width: 6.5rem; }
.quote-lines th.c-total { width: var(--col-total); }
.quote-lines th.c-acts { width: 7.5rem; }
/* A row whose boxes are outlined (one being changed, or the typing row) gets air
   above and below them, so the outlines do not touch the row's lines. */
.line-grid tr.line-editing td {
    padding-top: 8px;
    padding-bottom: 8px;
}
/* A saved line's words may run to two lines: they wrap, with a little air. */
.quote-lines td.line-product {
    line-height: 1.4;
    padding-top: 8px;
    padding-bottom: 8px;
}
.quote-lines td.line-product .pill {
    margin-top: 2px;
}

/* The tax code search list is fixed to the window, placed by the page, so the
   table's sideways scroll box cannot clip it on the last rows. */
.po-lines .issue-row__results {
    position: fixed;
    right: auto;
    z-index: 1000;
    width: 22rem;
    max-width: calc(100vw - 32px);
    font-size: 13px;
    line-height: 1.4;
}

/* ---- BOM lines table: geometry that does not move ----
   A row on this table toggles between a Source *badge* and a Source
   *select*, and between a printed unit cost and an input. With the browser
   sizing columns from content, every one of those toggles re-measured the
   whole grid and every column jumped. table-layout:fixed hands sizing to
   the <colgroup> in production/_bom_section.html instead, so the geometry
   is a property of the table, not of whichever row happens to be open.
   min-width keeps the grid readable and lets .table-scroll take over on a
   narrow viewport rather than crushing the columns. */
.bom-lines-table {
    table-layout: fixed;
    width: 100%;
    /* The template sets --bom-table-min to the sum of its own column widths,
       which differ by whether the cost columns render at all. */
    min-width: var(--bom-table-min, 78.5rem);
}
.bom-lines-table th,
.bom-lines-table td {
    vertical-align: top;
    /* break-word, not anywhere: a long item code may break mid-token, ordinary
       text still wraps between words. */
    overflow-wrap: break-word;
}

/* The three layouts that must NOT be stated inline: Alpine's x-show removes an
   element's inline `display` when it shows it (x-show.js →
   style.removeProperty('display')), so an inline display:flex survives the
   first paint and is gone after the first toggle — which is what made the edit
   controls stack vertically under the dropdown instead of sitting inline. */
.bom-source-badges {
    display: inline-flex;
    gap: 0.3rem;
    flex-wrap: wrap;
    align-items: center;
}
.bom-source-edit {
    display: flex;
    gap: 0.3rem;
    flex-wrap: wrap;
    align-items: center;
    margin: 0;
}
.bom-cost-edit {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    align-items: flex-end;
}

/* The toolbar under a BOM's lines: the inventory check and when it last ran.
   Inside the same card as the table it acts on, not a card of its own. */
.bom-table-actions {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
    margin-top: 0.6rem;
}
/* One <tbody> per line (each row needs its own Alpine scope so the guidance
   strip below it can read that row's state). The global
   `tbody tr:last-child td` border reset would then fire on EVERY line and
   erase all the row separators — restore them for every tbody but the last. */
.bom-lines-table tbody:not(:last-of-type) tr:last-child td {
    border-bottom: 1px solid var(--s-color-border);
}
/* The guidance strip under a row being edited: same row, quieter ground, no
   hover response — it is a caption, not a record. */
.bom-lines-table tr.bom-edit-hint td {
    padding-top: 0;
    background: var(--s-color-bg-alt);
    line-height: 1.35;
}
.bom-lines-table tr.bom-edit-hint:hover {
    background: none;
}

/* ---- A <select> wearing a .pill's clothes ----
   For a value that reads as a badge when it is settled and has to be
   editable in the same slot: the cell looks the same either way, so opening
   a row for edit no longer looks like a different table. Keeps the native
   dropdown arrow — a custom one buys nothing and loses platform behaviour. */
.pill-select {
    display: inline-block;
    max-width: 100%;
    padding: 0.1rem 0.25rem 0.1rem 0.5rem;
    border-radius: 999px;
    border: 1px solid var(--s-color-border-strong);
    background: var(--s-color-bg-alt);
    color: var(--s-color-text-secondary);
    font: inherit;
    font-size: var(--s-font-size-sm);
    font-weight: var(--s-font-weight-medium);
    line-height: 1.5;
    cursor: pointer;
}
.data-table .pill-select {
    font-size: var(--s-font-size-xs);
}
.pill-select:hover {
    border-color: var(--s-color-accent);
}
.pill-select:focus {
    outline: none;
    border-color: var(--s-color-accent);
    box-shadow: 0 0 0 3px var(--s-color-accent-light);
}

/* ---- Two-column cost estimate grid (BOM page) ---- */
.cost-estimate-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--s-space-4);
}

/* ---- AI judgment panel (BOM substitute suggestions) ---- */
.ai-judgment-panel {
    margin-top: var(--s-space-2);
    border-top: 1px solid var(--s-color-border);
    padding-top: var(--s-space-2);
}
.ai-candidate-row {
    display: flex;
    align-items: center;
    gap: var(--s-space-2);
    margin-bottom: var(--s-space-1);
    font-size: var(--s-font-size-sm);
}

/* ---- Tab strip (dashboard, and any future tabbed page) ----
   Both panels render server-side in the same response; Alpine only toggles
   x-show on the panel wrapper. .active reflects Alpine's :class binding. */
.tab-strip {
    display: flex;
    gap: var(--s-space-5);
    border-bottom: 1px solid var(--s-color-border);
    margin-bottom: var(--s-space-5);
}
/* `a` for tabs that are separate URLs (the Holds page), so each tab keeps
   its own search and paging. They look the same as the button tabs. */
.tab-strip button,
.tab-strip a {
    text-decoration: none;
    appearance: none;
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    padding: var(--s-space-3) var(--s-space-1);
    font: inherit;
    font-size: var(--s-font-size-base);
    font-weight: var(--s-font-weight-medium);
    color: var(--s-color-text-secondary);
    cursor: pointer;
    transition: color var(--s-transition-fast), border-color var(--s-transition-fast);
}
.tab-strip button:hover,
.tab-strip a:hover {
    color: var(--s-color-text);
}
.tab-strip button.active,
.tab-strip a.active {
    color: var(--s-color-accent-text);
    border-bottom-color: var(--s-color-accent);
}
/* A strip with more tabs than a phone is wide (the project page): it
   scrolls sideways inside itself, so the page never does. */
.tab-strip.tab-strip-scroll {
    overflow-x: auto;
    overflow-y: hidden;
    flex-wrap: nowrap;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
}
.tab-strip.tab-strip-scroll a,
.tab-strip.tab-strip-scroll button {
    flex: 0 0 auto;
    white-space: nowrap;
}
/* ---- Password requirements checklist (/profile/ "Change Password" tab,
   2026-08-27) — live check marks next to each AUTH_PASSWORD_VALIDATORS
   rule as the user types (Alpine :class binding, templates/users/
   profile.html). Unmet rows read as plain muted text with an open circle;
   .met flips both the icon glyph (via x-text) and this coloring to
   success green, so satisfied rules read as *done*, not just *different*. */
ul.password-requirements {
    list-style: none;
    margin: var(--s-space-1) 0 var(--s-space-3);
    padding: 0;
    display: grid;
    gap: var(--s-space-1);
}
ul.password-requirements li {
    display: flex;
    align-items: center;
    gap: var(--s-space-1);
    font-size: var(--s-font-size-sm);
    color: var(--s-color-text-secondary);
}
ul.password-requirements li .material-icons-outlined {
    font-size: 16px;
}
ul.password-requirements li.met {
    color: var(--s-color-success-text);
}
ul.password-requirements li.met .material-icons-outlined {
    color: var(--s-color-success);
}

/* ---- Django messages ---- */
ul.messages {
    list-style: none;
    padding: 0;
    margin: 0 0 var(--s-space-4);
}
ul.messages li {
    padding: var(--s-space-2) var(--s-space-3);
    border-radius: var(--s-radius-md);
    margin-bottom: var(--s-space-2);
    background: var(--s-color-info-bg);
    color: var(--s-color-info-text);
    border: 1px solid var(--s-color-border);
    font-size: var(--s-font-size-md);
}
ul.messages li.success {
    background: var(--s-color-success-bg);
    color: var(--s-color-success-text);
}
ul.messages li.warning {
    background: var(--s-color-warning-bg);
    color: var(--s-color-warning-text);
}
ul.messages li.error {
    background: var(--s-color-danger-bg);
    color: var(--s-color-danger-text);
}

/* ---- Upload Spinner ---- */
.upload-wrapper {
    position: relative;
}

/* The file's type beside the file picker (2026-10-06): the type is a short
   choice and the picker takes the rest of the row. One under the other when
   the column is too narrow to read both. */
.field-pair.upload-pair {
    grid-template-columns: minmax(12rem, 1fr) minmax(14rem, 2fr);
    align-items: end;
}
@media (max-width: 40rem) {
    .field-pair.upload-pair {
        grid-template-columns: 1fr;
    }
}

.upload-spinner {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--s-space-3);
    padding: var(--s-space-4);
    text-align: center;
}

.spinner {
    width: 32px;
    height: 32px;
    border: 3px solid var(--s-color-border);
    border-top-color: var(--s-color-accent);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

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

/* ---- Required-field marker (2026-08-31) ----
   A form that renders 17 fields with identical labels gives a person no way
   to tell the five they must fill from the twelve they may skip — the
   inventory item form's original defect. `required` on the input only
   surfaces at submit time, and only as the browser's own bubble, so the
   marker has to be visible before anyone types. Rendered by
   templates/common/components/form_field.html off `field.field.required`,
   so it can never disagree with what the form actually enforces.
   aria-hidden because the input's own `required` attribute is what a screen
   reader announces; this is the sighted equivalent, not a second signal. */
.req {
    color: var(--s-color-danger);
    margin-left: 0.15rem;
    font-weight: var(--s-font-weight-medium);
}

/* ---- Company typeahead (2026-08-31) ----
   The suggestion list under the name box on the Add Supplier / Add Customer
   forms. Positioned against the wrapper rather than the input so it overlays
   whatever follows instead of pushing the form down as matches arrive. */
.typeahead {
    position: relative;
}
.typeahead-results {
    position: absolute;
    z-index: 20;
    left: 0;
    right: 0;
    background: var(--s-color-surface);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md);
    box-shadow: var(--s-shadow-md, 0 4px 12px rgba(0, 0, 0, 0.12));
    max-height: 16rem;
    overflow-y: auto;
}
.typeahead-results:empty {
    display: none;
}
/* The same list in the page's flow rather than over it: the near matches a
   save stopped on (2026-10-15), which the person must read before deciding. */
.typeahead-results--static {
    position: static;
    box-shadow: none;
    margin-top: var(--s-space-2);
}
.typeahead-option {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-space-2);
    width: 100%;
    padding: 0.5rem 0.65rem;
    border: 0;
    border-bottom: 1px solid var(--s-color-border);
    background: transparent;
    color: var(--s-color-text);
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.typeahead-option:last-child {
    border-bottom: 0;
}
.typeahead-option:hover,
.typeahead-option:focus {
    background: var(--s-color-accent-light);
    outline: none;
}
/* The caption above a suggestion list ("Recently used"), and the line shown
   when a search matches nothing. `margin: 0` because it is a <p>, and a
   paragraph's default margins put a blank line above and below a caption that
   should sit tight against the names under it. */
.typeahead-note {
    margin: 0;
    padding: 0.3rem 0.65rem;
    font-size: var(--s-font-size-sm);
    color: var(--s-color-text-secondary);
}

/* A fieldset carrying .card keeps the card's own frame; it is a fieldset
   only so the whole block can be disabled at once. min-width:0 undoes the
   browser's default on fieldsets, which otherwise refuses to shrink and
   breaks the responsive width the card would have as a div. */
fieldset.card {
    min-width: 0;
}

/* ---- Legend on a structural fieldset (2026-09-09) ----
   Recording a transaction asks which kind of money it was first, and each
   branch of the form is wrapped in a fieldset so the half that does not
   apply can be DISABLED, not merely hidden: a hidden input still posts its
   value, a disabled one does not. Those fieldsets reset the browser's own
   frame inline, so the box never appears even before this stylesheet has
   been fetched. Only the legend is styled here, where it cannot affect
   whether the frame is drawn. */
fieldset > legend {
    padding: 0;
    font-weight: var(--s-font-weight-medium);
}

/* ---- Buttons under the purchase order lines (2026-09-25) ----
   Generate, save and cancel on the left, Preview at the right. */
.po-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-2);
    margin: var(--s-space-3) 0;
}
.po-actions__preview {
    margin-left: auto;
}

/* ---- Purchase order preview (2026-09-25) ----
   The document is A4 at its printed width (19.54cm); the page scrolls inside
   the dialog rather than stretching to the window, and zoom is applied to the
   page alone so the toolbar stays put. */
.po-preview__toolbar {
    position: sticky;
    top: calc(-1 * var(--s-space-4));
    z-index: 2;
    display: flex;
    align-items: center;
    gap: var(--s-space-2);
    margin: calc(-1 * var(--s-space-4)) calc(-1 * var(--s-space-5)) var(--s-space-3);
    padding: var(--s-space-2) var(--s-space-5);
    background-color: var(--s-color-surface);
    border-bottom: 1px solid var(--s-color-border);
}
.po-preview__level {
    min-width: 3rem;
    text-align: center;
}
.po-preview__scroll {
    overflow: auto;
    padding: var(--s-space-2);
    background-color: var(--s-color-bg-alt);
    border-radius: var(--s-radius-md);
}
/* The terms annexure. The PDF starts it on a new page with 2.2cm and 3.1cm
   margins (document_print.css, @page terms); a screen has no pages, so the
   preview marks the same break with a rule and sets the terms in the same
   measure (14.8cm of text on the 19.54cm form), rather than running them on
   in the form's own tight margins. Shared with the send pages' document box. */
.po-preview__page .setra-doc section.terms,
.document-preview .setra-doc section.terms {
    box-sizing: border-box;
    margin-top: 1.2cm;
    padding: 2.2cm 2.37cm 2cm;
    border-top: 1px dashed #999;
}
.po-preview__page {
    width: 19.54cm;
    box-sizing: content-box;
    background: #fff;
    color: #000;
    padding: 0.5rem;
}

/* ---- Email account page (2026-10-03) ----
   Sender, login and server each in their own card with their fields side by
   side, the way MONTI lays out the same form, so the whole account fits on
   one screen instead of a long single column. */
.email-account-page {
    max-width: 56rem;
}
/* An icon before a card heading. Both class names, because
   css/material-icons.css loads last and sets every icon to 24px/1, which
   would make the heading taller than its text. */
.card-title-icon.material-icons-outlined {
    font-size: 1.25rem;
    line-height: 0;
    vertical-align: middle;
    margin-right: var(--s-space-1);
    color: var(--s-color-text-secondary);
}

/* Choice buttons: a short set of options shown as side-by-side buttons with
   the chosen one filled in (the email account's security). Real radio
   inputs underneath, so the choice posts with the form and works from the
   keyboard. */
.choice-buttons {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-space-2);
}
form.stacked label.choice-button {
    display: inline-flex;
    align-items: center;
    margin: 0;
    padding: 0.45rem 0.85rem;
    border: 1px solid var(--s-color-border-strong);
    border-radius: var(--s-radius-md);
    background: var(--s-color-surface);
    color: var(--s-color-text);
    font-size: var(--s-font-size-md);
    font-weight: var(--s-font-weight-medium);
    cursor: pointer;
}
form.stacked label.choice-button:hover {
    border-color: var(--s-color-accent);
}
form.stacked label.choice-button:has(input:checked) {
    background: var(--s-color-accent);
    border-color: var(--s-color-accent);
    color: #fff;
}
form.stacked label.choice-button:has(input:focus-visible) {
    box-shadow: 0 0 0 3px var(--s-color-accent-light);
}
form.stacked .choice-button input[type="radio"] {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}
/* A quiet text-style button for a rare option ("Use a different port"). */
.link-button {
    padding: 0;
    border: 0;
    background: none;
    color: var(--s-color-accent-text);
    font: inherit;
    font-size: var(--s-font-size-sm);
    cursor: pointer;
    text-decoration: underline;
}

/* The outcome of a connection test or a test email: a tick or a cross, then
   what happened in words. */
.test-result {
    display: flex;
    align-items: flex-start;
    gap: var(--s-space-2);
    margin: var(--s-space-3) 0 0;
}
.test-result .material-icons-outlined {
    font-size: 1.25rem;
    line-height: 1.25rem;
}
.test-result--ok {
    color: var(--s-color-success-text);
}
.test-result--fail {
    color: var(--s-color-danger-text);
}
.test-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-3);
}
.test-row input[type="email"] {
    flex: 1 1 16rem;
    width: auto;
}

/* Who can use an email account: one row per person or team, what they may
   do, and a remove button. */
.access-row {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(9rem, 1fr) auto;
    gap: var(--s-space-2);
    align-items: center;
    margin-top: var(--s-space-2);
}
@media (max-width: 40rem) {
    .access-row {
        grid-template-columns: minmax(0, 1fr) auto;
    }
    .access-row select + select {
        grid-column: 1;
    }
}


/* ---- Rows dragged into order (static/js/common/sortable_rows.js) ---- */
.drag-cell {
    width: 2rem;
    padding-right: 0;
}
.drag-handle.material-icons-outlined {
    cursor: grab;
    color: var(--s-color-text-secondary);
    line-height: 0;
    vertical-align: middle;
    touch-action: none;
}
.sort-ghost {
    opacity: 0.4;
}

/* ---- Buttons in a row (2026-10-14) ----
   Buttons that belong together sit side by side and wrap only when the screen is too narrow. A form inside the
   row (a button that posts on its own) takes no margin of its own. */
.form-actions form,
.form-actions p {
    margin: 0;
}
/* Buttons in a table cell line up with the rest of the row: the gap above a
   row of buttons is for a form, and in a cell it pushes them below their
   neighbours. */
td .form-actions {
    margin-top: 0;
}

/* ---- Result of a check: a tick or a cross beside what was checked (inbound QC, 2026-10-14) ---- */
.check-results {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-space-2) var(--s-space-5);
    list-style: none;
    margin: var(--s-space-2) 0 0;
    padding: 0;
}
.check-result {
    display: inline-flex;
    align-items: center;
    gap: var(--s-space-1);
}
.check-result .material-icons-outlined {
    font-size: 20px;
    line-height: 0;
}
.check-result.is-ok .material-icons-outlined { color: var(--s-color-success-text); }
.check-result.is-bad .material-icons-outlined { color: var(--s-color-danger-text); }

/* ---- Earlier results kept on record, in a bordered box of their own ---- */
.record-history {
    margin-top: var(--s-space-6);
    padding: var(--s-space-4);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-lg);
    background: var(--s-color-surface-hover);
}
.record-history > h3 {
    margin: 0 0 var(--s-space-3);
    font-size: var(--s-font-size-md);
}
.record-history__list {
    list-style: none;
    margin: 0;
    padding: 0;
}
.record-history__item {
    padding: var(--s-space-3) 0;
    border-top: 1px solid var(--s-color-border);
}
.record-history__item:first-child {
    padding-top: 0;
    border-top: 0;
}
.record-history__item:last-child {
    padding-bottom: 0;
}
.record-history__line {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-1) var(--s-space-3);
}
/* ---- What the proposer added to an item that waits (2026-10-23) ----
   One row for each field: the field, what it was, what it is, and Use. On a phone each value
   carries its own caption and the row stacks, so nothing scrolls sideways. */
.detail-added {
    margin: var(--s-space-4) 0;
}
.detail-added__head,
.detail-added__row {
    display: grid;
    grid-template-columns: minmax(7rem, 1.2fr) minmax(5rem, 1fr) minmax(5rem, 1fr) minmax(8.5rem, auto);
    gap: var(--s-space-2) var(--s-space-4);
    align-items: center;
}
.detail-added__head {
    padding-bottom: var(--s-space-2);
    font-size: var(--s-font-size-sm);
    color: var(--s-color-text-secondary);
}
.detail-added__value small {
    display: none;
}
.detail-added__action {
    justify-self: end;
}
@media (max-width: 640px) {
    .detail-added__head {
        display: none;
    }
    .detail-added__row {
        grid-template-columns: 1fr 1fr;
    }
    .detail-added__field {
        grid-column: 1 / -1;
    }
    .detail-added__value small {
        display: block;
    }
    .detail-added__action {
        grid-column: 1 / -1;
        justify-self: start;
    }
}
/* ---- The conversation about a proposed item (2026-10-21) ----
   The proposer on the right in the accent colour, everyone who decides on the left. Colours are tokens so the
   dark theme follows; the bubble width is bounded so a long line stays readable and a phone shows no sideways
   scroll. */
.convo__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-space-2) var(--s-space-4);
    margin-bottom: var(--s-space-4);
}
.convo__head > h2 {
    margin: 0;
}
.convo__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--s-space-4);
}
.convo__day {
    display: flex;
    align-items: center;
    gap: var(--s-space-3);
    font-size: var(--s-font-size-xs);
    color: var(--s-color-text-secondary);
}
.convo__day::before,
.convo__day::after {
    content: "";
    flex: 1;
    border-top: 1px solid var(--s-color-border);
}
.convo__msg {
    display: flex;
    align-items: flex-end;
    gap: var(--s-space-2);
}
.convo__msg--proposer {
    flex-direction: row-reverse;
}
.convo__avatar {
    flex: none;
    width: 2rem;
    height: 2rem;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--s-radius-full);
    background: var(--s-color-bg-alt);
    border: 1px solid var(--s-color-border);
    color: var(--s-color-text-secondary);
    font-size: var(--s-font-size-xs);
    font-weight: var(--s-font-weight-semibold);
}
.convo__msg--proposer .convo__avatar {
    background: var(--s-color-accent);
    border-color: var(--s-color-accent);
    color: #fff;
}
.convo__body {
    min-width: 0;
    max-width: min(38rem, calc(100% - 2.5rem));
}
.convo__msg--proposer .convo__body {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
}
.convo__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-1) var(--s-space-2);
    margin-bottom: var(--s-space-1);
    font-size: var(--s-font-size-sm);
    color: var(--s-color-text-secondary);
}
.convo__meta strong {
    color: var(--s-color-text);
    font-weight: var(--s-font-weight-semibold);
}
.convo__role {
    padding: 0 var(--s-space-2);
    border-radius: 999px;
    background: var(--s-color-accent-light);
    color: var(--s-color-accent-text);
    font-size: var(--s-font-size-xs);
    font-weight: var(--s-font-weight-medium);
}
.convo__bubble {
    margin: 0;
    padding: var(--s-space-3) var(--s-space-4);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-xl);
    border-bottom-left-radius: var(--s-radius-sm);
    background: var(--s-color-surface-hover);
    color: var(--s-color-text);
    line-height: 1.5;
    overflow-wrap: anywhere;
}
.convo__msg--proposer .convo__bubble {
    border-color: transparent;
    border-bottom-left-radius: var(--s-radius-xl);
    border-bottom-right-radius: var(--s-radius-sm);
    background: var(--s-color-accent-light);
}
.convo__note {
    display: flex;
    align-items: center;
    gap: var(--s-space-2);
    margin: var(--s-space-4) 0 0;
    color: var(--s-color-text-secondary);
}
.convo__note .material-icons-outlined {
    font-size: 1.25rem;
    line-height: 0;
}
.convo__composer {
    margin-top: var(--s-space-5);
    padding-top: var(--s-space-4);
    border-top: 1px solid var(--s-color-border);
}
.convo__composer > label {
    display: block;
    margin-bottom: var(--s-space-2);
    font-weight: var(--s-font-weight-medium);
}
.convo__box {
    border: 1px solid var(--s-color-border-strong);
    border-radius: var(--s-radius-lg);
    background: var(--s-color-surface);
}
.convo__box:focus-within {
    border-color: var(--s-color-accent);
    box-shadow: 0 0 0 3px var(--s-color-accent-light);
}
.convo__box textarea {
    display: block;
    width: 100%;
    min-height: 3.5rem;
    max-height: 15rem;
    padding: var(--s-space-3) var(--s-space-4);
    border: 0;
    border-radius: var(--s-radius-lg) var(--s-radius-lg) 0 0;
    background: transparent;
    color: var(--s-color-text);
    /* 16px stops phones zooming in on the box. */
    font: inherit;
    font-size: 1rem;
    line-height: 1.5;
    resize: none;
    box-shadow: none;
}
.convo__box textarea:focus {
    outline: none;
    box-shadow: none;
}
.convo__foot {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--s-space-3);
    padding: var(--s-space-2) var(--s-space-3);
}
.convo__hint {
    margin-right: auto;
    font-size: var(--s-font-size-xs);
    color: var(--s-color-text-secondary);
}
/* A phone has no Ctrl key. */
@media (hover: none) {
    .convo__hint {
        display: none;
    }
}
.convo__count {
    font-size: var(--s-font-size-xs);
    color: var(--s-color-text-secondary);
    font-variant-numeric: tabular-nums;
}
.convo__count.is-over {
    color: var(--s-color-danger-text);
    font-weight: var(--s-font-weight-semibold);
}
.convo__foot .btn {
    display: inline-flex;
    align-items: center;
    gap: var(--s-space-1);
    min-height: 2.5rem;
}
.convo__foot .btn .material-icons-outlined {
    font-size: 1.1rem;
    line-height: 0;
}

/* ---- What differs from the plan: Planned, Used, Why (ui.md, never chain quantities) ---- */
.change-panel > h3 {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-2) var(--s-space-3);
}
.change-table {
    width: 100%;
    border-collapse: collapse;
}
.change-table th,
.change-table td {
    padding: var(--s-space-1) var(--s-space-3) var(--s-space-1) 0;
    text-align: left;
    vertical-align: top;
    font-weight: var(--s-font-weight-normal, 400);
}
.change-table th {
    width: 5.5rem;
    color: var(--s-color-text-secondary);
    white-space: nowrap;
}
.change-table .change-table__qty {
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    width: 1%;
    padding-right: var(--s-space-3);
}

/* ---- Card heading with an icon button at the top right ---- */
.card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-space-3);
    margin-bottom: var(--s-space-4);
}
.card-head > h2 { margin: 0; }

/* ---- Where an approval stands (workflows/_approval_status.html, 2026-10-14) ----
   A tinted banner with a large icon for the state, then one row per approver with a tick, cross or clock. */
.approval-status {
    display: flex;
    flex-direction: column;
    gap: var(--s-space-4);
}
.approval-status__banner {
    display: flex;
    align-items: center;
    gap: var(--s-space-3);
    padding: var(--s-space-3) var(--s-space-4);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-lg);
    background: var(--s-color-surface-hover);
}
/* Thicker line than the default hairline so the colour reads; compound selector because material-icons.css loads last. */
.approval-status__icon.material-icons-outlined {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    font-size: 32px;
    line-height: 1;
    font-variation-settings: 'FILL' 0, 'wght' 600, 'GRAD' 0, 'opsz' 24;
}
.approval-status__text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}
.approval-status__title {
    font-size: var(--s-font-size-md);
}
.approval-status__detail {
    color: var(--s-color-text-secondary);
}
.approval-status__rule {
    margin-left: auto;
    flex: none;
}
.approval-status__banner.is-ok { background: var(--s-color-success-bg); }
.approval-status__banner.is-ok .approval-status__icon { color: var(--s-color-success); }
.approval-status__banner.is-ok .approval-status__title { color: var(--s-color-success-text); }
.approval-status__banner.is-bad { background: var(--s-color-danger-bg); }
.approval-status__banner.is-bad .approval-status__icon { color: var(--s-color-danger); }
.approval-status__banner.is-bad .approval-status__title { color: var(--s-color-danger-text); }
.approval-status__banner.is-wait { background: var(--s-color-warning-bg); }
.approval-status__banner.is-wait .approval-status__icon { color: var(--s-color-warning); }
.approval-status__banner.is-wait .approval-status__title { color: var(--s-color-warning-text); }

.approval-approver-list {
    list-style: none;
    margin: 0;
    padding: 0;
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-lg);
}
.approval-approver {
    display: flex;
    align-items: flex-start;
    gap: var(--s-space-3);
    padding: var(--s-space-3) var(--s-space-4);
    border-top: 1px solid var(--s-color-border);
}
.approval-approver:first-child { border-top: 0; }
.approval-approver__icon.material-icons-outlined {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    font-size: 24px;
    line-height: 1;
    font-variation-settings: 'FILL' 0, 'wght' 600, 'GRAD' 0, 'opsz' 24;
    color: var(--s-color-text-secondary);
}
.approval-approver.is-ok .approval-approver__icon { color: var(--s-color-success); }
.approval-approver.is-bad .approval-approver__icon { color: var(--s-color-danger); }
.approval-approver__body { min-width: 0; }
.approval-approver__line {
    min-height: 24px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-1) var(--s-space-3);
}
.approval-approver__name { font-weight: var(--s-font-weight-medium); }
.approval-approver__reason {
    margin: var(--s-space-1) 0 0;
    color: var(--s-color-text-secondary);
}

/* ---- Optional fields behind a closed, clearly visible control (2026-10-14) ----
   A bordered, button-like row with an icon and a chevron. It opens by itself when the form comes back with an
   error or something in it, so a refusal is never hidden inside a closed section. */
details.optional-section {
    margin-top: var(--s-space-5);
}
details.optional-section > summary {
    display: flex;
    align-items: center;
    gap: var(--s-space-2);
    padding: var(--s-space-3) var(--s-space-4);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-lg);
    background: var(--s-color-surface);
    font-weight: var(--s-font-weight-medium);
    cursor: pointer;
    list-style: none;
}
details.optional-section > summary::-webkit-details-marker {
    display: none;
}
details.optional-section > summary:hover {
    background: var(--s-color-surface-hover);
}
details.optional-section > summary .material-icons-outlined {
    line-height: 0;
}
details.optional-section > summary .optional-section__chevron {
    margin-left: auto;
    transition: transform var(--s-transition-fast);
}
details.optional-section[open] > summary .optional-section__chevron {
    transform: rotate(180deg);
}
details.optional-section > .optional-section__body {
    padding-top: var(--s-space-4);
}

/* The pasted list on a sales BOM (2026-10-23): the questions about its values, the note above the columns, and
   the buttons that stay in view under a long list. */
details.staging-values {
    margin: var(--s-space-4) 0;
    padding: var(--s-space-3) var(--s-space-4);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-lg);
    background: var(--s-color-surface);
}
details.staging-values > summary {
    display: flex;
    align-items: center;
    gap: var(--s-space-2);
    font-weight: var(--s-font-weight-semibold);
    cursor: pointer;
    list-style: none;
}
details.staging-values > summary::-webkit-details-marker {
    display: none;
}
details.staging-values > summary .material-icons-outlined {
    line-height: 0;
}
.staging-value {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-2) var(--s-space-4);
    /* Room inside the row, so a lit row never touches its text or its dropdown; the row reaches out into
       the box's own padding so the text still lines up with the heading. */
    margin: 0 calc(-1 * var(--s-space-3));
    padding: var(--s-space-2) var(--s-space-3);
    border-top: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md);
}
/* Every answer box the same width, whatever the longest choice says. */
.staging-value select.form-select.form-select--sm {
    width: 15rem;
    max-width: 100%;
}
/* Something a data-jump link took the person to (jump_link.js): it and its first field light up once, then
   settle. Only the field the link focused is ringed, so a whole card can be a target. Nothing changes size; with reduced motion it just stays lit for a moment. */
.is-jumped {
    animation: jump-flash 1.4s ease-out;
}
.is-jumped :is(select, input, textarea):focus {
    animation: jump-ring 1.4s ease-out;
}
@keyframes jump-flash {
    0%, 35% { background: var(--s-color-accent-light); }
    100% { background: transparent; }
}
@keyframes jump-ring {
    0%, 35% { box-shadow: 0 0 0 3px var(--s-color-accent); }
    100% { box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: reduce) {
    .is-jumped,
    .is-jumped :is(select, input, textarea):focus {
        animation: none;
    }
    .is-jumped {
        background: var(--s-color-accent-light);
    }
}
.staging-value__text {
    display: flex;
    flex-direction: column;
    flex: 1 1 14rem;
    min-width: 0;
    overflow-wrap: anywhere;
}
.staging-value__done {
    color: var(--s-color-accent);
    line-height: 0;
}
.staging-value__done[hidden] {
    display: none;
}
.staging-columns-note {
    margin: var(--s-space-4) 0 var(--s-space-2);
}
.staging-actions {
    position: sticky;
    bottom: 0;
    z-index: 3;
    margin-top: var(--s-space-4);
    padding: var(--s-space-3) 0;
    border-top: 1px solid var(--s-color-border);
    background: var(--s-color-bg);
}
/* Space below the paste section, before whatever follows it (the history of changes). */
#sales-bom-staging {
    margin-bottom: var(--s-space-5);
}
/* The bar and what opens under it are one bordered section, not a bar with a field floating below it. */
#sales-bom-staging > details.optional-section {
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-lg);
    background: var(--s-color-surface);
}
#sales-bom-staging > details.optional-section > summary {
    border: 0;
    background: transparent;
}
#sales-bom-staging > details.optional-section[open] > summary {
    border-bottom: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-lg) var(--s-radius-lg) 0 0;
}
#sales-bom-staging > details.optional-section > .optional-section__body {
    padding: var(--s-space-4) var(--s-space-5) var(--s-space-5);
}
#sales-bom-staging .data-table td {
    vertical-align: middle;
}
.staging-col-item {
    min-width: 18rem;
}
.staging-col-ready {
    min-width: 13rem;
}
.staging-status {
    display: flex;
    align-items: center;
    gap: var(--s-space-2);
}
.staging-status--short {
    white-space: nowrap;
}
.staging-status .material-icons-outlined {
    line-height: 0;
}
.staging-suggest {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-space-3);
    margin: 0;
}
.staging-suggest + .staging-suggest {
    margin-top: var(--s-space-1);
}
.staging-suggest__text {
    flex: 1 1 auto;
    min-width: 0;
}
/* The item box of a row: the shared searchable select, looking like the other dropdowns on this screen. Its list
   opens over the page, so a row never grows. */
#sales-bom-staging .searchable-select [data-ss-input] {
    width: 100%;
    min-height: 2.25rem;
    padding: 0.3rem 0.75rem;
    padding-right: 2.25rem;
    border: 1px solid var(--s-color-border-strong);
    border-radius: var(--s-radius-lg);
    font: inherit;
    font-size: var(--s-font-size-md);
    background-color: var(--s-color-input-bg);
    color: var(--s-color-input-text);
}
#sales-bom-staging .searchable-select [data-ss-input]:focus {
    outline: none;
    border-color: var(--s-color-accent);
    box-shadow: 0 0 0 3px var(--s-color-accent-light);
}
#sales-bom-staging .searchable-select + .staging-suggest {
    margin-top: var(--s-space-2);
}
.staging-status--ready {
    color: var(--s-color-success-text);
}
.staging-status--waiting {
    color: var(--s-color-warning-text);
}
/* A pasted value looks like something to type in (an underline at rest), not plain text. */
#sales-bom-staging .data-table .bom-grid-input {
    border-bottom-color: var(--s-color-border-strong);
    border-radius: var(--s-radius-sm) var(--s-radius-sm) 0 0;
}

/* A row of buttons above a card or a figure row keeps clear of it (2026-10-14). */
.form-actions + .card,
.form-actions + .summary-row,
.form-actions + .alert {
    margin-top: var(--s-space-5);
}

/* Three columns of fields, for a page that uses the whole width (the supplier's invoice, 2026-10-14). Two on a
   medium screen, one on a phone. */
.form-grid.form-grid--3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (max-width: 1100px) {
    .form-grid.form-grid--3 {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}
@media (max-width: 760px) {
    .form-grid.form-grid--3 {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* The supplier's invoice lines: the delivery and the description take whatever the other columns leave. The widest
   drop-down is "We pay the tax (reverse charge)", 31 characters. */
.bill-lines { min-width: 140rem; }
.bill-lines th:nth-child(1) { width: 2.25rem; }
.bill-lines th:nth-child(4) { width: var(--col-qty); }
.bill-lines th:nth-child(5) { width: var(--col-select); }
.bill-lines th:nth-child(6) { width: var(--col-money); }
.bill-lines th:nth-child(7) { width: var(--col-pct); }
.bill-lines th:nth-child(8) { width: 8rem; }
.bill-lines th:nth-child(9) { width: var(--col-pct); }
.bill-lines th:nth-child(10) { width: var(--col-money); }
.bill-lines th:nth-child(11) { width: calc(31 * var(--cell-char) + 3.6rem); }
.bill-lines th:nth-child(12) { width: 7rem; }
.bill-lines th:nth-child(13) { width: 12rem; }
.bill-lines th:nth-child(14) { width: 12rem; }
.line-grid td.line-check { text-align: center; }

/* ---- Approvals tab on the Organization page (2026-10-15) ----
   One bordered card per approval. The heading of a group of cards has space above it, and the cards are
   spaced apart, so nothing touches the control above it. */
.approval-setup__group {
    margin-top: 1.5rem;
    margin-bottom: 0.5rem;
}
.approval-card {
    margin-bottom: 1rem;
}
.approval-card__head h4 {
    margin-bottom: 0.15rem;
}
.approval-card__choice {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 1.5rem;
    margin: 0.75rem 0;
    padding: 0;
    border: 0;
}
.approval-card__caption {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
}
.approval-card__note {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    max-width: 28rem;
    margin: 0 0 0.75rem;
    padding: 0.5rem 0.75rem;
    background: var(--s-color-bg-alt);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md);
}
.approval-card__note p {
    margin: 0;
}
.approval-card__choice label {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    margin: 0;
}
.approval-people {
    list-style: none;
    margin: 0 0 0.5rem;
    padding: 0;
    max-width: 28rem;
}
.approval-person {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.4rem 0.6rem;
    margin-bottom: 0.3rem;
    background: var(--s-color-surface);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md);
}
.approval-person__step {
    min-width: 1.4rem;
    text-align: center;
    font-weight: var(--s-font-weight-medium);
    color: var(--s-color-text-secondary);
}
.approval-person__name {
    flex: 1;
}
.approval-add {
    max-width: 18rem;
    min-width: 0;
    margin-bottom: 0.75rem;
}
.approval-card__none {
    margin-bottom: 0.75rem;
}
.approval-card__foot p {
    margin: 0 0 0.25rem;
}

/* ---- Approvals tab: routes, answers and the live picture (2026-10-17) ----
   An approval's card holds its routes (who raised it) as bordered groups with a heading that has space above it;
   the answers sit inside their route, never loose. On a wide screen the picture of the whole approval sits beside
   the routes in its own bordered box; on a phone it drops below them. */
.approval-setup__head {
    margin-bottom: var(--s-space-2);
}
.approval-card__where {
    margin: 0;
}
.approval-card__layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-space-5);
    margin-top: var(--s-space-4);
}
@media (min-width: 70rem) {
    .approval-card__layout {
        grid-template-columns: minmax(0, 1fr) minmax(18rem, 26rem);
        align-items: start;
    }
}
.approval-card__routes {
    display: flex;
    flex-direction: column;
    gap: var(--s-space-4);
    min-width: 0;
}
.approval-route {
    padding: var(--s-space-4);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-lg);
    background: var(--s-color-surface);
}
.approval-route--default {
    background: var(--s-color-surface-hover);
}
.approval-route__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-space-3);
}
.approval-route__title {
    margin: var(--s-space-2) 0 var(--s-space-3);
    font-size: var(--s-font-size-md);
}
.approval-route__hint {
    margin: 0 0 var(--s-space-2);
    font-size: var(--s-font-size-sm);
}
.approval-route__who {
    max-width: 26rem;
    margin-bottom: var(--s-space-3);
}
.approval-route__chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-space-2);
    list-style: none;
    margin: var(--s-space-2) 0;
    padding: 0;
}
.approval-route__mode {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-2) var(--s-space-5);
    margin: 0 0 var(--s-space-3);
    padding: 0;
    border: 0;
}
.approval-route__mode > label,
.approval-route__split > label {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    margin: 0;
}
.approval-route__split {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-space-2);
}
.approval-route__split input[type="number"] {
    width: 8rem;
    margin: 0;
}
.approval-route__unknown {
    margin-top: var(--s-space-4);
}
.approval-band {
    margin-top: var(--s-space-3);
    padding: var(--s-space-3) var(--s-space-4);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-md);
    background: var(--s-color-bg-alt);
}
.approval-band__title {
    margin: 0 0 var(--s-space-2);
    font-size: var(--s-font-size-sm);
    font-weight: var(--s-font-weight-medium);
    color: var(--s-color-text-secondary);
}
.approval-band__none {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    margin: 0 0 var(--s-space-2);
}
.approval-card__add-route {
    margin-top: 0;
}
.approval-card__picture {
    min-width: 0;
    padding: var(--s-space-4);
    border: 1px solid var(--s-color-border);
    border-radius: var(--s-radius-lg);
    background: var(--s-color-surface);
}
.approval-card__picture .card-subheading {
    margin-top: 0;
}
.approval-picture {
    overflow-x: auto;
}
.approval-picture svg {
    max-width: 100%;
    height: auto;
}
.approval-card__history {
    margin-top: var(--s-space-4);
}
.approval-card__history p {
    margin: 0 0 0.25rem;
}
