/* ==========================================================================
   InvoGrid — base stylesheet

   The visual language is deliberately Kitwell's: same CSS-variable theming,
   same [data-theme] switch on <html>, same header lockup, same component
   vocabulary. The two are sibling internal tools and should read as a family.
   This is InvoGrid's own copy of it, not a shared file — nothing here is
   loaded from, or has to stay byte-identical to, the other application.

   Where it differs, and why:
     - The accent is indigo rather than blue, so the two tools are told apart
       at a glance without leaving the family.
     - The desktop navigation bar appears at 1150px, the same as Kitwell's. It
       was 900px while this menu had three top-level items; a queue per question
       — review, existing invoices, duplicates — has taken it to six, which is
       Kitwell's count, so the difference that justified the lower breakpoint
       has gone.
     - There is a document/data split-pane vocabulary Kitwell has no need for.
   ========================================================================== */

:root {
    --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

    --radius: 10px;
    --radius-sm: 7px;
    --tap: 44px;

    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-5: 1.5rem;
    --space-6: 2rem;

    --shadow: 0 1px 2px rgb(15 23 42 / 8%), 0 2px 8px rgb(15 23 42 / 6%);
}

/* --- Light theme (default) ------------------------------------------------ */
[data-theme="light"] {
    --bg: #f1f5f9;
    --bg-raised: #ffffff;
    --bg-sunken: #e2e8f0;
    --bg-hover: #f8fafc;
    --text: #0f172a;
    --text-muted: #52627a;
    --border: #cbd5e1;
    --border-strong: #94a3b8;

    --accent: #4338ca;
    --accent-text: #ffffff;
    --accent-hover: #3730a3;
    --accent-soft: #e0e7ff;

    --ok: #15803d;
    --ok-soft: #dcfce7;
    /* Amber-800 rather than amber-700: on the soft amber background the lighter
       shade measures 4.42:1, just under the 4.5:1 needed for normal text. */
    --warn: #854d0e;
    --warn-soft: #fef3c7;
    --danger: #b91c1c;
    --danger-soft: #fee2e2;
    --info: #0369a1;
    --info-soft: #e0f2fe;

    --focus: #4338ca;

    /* What a modal lays over the page behind it. A token rather than a fixed
       colour because a scrim is one of the few things that cannot be the same
       in both themes: its whole job is to sit *darker* than the page, and
       slate-900 over a dark page does not. */
    --scrim: rgb(15 23 42 / 55%);

    color-scheme: light;
}

/* --- Dark theme ----------------------------------------------------------- */
[data-theme="dark"] {
    --bg: #0b1120;
    --bg-raised: #172033;
    --bg-sunken: #0f172a;
    --bg-hover: #1e293b;
    --text: #f1f5f9;
    --text-muted: #a3b1c6;
    --border: #33415c;
    --border-strong: #64748b;

    --accent: #a5b4fc;
    --accent-text: #10132e;
    --accent-hover: #c7d2fe;
    --accent-soft: #312e81;

    --ok: #4ade80;
    --ok-soft: #14532d;
    --warn: #fbbf24;
    --warn-soft: #52350a;
    --danger: #f87171;
    --danger-soft: #601616;
    /* Sky-300 rather than sky-400: on the deep soft-blue background the darker
       shade measures 4.42:1, just under 4.5:1 for normal text. */
    --info: #7dd3fc;
    --info-soft: #0c4a6e;

    --focus: #c7d2fe;

    /* Nearly black, and more of it. The light theme's slate-900 is *lighter*
       than this theme's own page background, so reused here it would dim
       nothing and wash the page slightly blue instead. */
    --scrim: rgb(2 6 16 / 70%);

    color-scheme: dark;
}

/* --- Reset ---------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

/* The browser's own `[hidden] { display: none }` lives in the user-agent
   stylesheet, so any author rule setting `display` beats it — and `.btn` sets
   `display: inline-flex`. This is the one place `!important` is the right tool:
   `hidden` means hidden, and no component should have to opt in. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    font-family: var(--font-sans);
    font-size: 17px;
    line-height: 1.55;
    background: var(--bg);
    color: var(--text);
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 { line-height: 1.25; margin: 0 0 var(--space-3); }
h1 { font-size: 1.6rem; letter-spacing: -0.01em; }
h2 { font-size: 1.25rem; }
h3 { font-size: 1.05rem; }
p  { margin: 0 0 var(--space-3); }

a { color: var(--accent); }
a:hover { color: var(--accent-hover); }

:focus-visible {
    outline: 3px solid var(--focus);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

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

.skip-link {
    position: absolute; left: -9999px; top: 0; z-index: 100;
    background: var(--accent); color: var(--accent-text);
    padding: var(--space-3) var(--space-4); border-radius: 0 0 var(--radius) 0;
}
.skip-link:focus { left: 0; }

.container {
    width: 100%;
    max-width: 1200px;
    margin-inline: auto;
    padding-inline: var(--space-4);
}

/* The document-facing screens — the three queues, the document list and record,
   the dashboard — opt into this.
   1200px is a measure for reading prose, and none of those screens is prose:
   they are a scan beside a form, or a table of six columns that has to stay
   readable without wrapping every cell onto three lines. 1760px is about as
   wide as a line of table text stays scannable, and it leaves a margin on a
   1920 monitor rather than running edge to edge. Forms inside keep their own
   `max-width`, so nothing here stretches a column of text boxes. */
.container-wide { max-width: 1760px; }

.muted { color: var(--text-muted); }
.mono { font-family: var(--font-mono); font-size: 0.86em; }
.nowrap { white-space: nowrap; }
.break { word-break: break-word; }

/* A multi-line value shown as written — request headers, a JSON body — scrolling
   rather than stretching the row when it is long. */
.pre-wrap { display: block; white-space: pre-wrap; max-height: 24rem; overflow: auto; }
.text-danger { color: var(--danger); }

/* --- Header and navigation ------------------------------------------------ */
.site-header {
    background: var(--bg-raised);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 40;
}

.header-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    min-height: 60px;
}

.brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-weight: 700;
    color: var(--text);
    padding-block: var(--space-2);
}

/* The logo is the link home; the wordmark next to it is not. */
.brand-home {
    display: inline-flex;
    align-items: center;
    border-radius: var(--radius-sm);
    color: inherit;
    text-decoration: none;
}
.brand-home:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The IG monogram, shown until a logo is uploaded. */
.brand-mark {
    display: grid;
    place-items: center;
    width: 34px; height: 34px;
    border-radius: 9px;
    background: var(--accent);
    color: var(--accent-text);
    font-size: 0.8rem;
    font-weight: 800;
    letter-spacing: 0.03em;
}

.brand-name { font-size: 1.02rem; }

.nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--tap); height: var(--tap);
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
}

.nav-toggle-bars { display: grid; gap: 4px; }
.nav-toggle-bars span {
    display: block; width: 20px; height: 2px;
    background: currentColor; border-radius: 2px;
}

.primary-nav {
    display: none;
    position: absolute;
    left: 0; right: 0;
    top: 100%;
    background: var(--bg-raised);
    border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow);
    padding: var(--space-3) var(--space-4) var(--space-4);

    /* The open menu hangs off a sticky header, so it does not scroll away with
       the page. Without a height of its own, anything past the bottom of the
       screen is unreachable. --header-h is measured and written by app.js when
       the menu opens; 60px is the header's min-height and the fallback. dvh so
       a phone's collapsing address bar does not hide the last item. */
    max-height: calc(100vh - var(--header-h, 60px));
    max-height: calc(100dvh - var(--header-h, 60px));
    overflow-y: auto;

    /* Scrolling to the end of the menu must not then scroll the page behind. */
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
}

.primary-nav.is-open { display: block; }

.nav-list {
    list-style: none;
    margin: 0 0 var(--space-3);
    padding: 0;
    display: grid;
    gap: var(--space-1);
}

.nav-link {
    display: flex;
    align-items: center;
    min-height: var(--tap);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    color: var(--text);
    text-decoration: none;
    font-weight: 600;
}

.nav-link:hover { background: var(--bg-hover); color: var(--text); }

.nav-link.is-active {
    background: var(--accent-soft);
    color: var(--text);
    box-shadow: inset 3px 0 0 var(--accent);
}

/* A destination a later stage builds. Shown so the shape of the application is
   visible, but not a link: a menu item that 404s is worse than one that says
   plainly it is not ready. */
.nav-link.is-pending {
    color: var(--text-muted);
    cursor: default;
    gap: var(--space-2);
    font-weight: 500;
}
.nav-link.is-pending:hover { background: transparent; }

.nav-divider {
    height: 1px;
    background: var(--border);
    margin: var(--space-2) 0;
}

.nav-account {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding-top: var(--space-3);
    border-top: 1px solid var(--border);
}

.nav-user {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex: 1 1 auto;
    padding: var(--space-2) var(--space-3);
}
/* It is a link now — to your own password — so it needs to say so on hover
   without turning the name blue and underlined in the middle of the bar. */
a.nav-user { color: inherit; text-decoration: none; border-radius: var(--radius-sm); }
a.nav-user:hover { background: var(--bg-hover); }
a.nav-user:hover .nav-user-name { text-decoration: underline; }

.nav-user-text { display: grid; line-height: 1.2; }
.nav-user-name { font-weight: 600; }
.nav-user-role { font-size: 0.82rem; color: var(--text-muted); font-weight: 500; }

.avatar {
    display: grid; place-items: center;
    width: 34px; height: 34px;
    border-radius: 50%;
    background: var(--bg-sunken);
    color: var(--text);
    font-weight: 700;
    flex: 0 0 auto;
}

.nav-logout { margin: 0; }

/* --- Navigation groups ---------------------------------------------------- */
/* A <details> element: an accordion in the drawer, a drop-down on the bar. One
   structure, and it works with JavaScript switched off. */
.nav-group summary { list-style: none; cursor: pointer; }
.nav-group summary::-webkit-details-marker { display: none; }

.nav-group-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    width: 100%;
}

/* The disclosure triangle, wherever a <details> is used as one, so the shape
   means the same thing everywhere. */
.caret {
    width: 0; height: 0;
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-top: 5px solid currentColor;
    flex: 0 0 auto;
    transition: transform 120ms ease;
}
details[open] > summary .caret { transform: rotate(180deg); }

.nav-sublist {
    list-style: none;
    margin: var(--space-1) 0 var(--space-2);
    padding: 0 0 0 var(--space-4);
    display: grid;
    gap: 2px;
    border-left: 2px solid var(--border);
}
.nav-sublink { font-weight: 500; }

