/* ==========================================================================
   WMS design tokens
   Single source of truth for colour, radius, shadow and spacing. Themes only
   ever redefine these variables, never a component rule, so light/dark and
   compact/comfortable stay in sync with every screen.

   Palette: the Shipping Pro / WareApp dashboard UI kit (dpopstudio) in its green
   colour profile. Emerald #0ACF83 is the kit's brand green; the light theme uses a
   darker shade of the same hue so links and buttons still pass WCAG AA, and the
   status colours keep the kit's coral and amber hues in a text-safe shade.
   ========================================================================== */
:root {
    --accent: #0b7a52;
    --accent-hover: #065f3e;
    --accent-soft: #d7f4e6;
    --success: #2f7d32;
    --success-soft: #eef7e2;
    --success-line: #cfe3ab;
    --danger: #b42318;
    --danger-soft: #fef3f2;
    --danger-line: #f3c2bc;
    --info-line: #bfe8d8;
    --warning: #b54708;
    --warning-soft: #fef6e3;
    --warning-line: #f2d29b;
    --warning-fill: #ffb300;

    --ink: #1d2939;
    --muted: #667085;
    --line: #eaecf0;
    /* --line separates rows and surfaces; it is deliberately quiet (1.1:1) and must never be used
       for something the operator has to notice. --marker is the quietest grey that still clears
       the 3:1 the sort arrows and breadcrumb separators need. */
    --marker: #7b8798;
    --bg: #f9fafb;
    --surface: #ffffff;
    --surface-2: #f7f7f9;

    --radius-sm: 6px;
    --radius: 10px;
    --radius-lg: 14px;
    --shadow-sm: 0 1px 2px rgba(29, 41, 57, .06);
    --shadow: 0 2px 8px rgba(29, 41, 57, .08);
    --shadow-lg: 0 18px 48px rgba(29, 41, 57, .18);

    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 24px;
    --space-6: 32px;

    --row-pad: 10px 12px;
    --field-gap: 12px;
    --nav-w: 236px;
    --topbar-h: 56px;

    --focus: 0 0 0 3px rgba(11, 122, 82, .35);

    /* --on-accent is the ink that sits on a surface filled with --accent. White clears the light
       theme's #0b7a52 at 5.4:1 and fails the dark theme's #0acf83 at 2.0:1, so the dark themes below
       trade it for a near-black green. Any rule that fills with the accent must text with this,
       never with a literal white. */
    --on-accent: #ffffff;

    /* Type scale. The app has drawn at these steps throughout; naming them is what stops a new
       screen inventing a 12.5px sibling. --text-tap is the smallest size a mobile browser accepts
       for a field before it zooms the page to read it. */
    --text-2xs: 11px;
    --text-xs: 12px;
    --text-sm: 12.5px;
    --text-md: 13px;
    --text-base: 14px;
    --text-lg: 15px;
    --text-xl: 18px;
    --text-2xl: 21px;
    --text-display: 26px;
    --text-tap: 16px;

    /* Layer scale, spaced by tens so a component can slip between two existing layers. The scrim
       dims the page and the drawer sits on it; a sticky head or save bar only needs to clear its own
       rows; the skip link outranks the modal because a keyboard user must always have a way out. */
    --z-in-page: 2;
    --z-scrim: 25;
    --z-topbar: 30;
    --z-drawer: 40;
    --z-toast: 50;
    --z-modal: 60;
    --z-skip-link: 70;

    /* Motion. --dur-fast is a control answering a hover, a focus or a toast arriving; --dur-base is
       the one thing that crosses the screen, the drawer. The last two are cycles that repeat rather
       than settle, so they are not transition timings. */
    --dur-fast: .15s;
    --dur-base: .2s;
    --dur-spin: .7s;
    --dur-pulse: 1s;

    /* Both layouts ship `viewport-fit=cover`, which is what lets the page run under the notch and
       the home indicator in the first place - so the insets have to be paid back to the four places
       an operator actually reaches: the top bar, the drawer's scroll box, the sticky save bar and
       the toast corner. `--safe-inline` takes the wider of the two side insets rather than mapping
       left to start, because a landscape phone has the cut-out on whichever side it is held and an
       Arabic page must not suddenly put it on the wrong one. On a desktop browser every one of
       these resolves to 0px, so nothing moves. */
    --safe-top: env(safe-area-inset-top, 0px);
    --safe-bottom: env(safe-area-inset-bottom, 0px);
    --safe-inline: max(env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px));

    /* The report chart series, read by reports.js at draw time. The light set is what the reports
       have always drawn with (4.9:1 to 7.1:1 on a white panel); the dark set is the lighter
       siblings of the same hues, because the light green reads 1.5:1 on a #1d2939 panel and a
       series nobody can see is not a series. */
    --chart-palette: #0b7a52, #0e7490, #b54708, #6d28d9, #b42318, #2f7d32, #1d4ed8, #a16207;

    color-scheme: light;
}

/* Operator-picked theme wins; with no data-theme the page follows the system. */
[data-theme="dark"] {
    --ink: #f2f4f7;
    --muted: #98a2b3;
    --line: #344054;
    --bg: #101828;
    --surface: #1d2939;
    --surface-2: #323e52;
    --accent: #0acf83;
    --accent-hover: #2ee08c;
    --accent-soft: #10352c;
    --success: #7ee787;
    --success-soft: #2a3312;
    --success-line: #4a5a20;
    --danger: #ff7262;
    --danger-soft: #3b1f1c;
    --danger-line: #6b302b;
    --info-line: #1f5744;
    --marker: #8a94a6;
    --warning: #ffb300;
    --warning-soft: #3a2e12;
    --warning-line: #6b5323;
    --warning-fill: #ffb300;
    --focus: 0 0 0 3px rgba(10, 207, 131, .4);
    /* The bright accent a dark theme paints with cannot carry white: white on #0acf83 is 2.0:1.
       This is the same hue taken to near black, so a filled button still reads as ink (8.1:1). */
    --on-accent: #04231a;
    --chart-palette: #0acf83, #22b8cf, #ffa726, #b197fc, #ff7262, #7ee787, #74a0ff, #ffd43b;
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .5);
    --shadow: 0 2px 10px rgba(0, 0, 0, .45);
    --shadow-lg: 0 20px 50px rgba(0, 0, 0, .6);
    color-scheme: dark;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme]) {
        --ink: #f2f4f7;
        --muted: #98a2b3;
        --line: #344054;
        --bg: #101828;
        --surface: #1d2939;
        --surface-2: #323e52;
        --accent: #0acf83;
        --accent-hover: #2ee08c;
        --accent-soft: #10352c;
        --success: #7ee787;
        --success-soft: #2a3312;
        --success-line: #4a5a20;
        --danger: #ff7262;
        --danger-soft: #3b1f1c;
        --danger-line: #6b302b;
        --info-line: #1f5744;
        --marker: #8a94a6;
        --warning: #ffb300;
        --warning-soft: #3a2e12;
        --warning-line: #6b5323;
        --warning-fill: #ffb300;
        --focus: 0 0 0 3px rgba(10, 207, 131, .4);
        --on-accent: #04231a;
        --chart-palette: #0acf83, #22b8cf, #ffa726, #b197fc, #ff7262, #7ee787, #74a0ff, #ffd43b;
        --shadow-sm: 0 1px 2px rgba(0, 0, 0, .5);
        --shadow: 0 2px 10px rgba(0, 0, 0, .45);
        --shadow-lg: 0 20px 50px rgba(0, 0, 0, .6);
        color-scheme: dark;
    }
}

