/* Kamatto Hub — app stylesheet.
   Extracted from index.html. Block order below is the original cascade order;
   moving a block can change which rule wins, so keep them in sequence. */

/* ================= faces =================
   Self-hosted from app/static/fonts/, not linked from a font CDN: the CSP in
   app/security.py names no external host, so a Google Fonts link is blocked and
   falls back to the system stack without telling anybody. See the README in
   that folder for licences and for what each face is for.

   font-display: swap. The alternative on a slow tablet connection is a screen
   of invisible text over data that has already arrived. */
@font-face {
  font-family: "Bebas Neue"; font-style: normal; font-weight: 400;
  font-display: swap; src: url("/static/fonts/bebas-neue-400.woff2") format("woff2");
}
@font-face {
  font-family: "Public Sans"; font-style: normal; font-weight: 400;
  font-display: swap; src: url("/static/fonts/public-sans-400.woff2") format("woff2");
}
@font-face {
  font-family: "Public Sans"; font-style: normal; font-weight: 600;
  font-display: swap; src: url("/static/fonts/public-sans-600.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono"; font-style: normal; font-weight: 400;
  font-display: swap; src: url("/static/fonts/jetbrains-mono-400.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono"; font-style: normal; font-weight: 500;
  font-display: swap; src: url("/static/fonts/jetbrains-mono-500.woff2") format("woff2");
}

/* ================= base: variables, layout, sidebar, nav, tables, badges, panels ================= */
  :root {
    /* ---- Kamatto red -------------------------------------------------------
       The brand colour, and the ONE colour that means "this is an action or the
       thing you are on". Filled buttons, the active nav marker, focus rings,
       the primary chart series. Nothing decorative is red, because red at
       volume is what makes a real alert invisible: a console where forty icons
       are red has no way left to say "this shipment failed".

       --accent is kept as the name every existing rule already calls, so the
       whole app repoints by changing these five lines and nothing else. */
    --brand: #c8102e; --brand-hover: #a80d26; --brand-press: #8c0b20;
    --accent: var(--brand);
    /* The tint behind a selected row, an active nav item, an autocomplete
       hover. NEUTRAL on purpose, not pink: these surfaces appear dozens of
       times per screen and a tinted-red ground on each one turns the whole app
       into a warning. The red comes back as the TEXT on top of it.

       Now a warmer tint than it was, because the ground it sits on is bone
       paper rather than cool grey. A cool selection tint on a warm ground reads
       as a rendering artefact rather than a state. */
    --accent-soft: #e9e6e0;
    /* The launcher chip. Brand red, and the ONE place the tinted-red
       ground the note above rules out is right: a 38px chip once per
       tile, on a screen that is nothing but tiles, is a brand mark
       rather than a warning repeated dozens of times per row. */
    --brand-soft: #fbe7ea;

    /* ---- neutrals: INSTRUMENT ----------------------------------------------
       The palette of a car's instrument binnacle, which is a language Kamatto
       already sells into rather than one borrowed from another industry: bone
       paper, a black binnacle for the figures, one red needle, and gauge grey
       for the ticks and labels.

       Light ground, not dark. A cockpit look tempts you into dark mode, and
       these screens are read on tablets on a warehouse floor in daylight all
       shift. The dark is spent where it earns its keep instead: the panels that
       hold the figures.

       One warm family throughout. The greys are warm-neutral to sit with the
       bone, not the cool slate they were: mixing a warm ground with cool greys
       is the single most common way a palette comes apart. */
    --bg: #f2f0ec;              /* bone paper, the page ground */
    --panel: #fbfaf8;           /* the working surface, a shade off white */
    --binnacle: #0e1116;        /* the figure panels, and the rail */
    --binnacle-2: #191d24;      /* a raised surface inside a dark panel */
    --gauge: #8a8f98;           /* ticks, labels, everything secondary on dark */
    /* A translucent line rather than a flat grey. The technique is off
       brex.com, where every border on the page is one low-alpha value: a
       translucent line picks up whatever ground is behind it, so a single token
       reads correctly on paper, on the white working surface and on a tinted
       status row. A flat hex cannot -- on the ground it goes nearly invisible,
       and on a green status row it turns muddy.

       The HUE is not theirs. Theirs is a cool slate, which was right for a
       white page and is wrong on bone: it reads blue against the warm ground.
       This is the binnacle black at low alpha, which stays neutral on every
       surface in the palette because it is the same ink the type is.

       Alpha rather than a solid mix so the two weights stay in proportion: the
       strong one is the same colour at twice the opacity, which is what keeps
       an input's edge and a card's edge recognisably related. */
    --border: rgba(14,17,22,.13); --border-strong: rgba(14,17,22,.26);
    /* Ink, and two steps down. Warm-shifted from the old graphite so it belongs
       to the bone rather than sitting on it. */
    --text: #14171c; --text-2: #43474e; --muted: #666a71;

    /* ---- semantic ----------------------------------------------------------
       Read with the rule above: since brand red owns the filled buttons,
       critical states are never a red fill. They are dark red text on a tinted
       ground, and they always carry a word or an icon as well ("Failed",
       "Low"), so they survive both a red-heavy screen and colour blindness.
       --danger is browner and two stops darker than --brand for that reason;
       --red stays as the name ~15 rules already use. */
    --green: #0e7a4f; --green-soft: #e6f4ec;
    --amber: #96590b; --amber-soft: #fdf3e3;
    --danger: #8e1b12; --danger-soft: #fbeae7; --danger-line: #e7c3be;
    --red: var(--danger);
    /* Alias, for the same reason --red is one: the Driver app was written
       against --red-soft before it shared this stylesheet. Two names for one
       value beats one name and a hardcoded hex in the other file. */
    --red-soft: var(--danger-soft);
    --info: #1f4e8c; --info-soft: #eaeff7;

    /* ---- channel identity --------------------------------------------------
       Data colours, never UI colours. Each one is the marketplace's own, which
       is the point: a Shopee row is recognised before it is read. This is also
       why the UI accent is not orange. Shopee's orange would stop meaning
       Shopee the moment a button was the same colour. */
    --shopee: #ee4d2d; --shopee_sg: #f97316; --lazada: #6b21a8; --tiktok: #111827;
    --shopify: #5a8f2f; --kamatto_pos: #0d9488;
    /* Three of the five fail WCAG AA as TEXT: measured on the review harness,
       Shopee is 3.66:1, Shopify 3.89:1 and POS 3.74:1 at the 13px the .pf
       column uses, where AA wants 4.5:1.

       Darkened to pass on BOTH grounds in this palette, and no further, so a
       Shopee row still reads as Shopee. Both grounds is the part worth writing
       down: bone paper is darker than white, and the first set of values here
       was tuned against white alone and came back under on the paper.

       The undarkened values above stay in use for dots, chips and fills, where
       the colour is decoration beside a label rather than the label itself. */
    --shopee-ink: #c93414; --shopify-ink: #4a7727; --kamatto_pos-ink: #09766d;

    /* ---- type -------------------------------------------------------------
       Three faces, three jobs, all self-hosted from app/static/fonts/ with the
       licences recorded in the README there. Not loaded from a font CDN: the
       CSP in app/security.py names no external host, so a Google Fonts link is
       blocked and the page falls back to the system stack without saying so.

       The system names stay behind each one. A face that has not arrived yet,
       or a browser that refuses the woff2, should land on the closest thing the
       machine has rather than on Times New Roman. */
    --font-sans: "Public Sans", "Segoe UI Variable Text", "Segoe UI",
                 system-ui, -apple-system, BlinkMacSystemFont, Roboto,
                 "Helvetica Neue", Arial, sans-serif;
    /* Every figure, code and identifier in the app. Not a stylistic choice: a
       monospace lines a column of digits up by construction rather than by
       remembering font-variant-numeric, and its slashed zero is the difference
       between 0 and O in a tracking number read off a label. */
    --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "Cascadia Mono",
                 Menlo, Consolas, monospace;
    /* Screen titles, and nothing else. Uppercase-only by design; see the note
       in fonts/README.md about why this one face gets positive tracking when
       everything else in here is tracked tight. */
    --font-display: "Bebas Neue", "Oswald", "Arial Narrow", Impact, sans-serif;

    /* ---- shape ------------------------------------------------------------
       One radius scale, applied by role: inputs and small controls 6-8px,
       cards and panels 10px, overlays 12px. Mixing scales inside one screen is
       the thing that reads as unfinished. */
    /* Each step up one from where this started, matching what brex.com actually
       uses in the page rather than what its token file claims: 6px on small
       chrome, 10px on buttons and inputs (the value on every CTA there), 12px
       on cards and panels (27 uses, the dominant radius on the page), 14px on
       overlays. The old 8px button against a 10px card read as two systems
       rounded slightly differently, which is the thing that makes an interface
       look assembled rather than designed. */
    --r-sm: 6px; --r-md: 10px; --r-lg: 12px; --r-xl: 14px;
    --radius: var(--r-lg);

    /* ---- tracking ---------------------------------------------------------
       One value for the whole scale. brex.com sets -0.02em at every size it
       uses: -1.44px at 72px, -0.96px at 48px, -0.48px at 24px, -0.32px at 16px,
       -0.28px at 14px, -0.24px at 12px. All the same ratio.

       This file had grown five different hand-picked values (-0.022em on the
       page title, -0.01em on panel titles, -0.03em on figures, -0.006em on
       body), which is five people's guesses rather than a scale. A constant
       ratio tightens everything by the same proportion, which is what makes a
       type scale read as one voice. */
    --track: -.02em;
    /* Figures only. A monospace sets every digit on the same advance width, so
       a large number in it is wider than the same number in the text face and
       wants one more step of tightening to sit in the same optical space. Named
       rather than written at the call site, for the same reason --track is. */
    --track-figure: -.03em;

    /* ---- elevation --------------------------------------------------------
       Separation comes from hairlines and ground colour, not shadow: a page of
       twelve shadowed cards has no hierarchy left to spend. Shadows are tinted
       to the ink hue rather than pure black, and reserved for surfaces that
       genuinely float above the page. */
    --shadow: 0 1px 2px rgba(21,25,30,.05);
    --shadow-md: 0 4px 14px rgba(21,25,30,.09);
    --shadow-lg: 0 18px 44px rgba(21,25,30,.16);

    /* ---- motion -----------------------------------------------------------
       One duration for everything. An ops console is used all day by people
       who already know where they are going; anything slower than this is felt
       as lag, not as polish. */
    --dur: 125ms; --ease: ease;

    /* Aliases. The later sections of this file (forms, permission boxes, the
       tablet cards, the scan inputs) were written against --line/--card and
       nothing ever defined them: an undefined var makes the whole declaration
       invalid, so those borders fell back to currentColor — near-black, much
       heavier than --border — and the backgrounds to transparent. That is what
       made every .um-form look unfinished next to the rest of the app. Defined
       as aliases rather than renamed at ~20 call sites so the two names cannot
       drift apart again. */
    --line: var(--border); --card: var(--panel);
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body { font-family: var(--font-sans);
         background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.5;
         /* Tight tracking is what separates a console from a document. At 13-14px
            it buys back the width that dense tables need, and Inter is drawn for
            it. One ratio for the whole scale, --track; see the note in :root.
            cv05/cv08 give the single-storey l and the straight-tailed y, so
            l/1/I stay apart in an SKU column. */
         letter-spacing: var(--track);
         font-feature-settings: "cv05" 1, "cv08" 1;
         -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
  .layout { display: flex; min-height: 100vh; }
  /* The rail reads as chrome, not as content: it sits on the page ground rather
     than on white, so the working surface is unambiguously the panel area to its
     right. */
  /* A COLUMN, so the rail has a top, a middle that scrolls and a bottom that
     stays put. It used to be one scrolling block: the brand, the find box, the
     tree and the collapse control all in normal flow, which put the collapse
     control wherever the tree happened to end — floating under the last item on
     a short nav, below the fold on a long one, and moving as sections opened
     and closed. A control that is somewhere different every time you look for
     it is one you stop looking for.
     Only .nav scrolls now; see its rule below. */
  /* No scrollbar-width here any more: this element does not scroll, so the
     declaration described a bar that could never appear. .nav owns the
     scrolling and its own (absent) bar. */
  .sidebar { width: 224px; background: var(--bg); border-right: 1px solid var(--border);
             padding: 18px 10px 24px; position: sticky; top: 0; height: 100vh;
             display: flex; flex-direction: column; overflow: hidden; }
  .brand { display: flex; align-items: center; gap: 10px; padding: 4px 8px 18px;
           flex: 0 0 auto; }
  /* The monogram box. It carries the letter K AND the real mark on top of it —
     the route answers 204 when the artwork is missing, and the letter is what
     shows through then (see the note in index.html). The red ground and the
     centred glyph are therefore the FALLBACK, not the design; k-logo.svg brings
     its own black plate and covers both. */
  /* 44px square — the same height as .brand-mark, so the brand row is the same
     height collapsed or expanded and Home does not jump when the rail toggles. */
  .brand .logo { width: 44px; height: 44px; border-radius: var(--r-md); background: var(--brand);
                 color: #fff; display: grid; place-items: center; font-weight: 700;
                 font-size: 22px; letter-spacing: var(--track); flex: 0 0 auto;
                 position: relative; overflow: hidden; }
  /* Fills the box and rounds with it. `cover` rather than `contain`: the asset
     is square and so is the box, so the two agree — and if art ever supplies a
     slightly-off-square export, cropping a hair off the plate is better than
     letterboxing the mark inside a red border. */
  .brand .logo img { position: absolute; inset: 0; width: 100%; height: 100%;
                     object-fit: cover; display: block; }
  .brand b { font-size: 14.5px; font-weight: 550; letter-spacing: var(--track); }
  /* THE ONLY THING IN THE RAIL THAT SCROLLS. The brand and the find box are
     pinned above it and the collapse control below it, so thirty-eight nav
     entries scroll past a frame that does not move. min-height: 0 is what makes
     that true — without it a flex child refuses to shrink below its content and
     the whole rail grows instead, which is the state this replaced. */
  /* overflow-x pinned to hidden: a scroll container with `auto` on one axis
     computes the other to `auto` too, which in a 62px rail is a horizontal
     scrollbar under every icon. The only thing that would overflow sideways is
     the 5%-alpha shadow on the active row, and clipping that is invisible.

     SCROLLS, BUT SHOWS NO SCROLLBAR. Expanding a section is the moment the tree
     first overflows, so a bar appeared the instant somebody opened Orders and
     took a strip off the right of every nav label — a control appearing as a
     side effect of reading the menu. The wheel, a trackpad, a touch drag and
     the keyboard all still scroll it; only the painted bar is gone.

     Three declarations because three engines: scrollbar-width for Firefox,
     -ms-overflow-style for legacy Edge, and the pseudo-element for WebKit and
     Blink. Same technique the Driver app's stop chips already use. */
  .nav { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
         scrollbar-width: none; -ms-overflow-style: none; }
  .nav::-webkit-scrollbar { width: 0; height: 0; display: none; }
  /* Nav items carry no ground of their own until hovered. The active one is the
     only marked item on screen, and it is marked twice: red label plus a red
     edge, so it survives being scanned past at a glance. */
  .nav a { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: var(--r-md);
           color: var(--text-2); text-decoration: none; font-weight: 500; cursor: pointer;
           font-size: 13.5px; position: relative;
           transition: background var(--dur) var(--ease), color var(--dur) var(--ease); }
  .nav a:hover { background: var(--accent-soft); color: var(--text); }
  .nav a.active { background: var(--panel); color: var(--brand); font-weight: 600;
                  box-shadow: inset 0 0 0 1px var(--border), var(--shadow); }
  .nav a.active::before { content: ""; position: absolute; left: 0; top: 7px; bottom: 7px;
                  width: 2.5px; border-radius: 0 2px 2px 0; background: var(--brand); }
  .nav .dot { width: 6px; height: 6px; border-radius: 50%; flex: 0 0 auto; }
  .main { flex: 1; min-width: 0; padding: 24px 28px 48px; max-width: 1440px; }
  /* Given a real page header, the h1 does not need to shout. Hierarchy comes
     from the hairline under it and the space around it. */
  .topbar { display: flex; justify-content: space-between; align-items: center;
            gap: 16px; margin-bottom: 20px; padding-bottom: 14px;
            border-bottom: 1px solid var(--border); }
  .topbar h1 { font-size: 21px; font-weight: 550; letter-spacing: var(--track);
              line-height: 1.1; }
  .channels { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
  .chip { display: flex; align-items: center; gap: 6px; padding: 5px 10px; border-radius: 20px;
          background: var(--panel); border: 1px solid var(--border); font-size: 12px; font-weight: 500;
          color: var(--text-2); }
  .chip .dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; }
  .connected { background: var(--green); } .disconnected { background: var(--border-strong); }
  /* auto-fit rather than four fixed columns: the same .tiles row is reused with
     four, five and six figures across the app, and at a fixed four the extras
     wrapped into a ragged short row. */
  .tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
           gap: 12px; margin-bottom: 20px; }
  .tile { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
          padding: 14px 16px 15px; }
  .tile .label { color: var(--muted); font-size: 11px; font-weight: 600; text-transform: uppercase;
          letter-spacing: .05em; }
  /* tabular-nums is the whole reason a column of figures reads as a column: with
     proportional digits a 1 is narrower than a 7 and nothing lines up. */
  .tile .value { font-size: 25px; font-weight: 550; margin-top: 7px; letter-spacing: var(--track);
          line-height: 1.15; font-variant-numeric: tabular-nums; }
  .tile .sub { font-size: 12px; color: var(--muted); margin-top: 3px; }
  .panel { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
           margin-bottom: 18px; }
  .panel h2 { font-size: 14px; font-weight: 550; letter-spacing: var(--track);
              padding: 14px 18px; border-bottom: 1px solid var(--border); }
  .panel .body { padding: 18px; }
  .grid-2 { display: grid; grid-template-columns: 1.6fr 1fr; gap: 20px; }
  canvas { max-height: 260px; }
  /* ==== THE TABLE ========================================================
     Seventy-one tables ship in this app and they are all this one. Two
     variants, and the only difference between them is the horizontal inset:

       .tbl-wrap > table   the data table. Bleeds to the panel's edge, so it
                           insets by --row-x and shares that left edge with
                           the panel header, the toolbar and the pager.
       table.dr-table      the inset table. Lives inside already-padded
                           content (a drawer, a .body), so it insets by
                           --row-x-inset instead — a full --row-x on top of
                           the 18px it already sits in reads as a margin
                           error.

     Everything else — the type, the colours, the hairlines, the alignment,
     the empty row — is shared and lives here. A module that wants a
     different table wants one of these two.

     Standardised 24 Aug 2026. What it replaced: .dr-table carrying its own
     scale (6/8, 13px, th 10px) that no longer differed from the default once
     compact landed; a dead `padding: 10px 18px` on th and `11px 18px` on td,
     both overridden by the token line further down and both misleading
     anybody reading for the real value; 22 of the 71 tables with no
     horizontal-scroll wrapper at all; and 48 hand-written empty rows, no two
     of them alike. */
  table { width: 100%; border-collapse: collapse;
          /* Every table in here has at least one column of quantities, prices or
             dates. Setting tabular figures once on the table is cheaper than
             remembering to set it on the right cells. */
          font-variant-numeric: tabular-nums; }
  /* NOT sticky, and it cannot be. .tbl-wrap sets overflow-x for the sideways
     scroll, which promotes overflow-y to auto and makes the wrap the nearest
     scrollport; the wrap has no height cap, so it never scrolls vertically and
     a `position: sticky` header has nothing to stick inside. It carried
     `position: sticky; top: 0; z-index: 2` for months and never once stuck —
     measured 24 Aug 2026, on a page-scrolled 80-row table.

     A header that genuinely holds needs a height-capped scroller
     (.tbl-wrap { max-height; overflow: auto }), which means nested scrolling
     on every list screen. That is a decision about how this app scrolls, not
     a table detail, so it is not taken here. Opaque ground is kept: it costs
     nothing and it is what a sticky header would need on the day one is
     added. */
  th { text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em;
       color: var(--muted); border-bottom: 1px solid var(--border);
       font-weight: 650; background: var(--panel);
       white-space: nowrap; }
  td { border-bottom: 1px solid var(--border); color: var(--text-2); }
  /* The identifying cell of a row, whatever it happens to be, carries the ink.
     Everything else is secondary text, which is what lets the eye run down one
     column instead of reading every field. */
  td:first-child { color: var(--text); font-weight: 500; }
  tr:last-child td { border-bottom: none; }
  tbody tr:hover td { background: var(--accent-soft); }
  /* The row that says there are no rows. One class, because there were 48 of
     these written by hand and no two agreed: some inherited td:first-child and
     came out bold ink, some nested a .empty div and got 40px of padding on top
     of the cell's own, one set .muted and a 16px inline padding.

     It is a cell, not a row, so it goes on the <td colspan>. Centred and muted
     because it is a statement about the table rather than a value in it, and
     it overrides td:first-child for the same reason. */
  td.tbl-none {
    padding: 30px var(--row-x); text-align: center;
    color: var(--muted); font-weight: 400; font-size: 13px;
  }
  td.tbl-none b { display: block; color: var(--text); font-size: 14px;
                  font-weight: 600; margin-bottom: 4px; }
  .badge { display: inline-block; padding: 2.5px 9px; border-radius: 20px; font-size: 11px;
           font-weight: 600; letter-spacing: var(--track); white-space: nowrap; }
  /* Channel name as text, so the readable variants. See the -ink note in :root. */
  .pf { font-weight: 600; }
  .pf.shopee { color: var(--shopee-ink); } .pf.lazada { color: var(--lazada); } .pf.tiktok { color: var(--tiktok); }
  .pf.shopify { color: var(--shopify-ink); } .pf.kamatto_pos { color: var(--kamatto_pos-ink); }
  /* Ready stock is NOT a channel, and this is the one .pf that must not read
     as one. A build has no marketplace and no buyer; giving it a channel
     colour puts it one glance from being taken for a sale. So it is the only
     .pf with a border and a ground — deliberately the shape of a label rather
     than the shape of the five channel names beside it — in the muted ink, so
     it reads as an annotation on the row and not as a sixth marketplace. */
  /* The courier control only appears when something in view genuinely needs a
     carrier chosen. When nothing does, the space says WHY rather than going
     blank — an absent control reads as a missing feature. */
  /* .proc-courier-ask / .proc-courier-none lived here until 1 Sep 2026. The
     header no longer carries a courier select at all — the Arrange Shipment
     form asks, and only about the orders that need one. */

  /* ---- an order on the Processing screen ---------------------------------
     An order is TWO <tr> elements — a header carrying the columns, and its
     items underneath — but it has to LOOK AND BEHAVE as one row. Two things
     make that true, and both are needed:

       the header drops its bottom border, so no rule runs between them;
       hover and selection are applied to BOTH by data-oid (orders.js), because
       the browser hovers one <tr> at a time and would otherwise cut the order
       in half under the pointer.

     Without the second, the mouse splits every multi-item order visually and it
     reads as two rows that happen to be adjacent. */
  tr.ord-top > td { border-bottom: none; padding-bottom: 4px; }
  tr.ord-items > td { padding-top: 0; }
  tbody tr.ord-top.hov > td, tbody tr.ord-items.hov > td { background: var(--accent-soft); }

  /* The order number, as a control: clicking it opens the same drawer All
     Orders opens. A button rather than a styled span so it is reachable by
     keyboard and announces itself. */
  .ord-ref-btn { font: inherit; font-weight: 600; font-size: 14px; padding: 0;
                 border: none; background: none; color: var(--text);
                 cursor: pointer; text-align: left; }
  .ord-ref-btn:hover { text-decoration: underline; }
  /* The COID on its own line. Nineteen digits run onto the order number makes
     one long string of figures, and somebody reading it out loses their place. */
  .ord-coid { font-size: 11px; color: var(--muted);
              font-family: ui-monospace, monospace; }

  /* A bordered card per item, starting under the ORDER column rather than at
     the table edge — the indent is what says these are the order's contents
     and not rows of their own. The indent comes from an empty leading cell in
     the markup, so it tracks the checkbox column's real width instead of a
     magic number here. */
  .ord-lines { display: flex; flex-direction: column; gap: 6px; }
  .ol { display: grid; grid-template-columns: 24px 1fr auto; gap: 10px;
        align-items: start; padding: 8px 12px; font-size: 13px;
        border: 1px solid var(--border); border-radius: 8px;
        background: var(--panel); }
  /* WHICH LINE — 1, 2, 3 down the order — not the quantity. 600, not 700: the
     design calibration holds ordinary text at 600, and tabular figures plus the
     fixed column are what make it scan, not the weight. */
  .ol-no { font-variant-numeric: tabular-nums; font-weight: 600;
           color: var(--text-2); text-align: right; }
  .ol-name { line-height: 1.35; }
  /* The ISKU under the name, in the mono the rest of the app uses for a code.
     One identifier printed two ways on one screen reads as two things. */
  .ol-sku { display: block; margin-top: 3px; font-size: 12px;
            font-family: ui-monospace, monospace; color: var(--muted); }
  .ol-right { text-align: right; }
  .ol-money { font-variant-numeric: tabular-nums; white-space: nowrap; }
  .ol-rack { font-size: 11px; white-space: nowrap; margin-top: 2px; }

  /* ---- Raise a ready stock build ----------------------------------------
     The mat picker is a RESULTS LIST, not the .ac-list dropdown the Add Order
     line picker uses. That one floats over the form absolutely positioned, and
     this list sits above a table the person is building up — a dropdown would
     cover the lines they have already added, which is the one thing they are
     checking against while they choose the next mat. */
  .rsb-row { display: flex; gap: 12px; flex-wrap: wrap; }
  .rsb-row .fld { display: flex; flex-direction: column; gap: 5px; flex: 1;
                 min-width: 160px; }
  /* ONE FIELD PER LINE, label above it, everything flush left. The two modal
     forms were laid out as pairs across the box (cutter beside sewer, code
     beside courier), which reads as a settings panel rather than as a form
     somebody fills top to bottom — and it is what made the stray centring rule
     so hard to see for what it was. CS asked for "just normal form" against
     SiteGiant's own Create Pick List, which stacks. */
  .kh-form { display: flex; flex-direction: column; gap: 14px; }
  .kh-form .fld { display: flex; flex-direction: column; gap: 5px;
                  align-items: stretch; }
  .kh-form .fld > input, .kh-form .fld > select { width: 100%; }
  /* The optional/hint text under a label, not beside it: beside it the label
     and the hint compete for the same reading position. */
  .kh-form .fld .sk { font-weight: 400; }
  .rsb-results { display: flex; flex-direction: column; gap: 4px; margin: 8px 0; }
  .rsb-results:empty { margin: 0; }
  .rsb-hit { display: flex; flex-direction: column; gap: 2px; align-items: flex-start;
            text-align: left; width: 100%; padding: 8px 10px; cursor: pointer;
            font: inherit; background: var(--panel); color: var(--text);
            border: 1px solid var(--border); border-radius: 6px; }
  .rsb-hit:hover { background: var(--accent-soft); }
  .rsb-hit .sk { font-size: 11px; color: var(--muted);
                font-family: ui-monospace, monospace; }

  /* ---- Assign to Wave: split across benches -----------------------------
     Was one wide table, a row per pair and a column per mat — fine for a
     build with two or three lines, unreadable past that and scrolling
     sideways on a tablet. Pairs are now cards stacked one under the other:
     "+ Add item" only offers what is still unassigned, so a card is built up
     rather than a whole grid divided at once. */
  .wsp-pairs { display: flex; flex-direction: column; gap: 12px; margin-bottom: 12px; }
  .wsp-pair { border: 1px solid var(--border); border-radius: var(--r-md);
             padding: 12px; background: var(--panel);
             transition: border-color .15s ease; }
  .wsp-pair:focus-within { border-color: var(--accent, var(--red)); }
  .wsp-pair-head { display: flex; align-items: center; justify-content: space-between;
                  margin-bottom: 8px; }
  /* Cutter beside Sewer, not stacked — one pair's own two names read as a
     single line here, and a card repeated per pair multiplies the cost of
     stacking them the way an ordinary one-off form does. */
  .wsp-fields { display: flex; gap: 12px; }
  .wsp-fields .fld { display: flex; flex-direction: column; gap: 5px; flex: 1; min-width: 0; }
  .wsp-fields .fld > select { width: 100%; }
  .wsp-items { display: flex; flex-direction: column; gap: 6px; margin: 10px 0; }
  .wsp-item { display: flex; align-items: center; gap: 8px; }
  .wsp-item-name { flex: 1; font-size: 13px; }
  .wsp-item input[type="number"] { width: 72px; }
  .wsp-add { width: 100%; }
  #wave-split-totals { display: flex; flex-wrap: wrap; gap: 4px 14px; }
  #wave-split-totals .ok { color: var(--green); }
  #wave-split-totals .muted { color: var(--muted); }
  /* "+ Add Pair" opens this rather than dropping a blank card onto the list —
     CS: pairs only appear once composed. Above .modal-overlay's z-index (60):
     it sits on top of the Assign to Wave modal that is still open behind it,
     not inside its own scroll, so the popover cannot be scrolled out of view. */
  .wsp-popover-backdrop { position: fixed; inset: 0; background: rgba(15,23,42,.45);
                          z-index: 61; display: flex; align-items: center;
                          justify-content: center; padding: 16px; }
  .wsp-popover { background: var(--panel); border-radius: var(--radius);
                width: 100%; max-width: 480px; max-height: 85vh; overflow-y: auto;
                padding: 18px 20px; box-shadow: var(--shadow-md); }
  .wsp-popover h3 { margin: 0 0 12px; font-size: 15px; }
  .wsp-popover-foot { display: flex; justify-content: flex-end; gap: 10px;
                      margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); }

  /* ---- Assign to Wave: Smart Wave --------------------------------------
     Cutters beside Sewers, each a closed control that reads like the "Give it
     to" select beside it — same border, radius, padding and chevron — and
     opens a menu of checkboxes FLOATING over the form, the way a native
     select's list does, rather than pushing the plan and the deadline down
     (Ryan, 1 Oct 2026). A <details>, so it opens with a tap and no script;
     a click anywhere else closes it (orders.js:waveSmartBind). */
  .swv-pick { display: flex; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
  .swv-pick > .fld { display: flex; flex-direction: column; gap: 5px; flex: 1; min-width: 200px; }
  .swv-drop { position: relative; }
  .swv-drop > summary { list-style: none; cursor: pointer; font-size: 13px;
                        background: var(--panel); border: 1px solid var(--border-strong);
                        border-radius: var(--r-md); padding: var(--ctl-y, 7px) 30px var(--ctl-y, 7px) 10px;
                        line-height: 1.4; position: relative; white-space: nowrap;
                        overflow: hidden; text-overflow: ellipsis; }
  .swv-drop > summary::-webkit-details-marker { display: none; }
  .swv-drop > summary::after { content: ""; position: absolute; right: 12px; top: 50%;
                               width: 6px; height: 6px; margin-top: -5px;
                               border-right: 1.5px solid var(--muted); border-bottom: 1.5px solid var(--muted);
                               transform: rotate(45deg); }
  .swv-drop[open] > summary { border-color: var(--brand); box-shadow: 0 0 0 3px rgba(200,16,46,.14); }
  .swv-menu { position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 5;
              background: var(--panel); border: 1px solid var(--border-strong);
              border-radius: var(--r-md); box-shadow: var(--shadow-md);
              max-height: 240px; overflow-y: auto; padding: 4px 0; }
  .swv-opt { display: flex; align-items: center; gap: 8px; padding: 5px 10px;
             font-size: 13px; cursor: pointer; }
  .swv-opt:hover { background: var(--accent-soft); }
  .swv-opt input { margin: 0; }
  .swv-all { border-bottom: 1px solid var(--border); font-weight: 600; margin-bottom: 2px; }
  /* A warehouse's heading inside the menu: EG's people, then NG's. */
  .swv-group { padding: 6px 10px 2px; font-size: 11px; font-weight: 600;
               letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
  .swv-pts { font-weight: 600; white-space: nowrap; }
  /* One line per order on a pair's card: which order, what is on it, what it
     weighs — so the preview shows how each pair got its points. */
  .swv-orders { list-style: none; margin: 6px 0 0; padding: 0; font-size: 12.5px; }
  .swv-orders li { display: flex; gap: 8px; padding: 2px 0; border-top: 1px dashed var(--border); }
  .swv-orders li:first-child { border-top: 0; }
  .swv-orders .swv-ref { font-family: ui-monospace, monospace; flex: 0 0 auto; }
  .swv-orders .swv-what { flex: 1; color: var(--muted); }
  .swv-err { color: var(--red); }

  .pf.ready_stock { color: var(--muted); border: 1px solid var(--border);
                    border-radius: 4px; padding: 1px 6px; font-size: 11px;
                    background: var(--bg); white-space: nowrap; }
  /* The remark tag (app/order_remarks.py), on the Order Processing row and in
     the order drawer. Tinted ground plus dark text of the same hue, the same
     way .st-* below does it and for the same reason: one of these can sit on
     a row that already carries a payment badge and a channel name, and a
     solid fill would be all anybody sees.

     Urgent is the only one in the warning hue. The other two are things to
     know before packing, not things to hurry — giving all three amber would
     make the colour mean "has a remark", which the badge already says. */
  .rmk { display: inline-block; margin-left: 6px; padding: 1px 7px;
         border-radius: 4px; font-size: 11px; font-weight: 600;
         white-space: nowrap; vertical-align: middle;
         /* An unknown key still renders (see remarkTagBadge) and has to be
            legible, so the neutral ground is on .rmk itself rather than
            arriving only with a per-tag class. */
         background: var(--bg); color: var(--muted);
         border: 1px solid var(--border); }
  .rmk-urgent_order { background: var(--amber-soft); color: var(--amber);
                      border-color: var(--amber-soft); }
  .rmk-customer_confirmation { background: var(--info-soft); color: var(--info);
                               border-color: var(--info-soft); }
  .rmk-affiliate_order { background: var(--green-soft); color: var(--green);
                         border-color: var(--green-soft); }
  /* Order.is_priority — one badge, always reading "Priority" (see
     priorityTagBadge in orders.js). Only ever rendered when the SERVER
     (app/shipping.py:is_priority_order) has already decided the order
     qualifies, so there is exactly one value to colour, unlike .rmk-* which
     needs a colour per tag. */
  .ship-pri { display: inline-block; margin-left: 6px; padding: 1px 7px;
              border-radius: 4px; font-size: 11px; font-weight: 600;
              white-space: nowrap; vertical-align: middle;
              background: var(--info-soft); color: var(--info);
              border: 1px solid var(--info-soft); }
  /* Order status. Tinted ground plus dark text of the same hue, never a solid
     fill: a solid fill in a table column competes with the brand red on the
     buttons above it, and eight solid pills per row is all anybody sees. */
  .st-completed { background: var(--green-soft); color: var(--green); }
  .st-shipped   { background: var(--info-soft); color: var(--info); }
  .st-to_ship   { background: var(--amber-soft); color: var(--amber); }
  .st-unpaid    { background: var(--accent-soft); color: var(--muted); }
  .st-cancelled { background: var(--danger-soft); color: var(--danger); }
  /* nowrap for the same reason as .badge: "Out of stock" broken over two lines
     doubles the row height of every out-of-stock line in a stock list, which is
     exactly the set of rows somebody is scanning for. */
  .flag { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: var(--r-sm);
          white-space: nowrap; }
  .flag.warn { background: var(--amber-soft); color: var(--amber); }
  .flag.low { background: var(--danger-soft); color: var(--danger); }
  .flag.ok { background: var(--green-soft); color: var(--green); }
  /* A trip package's photos in the Trip detail table. Thumbnails rather than
     "Photo 1 · Photo 2" links: settling a damage claim means comparing the
     pre-delivery shot with the post-delivery one, which needs both on screen at
     once instead of two tabs opened in turn. */
  .pod-row { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; margin-top: 4px; }
  .pod-lbl { font-size: 10px; font-weight: 700; color: var(--muted); text-transform: uppercase;
       letter-spacing: .03em; margin-right: 2px; }
  .pod-thumb { display: block; width: 34px; height: 34px; border: 1px solid var(--border);
       border-radius: 5px; overflow: hidden; line-height: 0; }
  .pod-thumb img { width: 100%; height: 100%; object-fit: cover; }
  /* Optional accent on a tile's number. The Trip list's own tiles are plain, and
     these stay the same card — only the figure is coloured, where the colour is
     information (a red Failed count is the one an admin is looking for). */
  .tile .value.ok { color: var(--green); } .tile .value.warn { color: var(--amber); }
  .tile .value.low { color: var(--danger); }
  /* A count tile that is also the filter for what it counts (Equipment
     Maintenance, Spare Parts). Only .clickable gets the pointer and the
     pressed border: a display-only tile that looked pressable and did nothing
     is why filterTiles() and summaryTiles() are two helpers and not one. */
  .tiles.clickable .tile { cursor: pointer; }
  .tiles.clickable .tile:hover { border-color: var(--border-strong); }
  .tiles.clickable .tile.active { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
  .tiles.clickable .tile:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
  /* A driver's trip-record tiles: the same .tile as the Trip list, but seven of
     them (two totals plus one per trip status), so auto-fit rather than the
     .tiles default of four fixed columns — at four they wrapped into a ragged
     second row of three. Tighter padding for the same reason. */
  .rec-tiles { grid-template-columns: repeat(auto-fit, minmax(112px, 1fr));
       gap: 12px; margin-bottom: 18px; }
  .rec-tiles .tile { padding: 13px 14px; }
  /* Heading for a screen whose summary sits outside any panel. */
  .rec-hd { font-size: 17px; font-weight: 550; margin: 10px 2px 12px; }
  .toolbar { display: flex; gap: 8px; padding: 14px 18px; border-bottom: 1px solid var(--border);
       flex-wrap: wrap; align-items: center; }
  .toolbar select { cursor: pointer; }
  /* A staff code on the Wave Shipment tab. Mono because it IS a code — the
     thing people type to sign in — and reading it as a word is how EBPV006 and
     EBPV008 stop looking different. */
  .wave-who { font-family: ui-monospace, monospace; font-size: 12.5px; }
  /* n/m, aligned so a column of them can be scanned down rather than read. */
  .wave-count { font-family: ui-monospace, monospace; font-size: 12.5px;
                color: var(--muted); }
  /* A section heading inside the Arrange Shipment form: it groups the
     selection by what each part still needs decided. */
  .arr-h { font-size: 12.5px; font-weight: 600; margin: 16px 0 2px; }
  .arr-h:first-child { margin-top: 0; }
  /* A heading with a count and an (i) after it. As a plain block the three
     stacked, giving a three-line heading over a two-field form with the icon
     orphaned beneath it. baseline, so the glyph sits on the text's line. */
  .arr-h-row { display: flex; align-items: baseline; gap: 6px;
       flex-wrap: wrap; }
  /* The Shopee courier pickup dates in the Arrange Shipment form.
     CARDS, NOT A <select>, and that is the point rather than decoration: this
     is the one control on this screen that books a real courier and cannot be
     undone, so every date on offer is visible at once and the chosen one is
     visibly chosen. A dropdown hides both — what the alternatives were, and
     which one is armed — behind a control people change by scrolling past.
     Nothing is selected until somebody presses it; there is no default. */
  /* The Shopee pickup address, under its own select. The address is what
     PROVES the warehouse name above it is the right place, so it stays on
     screen rather than going behind the (i) — but it is one line, clipped,
     because reading it is verification and not the job. */
  .shp-addr { font-size: 11.5px; color: var(--muted); margin: 4px 0 0;
       overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* One blocked order: its number, then why, in as few words as carry it.
     The sentence is in the row's own (i). */
  .shp-no { display: flex; gap: 6px; align-items: baseline; font-size: 12px;
       color: var(--muted); padding: 3px 0; }
  .shp-no b { font-weight: 600; color: var(--text); }
  .shp-dates { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
       gap: 8px; margin-top: 8px; }
  /* 44px floor. This form is used on the shared warehouse tablets, and a
     mis-tap here books the wrong day rather than costing a scroll. */
  .shp-date { min-height: 44px; display: flex; flex-direction: column;
       align-items: center; justify-content: center; gap: 2px;
       padding: 8px 10px; cursor: pointer; font-size: 12.5px; font-weight: 600;
       background: var(--card); color: var(--text);
       border: 1px solid var(--border); border-radius: 8px; }
  .shp-date:hover { border-color: var(--green); }
  /* CHOSEN, and it has to read as chosen from arm's length on a tablet —
     border AND fill AND a tick, not colour alone, because colour alone is
     what a person standing over a trolley in daylight does not see. */
  .shp-date.is-on { border-color: var(--green); background: var(--green-soft);
       color: var(--green); }
  .shp-date.is-on::after { content: '✓'; font-size: 11px; }
  .shp-date .sk { font-weight: 400; }
  /* "not on a trip", under the courier on a webstore row. AMBER, not red: it
     is work outstanding rather than something wrong — most webstore orders sit
     here for a while before anybody plans the van. */
  .trip-none { color: var(--amber); }
  /* A wave's deadline. Three states, and the two that matter are told apart by
     WEIGHT AND WORDS as well as colour — "3 h late" in bold reads as late on a
     warehouse tablet in daylight, where a red that is one step from the amber
     beside it does not. The state itself is the server's (app/waves.py). */
  .due-late { color: var(--red); }
  /* 600, not 700. The design calibration holds ordinary text at 600 and
     tests/js/design_calibration.test.mjs enforces it — 700 is for a single
     glyph or a tiny capital. What makes "3 h late" read as late is the WORD
     and the colour; the weight is the third of three, not the first. */
  .due-late b { font-weight: 600; }
  .due-soon { color: var(--amber); }
  /* The second deadline under the first in the Ship By cell (orders.js:
     shipByCell) — the fast-handover target and the marketplace's ship-by
     share one cell, whichever is next on top. */
  .dl-sub { display: block; font-size: 11px; color: var(--muted); }
  .dl-sub.due-late { color: var(--red); }
  .wave-due-line { font-size: 12.5px; margin: 6px 0 0; }
  .wave-due-line .sk { font-size: 12px; }
  /* "· sewn" on a Printed row. Its own class because `.ok` has no bare rule in
     this file — it is only ever qualified (.flag.ok, .fcard-pill.ok), so a
     <span class="ok"> renders as plain body text and the one word that says the
     parcel is ready reads the same as the one that says it is not. */
  .sew-done { color: var(--green); }
  .muted { color: var(--muted); } .right { text-align: right; }
  .view { display: none; } .view.active { display: block; }
  .empty { text-align: center; padding: 40px; color: var(--muted); }
  .banner { background: var(--info-soft); border: 1px solid #cfdcee; color: var(--info);
            padding: 10px 14px; border-radius: var(--r-md); margin-bottom: 18px; font-size: 13px; display: flex;
            justify-content: space-between; align-items: center; gap: 12px; }
  .banner button { background: var(--info); color: #fff; border: none; padding: 6px 12px;
            border-radius: var(--r-sm); font-size: 12px; cursor: pointer; font-weight: 600;
            font-family: inherit; white-space: nowrap; }
  /* Inventory module */
  .inv-summary { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; margin-bottom: 16px; }
  .inv-summary .tile { padding: 12px 14px; }
  .inv-summary .value { font-size: 19px; }
  .inv-tabs { display: flex; gap: 4px; margin-bottom: 14px; border-bottom: 1px solid var(--border); }
  .inv-tab { background: none; border: none; padding: 9px 16px; font-size: 13px; font-weight: 500;
             color: var(--muted); cursor: pointer; border-bottom: 2px solid transparent; font-family: inherit; }
  .inv-tab:hover { color: var(--text); }
  .inv-tab.active { color: var(--brand); border-bottom-color: var(--brand); font-weight: 550; }
  .inv-toolbar { display: flex; gap: 10px; padding: 14px 18px; border-bottom: 1px solid var(--border);
                 align-items: center; flex-wrap: wrap; }
  .inv-toolbar input[type=search] { flex: 1; min-width: 220px; }
  .inv-toolbar select { cursor: pointer; }
  /* Activity Log's filter panel (30 Sep 2026): four rows -- Live/Archived +
     sort, search-by + search + module, date range + presets, Clear -- each
     a left/right pair rather than every control wrapping loosely together
     like every other .inv-toolbar. #al-source's own padding fix (29 Sep)
     is folded in here now that it lives inside .al-toolbar instead of
     sitting bare in .panel. */
  #al-filters.al-toolbar { flex-direction: column; align-items: stretch; }
  .al-row { display: flex; align-items: center; justify-content: space-between;
            flex-wrap: wrap; gap: 10px; }
  /* Activity Log's date-range picker (2 Oct 2026) -- one button showing the
     current range opens this as a modal body (openModal(), core.js); picking
     is entirely by calendar, nothing typed, so there is no out-of-range value
     to validate: a span over AL_MAX_RANGE_DAYS (settings.js) simply is not
     selectable, the same bound settings.py's CSV export enforces server-side. */
  .al-cal { width: 280px; max-width: 100%; margin: 0 auto; }
  .al-cal-head { display: flex; align-items: center; justify-content: space-between;
                 margin-bottom: 10px; font-weight: 600; }
  .al-cal-dow, .al-cal-days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
  .al-cal-dow span { text-align: center; font-size: 11px; color: var(--muted); font-weight: 600; }
  .al-cal-days span { aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
                       font-size: 13px; border-radius: var(--r-sm); cursor: pointer; }
  .al-cal-days span.out { visibility: hidden; cursor: default; }
  .al-cal-days span.disabled { color: var(--muted); opacity: .4; cursor: not-allowed; }
  .al-cal-days span:not(.disabled):not(.out):hover { background: var(--accent-soft); }
  .al-cal-days span.in-range { background: var(--accent-soft); border-radius: 0; }
  .al-cal-days span.range-start, .al-cal-days span.range-end { background: var(--brand); color: #fff; }
  .al-cal-days span.range-start { border-radius: var(--r-sm) 0 0 var(--r-sm); }
  .al-cal-days span.range-end { border-radius: 0 var(--r-sm) var(--r-sm) 0; }
  .al-cal-days span.range-start.range-end { border-radius: var(--r-sm); }
  .al-cal-days span.today:not(.range-start):not(.range-end) { box-shadow: inset 0 0 0 1px var(--border); }
  .al-cal-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px;
                 margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); }
  .al-cal-foot .muted { font-size: 12.5px; }
  .inv-check { font-size: 13px; color: var(--muted); display: flex; align-items: center; gap: 5px; cursor: pointer; }
  .inv-count { font-size: 12px; color: var(--muted); margin-left: auto; }
  /* Filter controls that cannot say what they are on their own.
     A search box has a placeholder and a select reads "All Marketplaces", so
     both explain themselves. Two bare date inputs do not: nothing on screen
     said whether they filtered the order date or the packing date, or even
     that the pair belonged together. Every fulfillment screen had that same
     unlabelled pair sitting in a right-aligned actions bar. */
  .inv-toolbar .flt { display: flex; align-items: center; gap: 7px; }
  .inv-toolbar .flt > label { font-size: 11px; color: var(--muted); font-weight: 600;
                 text-transform: uppercase; letter-spacing: .03em; white-space: nowrap; }
  .inv-toolbar input[type=date] { color: var(--text); }
  .flt-range { display: flex; align-items: center; gap: 6px; }
  .flt-range .sep { color: var(--muted); font-size: 12px; }
  /* A control that was drawn but never wired to anything. Left in place rather
     than deleted, the same way an unbuilt tile is greyed rather than hidden:
     the gap stays visible instead of turning into a filter nobody remembers
     was meant to exist. */
  .inv-toolbar [disabled] { opacity: .55; cursor: not-allowed; }
  /* A ROW, NOT A COLUMN, since 20 Sep 2026: the thumbnail sits beside the
     name and the text stacks next to it, the way SiteGiant's own All
     Inventory Items row reads. */
  .inv-item-cell { display: flex; flex-direction: row; align-items: center; gap: 10px; }
  .inv-item-txt { display: flex; flex-direction: column; min-width: 0; }
  .inv-item-cell .nm { font-weight: 500; max-width: 380px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .inv-item-cell .sk { font-size: 11px; color: var(--muted); font-family: ui-monospace, monospace; }
  /* FIXED BOX, so a missing image does not reflow the row. The placeholder is
     the same size as the picture for exactly that reason — 55,258 rows paging
     fifty at a time, and a catalogue where some SKUs have no photo. */
  .inv-thumb { width: 38px; height: 38px; flex: 0 0 38px; border-radius: 4px;
               object-fit: cover; background: var(--bg-soft, #fafafa);
               border: 1px solid var(--border); }
  .inv-thumb-none { display: inline-block; }
  .inv-listings { color: var(--blue, #2563eb); }
  /* THE STOCK COLUMNS MAKE THIS TABLE WIDER THAN THE SCREEN, deliberately —
     five stock states plus brand and category. It scrolls sideways rather
     than dropping columns, because the whole point of the change was that a
     single "Stock" figure implied the other four were zero. The numbers stay
     on one line so a column cannot silently wrap into looking like two. */
  #inv-body td.right, #inv-body + thead th.right { white-space: nowrap; }
  /* ONE LINE PER CELL, so every row is the thumbnail's height (CS, 30 Sep
     2026). The iSKU column used to wrap at every hyphen, so a kit such as
     "(5-Seater) KMT-001-BLK-FULL + KRM-001-PCH" stood seven lines tall
     beside a one-line neighbour. The codes are NOT truncated: an iSKU is
     read character by character and a clipped one looks like another. The
     table already scrolls sideways (above), so the width goes there. The
     item name keeps its own ellipsis (.inv-item-cell .nm). */
  #inv-body td { white-space: nowrap; }
  /* "+ N More SKU(s)" — the variations expander, matching SiteGiant's own.
     A link, not a button-looking control: it reveals detail already on the
     page's subject rather than performing an action, and the row's real
     actions are the red buttons in the header. */
  .cl-varbtn { display: inline-block; margin-top: 3px; padding: 0; border: 0;
               background: none; cursor: pointer; font-size: 11px;
               font-family: inherit; color: var(--blue, #2563eb); }
  .cl-varbtn:hover { text-decoration: underline; }
  /* A variation's Master SKU, and the state that still needs a person.
     `Link…` is deliberately the RED of an unfinished thing rather than the
     blue of an ordinary link: an unlinked variation is stock that cannot move
     when the order for it arrives, so the screen should read as work
     outstanding rather than as an option. */
  .cl-linked { font-family: ui-monospace, monospace; font-size: 11px; }
  .cl-needslink { color: var(--red, #dc2626); }
  /* The struck-through list price, above what a buyer actually pays — the
     way Lazada Seller Center and SiteGiant both show a discounted listing.
     Only rendered when the listing IS discounted; see clPriceCell. */
  .cl-was { display: block; text-decoration: line-through; color: var(--muted);
            font-size: 11px; }
  .cl-now { display: block; }
  .cl-varrow > td { background: var(--bg-soft, #fafafa); }
  .cl-vartable { width: 100%; border-collapse: collapse; font-size: 12px; }
  .cl-vartable th { text-align: left; font-weight: 600; color: var(--muted); }
  .cl-vartable th.right, .cl-vartable td.right { text-align: right; }
  /* The inset variant, for the same reason .dr-table uses it: this table sits
     inside a cell that is already padded, so it insets by less. NOT a second
     scale — see "THE TABLE" in this file; the one that existed was removed. */
  .cl-vartable th, .cl-vartable td { padding: var(--row-y) var(--row-x-inset); }
  /* Graphite, not Lazada purple. A kit is a property of the item, not a
     marketplace, and borrowing a channel colour for it makes a bundled SKU look
     like a Lazada listing in a column where that distinction matters. */
  .kit-tag { display: inline-block; background: var(--text); color: #fff; font-size: 9px; font-weight: 700;
             padding: 1px 5px; border-radius: 4px; margin-left: 6px; vertical-align: middle; }
  .clickrow { cursor: pointer; }
  /* Three groups, not one centred row: rows-per-page on the left, where you are
     in the middle, movement on the right. Reading the middle is the common case
     and pressing the right is the frequent one, so they do not share a position.
     A hairline above, because a pager belongs to the table it ends rather than
     to whatever follows it. */
  .inv-pager { display: flex; align-items: center; justify-content: space-between;
       gap: 12px; padding: 12px 18px; border-top: 1px solid var(--border);
       flex-wrap: wrap; }
  /* The label and its select read as one control. Uppercase and muted so a
     setting does not compete with the figures in the table above it. */
  .pg-size { display: flex; align-items: center; gap: 8px; font-size: 11px;
       font-weight: 600; text-transform: uppercase; letter-spacing: .05em;
       color: var(--muted); white-space: nowrap; }
  .pg-size select { padding: 5px 8px; font-size: 12.5px; font-weight: 550;
       cursor: pointer; text-transform: none; letter-spacing: 0; color: var(--text); }
  .pg-nav { display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
       justify-content: flex-end; }
  .inv-pager button { padding: 6px 12px; border: 1px solid var(--border-strong); background: var(--panel);
                 border-radius: var(--r-sm); font-size: 13px; cursor: pointer; font-family: inherit;
                 color: var(--text); font-weight: 550; }
  .inv-pager button:hover:not(:disabled) { background: var(--accent-soft); }
  .inv-pager button:disabled { opacity: .4; cursor: default; }
  /* "1-15 of 240". Tabular figures because this string changes on every page
     press and proportional digits make it jump sideways as it does. */
  .inv-pager .pg { font-size: 13px; color: var(--text-2); font-weight: 550;
       font-variant-numeric: tabular-nums; white-space: nowrap; }
  /* ABOVE .modal-overlay's z-index (60): an order reference inside an open
     modal — the Wave panel's order list, among others — opens this same
     drawer, and at 40/50 it rendered BEHIND that modal instead of over it,
     so the order details it carries were cut off along the modal's edge. */
  .drawer-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.25); z-index: 62; }
  .drawer-overlay.open { display: block; }
  .drawer { position: fixed; top: 0; right: -480px; width: 460px; max-width: 92vw; height: 100vh;
            background: #fff; box-shadow: -4px 0 24px rgba(0,0,0,.12); z-index: 63; transition: right .2s ease;
            overflow-y: auto; }
  .drawer.open { right: 0; }
  .drawer-body { padding: 22px; }
  .dr-close { position: absolute; top: 16px; right: 18px; border: none; background: none; font-size: 22px;
              color: var(--muted); cursor: pointer; line-height: 1; }
  .dr-name { font-size: 16px; font-weight: 550; margin-bottom: 4px; padding-right: 30px; }
  .dr-sku { font-size: 12px; color: var(--muted); font-family: ui-monospace, monospace; margin-bottom: 16px; }
  .dr-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 18px; margin-bottom: 18px; }
  .dr-grid .k { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .03em; }
  .dr-grid .v { font-size: 14px; font-weight: 500; }
  .dr-sec { font-size: 13px; font-weight: 600; margin: 16px 0 8px; }
  /* The inset variant. Everything it used to declare for itself — the width,
     the collapse, the 13px cell, the hairline — is the shared table now; the
     one thing it still owns is the narrower horizontal inset, because it is
     already sitting inside 18px of padding. See THE TABLE above.

     It does not tint on hover: a line item in a drawer is a reading, not a row
     somebody is about to open. */
  .dr-table th, .dr-table td { padding: var(--row-y) var(--row-x-inset); }
  .dr-table tbody tr:hover td { background: none; }
  /* collapsible nav sections (SiteGiant-style dropdowns) */
  .nav-head { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px;
              font-size: 13px; font-weight: 600; color: var(--text); cursor: pointer; border-radius: 8px;
              margin-top: 4px; user-select: none; }
  .nav-head:hover { background: var(--bg); }
  /* The heading opens the module's landing page, so it takes the same "you are
     here" as an anchor: paper ground, red label, red edge in the gutter. It is
     a heading AND a destination, which is exactly what it looks like. */
  .nav-head.active { background: var(--bg); color: var(--brand); position: relative; }
  .nav-head.active::before { content: ""; position: absolute; left: 0; top: 8px; bottom: 8px;
                  width: 2.5px; border-radius: 0 2px 2px 0; background: var(--brand); }
  /* The chevron is the only part of the heading that still just toggles the
     dropdown, so it needs a hit area rather than a 9px glyph. Negative margin
     keeps the heading the height it was. */
  .nav-head .chev { color: var(--muted); font-size: 15px; transition: transform .18s ease;
                    padding: 4px 6px; margin: -4px -6px; border-radius: var(--r-sm); }
  .nav-head .chev:hover { background: rgba(255,255,255,.09); color: #fff; }
  .nav-sec.open > .nav-head .chev { transform: rotate(90deg); }
  .nav-items { overflow: hidden; max-height: 0; transition: max-height .2s ease; }
  .nav-sec.open > .nav-items { max-height: 500px; }
  .nav-items a { padding-left: 22px; }
  /* ---- buttons ------------------------------------------------------------
     Four roles and no more. Filled red is the one action a screen is FOR
     (Sync, Save, Create). Outline is everything else you might do. Ghost is
     navigation dressed as a button. Danger is outline-only, on purpose: see
     the --danger note in :root. Anything that looks like a fifth role is one
     of these four at .btn-sm size.

     :active nudges the label down a pixel rather than scaling it. On the
     warehouse tablets a tap has no cursor to confirm it landed, and a 1px
     press is the cheapest confirmation there is. */
  .btn-primary { background: var(--brand); color: #fff; border: 1px solid var(--brand);
                 padding: 8px 16px; border-radius: var(--r-md);
                 font-size: 13px; font-weight: 600; cursor: pointer; font-family: inherit;
                 white-space: nowrap;
                 transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease); }
  .btn-primary:hover:not(:disabled) { background: var(--brand-hover); border-color: var(--brand-hover); }
  .btn-primary:active:not(:disabled) { background: var(--brand-press); border-color: var(--brand-press);
                 transform: translateY(1px); }
  .btn-primary:disabled { background: var(--border-strong); border-color: var(--border-strong);
                 color: var(--panel); cursor: default; }
  .btn-mini { background: var(--panel); color: var(--text); border: 1px solid var(--border); padding: 4px 10px;
              border-radius: var(--r-sm); font-size: 12px; font-weight: 600; cursor: pointer;
              float: right; font-family: inherit; }
  .btn-mini:hover { background: var(--accent-soft); }
  .btn-link { background: none; border: 1px solid var(--border); color: var(--text); padding: 4px 10px;
              border-radius: var(--r-sm); font-size: 12px; cursor: pointer; font-family: inherit; }
  .btn-link:hover:not(:disabled) { background: var(--accent-soft); }
  /* Same value as .btn-outline's -- a ticket's Remark button once it's
     Resolved or No Repair Needed (11 Sept 2026), the first .btn-link to
     need this. */
  .btn-link:disabled { opacity: .5; cursor: default; }
  /* A boxed .btn-link reads as a separate control when it sits mid-sentence --
     e.g. Activity Log's "View details" after an import summary -- and wraps
     onto its own line under the text it belongs to. This stays part of the
     sentence: no border or box, coloured like a link, underlined so it still
     reads as clickable without the weight of a button. */
  .txt-link { background: none; border: none; padding: 0; margin-left: 2px;
              color: var(--accent); font: inherit; font-size: inherit; font-weight: 600;
              cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
              white-space: nowrap; }
  .txt-link:hover { color: var(--brand-hover); }
  /* A .btn-link that destroys something, sitting in a row of ones that do not.
     Staff Accounts puts Delete immediately beside Edit and Deactivate, and
     three identical buttons in a row is a misclick waiting to happen. Colour
     only — same size, same position — so it reads as a warning rather than
     moving the target the moment somebody is used to where it is. It borrows
     .btn-danger's tokens rather than restating a red (see the --danger note in
     :root), so a palette change reaches both. */
  .btn-link.danger { color: var(--danger); border-color: var(--danger-line); }
  .btn-link.danger:hover { background: var(--danger-soft); border-color: var(--danger); }
  /* Two actions on one row (Printed: Reprint AWB, Handover) — plain inline
     buttons run together with no gap between their borders, which read as
     cramped once the middot-joined sewing note beside them was dropped. */
  .proc-row-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
  /* One Action button per row opening a dropdown of everything that row
     offers, instead of a wall of separate .btn-links (Nadzrul, 5 Oct 2026,
     first on Spare Parts, meant to be reused on Equipment Maintenance and
     Machine Registry too). A plain <div class="act-menu"> toggled by the
     shared act-menu-toggle action in core.js, not native <details> -- that
     was tried first and dropped the same day over an unreliable toggle
     under browser automation. core.js repositions .act-menu-list with
     `position: fixed` the moment it opens, so the `right: 0`/`top` here are
     only the unopened/no-JS default -- see its own comment for why a plain
     CSS anchor isn't enough on a narrow row. */
  .act-menu { position: relative; display: inline-block; }
  .act-menu-list { display: none; }
  .act-menu.open .act-menu-list {
                   display: flex; flex-direction: column;
                   position: absolute; right: 0; top: calc(100% + 4px); z-index: 50;
                   background: var(--card); border: 1px solid var(--border);
                   border-radius: var(--r-md); box-shadow: 0 8px 24px rgba(0,0,0,.16);
                   padding: 4px; min-width: 170px; }
  .act-menu-list button { all: unset; box-sizing: border-box; width: 100%; padding: 7px 10px;
                   border-radius: var(--r-sm); font-size: 13px; font-family: inherit;
                   color: var(--text); cursor: pointer; white-space: nowrap; }
  .act-menu-list button:hover { background: var(--accent-soft); }
  /* adjustment form */
  .adj-form { display: flex; gap: 14px; flex-wrap: wrap; align-items: flex-start; }
  .adj-form .fld { display: flex; flex-direction: column; gap: 5px; flex: 1; min-width: 130px; }
  .adj-form label { font-size: 11px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: .03em; }
  .adj-form input, .adj-form select { min-width: 0; }
  .ac-list { position: absolute; top: 58px; left: 0; right: 0; background: #fff; border: 1px solid var(--border);
             border-radius: 8px; box-shadow: var(--shadow); z-index: 20; max-height: 240px; overflow-y: auto; display: none; }
  .ac-list.open { display: block; }
  .ac-item { padding: 8px 10px; cursor: pointer; font-size: 13px; border-bottom: 1px solid var(--border); }
  .ac-item:hover { background: var(--accent-soft); }
  .ac-item .sk { font-size: 11px; color: var(--muted); font-family: ui-monospace, monospace; }
  .adj-picked { margin-top: 8px; padding: 8px 10px; background: var(--accent-soft); border-radius: 8px; font-size: 13px; }
  .adj-msg { margin-top: 12px; font-size: 13px; }
  .adj-msg.ok { color: var(--green); } .adj-msg.err { color: var(--red); }
  .adj-msg.warn { color: var(--amber); }
  /* warehouses */
  .wh-layout { display: grid; grid-template-columns: 300px 1fr; gap: 18px; align-items: start; }
  .wh-list .body, .wh-racks .body { padding: 0; }
  .wh-row { padding: 12px 16px; border-bottom: 1px solid var(--border); cursor: pointer; display: flex;
            justify-content: space-between; align-items: center; }
  .wh-row:hover { background: var(--accent-soft); }
  .wh-row.sel { background: var(--accent-soft); box-shadow: inset 2.5px 0 0 var(--brand); }
  .wh-row .wn { font-weight: 500; } .wh-row .wm { font-size: 11px; color: var(--muted); }
  .wh-badge { font-size: 9px; font-weight: 700; background: var(--green); color: #fff; padding: 1px 6px; border-radius: 4px; }
  /* channel listings */
  .cl-chanbar { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
  .cl-chan { padding: 8px 16px; border: 1px solid var(--border); background: #fff; border-radius: 20px;
             font-size: 13px; font-weight: 600; cursor: pointer; color: var(--muted); font-family: inherit; }
  .cl-chan.active { background: var(--text); color: #fff; border-color: var(--text); }
  .st-live { background: var(--green-soft); color: var(--green); } .st-delisted { background: var(--accent-soft); color: var(--muted); }
  .st-reviewing { background: var(--amber-soft); color: var(--amber); } .st-violation { background: var(--danger-soft); color: var(--danger); }
  .st-draft { background: var(--info-soft); color: var(--info); }
  .mp-badge { display: inline-flex; align-items: center; gap: 3px; font-size: 11px; font-weight: 600;
              padding: 2px 7px; border-radius: 12px; margin-right: 4px; background: var(--bg); }
  /* order header actions (SiteGiant-style: top-right of All Orders) */
  .ord-actions { display: flex; justify-content: flex-end; gap: 10px; margin-bottom: 14px; align-items: center; }
  .btn-outline { background: var(--panel); border: 1px solid var(--border-strong); border-radius: var(--r-md);
                 padding: 8px 14px; font-size: 13px; font-weight: 550; cursor: pointer;
                 font-family: inherit; color: var(--text); white-space: nowrap;
                 transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease); }
  .btn-outline:hover:not(:disabled) { background: var(--accent-soft); border-color: var(--muted); }
  .btn-outline:active:not(:disabled) { transform: translateY(1px); }
  .btn-outline:disabled { opacity: .5; cursor: default; }
  /* The compact outline button. Used in nineteen places across the app — Retry
     camera, the drawing bar's Full screen / Close, Select all on QC, Edit on
     the penalty table, the role-set buttons — and defined in none of them, so
     every one rendered as a raw browser button. Noticed while moving QC's
     Select all out of the filter bar, where the mismatch became obvious. */
  .btn-sm { background: var(--panel); border: 1px solid var(--border-strong); border-radius: var(--r-sm);
            padding: 5px 11px; font-size: 12px; font-weight: 550; cursor: pointer;
            font-family: inherit; color: var(--text); white-space: nowrap;
            transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease); }
  .btn-sm:hover:not(:disabled) { background: var(--accent-soft); border-color: var(--muted); }
  .btn-sm:active:not(:disabled) { transform: translateY(1px); }
  .btn-sm:disabled { opacity: .45; cursor: default; }
  /* Reject on QC, and anything else that costs somebody money. It sat as a
     plain .btn-sm next to a blue Approve, so the destructive half of the pair
     was the quieter one. */
  .btn-danger { background: var(--panel); border: 1px solid var(--danger-line); color: var(--danger);
                border-radius: var(--r-md); padding: 8px 14px; font-size: 13px; font-weight: 600;
                cursor: pointer; font-family: inherit; white-space: nowrap;
                transition: background var(--dur) var(--ease); }
  .btn-danger:hover:not(:disabled) { background: var(--danger-soft); border-color: var(--danger); }
  .btn-danger:active:not(:disabled) { transform: translateY(1px); }
  .btn-danger:disabled { opacity: .5; cursor: default; border-color: var(--border);
                color: var(--muted); }
  .btn-proc { padding: 8px 16px; }
  .bulktools-wrap { position: relative; }
  .bulk-menu { display: none; position: absolute; top: 40px; left: 0; background: #fff; border: 1px solid var(--border);
               border-radius: 8px; box-shadow: var(--shadow); z-index: 30; min-width: 180px; overflow: hidden; }
  .bulk-menu.open { display: block; }
  .bulk-menu-hd { padding: 8px 14px; font-size: 11px; color: var(--muted); border-bottom: 1px solid var(--border); font-weight: 600; }
  .bulk-menu a { display: block; padding: 9px 14px; font-size: 13px; cursor: pointer; color: var(--text); }
  .bulk-menu a:hover { background: var(--accent-soft); color: var(--text); }
  /* order processing layout */
  .proc-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
  .proc-head-right { display: flex; gap: 10px; align-items: center; }
  .proc-head-right select { cursor: pointer; }
  .btn-back { background: none; border: none; color: var(--muted); font-size: 13px; cursor: pointer; font-family: inherit; }
  .btn-back:hover { color: var(--text); }
  /* The bar showView() puts above every Fulfillment tool. A block of its
     own rather than margin on .btn-back, so the button keeps working the
     same everywhere else it is used (drawer heads, detail screens). */
  .ful-backbar { margin-bottom: 10px; }
  .proc-layout { display: grid; grid-template-columns: 220px 1fr; gap: 18px; align-items: start; }
  .proc-rail { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px; box-shadow: var(--shadow); }
  .proc-rail-hd { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); padding: 4px 8px; }
  .proc-rail a { display: flex; justify-content: space-between; padding: 8px 10px; border-radius: 7px; font-size: 13px; cursor: pointer; color: var(--text); }
  .proc-rail a:hover { background: var(--bg); }
  .proc-rail a.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
  .proc-rail a .n { color: var(--muted); font-size: 12px; }
  .proc-filters { padding: 14px 18px; }
  /* ---- the Filter panel (Ryan, 1 Oct 2026) -------------------------------
     A button beside the search, opening the four filter rows in a panel that
     floats over the table rather than sitting across the top of the screen.
     Its own footer: Reset, Cancel, Save filter — nothing applies until Save. */
  /* RIGHT UNDER THE BUTTON (Ryan, 1 Oct 2026), its right edge on the
     button's. The button is the last thing on the toolbar, so the panel opens
     leftwards from it over the table and cannot run off the right of the
     screen — which is what it did when it hung off the button's LEFT edge
     back when the button sat beside the search box. */
  .pfp-wrap { position: relative; }
  .pfp-btn { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
  .pfp-n:empty { display: none; }
  .pfp-n { background: var(--brand); color: #fff; border-radius: 999px; font-size: 11px;
           font-weight: 600; min-width: 18px; height: 18px; padding: 0 5px;
           display: inline-flex; align-items: center; justify-content: center; }
  .pfp-pop { position: absolute; top: calc(100% + 6px); right: 0; left: auto; z-index: 30;
             width: min(820px, calc(100vw - 32px)); background: var(--panel);
             border: 1px solid var(--border-strong); border-radius: var(--radius);
             box-shadow: var(--shadow-md); }
  .pfp-pop[hidden] { display: none; }
  .pfp-pop .proc-filters { max-height: 60vh; overflow-y: auto; }
  .pfp-foot { display: flex; align-items: center; gap: 8px; padding: 10px 18px;
              border-top: 1px solid var(--border); }
  .pfp-gap { flex: 1; }

  .pf-row { display: flex; align-items: center; gap: 12px; padding: 6px 0; }
  .pf-lbl { font-size: 12px; color: var(--muted); font-weight: 600; width: 90px; flex-shrink: 0; }
  .pf-chips { display: flex; gap: 6px; flex-wrap: wrap; }
  .pf-chip { padding: 5px 12px; border: 1px solid var(--border); border-radius: 16px; font-size: 12px; cursor: pointer; background: #fff; font-family: inherit; }
  /* Filter chips fill graphite, not red, and match .cl-chan.active. Several are
     on at once by design, and five red chips above a table read as five
     problems. Red is reserved for the button that DOES something. */
  .pf-chip.active { background: var(--text); color: #fff; border-color: var(--text); font-weight: 600; }
  /* Anchor for the Custom date dropdown below — it positions against this
     row, not the page, so it floats under the chips instead of pushing
     Channel and Remark down every time Custom is picked. */
  .pf-daterow { position: relative; }
  .pf-dd {
    position: absolute; top: 100%; left: 90px; margin-top: 6px;
    display: flex; align-items: center; background: var(--panel);
    border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px;
    box-shadow: 0 4px 16px rgba(0,0,0,.14); z-index: 20;
  }
  /* [hidden] and .pf-dd both carry one class' worth of specificity, and the
     author rule above wins ties over the browser's own [hidden] { display:
     none } — so without this, the dropdown showed regardless of the
     attribute orders.js sets. This rule only needs to beat the one above it. */
  .pf-dd[hidden] { display: none; }
  /* The report, promotion, adjustment and purchase card grids were all
     declared here, each one slightly different from the next. They are one
     component now -- see "launcher tiles" further down this file. */

/* ================= warehouse tablet apps: app tiles, task/picklist/bin cards, scan frame ================= */
      /* The tablet Home's solid-colour tiles are gone: it renders the same
         .lch-tile as every other launcher now. Its accent colours survive as
         the icon-chip tint, which is the part the floor navigates by. */
      .task-card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin-bottom:10px;
                 display:flex;align-items:center;gap:14px}
      .task-card .tc-ic{width:42px;height:42px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#fff;flex-shrink:0}
      .task-card .tc-body{flex:1}
      .task-card .tc-bar{height:6px;border-radius:4px;background:var(--accent-soft);margin-top:6px;overflow:hidden}
      .task-card .tc-bar > div{height:100%;background:var(--green)}
      .pl-card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:16px;margin-bottom:12px}
      .pl-card .pl-head{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:8px}
      .pl-stats{display:flex;gap:18px;font-size:12px;color:var(--muted);margin-top:8px}
      .pl-stats b{display:block;color:var(--text);font-size:14px}
      .pl-badge{font-size:11px;padding:2px 8px;border-radius:10px;font-weight:600}
      .pl-badge.completed{background:var(--green-soft);color:var(--green)}
      .pl-badge.in_progress{background:var(--amber-soft);color:var(--amber)}
      .pl-badge.pending{background:var(--accent-soft);color:var(--muted)}
      .pl-actions{display:flex;gap:8px;margin-top:12px}
      .bin-card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin-bottom:10px}
      .bin-card .bin-head{display:flex;justify-content:space-between;font-weight:600}
      .bin-prod{display:flex;gap:10px;background:var(--bg);border-radius:8px;padding:10px;margin:10px 0;font-size:12px;color:var(--muted)}
      .bin-prod .ph{width:44px;height:44px;border-radius:6px;background:var(--accent-soft);flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:18px}
      .scan-box{max-width:420px;margin:20px auto;text-align:center}
      .scan-frame{background:#111;border-radius:14px;padding:50px 20px;color:#9ca3af;font-size:13px;letter-spacing:.05em;margin-bottom:16px}
      .cm-row{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;border-bottom:1px solid var(--line);cursor:pointer}
      .cm-row:hover{background:var(--bg)}
      .cm-row .ic{width:34px;height:34px;border-radius:8px;background:var(--accent-soft);color:var(--text);display:flex;align-items:center;justify-content:center;font-size:15px;margin-right:10px}
      .track-row{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-bottom:1px solid var(--line);font-size:13px}

/* ================= user management: permission tree editor ================= */
        .um-editor{display:grid;grid-template-columns:360px 1fr;gap:20px}
        @media(max-width:860px){.um-editor{grid-template-columns:1fr}}
        .um-form .fld{margin-bottom:12px}
        .um-form label{display:block;font-size:12px;color:var(--muted);margin-bottom:4px}
        .um-form input,.um-form select{width:100%;min-width:0}
        /* A checkbox is not a text field. The width:100% above stretches it
           across the whole row and shoves the text of its own inline label to
           the far right, which reads as two unrelated controls — it did that to
           the User form's Active toggle and the Warehouse form's Default
           toggle. Scoped by type so text inputs keep filling the row. */
        .um-form input[type=checkbox]{width:auto;padding:0;flex:none}
        /* The import modal's per-column requirements (openImportModal). A
           stacked list, not a table -- this modal renders on warehouse
           tablets too, and a wide table would need horizontal scroll there. */
        .imp-cols{list-style:none;margin:0 0 14px;padding:10px 12px;
                  border:1px solid var(--line);border-radius:var(--r-md);
                  background:var(--bg)}
        .imp-col{display:flex;align-items:baseline;flex-wrap:wrap;gap:8px;
                 padding:4px 0;font-size:13px}
        .imp-col-name{font-weight:600;font-family:var(--font-mono)}
        .imp-col-note{color:var(--muted);font-size:12px}
        .pw-row{display:flex;gap:6px}
        .pw-row input{flex:1}
        .pw-row button{white-space:nowrap;padding:0 10px;border:1px solid var(--border-strong);border-radius:var(--r-md);background:var(--panel);color:var(--text);cursor:pointer;font-size:12px;font-family:inherit}
        .pw-row button:hover{background:var(--accent-soft)}
        .perm-box{border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--card)}
        .perm-head{display:flex;justify-content:space-between;align-items:center;padding:10px 12px;border-bottom:1px solid var(--line);font-weight:600;font-size:13px}
        .perm-mod{border-bottom:1px solid var(--line)}
        .perm-mod:last-child{border-bottom:none}
        .perm-modhead{display:flex;align-items:center;gap:8px;padding:9px 12px;cursor:pointer;font-weight:600;font-size:13px;user-select:none}
        .perm-modhead .tw{width:12px;display:inline-block;color:var(--muted);transition:transform .15s}
        .perm-mod.open .perm-modhead .tw{transform:rotate(90deg)}
        .perm-leaves{display:none;padding:2px 12px 10px 36px}
        .perm-mod.open .perm-leaves{display:block}
        .perm-leaf{display:flex;align-items:center;gap:8px;padding:4px 0;font-size:13px;color:inherit}
        .perm-leaf input,.perm-modhead input{width:auto}

/* ================= settings: tile grid ================= */
        /* The Settings tile grid was hand-written markup with class names of
           its own. It is a launcher like the other eight now -- see "launcher
           tiles" below. */
        .set-back{display:inline-block;margin-bottom:14px;font-size:13px;color:var(--muted);cursor:pointer;font-weight:550}
        .set-back:hover{color:var(--text)}
        .set-section{margin-bottom:22px}
        .set-section h3{font-size:13px;margin:0 0 10px;color:var(--muted);text-transform:uppercase;letter-spacing:.03em}

/* Empty state for the sales chart. Occupies the same box as the canvas so the
   panel does not collapse when there is nothing to plot. */
.chart-empty { display: none; align-items: center; justify-content: center;
  min-height: 260px; text-align: center; color: var(--muted); }
.chart-empty strong { display: block; font-size: 15px; color: var(--text, #111827); margin-bottom: 6px; }
.chart-empty .sub { font-size: 13px; color: var(--muted); }

/* Role Set editor */
.rs-tree { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 14px; }
.rs-mod { border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; }
.rs-mod h4 { font-size: 13px; margin-bottom: 8px; }
/* A .rsb-row rule lived here until 1 Sep 2026, under this heading, styling
   nothing: the Role Set editor builds `perm-leaf d{n}` rows (users.js), not
   these. What it did style was the three places that DO use the class — Assign
   to Wave, Raise a Build, and the Ready Stock Rack panel — because it sits
   after their own rule at the top of this file and wins the cascade on equal
   specificity. `align-items: center` was the visible half: every field in both
   modals was centred inside its column, which is what the forms were reported
   for. `.rsb-row label` reached past `.rsb-row .fld` the same way.
   The JS twin of this is documented in orders.js — `rs` is Role Sets' prefix
   and `rsb` is not, and one name meaning two things has now cost a debugging
   session on each side. Do not reintroduce it; the editor's row is .perm-leaf. */
.rs-lvl { font-size: 12px; padding: 3px 6px; border: 1px solid var(--border); border-radius: 7px; }
.rs-lvl:disabled { opacity: .4; }
.rs-chans { display: flex; flex-wrap: wrap; gap: 16px; }
/* The editor's action bar. This screen is one form with nine permission modules
   on it -- about 2000px tall on a laptop -- so Save sat underneath all of them:
   correcting one tick at the top meant scrolling the whole tree to reach the
   button, and an admin who had scrolled back up could not see it at all. It
   rides the bottom of the window while the form is on screen and settles at the
   end of it, which is the answer .qc-bar already gives at the top of Car Mat QC.
   The full-bleed negative margin matches .panel .body's 18px padding, and the
   radius matches the panel's own so the settled bar sits in its corner.
   
   #rs-msg is INSIDE the bar. Above it, "A role name is required" was written
   1600px away from the button that produced it -- a 4xx that nothing displays,
   which is the failure the house rule about surfacing errors is about. */
/* Who the QC Inspection sheet will name (js/carpetadmin.js:renderQcSigner).
   A statement about the printed document, not a control, so it reads as a line
   of type rather than a banner -- but the two cases where the sheet will print
   a dash carry the amber the rest of the app uses for "this needs attention". */
.qc-signer { font-size: 12.5px; color: var(--muted); margin: -4px 0 14px; }
.qc-signer b { color: var(--text); font-weight: 600; }
.qc-signer.warn { color: var(--amber); }
.qc-signer.warn b { color: var(--amber); }
/* The signer line and the Add Audit button share a row, not a stack — they say
   two unrelated things, and space-between holds the button at the right edge
   even on the days #qc-signer has nothing to print. align-items: center lines
   the 36px button up against the signer's 12.5px text instead of hugging its
   top; the 14px bottom margin is what #qc-signer used to contribute on its
   own and had to be restated here once its own margin was zeroed below. */
.qc-audit-row { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin: 0 0 14px; }
.qc-audit-row #qc-signer { margin: 0; }

.rs-actions { position: sticky; bottom: 0; z-index: 5;
  margin: 16px -18px -18px; padding: 12px 18px;
  background: var(--panel); border-top: 1px solid var(--border);
  border-radius: 0 0 var(--radius) var(--radius); }
/* Left, against the buttons' flex-end, and only taking room when it says
   something. */
.rs-actions .err:empty { display: none; }
.rs-actions .err { margin-right: auto; }
.rs-chans label { display: flex; align-items: center; gap: 7px; font-size: 13px; margin: 0; }
.muted-sm { color: var(--muted); font-size: 12px; }

/* ================= launcher tiles ==========================================
   Fourteen screens in this app are launchers: a grid of doors, with no data of
   their own. Nine are the ones a person meets first -- Home, Orders >
   Fulfillment, Inventory Adjustment, Purchase, Analytics, Promotions,
   Marketing Automation and Settings -- plus the five module landing pages.

   They were built five different ways: a coloured header cap (Fulfillment), a
   solid colour block with the label inside it (the tablet Home), a horizontal
   icon card (Analytics, Inventory Adjustment), the same card turned vertical
   with a "Manage" button inside it (Purchase, Promotions, Marketing
   Automation), and hand-written markup with class names of its own (Settings).
   Five geometries, two type scales, and three different answers to "what do I
   click": the whole tile, a button inside it, or a button that looked
   identical to the one on the tile beside it that was not a button.

   One component now. Every launcher calls launcherGrid() in core.js with a
   list of items, so a tile added anywhere arrives with the geometry, the click
   target and the keyboard behaviour already decided.

   A real <button>, not a clickable <div>: all five of the old tiles were divs
   with an onclick, so none could be reached by keyboard and a screen reader
   read a wall of unlabelled text. For the warehouse roles the launcher IS the
   navigation -- there is nothing else on their Home. */
.lch-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(268px, 1fr)); gap: 14px; }
/* The first four declarations undo what a <button> brings and a card does not
   want: the UA font, centred text, and a width that shrinks to its content. */
.lch-tile { font: inherit; color: inherit; text-align: left; width: 100%;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow); padding: 15px 16px; cursor: pointer;
  display: flex; align-items: flex-start; gap: 13px;
  transition: border-color .15s, box-shadow .15s, transform .15s; }
.lch-tile:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md);
  transform: translateY(-1px); }
/* The chip is tinted with the tile's own accent where its list declares one --
   the fulfilment tools and the tablet Home have carried those colours long
   enough that the floor navigates by them -- and graphite where it does not.
   Tinted rather than filled, and the glyph stays graphite: a grid of these is
   a menu, not a set of alerts, and twelve saturated blocks was exactly what
   was wrong with the old tablet Home. */
/* Red, since 25 Aug 2026. The per-tile hexes went the day before and the
   fallback was the neutral selection tint, which left every launcher
   reading as grey furniture. One brand chip is the standard now; a tile
   that DOES carry a colour is a marketplace listing and overrides it
   inline. */
.lch-ic { width: 38px; height: 38px; border-radius: var(--r-lg); flex-shrink: 0;
  background: var(--brand-soft); color: var(--brand);
  display: grid; place-items: center; font-size: 18px; line-height: 1; }
/* min-width:0 so a long unbroken word wraps instead of widening the column
   past its 1fr share and knocking the tiles beside it out of alignment. */
.lch-bd { min-width: 0; }
.lch-tt, .lch-ds { display: block; }
.lch-tt { font-weight: 600; font-size: 14px; letter-spacing: var(--track); }
.lch-ds { font-size: 12px; color: var(--muted); line-height: 1.45; margin-top: 3px; }
/* A tool SiteGiant has and this app has not built. Shown greyed and inert
   rather than hidden, so the gap stays visible -- GDEX and PosLaju label
   printing in particular are easy to forget until somebody needs a label. */
.lch-tile[disabled] { cursor: not-allowed; opacity: .55; }
.lch-tile[disabled]:hover { border-color: var(--border); box-shadow: var(--shadow);
  transform: none; }
.lch-badge { display: inline-block; margin-left: 7px; padding: 1px 7px; border-radius: 20px;
  background: var(--accent-soft); color: var(--muted); font-size: 10.5px; font-weight: 600;
  text-transform: uppercase; vertical-align: middle; }
/* The one badge that is a recommendation rather than a status, so it is the one
   that gets the brand colour. */
.lch-badge.hot { background: var(--brand); color: #fff; }

/* ================= small screens =================
   The layout is a flex row with a fixed 220px sidebar. On a phone that leaves
   the content too narrow to read AND pushes it sideways, so the nav scrolls
   off-screen entirely — the app looks fine in landscape and is unusable in
   portrait, which is why it went unnoticed.

   Below 820px the sidebar becomes a narrow ICON RAIL that is always visible,
   expanding to full width when the button at its top is tapped. A rail rather
   than a hidden drawer on purpose: the current screen stays identifiable at a
   glance, and reaching another module is one tap rather than two.

   820px rather than a phone width: a 768px tablet in portrait has the same
   problem, and the warehouse floor runs on tablets. */
.nav-ico { width: 20px; height: 20px; flex: 0 0 auto; }
.nav-label { flex: 1 1 auto; }
.rail-toggle { display: none; }
.nav-head { gap: 10px; }

@media (max-width: 820px) {
  .rail-toggle {
    display: grid; place-items: center;
    /* flex: none, because the sidebar is a flex column now — without it this
       shrinks as the tree below it grows. `margin: 0 auto` still centres it:
       auto side margins centre a flex item on the cross axis too. */
    flex: 0 0 auto;
    width: 40px; height: 38px; margin: 0 auto 10px;
    border: 1px solid var(--border); border-radius: 8px;
    background: var(--bg); color: var(--text); cursor: pointer; padding: 0;
  }
  .rail-toggle svg { width: 20px; height: 20px; }

  .sidebar {
    position: fixed; top: 0; left: 0; z-index: 60;
    height: 100vh; width: 56px; padding: 12px 8px;
    transition: width .2s ease; overflow-x: hidden;
    /* White, not the page ground it uses on desktop. Fixed and overlapping the
       content, a rail the same colour as the page behind it has only a 1px
       hairline holding it apart, and once expanded it sits on top of a table it
       has to be opaque against. */
    background: var(--panel);
  }
  /* Collapsed: icons only. Labels are removed from the flow rather than made
     transparent, so nothing overflows the 56px rail and forces a scrollbar. */
  .sidebar .brand b,
  .sidebar .nav-label,
  .sidebar .chev,
  .sidebar .nav-items,
  .sidebar .nav .dot { display: none; }
  .sidebar .brand { justify-content: center; padding: 0 0 14px; }
  .sidebar .nav a,
  .sidebar .nav-head { justify-content: center; padding: 11px 0; }

  /* THE WIDTH CONTROL IS GONE BELOW 820px. The rail is already collapsed here
     and the hamburger at the top of it is what opens it — a second control that
     does the same job, in the opposite corner, is one the thumb has to choose
     between. It is also the wrong control: it toggles .rail-narrow, which is
     scoped to (min-width: 821px) and does nothing at all at this width. */
  .nav-foot { display: none; }

  .sidebar.open { width: 240px; max-width: 82vw; box-shadow: 0 0 24px rgba(0,0,0,.18); }
  .sidebar.open .brand b,
  .sidebar.open .nav-label { display: block; }
  .sidebar.open .chev { display: inline; }
  .sidebar.open .nav-sec.open .nav-items { display: block; }
  .sidebar.open .brand { justify-content: flex-start; padding: 6px 10px 20px; }
  .sidebar.open .nav a,
  .sidebar.open .nav-head { justify-content: flex-start; padding: 9px 12px; }

  /* The rail is fixed, so the content needs room for it. */
  .main { margin-left: 56px; padding: 16px 14px; max-width: 100%; }
  .topbar { gap: 8px; flex-wrap: wrap; }
  .topbar h1 { font-size: 17px; flex: 1 1 auto; }
  /* Wide tables and tile grids scroll inside their card rather than stretching
     the whole page sideways. */
  .card, .panel { overflow-x: auto; }
  /* ...which would clip the Filter panel, an absolutely-placed child of a
     .panel. Pinned to the screen instead, edge to edge inside the gutter. */
  .pfp-pop { position: fixed; top: 72px; left: 16px; right: 16px; width: auto;
             max-height: calc(100vh - 96px); display: flex; flex-direction: column; }
  .pfp-pop .proc-filters { max-height: none; flex: 1; min-height: 0; }
  /* Filters stack rather than squeezing every control to a sliver. The date
     pair stays on one line — split across two it stops reading as a range. */
  /* The verdicts split the width as two big targets, which is how they are
     pressed — on a tablet, by someone holding it. */
  .qc-select, .qc-decide { flex: 1 1 100%; }
  .qc-decide { margin-left: 0; }
  .qc-decide button { flex: 1; }
  .qc-defect-opt { padding: 14px; }
  .inv-toolbar { gap: 8px; padding: 12px 14px; }
  .inv-toolbar .flt { flex: 1 1 100%; }
  .inv-toolbar .flt > label { min-width: 84px; }
  .flt-range { flex: 1; }
  .flt-range input[type=date] { flex: 1; min-width: 0; }
  table { min-width: 520px; }

  /* Warehouses is a 300px + 1fr grid on wide screens. At this width the second
     column is left with a few dozen pixels, so the Add/Edit form's inputs and
     the racks table are both squeezed to slivers — stack the two instead. */
  .wh-layout { grid-template-columns: 1fr; }

  /* Only while expanded: tapping beside the menu closes it. */
  .nav-scrim { position: fixed; inset: 0; z-index: 50; background: rgba(0,0,0,.35); }
}

/* ---- AWB camera scanner (app/static/js/scanner.js) ---- */
.scan-frame { position: relative; overflow: hidden; padding: 0; min-height: 190px;
  display: grid; place-items: center; }
/* Hidden until a stream is attached, so the box does not sit blank-black with
   no explanation while the camera is off. */
.scan-video { display: none; width: 100%; max-height: 320px; object-fit: cover; }
.scan-frame.live .scan-video { display: block; }
.scan-frame.live .scan-placeholder { display: none; }
.scan-reticle { display: none; position: absolute; inset: 18% 12%;
  border: 2px solid rgba(255,255,255,.85); border-radius: 10px; pointer-events: none; }
.scan-frame.live .scan-reticle { display: block; }
.scan-controls { display: flex; gap: 8px; justify-content: center; margin-bottom: 10px; }
.scan-status { font-size: 12.5px; color: var(--muted); margin-bottom: 10px; min-height: 1.4em; }

/* Car Mat Photo's own rules are NOT here. That screen moved off this app
   onto the Commission Dashboard, which is self-contained by design (a
   phone on warehouse wifi should not wait on this stylesheet to render a
   pay figure), so the .cp-* rules live in
   app/static/commission_dashboard.html beside the markup they style.
   The .scan-* rules above stay: Order Fulfillment Assistant still uses
   them. */

/* ---- Car mat QC, rates and report (app/static/js/carpetadmin.js) ---- */
.qc-bar { display: flex; align-items: center; gap: 10px 16px; flex-wrap: wrap;
  padding: 10px 12px; margin-bottom: 14px; background: var(--panel);
  border: 1px solid var(--border); border-radius: 8px; position: sticky; top: 0; z-index: 5; }
/* Two clusters, not six loose controls: what is selected, and what to do with
   it. Each wraps as a unit, so a narrow screen never splits a verdict button
   away from the reason it needs. */
.qc-select, .qc-decide { display: flex; align-items: center; gap: 10px; }
.qc-decide { margin-left: auto; }
.qc-decide button { white-space: nowrap; }

/* The defect list in the Reject dialog. A radio list, not the dropdown this
   replaced: picking the wrong defect docks the wrong person, and an <option>
   could only show the name, the amount and who pays crushed onto one line that
   is invisible until the list is open. Here they are all readable at once, and
   each row is a tablet-sized target. */
.qc-defects { display: grid; gap: 8px; margin: 2px 0 4px; }
.qc-defect-opt { display: grid; grid-template-columns: auto 1fr auto; align-items: center;
  gap: 6px 14px; padding: 12px 14px; border: 1px solid var(--border); border-radius: 10px;
  cursor: pointer; font-size: 14px; background: #fff; }
.qc-defect-opt:hover { background: var(--bg); }
/* Toggled from JS rather than :has(), which the older tablet browsers miss —
   and the same click has to enable the confirm button anyway. */
.qc-defect-opt.sel { border-color: var(--red); box-shadow: 0 0 0 2px var(--danger-soft); }
.qc-defect-opt .nm { font-weight: 550; }
.qc-defect-opt .amt { font-weight: 600; color: var(--red); }
.qc-defect-opt .who { grid-column: 2 / -1; font-size: 11.5px; color: var(--muted);
  text-transform: uppercase; letter-spacing: .03em; }
.qc-grid { display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
.qc-card { border: 2px solid var(--border); border-radius: 10px; overflow: hidden;
  background: var(--panel); cursor: pointer; }
/* A thick accent border, not a faint tint: on a run of forty near-identical
   mats the reviewer needs to see at a glance which are selected. */
/* Graphite rather than the brand red it used to inherit from --accent: on this
   screen red is the Reject verdict, and a red border round a card the reviewer
   has merely SELECTED reads as one they have already failed. */
.qc-card.sel { border-color: var(--text); box-shadow: 0 0 0 2px var(--accent-soft); }
.qc-photos { display: grid; grid-template-columns: 2fr 1fr; gap: 2px; background: var(--border); }
.qc-photos img { width: 100%; height: 150px; object-fit: cover; display: block; }
/* Each thumbnail is a button onto the full frame. It carries no chrome of its
   own -- the photo IS the control -- but it is a real <button>, so it is
   reachable by keyboard and announces itself, which an <img> with a click
   handler never did. */
.qc-zoom { display: block; padding: 0; border: 0; background: none; cursor: zoom-in;
  position: relative; }
.qc-zoom:focus-visible { outline: 2px solid var(--text); outline-offset: -2px; }
/* Shown on hover and on keyboard focus alike: a control that only appears
   under a pointer is a control a keyboard user is never told about. */
.qc-zoom::after { content: "⤢"; position: absolute; right: 6px; bottom: 6px;
  width: 22px; height: 22px; display: grid; place-items: center; border-radius: 6px;
  background: rgba(17, 20, 24, .72); color: #fff; font-size: 12px; line-height: 1;
  opacity: 0; transition: opacity .12s ease; }
.qc-zoom:hover::after, .qc-zoom:focus-visible::after { opacity: 1; }
.qc-nophoto { display: grid; place-items: center; height: 150px; padding: 6px;
  background: var(--bg); color: var(--muted); font-size: 11px; text-align: center; }
/* The enlarged photo. Capped against the viewport, not against a fixed pixel
   size: the point of this dialog is the biggest the photo can honestly be
   drawn on the screen in front of the reviewer, and a mat is a tall subject
   photographed on a phone. */
.qc-view img { display: block; width: 100%; max-height: 68vh; object-fit: contain;
  background: var(--bg); border-radius: 8px; }
.qc-view-meta { margin-top: 10px; font-size: 13px; }
.qc-view-bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  margin-top: 12px; }
/* There is no global anchor reset, so the one <a> in this bar would carry the
   UA underline while the button beside it does not. */
.qc-view-bar a.btn-sm { text-decoration: none; }
.qc-meta { padding: 10px 12px; font-size: 13px; }
.qc-flag { display: inline-block; padding: 1px 7px; border-radius: 20px;
  background: var(--amber, #d97706); color: #fff; font-size: 10.5px; font-weight: 600; }

/* ---- Home ------------------------------------------------------------- */
/* One screen for everybody, and the only screen whose content depends on the
   role set rather than on which door you came through. Two rows: the figures
   that need attention, then the doors. */
.home-hi { font-size: 20px; font-weight: 550; margin: 2px 2px 16px;
  letter-spacing: var(--track); }
.home-h { font-size: 13px; color: var(--muted); text-transform: uppercase;
  letter-spacing: .03em; margin: 24px 0 9px; }

/* ONE FIGURE LEADS. Same argument the Overview settled with .gauges: cards of
   equal weight make the reader decide which one matters every time they open
   the screen. The lead takes two columns, two rows and the dark panel; the rest
   are the same numbers, sized as context.

   Four fixed columns rather than auto-fit: the lead spans two of them, and
   auto-fit collapsing to three would leave it filling two thirds of a row it
   was never proportioned for. */
.home-grid { display: grid; gap: 12px; margin-bottom: 4px;
  grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* Shared by both sizes: a card is a button, because every one of them goes
   somewhere. */
.home-card { font: inherit; color: inherit; text-align: left; width: 100%;
  cursor: pointer; display: flex; flex-direction: column; gap: 7px;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 14px 15px 15px;
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease); }
.home-card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }
.home-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Something to do today, rather than something merely counted. A left edge, not
   a tinted ground: the ground is what the figure's own colour is for. */
.home-card.urgent { border-left: 3px solid var(--brand); }

/* The lead. It earns its weight by SIZE, not by colour: two columns, two rows,
   a figure at reading-across-the-room scale and room for the shape of the last
   seven days along the bottom. It was the Overview's dark binnacle panel for an
   afternoon (CS asked for it light, 25 Aug 2026) -- and light is the better fit
   anyway, because Home is the first screen of the day rather than an instrument
   face, and one dark rectangle among six pale ones read as a different KIND of
   thing rather than as the most important one.

   Its chart bleeds to the card's edges, so the padding moves onto the body and
   the panel clips. */
.home-lead { grid-column: span 2; grid-row: span 2;
  padding: 0; overflow: hidden; justify-content: space-between; }
.home-lead-bd { display: flex; flex-direction: column; gap: 6px;
  padding: 17px 19px 6px; }
.home-lead .home-sub { font-size: 12px; }
/* clamp, so the lead keeps its proportion between a tablet and a wide desk
   screen without a breakpoint for each. */
.home-val.lead { font-size: clamp(30px, 4vw, 42px); font-weight: 500;
  line-height: 1.02; }
/* The chart band, flush across the bottom. This is what the lead has and the
   supporting cards do not: room for the shape of the last seven days at a size
   somebody can actually read. */
.home-lead-chart { display: block; margin-top: auto; }
.home-spark.big { width: 100%; height: 56px; }

/* The supporting cards. Paper-white, hairlined, deliberately quieter. */
.home-card-hd { display: flex; align-items: baseline; justify-content: space-between;
  gap: 8px; }
.home-card-lb { color: var(--muted); font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .05em; }
/* The arrow says the card is a door. Quiet until hover, because on a row of six
   it is the figures that should be read first. */
.home-card-go { color: var(--muted); font-size: 13px; opacity: 0;
  transition: opacity var(--dur) var(--ease); }
.home-card:hover .home-card-go, .home-card:focus-visible .home-card-go { opacity: 1; }
/* The figure and its sparkline share a line: the number leads at reading size
   and the shape sits beside it. */
.home-card-fig { display: flex; align-items: flex-end; justify-content: space-between;
  gap: 12px; }
.home-val { font-size: 24px; font-weight: 550; line-height: 1;
  letter-spacing: var(--track-figure); font-variant-numeric: tabular-nums; }
.home-val.ok { color: var(--green); }
.home-val.warn { color: var(--amber); }
.home-val.low { color: var(--danger); }
.home-spark { width: 96px; height: 28px; flex: 0 0 auto; display: block; }
.home-spark-l { fill: none; stroke: var(--brand); stroke-width: 1.6;
  stroke-linejoin: round; stroke-linecap: round; }
.home-spark-f { fill: var(--brand-soft); stroke: none; }
/* The lead's line is the same brand red, drawn a shade heavier because it
   is carrying a band four times the height. */
.home-lead .home-spark-l { stroke-width: 2; }
/* The comparison. A pill rather than plain text so a row of cards reads as
   several of the same object; the arrow carries the direction for anyone who
   cannot separate the two greens. */
.home-delta { align-self: flex-start; font-size: 11.5px; font-weight: 600;
  padding: 2px 8px; border-radius: 20px; background: var(--accent-soft);
  color: var(--muted); }
.home-delta.up { background: var(--green-soft); color: var(--green); }
.home-delta.down { background: var(--danger-soft); color: var(--danger); }
/* The meter. Drawn only where the server sent a denominator that is genuinely
   the numerator's own population -- see renderHomeCards. */
.home-meter { height: 5px; border-radius: 3px; background: var(--accent-soft);
  overflow: hidden; }
.home-meter i { display: block; height: 100%; background: var(--brand);
  border-radius: 3px; transition: width var(--dur) var(--ease); }
.home-sub { font-size: 11.5px; color: var(--muted); }

/* ---------------------------------------------------------------------------
   MAINTENANCE DASHBOARD (21 Sep 2026) -- additive content on the Maintenance
   module's own landing page (view-maintenance_home), drawn ABOVE the existing
   tile grid (.lch-grid#mh-maintenance_home), which stays exactly as it was.
   Everything here is prefixed .mnt-dash- so it cannot leak onto any other
   screen -- see app/static/js/maintenance_dashboard.js for what renders it.
   Desktop app conventions throughout (px, not the field apps' rem/vmin
   scaling -- that only applies to driver_app.html/commission_dashboard.html). */
.mnt-dash { margin-bottom: 18px; display: flex; flex-direction: column; gap: 14px; }

/* The Week / Month switch. A pill rather than a select: two states, always
   both visible, same reasoning as .pf-chip elsewhere in this file. */
.mnt-dash-toolbar { display: flex; justify-content: flex-end; }
.mnt-dash-toggle { display: inline-flex; background: var(--accent-soft);
  border-radius: 999px; padding: 3px; gap: 2px; }
.mnt-dash-toggle button { font: inherit; font-size: 12.5px; font-weight: 600;
  color: var(--muted); background: transparent; border: 0; border-radius: 999px;
  padding: 6px 16px; cursor: pointer; transition: background var(--dur) var(--ease),
  color var(--dur) var(--ease); }
.mnt-dash-toggle button.active { background: var(--panel); color: var(--text);
  box-shadow: var(--shadow); }

/* The KPI strip: four tiles, equal weight -- unlike Home's own cards, nothing
   here leads, because this page is a module dashboard read as a whole rather
   than a "what do I do right now" front door. */
.mnt-dash-kpis { display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
.mnt-dash-kpi { background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 14px 15px 15px; display: flex;
  flex-direction: column; gap: 6px; }
.mnt-dash-kpi-lb { color: var(--muted); font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .05em; }
.mnt-dash-kpi-val { font-size: 26px; font-weight: 550; line-height: 1;
  font-variant-numeric: tabular-nums; }
.mnt-dash-kpi-val.ok { color: var(--green); }
.mnt-dash-kpi-val.warn { color: var(--amber); }
.mnt-dash-kpi-val.low { color: var(--danger); }
.mnt-dash-kpi-sub { font-size: 11.5px; color: var(--muted); }
.mnt-dash-trend { align-self: flex-start; font-size: 11px; font-weight: 600;
  padding: 2px 8px; border-radius: 20px; background: var(--accent-soft);
  color: var(--muted); }
.mnt-dash-trend.up { background: var(--danger-soft); color: var(--danger); }
.mnt-dash-trend.down { background: var(--green-soft); color: var(--green); }
.mnt-dash-meter { height: 5px; border-radius: 3px; background: var(--accent-soft);
  overflow: hidden; margin-top: 2px; }
.mnt-dash-meter i { display: block; height: 100%; background: var(--brand);
  border-radius: 3px; }

/* The four panels: SLA, machines due, parts to replenish, repairs by machine
   -- a fixed 2x2 (24 Sep 2026; was 3 panels on auto-fit, which spread across
   more than 2 columns on a wide desktop screen once a 4th was added -- a
   deliberate 2-up grid reads as one set of four, not a ragged row). Collapses
   to one column below 900px, the same "desktop app, narrow window" breakpoint
   already used nearby for Account Management's .asm-sec -- this dashboard had
   no breakpoint of its own before (auto-fit's wrapping was fluid, not a fixed
   list), so this is the first one it needs. */
.mnt-dash-panels { display: grid; gap: 12px;
  grid-template-columns: repeat(2, 1fr); align-items: start; }
@media (max-width: 900px) {
  .mnt-dash-panels { grid-template-columns: 1fr; }
}
.mnt-dash-panel { background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 16px 17px; }
.mnt-dash-panel h3 { margin: 0 0 10px; font-size: 13px; font-weight: 600; }
.mnt-dash-empty { color: var(--muted); font-size: 12.5px; font-style: italic; margin: 0; }

/* SLA panel: a stacked bar, on-time green then late red, and the note that
   no_repair_needed counts as done -- confirmed with the user, not a guess,
   and worth saying on-screen since it is easy to assume otherwise. */
.mnt-dash-sla-figs { display: flex; gap: 18px; margin-bottom: 10px; }
.mnt-dash-sla-fig b { display: block; font-size: 22px; font-weight: 550;
  font-variant-numeric: tabular-nums; line-height: 1; }
.mnt-dash-sla-fig span { font-size: 11px; color: var(--muted); }
.mnt-dash-sla-fig.on-time b { color: var(--green); }
.mnt-dash-sla-fig.late b { color: var(--danger); }
.mnt-dash-sla-bar { display: flex; height: 10px; border-radius: 5px;
  overflow: hidden; background: var(--accent-soft); margin-bottom: 8px; }
.mnt-dash-sla-bar i.on-time { display: block; height: 100%; background: var(--green); }
.mnt-dash-sla-bar i.late { display: block; height: 100%; background: var(--danger); }
.mnt-dash-sla-pct { font-size: 12px; color: var(--muted); margin: 0 0 6px; }
.mnt-dash-note { font-size: 11px; color: var(--muted); font-style: italic; margin: 0; }

/* Machines-due and parts-to-replenish panels: a row of status pill counts,
   then a short list of the most urgent rows. */
.mnt-dash-pills { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.mnt-dash-pill { font-size: 11px; font-weight: 600; padding: 3px 9px;
  border-radius: 999px; background: var(--accent-soft); color: var(--muted); }
.mnt-dash-pill.overdue, .mnt-dash-pill.out { background: var(--danger-soft); color: var(--danger); }
.mnt-dash-pill.due_soon, .mnt-dash-pill.low { background: var(--amber-soft); color: var(--amber); }
.mnt-dash-pill.ok { background: var(--green-soft); color: var(--green); }
.mnt-dash-list { list-style: none; margin: 0; padding: 0; display: flex;
  flex-direction: column; gap: 8px; }
.mnt-dash-list li { display: flex; justify-content: space-between; align-items: center;
  gap: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
.mnt-dash-list li:last-child { border-bottom: 0; padding-bottom: 0; }
/* flex: 1 so this stretches to fill the row -- Machines Due and Spare Parts
   never needed it explicitly because their <li> uses justify-content:
   space-between with only two children (this div + a chip), which pushes the
   chip flush right on its own. Repairs by Machine's <li> has a third child
   (the rank number) ahead of this div, so space-between would also shove a
   gap between the rank and the name -- growing this div instead pushes just
   the count to the right edge, leaving the rank flush against the name
   (29 Sep 2026, CS). */
.mnt-dash-list-main { min-width: 0; flex: 1; }
.mnt-dash-list-name { font-size: 12.5px; font-weight: 600; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.mnt-dash-list-meta { font-size: 11px; color: var(--muted); }
.mnt-dash-chip { flex: 0 0 auto; font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .03em; padding: 3px 8px;
  border-radius: 999px; }
.mnt-dash-chip.out, .mnt-dash-chip.overdue { background: var(--danger-soft); color: var(--danger); }
.mnt-dash-chip.low, .mnt-dash-chip.due_soon { background: var(--amber-soft); color: var(--amber); }
.mnt-dash-chip.ok { background: var(--green-soft); color: var(--green); }
.mnt-dash-chip.not_scheduled, .mnt-dash-chip.in_repair { background: var(--accent-soft); color: var(--muted); }
/* Machines Due / Spare Parts quick actions (Request Maintenance, Receive
   Stock -- 1 Oct 2026): a third flex child between the name and the chip.
   .mnt-dash-list-main's own flex:1 already consumes the row's free space
   (see that rule's comment above, on Repairs by Machine's 3-child row), so
   this just sits flush between them rather than needing its own
   justify-content handling. */
.mnt-dash-list-action { flex: 0 0 auto; font-size: 11px; white-space: nowrap; }
/* Shared by every panel's "View All" link (Repairs by Machine's own
   .mnt-dash-rbm-viewall predates this and keeps its name; Machines Due and
   Spare Parts' are new, 1 Oct 2026, and share this generic one instead of a
   third near-identical rule). */
.mnt-dash-viewall { display: block; margin-top: 12px; font-size: 12px; }

/* Repairs by Machine panel (24 Sep 2026; period toggle removed 30 Sep 2026
   in favor of the dashboard's own shared one above -- this toolbar is now
   just the warehouse <select>, right-aligned rather than space-between
   since there is only the one control left in it). Deliberately no bar/
   meter on the row -- an earlier mock draft had one and the user asked for
   it to be removed; a plain number only. */
.mnt-dash-rbm-toolbar { display: flex; flex-wrap: wrap; align-items: center;
  justify-content: flex-end; gap: 8px; margin-bottom: 10px; }
.mnt-dash-rbm-wh { font: inherit; font-size: 12px; padding: 5px 8px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--panel); color: var(--text); }
.mnt-dash-rbm-nav { display: flex; align-items: center; justify-content: center;
  gap: 10px; margin-bottom: 10px; }
/* The dashboard's own copy of this nav (30 Sep 2026, shared filter) sits
   right under .mnt-dash-toolbar's right-aligned toggle -- centered like the
   View All Machines screen's copy would read as misaligned against it. */
.mnt-dash-toolbar-nav { justify-content: flex-end; margin-top: 8px; }
.mnt-dash-rbm-nav button { font: inherit; font-size: 15px; line-height: 1;
  background: var(--accent-soft); border: 0; border-radius: var(--radius);
  width: 26px; height: 26px; cursor: pointer; color: var(--text); }
.mnt-dash-rbm-period-lb { font-size: 12px; font-weight: 600; color: var(--muted);
  min-width: 120px; text-align: center; }
.mnt-dash-rbm-list { list-style: none; margin: 0; padding: 0; display: flex;
  flex-direction: column; gap: 8px; }
.mnt-dash-rbm-list li { display: flex; align-items: center; gap: 10px;
  padding-bottom: 8px; border-bottom: 1px solid var(--border); }
.mnt-dash-rbm-list li:last-child { border-bottom: 0; padding-bottom: 0; }
.mnt-dash-rbm-rank { flex: 0 0 auto; width: 18px; text-align: center;
  font-size: 11px; font-weight: 600; color: var(--muted); }
.mnt-dash-rbm-count { flex: 0 0 auto; font-size: 15px; font-weight: 600;
  font-variant-numeric: tabular-nums; }
.mnt-dash-rbm-viewall { display: block; margin-top: 12px; font-size: 12px; }

/* The doors: modules, plus one for anything granted that no module leads to.
   Squarer and quieter than a launcher tile -- this is wayfinding, and the
   descriptions belong on the module page itself where there is room. */
.home-mods { display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fill, minmax(152px, 1fr)); }
.home-mod { font: inherit; color: inherit; cursor: pointer; text-align: left;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 14px 15px 15px;
  display: flex; flex-direction: column; gap: 9px; align-items: flex-start;
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease); }
.home-mod:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }
.home-mod:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The rail's own icon, at the launcher chip's size and in its colours, so a
   module reads as the same object in both places. */
.home-mod-ic { width: 34px; height: 34px; border-radius: var(--r-lg);
  background: var(--brand-soft); color: var(--brand);
  display: grid; place-items: center; flex: 0 0 auto; }
.home-mod-ic svg { width: 19px; height: 19px; }
.home-mod-t { font-size: 13.5px; font-weight: 600; letter-spacing: var(--track); }
.home-mod-n { font-size: 11px; color: var(--muted); margin-top: -6px; }

/* Two columns at tablet width: the lead still spans both, so it stays the width
   of the row of pairs beneath it. */
@media (max-width: 1100px) {
  .home-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* One column on a phone. The lead keeps its panel and its chart -- it is the
   figure the person opened the app for -- but stops spanning anything. */
@media (max-width: 620px) {
  .home-grid { grid-template-columns: 1fr; }
  .home-lead { grid-column: auto; grid-row: auto; }
}
/* A gloved thumb on the shop floor presses these too. */
@media (max-width: 820px) {
  .home-mod { min-height: 92px; }
  .home-mods { grid-template-columns: repeat(auto-fill, minmax(138px, 1fr)); }
  /* The arrow never fades in without a pointer, so it is simply on. */
  .home-card-go { opacity: 1; }
}

/* ---- Account Management ---------------------------------------------- */
/* SiteGiant stacks Account, Role Set and Staff down one page, each with its
   heading in a left-hand column and the content card on the right. Below
   ~900px the label sits above its card instead, so the page still works on
   the warehouse tablets. */
.asm-sec { display: grid; grid-template-columns: 220px 1fr; gap: 24px;
  padding: 26px 0; border-bottom: 1px solid var(--line, #e6e8eb); }
.asm-sec:last-child { border-bottom: 0; }
.asm-sec > h2 { font-size: 17px; margin: 0; }
.asm-sec > div { min-width: 0; }   /* let wide tables scroll, not overflow */
@media (max-width: 900px) {
  .asm-sec { grid-template-columns: 1fr; gap: 12px; padding: 18px 0; }
}
/* The role name and its summary line, matching SiteGiant's single "Role"
   column: name in bold with the detail underneath, actions on the right. */
.asm-rolename { font-weight: 600; }
.asm-actions { white-space: nowrap; text-align: right; }
.asm-link { display: flex; align-items: center; justify-content: space-between;
  gap: 16px; flex-wrap: wrap; }

/* ---- on/off switch ----------------------------------------------------- */
/* One component, so the next screen that wants a switch does not invent a
   second one. First used by Notifications on Account Management.

   A real <button role="switch">, not a styled div: it has to answer Space and
   Enter, take focus, and announce its own state. `aria-checked` is what draws
   it, so the accessible state and the visible state cannot disagree — there is
   no second class to remember to toggle. */
.sw { display: inline-flex; align-items: center; gap: 10px; padding: 0;
  border: 0; background: none; font: inherit; color: var(--text);
  cursor: pointer; }
.sw-track { position: relative; flex: none; width: 40px; height: 22px;
  border-radius: 999px; background: var(--border-strong);
  transition: background .15s; }
.sw-knob { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px;
  border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,.25);
  transition: transform .15s; }
.sw[aria-checked="true"] .sw-track { background: var(--green); }
.sw[aria-checked="true"] .sw-knob { transform: translateX(18px); }
/* Blocked or unsupported. Not just dimmed: the cursor says it too, because a
   dimmed control that still looks pressable is the one people keep pressing. */
.sw[disabled] { opacity: .55; cursor: not-allowed; }
.sw-lb { font-size: 13px; font-weight: 550; }

/* The access level beside a screen when previewing what a role set grants.
   Muted and small: the screen name is what you scan for, the level is the
   detail you check once you have found it. */
.perm-level { color: var(--muted); font-size: 11px; margin-left: 6px; }
/* MODULE ACCESS RIGHTS — the nesting, and the rows that are not tickable.
   The tree became MODULE > submodule > its own submodules on 25 Aug 2026, so a
   row's depth has to be visible: an admin reading a flat list could not tell
   what granting Fulfillment did or did not include.

   Indented from the LABEL, not with a margin on the row, so the checkboxes stay
   in one column — a ragged column of boxes is harder to scan than a ragged
   column of words, and the boxes are what the eye is following. */
.perm-leaf.d1 { padding-left: 20px; }
.perm-leaf.d2 { padding-left: 40px; }
/* A page that cannot be granted: shown, and shown as such. It carries no
   control at all rather than a disabled one — a box that looks tickable and is
   not is the thing this replaced. */
.perm-leaf.fixed { color: var(--muted); }
.perm-why, .rs-why { font-size: 11px; color: var(--muted); font-style: italic;
  margin-left: auto; padding-left: 10px; white-space: nowrap; }
.perm-leaf.fixed { justify-content: space-between; }

/* ---- a wave, and the parcels inside it (cutter/sewer app) ----------------
   Work is HANDED OUT as a wave — one cutter, one sewer, one pick list — so the
   main screen lists WAVES and one wave's parcels are a screen of their own.

   It was an accordion until 1 Sep 2026, on the argument that a fourth level
   would add a history entry between the dashboard and the parcel. What settled
   it was the phone: one open wave of a dozen parcels pushed every other wave
   off the screen, so the list of waves stopped being readable the moment
   somebody used it. The .wave-jobs indent and the .wave-group wrapper it needed
   are gone with it.

   The wave row reuses .job, so a wave and a parcel read as the same kind of
   object on a phone — which is what they are, one level apart. */
.wave-head { font-weight: 600; }
/* A chevron INTO a screen, not a + that toggles one open. Same corner, so the
   row is unchanged to the eye except in what it promises. */
.wave-chev { float: right; font-size: 16px; line-height: 1; color: var(--muted); }
.wave-head.done { opacity: .62; }
/* Who is doing the other half of the wave, under its figures. On the row in the
   list this rides in .job-meta with everything else; on the wave's own screen
   there is room for it to be its own line. */
.wave-mate { font-size: 12.5px; color: var(--muted); margin: -4px 0 4px; }
/* The wave's deadline, on the field apps' wave row and wave screen. Its own
   line rather than another item in the meta run: on a tablet at arm's length a
   deadline joined to the parcel count by a middot is a deadline nobody reads.
   Colour is not carrying it alone — "3 h late" says it in words too, which is
   what survives daylight on a warehouse floor, and is what a red/green-blind
   reader is left with. */
.wave-due { font-size: 12.5px; color: var(--muted); margin: 3px 0 0; }
.wave-due.late { color: var(--red); }
.wave-due.soon { color: var(--amber); }
.wave-due b { font-weight: 600; }   /* see .due-late b — 600 is the ceiling */

/* The parcel card's AWB button is GONE from this list (CS, 1 Sep 2026), and
   .job-wrap, .job-actions and .job-awb-* went with it. It sat under each card,
   then on each card's own row, and it lives on the parcel's own page now — see
   carpetAwbRow in js/carpet.js and .cp-awb in commission_dashboard.html. A
   control wanted on one parcel does not belong on forty rows. */

/* ==== THE FIELD APPS' SHARED KIT =========================================
   Kamatto has two apps that live on a phone in somebody's hand all shift: the
   Driver app (/driver) and the cutter/sewer app (/commission). They were built
   months apart and each grew its own card, its own back control and its own
   idea of a filter, so the same object — "a job on a list" — was two different
   shapes depending on which app you opened.

   These are the pieces they now share. Each app keeps its own <style> block for
   what is genuinely specific to it (the Driver app's camera, the cutter's
   viewfinder); anything an operator would recognise from the other app lives
   here, once.

   Naming: f- for "field". Where the cutter/sewer app already had a class name
   for the same thing, that name is listed alongside rather than renamed — its
   markup and its tests are the older of the two and there is nothing to gain
   from churning them to reach an identical rule.

   One thing changed on the way in: every weight here is 600 rather than the 700
   these rules carried inside the apps' own <style> blocks. Public Sans ships
   only 400 and 600 (see the @font-face block at the top of this file), so a 700
   was never a real weight — the browser either synthesised a faux-bold or
   snapped it back to 600 anyway. It is also the house rule: authority comes from
   size and tracking, not weight (tests/js/design_calibration.test.mjs). The
   remaining 700s in each app's own stylesheet are equally unreal and equally
   worth sweeping, but that is a change to screens this one is not touching.
   ------------------------------------------------------------------------- */

/* ---- the card ----
   The cutter/sewer app's .job, promoted. Tighter than the Driver app's old
   card (12px 14px against 14px, 8px of gap against 12px) because a list of
   these is read by scrolling, and the Driver app's looser card fitted three
   trips on a phone where this fits four.

   The container form is a <div> holding its own buttons (a Driver package
   card); the tappable form is a <button> that IS the row (a cutter's job, a
   trip on the list). Both look identical, which is the point — a card must not
   change shape depending on how it is wired. */
.fcard, .job {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 1.2rem 1.4rem; margin-bottom: 0.8rem;
}
/* A <button> inherits none of these, and a card that is a button has to read as
   a card and not as a control. */
button.fcard, a.fcard, .job {
  display: block; width: 100%; min-height: 4.4rem; text-align: left;
  font-family: inherit; font-size: inherit; font-weight: 400;
  color: var(--text); text-decoration: none; cursor: pointer;
}
/* Only the tappable ones tint. .fcard on its own is a container, and tinting it
   on every touch of a button inside it reads as the whole card being pressed. */
button.fcard:active, a.fcard:active, .fcard.tap:active, .job:active {
  background: var(--accent-soft);
}
/* Finished work stays on the list and is dimmed rather than removed: "did I
   already do that one" is the question these screens exist to answer, and an
   empty space does not answer it. */
.fcard.done, .job.done { opacity: .58; }

/* A job card with a control beside it — .job-row and .job-print — lived here
   for the piece cards on a wave. They went on 2 Sep 2026 (CS): the print moved
   to the item page, so a card is a card again and needs no row around it. */

.fcard-top, .job-top { display: flex; justify-content: space-between;
                       align-items: baseline; gap: 1rem; }
/* The one thing on the card that is scanned for: an AWB, a trip number, an
   order reference. */
.fcard-t, .job-awb { font-size: 1.55rem; font-weight: 600; }
.fcard-meta, .job-meta { font-size: 1.2rem; color: var(--muted); margin-top: 0.2rem; }
/* A second line that is content rather than metadata — the mats on a parcel,
   the customer at a stop. Darker than .fcard-meta, lighter than the title. */
.fcard-sub, .job-mats { font-size: 1.25rem; color: var(--text-2); margin-top: 0.5rem; }

/* ---- the thin progress track under a card's meta line ----
   A hairline, not a block. The Driver app used to give this three rows and a
   15px bold figure of its own, which is most of why its cards read as twice the
   height of the cutter/sewer app's. The number that matters is on the pill above
   it; all this has to do is make "nearly done" and "barely started" tellable
   apart without reading, which 3px does as well as 8px. */
.fcard-bar { display: flex; height: 3px; margin-top: 0.8rem; border-radius: 999px;
             overflow: hidden; background: var(--border); }
.fcard-bar span { display: block; height: 100%; }
.fcard-bar .seg-done { background: var(--green); }
.fcard-bar .seg-failed { background: var(--red); }

/* ---- the summary row above a list ----
   The cutter/sewer app's .jobsum: ONE card with a column per figure, rather
   than one bordered box per figure. The Driver app had four separate cards,
   which is four borders and four shadows for one reading and the single biggest
   reason the two list screens did not look like the same app.

   The column count comes from however many children there are — three on the
   cutter's list, four on the Driver's trip list, three on a trip's packages —
   so nothing has to declare it.

   The figure follows .tile .value, the treatment every figure on the desktop
   already uses: 550 (which resolves to the 600 face, the heaviest Public Sans
   ships), the tracking token rather than a hand-picked -.5px, and tabular
   numerals — with proportional digits a 1 is narrower than a 7 and a row of
   counts does not line up. */
.fsum, .jobsum {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 0.8rem;
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 1.2rem 0.8rem; margin-bottom: 1.2rem; text-align: center;
}
.fsum .k, .jobsum .k { font-size: 1.05rem; color: var(--muted);
  text-transform: uppercase; letter-spacing: .05em; line-height: 1.2; }
.fsum .v, .jobsum .v { font-size: 2.2rem; font-weight: 550; margin-top: 0.2rem;
  letter-spacing: var(--track); line-height: 1.15; font-variant-numeric: tabular-nums; }
/* A figure that is not the point of the row. Also .jobsum's own name for it. */
.fsum .v.rest, .jobsum .v.rest, .fsum .v.zero { color: var(--muted); }
.fsum .v.ok { color: var(--green); }
.fsum .v.warn { color: var(--amber); }
.fsum .v.bad { color: var(--red); }
.fsum .v.info { color: var(--accent); }

/* ---- the bar above a detail screen ----
   The cutter/sewer app's .cp-panel-bar: a panel-coloured strip carrying the back
   control and the title of the thing being looked at. The Driver app's trip
   detail had the control alone on bone paper, which read as a stray link rather
   than as a header, and never said which trip you had opened once the card at
   the top scrolled away.

   The control inside it is .fback either way — see the note there for why the
   Driver app's form won and the cutter/sewer app's ergonomics came with it. */
.fbar, .cp-panel-bar {
  display: flex; align-items: center; gap: 0.2rem; flex: none;
  padding: 0.6rem 1.2rem 0.6rem 0.4rem;
  background: var(--panel); border-bottom: 1px solid var(--border);
}
/* Ellipsised: the title is a trip number, a parcel reference or a mat name, and
   a long one must not push the bar wider than the screen. */
.fbar .tt, .cp-panel-bar .tt { flex: 1; min-width: 0; font-size: 1.5rem;
  font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The cutter/sewer app's bar is a flex child of a shell that does not scroll, so
   it cannot move. The Driver app's views ARE the scrollers, so its bar has to
   stick — and bleed through the view's own inset, or it would sit in a frame of
   paper and stop reading as chrome. --fbar-bleed is that inset
   (driver_app.html: main > section); the two have to be changed together, which
   is why it is one value here and not three.

   `top` is NEGATIVE, and has to be. A sticky offset is measured from the
   scrollport's PADDING edge, so top: 0 pins the bar 14px down — the bleed and
   the stick fight each other and the margin looks like it did nothing. Matching
   top to the bleed lets it sit on the border edge and stay there. */
.fbar-flush { --fbar-bleed: 1.4rem;
  position: sticky; top: calc(-1 * var(--fbar-bleed)); z-index: 2;
  margin: calc(-1 * var(--fbar-bleed)) calc(-1 * var(--fbar-bleed)) 1.2rem; }

/* ---- the pill on the right of a card ----
   How this row stands, in one word or one number, at the end of the title line.
   Amber by default because the default case on both apps is "not done yet". */
.fcard-pill, .job-left {
  font-size: 1.2rem; font-weight: 600; white-space: nowrap;
  padding: 0.3rem 0.9rem; border-radius: 20px;
  background: var(--amber-soft); color: var(--amber);
}
.fcard-pill.ok, .job-left.ok { background: var(--green-soft); color: var(--green); }
.fcard-pill.bad { background: var(--red-soft); color: var(--red); }
.fcard-pill.info { background: var(--accent-soft); color: var(--accent); }
/* Nothing to report — a state that is neither progress nor a problem. */
.fcard-pill.idle { background: var(--bg); color: var(--muted); }

/* ---- the back control ----
   The Driver app's form — borderless, accent-coloured, and it NAMES WHERE IT
   GOES ("‹ My Trips") rather than saying "Back". On a phone with no browser
   chrome the destination is the only thing that tells you which level you are
   about to land on.

   The cutter/sewer app's .cp-up had the better ergonomics and they are kept:
   44px of height and a pressed state. The Driver app's original was 14px text
   with 10px of padding — about 40px, under the minimum touch target, with no
   feedback at all when tapped. */
.fback, .cp-up {
  display: inline-flex; align-items: center; gap: 0.3rem;
  min-height: 4.4rem; padding: 0 1rem 0 0; border: none; background: none;
  color: var(--accent); font-family: inherit; font-size: 1.5rem; font-weight: 600;
  cursor: pointer; white-space: nowrap; border-radius: var(--r-md, 8px);
}
.fback:active, .cp-up:active { background: var(--accent-soft); }
/* Inside a bar the control needs symmetric padding; standing alone above a
   body it is flush with the text it sits over. */
.fbar .fback, .cp-panel-bar .cp-up { padding: 0 1rem; min-width: 4.4rem; }

/* ---- the filter sheet (js/driver-app.js) ----
   A sheet over the screen, not a panel wedged into the list. It used to be a
   card that expanded in place, which pushed the first trip below the fold
   exactly when somebody was trying to find it — and the status filter was
   somewhere else entirely, on the summary cards, so "what am I filtered to"
   had two answers in two places.

   One sheet, every filter in it, and NOTHING happens until Apply. A panel that
   refetched on every tap meant three round trips to set two filters, each one
   redrawing the list under a driver's thumb. */
.fsheet { position: fixed; inset: 0; z-index: 40; display: none; }
.fsheet.open { display: block; }
.fsheet-scrim { position: absolute; inset: 0; background: rgba(15, 23, 42, .45); }
/* Bottom sheet: it is reached with a thumb, so it grows from the edge the thumb
   is already at. Capped at 88% so the list stays visible behind it — a driver
   has to be able to see that there IS something being filtered.

   Sized from --shell-w rather than pinned left-and-right to the viewport, so it
   is always exactly as wide as the app it opens over and can never become a
   different program opening on top of it. That token is 100% now, so in
   practice this fills the screen too -- but it is still READ FROM THE TOKEN
   rather than hardcoded, because the day the shell is capped again this has to
   follow it without anybody remembering to look here. Centring is what makes
   the one rule correct at either setting. The fallback matches the shell's own
   default so the two cannot drift apart silently. */
.fsheet-box {
  position: absolute; bottom: 0; max-height: 88%;
  left: 50%; transform: translateX(-50%);
  width: 100%; max-width: var(--shell-w, 100%);
  display: flex; flex-direction: column;
  background: var(--panel); border-radius: 18px 18px 0 0;
  box-shadow: 0 -8px 40px rgba(0, 0, 0, .28);
}
.fsheet-head { flex: none; display: flex; align-items: center; gap: 1rem;
               padding: 1.6rem 1.8rem 1.2rem; }
.fsheet-head .ft { flex: 1; display: flex; align-items: center; gap: 0.9rem;
                   font-size: 1.3rem; font-weight: 600; letter-spacing: .08em;
                   text-transform: uppercase; color: var(--accent); }
.fsheet-head .ft svg { flex: none; }
.fsheet-x { min-height: 4.4rem; min-width: 4.4rem; border: none; background: none;
            color: var(--muted); font-size: 2rem; line-height: 1; cursor: pointer;
            border-radius: var(--r-md, 8px); padding: 0; }
.fsheet-x:active { background: var(--accent-soft); }
/* The sections scroll; the head and the Apply button do not. Apply is the one
   control that must never be scrolled off. */
.fsheet-body { flex: 1; min-height: 0; overflow-y: auto;
               -webkit-overflow-scrolling: touch; padding: 0 1.8rem; }
.fsheet-foot { flex: none; padding: 1.4rem 1.8rem calc(1.6rem + env(safe-area-inset-bottom)); }
/* Accent, not the green .btn-primary carries in the Driver app. Green means one
   thing in that app — Mark Delivered, proof accepted, the parcel is gone — and
   spending it on "redraw the list" would make the two read as the same weight of
   decision. Applying a filter is reversible; delivering a parcel is not. */
.fsheet-foot .btn-primary { width: 100%; height: 5rem; font-size: 1.5rem;
                            letter-spacing: .06em; text-transform: uppercase;
                            background: var(--accent); color: #fff; }
.fsheet-foot .btn-primary:hover { background: var(--accent); filter: brightness(1.08); }

/* ---- one collapsible section of the sheet ----
   Label, then what is currently chosen, then the expander. Collapsed by
   default: the sheet's job is to show at a glance what is filtered, and five
   open sections is a form to read rather than an answer. */
.fsec { border-top: 1px solid var(--border); }
.fsec:first-child { border-top: none; }
.fsec-head { display: flex; align-items: center; gap: 1rem; width: 100%;
             min-height: 5.6rem; padding: 0; border: none; background: none;
             font-family: inherit; cursor: pointer; text-align: left; }
.fsec-head .fl { flex: 1; font-size: 1.25rem; font-weight: 600;
                 letter-spacing: .06em; text-transform: uppercase;
                 color: var(--text); }
/* What this section currently holds. A count when it is a set of choices, the
   value itself when it is a range — the same pill either way, so the eye finds
   the answer in the same place down the whole sheet. A zero is dimmed rather
   than hidden, so the sheet does not change shape as filters are set. */
.fsec-head .fv { flex: none; font-size: 1.15rem; font-weight: 600;
                 padding: 0.3rem 1rem; border-radius: 20px;
                 background: var(--accent-soft); color: var(--accent); }
.fsec-head .fv.zero { background: var(--bg); color: var(--muted); }
/* + and −, drawn rather than typed: a glyph that changes between two
   characters of different widths makes the row shift as it opens. */
.fsec-head .fx { flex: none; position: relative; width: 2.2rem; height: 2.2rem; }
.fsec-head .fx::before, .fsec-head .fx::after {
  content: ''; position: absolute; background: var(--accent);
  border-radius: 2px; transition: opacity .12s;
}
.fsec-head .fx::before { left: 0.3rem; right: 0.3rem; top: 1rem; height: 2px; }
.fsec-head .fx::after { top: 0.3rem; bottom: 0.3rem; left: 1rem; width: 2px; }
.fsec.open .fsec-head .fx::after { opacity: 0; }   /* + becomes − */
.fsec-body { padding: 0 0 1.4rem; }
.fsec:not(.open) .fsec-body { display: none; }

/* A choice inside a section. The whole row is the target, not the 20px box —
   this is tapped in a moving vehicle. */
.fopt { display: flex; align-items: center; gap: 1.2rem; width: 100%;
        min-height: 4.6rem; padding: 0; border: none; background: none;
        font-family: inherit; font-size: 1.45rem; color: var(--text);
        text-align: left; cursor: pointer; }
.fopt:active { background: var(--accent-soft); }
.fopt .fbox { flex: none; width: 2.1rem; height: 2.1rem; border-radius: 5px;
              border: 1.5px solid var(--border-strong); position: relative; }
.fopt.on .fbox { background: var(--accent); border-color: var(--accent); }
/* The tick, drawn as two borders of a rotated box: no glyph, so it cannot be
   the wrong weight in a font that has not loaded yet. */
.fopt.on .fbox::after {
  content: ''; position: absolute; left: 0.6rem; top: 0.2rem; width: 0.6rem; height: 1.1rem;
  border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(40deg);
}
/* Radio behaviour — one of a set, like a period. Round rather than square, so
   "you get one of these" is visible before anything is tapped. */
.fopt.radio .fbox { border-radius: 50%; }
.fopt.radio.on .fbox::after {
  left: 0.5rem; top: 0.5rem; width: 0.7rem; height: 0.7rem; border: none;
  border-radius: 50%; background: #fff; transform: none;
}
.fopt .fn { flex: 1; }
/* How many rows this choice would get you. Dimmed, because it is the reason to
   tap rather than the thing being tapped. */
.fopt .fc { flex: none; font-size: 1.2rem; font-weight: 600; color: var(--muted); }
.fopt.on .fc { color: var(--accent); }

/* A pair of dates inside a section. */
.frange { display: flex; align-items: center; gap: 0.8rem; padding-top: 0.4rem; }
.frange input[type="date"] { flex: 1; min-width: 0; font: inherit; font-size: 1.4rem;
  padding: 1rem; border: 1px solid var(--border); border-radius: 9px;
  background: var(--bg); color: inherit; min-height: 4.4rem; }
.frange .dash { flex: none; color: var(--muted); }
.fhint { font-size: 1.2rem; color: var(--muted); margin-top: 0.8rem; line-height: 1.4; }

/* ---- the control that opens the sheet ----
   Full-width, because it is the only control above the list now and a driver
   should not have to aim. Says what is filtered rather than only that
   something is: "3 filters" sends somebody into the sheet to find out which. */
/* text-decoration: none because this is worn by <a> as well as <button> — the
   cutter app's Quick Access rows are real links, so long-press and
   open-in-new-tab behave normally, and a UA underline made them a second kind
   of row sitting under identical ones that had none. */
.fopen { display: flex; align-items: center; gap: 0.9rem; width: 100%;
         min-height: 4.6rem; padding: 1rem 1.4rem; margin-bottom: 1.2rem;
         border: 1px solid var(--border); border-radius: var(--radius);
         background: var(--panel); box-shadow: var(--shadow);
         font-family: inherit; font-size: 1.35rem; font-weight: 600;
         color: var(--text); text-align: left; text-decoration: none;
         cursor: pointer; }
.fopen:active { background: var(--accent-soft); }
.fopen.on { border-color: var(--accent); color: var(--accent); }
/* NOT .fsum — that is the summary CARD above the list, and a span sharing the
   name inherited its grid, its panel fill and its border inside this bar. */
.fopen .fopen-sum { flex: 1; min-width: 0; overflow: hidden;
                    text-overflow: ellipsis; white-space: nowrap; }
.fopen svg { flex: none; }
/* Clear, on the right of the same row. Its own tap target, and only there when
   there is something to clear. */
.fclear { flex: none; min-height: 3.4rem; padding: 0.6rem 1.2rem; border-radius: 20px;
          border: 1px solid var(--border); background: var(--bg);
          font-family: inherit; font-size: 1.25rem; font-weight: 600;
          color: var(--muted); cursor: pointer; }
.fclear:active { background: var(--accent-soft); }

/* ---- the Profile tab (js/profile.js) ----
   The third footer tab on both field apps, added 25 Aug 2026. One set of rules
   because there is one screen: profile.js draws the same markup into
   #view-profile on the Driver app and #tab-profile on the cutter/sewer app, so
   a person who drives some days and sews others meets the same form.

   It lives here rather than in either app's own <style> block for the reason the
   whole kit exists: two copies of a password form is two places for the touch
   targets to drift apart.

   THE PASSWORD FORM IS IN A SHEET (26 Aug 2026), not on the tab. It sat open
   under the identity card for a day, which put a permanent password form on a
   screen whose everyday job is answering "what is my staff code" and "which van
   am I on" — and left a half-typed password one glance away on a tablet that
   gets passed around. The sheet is .fsheet above, unchanged and unforked: its
   scrolling body with a pinned foot is exactly what keeps Update on screen when
   a phone keyboard takes the bottom half of the display.

   So the .fprof-fld / .fprof-show rules below now style content INSIDE
   .fsheet-box, and .fsheet-foot .btn-primary already owns the Update button —
   which is why there is no .fprof-save override for it any more.

   Everything tappable clears 44px. This is operated with a gloved thumb, and a
   password is the one thing on these apps that cannot be corrected by trying
   again — a mistyped confirm box is three fields to retype. */
.fprof-title { font-size: 1.9rem; font-weight: 600; letter-spacing: var(--track);
               margin: 0 0 1.2rem; }
/* text-transform and color are SET, not left to inherit, and this is not
   decoration: commission_dashboard.html carries a bare `h2 { font-size: 12px;
   color: var(--muted); text-transform: uppercase }` for its own section labels,
   and these headings are <h2> inside that page. The class wins on the four
   properties it declared and lost on the two it did not, so the same component
   rendered as small grey capitals on the cutter app and as this on the Driver
   app, which has no h2 rule at all. A shared kit must not depend on what the
   page around it happens to set — the same lesson as .fprof-fld input's colour
   below, found the same way. */
.fprof-h { font-size: 1.5rem; font-weight: 600; letter-spacing: var(--track);
           margin: 0 0 0.6rem; text-transform: none; color: var(--text); }
/* A heading that opens a SECTION rather than titling a card. The rows beneath
   sit 8px apart, which is the gap WITHIN a section and far too little to say a
   new one has started. */
.fprof-sec { margin-top: 1.8rem; }
/* The identity card. Read-only on purpose — see the header of profile.js. */
.fprof-id { padding: 1.6rem 1.4rem; }
.fprof-name { font-size: 2rem; font-weight: 600; letter-spacing: var(--track);
              line-height: 1.2; }
.fprof-role { font-size: 1.3rem; color: var(--accent); font-weight: 600;
              margin-top: 0.2rem; }
.fprof-rows { margin-top: 1.4rem; }
/* Label left, value right, on one line each: a staff code and an email are
   looked UP, not read through, and a two-line stack of them makes the eye
   travel twice as far to find the one being checked. */
.fprof-row { display: flex; align-items: baseline; justify-content: space-between;
             gap: 1.2rem; padding: 0.8rem 0; border-top: 1px solid var(--border); }
.fprof-row .k { flex: none; font-size: 1.1rem; color: var(--muted);
                text-transform: uppercase; letter-spacing: .05em; }
/* Breakable, because an email address is the one value here long enough to push
   a 375px screen sideways. */
.fprof-row .v { min-width: 0; font-size: 1.4rem; text-align: right;
                word-break: break-word; }
.fprof-note { font-size: 1.25rem; color: var(--muted); line-height: 1.45;
              margin: 1rem 0 0; }

/* One labelled box. The <label> wraps its input, so the label text is part of
   the tap target rather than a decoration beside it. */
.fprof-fld { display: block; margin-top: 1.2rem; }
.fprof-fld > span { display: block; font-size: 1.2rem; color: var(--muted);
                    margin-bottom: 0.5rem; }
/* 16px, and this is not a style choice: iOS Safari zooms the whole page in on
   focus for anything smaller, and a zoomed password form on a phone leaves the
   Update button off the right of the screen.

   var(--text), NOT `inherit`. The Driver app's own stylesheet carries
   `label { color: var(--muted) }`, and since this input sits INSIDE its label,
   `inherit` handed it that grey — a password typed in #8a8f98 on a phone in a
   van, measured in the browser rather than guessed at. A shared kit must not
   depend on what the page around it happens to set. */
.fprof-fld input, .fprof-fld select, .fprof-fld textarea {
                   width: 100%; min-height: 4.6rem; font: inherit; font-size: 1.6rem;
                   padding: 1rem 1.2rem; border: 1px solid var(--border);
                   border-radius: var(--r-md); background: var(--bg);
                   color: var(--text); }
.fprof-fld textarea { min-height: 8.4rem; resize: vertical; }
/* Reveal, for a password typed with a thumb and no way to check it. A toggle
   rather than a permanent plain-text field: this is used on a shared warehouse
   tablet with people standing behind it. */
.fprof-show { min-height: 4.4rem; margin-top: 1rem; padding: 0 1.4rem;
              border: 1px solid var(--border); border-radius: var(--r-md);
              background: var(--panel); color: var(--text);
              font-family: inherit; font-size: 1.3rem; font-weight: 600;
              cursor: pointer; }
.fprof-show:active { background: var(--accent-soft); }
/* ---- the control that opens the sheet ----
   .fopen, the field kit's own opener — the same shape the Driver app's filter
   bar uses, so "tap this, a sheet comes up" is one gesture in these apps and not
   two. Only the chevron is added: a filter bar summarises its own state, and
   this has no state to summarise, so the right-hand slot says "there is more
   behind this" instead. */
.fprof-open .fopen-sum { flex: 1; }
.fprof-open svg { color: var(--muted); }
/* Tighter than a bare .fopen. That 12px is right for the Driver app's filter
   bar, which is ONE control with a list under it; these come in runs of two and
   three under a heading, and at 12px a section read as three separate things
   rather than one group. Scoped to .fprof-open so the filter bar keeps its
   own spacing. */
.fprof-open { margin-bottom: 0.8rem; }

/* Full width and 50px, matching the filter sheet's Apply: it is the only
   decision on the sheet, so it should not have to be aimed at.

   THE UPDATE BUTTON IS NOT STYLED HERE. It lives in .fsheet-foot, and
   `.fsheet-foot .btn-primary` above already gives it the full width, the 50px
   and — the part that matters — the ACCENT fill. Green is what .btn-primary
   carries in the Driver app and it means one thing there: Mark Delivered, proof
   accepted, the parcel is gone. A one-class override loses that tie on source
   order (driver_app.html's <style> block ships after this file), which is how a
   delivery-green password button reached the browser on 25 Aug 2026 with the
   tests passing. Two selectors is what wins, and the sheet's foot already is
   two. Leave it there.

   What is left here is the RETRY button on the could-not-load state, which is
   not in a sheet and therefore does need its own two classes. */
.fprof-save.btn-primary { width: 100%; min-height: 5rem; margin-top: 1.4rem;
              font-size: 1.5rem; background: var(--accent); color: #fff; }
.fprof-save.btn-primary:hover { background: var(--accent); filter: brightness(1.08); }
/* Greyed while the change is in flight, so a disabled button does not still
   read as one that can be pressed — a second tap sends the old password. */
.fsheet-foot .btn-primary:disabled, .fprof-save:disabled { opacity: .6; }
/* The terminal state, after a change that worked. The form is replaced by this
   rather than left standing with a green line under it: nothing on screen is
   still asking to be filled in, and no password is left in a box. */
.fprof-done-t { font-size: 1.6rem; font-weight: 600; letter-spacing: var(--track);
                color: var(--green); }
/* Empty until there is something to say — an errand that succeeded silently is
   what makes people press a button twice. :empty keeps the card from reserving
   a gap for a message that has not happened. */
.fprof-msg { font-size: 1.3rem; line-height: 1.45; margin-top: 1rem; }
.fprof-msg:empty { display: none; }
.fprof-msg.ok { color: var(--green); }
.fprof-msg.err { color: var(--red); }

/* ---- Address map pin (js/pinpicker.js) ----
   Tall enough to recognise a junction and a row of shoplots, which is what
   confirming a pin actually takes; any shorter and it is a decoration. */
.pin-map { height: 240px; margin-top: 8px; border: 1px solid var(--line);
           border-radius: 8px; overflow: hidden; }
/* Leaflet sets its own z-index stack; keep it under the app's modals so a map
   cannot sit on top of the camera or a confirm panel. The control CORNERS are
   the ones that actually bite — .leaflet-top/.leaflet-bottom ship at z-index
   1000, which beats .modal-overlay at 60, so the zoom buttons showed through
   any panel opened over the map. */
.pin-map .leaflet-pane, .pin-map .leaflet-control,
.pin-map .leaflet-top, .pin-map .leaflet-bottom { z-index: 1; }
.pin-map .leaflet-container { font: inherit; }

/* ---- Malaysian phone field (core.js phoneField) ----
   The +60 is furniture, not an input: it is drawn as part of the box so the
   country code is visibly always there, but there is nothing to select, clear
   or mistype. The inner input owns the border-radius on its side only, so the
   pair still reads as one control. */
.phone-input { display: flex; align-items: stretch; border: 1px solid var(--border);
               border-radius: 8px; overflow: hidden; background: #fff; }
.phone-input .phone-cc { display: flex; align-items: center; padding: 0 10px;
               background: var(--bg); border-right: 1px solid var(--border);
               font-size: 13px; color: var(--muted); font-weight: 600; user-select: none; }
.phone-input input { flex: 1; min-width: 0; border: none; outline: none; background: none;
               padding: 8px 10px; font-size: 13px; font-family: inherit; color: inherit; }

/* ---- Modal (js/core.js openModal) ----
   For a picker that interrupts what you were doing — Add Package is chosen,
   acted on and dismissed, and appending it under the list it feeds pushed the
   trip's own footer off the screen while you scrolled to find it. */
.modal-overlay { position: fixed; inset: 0; background: rgba(15,23,42,.45); z-index: 60;
                 display: flex; align-items: flex-start; justify-content: center;
                 padding: 5vh 16px; overflow-y: auto; }
.modal-box { background: var(--card, #fff); border-radius: 12px; width: 100%; max-width: 980px;
             box-shadow: 0 20px 60px rgba(0,0,0,.28); overflow: hidden; }
.modal-head { display: flex; align-items: center; gap: 12px; padding: 16px 20px;
              border-bottom: 1px solid var(--border); position: sticky; top: 0;
              background: inherit; z-index: 1; }
.modal-head h2 { margin: 0; font-size: 16px; font-weight: 550; flex: 1; }
.modal-close { border: none; background: none; font-size: 22px; line-height: 1; cursor: pointer;
               color: var(--muted); padding: 0 4px; font-family: inherit; }
.modal-body { padding: 20px; }
.modal-foot { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px;
  padding-top: 16px; border-top: 1px solid var(--border); }
.modal-note { font-size: 13px; color: var(--muted); margin: 0 0 14px; }
@media (max-width: 700px) {
  .modal-overlay { padding: 0; }
  .modal-box { max-width: none; min-height: 100%; border-radius: 0; }
}

/* ---- Fluid form layout (.um-form.um-grid) ----
   .um-form was one field per row inside a fixed 520–640px column. On a desktop
   panel that left two-thirds of the width empty and pushed Save below the fold
   on forms of only five or six fields.

   The column COUNT is not fixed either, because a fixed one is just a narrower
   version of the same problem: two columns on a 1600px screen still leaves half
   the panel blank, and two columns on a phone is unusable. auto-fit fits as many
   ~270px columns as the panel is actually wide enough for — four on a desktop,
   three on a laptop, two on a tablet, one on a phone — with no breakpoints to
   keep in step with anything.

   The min() is not decoration: a bare minmax(270px, 1fr) refuses to go below
   270px, so on a 320px phone the grid overflows the screen sideways instead of
   collapsing to one column. */
.um-grid {
  display: grid; gap: 0 16px; align-items: start;
  /* Read the track size from the inside out:
       calc((100% - 3*16px) / 4)  a quarter of the panel, gaps taken off. Making
                                  this the MINIMUM track is what caps the form at
                                  four columns — a fifth cannot fit by definition
                                  — while still letting those four stretch to
                                  fill a 1920px screen. Past four, a form stops
                                  reading as a form and starts reading as a
                                  spreadsheet.
       max(270px, …)              …unless a quarter is narrower than a usable
                                  field, at which point 270px takes over and the
                                  count drops to three, two, one on its own.
       min(100%, …)               the phone guard. Without it the 270px floor is
                                  absolute and a 320px screen scrolls sideways
                                  instead of collapsing to a single column. */
  grid-template-columns: repeat(auto-fit,
    minmax(min(100%, max(270px, calc((100% - 3 * 16px) / 4))), 1fr));
}
.um-grid > .fld { margin-bottom: 10px; min-width: 0; }
/* A field whose content needs the room — a note, a search box, a table. */
.um-grid > .wide { grid-column: 1 / -1; }
/* A set of fields revealed together — the Lalamove pair — starts a line of its
   own. They appear and disappear with the Driver choice, and letting them flow
   into whatever slot happened to be free meant the rest of the form reshuffled
   around them every time; on their own row the fields above never move.
   
   The inner track formula is the parent's, so the pair sizes by viewport like
   everything else. auto-FILL, not auto-fit: auto-fit would collapse the tracks
   the pair has no field for and stretch the two selects across the whole row,
   which leaves them twice the width of every field above and the column edges
   no longer lining up. Keeping the empty tracks costs some blank space on the
   right and buys a form whose columns are straight all the way down.

   The toggle sets style.display to '' or 'none': '' falls back to the grid
   here, and 'none' still hides the subtree, so both states behave. */
.um-grid .fld-group {
  grid-column: 1 / -1;
  display: grid; gap: 0 16px;
  grid-template-columns: repeat(auto-fill,
    minmax(min(100%, max(270px, calc((100% - 3 * 16px) / 4))), 1fr));
}
.um-grid .um-actions { grid-column: 1 / -1; display: flex; gap: 8px;
                       align-items: center; margin-top: 4px; }
.um-grid .adj-msg { grid-column: 1 / -1; margin-top: 2px; }
/* A form field that is disabled because the record is past editing should read
   as settled, not as broken. Same text colour, calmer ground, no text cursor. */
.um-form input:disabled, .um-form select:disabled {
  background: var(--bg); color: var(--text); opacity: 1; cursor: default; }

/* 40px of padding is right for an empty table filling a screen; inside a form
   it is a hole between two controls. */
.empty.compact { padding: 16px; text-align: left; }

/* ---- Lalamove delivery card (js/driver.js lalamoveCard) ----
   One quote or one booking. The price and what it buys lead, because that is
   the decision; the stops are the one thing still worth a permanent section.

   The price breakdown used to be its own section here too — a header, two or
   three lines and a total, always open, between the headline and Stops. It
   was the one thing on the card nobody reads twice a minute after quoting,
   taking the same vertical space as the two things that get checked
   constantly. Standardised into a hover/click popover off the price instead
   (CS, 24 Aug 2026): a card with three quotes stacked is now three prices and
   three specs, not three invoices. */
.llm-card { border: 1px solid var(--border); border-radius: 10px; background: var(--panel);
            margin-bottom: 10px; overflow: hidden; }
.llm-top { display: flex; align-items: flex-start; gap: 14px; flex-wrap: wrap;
           padding: 12px 14px; }
.llm-headline { flex: 1; min-width: 200px; }
.llm-price { font-size: 20px; font-weight: 550; letter-spacing: var(--track); }
.llm-spec { font-size: 12px; color: var(--muted); margin-top: 2px; }
.llm-acts { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
/* ---- Add Package picker (driver.js) ----
   Two lookups, STACKED — webstore orders, then stock transfers. Each is a
   title on its own line, a search box on the next, and the chosen package's
   details underneath. Side by side was an earlier version and it read as two
   competing halves of one decision; they are two decisions, and only one of
   them is usually being made.

   The popup is a FIXED SIZE, and its body is the scroller. It used to be a
   strip with nothing picked and a page with something picked, so the Add
   button moved every time somebody changed their mind.

   That scroller is why the menu is position:fixed rather than absolute: an
   overflow ancestor clips an absolutely positioned descendant — which is how
   .modal-box's own overflow:hidden cropped the first version of this dropdown
   — while a fixed one is measured against the viewport and cannot be cropped
   by anything in the modal. driver.js:placePkgMenu puts it under its field and
   moves it with the body's scroll. */
.modal-box.pkg-modal { height: min(78vh, 760px); display: flex; flex-direction: column; }
.modal-box.pkg-modal .modal-body { flex: 1; overflow-y: auto; }
.pkg-picker { min-height: 240px; }
.pkg-sec + .pkg-sec { margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--border); }
/* The section title owns its line: the box under it is the thing being used,
   and a label sitting beside it reads as a caption on the field rather than a
   heading over the pair. */
.pkg-sec-h { margin: 0 0 8px; font-size: 13px; font-weight: 600; }
.pkg-find { position: relative; }
.pkg-find input { width: 100%; }
/* left/top/width/max-height are set by placePkgMenu, from the field's own
   position on screen — only the browser knows where that ended up. */
.pick-menu { display: none; position: fixed; z-index: 70;
             background: var(--card); border: 1px solid var(--border); border-radius: 8px;
             overflow-y: auto; overscroll-behavior: contain; }
.pick-menu.open { display: block; }
.pick-row { display: flex; gap: 10px; align-items: baseline;
            padding: 7px 10px; border-bottom: 1px solid var(--border); cursor: pointer;
            font-size: 13px; }
.pick-row:last-child { border-bottom: none; }
/* The name is the second column and takes what is left: a list is read by
   scanning down the numbers, so those stay hard against the left edge. */
.pick-row .nm { color: var(--muted); flex: 1; min-width: 0;
                overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pick-row:hover { background: var(--accent-soft); }
.pick-row.muted { cursor: default; }
.pick-row.muted:hover { background: none; }
.pick-hint { font-size: 11px; min-height: 14px; margin-top: 6px; }
/* The details of the picked one, opened under its own box. */
.pkg-detail { margin-top: 10px; }
/* The row that acts on the chosen package: count, then Add, then the aside. */
.pkg-add-row { margin-top: 12px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

.llm-sec { border-top: 1px solid var(--border); padding: 10px 14px; }
.llm-sec-h { font-size: 10px; text-transform: uppercase; letter-spacing: .05em;
             color: var(--muted); font-weight: 700; margin-bottom: 6px; }
.llm-lines { display: grid; grid-template-columns: 1fr auto; gap: 2px 16px;
             font-size: 12px; max-width: 320px; }
.llm-lines .v { text-align: right; font-variant-numeric: tabular-nums; }
.llm-lines .tot { border-top: 1px solid var(--border); padding-top: 4px; margin-top: 2px;
                  font-weight: 600; }
/* ---- the breakdown popover ----
   Its own hover/focus zone, not the price alone: the price and the trigger
   button share it, so glancing at the figure and aiming for the small icon
   both count. The popover sits flush against the zone's bottom edge with NO
   gap (top: 100%, nothing added) — a gap is where a hover popover always
   breaks, because the pointer crosses dead space between the trigger and the
   panel and the hover state drops before it arrives. Same reasoning
   .bulk-menu and .ac-list already use for their own dropdowns, just applied
   to a zone the mouse can enter from open air instead of from a button. */
.llm-bd { position: relative; display: inline-flex; align-items: center; gap: 5px; }
.llm-bd-btn { display: grid; place-items: center; width: 20px; height: 20px; flex: 0 0 auto;
              border: none; background: none; padding: 0; margin: 0; border-radius: 50%;
              color: var(--muted); cursor: pointer; }
.llm-bd-btn svg { width: 14px; height: 14px; }
.llm-bd-btn:hover, .llm-bd.open .llm-bd-btn { color: var(--text); background: var(--accent-soft); }
.llm-bd-pop { display: none; position: absolute; top: 100%; left: 0; min-width: 230px;
              background: #fff; border: 1px solid var(--border); border-radius: var(--r-md);
              z-index: 20; padding: 12px 14px; }
.llm-bd:hover .llm-bd-pop, .llm-bd:focus-within .llm-bd-pop, .llm-bd.open .llm-bd-pop {
  display: block; }
.llm-stop { display: grid; grid-template-columns: 62px 1fr auto; gap: 10px;
            align-items: start; padding: 5px 0; font-size: 13px; }
.llm-stop + .llm-stop { border-top: 1px dashed var(--border); }
.llm-stop .seq { font-size: 11px; color: var(--muted); padding-top: 2px; }
.llm-stop .addr { font-size: 12px; color: var(--muted); }
/* The per-stop message to the Lalamove rider. Sits under the address inside
   the stop row's middle column, so it lines up with the address it qualifies
   rather than starting a column of its own. */
.llm-note { margin-top: 6px; }
.llm-note label { display: block; font-size: 11px; color: var(--muted); margin-bottom: 3px; }
.llm-note-in { width: 100%; box-sizing: border-box; font: inherit; font-size: 12px;
               padding: 6px 8px; border: 1px solid var(--border);
               border-radius: var(--r-md); resize: vertical; }
/* What the rider was actually sent, on a delivery already booked. Read-only,
   and set apart from the address above it so the two are not confused. */
.llm-sent-note { white-space: pre-wrap; border-left: 2px solid var(--border);
                 padding-left: 7px; margin-top: 4px; }
.llm-foot { border-top: 1px solid var(--border); padding: 8px 14px; font-size: 11px;
            color: var(--muted); display: flex; gap: 12px; flex-wrap: wrap; }
/* The number Lalamove support asks for first and an invoice line is matched on,
   so it is selectable in one click and legible at a glance. */
.llm-ref { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
           font-weight: 600; background: var(--bg); border: 1px solid var(--border);
           border-radius: 6px; padding: 2px 8px; user-select: all; display: inline-block; }
@media (max-width: 820px) {
  .llm-bd-btn { width: 32px; height: 32px; }
  .llm-bd-btn svg { width: 16px; height: 16px; }
}

/* ---- the (i), anywhere ----------------------------------------------------
   .llm-bd above, generalised — see infoTip() in core.js for what belongs
   behind one and what does not. Kept beside it rather than merged into it:
   that one is a price breakdown with its own grid inside, this one holds a
   sentence, and collapsing the two would make every future change to either
   a change to both.

   OPENS THREE WAYS. Hover for a mouse, focus-within for a keyboard, and
   .open for a tap — the warehouse tablets have no hover at all, which is the
   whole reason this is not a `title` attribute. */
.itip { position: relative; display: inline-flex; align-items: center;
        vertical-align: middle; }
.itip-btn { display: grid; place-items: center; width: 20px; height: 20px;
        flex: 0 0 auto; border: none; background: none; padding: 0; margin: 0;
        border-radius: 50%; color: var(--muted); cursor: pointer; }
.itip-btn svg { width: 14px; height: 14px; }
.itip-btn:hover, .itip.open .itip-btn { color: var(--text);
        background: var(--accent-soft); }
/* LEFT-ANCHORED, and the first version was not — it read "an (i) sits at the
   END of a label, so anchor the panel's right edge to it", which is true of
   a label at the right of a wide row and wrong for every (i) this app
   actually has. They sit a few characters in from the LEFT of a 520px modal,
   so a 260px panel hung leftward from there starts at a negative x: the
   first one shipped opening half off the modal, clipped against the window.
   Hanging rightward from an icon near the left edge has 200px+ of room.

   The other end is a real limit and is left un-handled on purpose: an (i)
   placed in the right third of a wide row would overflow, and CSS alone
   cannot flip it without measuring. Nothing in the app does that yet, and a
   modifier class nobody applies is a claim that something needs one. Add it
   when a screen actually does. */
.itip-pop { display: none; position: absolute; top: 100%; left: 0; right: auto;
        width: 260px; max-width: 70vw; background: #fff;
        border: 1px solid var(--border); border-radius: var(--r-md);
        box-shadow: var(--shadow-md); z-index: 30; padding: 10px 12px;
        font-size: 12px; font-weight: 400; line-height: 1.5;
        color: var(--text-2); white-space: normal; text-align: left; }
.itip:hover .itip-pop, .itip:focus-within .itip-pop, .itip.open .itip-pop {
  display: block; }
@media (max-width: 820px) {
  .itip-btn { width: 32px; height: 32px; }
  .itip-btn svg { width: 16px; height: 16px; }
}

/* ================= foundation =================
   Appended last on purpose. Everything here is a cross-cutting rule that has to
   win over the per-module blocks above, and the file's cascade order is load
   bearing (see the note at the top), so this sits at the end rather than being
   threaded through twenty sections. Nothing in here is module-specific: if a
   rule below only applies to one screen, it is in the wrong place. */

/* ---- keyboard focus -------------------------------------------------------
   There was no focus style at all before this, which meant tabbing through a
   form was invisible: the browser default outline is suppressed by the reset on
   most of these controls. :focus-visible rather than :focus so a MOUSE click on
   a button does not leave a ring behind it, which is what makes authors remove
   focus styles in the first place.

   Kamatto red at 2px with an offset, over an inset white gap, so the ring is
   legible on the page ground, on white panels and on the graphite filled
   buttons alike. */
:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
/* On a filled red button the red ring would disappear into it, so that one case
   gets a white gap between button and ring instead. */
.btn-primary:focus-visible, .pf-chip.active:focus-visible, .cl-chan.active:focus-visible {
  outline-color: var(--text);
  box-shadow: 0 0 0 2px var(--panel);
}

/* ---- one form control -----------------------------------------------------
   Six modules wrote their own input rule (.inv-toolbar, .adj-form, .um-form,
   .toolbar select, .pw-row, .rs-lvl) and they disagreed on border, radius, font
   size and height, which is most of why the app read as several apps. This is
   the single treatment for all of them.

   :where() keeps specificity at zero, so any module rule above that genuinely
   needs to differ still wins without needing !important. */
:where(input:not([type=checkbox]):not([type=radio]):not([type=file]),
       select, textarea) {
  font-family: inherit; font-size: 13px; color: var(--text);
  background: var(--panel);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  padding: 7px 10px;
  line-height: 1.4;
  transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}
input:not([type=checkbox]):not([type=radio]):focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--brand);
  /* An inset ring rather than an outline offset: these sit inside dense filter
     bars and table cells where 2px of outside ring would overlap the control
     beside them. */
  box-shadow: 0 0 0 3px rgba(200,16,46,.14);
}
/* --muted, measured at 5.17:1 on white, rather than a lighter grey drawn to
   look like a placeholder. Anything light enough to read as "not filled in"
   fails AA, and the distinction is carried anyway by entered text being
   --text at 16:1 against this. */
::placeholder { color: var(--muted); opacity: 1; }
/* Checkboxes and radios pick up the brand colour rather than the browser's own
   blue, which was the last piece of the old palette still visible. */
input[type=checkbox], input[type=radio] { accent-color: var(--brand); cursor: pointer; }
label { cursor: default; }

/* ---- floating surfaces ----------------------------------------------------
   --shadow is deliberately almost invisible because cards should separate with
   a hairline. Anything that genuinely floats over the page needs more than
   that, or it reads as part of what it is covering. */
.bulk-menu, .ac-list, .modal-box, .llm-bd-pop { box-shadow: var(--shadow-md); }
.modal-box { box-shadow: var(--shadow-lg); }
.drawer { box-shadow: var(--shadow-lg); }
.modal-overlay { background: rgba(21,25,30,.42); }
.drawer-overlay { background: rgba(21,25,30,.3); }

/* ---- empty states ---------------------------------------------------------
   An empty table said nothing but "No records", centred in 40px of white, which
   is indistinguishable from a screen that failed to load. */
.empty { font-size: 13px; color: var(--muted); }

/* ---- selection ------------------------------------------------------------ */
::selection { background: rgba(200,16,46,.16); color: var(--text); }

/* ---- reduced motion -------------------------------------------------------
   Everything animated in here is 125ms of convenience. For anyone who has asked
   the OS for less of it, none of it is worth keeping: the transform nudge on
   :active, the drawer slide, the nav chevron. Transitions are cut rather than
   shortened so no state is left half-applied. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: .01ms !important;
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
  }
  .btn-primary:active, .btn-outline:active, .btn-sm:active, .btn-danger:active,
  .lch-tile:hover { transform: none; }
}

/* ---- topbar account cluster ----
   The two links that sit on every screen. Quiet by default because they are
   furniture, and a hairline divider between them rather than a middle dot so
   they do not read as one label. */
/* 8px, not 14: these are bordered controls now, and a wide gap between two
   controls that belong together reads as two unrelated things. The divider
   that used to sit between them is gone with the text treatment — a hairline
   between two hairlined boxes is a third line for no reason. */
.topbar-acct { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }

/* ---- pager on touch screens ----------------------------------------------
   The pager buttons are 31px tall, which is fine for a mouse and under the
   44px a finger needs. The warehouse floor runs on tablets, and this is the
   control somebody presses forty times going through a picklist.

   Below 560px the four buttons also share the row equally instead of wrapping
   into a ragged second line: at 375px they wrapped 3 + 1, which put Last on a
   line of its own directly under Next and made the two easy to confuse. */
@media (max-width: 820px) {
  .inv-pager button { min-height: 44px; padding: 6px 14px; }
  .pg-size select { min-height: 44px; }
}
@media (max-width: 560px) {
  .inv-pager { justify-content: flex-start; }
  .pg-nav { flex: 1 1 100%; }
  .pg-nav button { flex: 1; min-width: 0; padding: 6px 6px; }
}

/* ================= application frame =================
   The layer that makes this read as software rather than as a styled page:
   a header that stays put, a rail with a width you choose, a row height you
   choose, a keyboard route to any screen, and loading states shaped like the
   answer. Appended last for the same cascade reason as the foundation block.

   Everything here is chrome. Nothing in this section knows about orders,
   inventory or any other module. */

/* ---- row height ----------------------------------------------------------
   Four tokens, set once, read by every table and control in the app. A module
   that uses them inherits the app's rhythm; one that hardcodes its own padding
   silently opts out, which is the whole reason they are tokens.

   COMPACT IS THE ONLY SETTING (CS, 24 Aug 2026). There was a comfortable
   default and a header button to switch, on the argument that a gloved thumb
   on a warehouse tablet needs the taller row. It does not: no table row in
   this app is a tap target — every row's actions are real buttons inside it,
   and those are lifted to 44px under 820px by the touch calibration block
   further down. So the tall default was costing every desk screen a third of
   its rows to protect a tap that never happens.

   One row height also means one screen to reason about. The button had to
   remember its state, label its own next state, and every screenshot or
   support call started with "which density are you on".

   Anything that wants the old roomier figures should scale from these, not
   reintroduce a second mode. */
:root {
  --row-y: 6px;       /* table cell padding, vertical */
  --row-x: 12px;      /* table cell padding, horizontal */
  --row-x-inset: 8px; /* ...for a table already inside padded content */
  --ctl-y: 5px;       /* inputs, selects, small buttons */
  --bar-y: 9px;       /* toolbars and panel headers */
}
th, td { padding: var(--row-y) var(--row-x); }
.panel h2 { padding: var(--bar-y) var(--row-x); }
.toolbar, .inv-toolbar { padding: var(--bar-y) var(--row-x); }
.inv-pager { padding: var(--bar-y) var(--row-x); }
:where(input:not([type=checkbox]):not([type=radio]):not([type=file]),
       select, textarea) { padding: var(--ctl-y) 10px; }
/* The type comes down with the rows. Cells six pixels apart with 14px text set
   solid is denser than it is readable; the two have to move together. These
   four were the compact overrides and are now simply the values. */
td { font-size: 13px; }
.tile { padding: 10px 13px 11px; }
.tile .value { font-size: 21px; }
.panel { margin-bottom: 12px; }

/* ---- header -------------------------------------------------------------
   Sticky. Scrolling a fifty-row list used to carry the screen's name and its
   actions off the top of the window, which is how you end up pressing Sync on
   the screen you thought you had left. Opaque ground and a hairline, because
   table rows scroll underneath it. */
.topbar {
  position: sticky; top: 0; z-index: 20;
  background: var(--bg);
  margin: -24px -28px 20px; padding: 14px 28px 12px;
  align-items: flex-end;
}
.topbar-id { display: grid; gap: 2px; min-width: 0; }
.topbar h1 { line-height: 1.15; }
/* The trail. Small and muted: it answers "where am I" for someone who arrived
   by link or by palette, and is furniture for everyone else. */
.crumb { display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  font-size: 11.5px; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .06em; }
.crumb:empty { display: none; }
.crumb i { font-style: normal; color: var(--border-strong); font-weight: 400; }
.topbar-tools { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }

/* The palette's own trigger. Shaped like a search field rather than a button
   because that is what it opens, and carrying its shortcut so the shortcut is
   discoverable without documentation. */
.cmd-trigger {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px 6px 10px; min-width: 170px;
  background: var(--panel); border: 1px solid var(--border-strong);
  border-radius: var(--r-md); cursor: pointer; font-family: inherit;
  font-size: 13px; color: var(--muted); text-align: left;
  transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.cmd-trigger:hover { border-color: var(--muted); color: var(--text); }
.cmd-trigger svg { width: 15px; height: 15px; flex: 0 0 auto; }
.cmd-trigger-label { flex: 1; }
kbd {
  font-family: var(--font-mono); font-size: 10.5px; font-weight: 600;
  padding: 2px 5px; border-radius: 4px; background: var(--accent-soft);
  border: 1px solid var(--border); color: var(--text-2); white-space: nowrap;
}

/* ---- the account controls -------------------------------------------------
   My Account and Log out. They were 12.5px muted text with a hairline divider
   between them, which is the same treatment this file gives a caption — and
   nobody could tell they were clickable (CS, 24 Aug 2026).

   They are now the palette trigger's shape: panel ground, one hairline, the
   10px radius. That is already the header's vocabulary for "a control", so
   three controls in a row read as three controls instead of one control and
   two labels. Labelled, not icon-only, for the same reason the button they
   replaced is gone: an icon in a header is a guess.

   Log out is quiet at rest and turns danger red on hover. It ends the session,
   which is worth signalling on approach — but a red control sitting in the
   header all day is an alarm nobody would keep reading. */
.acct-btn {
  display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto;
  min-height: 34px; padding: 0 11px;
  background: var(--panel); border: 1px solid var(--border-strong);
  border-radius: var(--r-md); cursor: pointer;
  font-size: 12.5px; font-weight: 600; color: var(--text);
  text-decoration: none; white-space: nowrap;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
              color var(--dur) var(--ease);
}
.acct-btn svg { width: 15px; height: 15px; flex: 0 0 auto; color: var(--muted);
  transition: color var(--dur) var(--ease); }
.acct-btn:hover { background: var(--accent-soft); border-color: var(--muted);
  color: var(--text); }
.acct-btn:hover svg { color: var(--text-2); }
/* A long name must not push the log-out control off the header. */
.acct-name { max-width: 148px; overflow: hidden; text-overflow: ellipsis; }
.acct-out { color: var(--text-2); }
.acct-out:hover { background: var(--danger-soft); border-color: var(--danger-line);
  color: var(--danger); }
.acct-out:hover svg { color: var(--danger); }

/* ---- rail ---------------------------------------------------------------- */

/* Pinned to the bottom of the rail, always, whatever the tree above it is
   doing. `margin-top: auto` in the sidebar's flex column is what does it: the
   nav takes the space that is left and this sits under it against the floor.

   Before this it was in normal flow after a scrolling nav, so on a short tree
   it floated up under the last item and on a long one it was below the fold —
   and it MOVED as sections were opened and closed, which is the thing that made
   it hard to find twice in a row.

   The hairline is now a real edge rather than a separator between two blocks of
   nav, so it reads as the floor of the rail. */
.nav-foot { flex: 0 0 auto; margin-top: auto; padding-top: 12px;
            border-top: 1px solid var(--border); }
.nav-collapse {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px 10px; border: none; background: none; cursor: pointer;
  border-radius: var(--r-md); font-family: inherit; font-size: 12.5px;
  font-weight: 550; color: var(--muted);
}
.nav-collapse svg { width: 18px; height: 18px; flex: 0 0 auto; }
.nav-collapse:hover { background: var(--accent-soft); color: var(--text); }

/* Collapsed rail, desktop only. Below 820px the sidebar is already a fixed
   icon rail with its own toggle (see the small-screens block above), and two
   competing width rules would fight; this is scoped above that breakpoint. */
@media (min-width: 821px) {
  .rail-narrow .sidebar { width: 62px; padding: 18px 8px 20px; overflow-x: hidden; }
  .rail-narrow .sidebar .brand b,
  .rail-narrow .sidebar .nav-label,
  .rail-narrow .sidebar .chev,
  .rail-narrow .sidebar .nav-items,
  .rail-narrow .sidebar .nav .dot { display: none; }
  /* Same top/bottom padding as the expanded row (4px 8px 18px) — only the
     horizontal padding drops to 0, since the box is centred either way — so
     the row's total height does not change when the rail collapses. */
  .rail-narrow .sidebar .brand { justify-content: center; padding: 4px 0 18px; }
  .rail-narrow .sidebar .nav a,
  .rail-narrow .sidebar .nav-head,
  .rail-narrow .sidebar .nav-collapse { justify-content: center; padding: 11px 0; }
  /* The chevron points the other way once collapsed, since the button now
     expands. */
  .rail-narrow .nav-collapse svg { transform: scaleX(-1); }
}

/* ---- jump to a screen ---------------------------------------------------
   Anchored a third of the way down rather than centred: the results grow
   downward, and a centred dialog moves its own input as it fills. */
.cmd-overlay {
  position: fixed; inset: 0; z-index: 120; background: rgba(21,25,30,.46);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 12vh 16px 16px;
}
.cmd-overlay[hidden] { display: none; }
.cmd-box {
  width: 100%; max-width: 520px; background: var(--panel);
  border: 1px solid var(--border); border-radius: var(--r-xl);
  box-shadow: var(--shadow-lg); overflow: hidden;
  display: flex; flex-direction: column; max-height: 70vh;
}
.cmd-field { display: flex; align-items: center; gap: 10px; padding: 14px 16px;
  border-bottom: 1px solid var(--border); flex: 0 0 auto; }
.cmd-field svg { width: 17px; height: 17px; color: var(--muted); flex: 0 0 auto; }
/* Borderless: the dialog itself is the field, and a box inside a box reads as
   two controls. */
.cmd-field input { flex: 1; min-width: 0; border: none; background: none;
  padding: 0; font-size: 15px; color: var(--text); }
.cmd-field input:focus { outline: none; box-shadow: none; }
.cmd-esc { border: 1px solid var(--border); background: var(--accent-soft);
  color: var(--text-2); border-radius: 4px; padding: 3px 6px; font-size: 10.5px;
  font-weight: 600; font-family: inherit; cursor: pointer; }
.cmd-results { overflow-y: auto; padding: 6px; flex: 1 1 auto; }
.cmd-row { display: flex; align-items: baseline; gap: 10px; width: 100%;
  padding: 9px 11px; border: none; background: none; border-radius: var(--r-md);
  cursor: pointer; font-family: inherit; text-align: left; }
.cmd-name { font-size: 14px; font-weight: 550; color: var(--text); }
.cmd-sec { font-size: 11px; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .05em; margin-left: auto;
  white-space: nowrap; }
/* One highlight, driven by the keyboard. Hover does not paint a second one:
   with both, the mouse resting anywhere makes it ambiguous which row Enter
   takes. */
.cmd-row.at { background: var(--accent-soft); }
.cmd-row.at .cmd-name { color: var(--brand); }
.cmd-none { padding: 22px 12px; text-align: center; font-size: 13px; color: var(--muted); }
.cmd-foot { display: flex; gap: 16px; padding: 9px 16px; flex: 0 0 auto;
  border-top: 1px solid var(--border); background: var(--bg);
  font-size: 11px; color: var(--muted); }
.cmd-foot span { display: flex; align-items: center; gap: 5px; }
.cmd-foot kbd { font-size: 10px; background: var(--panel); }

/* ---- loading ------------------------------------------------------------
   The shape of the answer, in the table that is about to hold it. The pulse is
   slow and low-contrast on purpose: this sits under the eye for a second or
   two, and anything faster reads as an error. */
.skel { display: block; height: 11px; border-radius: 3px;
  background: var(--accent-soft); animation: skel-pulse 1.4s ease-in-out infinite; }
.skel-row td { border-bottom: 1px solid var(--border); }
.skel-row:hover td { background: transparent; }
@keyframes skel-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) {
  .skel { animation: none; opacity: .8; }
}

/* ---- toast --------------------------------------------------------------
   Was inline-styled in core.js against the old palette, so it stayed the one
   near-black pill in an app that had moved on. Bottom right rather than centre:
   centred, it covered the pager of whatever list had just been acted on. */
#kh-toast {
  position: fixed; bottom: 22px; right: 22px; left: auto; transform: none;
  max-width: min(420px, calc(100vw - 44px));
  background: var(--text); color: #fff; padding: 11px 16px;
  border-radius: var(--r-md); font-size: 13px; font-weight: 500;
  box-shadow: var(--shadow-lg); z-index: 130; transition: opacity var(--dur) var(--ease);
}

@media (max-width: 820px) {
  /* The header's negative margins are keyed to the desktop .main padding. */
  .topbar { margin: -16px -14px 16px; padding: 12px 14px 10px; }
  /* The trigger's label and shortcut go; the magnifier stays. A 170px search
     box beside a page title does not fit a tablet, and the palette is a
     keyboard feature anyway. */
  .cmd-trigger { min-width: 0; padding: 0; width: 40px; height: 40px;
    justify-content: center; }
  .cmd-trigger-label, .cmd-trigger kbd { display: none; }
  .acct-btn { min-height: 40px; }
  .topbar-tools { gap: 8px; }
  .cmd-overlay { padding: 6vh 12px 12px; }
}

/* ================= shared patterns =================
   Written down once, having been repeated inline at 79 call sites. Each of these
   existed already as a habit; the only thing missing was a name. A pattern with
   a name can be changed everywhere at once, which is the whole difference
   between a design system and a convention. */

/* The wrapper around a table too wide for its panel. Was
   `<div style="overflow-x:auto">` at 49 sites across the markup and eight
   modules.

   overscroll-behavior-x matters on the warehouse tablets: without it, flicking
   a wide table sideways past its end hands the gesture to the browser, which
   reads it as a back-swipe and leaves the screen. */
.tbl-wrap { overflow-x: auto; overscroll-behavior-x: contain; }

/* The second line under a primary value: the buyer under an order number, the
   email under a customer's name, the SKU under a product. Was
   `class="sk" style="font-size:11px;color:var(--muted)"` at nine sites, which is
   the class and its own definition written out side by side.

   .inv-item-cell .sk stays as it is: an iSKU is a code that gets read character
   by character and wants the mono face. */
.sk { display: block; font-size: 11px; color: var(--muted); font-weight: 400; }
/* On a table cell the class is only the muted 11px type, never a box. Without
   this, display:block takes the cell out of its row: it sizes itself instead of
   the row, so its bottom border and the cells either side of it stop lining up
   — which is what Add Order's iSKU column was doing. */
td.sk, th.sk { display: table-cell; }

/* Channel dots in the nav, on the six listing screens where the colour says
   which marketplace the screen is for. The other twenty-one nav entries used to
   carry one too, coloured by section, which is the section heading directly
   above them said again in a form nobody can read. */
.nav .dot.shopee { background: var(--shopee); }
.nav .dot.shopee_sg { background: var(--shopee_sg); }
.nav .dot.lazada { background: var(--lazada); }
.nav .dot.tiktok { background: var(--tiktok); }
.nav .dot.shopify { background: var(--shopify); }
.nav .dot.kamatto_pos { background: var(--kamatto_pos); }
/* The 22px indent on .nav-items a (in the base block above) already sets the
   sub-item's alignment, and it now applies to every entry rather than being
   nudged further by a dot on some of them. The six that keep a dot pull it back
   into the indent so all thirty-eight labels start on the same line. */
.nav-items a .dot { margin-left: -12px; }

/* ================= row selection =================
   A count in a dropdown label was the only sign that twenty orders were
   selected, and it was only visible with the Bulk Tools menu on screen. Ticking
   rows and then scrolling left no indication that a selection existed at all,
   which is how a bulk cancel reaches orders somebody had forgotten they picked.

   This is a bar that appears when a selection does, says exactly what is
   selected, and carries the way out. Fixed to the bottom rather than the top:
   the toolbar at the top is where filters live, and a bar that pushes the table
   down every time a row is ticked makes the row under the cursor move.

   z-index 61, not 40: the sign-off queues (Pending Approval, Needs a Second
   Look) render their own checkboxes inside an open modal (.modal-overlay is
   60), and a bulk bar a viewer can't reach because the modal is drawn on top
   of it is worse than no bulk bar at all. Same "above .modal-overlay" pattern
   already used by .wsp-popover-backdrop above. */
.selbar {
  position: fixed; z-index: 61; left: 50%; bottom: 20px; transform: translateX(-50%);
  display: none; align-items: center; gap: 14px;
  padding: 10px 12px 10px 16px; max-width: calc(100vw - 32px);
  background: var(--text); color: #fff;
  border-radius: var(--r-xl); box-shadow: var(--shadow-lg);
}
.selbar.on { display: flex; }
.selbar-count { font-size: 13px; font-weight: 600; white-space: nowrap;
  font-variant-numeric: tabular-nums; }
/* Buttons on a near-black bar cannot be the app's outline button, which is a
   white box with a grey edge. These are the same shapes in reverse. */
.selbar .btn-sm, .selbar .btn-outline {
  background: rgba(255,255,255,.12); border-color: rgba(255,255,255,.28); color: #fff;
}
.selbar .btn-sm:hover:not(:disabled), .selbar .btn-outline:hover:not(:disabled) {
  background: rgba(255,255,255,.2); border-color: rgba(255,255,255,.45);
}
/* Clear is a link, not a third button: it undoes rather than does. */
.selbar-clear { background: none; border: none; color: rgba(255,255,255,.72);
  font-family: inherit; font-size: 12.5px; font-weight: 550; cursor: pointer;
  padding: 4px 2px; text-decoration: underline; text-underline-offset: 2px; }
.selbar-clear:hover { color: #fff; }
/* Machine Registry's own bulk bar (1 Oct 2026): explains why
   Deactivate/Reactivate dropped out rather than leaving that silent --
   one button cannot mean two different things for two different rows in
   the same click, so a mixed Active/Inactive selection only offers Print
   QR, and says so. */
.selbar-note { font-size: 11.5px; color: rgba(255,255,255,.6); white-space: nowrap; }
/* A selected row is marked on the row, not only in the checkbox. Scanning a
   page of forty for the four ticked boxes is not scanning, it is searching. */
tbody tr.sel td { background: var(--accent-soft); }
tbody tr.sel td:first-child { box-shadow: inset 2.5px 0 0 var(--brand); }

@media (max-width: 560px) {
  .selbar { left: 12px; right: 12px; transform: none; max-width: none;
    flex-wrap: wrap; justify-content: space-between; }
}

/* ================= type and control calibration =================
   Measured off brex.com rather than taken from the token file, because the two
   disagree and the page is what is real. Three rules came out of it, and all
   three are about restraint.

   WHAT DID NOT TRANSFER, and why. Their page is a marketing site and this is a
   console, so most of its scale is wrong here on purpose:
     * 72px H1 and 48px section heads. A screen title stays at 21px: it names
       where you are, it is not the message of the page.
     * 16px body. A table of order rows at 16px fits four fewer columns.
     * A static, transparent nav. An app needs the header pinned.
     * The orange accent, the inverted dark sections, the logo wall, the hero.
   What did transfer is the part that is about craft rather than about being a
   landing page. */

/* ---- 1. one tracking ratio, everywhere ---------------------------------- */
body { letter-spacing: var(--track); }
/* The four display roles state the ratio in their own rule (see the base block
   above); these are the ones that had no tracking of their own at all. */
.rec-hd, .dr-name, .modal-head h2, .cmd-name, .asm-sec > h2 { letter-spacing: var(--track); }
/* Uppercase micro-labels are the exception, and the only one. Tracking a
   10-11px capital letterform tighter closes the counters and turns SOLD OUT
   into a smudge; caps want the opposite treatment, which the rules that set
   them already do. */
th, .tile .label, .crumb, .pg-size, .llm-sec-h, .proc-rail-hd, .qc-defect-opt .who,
.set-section h3, .adj-form label, .inv-toolbar .flt > label { letter-spacing: .05em; }

/* ---- 2. weight discipline ------------------------------------------------
   brex.com sets 500 on every heading on the page: 72px/500, 48px/500, 24px/500,
   14px/500 labels. Body is 400. There are exactly two 600s on the whole page.
   Authority comes from size and tracking, not from weight.

   This file had drifted to 650 and 700 on titles, figures and buttons, which at
   14px is most of the difference between "confident" and "shouting". Everything
   drops one step. Uppercase micro-labels keep their weight, for the same reason
   they keep their tracking: small caps genuinely need it to hold together. */
.asm-sec > h2 { font-weight: 550; }
.btn-primary, .btn-danger, .btn-mini { font-weight: 500; }
.btn-outline, .btn-sm { font-weight: 500; }
.nav a.active { font-weight: 550; }
.pf-chip.active, .cl-chan { font-weight: 500; }
.badge, .flag { font-weight: 550; }
.selbar-count { font-weight: 550; }
.cmd-name { font-weight: 500; }

/* ---- 3. display type set solid ------------------------------------------
   Their 72px headline sits on a 72px line, their 48px on 48px. Display type
   set solid is a deliberate look and it costs nothing here, where the figures
   in a KPI tile are one line by definition. Body copy keeps its 1.5: prose set
   solid is unreadable, which is the whole reason the two are different. */
/* Figures are one line by definition, so they can be set solid. Body copy keeps
   its 1.5: prose set solid is unreadable, which is the whole reason the two are
   different. The page title carries its own 1.1 in the base block. */
.tile .value { line-height: 1; }

/* ---- 4. the button height ladder ----------------------------------------
   Their buttons are 36px, 40px and 48px tall with horizontal-only padding:
   `0 12px`, `0 16px`, `0 20px`. Height is set, not accumulated out of line-box
   plus vertical padding.

   That is worth copying for a reason beyond looks. A row of controls built from
   vertical padding comes out at whatever height each one's font and line-height
   happen to produce, so a select, a button and a date input in the same filter
   bar end up 33px, 35px and 34px and never quite line up. A fixed height makes
   the bar a bar. */
.btn-primary, .btn-outline, .btn-danger {
  height: 36px; padding: 0 16px; display: inline-flex; align-items: center;
  justify-content: center; gap: 6px;
}
.btn-sm, .btn-mini, .btn-link {
  height: 30px; padding: 0 11px; display: inline-flex; align-items: center;
  justify-content: center; gap: 5px;
}
/* Controls in a toolbar share the button height, which is what the fixed height
   bought. */
.toolbar select, .toolbar input, .inv-toolbar select, .inv-toolbar input,
.proc-head-right select, .pg-size select { height: 36px; }
.inv-pager button { height: 32px; padding: 0 12px; }
/* The touch sizes stay ahead of the desktop ones. 36px is a comfortable mouse
   target and not a comfortable thumb target. */
@media (max-width: 820px) {
  .btn-primary, .btn-outline, .btn-danger { height: 44px; padding: 0 18px; }
  .btn-sm, .btn-mini, .btn-link { height: 38px; }
  .toolbar select, .toolbar input, .inv-toolbar select, .inv-toolbar input,
  .proc-head-right select, .pg-size select { height: 44px; }
  .inv-pager button { height: 44px; }
}
/* A fixed height and a stretched grid cell disagree; these two are laid out by
   their parent and keep their own sizing. */
.qc-defect-opt { height: auto; }
.um-grid .um-actions .btn-primary, .um-grid .um-actions .btn-outline { flex: 0 0 auto; }

/* ---- 5. the shadow that is really a hairline ----------------------------
   Their cards carry `rgba(66,87,138,.15) 0 1px 0 0`: a border drawn as a shadow
   on one edge only, so a card reads as sitting on the page rather than floating
   over it. Applied to the tiles, which are the one place in this app with a row
   of equal cards that all want the same weight of separation. */
.tile { box-shadow: 0 1px 0 0 var(--border); }

/* Form controls do not inherit letter-spacing: a button, input and select all
   reset to `normal` however the body is tracked, which left every control on
   the page at a different ratio from the text beside it. Measured on the review
   harness: the page title came out at -0.42px and the button next to it at
   -0.078px.

   Named explicitly rather than through `inherit` so a control inside an
   uppercase micro-label does not pick up that label's positive tracking. */
button, input, select, textarea, .btn-primary, .btn-outline, .btn-sm, .btn-mini,
.btn-danger, .btn-link, .pf-chip, .cl-chan, .inv-tab, .cmd-trigger,
.selbar-clear, .nav-collapse, .inv-pager button {
  letter-spacing: var(--track);
}
/* The label is a small capital one and wants the loose tracking; the numbers in
   its select are not, and 15/30/50 spaced apart reads as a gap rather than a
   value. */
.pg-size { letter-spacing: .05em; }
.pg-size select, .pg-size option { letter-spacing: var(--track); }

/* An em letter-spacing resolves to pixels at the element that declares it, and
   then inherits as those pixels. So `body { letter-spacing: -.02em }` hands
   -0.28px to everything below it, and any element with a smaller font-size is
   then tracked tighter than the ratio: .sk at 11px inside a 14px cell measured
   -0.0255em, not -0.02em.

   Which means a constant ratio is not something the cascade can do for you. Any
   rule that changes font-size has to restate the token. These are the
   size-changing text classes in this file; the check is mechanical, so a new one
   is easy to spot -- if a rule sets font-size and its text is not uppercase, it
   wants this line too. */
.sk, .tile .sub, .tile .label, .muted-sm, .dr-sku, .dr-grid .v, .cmd-sec,
.llm-spec, .llm-ref, .selbar-count, .selbar-clear, .chip, .inv-count,
.inv-item-cell .sk, .pod-lbl, .perm-level, .scan-status,
.lch-ds, .empty, .banner, .crumb,
.acct-btn, .cmd-trigger, kbd, .pg, .badge, .flag, .pl-badge, .qc-flag,
.st-live, .mp-badge, .kit-tag, .wh-badge, .wh-row .wm, .rs-lvl,
.modal-note, .cmd-foot, .nav-collapse {
  letter-spacing: var(--track);
}
/* The uppercase micro-labels, restated for the same reason and in the other
   direction: they change size too, so they would inherit the tight ratio. */
th, .tile .label, .crumb, .pg-size, .llm-sec-h, .proc-rail-hd,
.qc-defect-opt .who, .set-section h3, .adj-form label,
.inv-toolbar .flt > label, .pod-lbl, .lch-badge {
  letter-spacing: .05em;
}

/* <b> is how every table in this app marks its identifying cell: the order
   number, the customer, the vendor, the transfer no. The browser resolves that
   to 700, which nothing here ever chose -- it is the UA default, and it was
   putting the heaviest weight on the page into every row of every list while
   the headings above them sat at 550.

   600 keeps the cell unmistakably the identifier and stops it outweighing the
   screen title. brex.com uses 600 exactly twice on its whole front page, both
   times for this same job of marking one value among many.

   Left as <b> rather than converted at ~90 call sites: the markup is correct,
   it is the weight that was never a decision. */
b, strong { font-weight: 600; }

/* ================= INSTRUMENT =================
   The direction, applied. The language is a car's instrument binnacle, which
   Kamatto sells into rather than borrows from: bone paper, a black panel for
   the figures, one red needle, gauge grey for the ticks and labels.

   Light ground on purpose. A cockpit look tempts you into dark mode, and these
   screens are read on tablets on a warehouse floor in daylight all shift. The
   dark is spent where it earns its keep: the panel holding the number the
   screen exists to answer. */

:root {
  /* Semantic colour has to change on a dark panel. The greens and reds tuned
     for a white surface are 2:1 against the binnacle; these are the same hues
     lifted until they carry. */
  --green-on-dark: #3fbe86;
  --red-on-dark: #ff6b6b;
  --amber-on-dark: #e0a44a;
}

/* ---- 1. the three faces, by role ---------------------------------------- */

/* Screen titles. Bebas is uppercase-only and condensed, so it takes both more
   size and POSITIVE tracking: the app tracks everything at -0.02em, which on a
   condensed capital closes the counters and turns ALL ORDERS into a fence.
   This one face is the exception, and it is the only one. */
.topbar h1 {
  font-family: var(--font-display);
  font-size: 27px; font-weight: 400; line-height: 1;
  letter-spacing: .03em; text-transform: uppercase;
  color: var(--text);
}

/* Every figure, code and identifier. A monospace lines a column up by
   construction, and its slashed zero is the difference between 0 and O in a
   tracking number read off a printed label. */
.tile .value, .gauge .value, .gauge-lead .value, .gauge-delta,
td.right, .inv-item-cell .sk, .dr-sku, .llm-price, .llm-ref, .llm-lines .v,
.pg, .selbar-count, .tile .sub, .cmd-foot kbd, kbd {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
/* Mono runs wide, so the figures give back a little of the tracking. */
.tile .value, .gauge .value, .gauge-lead .value { letter-spacing: var(--track-figure); }

/* ---- 2. the gauge cluster ------------------------------------------------
   The Overview's answer, laid out as what it is: one figure that leads and
   three that give it context. Four tiles of equal weight made the reader decide
   which one mattered every time they opened the screen.

   Asymmetric by ratio rather than by breakpoint, so the lead keeps its
   proportion at every width until there is no room for two columns at all. */
.gauges {
  display: grid; gap: 12px; margin-bottom: 20px;
  grid-template-columns: minmax(0, 1.85fr) minmax(0, 1fr);
  align-items: stretch;
}
.gauge-stack { display: grid; gap: 12px; grid-template-rows: repeat(3, 1fr); }

/* The binnacle. Dark, because this is the one figure worth spending the
   contrast on, and because a black panel on bone paper is the whole visual
   idea in one element. */
.gauge-lead {
  background: var(--binnacle); color: #fff;
  border-radius: var(--r-lg); padding: 20px 22px 18px;
  display: flex; flex-direction: column; gap: 2px;
  position: relative; overflow: hidden;
}
/* The gauge ticks. Informational rather than decorative: they mark the panel as
   the instrument face, and they are the one ornament in the palette. Drawn with
   a gradient rather than elements so there is nothing in the DOM to maintain. */
.gauge-lead::after {
  content: ""; position: absolute; left: 22px; right: 22px; bottom: 0; height: 7px;
  background: repeating-linear-gradient(to right,
    var(--gauge) 0 1px, transparent 1px 14px);
  opacity: .45;
}
.gauge-lead .label {
  font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .1em; color: var(--gauge);
}
.gauge-lead .value {
  font-size: clamp(30px, 4.4vw, 44px); font-weight: 500; line-height: 1.02;
  margin-top: 8px; color: #fff;
}
.gauge-lead .sub { font-size: 12px; color: var(--gauge); margin-top: 4px; }
/* The needle: the one figure on the panel that is allowed a colour. */
.gauge-delta {
  margin-top: auto; padding-top: 14px;
  font-size: 14px; font-weight: 500; letter-spacing: var(--track);
}

/* The supporting three. Paper-white, hairlined, deliberately quieter: same
   numbers, sized as context. */
.gauge {
  background: var(--panel); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: 11px 14px;
  display: flex; flex-direction: column; justify-content: center;
}
.gauge .label {
  font-size: 10.5px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .09em; color: var(--muted);
}
.gauge .value {
  font-size: 19px; font-weight: 500; line-height: 1.1; margin-top: 3px;
  color: var(--text);
}
.gauge .sub {
  font-size: 11px; color: var(--muted); margin-top: 2px; line-height: 1.35;
  font-family: var(--font-sans);
}

/* Below the tablet breakpoint there is no room for two columns of figures, so
   the lead goes full width and the three become a row under it. */
@media (max-width: 820px) {
  .gauges { grid-template-columns: 1fr; }
  /* Exactly three, because there are exactly three: Orders, Average order and
     vs previous. auto-fit was leaving a collapsed fourth track and, with it, one
     trailing gap of dead space on the right of the row. A grid should have as
     many cells as there is content for. */
  .gauge-stack { grid-template-rows: none; grid-template-columns: repeat(3, 1fr); }
  .gauge-lead .value { font-size: 34px; }
}
/* Three across stops working somewhere around a phone: at 375px they are 105px
   each, which is narrower than "Average order" plus its figure. */
@media (max-width: 560px) {
  .gauge-stack { grid-template-columns: 1fr; }
  .gauge { padding: 10px 12px; }
}

/* ---- 3. the rail, as the binnacle surround ------------------------------
   Dark. The rail is chrome and it frames the working surface, which is exactly
   what a binnacle surround does; it also means the one dark panel on the
   Overview reads as continuous with the frame rather than as a stray black box.

   The working area stays paper, so nothing anybody reads for eight hours is
   light-on-dark. */
.sidebar {
  background: var(--binnacle);
  border-right: 1px solid rgba(255,255,255,.07);
}
.brand b { color: #fff; }
.nav a, .nav-head { color: #b9bec6; }
.nav a:hover, .nav-head:hover { background: rgba(255,255,255,.06); color: #fff; }
/* The active item is the lit one on the panel: paper ground, red label, red
   edge. It is the only light surface in the rail, which is what makes it read
   as selected without needing a second signal. */
.nav a.active {
  background: var(--bg); color: var(--brand);
  box-shadow: none;
}
.nav a.active::before { background: var(--brand); }
.nav-head.active, .nav-head.active:hover { background: var(--bg); color: var(--brand); }
.nav-head .chev { color: var(--gauge); }
.nav-foot { border-top-color: rgba(255,255,255,.09); }
.nav-collapse { color: var(--gauge); }
.nav-collapse:hover { background: rgba(255,255,255,.06); color: #fff; }
/* The mobile rail is the same dark surround rather than the white it used to
   be, so the two states of the same element are the same element. */
@media (max-width: 820px) {
  .sidebar { background: var(--binnacle); }
  .rail-toggle {
    background: var(--binnacle-2); border-color: rgba(255,255,255,.14); color: #fff;
  }
}

/* ---- 4. surfaces on paper ------------------------------------------------
   The working area is the lighter of the two paper tones, so a panel lifts off
   the ground without needing a shadow to say so. */
.panel, .tile, .lch-tile, .llm-card, .task-card,
.pl-card, .bin-card, .qc-card, .perm-box, .cmd-box, .modal-box, .drawer {
  background: var(--panel);
}
.topbar { background: var(--bg); }
/* A table's header sits on the panel, not on the ground: it scrolls under the
   sticky page header and has to stay opaque against the rows beneath it. */
th { background: var(--panel); }

/* ---- 5. the needle, kept rationed --------------------------------------
   Unchanged from the rule this app already follows, restated because the
   direction makes the temptation worse: red is an action or the thing you are
   on. Everything selected or hovered is neutral. On bone paper that neutral is
   warm, which is the only part that moved. */
tbody tr:hover td { background: var(--accent-soft); }
tbody tr.sel td { background: var(--accent-soft); }
.cmd-row.at { background: var(--accent-soft); }
.wh-row.sel { background: var(--accent-soft); }

/* ================= interactive states, completed =================
   Audited by reading the stylesheet back rather than by eye: five interactive
   components had no :hover rule at all, and twenty-four had no :active. Focus
   was already covered globally by :focus-visible.

   Two kinds of press feedback, and the distinction matters. Anything that reads
   as a BUTTON moves down 1px, which is the cheapest confirmation available on a
   tablet with no cursor. Anything that is a ROW in a list does not move: a table
   row that shifts under the finger reads as a rendering fault, so those deepen
   their tint instead. */

/* ---- things with no hover state at all ---------------------------------- */

/* Filter chips and channel pills. Both have a clear selected state and had
   nothing at all for the pointer, so on a bar of six there was no telling which
   one you were about to pick. */
.pf-chip:not(.active):hover, .cl-chan:not(.active):hover {
  background: var(--accent-soft); border-color: var(--border-strong); color: var(--text);
}
/* Already-selected: the hover is a darkening of the fill, not a change of it. */
.pf-chip.active:hover, .cl-chan.active:hover { background: var(--text-2); border-color: var(--text-2); }

/* The palette rows. The keyboard highlight is the authority here, so hover is
   deliberately quieter than .at: with two equal highlights the mouse resting
   anywhere makes it ambiguous which row Enter takes. */
.cmd-row:hover:not(.at) { background: rgba(20,23,28,.05); }

/* Any row that is a link to a detail screen. The cursor already says it is
   clickable; this says which one. */
.clickrow:hover td { background: var(--accent-soft); }

/* QC cards. A grid of forty near-identical mat photos, where the reviewer needs
   to know which one the click will take. */
.qc-card:hover:not(.sel) { border-color: var(--border-strong); }

/* ---- press feedback: buttons ------------------------------------------- */
.btn-link:active:not(:disabled), .btn-mini:active:not(:disabled),
.acct-btn:active, .cmd-trigger:active, .inv-pager button:active:not(:disabled),
.selbar-clear:active, .nav-collapse:active, .rail-toggle:active,
.pf-chip:active, .cl-chan:active, .inv-tab:active, .llm-bd-btn:active,
.modal-close:active, .dr-close:active, .cmd-esc:active {
  transform: translateY(1px);
}
/* Cards that behave as buttons. They lift on hover, so the press takes the lift
   back rather than pushing further down: the movement has to reverse the hover
   or the two read as unrelated. */
.lch-tile:not([disabled]):active { transform: translateY(0); box-shadow: var(--shadow); }

/* ---- press feedback: rows ---------------------------------------------- */
/* No movement. A deeper tint of the same hover colour, so press reads as more
   of the same signal rather than a different one. */
.cmd-row:active, .clickrow:active td, .wh-row:active, .cm-row:active,
.bulk-menu a:active, .ac-item:active,
.qc-defect-opt:active, .track-row:active {
  background: rgba(20,23,28,.09);
}
/* A QC card is pressed with a thumb on a tablet, forty times in a run, and it
   toggles rather than navigates. The border goes to ink on press so the tap
   registers even when the finger is covering most of the card. */
.qc-card:active { border-color: var(--text); }

/* ---- states on the dark rail ------------------------------------------
   The rail is now the binnacle, so its states are lifts out of black rather
   than tints into white. A hover built from a light tint disappears on it. */
.nav a:active, .nav-head:active { background: rgba(255,255,255,.11); }
/* The focus ring has to change colour here too: Kamatto red at 2px on near-black
   is a 2.4:1 ring, which is not a focus indicator. White on the rail, red
   everywhere else. */
.sidebar :focus-visible { outline-color: #fff; }

/* ---- disabled, stated once --------------------------------------------
   Scattered across a dozen rules at three different opacities. A disabled
   control has to look settled rather than broken, and it must not answer to
   hover at all. */
:where(button, .btn-primary, .btn-outline, .btn-sm, .btn-danger, .btn-link,
       .btn-mini):disabled {
  cursor: default; transform: none;
}
:where(button):disabled:hover { background: inherit; border-color: inherit; }

/* ---- [hidden] has to win --------------------------------------------------
   The button rules above set `display: inline-flex`, which is more specific
   than the browser's own [hidden] { display: none }. So `el.hidden = true` on
   any .btn-sm, .btn-mini or .btn-link did nothing at all: the control stayed on
   screen, and tapping it did nothing either, because whatever it was hidden for
   was not there to do.

   Found on the cutter and sewer app, 24 Aug 2026: the job screen's Back button
   is hidden on the scanner, where there is no screen to go back to. It was
   fully visible and completely dead, and the only way out of the screen was the
   Close button beside it.

   Specificity, not !important: [hidden] on the class is 0,2,0 and beats the
   0,1,0 rule above it, so nothing else has to be reasoned about.

   .al-row was briefly on this list too (2 Oct 2026, the same trap: an
   out-of-range date warning that was a `.al-row` and so never actually
   disappeared when `hidden` was set). Removed the same day -- the warning
   itself was replaced by a date-range picker modal that cannot select an
   out-of-range span in the first place, so there is nothing left to hide. */
.btn-sm[hidden], .btn-mini[hidden], .btn-link[hidden] { display: none; }


/* ---- the wordmark ------------------------------------------------------
   Sized by height so it sits on the same baseline as the text beside it
   whatever the artwork's aspect turns out to be; width auto keeps 4.09:1
   honest. A fixed width/height pair in the markup as well, so the row does not
   reflow when the SVG arrives.

   Inverted on the rail. The brand supplies a black-on-transparent wordmark and
   a white one, but the white variant carries a baked #000000 plate, which on
   the #0e1116 binnacle would show as a slightly darker rectangle. Inverting the
   black one gives white on transparent, which is what the rail actually needs
   and keeps one asset rather than two. */
.brand-mark { height: 44px; width: auto; display: block; flex: 0 0 auto; }
.sidebar .brand-mark { filter: invert(1); }
/* Expanded rail: the wordmark, and no monogram. Centred in the row now that
   it is the only thing in it. */
.brand .logo { display: none; }
.brand { gap: 8px; justify-content: center; }

@media (min-width: 821px) {
  /* Collapsed: the monogram comes back, because a 4:1 wordmark does not fit in
     62px and cropping a logo is worse than not showing it. */
  .rail-narrow .sidebar .brand .logo { display: grid; }
  .rail-narrow .sidebar .brand-mark { display: none; }
}
@media (max-width: 820px) {
  /* Same swap on the tablet rail, which is collapsed by default. */
  .sidebar .brand .logo { display: grid; }
  .sidebar .brand-mark { display: none; }
  .sidebar.open .brand .logo { display: none; }
  .sidebar.open .brand-mark { display: block; }
}

/* ---- Maintenance screens on a tablet -------------------------------------
   Equipment Maintenance, Machine Registry, Spare Parts and Issue Categories
   are the four screens a technician works in standing next to the machine, on
   a shared Android tablet, and all four are eight-column tables. Below 560px
   each row becomes a card and every cell carries its own heading, read from
   the data-label the renderers already write — a horizontal scrollbar in a
   warehouse is a column nobody ever finds.

   Scoped to .mnt-screen rather than applied to every table: the desktop-only
   screens have layouts that assume a real table, and this block was lost once
   already by being general enough to look removable. Originally the four
   Maintenance view containers in index.html; #hd-body (Helpdesk) carries the
   class too, since it's the same eight-ish-column table shape and the same
   "opened on a phone by mistake" problem. */
@media (max-width: 560px) {
  .mnt-screen .tbl-wrap { overflow-x: visible; }
  .mnt-screen table { display: block; min-width: 0; }
  .mnt-screen table thead { display: none; }
  .mnt-screen table tbody { display: block; }
  .mnt-screen table tr { display: flex; flex-direction: column;
       border: 1px solid var(--border); border-radius: var(--radius);
       margin-bottom: 10px; padding: 4px 0; }
  .mnt-screen table tr:last-child { margin-bottom: 0; }
  .mnt-screen table tr:hover td { background: none; }
  /* The same cell padding the tokens give every other table (see "THE TABLE"):
     a card row is a re-LAYOUT of a table cell, not a second scale for it. */
  .mnt-screen table td { display: flex; justify-content: space-between;
       align-items: flex-start; gap: 12px; border: 0; }
  /* The column heading, per cell. Only cells that were given a data-label get
     one; the action cell is labelled "" on purpose and stays a plain row of
     buttons rather than growing an empty heading above them. */
  .mnt-screen table td::before { content: attr(data-label); font-size: 10.5px;
       font-weight: 600; text-transform: uppercase; letter-spacing: .04em;
       color: var(--muted); flex: 0 0 40%; }
  .mnt-screen table td[data-label=""] { justify-content: flex-start;
       flex-wrap: wrap; gap: 6px; padding-top: 8px; }
  .mnt-screen table td[data-label=""]::before { display: none; }
  /* The empty-state row is one centred sentence, not a labelled field. */
  .mnt-screen table td[colspan] { display: block; text-align: center; }
  .mnt-screen table td[colspan]::before { display: none; }
  /* Four or five count tiles will not fit side by side; they scroll as a strip
     rather than stacking into a column that pushes the table off the screen.
     The mask fades the trailing edge instead of a hard crop, so a tile ending
     mid-card reads as "swipe for more" rather than "that's all of them" —
     Helpdesk has six tiles (five statuses plus Total) and the last one or two
     were disappearing off the edge with nothing to say they existed. Snap
     keeps a swipe from stopping half-way through a tile. */
  .mnt-screen .tiles { display: flex; flex-wrap: nowrap; overflow-x: auto;
       scroll-snap-type: x mandatory; gap: 8px; grid-template-columns: none !important;
       -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
               mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%); }
  .mnt-screen .tiles .tile { flex: 0 0 auto; min-width: 118px; scroll-snap-align: start; }
}

/* ---- Helpdesk ticket-detail modal, on a phone -----------------------------
   The Priority/Action row (hd-manager-row, in helpdesk.js) is a right-aligned
   flex row that reads fine on desktop, but at phone width it wraps and each
   wrapped line re-centres against the right edge on its own -- Priority and
   Action end up staggered instead of stacked. This is a separate block from
   the .mnt-screen one above rather than folded into it: openModal() appends
   the ticket modal straight to <body> (see core.js), outside #hd-body, so
   nothing scoped to .mnt-screen ever reaches it. */
@media (max-width: 560px) {
  .hd-manager-row { flex-direction: column; align-items: stretch; }
  .hd-priority-field, .hd-action-field { flex-direction: column; align-items: stretch; gap: 4px; }
  .hd-priority-field select, .hd-action-field select { width: 100%; }
}

/* --- Ad Performance: the metric chart under the tiles ----------------------
   Drawn as inline SVG rather than with a chart library: there is no build step
   here, and the CSP forbids pulling one from a CDN. preserveAspectRatio is
   "none" on the element, so the bars stretch to whatever width the panel has
   and the height stays fixed — a chart that reflows its own aspect ratio on a
   tablet rotation is harder to read, not easier. */
.ads-chart { background: var(--panel); border: 1px solid var(--border);
     border-radius: var(--radius); padding: 12px 14px; margin-bottom: 20px; }
.ads-chart-svg { display: block; width: 100%; height: 160px; }
.ads-bar { fill: var(--accent); }
/* A row of controls, and the heading-left / controls-right variant. The
   screen used `row between wrap` and `btn-row` until 28 Sep 2026, which no
   rule here ever defined; these are the two it actually needs. */
.ads-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.ads-spread { justify-content: space-between; margin-bottom: 8px; }
/* A credit tile about to run dry. Red rule rather than red fill: the figure
   is still the thing to read. */
.tile.ads-low { border-color: var(--red); box-shadow: inset 0 0 0 1px var(--red); }
.tile.ads-low .value { color: var(--red); }
/* Figures on one line. The tables already scroll sideways (.tbl-wrap), so a
   narrow column should scroll rather than break "RM 1,425.00" over two lines,
   where the amount reads as two cells. Same rule as #inv-body, and found the
   same way — on the TikTok panel's first render, 28 Sep 2026, whose tables
   carry more columns than Shopee's. */
#ads-body td.right { white-space: nowrap; }
/* A day Shopee reported as zero still draws a hairline, so "ran and sold
   nothing" is visibly different from a day with no data at all, which draws
   nothing. Those two readings are not the same and the chart must not merge
   them. */

/* ---- the audit on a QC card ----
   Two controls on one line: the detail shots this mat already carries, and a
   request for more of the next one. Small and quiet — the card's subject is
   still the mat and its verdict. */
.qc-audit { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.qc-audit .btn-sm { font-size: 11.5px; padding: 3px 8px; }
/* The set, opened. A grid rather than a strip: five to fifteen shots of one mat
   are compared against each other, and a row that scrolls sideways makes that
   the one thing you cannot do. */
.qc-audit-grid { display: grid; gap: 8px;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
.qc-audit-shot { display: block; border: 1px solid var(--border);
  border-radius: 8px; overflow: hidden; }
.qc-audit-shot img { width: 100%; height: 140px; object-fit: cover; display: block; }

/* The Add Audit dropdown: one choice, then the button that acts on it — not a
   button on every row of a table, which put the decision and the record of
   past decisions in the same place fighting for the same row. */
.qc-audit-add { display: flex; gap: 8px; margin: 10px 0 14px; }
.qc-audit-add select { flex: 1; min-width: 0; }

/* ---- flagging a mat before it is sewn ----
   A <details> above the verdict queue. Collapsed by default: the screen's
   subject is still what QC has decided, and a list of everything waved would
   push that off the top. The summary carries the counts so it is worth opening
   or plainly not. */
.qc-outstanding { margin: 12px 0; border: 1px solid var(--border);
  border-radius: 10px; background: var(--panel); }
.qc-outstanding > summary { padding: 10px 12px; cursor: pointer;
  font-weight: 600; font-size: 14px; }
.qc-outstanding > div { padding: 0 12px 12px; }

/* ---- a control whose request is still out (core.js: one tap, one write) ----
   Dimmed so a second tap is visibly pointless; the tap itself is swallowed in
   core.js. NOT pointer-events:none — that would pass the tap through to
   whatever is under the button, often a row with its own onclick. */
.kh-busy { opacity: .55; cursor: progress; }

/* ---- Customers (customers.js, 5 Oct 2026) ------------------------------
   SiteGiant's list puts three lines in the Customer and Order cells; the
   second and third are the quiet ones. The detail page is two columns —
   history on the left, profile and address on the right — one on a phone. */
.cust-cell .sk { font-size: 12px; color: var(--muted); }
/* One line per line: "Total Spent: RM40,998.80" broken after the colon, or
   the WhatsApp link pushed under the number, reads as two separate facts.
   The table scrolls sideways in its .tbl-wrap instead. */
.cust-cell .sk, .cust-cell > div, .cust-nw { white-space: nowrap; }
.cust-flags { display: flex; gap: 6px; margin-top: 4px; flex-wrap: wrap; }
.cust-cell a { color: inherit; text-decoration: none; }
.cust-cell a:hover b { text-decoration: underline; }
.cust-wa { color: var(--green); font-size: 11px; font-weight: 600; margin-left: 6px; }
.cust-head .body { display: flex; gap: 14px; align-items: center; }
.cust-avatar { flex: none; width: 48px; height: 48px; border-radius: 50%;
  display: grid; place-items: center; background: var(--bg);
  border: 1px solid var(--border); font-weight: 600; }
.cust-detail { display: grid; gap: 14px; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  align-items: start; margin-top: 14px; }
.cust-detail-side { display: flex; flex-direction: column; gap: 14px; }
@media (max-width: 820px) { .cust-detail { grid-template-columns: minmax(0, 1fr); } }
/* Five buttons in one row do not fit a phone; they wrap rather than push
   the back button off the left edge. */
#view-cust_detail .ord-actions, #view-customers .ord-actions { flex-wrap: wrap; }
/* The detail page's write buttons, drawn by customers.js only for an account
   that may use them; an empty span takes no room in the row. */
.cust-writes { display: inline-flex; gap: 10px; flex-wrap: wrap; }
.cust-writes:empty { display: none; }
.cust-phone { display: flex; gap: 6px; }
.cust-phone select { flex: none; width: auto; }
.cust-phone input { flex: 1; min-width: 0; }
.cust-gender { display: flex; gap: 14px; padding-top: 6px; }
.cust-dob { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1.3fr); gap: 6px; }
.cust-goto { display: flex; gap: 8px; align-items: center; justify-content: flex-end;
  padding: 0 14px 12px; font-size: 13px; }
.cust-goto input { width: 80px; }
.cust-item { display: flex; gap: 8px; align-items: flex-start; margin-bottom: 6px;
  min-width: 220px; }
.cust-thumb { flex: none; width: 40px; height: 40px; object-fit: cover;
  border-radius: 6px; border: 1px solid var(--border); }
.cust-gender label, .cust-choice label { display: flex; align-items: center; gap: 8px;
  color: var(--text); font-size: 14px; }
.cust-gender input, .cust-choice input { width: auto; flex: none; }