/* Desktop navigation ------------------------------------------------------- */
/* The horizontal bar appears at 1150px. It was 900px when there were three
   top-level items and roughly 700px of content; the duplicate queue made a
   sixth, and six items plus the account block and the brand run to about
   1100px, which is a wrapped two-row header at 900. Below the breakpoint the
   drawer is used instead: a vertical list cannot wrap, and it is the same
   control phones already use. */
@media (min-width: 1150px) {
    .nav-toggle { display: none; }

    /* Block padding is a mobile tap-target concern; on a desktop the row's own
       min-height and alignment do that job. */
    .brand { padding-block: 0; }

    .primary-nav {
        display: flex;
        position: static;
        align-items: center;
        justify-content: space-between;
        flex: 1 1 auto;
        gap: var(--space-4);
        background: none;
        border: 0;
        box-shadow: none;
        padding: 0;

        /* The drawer's own scroll, undone: here the nav is part of the header
           row, so a max-height would clip the drop-downs. */
        max-height: none;
        overflow: visible;
    }

    .nav-list {
        display: flex;
        align-items: center;
        gap: var(--space-1);
        margin: 0;
        /* Never wrap: a menu that reflows onto a second row doubles the header
           height and pushes the page down. */
        flex-wrap: nowrap;
    }

    .nav-item { flex: 0 0 auto; }

    .site-header .btn,
    .nav-account .btn,
    .brand-name { white-space: nowrap; }

    .nav-account { flex: 0 0 auto; border: 0; padding-top: 0; flex-wrap: nowrap; }

    .nav-link { min-height: 40px; padding-inline: var(--space-2); white-space: nowrap; }

    /* The menu text stands on the foot of the logo.
       Everything in the bar bottom-aligns against a shared floor, and the brand
       and the menu text are each lifted off it by the same variable, so they
       cannot drift apart.

       `align-items: baseline` is the obvious thing to reach for and is a trap:
       a menu item is display:flex, and a flex container's baseline is
       synthesised from its first child — which differs between a plain link and
       a group's <summary>. `line-height: 1` collapses the line box onto the
       text, after which one shared floor value lines them up. */
    .header-inner { align-items: flex-end; --bar-floor: 14px; }
    .header-actions { align-self: center; }
    .primary-nav { align-items: flex-end; }
    .nav-account { align-self: center; }
    .nav-list { align-items: flex-end; }

    .brand { padding-bottom: var(--bar-floor); }

    .nav-list > .nav-item > .nav-link,
    .nav-list > .nav-item > .nav-group > .nav-group-toggle {
        align-items: flex-end;
        line-height: 1;
        padding-bottom: var(--bar-floor);
    }

    /* The caret is a shape, not a letter: it belongs against the middle of the
       text rather than on the line its feet stand on. */
    .nav-list > .nav-item > .nav-group > .nav-group-toggle .caret { align-self: center; }

    /* The underline sits at the foot of the bar rather than under the text, so
       it reads as an underline for the whole row instead of a strike through
       the descenders. */
    .nav-link.is-active {
        box-shadow: inset 0 -3px 0 var(--accent);
        background: transparent;
    }

    .nav-item-group { position: relative; }

    /* The panel floats over the page rather than pushing the bar taller. */
    .nav-group > .nav-sublist {
        position: absolute;
        top: calc(100% + 4px);
        left: 0;
        z-index: 50;
        min-width: 15rem;
        margin: 0;
        padding: var(--space-2);
        border: 1px solid var(--border);
        border-radius: var(--radius);
        background: var(--bg-raised);
        box-shadow: var(--shadow);
    }

    .nav-group-toggle { width: auto; }

    /* A group is rendered `open` when the current page is inside it, so the
       menu shows where you are. On a phone that expands an accordion, which is
       the point. Here the panel floats, so an auto-opened one would land on top
       of the page just navigated to. :focus-within rather than a class, so this
       still works with JavaScript off. */
    .nav-group[data-nav-autoopen]:not(:focus-within) > .nav-sublist { display: none; }

    /* On a desktop the bar shows the avatar alone; the name is in the footer
       and on the account screen. */
    .nav-user-text { display: none; }
    .nav-user { padding: 0; flex: 0 0 auto; }
}

/* --- Main ----------------------------------------------------------------- */
main.container { padding-block: var(--space-5) var(--space-6); }

.page-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-5);
}
.page-head h1 { margin-bottom: var(--space-1); }
.page-head p { margin: 0; }

.section-title {
    margin: var(--space-6) 0 var(--space-3);
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}

/* --- Cards ---------------------------------------------------------------- */
.card {
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-4);
    margin-bottom: var(--space-4);
    box-shadow: var(--shadow);
}

.card-grid {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}

/* A card that is itself the link — the synced-data index, one per record
   type. `.card` alone is not a link target's shape: block-level, no
   underline, and a hover state to say it is clickable at all. */
.card-link {
    display: block;
    color: inherit;
    text-decoration: none;
    transition: border-color 0.15s ease;
}
.card-link:hover, .card-link:focus-visible {
    border-color: var(--accent);
}
.card-link h3 { margin-top: 0; }

.notice-card { border-left: 4px solid var(--accent); }

/* Status cards: a coloured edge rather than a coloured panel, so the text
   inside keeps the page's normal contrast. */
.card-ok     { border-left: 4px solid var(--ok); }
.card-warn   { border-left: 4px solid var(--warn); }
.card-danger { border-left: 4px solid var(--danger); }

/* Present but demoted: the duplicate screen shows a candidate that no longer
   clears the bar, because hiding it would leave the page saying "possible
   duplicate" with nothing visible to be a duplicate of. Sunken rather than
   faded — text a person may still have to read must not lose its contrast. */
.card-muted {
    background: var(--bg-sunken);
    box-shadow: none;
}

.plain-list { margin: 0; padding-left: var(--space-4); }
.plain-list li + li { margin-top: var(--space-1); }

/* --- Sub-navigation ------------------------------------------------------- */
.subnav {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    margin-bottom: var(--space-4);
    border-bottom: 1px solid var(--border);
}

.subnav-link {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
    padding: var(--space-2) var(--space-3);
    color: var(--text-muted);
    text-decoration: none;
    font-weight: 600;
    border-bottom: 3px solid transparent;
    margin-bottom: -1px;
}
.subnav-link:hover { color: var(--text); }
.subnav-link.is-active { color: var(--text); border-bottom-color: var(--accent); }

/* --- Stats ----------------------------------------------------------------
   Compact by intent. These are a glanceable summary above the day's work, not
   the point of the page.

   auto-fit rather than auto-fill: a row with fewer cards than it has room for
   should stretch across the width rather than leave a gap at the right. */
.stat-grid {
    display: grid;
    gap: var(--space-2);
    grid-template-columns: repeat(auto-fit, minmax(145px, 1fr));
    margin-bottom: var(--space-4);
}

.stat-card {
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-left: 4px solid var(--border-strong);
    border-radius: var(--radius);
    padding: var(--space-3);
    display: grid;
    gap: 2px;
    align-content: start;
}

.stat-value { font-size: 1.5rem; font-weight: 700; line-height: 1.1; }
.stat-label { font-size: 0.85rem; line-height: 1.35; color: var(--text-muted); }

.stat-card.stat-danger { border-left-color: var(--danger); }
.stat-card.stat-danger .stat-value { color: var(--danger); }
.stat-card.stat-warn { border-left-color: var(--warn); }
.stat-card.stat-warn .stat-value { color: var(--warn); }
.stat-card.stat-info { border-left-color: var(--info); }
.stat-card.stat-ok { border-left-color: var(--ok); }

/* --- Tables --------------------------------------------------------------- */
.table-wrap {
    overflow-x: auto;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    margin-bottom: var(--space-4);
    -webkit-overflow-scrolling: touch;
}

.table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.96rem;
}

.table th, .table td {
    padding: var(--space-3);
    text-align: left;
    border-bottom: 1px solid var(--border);
    vertical-align: top;
}

.table thead th {
    background: var(--bg-sunken);
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    position: sticky;
    top: 0;
}

.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr:hover { background: var(--bg-hover); }
.table-compact th, .table-compact td { padding: var(--space-2) var(--space-3); }

/* Room to breathe, once there is room to give.
   The tight padding above is a phone measurement: on a 1920 monitor the same
   table reads as a wall, and the rows a reviewer is scanning for run together.
   Nothing changes below 1150px — the same breakpoint the navigation bar uses,
   so a screen has either the desktop treatment throughout or none of it. */
@media (min-width: 1150px) {
    .table { font-size: 1rem; }
    .table th, .table td { padding: var(--space-3) var(--space-4); }
    .table-compact th, .table-compact td { padding: var(--space-2) var(--space-3); }

    /* A queue is read down its rows rather than across them, so the row
       separators earn a little more space than the columns do. */
    .table tbody th, .table tbody td { line-height: 1.45; }
}

/*
 * Column widths, stated rather than left to the browser.
 *
 * `table-layout: auto` gives the widest column whatever is left, which on the
 * queues means the "what is outstanding" sentence takes two thirds of the page
 * and the supplier — the thing being scanned for — is squeezed into three
 * wrapped lines. These say what each column is *for* instead, and are applied
 * per screen rather than globally, because a width that suits a queue is wrong
 * for the pipeline log.
 *
 * They are `min-width` on the cell rather than `width` on a <col>: a table in
 * `.table-wrap` can scroll sideways, so a minimum is honoured and a percentage
 * is negotiated away.
 */
.col-tight   { width: 1%; white-space: nowrap; }
.col-narrow  { min-width: 7rem; }
.col-date    { min-width: 8rem; white-space: nowrap; }
.col-name    { min-width: 16rem; }
.col-wide    { min-width: 22rem; }

/* The one exception to "no fixed widths": a description column is the reason
   the table is wide at all, and it should take what is left over. */
.col-grow { width: 100%; }

/* Money reads as a column of figures, so it lines up on the right and uses
   tabular digits — a column of amounts whose decimal points wander is a column
   nobody can add up by eye. */