* { box-sizing: border-box; }

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

body {
    margin: 0;
    font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
    font-size: var(--text-base);
    line-height: 1.45;
    color: var(--ink);
    background: var(--bg);
    min-height: 100vh;
    min-height: 100svh;
}

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

/* Keyboard users must always see where they are. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    outline: none;
    box-shadow: var(--focus);
    border-radius: var(--radius-sm);
}
/* ==========================================================================
   Shell: sticky header, sidebar rail, content, footer
   ========================================================================== */
.shell { display: flex; flex-direction: column; min-height: 100vh; min-height: 100svh; }

.topbar {
    position: sticky;
    top: 0;
    z-index: var(--z-topbar);
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    min-height: var(--topbar-h);
    padding: calc(6px + var(--safe-top)) calc(clamp(12px, 2.5vw, 20px) + var(--safe-inline));
    background: var(--surface);
    border-bottom: 1px solid var(--line);
    box-shadow: var(--shadow-sm);
}

.topbar .brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-weight: 700;
    letter-spacing: .06em;
    text-decoration: none;
    color: var(--ink);
    padding: 6px 4px;
    min-height: 40px;
}

.topbar .brand .brand-mark {
    display: grid;
    place-items: center;
    width: 28px;
    height: 28px;
    border-radius: 8px;
    background: var(--accent);
    color: var(--on-accent);
    font-size: var(--text-xs);
    letter-spacing: 0;
}

/* An uploaded company logo takes the place of the "WMS" mark in the shell. */
.topbar .brand .brand-logo {
    display: block;
    max-width: 150px;
    max-height: 32px;
    width: auto;
    object-fit: contain;
}

.logo-preview {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-bottom: var(--space-2);
}

.company-logo,
.brand-logo {
    display: block;
    max-width: 260px;
    max-height: 140px;
    width: auto;
    object-fit: contain;
    padding: 6px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: #fff;
}

/* The same mark in the brand list, small and never cropped. */
.brand-mark {
    display: inline-block;
    vertical-align: middle;
    width: 32px;
    height: 32px;
    object-fit: contain;
    margin-inline-end: var(--space-2);
    padding: 2px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: #fff;
}

.topbar .crumbs { flex: 1; min-width: 0; }

.frame { display: flex; flex: 1; align-items: stretch; min-width: 0; }

/* The rail stretches to the frame so its tint and border reach the footer on short
   pages; the links inside stay sticky so a long list never scrolls them away. */
.sidebar {
    flex: 0 0 var(--nav-w);
    width: var(--nav-w);
    align-self: stretch;
    background: var(--surface);
    border-inline-end: 1px solid var(--line);
}

.nav-inner {
    position: sticky;
    top: calc(var(--topbar-h) + var(--safe-top));
    max-height: calc(100vh - var(--topbar-h));
    /* 100vh is the viewport with the browser's own bar hidden, so on a phone the drawer could grow
       past the visible area and its last group sat behind it. The small-viewport unit is the height
       that is always reachable; the line above stays as the answer for a browser that has no it. */
    max-height: calc(100svh - var(--topbar-h));
    overflow: auto;
    padding: var(--space-3) calc(var(--space-2) + var(--safe-inline)) calc(var(--space-5) + var(--safe-bottom));
}

/* Above the drawer breakpoint the rail is part of the page, so an operator who wants the whole
   width for a wide table can put it away and get it back with the same button. The choice belongs
   to this browser and is applied before first paint, the same way the theme is. Below that
   breakpoint the rail is already out of the way until the button opens it as a drawer, so the
   attribute must not hide it there. */
@media (min-width: 961px) {
    html[data-nav="hidden"] .sidebar { display: none; }
    html[data-nav="hidden"] .nav-toggle {
        background: var(--accent-soft);
        border-color: var(--accent);
        color: var(--accent);
    }
}

.sidebar h2 { margin: var(--space-4) var(--space-3) var(--space-1); }

.sidebar a {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: 9px var(--space-3);
    border-radius: var(--radius-sm);
    color: var(--muted);
    text-decoration: none;
    font-weight: 500;
    min-height: 40px;
}

.sidebar a .ico { flex: 0 0 auto; }

.sidebar a:hover { background: var(--surface-2); color: var(--ink); }

.sidebar a[aria-current="page"] {
    background: var(--accent-soft);
    color: var(--accent);
    font-weight: 650;
    box-shadow: inset 2px 0 0 var(--accent);
}

[dir="rtl"] .sidebar a[aria-current="page"] { box-shadow: inset -2px 0 0 var(--accent); }

/* The rail groups are collapsible sections. A closed heading still says where the group is, and
   the page opens the one it belongs to. <details>/<summary> do the folding themselves, so the rail
   works with scripting off; the script only remembers what the operator folded by hand. */
.nav-section { margin: 0 0 var(--space-1); }

summary.nav-group {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: var(--space-3) var(--space-2) var(--space-1);
    padding: 6px var(--space-2);
    border-radius: var(--radius-sm);
    color: var(--muted);
    font-size: var(--text-2xs);
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    cursor: pointer;
    list-style: none;
    min-height: 32px;
    user-select: none;
}

summary.nav-group::-webkit-details-marker { display: none; }

summary.nav-group:hover { background: var(--surface-2); color: var(--ink); }

summary.nav-group .ico { flex: 0 0 auto; width: 15px; height: 15px; }

/* The chevron points at the reading direction when closed and at the list when open, and the open
   angle is the same in both directions because the closed one is mirrored. */
summary.nav-group .chev {
    margin-inline-start: auto;
    width: 13px;
    height: 13px;
    transition: transform var(--dur-fast) ease;
}

.nav-section[open] > summary.nav-group .chev { transform: rotate(90deg); }

[dir="rtl"] summary.nav-group .chev { transform: rotate(180deg); }
[dir="rtl"] .nav-section[open] > summary.nav-group .chev { transform: rotate(90deg); }