.table .amount {
    text-align: right;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.row-muted td { opacity: 0.62; }
.cell-sub { font-size: 0.85rem; color: var(--text-muted); }
.empty { text-align: center; color: var(--text-muted); padding: var(--space-6) !important; }

/* The actions column: one or more buttons in the last cell of a row.
 *
 * The cell stays a table-cell. Anything that takes it out of table layout —
 * `display: flex` in particular — stops it stretching to the row height, and
 * since the row separator is a border-bottom on the cell, its line is then
 * drawn short of the others. Inline layout means `gap` does not apply, hence
 * the margin.
 */
td.actions { white-space: nowrap; vertical-align: middle; }
td.actions > * { vertical-align: middle; }
td.actions > * + * { margin-left: var(--space-1); }

.inline-form { display: inline; margin: 0; }

/* --- Stage indicator (Prompt 46) -------------------------------------------
   The same six-phase track on the document page and all three review queues,
   so "where is this" reads the same way everywhere. A line of dots joined by
   a rule; done phases and the connector before them turn ok-green, the
   current phase is coloured by what it means (accent while it is still
   in-flight, warn when it wants a person, ok once it is settled) and carries
   the actual status word rather than the phase's generic one, upcoming
   phases stay flat. Ignored/deleted/failed do not try to guess a position on
   the happy path — the whole track stays flat and the specific state is a
   badge beside it instead. */
.stage-track {
    display: flex;
    margin-bottom: var(--space-1);
}

.stage-step {
    flex: 1 1 0;
    position: relative;
    text-align: center;
    padding-top: 15px;
}

.stage-step:not(:first-child)::before {
    content: '';
    position: absolute;
    top: 6px;
    right: 50%;
    width: 100%;
    height: 2px;
    background: var(--border);
}

.stage-step.is-done:not(:first-child)::before { background: var(--ok); }

.stage-dot {
    display: block;
    position: absolute;
    top: 0;
    left: calc(50% - 6px);
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--bg-raised);
    border: 2px solid var(--border);
}

.stage-step.is-done .stage-dot { background: var(--ok); border-color: var(--ok); }

.stage-label {
    font-size: 0.72rem;
    color: var(--text-muted);
    line-height: 1.3;
}

.stage-step.is-current .stage-dot { border-width: 3px; left: calc(50% - 7px); width: 14px; height: 14px; }
.stage-step.is-current .stage-label { color: var(--text); font-weight: 700; }

.stage-step.is-current.stage-tone-accent .stage-dot { border-color: var(--accent); background: var(--accent-soft); }
.stage-step.is-current.stage-tone-warn .stage-dot { border-color: var(--warn); background: var(--warn-soft); }
.stage-step.is-current.stage-tone-ok .stage-dot { border-color: var(--ok); background: var(--ok-soft); }
.stage-step.is-current.stage-tone-danger .stage-dot { border-color: var(--danger); background: var(--danger-soft); }

.stage-terminal-badge { margin-top: var(--space-2); display: inline-block; }

@media (min-width: 640px) {
    .stage-label { font-size: 0.78rem; }
}

/* --- Badges and chips ----------------------------------------------------- */
.badge {
    display: inline-block;
    padding: 2px 10px;
    border-radius: 999px;
    font-size: 0.78rem;
    font-weight: 600;
    background: var(--bg-sunken);
    color: var(--text);
    white-space: nowrap;
}

.badge-ok { background: var(--ok-soft); color: var(--ok); }
.badge-warn { background: var(--warn-soft); color: var(--warn); }
.badge-danger { background: var(--danger-soft); color: var(--danger); }
.badge-info { background: var(--info-soft); color: var(--info); }
.badge-accent { background: var(--accent-soft); }
.badge-muted { background: var(--bg-sunken); color: var(--text-muted); }

.chip {
    background: var(--bg-sunken);
    border-radius: 999px;
    padding: 4px 12px;
    font-size: 0.82rem;
}

/* A numbered explanation — "what happens next" on the upload page. Ordinary
   list numbering, given room to breathe: the point is that it is read once,
   understood, and never looked at again. */
.steps {
    margin: 0 0 var(--space-3);
    padding-left: var(--space-5);
    display: grid;
    gap: var(--space-2);
    font-size: 0.92rem;
}

.steps li { padding-left: var(--space-1); }

.meta-list {

    list-style: none;
    margin: 0 0 var(--space-3);
    padding: 0;
    display: grid;
    gap: var(--space-1);
    font-size: 0.92rem;
}

/* --- Buttons -------------------------------------------------------------- */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: var(--tap);
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
    color: var(--text);
    font: inherit;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
    transition: background-color 0.12s ease, border-color 0.12s ease;
}

.btn:hover { background: var(--bg-hover); color: var(--text); }
.btn:active { transform: translateY(1px); }

.btn-primary {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-text);
}
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: var(--accent-text); }

.btn-warning { background: var(--warn-soft); border-color: var(--warn); color: var(--warn); }
.btn-danger  { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); }

.btn-ghost { background: transparent; border-color: transparent; }
.btn-ghost:hover { background: var(--bg-hover); }

.btn-sm { min-height: 38px; padding: var(--space-1) var(--space-3); font-size: 0.9rem; }
.btn-lg { min-height: 52px; font-size: 1.05rem; padding-inline: var(--space-5); }
.btn-block { width: 100%; }
.btn-icon .btn-label { display: none; }

/* In the drawer there is room for the label; on the bar there is not. */
@media (max-width: 1149px) {
    .btn-icon .btn-label { display: inline; }
}

/* --- Forms ---------------------------------------------------------------- */
.form { max-width: 720px; }
.form.card { max-width: 720px; }

/* A form that is a whole pane rather than a column of boxes on a page. The
   review screen's is one: it holds a six-column table, and capping it at 720px
   would waste the width the screen was widened to have. */
.form-full { max-width: none; }

.field { margin-bottom: var(--space-4); }

.field-row {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: 1fr;
}
@media (min-width: 640px) {
    .field-row { grid-template-columns: 1fr 1fr; }
}

.label { display: block; font-weight: 600; margin-bottom: var(--space-1); }

.input {
    display: block;
    width: 100%;
    min-height: var(--tap);
    padding: var(--space-2) var(--space-3);
    font: inherit;
    font-size: 1rem;                 /* >=16px stops iOS zooming on focus */
    color: var(--text);
    background: var(--bg-raised);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
}

.input:focus { border-color: var(--accent); }
.input.has-error { border-color: var(--danger); background: var(--danger-soft); }

select.input { appearance: auto; }
textarea.input { min-height: 120px; resize: vertical; }

.field-hint { margin: var(--space-1) 0 0; font-size: 0.86rem; color: var(--text-muted); }
.field-error { margin: var(--space-1) 0 0; font-size: 0.88rem; font-weight: 600; color: var(--danger); }

.input-with-button { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: stretch; }
.input-with-button .input { flex: 1 1 12rem; min-width: 0; }

/* A short answer — a number of minutes, a code — should not get a box the
   width of the card. The pair still wraps on a narrow screen. */
.input-with-button.compact { max-width: 24rem; }
.btn-inline { white-space: nowrap; border: 1px solid var(--border); }

.checkbox {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    min-height: var(--tap);
    padding: var(--space-2);
    border-radius: var(--radius-sm);
    cursor: pointer;
}
.checkbox:hover { background: var(--bg-hover); }
.checkbox input[type="checkbox"] {
    width: 22px; height: 22px;
    margin-top: 2px;
    accent-color: var(--accent);
    flex: 0 0 auto;
}

.fieldset {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--space-4);
    margin: 0 0 var(--space-4);
}
.fieldset legend { padding-inline: var(--space-2); font-weight: 700; }

.form-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    align-items: center;
}

/* A form inside .form-actions is a wrapper around one button (each POST needs
   its own form and CSRF token), so it must not add layout of its own. */
.form-actions > form { margin: 0; }

/*
 * Filter bars come in two shapes: unlabelled boxes and ones with visible labels
 * above each control. Both have to put every control on the same bottom edge,
 * which is why this aligns to flex-end rather than centring — centring lines up
 * the middles of columns of different heights, so a submit button with no label
 * above it floats above the boxes beside it.
 */
.filter-bar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    align-items: flex-end;
    margin-bottom: var(--space-4);
}
.filter-bar .field { margin-bottom: 0; flex: 1 1 180px; }
.filter-bar .btn { flex: 0 0 auto; }

/* Six filters rather than two, so the free-text box gets its own line on a
   narrow screen and the hints under the boxes do not shove the row apart. */
.filter-bar .field-wide { flex: 1 1 100%; }
.filter-bar .field-hint { margin-bottom: 0; }
.filter-bar .filter-actions {
    display: flex;
    gap: var(--space-2);
    flex: 0 0 auto;
    align-self: flex-start;
    padding-top: 1.9rem; /* clears the label row beside it */
}

/* --- Flash messages ------------------------------------------------------- */
.flash-stack { display: grid; gap: var(--space-2); margin-bottom: var(--space-4); }

.flash {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-sm);
    border-left: 4px solid var(--border-strong);
    background: var(--bg-raised);
    font-weight: 500;
    transition: opacity 220ms ease, transform 220ms ease;
}

.flash.is-leaving { opacity: 0; transform: translateY(-4px); }

.flash-success { background: var(--ok-soft); border-left-color: var(--ok); color: var(--ok); }
.flash-error   { background: var(--danger-soft); border-left-color: var(--danger); color: var(--danger); }
.flash-warning { background: var(--warn-soft); border-left-color: var(--warn); color: var(--warn); }
.flash-info    { background: var(--info-soft); border-left-color: var(--info); color: var(--info); }

.flash-close {
    background: none; border: 0; cursor: pointer;
    font-size: 1.4rem; line-height: 1; color: inherit; opacity: 0.7;
    min-width: 32px; min-height: 32px;
}
.flash-close:hover { opacity: 1; }

/* --- Auth screens --------------------------------------------------------- */
.body-centred {
    min-height: 100vh;
    min-height: 100dvh;
    display: grid;
    place-items: center;
    background: var(--bg);
    padding: var(--space-4);
}

.auth-shell { width: 100%; max-width: 430px; display: grid; gap: var(--space-4); justify-items: center; }

.auth-card {
    width: 100%;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-5);
    box-shadow: var(--shadow);
}

.auth-brand {
    display: flex; align-items: center; gap: var(--space-2);
    margin-bottom: var(--space-5);
    font-weight: 700; font-size: 1.1rem;
}

.auth-title { margin-bottom: var(--space-1); }
.auth-subtitle { color: var(--text-muted); margin-bottom: var(--space-5); font-size: 0.95rem; }
.auth-help { margin: var(--space-4) 0 0; font-size: 0.88rem; text-align: center; }

.theme-toggle-standalone { border: 1px solid var(--border); background: var(--bg-raised); }

/* A moon carved out of a disc: filled in dark, crescent in light. */
.theme-icon {
    width: 18px; height: 18px;
    border-radius: 50%;
    background: currentColor;
    box-shadow: inset -6px -3px 0 0 var(--bg-raised);
}
[data-theme="dark"] .theme-icon { box-shadow: none; }

/* --- Error pages ---------------------------------------------------------- */
.error-page { text-align: center; padding-block: var(--space-6); }
.error-status { font-size: 3rem; font-weight: 800; color: var(--text-muted); margin: 0; line-height: 1; }

/* --- Footer --------------------------------------------------------------- */
.site-footer {
    border-top: 1px solid var(--border);
    background: var(--bg-raised);
    padding-block: var(--space-4);
    font-size: 0.85rem;
}
.footer-inner { display: flex; flex-wrap: wrap; gap: var(--space-2); justify-content: space-between; }
.footer-brand { display: grid; gap: 2px; }
.footer-brand a { color: inherit; }

/* --- Branding -------------------------------------------------------------
   The logo is constrained by height and never by width, so it keeps its
   proportions whatever shape it is; max-width stops a very wide wordmark from
   pushing the navigation off the bar. object-fit: contain letterboxes rather
   than stretches, which is the whole point. */

.brand-logo-wrap { display: inline-flex; align-items: flex-end; }

/* Logo and wordmark side by side on the same baseline — the text aligns with
   the bottom of the logo rather than its middle, which is what makes the pair
   read as one lockup instead of two loose objects. */
.brand-stack {
    display: flex;
    align-items: flex-end;
    gap: var(--space-2);
    min-width: 0;
}

.brand-logo {
    display: block;
    height: 34px;
    width: auto;
    max-width: 190px;
    object-fit: contain;
}

/* The wordmark's own line box carries space under the baseline; trimming it is
   what lines the text bottom up with the foot of the logo. */
.brand-stack .brand-name { line-height: 1; padding-bottom: 2px; }

/* Both variants ship in the markup and CSS picks: the theme can change without
   a page load, so the server cannot know which one to send. Where only one was
   uploaded it is used for both — one logo is better than none, and it is then
   the only .brand-logo present, so nothing hides it. */
[data-theme="light"] .brand-logo-dark,
[data-theme="dark"]  .brand-logo-light { display: none; }

@media (min-width: 1150px) {
    /* 176px, not 128.
       A wordmark logo is commonly 4:1 or wider, and at 36px tall that wants
       144px. The old 128px cap did not distort it — `object-fit: contain`
       letterboxes rather than squashes — but it silently drew a 4:1 logo at 32px
       instead of 36 and left a strip of empty box between it and the wordmark.
       176px clears everything up to about 4.8:1 at full height, and the wordmark
       beside it is separately capped, so the bar still cannot reach two rows. */
    .brand-stack-logo .brand-logo { height: 36px; max-width: 176px; }

    /* Item for item with the menu: same size, same weight, standing on the same
       line. max-width is the safety net — APP_NAME is whatever the operator
       typed, and an unbounded wordmark is the one thing in this bar that could
       put it back on two rows. */
    .brand-stack .brand-name {
        font-size: inherit;
        font-weight: 600;
        line-height: 1;
        padding-bottom: 0;
        max-width: 12rem;
        overflow: hidden;
        text-overflow: ellipsis;
    }
}

/* --- Two blocks side by side ----------------------------------------------
   For a pair of sections where one is narrow by nature and the other is not —
   the dashboard's stage tally beside its recent-activity list. Stacked below
   the desktop breakpoint, and the first column takes a third, because a table
   of "stage, count" does not get better with more room and the one beside it
   does. `minmax(0, …)` on both, so a wide cell inside cannot push the grid
   past the page. */
.wide-split { display: grid; gap: var(--space-4); }
.wide-split > * { min-width: 0; }
.wide-split .section-title:first-child { margin-top: 0; }

@media (min-width: 1150px) {
    .wide-split { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); align-items: start; }
}

/* --- Spacing between blocks that are not cards ----------------------------
   .card carries only a bottom margin, so a card that follows something else
   sits flush against it. */
.card + .section-title { margin-top: var(--space-5); }

/* --- The document / data split --------------------------------------------
   The scan on one side, what was read off it on the other. Comparing the two is
   the entire job of the review screen this grows into, so they go side by side
   wherever there is room and stack where there is not — scrolling between a
   figure and the number it should match is how transposition errors get missed.

   The breakpoint is higher than the navigation's 900px: two useful columns need
   more width than a menu does, and at 1000px each pane would be too narrow to
   read a line item in. */
.doc-split {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: 1fr;
    margin-bottom: var(--space-4);
}

@media (min-width: 1100px) {
    .doc-split { grid-template-columns: 1fr 1fr; align-items: start; }

    /* Each pane scrolls against the viewport rather than the page, so the
       transcription can be read past the end of the scan beside it without the
       other pane sliding away. */
    .doc-pane {
        position: sticky;
        top: calc(var(--header-h, 60px) + var(--space-4));
        max-height: calc(100dvh - var(--header-h, 60px) - var(--space-6));
        overflow-y: auto;
        overscroll-behavior: contain;
    }
}

/*
 * The review screen's own proportions, once there is width to divide.
 *
 * An even split is right while both sides are documents. On the review screen
 * they are not: the left is one page of A4, which stops being easier to read
 * somewhere around 700px, and the right is a form with a six-column line table
 * in it, which does not — so the data pane gets the larger share from the same
 * 1100px breakpoint the two-column layout itself starts at, not held back to a
 * separate, higher one. Prompt 21 moved this down from "past 1500px" for
 * exactly that reason: most desktop monitors never reached it.
 */
@media (min-width: 1100px) {
    .doc-split-review { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); }
}

/* The scan pane does not scroll; the image inside it does.
   Without this the pane and the image are two nested scrolling boxes, and a
   wheel gesture over the scan moves whichever one the browser feels like. */
@media (min-width: 1100px) {
    .doc-pane-scan {
        display: flex;
        flex-direction: column;
        overflow: hidden;
    }

    .doc-pane-scan > .scan { flex: 1 1 auto; min-height: 0; }
}

/*
 * The review screen's panes size to their own content instead of scrolling
 * inside a fixed-height box — Prompt 21. `.doc-pane`'s sticky/scroll pairing
 * above suits a screen and reference document that are both short; the review
 * form is not, and clipping it into `100dvh` turned a long document into a
 * card with its own internal scrollbar fighting the page's. Scoped to this
 * class rather than changed on `.doc-pane` itself, because the document record
 * and the Existing Invoice screen's shorter panes still want the sticky
 * compare-while-scrolling behaviour.
 */
@media (min-width: 1100px) {
    .doc-split-flow .doc-pane {
        position: static;
        max-height: none;
        overflow-y: visible;
    }

    /*
     * The scan pane is the one exception — Prompt 28. Everything else about
     * §37's flow layout still applies (the form is not clipped to a fixed
     * height and scrolls with the page), but a reviewer checking a value
     * against the page should not have to scroll the much longer form back
     * up to see it again. `align-items: start` on `.doc-split` (above) keeps
     * this pane's grid cell the height of the *form* beside it, so the
     * ordinary CSS grid sticky-sidebar behaviour applies: it sticks at `top`
     * while the form scrolls past, and releases on its own once the form's
     * content — and so the shared grid row — runs out.
     */
    .doc-split-flow .doc-pane-scan {
        position: sticky;
        top: calc(var(--header-h, 60px) + var(--space-4));
        overflow: visible;
    }
}

.doc-pane {
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-4);
    min-width: 0;
}

.doc-pane h3 { margin-top: 0; }
.doc-pane h3 + h3 { margin-top: var(--space-5); }

/* The embedded PDF. Only ever reached now through the scan viewer's "View PDF"
   button — every screen that used to embed it shows the rendered page images
   instead, and this is what opens underneath them. `.scan-pdf` sets the
   height; this is everything else about the box.

   The thumbnail strip that used to live here went with the change: the viewer
   has its own, and two page pickers that looked different and behaved
   differently was one too many. */
.pdf-frame {
    display: block;
    width: 100%;
    height: 60vh;
    min-height: 380px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
}

/* The transcription, keeping the document's own line breaks and column spacing
   — which is what makes a line-item table still read as a table. `.json-view`
   is the same box for the synced-data viewer's raw-record dump: a scrollable,
   monospaced pane is exactly what a pretty-printed JSON blob wants too. */
.ocr-text, .json-view {
    margin: 0;
    padding: var(--space-3);
    max-height: 60vh;
    overflow: auto;
    background: var(--bg-sunken);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: 0.82rem;
    line-height: 1.5;
    white-space: pre-wrap;
    word-break: break-word;
    tab-size: 4;
}

/* §55: the transcription as an editable box.

   Monospaced and tab-sized to match `.ocr-text` above, because it is the same
   text — a correction typed in a proportional font against a transcription
   read in a monospaced one is a correction made against a different-looking
   document. `resize: vertical` because a thirty-page transcription needs more
   than twenty rows and a page that will not let somebody see what they are
   editing is a page they will edit wrongly. */
.ocr-editor {
    font-family: var(--font-mono);
    font-size: 0.82rem;
    line-height: 1.5;
    tab-size: 4;
    resize: vertical;
    min-height: 12rem;
}

.annotation-summary {
    padding: var(--space-3);
    margin-bottom: var(--space-3);
    border-left: 4px solid var(--accent);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
    font-size: 0.94rem;
}
.annotation-summary p:last-child { margin-bottom: 0; }
.annotation-summary .plain-list { margin-bottom: var(--space-3); }

/* --- Extracted fields, shaped like the form they will become ---------------
   Read-only for now: editing arrives in a later build. Laid out as fields with
   labels rather than as a table of values, so turning it into a form is
   swapping each value for an input — the labels, grouping and order are already
   the ones a person will edit in. */
.field-readonly { margin-bottom: var(--space-3); }
.field-readonly .label { margin-bottom: 0; font-size: 0.82rem; color: var(--text-muted); font-weight: 600; }

.field-value {
    display: block;
    min-height: 1.6em;
    padding: 2px 0;
    font-size: 1.02rem;
    word-break: break-word;
}

/* An absent value is stated rather than left blank: a gap reads as a rendering
   fault, and "not found" is frequently the correct answer here. */
.field-value.is-empty { color: var(--text-muted); font-style: italic; font-size: 0.94rem; }

.review-notes li { margin-bottom: var(--space-1); }

/* A line description can carry several lines of its own — product code, full
   description, spec — and squeezing that into one row loses what the prompt
   went out of its way to keep. */
.line-description { min-width: 18rem; white-space: pre-line; }