/* The links hang under their heading, so they start a little further in. */
.nav-items a { padding-inline-start: calc(var(--space-3) + 2px); }

.content {
    flex: 1;
    min-width: 0;
    padding: clamp(12px, 2.5vw, 22px);
    max-width: 1500px;
    margin: 0 auto;
    width: 100%;
}

.footer {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    justify-content: space-between;
    padding: var(--space-4) clamp(12px, 2.5vw, 20px);
    color: var(--muted);
    border-top: 1px solid var(--line);
    background: var(--surface);
}

/* ==========================================================================
   Header cluster: search, theme, density, language
   ========================================================================== */
.topbar-cluster { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.topbar-cluster input[type="search"] { width: 210px; }

.icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--muted);
    cursor: pointer;
}

.icon-btn:hover { background: var(--surface-2); color: var(--ink); }
.icon-btn[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }

.nav-toggle {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    margin-inline-start: 0;
    padding: 8px 12px;
    min-height: 40px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    cursor: pointer;
}

.nav-toggle:hover { background: var(--surface-2); color: var(--ink); }

.scrim {
    position: fixed;
    inset: 0;
    z-index: var(--z-scrim);
    background: rgba(29, 41, 57, .5);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--dur-fast) ease;
}

body.nav-open .scrim { opacity: 1; pointer-events: auto; }

/* ==========================================================================
   Breadcrumb
   ========================================================================== */
.crumbs ol { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0; padding: 0; list-style: none; }
.crumbs li { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: var(--text-md); }
.crumbs li + li::before { content: "/"; color: var(--marker); }
.crumbs a { color: var(--muted); text-decoration: none; }
.crumbs a:hover { color: var(--accent); text-decoration: underline; }
.crumbs [aria-current="page"] { color: var(--ink); font-weight: 600; }

/* ==========================================================================
   Type
   ========================================================================== */
h1 { font-size: var(--text-2xl); font-weight: 650; margin: 0 0 4px; letter-spacing: -.01em; }
h2 { font-size: var(--text-lg); font-weight: 650; margin: var(--space-5) 0 var(--space-2); color: var(--ink); }
h3 { font-size: var(--text-base); margin: 0 0 var(--space-2); }
.subtitle { color: var(--muted); margin: 0 0 var(--space-5); }

.page-head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
    margin-bottom: var(--space-4);
}

.page-head h1 { margin-bottom: 2px; }
.page-head .subtitle { margin: 0; }

/* ==========================================================================
   Surfaces
   ========================================================================== */
.panel {
    --panel-pad: var(--space-5);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: var(--panel-pad);
    margin-bottom: var(--space-5);
    box-shadow: var(--shadow-sm);
}

.panel > h1:first-child { margin-top: 0; }

.cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
    gap: var(--space-3);
    margin-bottom: var(--space-5);
}

.card {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 2px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: var(--space-4);
    box-shadow: var(--shadow-sm);
    transition: box-shadow var(--dur-fast) ease, transform var(--dur-fast) ease, border-color var(--dur-fast) ease;
}

.card:hover { box-shadow: var(--shadow); border-color: var(--muted); }
.card .label { color: var(--muted); font-size: var(--text-sm); font-weight: 600; letter-spacing: .02em; }
.card .value { font-size: var(--text-display); font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
.card .foot { color: var(--muted); font-size: var(--text-xs); }
.card .ico { position: absolute; inset-inline-end: 14px; top: 14px; color: var(--accent); opacity: .5; }

a.card { text-decoration: none; color: inherit; }
a.card:hover { border-color: var(--accent); }

/* Quick-action tiles on the dashboard - the three things an operator does all day. */
.tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr));
    gap: var(--space-3);
    margin-bottom: var(--space-5);
}

.tile {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-4);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink);
    text-decoration: none;
    font-weight: 600;
    box-shadow: var(--shadow-sm);
    transition: border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease, transform var(--dur-fast) ease;
}

.tile:hover { border-color: var(--accent); box-shadow: var(--shadow); transform: translateY(-1px); }
.tile .ico { color: var(--accent); flex: 0 0 auto; }
.tile small { display: block; color: var(--muted); font-weight: 400; font-size: var(--text-xs); }

/* Capacity / utilisation meter. */
.meter { display: grid; gap: 4px; min-width: 120px; }
.meter .track { height: 6px; border-radius: 999px; background: var(--surface-2); border: 1px solid var(--line); overflow: hidden; }
.meter .fill { height: 100%; background: var(--accent); border-radius: 999px; }
.meter .fill.warn { background: var(--warning-fill); }
.meter .fill.over { background: var(--danger); }
.meter .read { color: var(--muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }

/* ==========================================================================
   Tables
   ========================================================================== */
/* The wrapper scrolls sideways only when the columns genuinely do not fit.
   Below that width a scrolling wrapper would also become the sticky header's
   scrollport, pushing the header down over the first row - so the desktop
   layout keeps the wrapper open and lets the page scroll. */
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; border-radius: var(--radius); }

table {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
}

/* Rounded corners come from the corner cells; the table itself must not clip,
   because overflow would trap the sticky header. */
thead tr:first-child th:first-child { border-start-start-radius: var(--radius); }
thead tr:first-child th:last-child { border-start-end-radius: var(--radius); }
tbody tr:last-child td:first-child { border-end-start-radius: var(--radius); }
tbody tr:last-child td:last-child { border-end-end-radius: var(--radius); }

th, td {
    text-align: start;
    padding: var(--row-pad);
    border-bottom: 1px solid var(--line);
    white-space: nowrap;
}

tbody tr:last-child td { border-bottom: 0; }
th { background: var(--surface-2); font-weight: 650; color: var(--muted); font-size: var(--text-sm); letter-spacing: .02em; }
tbody tr:hover { background: var(--surface-2); }
/* "plaintext" keeps the digits and their sign in one left-to-right run while the cell itself
   stays in the mirrored alignment: in an Arabic table a negative quantity would otherwise
   render as "12.50-" with the minus on the far side. */
td.numeric, th.numeric { text-align: end; font-variant-numeric: tabular-nums; unicode-bidi: plaintext; }
td b, td strong { font-weight: 650; }

/* Column sorting: the header is a button, so keyboard and screen readers get it too. */
th.sortable { padding: 0; }
th.sortable button {
    display: flex;
    align-items: center;
    gap: 6px;
    width: 100%;
    padding: var(--row-pad);
    border: 0;
    background: transparent;
    color: inherit;
    font: inherit;
    letter-spacing: inherit;
    text-align: start;
    cursor: pointer;
}

th.sortable.numeric button { justify-content: flex-end; }
th.sortable button::after { content: "↕"; color: var(--marker); font-size: var(--text-2xs); }
th.sortable button:hover { color: var(--accent); }
th[aria-sort="ascending"] button::after { content: "↑"; color: var(--accent); }
th[aria-sort="descending"] button::after { content: "↓"; color: var(--accent); }