/* --- The scan viewer -------------------------------------------------------
   What a reviewer looks at, and by default it is **the page image rather than
   the PDF**.

   The images already exist: every document is rendered to one PNG per page
   before anything is read, and those are the images the model itself was shown.
   Serving one is a same-origin `<img>` the browser paints immediately, where
   the `<object>` boots a whole PDF viewer with its own toolbar, its own
   scrolling and its own idea of zoom inside a box a third of the screen wide.
   The PDF is still one click away underneath, for the times the render is not
   enough — a signature to squint at, a second page to flick to quickly.

   Two sizes and no more: fit the width of the pane, or the image's own pixels
   with the box scrolling. A zoom slider is a control nobody uses twice; "make
   it big enough to read the handwriting" is the whole requirement.
   ========================================================================== */

.scan { display: flex; flex-direction: column; gap: var(--space-3); min-height: 0; }

.scan-stage {
    flex: 1 1 auto;
    min-height: 0;
    /* Enough to read an A4 page at a glance when the pane is not the flex
       parent — a stacked phone layout, or the duplicate screen's full-width
       card. The flex rule above wins wherever the pane sets a height. */
    height: 72vh;
    overflow: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    /* The strip and the page arrows are in-page anchors and scroll this box;
       the reduced-motion block at the end of this file turns it off again for
       anybody who has asked their system for less movement. */
    scroll-behavior: smooth;
    background: var(--bg-sunken);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}

/* A page scrolled to sits at the top of the box rather than jammed against its
   edge, and a multi-page document reads as separate sheets rather than a roll. */
.scan-page + .scan-page { margin-top: var(--space-3); }
.scan-page { scroll-margin-top: var(--space-2); }

.scan-page {
    display: block;
    width: 100%;
    /* A ceiling, for the screens where the viewer is a full-width card rather
       than half a split — the duplicate comparison, chiefly. A sheet of A4
       stretched across 1700px is not easier to read, it is a stretch of the
       neck; 52rem is about where a scanned page stops gaining from more width.
       Lifted by "actual size", which is the control for reading the small
       print. */
    max-width: 52rem;
    height: auto;
    margin-inline: auto;
    /* White whatever the theme: a scan of white paper on a dark ground reads
       as a hole in the page. */
    background: #ffffff;
}

/* Actual size: the image keeps its own pixels and the box scrolls both ways.
   `max-width: none` is the load-bearing half — the reset gives every image a
   width ceiling, and without lifting it "actual size" silently does nothing. */
.scan-stage.is-actual { display: block; }
.scan-stage.is-actual .scan-page { width: auto; max-width: none; margin-inline: 0; }

.scan-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    justify-content: space-between;
}

.scan-nav { display: flex; align-items: center; gap: var(--space-2); }
.scan-count { font-size: 0.88rem; color: var(--text-muted); white-space: nowrap; }

/* A page picker only worth showing past two pages; below that the arrows are
   quicker than aiming at a thumbnail. Ordinary in-page anchors, so it still
   works with no script — which is why these are links and not buttons. */
.scan-strip { display: flex; gap: var(--space-2); overflow-x: auto; padding-bottom: var(--space-1); }

.scan-strip a {
    flex: 0 0 auto;
    display: block;
    padding: 2px;
    background: var(--bg-sunken);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    line-height: 0;
}

.scan-strip a img { display: block; width: 54px; height: auto; background: #ffffff; }
.scan-strip a[aria-current="true"] { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }

/* The PDF, revealed under the image rather than replacing it — a reviewer who
   opened it to check one thing should not have to work out how to get the
   image back. */
.scan-pdf { display: grid; gap: var(--space-2); }
.scan-pdf .pdf-frame { height: 68vh; }

/* --- Per-field issues ------------------------------------------------------
   Every note, every unresolved match and every uncertain reading is drawn on
   the field it is about. The banner this replaces listed four sentences at the
   top of a screen with forty inputs below it, which left the reviewer to work
   out which four boxes were meant — the part of the job the machine can do.

   Two tones and no more: `danger` stands between this document and Clear Books,
   `warn` is worth a look and may well turn out to be right. Both are drawn the
   same way, with a colour variable swapped, so a field cannot be marked in a
   way the reader has to learn.
   ========================================================================== */

.is-flagged {
    --flag: var(--warn);
    --flag-soft: var(--warn-soft);
}

.is-flagged-danger {
    --flag: var(--danger);
    --flag-soft: var(--danger-soft);
}

/* The rule down the left edge is the thing seen at a glance; the colour on the
   input's own border is what confirms which control it means when two fields
   sit side by side in a `.field-row`. */
.field.is-flagged {
    padding-left: var(--space-3);
    border-left: 3px solid var(--flag);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}

.field.is-flagged > .input,
.field.is-flagged .input { border-color: var(--flag); }

.flag-label { display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }

/* The word, not just a colour: colour alone is not a signal somebody with a
   colour vision deficiency receives, and this is the whole point of the
   screen. */
.flag-tag {
    display: inline-block;
    padding: 1px 8px;
    border-radius: 999px;
    background: var(--flag-soft, var(--warn-soft));
    color: var(--flag, var(--warn));
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    white-space: nowrap;
}

/* The fallbacks matter: both of these are used in a couple of places that are
   not inside a `.is-flagged` — a heading for the line-item table, a chip in the
   index — and a `var()` with nothing behind it resolves to nothing at all. */
.flag-notes {
    list-style: none;
    margin: var(--space-1) 0 0;
    padding: 0;
    display: grid;
    gap: 2px;
    font-size: 0.86rem;
    line-height: 1.4;
    color: var(--flag, var(--warn));
}

.flag-notes li { display: flex; gap: var(--space-2); }

/* A drawn triangle rather than a typed "!". Generated content is read out by
   some screen readers, and "exclamation mark" before every note is noise on top
   of a message that already says what is wrong — while the word in `.flag-tag`
   beside the label carries the meaning for anybody who cannot see the colour. */
.flag-notes li::before {
    content: "";
    flex: 0 0 auto;
    width: 0;
    height: 0;
    margin-top: 0.32em;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-bottom: 7px solid currentColor;
}

/* A flagged cell in the line-item table. The rule goes on the cell rather than
   the input so a whole-row problem — a line total that does not follow from
   the quantity — has somewhere to be drawn. */
.table td.is-flagged {
    box-shadow: inset 3px 0 0 var(--flag);
    /* The flat colour first for anything without `color-mix`: a stronger tint
       than intended is still a correct signal, where no tint at all is not. */
    background: var(--flag-soft);
    background: color-mix(in srgb, var(--flag-soft) 45%, transparent);
}

.table td.is-flagged .input { border-color: var(--flag); }
.table td .flag-notes { font-size: 0.8rem; margin-top: var(--space-1); }

/* A flagged line-item slot (`.line-slot`, Prompt 52, now Prompt 56's grid cell)
   is marked by the rules beside `.line-grid` further down. */

/* The index at the top of the form: one link per flagged field, so the answer
   to "what needs a look" is a list rather than a scroll. */
.issue-index {
    padding: var(--space-3) var(--space-4);
    margin-bottom: var(--space-4);
    border: 1px solid var(--border);
    /* The edge takes the worst tone in the panel, so "something here stops this
       being submitted" is legible before a word of it is read. The chips inside
       set their own, so a red panel can still hold amber ones. */
    border-left: 4px solid var(--flag, var(--warn));
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
}

.issue-index.is-clear { --flag: var(--ok); }
.issue-index h3 { margin-bottom: var(--space-2); font-size: 0.98rem; }
.issue-index p:last-child { margin-bottom: 0; }

.issue-jumps { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0; padding: 0; list-style: none; }

.issue-jump {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: 3px 10px;
    border: 1px solid var(--flag);
    border-radius: 999px;
    background: var(--flag-soft);
    color: var(--flag);
    font-size: 0.84rem;
    font-weight: 600;
    text-decoration: none;
}

.issue-jump:hover { color: var(--flag); text-decoration: underline; }

/* A field jumped to should not land under the sticky header. */
.field, .table td { scroll-margin-top: calc(var(--header-h, 60px) + var(--space-4)); }

/* --- The review screen ---------------------------------------------------- */

/*
 * A denser field layout — Prompt 21, pairing with the desktop-first rework of
 * Prompt 19. This form holds around forty fields; the ordinary `.field`
 * spacing was chosen for a handful of them on a settings screen; scoped to
 * `.form-full` because that class is this form and nothing else.
 */
.form-full .field { margin-bottom: var(--space-2); }
.form-full .input { padding-block: var(--space-1); }
.form-full h3 { margin-bottom: var(--space-2); }
.form-full h3 + p.field-hint { margin-top: calc(var(--space-2) * -1 + var(--space-1)); }

/* `.field-row` is two columns by default; the review form uses it for three
   dates and four totals, which want to keep going rather than wrap in pairs. */
@media (min-width: 640px) {
    .field-row:has(> .field:nth-child(3)) { grid-template-columns: repeat(3, 1fr); }
    .field-row:has(> .field:nth-child(4)) { grid-template-columns: repeat(4, 1fr); }
}

/*
 * VAT treatment / Project / Currency — Prompt 56, which supersedes Prompt
 * 51's half-width Project and takes Currency off the Net/VAT/Gross row. Two
 * fifths, two fifths, one fifth.
 *
 * Project is hidden in Per line and Percentage split allocation (Prompt 28's
 * rule stands), and the columns must not follow it: the track stays, and
 * Currency is put in the third explicitly rather than left to auto-placement,
 * which would slide it into the vacated second column and stretch it. Below
 * 640px the row is a single column and stacks as any other.
 *
 * The selector is a class longer than the generic three-/four-field rules
 * above (`.field-row:has(> .field:nth-child(3))`) on purpose: those count
 * children, and this row has three.
 */
.form-full .field-row.field-row-vat-project { grid-template-columns: 1fr; }
@media (min-width: 640px) {
    .form-full .field-row.field-row-vat-project { grid-template-columns: 2fr 2fr 1fr; }
    .field-row-vat-project > .field-currency { grid-column: 3; }
}

/* The project-mode radios and "New project" on one line — Prompt 51 moved
   the button here from beside the (sometimes hidden) Project dropdown, so
   it is available whichever of the three allocation modes is chosen. */
.project-mode-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}

/*
 * The supplier picker — Prompt 28, moved into the Supplier field's own
 * position by Prompt 31 (`#field-supplier_name_raw`, formerly the standalone
 * `#resolve-supplier` card). `.input-with-button .input`'s general
 * `flex: 1 1 12rem` never actually reaches this field: `data-combo` (app.js)
 * wraps the real `<select>` in a `.combo` div and that div, not the `.input`
 * inside it, is the flex item `.input-with-button` lays out — so the combo
 * was sized by its placeholder text, which is narrower still. Widened here
 * on `.combo` itself, and scoped to this field rather than every
 * `.input-with-button` combo, since a short code elsewhere (a settings box)
 * still wants the narrower default.
 */
#field-supplier_name_raw .input-with-button .combo { flex: 1 1 32rem; min-width: 0; }

/*
 * Prompt 31: the supplier card, now rendered inside the Supplier field rather
 * than as its own page-level `.card` — `.card-inset` keeps the same coloured
 * left edge and spacing without the shadow and background a top-level card
 * gets, which read as a second surface floating inside a field that is
 * already indented for being flagged.
 */
.card-inset {
    margin-top: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-left: 4px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
}

.card-inset.card-ok   { border-left-color: var(--ok); }
.card-inset.card-warn { border-left-color: var(--warn); }
.card-inset h3 { margin-top: 0; }

/* Prompt 56: the dropdown is the supplier display, and what it says about how
   the match was made — "matched Clear Books 1234 — chosen by a person" — sits
   directly beneath it, small enough to read as a footnote to it. */
.supplier-pick { margin-bottom: 0; }
.supplier-note {
    margin: var(--space-1) 0 0;
    font-size: 0.76rem;
    line-height: 1.3;
    color: var(--text-muted);
}
.supplier-note .badge { padding: 0 6px; font-size: 0.68rem; }

/*
 * Prompt 51: the flag *is* the resolve control now, a `<button class="flag-tag
 * flag-tag-resolve">` rather than a `.flag-tag` plus a separate "Resolved"
 * button beside it. It has to look exactly like the inert `.flag-tag` span it
 * replaces at rest, then read "resolve" on hover or focus without changing
 * size — the clickable area has to stay put, not grow or shrink under the
 * pointer.
 *
 * The fixed width comes from `::before`, a hidden copy of the longer of the
 * two labels ("check this" is longer than "resolve" once both are run
 * through the same uppercase transform): it reserves the box's width and the
 * two real labels sit on top of it, absolutely positioned, swapped by
 * opacity rather than `display` so neither one ever reflows the other.
 */
.flag-tag-resolve {
    position: relative;
    border: 0;
    font-family: inherit;
    cursor: pointer;
}

.flag-tag-resolve::before {
    content: "check this";
    visibility: hidden;
}

.flag-tag-resolve .flag-tag-label {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: opacity 0.1s ease;
}

.flag-tag-resolve [data-label-hover] { opacity: 0; }

.flag-tag-resolve:hover [data-label-default],
.flag-tag-resolve:focus-visible [data-label-default] { opacity: 0; }

.flag-tag-resolve:hover [data-label-hover],
.flag-tag-resolve:focus-visible [data-label-hover] { opacity: 1; }

.flag-tag-resolve:disabled {
    cursor: default;
    opacity: 0.6;
}

/* The green mark a resolved flag leaves behind — no longer a control, so a
   plain `.flag-tag` span rather than `.flag-tag-resolve`'s button. Persists
   through a reload exactly as it read right after the click
   (`FieldIssues::wasResolved()`), so the two can never disagree. */
.flag-tag-resolved {
    --flag: var(--ok);
    background: var(--ok-soft);
    color: var(--ok);
}

/* The short inline message app.js shows beside a resolve button when the
   request failed — nothing else on the field changes, so this is the only
   sign anything went wrong. */
.flag-resolve-error {
    display: inline-block;
    margin-left: var(--space-2);
    font-size: 0.82rem;
    color: var(--danger);
}

/*
 * Project allocation's three radios — Prompt 28. Stacked by default, same as
 * any `.checkbox` group; from 640px (the same breakpoint `.field-row` itself
 * widens at) there is room for all three on one line, with each option's own
 * explanation moved to the `.tooltip` beside it rather than printed after it.
 */
.project-mode-options {
    display: flex;
    flex-direction: column;
}
@media (min-width: 640px) {
    .project-mode-options {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        column-gap: var(--space-5);
    }
}

.project-mode-option {
    display: flex;
    align-items: center;
    gap: var(--space-1);
}

/* A small "?" disc, hover- and keyboard-focusable, holding the description an
   inline `<span>` used to carry. Kept outside the `<label>` it sits beside —
   nested inside it, clicking the disc would also toggle the label's own radio,
   which is not what hovering it for a tooltip is asking for. */
.tooltip {
    position: relative;
    display: inline-flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--bg-sunken);
    color: var(--text-muted);
    font-size: 0.76rem;
    font-weight: 700;
    line-height: 1;
    cursor: help;
}
.tooltip:hover,
.tooltip:focus-visible {
    background: var(--accent-soft);
    color: var(--accent);
    outline: none;
}

.tooltip::after {
    content: attr(data-tooltip);
    position: absolute;
    z-index: 40;
    left: 50%;
    bottom: calc(100% + var(--space-2));
    transform: translateX(-50%);
    width: max-content;
    max-width: 18rem;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--text);
    color: var(--bg);
    font-size: 0.82rem;
    font-weight: 400;
    line-height: 1.4;
    text-align: left;
    box-shadow: var(--shadow);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.12s ease;
    pointer-events: none;
}
.tooltip:hover::after,
.tooltip:focus-visible::after {
    opacity: 1;
    visibility: visible;
}

/* Net, VAT and gross are read-only computed displays, not inputs a reviewer
   can override — see §22. Styled to look it: the same box, quietly inactive. */
.input-computed {
    background: var(--bg-sunken);
    color: var(--text-muted);
    cursor: default;
}

/* Prompt 55. The figure the document prints for this total, beside the box's
   title — the target the Total Adjustment reconciles to — and a pale red glow
   on the box when it is not exactly that. The glow always carries a `title`
   and screen-reader text naming what it differs from (partials/total-box). */
.total-ocr {
    margin-left: var(--space-1);
    font-weight: 400;
    font-size: 0.85em;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* One figure per line in a table cell — the Totals check's differences. */
.cell-line { display: block; white-space: nowrap; }

.input-computed.total-mismatch {
    border-color: color-mix(in srgb, var(--danger) 45%, var(--border, transparent));
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 18%, transparent);
    cursor: help;
}

/* --- The combobox — Prompt 21 -----------------------------------------------
   A progressive enhancement over an ordinary <select>: `app.js` wraps one
   marked `data-combo`, keeps it as the actual form control (visually hidden
   the same way `.sr-only` is, not `display:none`, so a required field still
   validates and the native control still works if the script fails to run),
   and drives it from a text input a reviewer can type into. Typing filters
   the list; choosing an option is the only way the real <select>'s value
   changes, so the field can never hold a value that was not already in it. */
.combo { position: relative; }

.combo-select {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.combo-input { cursor: text; }

/* Rendered on `document.body` and positioned in fixed coordinates by script —
   not inside `.combo` — so it is never clipped by an ancestor's own scrolling
   box, chiefly `.table-wrap` around the line-item table. */
.combo-list {
    position: fixed;
    z-index: 60;
    margin: 0;
    padding: var(--space-1) 0;
    list-style: none;
    max-height: 16rem;
    overflow-y: auto;
    background: var(--bg-raised);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow);
}

.combo-option {
    padding: var(--space-2) var(--space-3);
    font-size: 0.95rem;
    cursor: pointer;
    line-height: 1.35;
}

.combo-option:hover,
.combo-option.is-active { background: var(--bg-hover); }
.combo-option[aria-selected="true"] { font-weight: 600; }

.combo-empty { color: var(--text-muted); font-style: italic; cursor: default; }
.combo-empty:hover { background: none; }

/* --- "New project" modal — Prompt 33 ---------------------------------------
   The first inline-create-via-modal in the app, so nothing to match: styled
   from the same `.card`/`.field`/`.btn` primitives every other panel uses. */
.modal-overlay {
    position: fixed;
    inset: 0;
    z-index: 70;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
    background: var(--scrim);
}

.modal {
    width: 100%;
    max-width: 26rem;
    margin: 0;
    max-height: calc(100vh - 2 * var(--space-4));
    overflow-y: auto;
}

.modal h3 { margin-top: 0; }

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

/* A <details> whose summary is the button that opens it. The marker is removed
   because the button already says what it does, and a disclosure triangle
   beside "Create in Clear Books" reads as decoration rather than as a control. */
details.fieldset > summary { list-style: none; cursor: pointer; display: inline-flex; }
details.fieldset > summary::-webkit-details-marker { display: none; }
details.fieldset[open] > summary { margin-bottom: var(--space-4); }

/* Editable line items. The inputs sit flush in their cells so the table still
   reads as a table rather than as a wall of boxes, and the numeric columns stay
   narrow enough that the description keeps the width it needs. */
.table td > .input,
.table td > textarea.input {
    width: 100%;
    min-width: 0;
    padding: var(--space-1) var(--space-2);
}

.table td.amount > .input { text-align: right; font-variant-numeric: tabular-nums; }

/* The description column holds more than any other field on this screen —
   product code, full description, spec — and 3.2rem (about two lines) was
   cramped for it. Taller by default; still resizable for the rare line that
   needs more. */
.table td > textarea.input { resize: vertical; min-height: 5.4rem; }

/* Prompt 56: the review screen's line items. Each line is one `.line-item`
   holding two lines of controls, in one column template shared by every item
   so the columns read straight down the page:

     .line-main     Description | Qty | Unit | Net | VAT | Gross | X
     .line-pickers  Project | Account code | VAT rate | Quick pick

   This replaces Prompt 52's stacked pairs (Unit over VAT, Net over Gross,
   Account code over VAT rate) — every figure is its own box under one column
   header now, and the dropdowns are untitled. `/documents/{id}` and the
   extraction partial still use the `.table-lines` table below, untouched.

   The description takes what the fixed columns leave. On a 1920px display the
   pane beside the scan is roughly 55rem, so it gets about 20rem and each
   dropdown about 13rem; `min-width` on the grid, rather than a media query,
   is what decides when a narrower pane scrolls inside `.table-wrap`. */
.line-grid {
    --line-cols: minmax(12rem, 1fr) 4.5rem 6.5rem 6.5rem 6.5rem 6.5rem 1.9rem;
    --line-gap: var(--space-1);
    min-width: 47rem;
    font-size: 0.96rem;
}

.line-grid-head,
.line-main {
    display: grid;
    grid-template-columns: var(--line-cols);
    column-gap: var(--line-gap);
    align-items: start;
}

.line-grid-head {
    padding: var(--space-2);
    background: var(--bg-sunken);
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}
.line-grid-head .amount { text-align: right; padding-inline: var(--space-2); }