/* Sticky header keeps the column names visible while a long list scrolls.
   Only where the wrapper does not scroll, otherwise the header would stick
   inside the wrapper's scrollport and cover the first row. */
@media (min-width: 1100px) {
    .table-wrap { overflow: visible; }
    .sticky-head thead th { position: sticky; top: calc(var(--topbar-h) + var(--safe-top) - 1px); z-index: var(--z-in-page); }
}

.table-caption { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); color: var(--muted); font-size: var(--text-sm); margin: 0 0 var(--space-2); }

/* Pager under a long list. The buttons keep the .btn look so they sit with the rest of the page,
   and the range tells an operator where the page sits in the whole set. */
.pager {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2) var(--space-3);
    margin-top: var(--space-3);
    padding-top: var(--space-2);
    border-top: 1px solid var(--line);
    color: var(--muted);
    font-size: var(--text-sm);
}

.pager-steps { display: flex; align-items: center; gap: var(--space-2); }
.pager-page { white-space: nowrap; }
.btn.is-disabled, .btn[aria-disabled="true"] { opacity: .45; cursor: default; }

/* Below 820px a table is a stack of cards and the column headings are hidden, so the sort
   control is the only way to sort. Above it the headings do the job and this bar stays out. */
.sort-bar { display: none; gap: var(--space-2); align-items: center; margin: 0 0 var(--space-2); }
.sort-bar select { flex: 1 1 auto; max-width: none; min-width: 0; }
.sort-bar .sort-direction { min-width: 40px; }

@media (max-width: 820px) {
    .sort-bar { display: flex; }
}

/* Density: an operator standing at a pallet cage reads more rows in the same height.
   The attribute lives on <html> so the pre-paint script can set it before body exists. */
[data-density="compact"] {
    --row-pad: 5px 10px;
    --field-gap: 8px;
    font-size: var(--text-md);
}

[data-density="compact"] .panel { --panel-pad: var(--space-4); }
[data-density="compact"] .card .value { font-size: var(--text-2xl); }

/* ==========================================================================
   Toolbars, buttons, actions
   ========================================================================== */
.toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) 0;
    margin-bottom: var(--space-3);
}

.toolbar .spacer { flex: 1; }
form.inline { display: inline; }

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: 40px;
    padding: 8px 14px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    font-weight: 550;
    text-decoration: none;
    cursor: pointer;
    transition: background var(--dur-fast) ease, border-color var(--dur-fast) ease, color var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}

.btn:hover { border-color: var(--muted); background: var(--surface-2); }
.btn:active { transform: translateY(1px); }
.btn[disabled], .btn.is-busy { opacity: .6; cursor: progress; pointer-events: none; }

/* A slow post has always dimmed its button; the ring shows the click was taken and the page is
   waiting on the server rather than on the operator. It is drawn in the pseudo-element so the
   button's own box and text do not move. */
.btn.is-busy { position: relative; padding-inline-end: 30px; }
.btn.is-busy::after {
    content: "";
    position: absolute;
    inset-inline-end: 9px;
    top: 50%;
    width: 12px;
    height: 12px;
    margin-top: -6px;
    border: 2px solid currentColor;
    border-inline-end-color: transparent;
    border-radius: 50%;
    animation: btn-spin var(--dur-spin) linear infinite;
}

@keyframes btn-spin {
    to { transform: rotate(360deg); }
}
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.btn.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: var(--on-accent); }
.btn.danger { color: var(--danger); border-color: var(--danger-line); background: var(--danger-soft); }
.btn.danger:hover { border-color: var(--danger); }
.btn.link { border-color: transparent; background: transparent; color: var(--accent); padding-inline: 8px; }
.btn.link:hover { background: var(--accent-soft); border-color: transparent; }
.btn.sm { min-height: 32px; padding: 4px 10px; font-size: var(--text-sm); }

/* Press and inert states for the controls that are not .btn. A warehouse tablet gives no hover, so
   an icon button, a rail toggle, a chip, a tab or a pager step has to answer a press the same way a
   button does, and a disabled one has to read as inert rather than merely quieter. The keyboard is
   already covered by the global :focus-visible ring above; this is what the thumb gets. */
.icon-btn:active, .nav-toggle:active, .chip:active, .tab:active, .pager .btn:active { transform: translateY(1px); }
.icon-btn:disabled, .nav-toggle:disabled, .chip.is-disabled, .tab.is-disabled {
    opacity: .45;
    cursor: not-allowed;
    pointer-events: none;
}

.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }

.pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 9px;
    border-radius: 999px;
    background: var(--surface-2);
    border: 1px solid var(--line);
    color: var(--muted);
    font-size: var(--text-xs);
    font-weight: 600;
    white-space: nowrap;
}

.pill.on { background: var(--success-soft); border-color: var(--success-line); color: var(--success); }
.pill.off { background: var(--danger-soft); border-color: var(--danger-line); color: var(--danger); }
.pill.warn { background: var(--warning-soft); border-color: var(--warning-line); color: var(--warning); }
.pill.info { background: var(--accent-soft); border-color: var(--info-line); color: var(--accent); }

/* A signed quantity is one number, sign included, and the sign belongs on the left of it even
   when the page reads right to left. The figures are tabular so a column of +2 / +10 / +105 lines
   up instead of drifting on proportional widths. */
.pill.numeric { direction: ltr; unicode-bidi: isolate; font-variant-numeric: tabular-nums; }

kbd {
    display: inline-block;
    padding: 1px 6px;
    border: 1px solid var(--line);
    border-bottom-width: 2px;
    border-radius: 5px;
    background: var(--surface-2);
    font-family: ui-monospace, Consolas, monospace;
    font-size: var(--text-2xs);
    color: var(--muted);
}

/* ==========================================================================
   Detail lists, forms
   ========================================================================== */
dl.detail {
    display: grid;
    grid-template-columns: minmax(140px, 220px) 1fr;
    gap: var(--space-2) var(--space-4);
    margin: 0;
}

dl.detail dt { color: var(--muted); font-size: var(--text-sm); font-weight: 600; letter-spacing: .02em; }
dl.detail dd { margin: 0; overflow-wrap: anywhere; unicode-bidi: plaintext; }

.field { display: block; margin-bottom: var(--field-gap); }
.field > span { display: block; margin-bottom: 4px; color: var(--muted); font-weight: 600; font-size: var(--text-sm); }
.field input, .field select, .field textarea { width: 100%; max-width: 480px; }
.field .help { display: block; margin-top: 3px; color: var(--muted); font-size: var(--text-xs); font-weight: 400; }
.field .required { color: var(--danger); }