.line-item { padding: var(--space-2); border-top: 1px solid var(--border); }
.line-item:first-child { border-top: 0; }
.line-item:hover { background: var(--bg-hover); }

.line-pickers {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr)) 1.9rem;
    column-gap: var(--line-gap);
    align-items: start;
    margin-top: var(--space-1);
}
.line-grid-quickpick .line-pickers { grid-template-columns: repeat(3, minmax(0, 1fr)) 9.5rem 1.9rem; }

/* One box (and any flag notes under it) in a grid cell. `min-width: 0` so a
   long value shortens the box rather than widening the column. */
.line-slot { min-width: 0; scroll-margin-top: calc(var(--header-h, 60px) + var(--space-4)); }
.line-slot .input:not(.combo-select) { min-width: 0; padding-inline: var(--space-2); }
.line-slot.amount .input { text-align: right; font-variant-numeric: tabular-nums; }
.line-slot .flag-notes { font-size: 0.8rem; margin-top: var(--space-1); }

/* The description holds more than any other field on this screen — product
   code, full description, spec — so it stays a textarea, and resizable. */
.line-slot-desc textarea.input { resize: vertical; min-height: 4.2rem; }

/* A flagged slot: the same mark `.table td.is-flagged` gives a table cell. */
.line-slot.is-flagged {
    box-shadow: inset 3px 0 0 var(--flag);
    background: var(--flag-soft);
    background: color-mix(in srgb, var(--flag-soft) 45%, transparent);
}
.line-slot.is-flagged .input { border-color: var(--flag); }

/* Project is only asked for in Per line allocation, but its slot is always in
   the layout: hidden by *visibility*, so Account code and VAT rate stay
   exactly where they are whichever allocation is chosen. Needed as an
   override, because `[hidden]` is `display: none !important` (top of file).
   `visibility: hidden` also takes the combobox out of the tab order. */
.line-slot-project[hidden] { display: block !important; visibility: hidden; }

/* The Total Adjustment's note is a direct child of the item, not of the
   description slot, so it spans the whole grid. */
.line-item.row-adjustment { background: var(--bg-sunken); }
.line-note { margin: var(--space-1) 0 0; }

.line-add { padding: var(--space-2); border-top: 1px solid var(--border); }

/* Prompt 52: a small X, not a full button — the row already carries a Quick
   pick trigger and two pickers, and a `.btn` here would be the loudest thing
   in it for the one action every row on the page can take. */
.line-remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.7rem;
    height: 1.7rem;
    padding: 0;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
    color: var(--text-muted);
    font-size: 1.05rem;
    line-height: 1;
    cursor: pointer;
}
.line-remove:hover { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); }

/* Prompt 52: the typed-arithmetic and can't-derive-Unit failures both leave
   the field exactly as it was and mark it this way — a glow rather than a
   standing border, since neither is a `FieldIssues` flag the "Resolved"
   button on the rest of the page understands, and both clear themselves on
   the next keystroke (`app.js` strips the class on `input`). */
@keyframes line-calc-error-glow {
    from { box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 55%, transparent); }
    to   { box-shadow: 0 0 0 0 transparent; }
}
.table-lines .input.line-calc-error,
.line-grid .input.line-calc-error {
    border-color: var(--danger);
    animation: line-calc-error-glow 900ms ease-out;
}

/*
 * The line table's own column widths.
 *
 * Left to itself the browser gives every column an equal share, which on a
 * mostly-numeric table means three characters of quantity get as much room as
 * a three-line product description. The numeric columns are sized to the
 * longest number anybody posts to Clear Books and no wider; the picker
 * columns to the length of a nominal code plus its name; the description
 * takes everything left over, and on a wide screen that is most of it.
 */
.table-lines { table-layout: fixed; min-width: 40rem; }

/* Prompt 41: `.table-compact`'s own cell padding (space-2/space-3, i.e.
   8px/12px) is the gap between one box's border and the next — above,
   below, and side to side — and that was wider than the padding inside a
   box around its own text. Cut to near nothing; the padding inside each
   box (set on `.input` above) is what still keeps its text off the sides. */
.table-lines th, .table-lines td { padding: 0.1rem; }

/* The only `auto` column, so it takes everything the others leave: 8rem on a
   narrow pane, and better than 25rem beside a scan on a 1920 monitor. The
   others are sized to their content and stop there — a quantity is three
   characters and a nominal code is five, and neither reads better with more.
   The two pickers are `<select>`s, which truncate an over-long option with an
   ellipsis and show it in full when opened, so they are the cheapest place to
   take the room from.

   `min-width` on the table rather than a media query, so what decides the
   layout is the width of the *pane* — which on this screen is about half the
   window — rather than the width of the window. Below it the table scrolls
   inside `.table-wrap`, which is what that wrapper is for. */
.table-lines .col-desc { width: auto; }
.table-lines .col-qty { width: 4.5rem; }

/* `.col-money`/`.col-picker` are what `templates/partials/extraction.php` and
   `edit-extraction.php` use: those tables still lay Unit/Net/VAT and Account
   code/VAT rate out as separate columns. The review screen no longer uses this
   table at all — see `.line-grid` above (Prompt 56). */
.table-lines .col-money { width: 6.5rem; }
.table-lines .col-picker { width: 11rem; }

/* Prompt 41: the Project select sat in the generic `.col-narrow` (7rem),
   noticeably tighter than every other dropdown on the row — Quick Pick
   (10rem) is the widest of the bunch and the one to match. */
.table-lines .col-project { width: 10rem; }

/* Prompt 42: the Total Adjustment row.
   InvoGrid's own arithmetic rather than a line off the invoice, recomputed on
   every save, and therefore read-only — the tint says "this row is not like the
   others" before the reviewer reaches the hint that explains why. Deliberately
   quiet: it is not a warning, and the flag on Gross is where the actual
   "check this" lives. */
.table-lines .row-adjustment > td { background: var(--bg-sunken); }
.table-lines .row-adjustment .field-hint { margin: 0.25rem 0 0; }

/* The transcription beside the scan scrolls on its own rather than stretching
   the pane: on a long document it is several screens of text and the reviewer
   wants the PDF to stay where it is. */
.doc-pane .ocr-text { max-height: 22rem; overflow-y: auto; }

/* On the review screen the transcription is folded away under the scan. It is
   InvoGrid's own reading of the page and the scan is the authority; a reviewer
   who wants the text wants it deliberately, and unfolded it pushes the page
   images off the pane on a laptop. */
.ocr-panel { flex: 0 0 auto; }

.ocr-panel > summary {
    cursor: pointer;
    padding: var(--space-2) 0;
    font-weight: 600;
    font-size: 0.92rem;
}

.ocr-panel[open] > summary { margin-bottom: var(--space-2); }

/* --- The prompt editor ---------------------------------------------------- */

/* A prompt is several screens of prose with structure that matters — headings,
   bullet rules, a JSON example. Monospaced so the example lines up, and tall,
   because scrolling a four-line box to read four kilobytes is how a rule gets
   edited out by accident. */
.prompt-editor {
    min-height: 60vh;
    resize: vertical;
    line-height: 1.5;
    tab-size: 2;
    white-space: pre;
    overflow-wrap: normal;
    overflow-x: auto;
}

/* The variable panel: each one opens to show what it holds today. */
details.fieldset > summary .cell-sub { display: block; margin-top: 2px; }
details.fieldset + details.fieldset { margin-top: calc(var(--space-3) * -1 + var(--space-4)); }

/* The capability list on the users screen. A tick rather than a bullet: the
   list is "what this role can do", and every line is a yes. */
.tick-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: var(--space-2);
}

.tick-list li {
    position: relative;
    padding-left: 1.4em;
    line-height: 1.4;
}

.tick-list li::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0.42em;
    width: 0.5em;
    height: 0.8em;
    border: solid var(--accent);
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}

/* A radio row reads the same as a checkbox row — a control, a bold name, and a
   sentence of what choosing it means. */
.checkbox input[type="radio"] {
    width: 22px; height: 22px;
    margin-top: 2px;
    accent-color: var(--accent);
    flex: 0 0 auto;
}

.checkbox .cell-sub { display: block; margin-top: 2px; }

/* A form with three short fields and nothing else on the page. Full width
   would put the label a hand's width from its box on a desktop. */
.form-narrow { max-width: 32rem; }

/* A <fieldset> used only to switch a whole form off for somebody who may read
   it but not change it. One `disabled` beats the same attribute on forty
   controls — the one that gets forgotten is a box somebody types into for a
   minute before finding out it was never going to save. */
.form-body {
    border: 0;
    padding: 0;
    margin: 0;
    min-width: 0;
}

.form-body:disabled {
    opacity: 0.75;
}

.form-body:disabled .input,
.form-body:disabled select {
    background: var(--bg-sunken);
    cursor: not-allowed;
}

/* The activity feed: a time on the left, what happened on the right. A table
   would be three columns of which one is always short and one is always long,
   which reads worse than it sounds. */
.feed { list-style: none; margin: 0 0 var(--space-4); padding: 0; display: grid; gap: var(--space-2); }

.feed-item {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-left: 3px solid var(--border);
    background: var(--bg-raised);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}

.feed-when { color: var(--text-muted); font-size: 0.88rem; flex: 0 0 auto; min-width: 9.5rem; }
.feed-what { flex: 1 1 16rem; }
.feed-what .cell-sub { display: block; }

/* A failure's structured detail, folded away inside the event row. Open by
   click rather than always visible: the history table is scanned far more often
   than any one row is read. */
.event-detail summary { cursor: pointer; }
.event-detail .meta-list { margin-top: var(--space-2); }

/* Separates the sections inside a synced-data record's expanded detail —
   InvoGrid's own additions, then the raw Clear Books record — without
   another nested card. */
.event-detail .detail-heading { margin-top: var(--space-4); margin-bottom: var(--space-1); }

/* --- Logo previews on the branding screen --------------------------------- */

/* Each preview sits on the ground its variant is *for*, not on the page's
   current theme — a dark-mode logo previewed on a light card is an invisible
   logo, and somebody would reasonably conclude the upload had failed. These two
   are therefore fixed colours rather than theme variables, which is the one
   place in this stylesheet where that is correct. */
.logo-preview {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: 84px;
    padding: var(--space-4);
    margin-bottom: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}

.logo-preview img {
    display: block;
    max-height: 48px;
    max-width: 100%;
    width: auto;
    object-fit: contain;
}