input[type="text"], input[type="search"], input[type="number"], input[type="date"],
input[type="email"], input[type="password"], input[type="tel"], input[type="url"],
select, textarea {
    padding: 8px 10px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    font: inherit;
    color: var(--ink);
    background: var(--surface);
    max-width: 100%;
    transition: border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}

input:hover, select:hover, textarea:hover { border-color: var(--muted); }
input:focus, select:focus, textarea:focus { border-color: var(--accent); outline: none; box-shadow: var(--focus); }
input:disabled, select:disabled, textarea:disabled { background: var(--surface-2); color: var(--muted); cursor: not-allowed; }

/* Read-only is not disabled: the value is the operator's to read and select, just not to change, so
   it keeps full ink contrast and a plain cursor instead of the not-allowed one. The dashed edge says
   "nothing comes out of this box" while the solid one still promises an editable field, and the
   counted quantity on an uncounted stocktake line is exactly this case. */
input[readonly], textarea[readonly] { border-style: dashed; border-color: var(--line); cursor: default; }
input[readonly]:hover, textarea[readonly]:hover { border-color: var(--line); }

input[type="checkbox"], input[type="radio"] { width: 16px; height: 16px; accent-color: var(--accent); }

/* The message a server-side check raised for one field. site.js brings it here from the block at the
   top of the form; with scripting off that block keeps every line, so nothing is lost either way.
   The second rule repeats .field because the label's own span rule would otherwise keep the text
   grey, and a message nobody reads as an error is not a message. */
.field-error {
    display: block;
    margin-top: 3px;
    color: var(--danger);
    font-size: var(--text-sm);
    font-weight: 600;
    white-space: normal;
}

.field > .field-error { color: var(--danger); margin-bottom: 0; }

.is-invalid { border-color: var(--danger); }
.is-invalid:focus { border-color: var(--danger); box-shadow: 0 0 0 3px rgba(180, 35, 24, .3); }

.field:has(input:focus), .field:has(select:focus), .field:has(textarea:focus) { }

label:has(input[type="checkbox"]) { display: inline-flex; align-items: center; gap: var(--space-2); min-height: 40px; cursor: pointer; }

.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); gap: 0 var(--space-4); }
.grid-3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); gap: 0 var(--space-4); }

/* Long forms keep their save/reject controls in reach without a scroll back to the top.
   The negative margins are taken from the panel so the bar spans the full card at every density. */
.form-actions {
    position: sticky;
    bottom: 0;
    z-index: var(--z-in-page);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3) calc(var(--space-4) + var(--safe-inline));
    /* The bar sticks to the bottom of the window, which is exactly where a phone draws its gesture
       strip, so the inset is paid back as padding rather than leaving Save under the strip. */
    padding-block-end: calc(var(--space-3) + var(--safe-bottom));
    margin: var(--space-4) calc(var(--panel-pad) * -1) calc(var(--panel-pad) * -1);
    background: var(--surface);
    border-top: 1px solid var(--line);
    border-radius: 0 0 var(--radius) var(--radius);
}

.empty {
    display: grid;
    justify-items: center;
    gap: var(--space-2);
    padding: var(--space-6);
    text-align: center;
    color: var(--muted);
    background: var(--surface);
    border: 1px dashed var(--line);
    border-radius: var(--radius);
}

.empty .ico { color: var(--muted); opacity: .6; }
.empty p { margin: 0; max-width: 46ch; }

/* ==========================================================================
   Pictures: a thumbnail in a list, a gallery on the details screen.
   ========================================================================== */

.thumb {
    display: block;
    width: 44px;
    height: 44px;
    object-fit: cover;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface-2);
}

.thumb-empty {
    display: grid;
    place-items: center;
    color: var(--muted);
    opacity: .55;
}

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

.picture {
    margin: 0;
    display: grid;
    gap: var(--space-2);
    padding: var(--space-3);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface);
}

.picture img {
    width: 100%;
    height: 150px;
    object-fit: cover;
    border-radius: var(--radius);
    background: var(--surface-2);
}

.picture figcaption {
    display: grid;
    gap: 2px;
    font-size: var(--text-xs);
}

.picture figcaption .pill { justify-self: start; }

.hint { color: var(--muted); font-size: var(--text-xs); margin: var(--space-2) 0 0; }

/* ==========================================================================
   Reports: the filters on top and the charts under them.
   ========================================================================== */

.report-filter {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-3);
    margin-bottom: var(--space-5);
}

.report-filter .field { min-width: 160px; }

.chart-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
    gap: var(--space-4);
    margin-bottom: var(--space-5);
}

.chart-panel { margin-bottom: 0; }
.chart-panel.wide { grid-column: 1 / -1; }

.chart-box {
    position: relative;
    height: 260px;
    margin-top: var(--space-3);
}

.chart-panel.wide .chart-box { height: 300px; }

.chart-box canvas { max-width: 100%; }

.panel-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}

.panel-head h2 { margin: 0; }

.error { color: var(--danger); }
.error-summary { border-color: var(--danger-line) !important; background: var(--danger-soft) !important; color: var(--danger); }
.error-summary .field { color: var(--ink); }

/* ==========================================================================
   Flash messages as toasts: they appear, they leave, they never block a form.
   ========================================================================== */
.toast-region { position: fixed; inset-block-end: calc(var(--space-4) + var(--safe-bottom)); inset-inline-start: calc(var(--space-4) + var(--safe-inline)); z-index: var(--z-toast); display: grid; gap: var(--space-2); width: min(420px, calc(100vw - (var(--space-4) + var(--safe-inline)) * 2)); }

.toast {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: 12px 14px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface);
    box-shadow: var(--shadow-lg);
    border-inline-start: 4px solid var(--accent);
    animation: toast-in var(--dur-fast) ease-out;
}

.toast.ok { border-inline-start-color: var(--success); }
.toast.error { border-inline-start-color: var(--danger); }
.toast .msg { flex: 1; }
.toast .close { border: 0; background: transparent; color: var(--muted); cursor: pointer; font-size: var(--text-tap); line-height: 1; padding: 2px 4px; }
.toast.is-leaving { animation: toast-out var(--dur-fast) ease-in forwards; }

@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes toast-out { to { opacity: 0; transform: translateY(8px); } }

/* Banner stays for anything that must be read in place (server-side validation panels). */
.banner {
    margin: 0 0 var(--space-4);
    padding: 12px 14px;
    border-radius: var(--radius);
    border: 1px solid var(--line);
    background: var(--surface);
}

.banner.ok { border-color: var(--success-line); background: var(--success-soft); color: var(--success); }
.banner.error { border-color: var(--danger-line); background: var(--danger-soft); color: var(--danger); }

/* A banner that puts an action on the far end of the row (linked order on a stock document). */
.banner.split {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}

.banner.split .help { display: block; margin-top: 2px; }

/* Recent scans on the scanner page: a compact row of tap targets above the form. */
.chips { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin: 0 0 var(--space-3); }
.chips .label { color: var(--muted); font-size: var(--text-md); }

.chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 5px 10px;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: var(--surface-2);
    color: var(--ink);
    font-size: var(--text-md);
    font-variant-numeric: tabular-nums;
    text-decoration: none;
}

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

/* ==========================================================================
   Tabs (switching a screen between two views of the same data)
   ========================================================================== */
.tabs {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    margin: 0 0 var(--space-3);
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--line);
}

.tab {
    padding: 7px 14px;
    border-radius: 999px;
    border: 1px solid transparent;
    color: var(--muted);
    font-size: var(--text-base);
    font-weight: 600;
    text-decoration: none;
}

.tab:hover { color: var(--accent); background: var(--accent-soft); }

.tab.on {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--on-accent);
}

/* ==========================================================================
   Scan bar (reading a barcode straight into a stock document)
   ========================================================================== */
.scan-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-3);
    margin: 0 0 var(--space-3);
    padding: var(--space-3);
    border: 1px dashed var(--line);
    border-radius: var(--radius);
    background: var(--surface-2);
}

.scan-bar .field { flex: 1 1 260px; margin: 0; }
.scan-bar .help { flex: 0 1 auto; margin: 0; }
.scan-bar .help.is-error { color: var(--danger); font-weight: 600; }
.scan-bar .help.is-ok { color: var(--success); font-weight: 600; }

/* The lookup is a round trip, and the field is empty while it runs, so the bar has to say
   that something is happening rather than look ready for the next read. */
.line-scan.is-busy {
    border-color: var(--accent);
    animation: scan-pulse var(--dur-pulse) ease-in-out infinite;
}

@keyframes scan-pulse {
    50% { box-shadow: var(--focus); }
}

/* ==========================================================================
   Modal (destructive confirmations, keyboard shortcut help)
   ========================================================================== */
.modal[hidden] { display: none; }

.modal {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    display: grid;
    place-items: center;
    padding: var(--space-4);
    background: rgba(29, 41, 57, .55);
}

.modal .box {
    width: min(520px, 100%);
    display: grid;
    gap: var(--space-4);
    padding: var(--space-5);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: var(--shadow-lg);
}

.modal h2 { margin: 0; }
.modal p { margin: 0; color: var(--muted); }
.modal .box .actions { justify-content: flex-end; }
.modal table { box-shadow: none; }

/* ==========================================================================
   Icons
   ========================================================================== */
.ico { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; flex: 0 0 auto; }
.ico.lg { width: 26px; height: 26px; }
.sprite { display: none; }

/* Present for assistive tech, absent from the screen. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ==========================================================================
   Barcode scanning
   ========================================================================== */
.scan-input {
    flex: 1;
    max-width: 520px;
    padding: 14px 16px;
    font-family: ui-monospace, Consolas, "Courier New", monospace;
    font-size: var(--text-xl);
    letter-spacing: .06em;
}

.scan-hit { display: grid; gap: var(--space-2); }
.scan-hit .row { display: flex; justify-content: space-between; gap: var(--space-3); border-bottom: 1px dashed var(--line); padding-bottom: 6px; }
.scan-hit .row:last-child { border-bottom: 0; }

.rtl-text { direction: rtl; unicode-bidi: isolate; text-align: right; }

/* The second language of a bilingual name, shown under the primary one. */
.alt-name { display: block; color: var(--muted); font-size: var(--text-xs); }

/* Printable barcode label sheets. Sizes come from the --label-w/--label-h/--label-cols
   custom properties the view sets, so one rule covers every preset. */
.label-sheet {
    display: grid;
    grid-template-columns: repeat(var(--label-cols, 2), var(--label-w, 70mm));
    gap: 2mm;
    justify-content: start;
}

.label-sheet .label {
    width: var(--label-w, 70mm);
    height: var(--label-h, 50mm);
    padding: 3mm;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1mm;
    background: #fff;
    border: 1px dashed #98a2b3;
    border-radius: 2mm;
    overflow: hidden;
    break-inside: avoid;
}

.label-sheet .label-barcode { width: 100%; max-height: 55%; height: auto; image-rendering: pixelated; }

.label-sheet .label-name {
    font-size: 10pt;
    font-weight: 600;
    line-height: 1.2;
    text-align: center;
    max-width: 100%;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.label-sheet .label-meta {
    display: flex;
    justify-content: space-between;
    gap: 4px;
    width: 100%;
    font-size: 8pt;
    color: var(--muted);
}

.label-sheet .label-value { font-family: ui-monospace, Consolas, monospace; }

.sheet-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; padding-bottom: 8px; }

/* Camera scanning on phones and tablets: the video replaces the scanner gun that a
   desktop warehouse uses. The strip only appears once a stream is running. */