.logo-preview-light { background: #ffffff; color: #0f172a; }
.logo-preview-dark  { background: #0b1120; color: #f1f5f9; }

/* The monogram placeholder has to flip with the ground it is standing on. */
.logo-preview-light .brand-mark { background: #4338ca; color: #ffffff; }
.logo-preview-dark  .brand-mark { background: #a5b4fc; color: #10132e; }
.logo-preview .muted { color: inherit; opacity: 0.65; }

/* A file input is a browser widget with its own idea of padding; .input's
   fixed height crops the button in some of them. */
.input[type="file"] {
    height: auto;
    padding: var(--space-2);
    line-height: 1.4;
}

/* --- The upload page — Prompt 50 --------------------------------------------
   A drop zone, and a queue of files each with its own progress.

   The zone is a <label> pointing at the file input, so clicking it opens the
   dialogue and a keyboard reaches it the same way it reaches any other label.
   Nothing here is click-only: everything the zone does, the input does too. */
.dropzone {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-1);
    padding: var(--space-6) var(--space-4);
    margin-bottom: var(--space-4);
    border: 2px dashed var(--border-strong);
    border-radius: var(--radius);
    background: var(--bg-hover);
    text-align: center;
    cursor: pointer;
    transition: border-color 0.15s ease, background 0.15s ease;
}

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

/* The dragged-over state. A colour change alone would be invisible to anybody
   who cannot see it, but a drag is a pointer gesture by definition — there is
   no keyboard equivalent to signal, and the input below is the keyboard path. */
.dropzone.is-over {
    border-color: var(--accent);
    border-style: solid;
    background: var(--accent-soft);
}

.dropzone-icon { font-size: 1.8rem; line-height: 1; color: var(--text-muted); }
.dropzone-lead { font-weight: 600; }
.dropzone-sub  { color: var(--text-muted); font-size: 0.92rem; }

/* The input is the accessible twin of the zone, not a duplicate control: it
   keeps its own label and hint, and both feed the same queue. */
.upload-queue {
    list-style: none;
    margin: 0 0 var(--space-4);
    padding: 0;
    display: grid;
    gap: var(--space-2);
}

.upload-item {
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-left-width: 4px;
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
}

/* One mark per state, in the same colours the badges use, so a long queue can
   be read down the left edge without reading any of the words. */
.upload-pending   { border-left-color: var(--border-strong); }
.upload-uploading { border-left-color: var(--info); }
.upload-done      { border-left-color: var(--ok); }
.upload-duplicate { border-left-color: var(--warn); }
.upload-failed    { border-left-color: var(--danger); }

.upload-item-head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

/* The filename takes what is left and truncates rather than wrapping: a queue
   of fifty rows whose heights depend on how long a scanner names its files is
   a queue nobody can scan. */
.upload-item-name {
    flex: 1 1 auto;
    min-width: 0;
    font-weight: 600;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.upload-item-size { flex: 0 0 auto; font-size: 0.88rem; }

.upload-bar {
    margin-top: var(--space-2);
    height: 6px;
    border-radius: 999px;
    background: var(--bg-sunken);
    overflow: hidden;
}

/* Width is set from the XHR's own progress events, so this moves because bytes
   moved. There is no animation on it beyond smoothing the steps between
   events — a bar that advances on a timer is a lie about a stalled upload. */
.upload-bar-fill {
    height: 100%;
    width: 0;
    background: var(--info);
    transition: width 0.12s linear;
}

.upload-item-note {
    margin: var(--space-2) 0 0;
    font-size: 0.9rem;
    color: var(--text-muted);
}

.upload-failed .upload-item-note { color: var(--danger); }

.upload-item-tools {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-top: var(--space-2);
}

.upload-item-tools:empty { display: none; }
.upload-item-link { font-size: 0.9rem; }

.queue-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
}

.queue-head h2 { margin: 0; }
.queue-head p  { margin: 0; }

@media (max-width: 640px) {
    .dropzone { padding: var(--space-5) var(--space-3); }

    /* The size drops below the name rather than squeezing it: on a narrow
       screen the filename is the thing being looked for. */
    .upload-item-head { flex-wrap: wrap; }
    .upload-item-name { flex-basis: 100%; }
}

/* --- Motion --------------------------------------------------------------- */

/* Somebody who has asked their operating system for less motion has asked
   every application on it, and the answer has to cover the page rather than the
   three components that happened to be remembered. This replaces two
   narrower blocks that killed the caret and the flash transitions and left the
   nav drawer, the buttons and the flash's leaving transform still moving.

   `.flash.is-leaving` also has its transform cancelled: a message that slides
   away is still motion even when the slide is instant. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        transition-duration: 0.01ms !important;
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        scroll-behavior: auto !important;
    }

    .flash.is-leaving { transform: none; }
}

/* ==========================================================================
   Print

   Two parts, and the distinction matters. The `.print-*` classes below style
   the dedicated print layout *on screen* — somebody looks at that page before
   they print it, and a preview that looks nothing like the output is not a
   preview. The print block at the end is the small set of changes that only
   make sense on paper.

   InvoGrid follows Kitwell here: a separate layout for anything meant to be
   printed, rather than the ordinary page with its chrome hidden. Hiding the
   navigation still ships it, and a printed record that quietly breaks when the
   menu changes is one nobody notices until a supplier queries an invoice.
   ========================================================================== */

.print-body {
    background: var(--bg-sunken);
}

/* The only chrome, and it is .no-print. Sticky so "Print this" is reachable
   from the bottom of a long summary without scrolling back up. */
.print-toolbar {
    position: sticky;
    top: 0;
    z-index: 10;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    background: var(--bg-raised);
    border-bottom: 1px solid var(--border);
}

/* A sheet of A4, near enough, so what is on screen is what comes out. */
.print-sheet {
    width: 100%;
    max-width: 210mm;
    min-height: 260mm;
    margin: var(--space-5) auto;
    padding: 16mm 14mm;
    background: #ffffff;
    color: #0f172a;
    box-shadow: var(--shadow);
}

.print-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-4);
    padding-bottom: var(--space-4);
    margin-bottom: var(--space-5);
    border-bottom: 2px solid #0f172a;
}

.print-brand { display: flex; align-items: flex-end; gap: var(--space-3); min-width: 0; }

/* Same aspect-ratio-safe treatment as the header: a height, an automatic
   width, and a ceiling so a very wide logo cannot push the date off the sheet. */
.print-logo {
    display: block;
    height: 40px;
    width: auto;
    max-width: 220px;
    object-fit: contain;
}

.print-wordmark { font-size: 1.15rem; font-weight: 700; line-height: 1; padding-bottom: 2px; }

.print-meta {
    display: grid;
    gap: 2px;
    text-align: right;
    font-size: 0.85rem;
    color: #52627a;
}

.print-title { font-size: 1.5rem; margin-bottom: var(--space-1); }
.print-subtitle { color: #52627a; margin-bottom: var(--space-5); }

.print-block { margin-bottom: var(--space-5); break-inside: avoid; }

.print-block h2 {
    font-size: 0.78rem;
    font-weight: 800;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: #52627a;
    padding-bottom: var(--space-1);
    margin-bottom: var(--space-3);
    border-bottom: 1px solid #cbd5e1;
}

/* The Clear Books outcome, which is what somebody printing this wants to point
   at. An indigo rule rather than a filled panel: a block of colour is a block
   of ink, and this page exists to be printed. */
.print-block-key { border-left: 4px solid #4338ca; padding-left: var(--space-4); }
.print-block-pending { border-left: 4px solid #854d0e; padding-left: var(--space-4); }

.print-pairs { display: grid; gap: var(--space-2); margin: 0; }
.print-pairs > div { display: flex; gap: var(--space-3); align-items: baseline; }

.print-pairs dt {
    flex: 0 0 12rem;
    font-weight: 600;
    color: #52627a;
}

.print-pairs dd { margin: 0; flex: 1 1 auto; min-width: 0; }

.print-pairs-totals > div { max-width: 24rem; justify-content: space-between; }
.print-pairs-totals dd { text-align: right; flex: 0 0 auto; font-variant-numeric: tabular-nums; }

.print-total {
    padding-top: var(--space-2);
    border-top: 1px solid #cbd5e1;
    font-weight: 700;
    font-size: 1.05rem;
}
.print-total dt { color: inherit; }

.table-print { font-size: 0.92rem; }
.table-print th, .table-print td { border-color: #cbd5e1; }
.table-print tr { break-inside: avoid; }

.print-list { margin: 0; padding-left: 1.2rem; }
.print-list li { margin-bottom: var(--space-1); }
.print-note-line { color: #52627a; font-size: 0.9rem; }

.print-foot {
    margin-top: var(--space-6);
    padding-top: var(--space-3);
    border-top: 1px solid #cbd5e1;
    font-size: 0.8rem;
    color: #52627a;
}

@media print {
    @page { size: A4; margin: 12mm; }

    .no-print { display: none !important; }

    /* Anything from the ordinary layout, for the day somebody prints a normal
       page — better a readable sheet than the navigation across the top of it. */
    .site-header, .site-footer, .filter-bar, .form-actions,
    .nav-toggle, .btn, .flash-stack { display: none !important; }

    html, body {
        background: #ffffff !important;
        color: #000000;
        margin: 0;
        padding: 0;
        font-size: 11pt;
    }

    /* The sheet is the page now: no shadow, no margin, no second set of edges
       inside the printer's own. */
    .print-sheet {
        max-width: none;
        min-height: 0;
        margin: 0;
        padding: 0;
        box-shadow: none;
    }

    .card, .table-wrap { border: 1px solid #999; box-shadow: none; }

    /* A table split over a page break repeats its headings rather than leaving
       the second half as unlabelled columns. */
    thead { display: table-header-group; }
    tr, .print-block { break-inside: avoid; }

    /* A link's href is invisible on paper, and a summary that says "see the
       document" without saying where is a dead end. */
    a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.85em; word-break: break-all; }
}

/* --- Reference files on a document (§59) ------------------------------------
   Quieter than the document's own data on purpose: one line per file, and
   everything read off it folded away. */
.attachment-list { list-style: none; margin: 0; padding: 0; }
.attachment-list > .attachment { padding: var(--space-3) 0; border-top: 1px solid var(--border); }
.attachment-list > .attachment:first-child { border-top: 0; padding-top: 0; }
.attachment-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.attachment .cell-sub { margin: var(--space-1) 0 0; }
.attachment form { margin-top: var(--space-2); }
.attachment-detail .meta-list { margin-top: var(--space-2); }
.attachment-detail .ocr-text { max-height: 24rem; overflow: auto; }
.attachment-add { margin-top: var(--space-3); }
.attachment-add[open] > summary { margin-bottom: var(--space-3); }