.scan-camera { margin-top: var(--space-3); }
.scan-camera video { width: 100%; max-width: 520px; aspect-ratio: 4 / 3; background: #101828; border-radius: var(--radius); object-fit: cover; }
.scan-camera .hint { color: var(--muted); margin: 6px 0 0; }
.scan-camera.is-hidden { display: none; }

.skip-link {
    position: absolute;
    inset-inline-start: -9999px;
    top: 0;
    background: var(--surface);
    padding: 10px 14px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    z-index: var(--z-skip-link);
}

.skip-link:focus { inset-inline-start: 8px; }

/* The language switch sits at the end of the nav; the active language reads as a chip, the other
   as a button that posts the choice and returns to the same page. */
.nav-lang { display: flex; align-items: center; gap: 6px; margin-inline-start: auto; }

.lang-link, .lang-current {
    font: inherit;
    padding: 8px 10px;
    border-radius: var(--radius-sm);
    border: 1px solid transparent;
    background: none;
    color: var(--muted);
    cursor: pointer;
    white-space: nowrap;
}

.lang-link { border-color: var(--line); }
.lang-link:hover { background: var(--surface-2); color: var(--ink); }
.lang-current { background: var(--surface-2); color: var(--ink); font-weight: 600; }

/* Everything above this file already uses logical properties, so dir="rtl" flips the layout on its
   own. Only two things need stating: the Arabic font stack, and the runs that must keep reading
   left to right - barcodes, SKUs and numbers. */
[dir="rtl"] body { font-family: "Segoe UI", "Noto Naskh Arabic", "Tahoma", system-ui, -apple-system, sans-serif; }

[dir="rtl"] .scan-input,
[dir="rtl"] .label-value,
[dir="rtl"] input[name="barcodeValue"],
[dir="rtl"] input[name="code"],
[dir="rtl"] input[inputmode="numeric"] { direction: ltr; text-align: left; }

/* Phones and small tablets: the rail becomes an off-canvas drawer and a record reads as a card,
   one field per line. A nine-column table cannot be scanned on a 360 px screen, and a warehouse
   operator is holding a phone. */
@media (max-width: 960px) {
    .nav-toggle { display: inline-flex; min-height: 44px; margin-inline-start: 0; }

    .sidebar {
        position: fixed;
        inset-block: 0;
        inset-inline-start: 0;
        top: 0;
        z-index: var(--z-drawer);
        width: min(320px, 86vw);
        max-height: 100vh;
        max-height: 100dvh;
        box-shadow: var(--shadow-lg);
        transform: translateX(-100%);
        transition: transform var(--dur-base) ease;
    }

    /* In the drawer the whole rail scrolls, not an inner box. The drawer starts at the physical top
       of the screen, so on a notched phone the close row sat under the glass and the last link under
       the gesture strip; both insets are paid back as padding, and `100dvh` above is what keeps the
       panel to the height the browser is actually showing. */
    .nav-inner {
        position: static;
        max-height: none;
        height: 100%;
        padding-top: calc(var(--space-4) + var(--safe-top));
        padding-bottom: calc(var(--space-4) + var(--safe-bottom));
        overflow: auto;
    }

    [dir="rtl"] .sidebar { transform: translateX(100%); }
    body.nav-open .sidebar { transform: none; }
    .sidebar a { min-height: 44px; padding: 12px var(--space-3); }
    summary.nav-group { min-height: 44px; padding: 10px var(--space-2); }
}

/* Phones: the breadcrumb repeats what the page heading already says, so the top bar
   keeps its height for search and actions instead. */
@media (max-width: 600px) {
    .topbar .crumbs { display: none; }
    /* The word comes off the button but not out of its name: the icon is decorative, so a
       display:none label would leave a screen reader announcing an unnamed control. */
    .nav-toggle .nav-label {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
    }
}

@media (max-width: 820px) {
    .topbar-cluster { margin-inline-start: auto; }
    .topbar .crumbs { order: 3; flex-basis: 100%; }

    /* The header is hidden visually but stays in the accessibility tree, and every cell
       repeats it through data-label so a stacked row still reads as a record. */
    .stack thead {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
    }

    .stack, .stack tbody, .stack tr, .stack td { display: block; width: auto; }
    .stack { border: 0; background: transparent; box-shadow: none; }

    .stack tr {
        border: 1px solid var(--line);
        border-radius: var(--radius);
        margin-bottom: 10px;
        padding: 2px 12px;
        background: var(--surface);
        box-shadow: var(--shadow-sm);
    }

    .stack td {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-4);
        min-height: 44px;
        padding: 10px 0;
        border-bottom: 1px dashed var(--line);
        white-space: normal;
        text-align: end;
    }

    .stack td:last-child { border-bottom: 0; }
    .stack td::before { content: attr(data-label); color: var(--muted); font-weight: 600; text-align: start; }
    .stack td img { max-width: 60%; height: auto; }
    .stack .actions { justify-content: flex-end; }
    .stack th.sortable { display: none; }

    /* Matching the base rule's specificity, otherwise its `font: inherit` wins and iOS
       zooms the viewport when a field is focused. */
    input[type="text"], input[type="search"], input[type="number"], input[type="date"],
    input[type="email"], input[type="password"], input[type="tel"], input[type="url"],
    select, textarea, button { font-size: var(--text-tap); }

    .btn { min-height: 44px; flex: 0 0 auto; }
    .icon-btn { width: 44px; height: 44px; }
    .card .value { font-size: var(--text-2xl); }
    .scan-input { font-size: var(--text-tap); padding: 12px; }

    dl.detail { grid-template-columns: 1fr; gap: 0; }
    dl.detail dt { font-size: var(--text-2xs); letter-spacing: .05em; text-transform: uppercase; margin-top: var(--space-2); }
    dl.detail dd { margin-bottom: 4px; }

    .field input, .field select, .field textarea { max-width: 100%; }
    /* The tighter corner still has to pay the inset back - this is the breakpoint where the phone's
       home indicator and notch actually apply, so dropping it here would hide the toast exactly where
       the operator is most likely to be reading one. */
    .toast-region { inset-block-end: calc(12px + var(--safe-bottom)); inset-inline: calc(12px + var(--safe-inline)); width: auto; }
}

/* Touch devices have no hover, so a highlighted row would be a lie. */
@media (hover: none) {
    tbody tr:hover { background: transparent; }
    .card:hover, .tile:hover { transform: none; }
}

/* Touch: a phone on a pallet cage or a wristed scanner, where a 32px target is a mis-tap. The
   compact controls keep their tight look under a mouse and grow here, and the selectors name the
   same classes the desktop rules do so this block, being later in the file, wins over them. */
@media (max-width: 820px), (pointer: coarse) {
    .btn.sm { min-height: 44px; padding: 8px 12px; font-size: var(--text-md); }
    .icon-btn { width: 44px; height: 44px; }
    .nav-toggle { min-height: 44px; }
    .tab { min-height: 44px; padding: 11px 16px; }
    .chip { min-height: 44px; padding: 10px 14px; }
    .lang-link, .lang-current { min-height: 44px; padding: 11px 12px; }
    .inline-check { min-height: 44px; }
}

/* Forced colours (Windows High Contrast, macOS Increase contrast): the OS repaints text, borders
   and backgrounds and throws away the ones it does not own, so every state this design draws with
   a box-shadow or a fill has to say it again with a border the OS will honour. */
@media (forced-colors: active) {
    :where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
        outline: 2px solid Highlight;
        outline-offset: 2px;
        box-shadow: none;
    }

    /* Where the operator is standing in the rail is a tint plus an inset shadow, both of which the
       OS discards. The links carry no border otherwise, so it is drawn here. */
    .sidebar a[aria-current="page"] { box-shadow: none; border: 1px solid Highlight; }

    .icon-btn[aria-pressed="true"] { border-color: Highlight; }

    /* A rail the operator put away is shown by a tinted button; the OS discards the tint, so the
       button keeps a border for as long as the rail is hidden. */
    html[data-nav="hidden"] .nav-toggle { border: 1px solid Highlight; }

    /* A toast tells ok from error with a coloured edge; the border keeps the box legible whatever
       the OS does to that colour, and the role= status/alert text still carries the meaning. */
    .toast { border: 1px solid CanvasText; }

    .skip-link:focus { border-color: Highlight; }
}

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

/* A4 portrait, 10 mm margins: 210 x 297 mm paper leaves a 190 x 277 mm printable area.
   Labels and stock documents are laid out inside that box and never rely on the browser's
   own scaling, so the printed sheet matches the on-screen millimetres. */
@media print {
    @page { size: A4 portrait; margin: 10mm; }

    /* Paper is light, whatever the operator reads on screen. The themes redefine these tokens and
       nothing else, so redefining them here is what sends a receipt printed from the dark theme to
       the printer as black ink on white rather than as a #1d2939 panel with `color: #000` written
       over it - which `print-color-adjust: exact` on the line below would faithfully put on paper.
       The values are the light theme's own, since those were picked for white, plus a `--line` grey
       dark enough to survive toner. `:root:root` doubles the specificity so this also beats the
       `prefers-color-scheme` block that styles the page when no theme is chosen. */
    :root:root {
        --accent: #0b7a52;
        --accent-hover: #065f3e;
        --accent-soft: #d7f4e6;
        --success: #2f7d32;
        --success-soft: #eef7e2;
        --success-line: #cfe3ab;
        --danger: #b42318;
        --danger-soft: #fef3f2;
        --danger-line: #f3c2bc;
        --info-line: #bfe8d8;
        --warning: #b54708;
        --warning-soft: #fef6e3;
        --warning-line: #f2d29b;
        --ink: #1d2939;
        --muted: #475467;
        --line: #c3cad5;
        --marker: #667085;
        --bg: #ffffff;
        --surface: #ffffff;
        --surface-2: #f4f6f8;
        --shadow-sm: none;
        --shadow: none;
        --shadow-lg: none;
        --on-accent: #ffffff;
        color-scheme: light;
    }

    html, body {
        width: 190mm;
        min-width: 0;
        margin: 0;
        padding: 0;
        background: var(--surface);
        color: var(--ink);
        print-color-adjust: exact;
        -webkit-print-color-adjust: exact;
    }

    body { font-size: 10.5pt; }
    .topbar, .sidebar, .footer, .no-print, .toast-region, .scrim, .skip-link,
    .btn, button, .toolbar, .form-actions, .pager, .sort-bar { display: none !important; }

    /* Collapse the app shell so the document starts at the top-left of the page box. */
    .shell, .frame, .content {
        display: block;
        width: auto;
        max-width: none;
        min-height: 0;
        margin: 0;
        padding: 0;
        overflow: visible;
    }

    .content > * { max-width: 100%; }
    .page-head { margin-bottom: 6mm; }
    .panel, .card { box-shadow: none; border-color: var(--line); break-inside: avoid; }
    .table-wrap, .sheet-scroll { overflow: visible !important; }

    /* Repeat table headers on every printed page and keep rows whole. */
    table { width: 100%; box-shadow: none; border-collapse: collapse; font-size: 9pt; }
    thead { display: table-header-group; }
    tfoot { display: table-footer-group; }
    tr { break-inside: avoid; page-break-inside: avoid; }
    th, td { padding: 4px 6px; }

    /* An A4 content box is ~718 CSS px wide, which would otherwise pick up the phone
       breakpoint and print every record as a stacked card. Print keeps real rows. */
    .stack { display: table; width: 100%; box-shadow: none; }
    .stack thead { display: table-header-group; position: static; width: auto; height: auto; padding: 0; border: 0; }
    .stack tbody { display: table-row-group; }
    .stack tr { display: table-row; box-shadow: none; border: 0; border-radius: 0; margin: 0; padding: 0; }
    .stack th, .stack td {
        display: table-cell;
        width: auto;
        min-height: 0;
        padding: 4px 6px;
        border: 0;
        border-bottom: 1px solid var(--line);
        text-align: start;
        white-space: normal;
    }
    .stack td::before { content: none; }
    .stack th.sortable { display: table-cell; }

    a { color: inherit; text-decoration: none; }
    img { print-color-adjust: exact; -webkit-print-color-adjust: exact; }

    /* Cut-ready label sheets: no clipping, no page split inside a single label. */
    .label-sheet { width: 190mm; justify-content: start; }
    .label-sheet .label {
        border-color: var(--line);
        break-inside: avoid;
        page-break-inside: avoid;
    }
    .label-sheet .label + .label { break-before: auto; }
}

/* Stock-document line editor: several controls share a row, so each one keeps a usable
   minimum without widening the table beyond the viewport. */
.stock-lines .table-wrap { margin-bottom: var(--space-3); }
.stock-lines td select,
.stock-lines td input { min-width: 7rem; }
.stock-lines .line-quantity,
.stock-lines .line-price,
.stock-lines .line-tax { min-width: 5rem; max-width: 8rem; }
.stock-lines .line-actions { white-space: nowrap; }
.stock-lines .actions { flex-wrap: wrap; gap: var(--space-2); }

/* BOM component editor: same reasoning as the stock line editor. */
.bom-lines .table-wrap { margin-bottom: var(--space-3); }
.bom-lines td select,
.bom-lines td input { min-width: 7rem; }
.bom-lines .bom-qty,
.bom-lines .bom-cost { min-width: 5rem; max-width: 8rem; }
.bom-lines .bom-remove { white-space: nowrap; }
.actions .help { color: var(--muted); font-size: var(--text-xs); }

.inline-check { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-sm); color: var(--muted); }
.inline-check input { width: auto; min-width: 0; }
.muted { color: var(--muted); }

/* ==========================================================================
   Sign-in screen: one centred card, no navigation rail
   ========================================================================== */
.auth-body {
    display: grid;
    place-items: center;
    min-height: 100vh;
    min-height: 100svh;
    padding: var(--space-4);
}

.auth-shell { width: min(420px, 100%); }

.auth-card {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    padding: var(--space-5);
}

.auth-head { text-align: center; margin-bottom: var(--space-5); }

.auth-head .brand-mark {
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    margin: 0 auto var(--space-3);
    border-radius: 12px;
    background: var(--accent);
    color: var(--on-accent);
    font-size: var(--text-md);
    font-weight: 700;
}

.auth-head h1 { margin: 0 0 2px; font-size: var(--text-2xl); }
.auth-head .subtitle { margin: 0; }

.auth-form { display: grid; gap: var(--space-2); }
.auth-form .field { margin-bottom: var(--space-3); }
.auth-form .field input { width: 100%; max-width: none; }
.auth-form .error-summary ul { margin: 0; padding-inline-start: 18px; }
.auth-form .btn.block { margin-top: var(--space-2); }

.auth-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    margin-top: var(--space-5);
    padding-top: var(--space-4);
    border-top: 1px solid var(--line);
}

.auth-foot .nav-lang { margin-inline-start: 0; }

.btn.block { width: 100%; justify-content: center; }

/* The signed-in operator in the top bar. */
.user-menu {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding-inline-start: var(--space-2);
    border-inline-start: 1px solid var(--line);
}

.user-menu .user-name {
    max-width: 14ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 600;
    font-size: var(--text-sm);
}

@media (max-width: 720px) {
    .user-menu .user-name { max-width: 8ch; }
}