/**
 * V2T Warehouse AI, base primitives.
 *
 * Every primitive the design system names is implemented ONCE, here, at P0. That
 * is deliberate: section 17's definition of done applies to all 28 screens, and
 * discharging the accessibility floor once in the primitives is the difference
 * between a fixed cost and a per-screen tax.
 *
 * RULES THIS FILE OBEYS
 *   - No hex literals. Every colour resolves through a token. The only sanctioned
 *     exceptions are rgba(0,0,0,x) for backdrops and the keywords white/black on a
 *     saturated fill.
 *   - No dark-mode branch. The neutral ramp inverts, so components get dark for
 *     free. The single exception is the focus ring, where the RELATIONSHIP changes
 *     rather than the colour, and section 10.1 sanctions it explicitly.
 *   - No bare z-index numbers. Every layer uses a --z-* slot.
 */

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

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

body {
    margin: 0;
    background: var(--color-bg);
    color: var(--color-text);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    line-height: var(--leading-normal);
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4, h5, h6 {
    font-family: var(--font-display);
    font-weight: var(--font-normal);
    line-height: var(--leading-tight);
    margin: 0 0 var(--space-3);
    color: var(--color-text);
}

h1 { font-size: var(--text-3xl); }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-xl); }

/* --color-accent-text, NOT --color-primary-darker, and the difference is a real
   defect this file carried until it was measured. The -darker stop is the right
   answer on white at 5.30:1 and the wrong answer on the dark surface at 3.10:1,
   which is an outright AA failure on every link in the product for anyone running
   the dark theme. The token resolves to the legible stop in each theme; see the
   note beside its declaration in variables.css. */
a { color: var(--color-accent-text); }

/* Density. Compact is the product default: these are operator tools and a planner
   scanning two hundred rows needs information per square centimetre. */
:root {
    --row-height: 32px;
    --row-font: var(--text-xs);
}
body[data-density="standard"] {
    --row-height: 44px;
    --row-font: var(--text-sm);
}

/* ---------------------------------------------------------------------------
   Accessibility floor
   --------------------------------------------------------------------------- */
:focus-visible {
    outline: 2px solid var(--color-primary-dark);   /* 3.02:1 on white */
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}
:focus:not(:focus-visible) { outline: none; }

/* The one legitimate per-theme branch in the whole stylesheet. A focus indicator
   is a non-text element and must clear 3:1 against its background. The bright teal
   is 1.91:1 on white but 8.60:1 on the dark surface, so the relationship genuinely
   changes rather than the colour being under-tokenised. Section 10.1. */
body[data-theme="dark"] :focus-visible {
    outline-color: var(--color-primary);
}

.skip-link {
    position: absolute;
    left: -9999px;
    padding: var(--space-2) var(--space-4);
    background: var(--color-surface);
    color: var(--color-text);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    z-index: var(--z-dialog);
}
.skip-link:focus { left: var(--space-4); top: var(--space-4); }

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

.hidden { display: none !important; }

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

/* ---------------------------------------------------------------------------
   Page shell
   --------------------------------------------------------------------------- */
.shell { display: flex; min-height: 100vh; }

.topbar {
    position: fixed;
    top: 0; left: 0; right: 0;
    height: var(--nav-height);
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: 0 var(--space-6);
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
    z-index: var(--z-nav);
}

/* Identity and context only. Navigation lives in the sidebar: section 11.1 forbids
   using both as PRIMARY navigation. */
.topbar__brand {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    color: var(--color-text);
    text-decoration: none;
    white-space: nowrap;
}

/* ---------------------------------------------------------------------------
   The brand mark.

   WHY THIS BLOCK EXISTS, AND WHAT WAS ACTUALLY WRONG BEFORE IT
   Every mechanical check passed while the product still did not look like a V2T
   application. check_design read the source and found no hex literal, no undefined
   token and no dark branch, all of which were true and none of which was the
   problem: the accent was correctly tokenised and simply never reached paint. A
   browser pass sampling COMPUTED style found the brand accent on one element on the
   rate cards screen and two on four others, and the mark on none of the eighteen.
   A screen can be perfectly tokenised and entirely grey.

   The plate is the one place --color-primary is used at full strength on a light
   surface, and DESIGN 6.2 sanctions exactly this: "Large fills carrying dark text;
   decorative accents". It carries no meaning a user has to read, so the 1.91:1
   ratio against the page is not a contrast failure; the glyph ON it is what has to
   clear the floor, and black on the brand teal is 11.0:1.
   --------------------------------------------------------------------------- */
.brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    text-decoration: none;
    color: var(--color-text);
    white-space: nowrap;
}
.brand__mark { flex: none; border-radius: var(--radius-md); }
.brand__mark-plate { fill: var(--color-primary); }
/* The keyword, not var(--color-charcoal), and for the same reason DESIGN 7.1 gives
   for `white` on a saturated fill: that token INVERTS in the dark theme, and a brand
   mark is brand rather than neutral, so it is identical in both (DESIGN 3.3). */
.brand__mark-glyph { fill: black; }

/* THE SEAMS ARE THE PLATE SHOWING THROUGH, NOT A THIRD COLOUR.
   Cutting back to --color-primary rather than painting a second ink is what keeps
   the mark a two-value drawing, so it survives being printed in one colour, being
   scaled to 16px, and being placed on either theme. --color-primary is defined once
   in :root and never redefined for dark, so the seam cannot drift from the plate it
   is cut out of, which is what DESIGN 3.3 requires of a brand mark. */
.brand__mark-seam {
    fill: none;
    stroke: var(--color-primary);
    stroke-width: 1.7;
    stroke-linejoin: round;
    stroke-linecap: round;
}

/* THE LOCKUP IS TWO LINES BECAUSE IT IS TWO FACTS.
   "V2T" is the company standing behind the product and "Warehouse AI" is what the
   product is called. Run together in one serif string they competed, and the plate
   beside them repeated the "V2" a third time. Stacked, the eye takes the product
   name first and the vendor as the endorsement it is.

   The vendor line is mono on purpose: JetBrains Mono is the face this system counts
   in, so the company signs its name in the same voice its figures are set in.
   DESIGN 5.1 forbids mono for PROSE and reserves it for codes and IDs. A company
   code is not prose. */
.brand__word {
    display: inline-flex;
    flex-direction: column;
    justify-content: center;
    /* Nudges the two-line stack onto the optical centre of the plate. Without it the
       block sits a hair high, because the vendor line's cap height is smaller than
       the serif's and flexbox centres the boxes rather than the letterforms. */
    gap: 0.1em;
    line-height: 1;
}
.brand__vendor {
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    font-weight: 500;
    /* DESIGN 5.3: at or below --text-2xs, uppercase with tracking is the condition
       for staying legible. It is already uppercase, so this is the tracking half.
       0.12em, not the 0.18em first tried: at the wider setting the three letters
       drifted apart into V 2 T and stopped reading as one company code. */
    letter-spacing: 0.12em;
    color: var(--color-text-muted);
    line-height: 1;
}
.brand__product {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    line-height: 1;
}
/* Hover moves the PRODUCT name only. Moving both made the whole block flash and read
   as a state change rather than a link; moving the name the user is actually
   clicking towards is the quieter and more accurate signal. DESIGN 2: quiet motion,
   and colour only where it means something. */
.brand:hover .brand__product { color: var(--color-accent-text); }
.brand__product { transition: color 150ms ease; }
@media (prefers-reduced-motion: reduce) {
    .brand__product { transition: none; }
}
.topbar__spacer { flex: 1; }

/* A CHIP FOR A TERSE MONO FACT: the release string, the board's date.

   Not loose grey text. The release string is read aloud in support calls and the
   date anchors the dashboard, so each is a discrete thing with an edge rather than
   a trailing caption. The accent tint ties them to the mark without spending a
   second accent: --color-override-tint is the brand at 0.06 alpha, and the text is
   the theme-aware accent stop, so it clears 4.5:1 in both themes.

   NAMED .meta-chip, NOT .topbar__meta, which is what it was called while the
   dashboard used it for a date that is nowhere near the topbar. DESIGN 14.1's
   convention is prefix-plus-BEM, and a block name that asserts a location is a lie
   the moment a second screen includes it. The mark had the same fault an hour ago. */
.meta-chip {
    display: inline-flex;
    align-items: center;
    padding: 3px var(--space-2);
    border-radius: var(--radius-full);
    background: var(--color-override-tint);
    border: 1px solid var(--color-border);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.02em;
    color: var(--color-accent-text);
    font-variant-numeric: tabular-nums;
}

/* WHO IS LOOKING, AND WHEN.

   The greeting is prose and takes the body face; the date and the zone are facts
   and take the chip, which is the same primitive the dashboard already uses for
   its date. That split is the point: a reader scanning the bar sees two quotable
   values with edges and one sentence, rather than three runs of grey text.

   NOWRAP ON ALL THREE. The topbar is a single flex row with no wrap, so a long
   name or a long zone key would otherwise break inside itself and push the bar's
   height past --nav-height, which every fixed offset on the page is measured
   against. min-width: 0 lets the greeting be the thing that shrinks when the row
   is tight, because it is the one item here whose meaning survives being clipped. */
.topbar__greeting {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-size: var(--text-xs);
    color: var(--color-text-secondary);
}
.topbar__greeting strong {
    font-weight: var(--font-semibold);
    color: var(--color-text);
}
.topbar__date, .topbar__zone { flex: none; white-space: nowrap; }

/* THE ZONE IS QUIETER THAN THE DATE, DELIBERATELY. It is a qualifier on every time
   in the product rather than a headline, so it keeps the chip's shape and drops the
   accent tint: two chips at equal weight beside each other read as two equally
   important facts, and they are not. --color-text-secondary on the surface is
   7.0:1, so quieter is still well clear of the 4.5:1 floor. */
.topbar__zone {
    background: transparent;
    color: var(--color-text-secondary);
}

/* THE ORDER THINGS LEAVE THE BAR IN, and it is an order rather than a breakpoint.

   The topbar is one flex row that may not wrap, because --nav-height is the offset
   every fixed element on the page is positioned against and a bar that grows a
   second line pushes the rail and the content out from under themselves. So when
   the row runs out of width, items are dropped, cheapest meaning first: the zone
   (a qualifier), then the demo control (a presenter's tool, and the rail it belongs
   beside has already gone at this width), then the date, and the greeting last,
   which by then is the only thing left saying who is signed in. The client scope
   banner is never dropped at any width: it is the product's central claim. */
@media (max-width: 1024px) {
    .topbar__zone { display: none; }
    .demo-role { display: none; }
}
@media (max-width: 768px) {
    .topbar__date { display: none; }
}
@media (max-width: 480px) {
    .topbar__greeting { display: none; }
}

.sidebar {
    position: fixed;
    /* BOTTOM IS THE FOOTER, NOT ZERO. The signature block below occupies the foot of
       the rail, and a rail that still claimed the full height would scroll its last
       links underneath it. Subtracting the token means the rail's own clientHeight
       is the space it actually has, which is the number check_p7_presentation
       measures its headroom against, so the measurement keeps telling the truth. */
    top: var(--nav-height); bottom: var(--footer-height); left: 0;
    width: var(--sidebar-width);
    padding: var(--space-2) var(--space-3);
    background: var(--color-surface);
    border-right: 1px solid var(--color-border);
    overflow-y: auto;
    z-index: var(--z-sticky);
}
/* DENSITY HERE IS LOAD BEARING, NOT TASTE, and the numbers below are measured.

   At P6 the rail reached sixteen items and stopped fitting a 1440x900 window: the
   Integration monitor link sat below the fold and could not be reached without
   scrolling, which for a scripted walkthrough means a presenter hunting for a
   differentiator screen on stage.

   I tightened it then on a CALCULATION that it would fit 656 px, the height a
   1280x720 projector leaves. That calculation was wrong. Measured at 720p with the
   counting and billing screens added: the rail needed 770 px against 656 available,
   and the three items cut off the bottom were Invoices, Rate cards and the
   Integration monitor, which is the whole of differentiator three.

   So this is now sized against a measurement that runs on every build, in
   check_p7_presentation. Rows are 22 px, group headers lose a step of margin, and
   the rail's own vertical padding halves. DESIGN 2 sets the body floor at 12 px and
   the 4 px scale is intact. */
.sidebar__group {
    margin: var(--space-2) 0 var(--space-1);
    padding: 0 var(--space-3);
    font-size: var(--text-2xs);
    font-weight: var(--font-bold);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    line-height: var(--leading-snug);
    color: var(--color-text-muted);
}
.sidebar__group:first-child { margin-top: 0; }
.sidebar__link {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: 0 var(--space-2);
    border-radius: var(--radius-md);
    color: var(--color-text-secondary);
    text-decoration: none;
    font-size: var(--text-xs);
    line-height: var(--leading-snug);
    /* A 22 px row, below the 44 px touch minimum and deliberately so: this is a
       pointer-and-keyboard application on a desktop, and DESIGN 10 sets the touch
       floor for touch surfaces. The hover and focus area is the full row width, so
       the pointer target is 280 x 22, not 22 x 22. */
    min-height: 22px;
}
.sidebar__link:hover { background: var(--color-surface-hover); color: var(--color-text); }

/* The icon is the landmark, not the message: it sits a step quieter than the label
   so the rail still reads as a list of words. stroke="currentColor" means it takes
   the accent on the active row for free, which is the whole reason DESIGN 8.2 makes
   currentColor load-bearing. */
.sidebar__icon {
    flex: none;
    display: inline-flex;
    color: var(--color-text-muted);
    transition: color var(--transition-fast);
}
.sidebar__link:hover .sidebar__icon { color: var(--color-text-secondary); }
.sidebar__link[aria-current="page"] .sidebar__icon { color: inherit; }

/* THE CURRENT LOCATION CARRIES THREE CHANNELS, NOT ONE.
   The tint alone is --color-override-tint, which is the accent at 0.06 alpha and
   measured against the rail's own surface is a wash a viewer at the back of a room
   cannot see. DESIGN 6.6 forbids colour as the sole carrier of a state, so the rail
   also gets weight and a 3px edge in the accent. The edge is --color-primary-dark,
   the lightest stop that clears the 3:1 non-text threshold in BOTH themes (3.02:1
   on white, 5.44:1 on the dark surface); the label is --color-accent-text, which
   resolves to whichever stop clears 4.5:1 for the theme in play. */
.sidebar__link[aria-current="page"] {
    background: var(--color-override-tint);
    color: var(--color-accent-text);
    font-weight: var(--font-medium);
    box-shadow: inset 3px 0 0 var(--color-primary-dark);
}

.main {
    margin-top: var(--nav-height);
    margin-left: var(--sidebar-width);
    padding: var(--space-6);
    max-width: var(--content-max-width);
}

/* Deliberately NO width: 100% here. Combined with the sidebar margin it makes the
   element 100% + 280px wide, so the page scrolls horizontally by exactly the
   sidebar width. The design system is explicit that the page body never scrolls
   horizontally; wide content scrolls inside its own container instead. A block
   element fills the available width on its own. */

.main--full { margin-left: 0; }

@media (max-width: 1024px) {
    .sidebar { display: none; }
    .main { margin-left: 0; padding: var(--space-4); }
}
/* ---------------------------------------------------------------------------
   The vendor signature, bottom left.

   FIXED, AND OCCUPYING SPACE THE RAIL GAVE UP. The rail above sets
   bottom: var(--footer-height) for exactly this block, so the two tile the left
   column rather than overlapping. Putting the signature in the rail's own scroll
   flow instead was rejected on a measurement rather than on taste: the rail does
   not fit a 720p projector, so a signature at the end of that flow is a signature
   nobody ever scrolls to, on the machine the product is presented from.

   IT SPANS THE RAIL, NOT THE PAGE. A full-width bar at the foot of the viewport
   would sit over the content column, and the content column is the one thing on
   this screen that scrolls: every table would run underneath it.

   EVERY NUMBER IN THIS BLOCK IS A RAIL LINK. The first version of this file stood
   96 px tall in three comfortable rows and was measured in a browser: the rail on a
   1440x900 laptop went from 93 px of headroom to minus three, and on the projector
   from 23 px to minus 51. What falls off the bottom of that rail is whatever is
   last, which is the differentiator screens. So this is two rows, the artwork is
   20 px, and the gaps are the smallest step on the scale. check_p14_chrome measures
   the result rather than trusting it.
   --------------------------------------------------------------------------- */
.site-footer {
    position: fixed;
    left: 0;
    bottom: 0;
    width: var(--sidebar-width);
    /* MIN HEIGHT, NOT HEIGHT, and this is the second version of this rule.
       The first fixed the box at the token and centred the content inside it. The
       content measured 95 px against an 84 px token, so the block silently clipped
       its own bottom and what disappeared was the copyright line: a legal notice,
       invisible, with nothing on screen to say it had ever been there. A fixed
       height fails by hiding; a minimum fails by growing a few pixels into a rail
       that scrolls anyway. The token is set from a measurement so the two coincide
       today, and min-height is what keeps the failure survivable when a font metric
       or a longer release string moves it tomorrow. */
    min-height: var(--footer-height);
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: var(--space-1);
    /* THE SMALLEST STEP ON THE SCALE, VERTICALLY. Eight pixels of padding here read
       as eight pixels off the rail, which measured as the difference between 33 px
       of navigation headroom and 41: one side of the forty the projector rule asks
       for. Horizontal padding keeps the normal step, because the rail has width to
       spare and none of it is contested. */
    padding: var(--space-1) var(--space-3);
    background: var(--color-surface);
    border-top: 1px solid var(--color-border);
    border-right: 1px solid var(--color-border);
    /* The same layer as the rail it continues, and below the topbar, which is
       --z-nav. A signature that outranks the navigation is a signature that draws
       over an open menu. */
    z-index: var(--z-sticky);
}

/* ONE ROW, THREE THINGS, ONE LINK. The mark, who powers it and where to find them
   read left to right as a single sentence, which is also what lets the block be two
   rows instead of three. */
.site-footer__brand {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    text-decoration: none;
    color: inherit;
    min-width: 0;
}

/* HEIGHT DRIVES, WIDTH FOLLOWS. The artwork is 594x368 and the tag carries both, so
   the intrinsic ratio reserves the box before the file decodes and the labels beside
   it do not jump left when it lands. */
.site-footer__logo {
    flex: none;
    height: 20px;
    width: auto;
}

.site-footer__powered {
    font-size: var(--text-2xs);
    font-weight: var(--font-medium);
    color: var(--color-text-secondary);
    white-space: nowrap;
}

/* THE ADDRESS TAKES THE THEME AWARE ACCENT STOP, not --color-primary. The design
   system's fourth non-negotiable: the brand teal measures 1.91:1 on a light surface,
   so it may carry a shape but never a word. --color-accent-text resolves to whichever
   stop clears 4.5:1 in the theme that is running. */
.site-footer__url {
    font-size: var(--text-2xs);
    color: var(--color-accent-text);
    white-space: nowrap;
}
.site-footer__brand:hover .site-footer__url { text-decoration: underline; }

/* THE SECOND ROW: the release string and the copyright, side by side because
   stacking them costs a row and a row here costs four navigation links. */
.site-footer__meta {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
}

.site-footer__legal {
    font-size: var(--text-2xs);
    line-height: var(--leading-snug);
    color: var(--color-text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The chip loses its own leading here, not its size: DESIGN 2 floors the face and
   this is already at the 11 px chip step, so what compresses is the space around
   the string. */
.site-footer__version { flex: none; line-height: 1; }

/* BELOW 1024 THE RAIL DISAPPEARS AND SO DOES THE COLUMN THIS SITS IN. Left fixed, it
   would float over the content of a narrow page with nothing behind it. It goes back
   into the document flow instead, at the end of the page, which is where a footer
   belongs on a layout that has no chrome column. */
@media (max-width: 1024px) {
    .site-footer {
        position: static;
        width: auto;
        min-height: 0;
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-3);
        padding: var(--space-4);
        border-right: 0;
    }
}
/* ---------------------------------------------------------------------------
   Button
   --------------------------------------------------------------------------- */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    height: 40px;
    padding: var(--space-2) var(--space-4);
    border: none;
    border-radius: var(--radius-md);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: var(--font-medium);
    line-height: 1;
    cursor: pointer;
    text-decoration: none;
    /* A LABEL NEVER WRAPS. The height is pinned per size, so a label that wraps
       does not make the button taller, it overflows it: "Put away" rendered as two
       lines spilling out of a 32px box on every row of the putaway table. The
       measurement said height 32 and the screenshot said two lines, which is what
       a fixed height plus wrapping text looks like. */
    white-space: nowrap;
    /* NAMED PROPERTIES, NOT `all`, AND THE REASON IS THE FOCUS RING.
       `transition: all` animates every animatable property, including outline-width,
       so the keyboard focus ring faded in over the transition instead of appearing.
       A ring that takes 150 ms to arrive is a ring a fast tabber never sees, and it
       was found by an accessibility check sampling the computed outline and reading
       0px while :focus-visible was already matching. Transition what should move. */
    transition: background var(--transition-fast),
                box-shadow var(--transition-fast),
                transform var(--transition-fast),
                color var(--transition-fast);
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn:active { transform: translateY(0); }

.btn-sm { height: 32px; font-size: var(--text-xs); padding: var(--space-1) var(--space-3); }
.btn-lg { height: 48px; font-size: var(--text-base); padding: var(--space-3) var(--space-6); }

/* THE PRIMARY ACTION IS WHERE THIS PRODUCT SPENDS ITS BOLDNESS, AND IT IS THE ONLY
   PLACE IT DOES.

   This was --gradient-primary-safe under white text, which is the correct answer to
   the wrong question. White caps the fill at --color-primary-darker (5.30:1, and
   3.02:1 one stop brighter), so every filled control in the product resolved to a
   flat dark green that shares a hue with the brand and none of its character. Beside
   the reference implementation it read as a different product.

   Flipping the text lifts the ceiling rather than lowering the fill. Black on the
   bright gradient measures 11.00:1 and 6.96:1, so this button is MORE legible than
   the one it replaces, not less. DESIGN 7.1 sanctions it in terms.

   The 1px border is not decoration. The fill is 1.82:1 against the page, and WCAG
   1.4.11 wants 3:1 for the visual information that identifies a control, so the
   boundary is carried by --color-primary-dark at 3.02:1 while the fill carries the
   brand. Without it a bright button on a near-white page has no edge. */
.btn-primary {
    /* WHITE TEXT, SO THE FILL IS THE ONE THAT CAN CARRY IT.
       White needs 4.5:1 against its fill and measures 1.91:1 on --color-primary and
       3.02:1 on --color-primary-dark, so the bright gradient cannot hold it at
       either stop. On the -safe pair it is 5.30:1 and 7.76:1. This is the filled
       button law in DESIGN 7.1, and the whole reason the -safe pair exists.

       The button keeps what the bright pass added other than the fill: a real
       border, semibold weight and the hover glow. Those cost no contrast and are
       what separates it from the flat control this replaced. */
    background: var(--gradient-primary-safe);
    color: white;                                    /* 5.30:1 / 7.76:1 */
    border: 1px solid var(--color-primary-darker);
    font-weight: var(--font-semibold);
}
.btn-primary:hover { box-shadow: var(--shadow-glow-primary); transform: translateY(-1px); }
/* The ring must separate from a saturated fill rather than sit on it, and against
   this dark teal the candidates are not close. Measured on --color-primary-darker,
   the button's lightest stop: white 5.30:1, black 3.96:1, --color-primary-light
   3.07:1, --color-primary 2.77:1 which fails outright. White, and it does not
   invert, so the ring holds in both themes because the fill does not move either. */
.btn-primary:focus-visible { outline-color: white; }

/* THE MACHINE'S OWN ACTION, IN THE MACHINE'S OWN COLOUR.
   DESIGN 6.1 gives the AI accent its own row in the ladder and 6.3 reserves violet
   for machine-generated work. This class existed from P0 and no screen used it, so
   asking the product to measure a billing period looked exactly like asking it to
   save a form. White on the -safe stops is 5.38:1 and 6.98:1. */
.btn-ai { background: var(--gradient-ai-safe); color: white; }
.btn-ai:hover { box-shadow: var(--shadow-glow-ai); transform: translateY(-1px); }

.btn-secondary { background: var(--color-gray-200); color: var(--color-text); }
.btn-secondary:hover { background: var(--color-gray-300); }

.btn-outline {
    background: transparent;
    border: 1px solid var(--color-border);
    color: var(--color-gray-700);
}
.btn-outline:hover { background: var(--color-gray-100); }

.btn-ghost { background: transparent; color: var(--color-gray-700); }
.btn-ghost:hover { background: var(--color-gray-100); }

.btn-danger { background: var(--color-danger-text); color: white; }  /* 6.47:1 */

/* The rest of the size ladder, DESIGN 7.1. All five exist as real classes precisely
   so nobody reaches for an !important override to squeeze a control into a toolbar,
   which is how the reference codebase ended up with 20px buttons fighting a 40px
   base. */
.btn-xs  { height: 26px; font-size: var(--text-2xs); padding: 0 var(--space-2); }
.btn-2xs { height: 20px; font-size: var(--text-2xs); padding: 0 var(--space-1); }

/* Square at the current size, no padding. MUST carry aria-label: it is the whole
   content of the control, so the control is what a screen reader has to read. */
.btn-icon { padding: 0; width: 40px; }
.btn-sm.btn-icon  { width: 32px; }
.btn-xs.btn-icon  { width: 26px; }
.btn-2xs.btn-icon { width: 20px; }

/* ---------------------------------------------------------------------------
   Pill. DESIGN 7.2's third primitive, and the one this product was missing.

   A pill is INTERACTIVE: it is a filter toggle. A badge is a static status marker.
   The list screens were rendering their filter toggles as .btn-primary, which is a
   submit button's treatment on something that is not a submit button, and it cost
   the product the single strongest brand signal it has: the bright teal never
   appeared anywhere except a hover glow nobody screenshots.

   THE TEXT COLOUR IS THE ONE PLACE THIS FILE DEPARTS FROM A LITERAL READING OF THE
   RECIPE, AND DELIBERATELY. DESIGN 7.2 says an active pill is "a solid
   --color-primary fill with white text". White on --color-primary is 1.91:1, which
   DESIGN 6.2, DESIGN 7.1 and the DESIGN 17 checklist each forbid outright; 7.1
   states the resolution in terms: "If a product genuinely needs the bright teal as a
   button fill, the button takes dark text." So the fill is the bright teal the recipe
   asks for and the text is `black`, which measures 11.0:1 on it. The keyword rather
   than --color-charcoal because that token inverts and a saturated fill's text must
   not (the same note DESIGN 7.1 attaches to `white`).
   --------------------------------------------------------------------------- */
.pill {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    height: 32px;
    padding: 0 var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-full);
    background: var(--color-surface);
    color: var(--color-text-secondary);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: var(--font-medium);
    line-height: 1;
    text-decoration: none;
    cursor: pointer;
    transition: background var(--transition-fast),
                border-color var(--transition-fast),
                color var(--transition-fast);
}
.pill:hover { border-color: var(--color-border-hover); background: var(--color-surface-hover); }
.pill[aria-pressed="true"],
.pill[aria-current="true"] {
    background: var(--color-primary);
    border-color: var(--color-primary-dark);
    color: black;   /* 11.0:1 on the brand teal. See the note above. */
}
/* AGAINST A SATURATED FILL THE RING HAS TO CHANGE, AND THE OBVIOUS CHOICES ARE BOTH
   WRONG HERE. DESIGN 10.1 suggests "a light neutral or --color-primary-light", but
   that advice is written for a DARK saturated fill such as the primary button, and
   this fill is the bright teal: white on it is 1.91:1 and --color-primary-light is
   1.13:1, so both fail the 3:1 a focus indicator must clear.

   It is not --color-charcoal either, which was the first attempt and is a bug: that
   token inverts, so in the dark theme the ring resolves to the near-white top of the
   ramp on the same bright teal and lands at 1.5:1. A ring that vanishes for exactly
   the users who need it is worse than no ring, because it passes a glance.

   `black`, the sanctioned non-inverting keyword, is 11.0:1 on the fill in both
   themes. It is the same reasoning that puts `black` on the pill's label. */
.pill[aria-pressed="true"]:focus-visible,
.pill[aria-current="true"]:focus-visible { outline-color: black; }
.pill__count {
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    font-variant-numeric: tabular-nums;
    opacity: 0.75;
}
.pill-group { display: flex; gap: var(--space-2); flex-wrap: wrap; }

/* ---------------------------------------------------------------------------
   Card
   --------------------------------------------------------------------------- */
.card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--space-6);
}
/* Static cards get a border, not a shadow: elevation means "floating above the
   page", so it is reserved for hover, dropdowns, modals and drawers. */
.card--interactive:hover {
    border-color: var(--color-primary);
    transform: translateY(-2px);
    box-shadow: var(--shadow-lg);
}
@media (max-width: 1024px) { .card { padding: var(--space-4); } }
@media (max-width: 768px)  { .card { padding: var(--space-3); } }

.card__title {
    font-family: var(--font-display);
    font-size: var(--text-lg);
    margin: 0 0 var(--space-4);
}

/* ---------------------------------------------------------------------------
   Badge, chip
   --------------------------------------------------------------------------- */
.badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-sm);
    font-size: var(--text-xs);
    font-weight: var(--font-medium);
    text-transform: uppercase;
}
.badge--success { background: var(--color-success-light); color: var(--color-success-text); }
.badge--warning { background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--danger  { background: var(--color-danger-light);  color: var(--color-danger-text); }
.badge--info    { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--neutral { background: var(--color-gray-200);      color: var(--color-gray-700); }

/* Machine provenance. A SEPARATE chip beside the status badge, never the status
   badge itself: no status in the product maps to the AI tone, because violet must
   mean "a machine decided this" and nothing else. */
.badge--ai { background: var(--color-ai-tint); color: var(--color-ai-dark); }

/* ---------------------------------------------------------------------------
   Table
   --------------------------------------------------------------------------- */
.table { width: 100%; border-collapse: collapse; font-size: var(--row-font); }
.table thead th {
    position: sticky;
    top: 0;
    z-index: var(--z-raised);
    background: var(--color-surface);
    padding: var(--space-2) var(--space-3);
    text-align: left;
    font-size: var(--text-xs);
    font-weight: var(--font-semibold);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-text-secondary);
    border-bottom: 1px solid var(--color-border);
}
.table td {
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--color-border);
    height: var(--row-height);
}

/* HORIZONTAL PADDING IS THE CHEAPEST WIDTH IN A NINE-COLUMN TABLE, AND IT IS PAID
   NINE TIMES.

   Identifiers stopped wrapping a version ago, which is correct for the domain: a bin
   code broken at a hyphen reads as two addresses. The cost is that every ident
   column is now pinned to its longest value, and on the picking table that is four
   columns at once. Measured, the nowrap added 146px past the container at 1440 and
   306px at 1280, which pushed the Pick button off the right-hand edge.

   Padding is the part of that width nobody is reading. Twelve pixels either side of
   nine columns is 216px of the table spent on gutters. Eight is still comfortably
   above the 4px scale's floor and buys back 72px without touching a single value. */
.table th, .table td { padding-left: var(--space-2); padding-right: var(--space-2); }
.table tbody tr:hover { background: var(--color-surface-hover); }
.table tbody tr[aria-selected="true"] { background: var(--color-override-tint); }

/* The mono rule. Any figure a user might compare against another figure is mono
   and right-aligned. tabular-nums stops the column shivering as values update. */
.table .num, .num {
    font-family: var(--font-mono);
    text-align: right;
    font-variant-numeric: tabular-nums;
}
/* Identifiers an operator reads aloud: SKU codes, bin locations, gate pass numbers.

   AND THEY DO NOT BREAK. A bin code is this product's atom: an operator reads
   "FZ-01-01-A-02" off the screen and walks to it, and the hyphens are the segment
   separators of the aisle-bay-level-position scheme, not soft wrap opportunities.
   The browser treats them as break points, so a narrow column split the code across
   two lines at a hyphen, which is the one place it must never break, because
   "FZ-01-01-A-" and "02" read as two things. If the column is too narrow the cell
   scrolls with the table; it does not rewrite the address. */
.ident {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.01em;
    white-space: nowrap;
}

.table-scroll { overflow-x: auto; }   /* the page body never scrolls horizontally */

/* ---------------------------------------------------------------------------
   THE PINNED ACTION COLUMN.

   WHAT THIS FIXES, AND WHY THE SCROLL ITSELF IS NOT THE DEFECT
   DESIGN 15.2 says wide content scrolls inside its own overflow-x container and the
   page body never scrolls sideways. Both were true on the picking screen while its
   Pick button sat past the right-hand edge, because "the table scrolls in its own
   box" is satisfied by a box scrolled so far that the primary action is unreachable
   until you find a scrollbar. The rule was met and the screen was still broken.

   The width is not recoverable by tuning. Four of picking's nine columns are
   identifiers, and identifiers do not wrap, because a bin code broken at a hyphen
   reads as two addresses and an operator walks to it. Padding and the short-reason
   input gave back 84px of 146; the rest is the codes themselves, and shortening
   those means lying about the warehouse.

   So the column that must never be hidden stops moving. The row scrolls under it.
   Nothing is lost at any width, and the residual scroll becomes what the design
   system always intended it to be: a way to see more, not a way to lose the button.

   The background is opaque and deliberately not inherited: a transparent sticky cell
   shows the scrolling row sliding beneath it. The hover and selected states are
   repeated here for the same reason, or the pinned cell stays pale while its row
   lights up and reads as a different row.
   --------------------------------------------------------------------------- */
.table--pinned thead th:last-child,
.table--pinned tbody td:last-child {
    position: sticky;
    right: 0;
    background: var(--color-surface);
    /* The edge has to READ, or the column under it looks truncated rather than
       covered: at rest the status badge is half hidden behind this cell, and without
       a visible boundary a clipped "PENDING" reads as a rendering fault instead of
       as "there is more here, scroll". rgba(0,0,0,x) is the sanctioned exception for
       a shadow, and it works over either theme's surface. */
    box-shadow: -10px 0 10px -6px rgba(0, 0, 0, 0.28);
}
.table--pinned thead th:last-child { z-index: var(--z-overlay); }
.table--pinned tbody tr:hover td:last-child { background: var(--color-surface-hover); }
.table--pinned tbody tr[aria-selected="true"] td:last-child { background: var(--color-override-tint); }

/* Sortable header. The whole label is the target, not a tiny arrow. */
.table__sort {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    color: inherit;
    text-decoration: none;
}
.table__sort:hover { color: var(--color-accent-text); }

/* A blank cell reads as deliberate rather than as missing data. */
.cell-blank { color: var(--color-text-muted); }

/* Client identity. The colour is the fast channel, the mono code is the reliable
   one: six categorical hues cannot be told apart by every viewer, and the demo runs
   on an unknown projector. Both always ship together. */
.client-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}
.client-chip__swatch {
    width: 10px;
    height: 10px;
    border-radius: var(--radius-sm);
    background: var(--client-colour, var(--color-gray-400));
    flex: none;
}
.client-chip__code {
    font-family: var(--font-mono);
    font-weight: var(--font-medium);
    font-size: var(--text-xs);
    letter-spacing: 0.02em;
}

/* ---------------------------------------------------------------------------
   Page head and filter bar
   --------------------------------------------------------------------------- */
.page-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-4);
    margin-bottom: var(--space-5);
    flex-wrap: wrap;
}
.page-head h1 { margin: 0; }
.page-head__subtitle {
    margin: var(--space-1) 0 0;
    color: var(--color-text-secondary);
    font-size: var(--text-sm);
}
.page-head__actions { display: flex; gap: var(--space-2); }

.filter-bar {
    display: flex;
    align-items: flex-end;
    gap: var(--space-3);
    flex-wrap: wrap;
    padding: var(--space-4);
    margin-bottom: var(--space-4);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
}
.filter-bar__field { display: flex; flex-direction: column; min-width: 160px; }
.filter-bar__field .field__label { margin-bottom: var(--space-1); }
.filter-bar__field select,
.filter-bar__field input {
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text);
    font-family: var(--font-body);
    font-size: var(--text-sm);
}
.filter-bar__field select:focus,
.filter-bar__field input:focus {
    outline: none;
    border-color: var(--color-primary-dark);
    box-shadow: 0 0 0 3px rgba(0, 212, 170, 0.1);
}
.filter-bar__actions { display: flex; gap: var(--space-2); margin-left: auto; }

/* ---------------------------------------------------------------------------
   Pagination
   --------------------------------------------------------------------------- */
.pagination {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-3) 0 0;
    flex-wrap: wrap;
}
.pagination__summary {
    margin: 0;
    font-size: var(--text-xs);
    color: var(--color-text-muted);
}
.pagination__summary .num { font-weight: var(--font-medium); color: var(--color-text-secondary); }
.pagination__controls { display: flex; align-items: center; gap: var(--space-2); }
.pagination__position {
    font-size: var(--text-xs);
    color: var(--color-text-secondary);
    padding: 0 var(--space-2);
}
.pagination [aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }

/* ---------------------------------------------------------------------------
   State surfaces. All four, every data view. Missing states are the most common
   review finding, and with a heavily seeded database the filtered-empty variant is
   the one that actually appears on stage.
   --------------------------------------------------------------------------- */
.state {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    padding: var(--space-12) var(--space-6);
    gap: var(--space-3);
}
/* 64px, DESIGN 7.9. The grey stop is deliberate: an empty state is not an error and
   must not read as one, so the icon is the quietest thing on the card. */
.state__icon { color: var(--color-gray-400); line-height: 0; }
.state__title {
    font-size: var(--text-lg);
    font-weight: var(--font-semibold);
    color: var(--color-text-secondary);
    font-family: var(--font-body);
    margin: 0;
}
.state__message {
    font-size: var(--text-sm);
    color: var(--color-text-muted);
    max-width: 400px;
    margin: 0;
}

.spinner {
    width: 28px; height: 28px;
    border: 3px solid var(--color-border);
    border-top-color: var(--color-primary-dark);
    border-radius: var(--radius-full);
    animation: spin 700ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Skeleton rows, the second placeholder variant. */
.skeleton-row {
    height: var(--row-height);
    background: var(--color-gray-100);
    border-radius: var(--radius-sm);
    margin-bottom: var(--space-2);
}

/* ---------------------------------------------------------------------------
   Forms
   --------------------------------------------------------------------------- */
.field { margin-bottom: var(--space-4); }
.field__label {
    display: block;
    margin-bottom: var(--space-1);
    font-size: var(--text-sm);
    font-weight: var(--font-medium);
    color: var(--color-gray-700);
}
.field__label--required::after { content: " *"; color: var(--color-danger); }
.field__input {
    width: 100%;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text);
    font-family: var(--font-body);
    font-size: var(--text-sm);
    /* Named, for the same reason as .btn above: `all` animates the focus ring. */
    transition: border-color var(--transition-fast),
                box-shadow var(--transition-fast);
}
/* The border carries the 3:1 compliance; the glow carries the softness. An outline
   would clip against adjacent fields, which is why this ring differs from the
   global 2px outline. */
.field__input:focus {
    outline: none;
    border-color: var(--color-primary-dark);
    box-shadow: 0 0 0 3px rgba(0, 212, 170, 0.1);
}
.field__input[aria-invalid="true"] {
    border-color: var(--color-danger);
    box-shadow: 0 0 0 3px var(--color-danger-light);
}
.field__hint  { margin-top: var(--space-1); font-size: var(--text-xs); color: var(--color-text-muted); }
.field__error { margin-top: var(--space-1); font-size: var(--text-xs); color: var(--color-danger-text); }

/* ---------------------------------------------------------------------------
   Toast region. One implementation, mounted in the base template.
   --------------------------------------------------------------------------- */
.toast-region {
    position: fixed;
    bottom: var(--space-6);
    right: var(--space-6);
    z-index: var(--z-toast);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}
.toast {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    background: var(--color-surface);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    border-left: 4px solid var(--color-info);
    max-width: 380px;
    font-size: var(--text-sm);
}
.toast--success { border-left-color: var(--color-success); }
.toast--warning { border-left-color: var(--color-warning); }
.toast--error   { border-left-color: var(--color-danger); }

/* The tone's second channel. A 4px edge is the whole of the signal otherwise, and a
   4px edge in a colour is exactly what DESIGN 6.6 says a state may not rely on. The
   icon shapes differ per tone, not just their hue, so the tone survives greyscale. */
.toast__icon { flex: none; color: var(--color-info-text); }
.toast--success .toast__icon { color: var(--color-success-text); }
.toast--warning .toast__icon { color: var(--color-warning-text); }
.toast--error   .toast__icon { color: var(--color-danger-text); }
.toast__message { margin: 0; }

@media (max-width: 768px) {
    .toast-region { left: var(--space-4); right: var(--space-4); bottom: var(--space-4); }
    .toast { max-width: none; }
}

/* ---------------------------------------------------------------------------
   Demo role control. Only rendered when DEMO_MODE is on.

   THIS WAS A FULL WIDTH AMBER STRIP ACROSS THE TOP OF EVERY SCREEN. It carried the
   word "Demo", the sentence "Signed in as manager", and this switch. The first is
   true of every pixel in the build, the second is now said by the greeting a few
   centimetres to the right, and only the third was doing work, so only the third
   survived. Warning colour is a scarce signal and this was never a warning; it is
   a control, and it is styled as one.
   --------------------------------------------------------------------------- */
.demo-role {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}
/* Not the field's full 36 px: this sits in a 64 px bar beside a small button and a
   chip, and a form control at page height makes the bar look like a form. */
.demo-role__select {
    height: 28px;
    width: auto;
    padding: 0 var(--space-2);
    font-size: var(--text-xs);
}

/* ---------------------------------------------------------------------------
   Client scope banner. The client-user restriction has to be VISIBLY OBVIOUS:
   it is the product's central claim, so it gets permanent chrome rather than a
   subtle cue.
   --------------------------------------------------------------------------- */
.client-scope {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-md);
    background: var(--color-info-light);
    color: var(--color-info-text);
    font-size: var(--text-xs);
    font-weight: var(--font-medium);
}
.client-scope__code {
    font-family: var(--font-mono);
    font-weight: var(--font-bold);
}

/* ---------------------------------------------------------------------------
   Branded dialog. Sits at --z-dialog, above modals and toasts, because it is the
   layer that asks permission to do something to the layer beneath.
   --------------------------------------------------------------------------- */
.v2t-dialog {
    position: fixed;
    inset: 0;
    z-index: var(--z-dialog);
    display: flex;
    align-items: center;
    justify-content: center;
}
.v2t-dialog__backdrop {
    position: absolute;
    inset: 0;
    background: rgba(13, 21, 23, 0.5);
    backdrop-filter: blur(4px);
}
.v2t-dialog__shell {
    position: relative;
    display: flex;
    flex-direction: column;
    width: 420px;
    max-width: 90vw;
    max-height: 90vh;
    background: var(--color-surface);
    border-radius: var(--radius-xl);
    box-shadow: var(--shadow-xl);
}
.v2t-dialog__header {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-5) var(--space-6) 0;
}
.v2t-dialog__title {
    font-family: var(--font-display);
    font-size: var(--text-xl);
    line-height: var(--leading-snug);
    margin: 0;
}

/* THE ROUND VARIANT ICON. DESIGN 7.6: "Variant changes exactly two things: the round
   icon tint in the header, and the primary button colour." This product shipped the
   second and not the first, so an alert and a delete confirmation were typographically
   identical and a destructive question looked exactly like a notice.

   The tint formula from DESIGN 6.4 verbatim: the semantic at 0.15 behind the
   semantic's own deep stop, which is the pairing measured at 4.57:1 or better. Icon
   strokes carry meaning here, so they clear the 3:1 non-text floor on the tint. */
.v2t-dialog__icon {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: var(--radius-full);
    background: var(--color-info-light);
    color: var(--color-info-text);
}
.v2t-dialog--warning .v2t-dialog__icon {
    background: var(--color-warning-light);
    color: var(--color-warning-text);
}
.v2t-dialog--danger .v2t-dialog__icon {
    background: var(--color-danger-light);
    color: var(--color-danger-text);
}
.v2t-dialog--ai .v2t-dialog__icon {
    background: var(--color-ai-tint);
    color: var(--color-ai-dark);
}
.v2t-dialog__body { padding: var(--space-4) var(--space-6); overflow-y: auto; }
.v2t-dialog__message { margin: 0; color: var(--color-text-secondary); }
.v2t-dialog__footer {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2);
    padding: var(--space-4) var(--space-6) var(--space-5);
}
@media (max-width: 768px) {
    .v2t-dialog__footer { flex-direction: column-reverse; }
    .v2t-dialog__footer .btn { width: 100%; }
}

/* ===========================================================================
   P1 inventory surfaces: explorer, floor map, expiry board, lot detail.

   NO COLOUR LITERALS. Every value resolves through a token, including the zone
   and client tints, which arrive as inline custom properties from the server
   (DESIGN 16.5) because those are data rather than design decisions.
   =========================================================================== */

/* --color-surface-sunken MOVED TO variables.css.
   It was declared here, with its own dark override, which is a token definition and
   a theme branch sitting in component CSS. Both are DESIGN 17 items and both were
   found by check_design rather than by reading the file. Colour lives in the
   contract; this file only ever consumes it. */

/* Horizontal scroll belongs to the table, never to the page body. */
.table-wrap { overflow-x: auto; }

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

.card__head {
    padding: var(--space-4) var(--space-5);
    border-bottom: 1px solid var(--color-border);
}
.card__head h2 { margin: 0; font-size: var(--text-base); }
.card__subtitle {
    margin: var(--space-1) 0 0;
    color: var(--color-text-muted);
    font-size: var(--text-xs);
}

/* .empty AND .empty__* ARE GONE. They were a second implementation of .state, used
   by eight screens, and DESIGN 17 forbids a second implementation of a primitive
   that already exists. The two had drifted: .empty wrapped its title in an h3 and
   .state in a p, so half the product's empty states injected a heading level into
   the page outline and half did not, which is a heading-order failure that only
   appears on the screens that happen to be empty. Every caller now includes
   core/_empty.html. */

.check {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-xs);
    color: var(--color-text-muted);
    white-space: nowrap;
}

.filter-bar__field--grow { flex: 1 1 240px; }

/* ---- summary strip ---- */
/* Wraps rather than clipping. Sitting in page-head__actions it competes with the
   heading for width, and at 1440 the third figure was cut off at the viewport edge:
   a total that is present but unreadable is worse than one that has moved to the
   next line. */
.stat-strip {
    display: flex;
    gap: var(--space-5);
    align-items: flex-start;
    flex-wrap: wrap;
    justify-content: flex-end;
}
.page-head { flex-wrap: wrap; }
.page-head__actions { flex-wrap: wrap; }
.stat-strip--stacked { flex-direction: column; gap: var(--space-3); }
.stat__label {
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
}
.stat__value { font-size: var(--text-lg); line-height: 1.1; }

/* ---- expiry board ---- */
.band-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: var(--space-3);
    margin-bottom: var(--space-5);
}
.band {
    padding: var(--space-4);
    border-radius: var(--radius-md);
    border: 1px solid var(--color-border);
    border-left-width: 4px;
    background: var(--color-surface);
}
.band__label { font-size: var(--text-xs); color: var(--color-text-muted); }
.band__value { font-size: var(--text-2xl); line-height: 1.1; margin: var(--space-1) 0; }
.band__detail { font-size: var(--text-2xs); color: var(--color-text-muted); }
.band__foot {
    font-size: var(--text-2xs);
    color: var(--color-text-muted);
    margin-top: var(--space-2);
}

/* Urgency is carried by the label and the count as well as by the edge, so it
   survives greyscale and a projector at the back of a room. */
.band--expired  { border-left-color: var(--color-danger-text); }
.band--critical { border-left-color: var(--color-danger-text); }
.band--warning  { border-left-color: var(--color-warning-text); }
.band--watch    { border-left-color: var(--color-info-text); }
.band--clear    { border-left-color: var(--color-success-text); }

.badge--expired, .badge--critical {
    background: var(--color-danger-light); color: var(--color-danger-text);
}
.badge--warning { background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--watch   { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--clear, .badge--available {
    background: var(--color-success-light); color: var(--color-success-text);
}
.badge--quarantine, .badge--qc_hold {
    background: var(--color-warning-light); color: var(--color-warning-text);
}
.badge--damaged { background: var(--color-danger-light); color: var(--color-danger-text); }
.badge--customs { background: var(--color-gray-200); color: var(--color-gray-700); }

/* ---- lot detail ---- */
.detail-grid {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    gap: var(--space-5);
    margin-bottom: var(--space-5);
}
@media (max-width: 900px) { .detail-grid { grid-template-columns: 1fr; } }
.detail {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: var(--space-2) var(--space-5);
    margin: 0;
    padding: var(--space-4) var(--space-5);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
}
.detail dt { color: var(--color-text-muted); font-size: var(--text-xs); }
.detail dd { margin: 0; font-size: var(--text-sm); }

/* ---- floor map ---- */
.map { display: flex; flex-direction: column; gap: var(--space-3); }
.map__toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    flex-wrap: wrap;
}
.map__canvas-wrap {
    position: relative;
    overflow-x: auto;
    padding: var(--space-3);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
}
.map__canvas { display: block; }

/* Zone labels sit in the DOM above the canvas, in the one-unit strip left clear at
   the top of every zone. pointer-events: none so the canvas still receives the
   hover that drives the tooltip. */
.map__labels {
    position: absolute;
    top: var(--space-3);
    left: var(--space-3);
    pointer-events: none;
    z-index: var(--z-overlay);
}
.map__label {
    position: absolute;
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    padding: 0 var(--space-1);
    height: 26px;
    overflow: hidden;
    white-space: nowrap;
    font-size: var(--text-2xs);
    color: var(--color-text-muted);
}
.map__label-code { color: var(--color-text); font-weight: 600; }
.map__label-name { overflow: hidden; text-overflow: ellipsis; }
.map__label-tag {
    padding: 0 var(--space-1);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-size: var(--text-2xs);
    line-height: 1.4;
}

.segmented {
    display: inline-flex;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    overflow: hidden;
}
.segmented__btn {
    appearance: none;
    border: 0;
    background: var(--color-surface);
    color: var(--color-text-muted);
    font: inherit;
    font-size: var(--text-xs);
    padding: var(--space-2) var(--space-4);
    cursor: pointer;
    border-right: 1px solid var(--color-border);
}
.segmented__btn:last-child { border-right: 0; }
.segmented__btn:hover { background: var(--color-surface-hover); }
.segmented__btn.is-active {
    background: var(--color-surface-hover);
    /* The text stop, never --color-primary, which is 1.91:1 on a light surface. */
    color: var(--color-accent-text);
    font-weight: 600;
    box-shadow: inset 0 -2px 0 var(--color-primary-dark);
}
.segmented__btn:focus-visible {
    outline: 2px solid var(--color-primary-dark);
    outline-offset: -2px;
}

.map__legend { display: flex; gap: var(--space-4); flex-wrap: wrap; }
.legend__item {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-2xs);
    color: var(--color-text-muted);
}
.legend__dot { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
.legend__dot--danger  { background: var(--color-danger); }
.legend__dot--warning { background: var(--color-warning); }
.legend__dot--success { background: var(--color-success); }

.map-tip {
    position: absolute;
    z-index: var(--z-tooltip);
    min-width: 200px;
    max-width: 280px;
    padding: var(--space-3);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    pointer-events: none;
    font-size: var(--text-2xs);
}
.map-tip__head {
    display: flex;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-2);
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--color-border);
}
.map-tip__zone { color: var(--color-text-muted); }
.map-tip__body {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: var(--space-1) var(--space-3);
    margin: 0;
}
.map-tip__body dt { color: var(--color-text-muted); }
.map-tip__body dd { margin: 0; text-align: right; }
.map-tip__empty { margin: 0; color: var(--color-text-muted); }
.map-tip__clients {
    list-style: none;
    margin: var(--space-2) 0 0;
    padding: var(--space-2) 0 0;
    border-top: 1px solid var(--color-border);
}
.map-tip__clients li {
    display: grid;
    grid-template-columns: 10px max-content 1fr;
    align-items: center;
    gap: var(--space-2);
}
.map-tip__clients .num { text-align: right; }
.chip__swatch { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }

.map__table > summary {
    cursor: pointer;
    font-size: var(--text-xs);
    color: var(--color-accent-text);
    padding: var(--space-2) 0;
}

/* ===========================================================================
   P2 inbound: advices, receipts and putaway.
   =========================================================================== */

/* THE AI VOICE, AND THE RESTRAINT THAT MAKES IT MEAN SOMETHING.
   A suggested location is a judgement a person can override, so it carries the
   violet. The tint is the panel token, never the solid accent, and the marking is
   a separate chip beside the status rather than the status badge itself, so no
   status can ever map to the AI tone. Once a task completes this class is gone from
   the row: a committed fact does not wear the AI voice. */
.ai-suggestion {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: 1px var(--space-2);
    border-radius: var(--radius-sm);
    background: var(--color-ai-tint);
}

/* Document states. Each keeps a second, non-colour channel: the word itself. */
.badge--expected   { background: var(--color-gray-200);      color: var(--color-gray-700); }
.badge--arrived    { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--receiving  { background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--received   { background: var(--color-success-light); color: var(--color-success-text); }
.badge--closed     { background: var(--color-gray-200);      color: var(--color-gray-700); }
.badge--cancelled  { background: var(--color-gray-200);      color: var(--color-gray-700); }
.badge--draft      { background: var(--color-gray-200);      color: var(--color-gray-700); }
.badge--posted     { background: var(--color-success-light); color: var(--color-success-text); }
.badge--pending    { background: var(--color-gray-200);      color: var(--color-gray-700); }
.badge--assigned   { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--in_progress{ background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--completed  { background: var(--color-success-light); color: var(--color-success-text); }

/* Variance against an advice. Short and over are different findings, not one
   "discrepancy", because they have different causes and different consequences. */
.badge--short       { background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--over        { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--outstanding { background: var(--color-gray-200);      color: var(--color-gray-700); }
.badge--complete    { background: var(--color-success-light); color: var(--color-success-text); }

/* Server-rendered messages. Each carries its meaning in the words as well as the
   colour, so it survives greyscale and a projector. */
.messages { margin-bottom: var(--space-4); display: grid; gap: var(--space-2); }
.message {
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-md);
    border-left: 4px solid var(--color-border);
    background: var(--color-surface);
    font-size: var(--text-sm);
}
.message--success {
    border-left-color: var(--color-success-text);
    background: var(--color-success-light);
    color: var(--color-success-text);
}
.message--error {
    border-left-color: var(--color-danger-text);
    background: var(--color-danger-light);
    color: var(--color-danger-text);
}
.message--warning {
    border-left-color: var(--color-warning-text);
    background: var(--color-warning-light);
    color: var(--color-warning-text);
}
.message--info { border-left-color: var(--color-info-text); background: var(--color-info-light); }

/* The identifier cell as a link. Underlined on hover and focus only, so a table of
   two hundred rows is not a wall of blue, but the focus ring is always visible
   because that is how a keyboard user knows where they are. */
.row-link { color: inherit; text-decoration: none; }
.row-link:hover { color: var(--color-accent-text); text-decoration: underline; }
.row-link:focus-visible {
    outline: 2px solid var(--color-primary-dark);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

/* ---- P3a outbound ---- */
.card__foot {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-4) var(--space-5);
    border-top: 1px solid var(--color-border);
    flex-wrap: wrap;
}
.card__foot .muted { font-size: var(--text-2xs); max-width: 52ch; }

/* Order and wave states. Each keeps the word as its second channel. */
.badge--allocated { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--picking   { background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--picked    { background: var(--color-success-light); color: var(--color-success-text); }
.badge--released  { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--planned   { background: var(--color-gray-200);      color: var(--color-gray-700); }
.badge--partial   { background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--outstanding { background: var(--color-gray-200);    color: var(--color-gray-700); }

/* ===========================================================================
   P4 yard and gate.
   =========================================================================== */

/* ---- the yard map: DOM, not canvas ----
   Twenty-six bays. Absolutely positioned on a unit grid, the same geometry the
   floor map uses, but as elements: selectable, focusable, translatable, and
   reachable by a screen reader. Canvas would buy nothing at this size. */
/* THE UNIT IS SIZED BY WHAT HAS TO FIT IN A BAY, NOT PICKED FIRST.
   A bay is three units wide and has to hold a registration plate. At 14px a unit
   that is 42px, which cut "RLC-7469" to "RLC-" and "power" to "POW". A plate is the
   one thing on this screen that must be readable, so the unit is 26px and the bay is
   78px. Measured against the real seeded plates rather than guessed. */
.yard {
    position: relative;
    height: calc(20 * 26px + var(--space-6));
    margin: var(--space-4) var(--space-5);
    overflow-x: auto;
}
.yard__bay {
    position: absolute;
    left: calc(var(--x) * 26px);
    top: calc(var(--y) * 26px);
    width: calc(var(--w) * 26px - 5px);
    height: calc(var(--h) * 26px - 5px);
    padding: var(--space-1) var(--space-2);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface-sunken);
    font-size: var(--text-2xs);
    overflow: hidden;
}
.yard__bay.is-occupied {
    background: var(--color-info-light);
    border-color: var(--color-info-text);
}
/* THE ALARM CARRIES A HATCH AS WELL AS A COLOUR, so it survives a projector at the
   back of a room and a monochrome print. A refrigerated trailer on an unpowered bay
   is a spoiled load nobody notices until the doors open. */
.yard__bay.is-alarm {
    background: var(--color-danger-light);
    border-color: var(--color-danger-text);
    background-image: repeating-linear-gradient(
        45deg, transparent, transparent 4px,
        rgba(0, 0, 0, 0.10) 4px, rgba(0, 0, 0, 0.10) 8px
    );
}
.yard__bay.has-power { border-left-width: 3px; border-left-color: var(--color-primary-dark); }
.yard__code { font-weight: 600; }
.yard__power {
    font-size: var(--text-2xs);
    color: var(--color-accent-text);
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.yard__plate { font-weight: 600; }
.yard__meta, .yard__free { color: var(--color-text-muted); }
.yard__alarm { color: var(--color-danger-text); font-weight: 600; }

.legend__dot--info    { background: var(--color-info-text); }
.legend__dot--neutral { background: var(--color-gray-400); }
/* The powered marker is a left edge on the bay, so its legend swatch is one too. */
.legend__dot--power   { background: transparent; border-left: 3px solid var(--color-primary-dark); }

/* ---- the dock scheduler board ----
   Positioned blocks in a lane. The board scrolls inside its own container; the page
   body never scrolls horizontally. */
.board-wrap { overflow-x: auto; padding: var(--space-4) var(--space-5); }
.board { min-width: 900px; }
.board__head, .board__row { display: flex; align-items: stretch; }
.board__door-label {
    flex: 0 0 7ch;
    font-size: var(--text-2xs);
    display: flex;
    align-items: center;
    padding-right: var(--space-2);
}
.board__hours { position: relative; flex: 1; display: flex; }
.board__hour {
    flex: 0 0 var(--pct);
    font-size: var(--text-2xs);
    color: var(--color-text-muted);
    border-left: 1px solid var(--color-border);
    padding-left: 2px;
}
.board__lane {
    position: relative;
    flex: 1;
    height: 28px;
    margin-bottom: 3px;
    background: var(--color-surface-sunken);
    border-radius: var(--radius-sm);
    overflow: hidden;
}
.board__rule {
    position: absolute; top: 0; bottom: 0; width: 1px;
    background: var(--color-border);
}
/* Now, as a line. A board with no "now" makes you work out where you are. */
.board__now {
    position: absolute; top: 0; bottom: 0; width: 2px;
    background: var(--color-primary-dark);
    z-index: var(--z-overlay);
}
.board__block {
    position: absolute; top: 2px; bottom: 2px;
    display: flex; align-items: center; gap: var(--space-2);
    padding: 0 var(--space-2);
    border-radius: var(--radius-sm);
    font-size: var(--text-2xs);
    white-space: nowrap;
    overflow: hidden;
    /* The client's own colour as a left edge, so a door's day can be read by client
       without the block itself becoming a client chip. */
    border-left: 3px solid var(--client-colour, var(--color-gray-400));
    background: var(--color-gray-200);
    color: var(--color-gray-700);
}
.board__block--confirmed   { background: var(--color-info-light);    color: var(--color-info-text); }
.board__block--arrived     { background: var(--color-info-light);    color: var(--color-info-text); }
.board__block--in_progress { background: var(--color-warning-light); color: var(--color-warning-text); }
.board__block--complete    { background: var(--color-success-light); color: var(--color-success-text); }
.board__block--no_show     { background: var(--color-danger-light);  color: var(--color-danger-text); }
.board__block--cancelled   { opacity: 0.5; }
.board__block-time { font-weight: 600; }
.board__block-client { font-weight: 600; }

.gate-actions { display: flex; gap: var(--space-3); flex-wrap: wrap; }
.pass-codes {
    padding: var(--space-4) var(--space-5);
    display: grid;
    gap: var(--space-4);
    justify-items: center;
}
.pass-codes svg { max-width: 100%; height: auto; }

/* Yard and appointment states. */
.badge--at_gate  { background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--in_yard  { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--on_dock  { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--loaded   { background: var(--color-success-light); color: var(--color-success-text); }
.badge--released { background: var(--color-success-light); color: var(--color-success-text); }
.badge--departed { background: var(--color-gray-200);      color: var(--color-gray-700); }
.badge--refused  { background: var(--color-danger-light);  color: var(--color-danger-text); }
.badge--issued   { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--presented{ background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--cleared  { background: var(--color-success-light); color: var(--color-success-text); }
.badge--void     { background: var(--color-danger-light);  color: var(--color-danger-text); }
.badge--booked   { background: var(--color-gray-200);      color: var(--color-gray-700); }
.badge--confirmed{ background: var(--color-info-light);    color: var(--color-info-text); }
.badge--arrived  { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--no_show  { background: var(--color-danger-light);  color: var(--color-danger-text); }
.badge--complete { background: var(--color-success-light); color: var(--color-success-text); }
/* Shipment and handling unit states. */
.badge--building { background: var(--color-gray-200);      color: var(--color-gray-700); }
.badge--loading  { background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--empty    { background: var(--color-gray-200);      color: var(--color-gray-700); }
.badge--open     { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--closed   { background: var(--color-success-light); color: var(--color-success-text); }
.badge--info     { background: var(--color-info-light);    color: var(--color-info-text); }

.inline-form { display: flex; gap: var(--space-2); align-items: center; }
/* The picker roster in a table cell. Six names on one line push the pinned action
   column off the viewport, so this one wraps rather than scrolling the row. */
.inline-form--wrap { flex-wrap: wrap; row-gap: var(--space-1); max-width: 26rem; }
.input--sm { height: 32px; font-size: var(--text-xs); padding: 0 var(--space-2); }

/* ---------------------------------------------------------------------------
   P6. Billing, the integration monitor and the client portal.

   No colour literal here, per DESIGN 1. Every value resolves through a token, and
   the invoice states reuse the same four semantic families every other document in
   the product uses, so an approved invoice reads the same way as a posted count.
   --------------------------------------------------------------------------- */

/* Invoice states. */
.badge--review    { background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--approved  { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--sent      { background: var(--color-success-light); color: var(--color-success-text); }

/* Integration message states. */
.badge--processed { background: var(--color-success-light); color: var(--color-success-text); }
.badge--failed    { background: var(--color-danger-light);  color: var(--color-danger-text); }
.badge--retried   { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--ignored   { background: var(--color-gray-200);      color: var(--color-gray-700); }
.badge--received  { background: var(--color-gray-200);      color: var(--color-gray-700); }

/* The charge table.

   The BASIS column is the point of this screen and is deliberately given room. A
   line reading "Storage 4,812.00" invites the question "for what", and a 3PL that
   cannot answer it in the same breath is a 3PL in a dispute. */
.charge-basis {
    font-size: var(--text-2xs);
    color: var(--color-gray-600);
    line-height: var(--leading-snug);
}
.charge-total td {
    border-top: 2px solid var(--color-gray-300);
    font-weight: var(--font-semibold);
    padding-top: var(--space-3);
}
.charge-total__label { text-align: right; }

/* The machine marking on a draft invoice's lines.

   DESIGN 6.3: violet is reserved for machine judgements a person can accept or
   override, and it decays the moment a human commits. A draft's charge lines are a
   proposal; from approval onwards they are a committed figure and this rail is
   simply absent rather than restyled. */
.charge-proposed {
    border-left: 3px solid var(--color-ai);
    background: var(--color-ai-tint);
}
.proposal-note {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-left: 3px solid var(--color-ai);
    background: var(--color-ai-tint);
    color: var(--color-ai-dark);
    font-size: var(--text-xs);
}
.committed-note {
    padding: var(--space-3) var(--space-4);
    border-left: 3px solid var(--color-success);
    background: var(--color-success-light);
    color: var(--color-success-text);
    font-size: var(--text-xs);
}

/* Endpoint tiles on the monitor. Green is the resting state, and it should be. */
.endpoint-strip {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}
.endpoint {
    padding: var(--space-3);
    border: 1px solid var(--color-gray-200);
    border-radius: var(--radius-md);
    background: var(--color-surface);
}
.endpoint--alert { border-color: var(--color-danger); }
.endpoint__name { font-size: var(--text-xs); font-weight: var(--font-semibold); }
.endpoint__meta { font-size: var(--text-2xs); color: var(--color-gray-600); }
.endpoint__count {
    font-family: var(--font-mono);
    font-size: var(--text-lg);
    font-variant-numeric: tabular-nums;
}

/* The failure reason, in words a person can act on. A monitor full of red rows
   saying "error" teaches people to ignore the monitor. */
.failure-reason {
    font-size: var(--text-xs);
    color: var(--color-danger-text);
}
.failure-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--color-gray-200);
}
.failure-row:last-child { border-bottom: 0; }
.failure-row__actions { display: flex; gap: var(--space-2); flex-shrink: 0; }

/* Client portal. Wider gutters and larger figures than an operator screen, because
   this one is read by somebody who opens it once a week, not all day. */
.portal-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: var(--space-4);
}

/* A policy note. NOT the violet panel.

   The separation-of-duties warning first reused .proposal-note, which carries the
   machine-generated rail. DESIGN 6.3 reserves violet strictly for machine judgements
   a person can accept or override, and "you counted this, so somebody else has to
   approve it" is a rule rather than a judgement. Borrowing the AI tone for a policy
   message is exactly how the convention stops meaning anything. */
.policy-note {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-left: 3px solid var(--color-warning);
    background: var(--color-warning-light);
    color: var(--color-warning-text);
    font-size: var(--text-xs);
}

/* ---------------------------------------------------------------------------
   The operations dashboard.

   EXCEPTIONS ARE THE HEADLINE, so they get the largest figures on the page. A
   queue at zero is deliberately quiet: no colour, no border, just the number, so
   that the one row that is not zero is the thing the eye lands on. A dashboard
   where every tile shouts is a dashboard nobody reads.
   --------------------------------------------------------------------------- */
.exception-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: var(--space-3);
}
.exception {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--color-border);
    border-left: 3px solid var(--color-gray-300);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    text-decoration: none;
    color: var(--color-text);
}
.exception:hover { background: var(--color-surface-hover); }
.exception__count {
    font-family: var(--font-mono);
    font-size: var(--text-2xl);
    font-variant-numeric: tabular-nums;
    line-height: var(--leading-tight);
    color: var(--color-text-muted);
}
.exception__label { font-size: var(--text-xs); font-weight: var(--font-semibold); }
.exception__detail { font-size: var(--text-2xs); color: var(--color-gray-600); }

/* A queue with something in it earns its colour, and nothing else does. */
.exception--live .exception__count { color: var(--color-text); }
.exception--warning { border-left-color: var(--color-warning); }
.exception--danger  { border-left-color: var(--color-danger); }
.exception--info    { border-left-color: var(--color-info); }

.dash-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
    gap: var(--space-4);
    margin-top: var(--space-4);
}

/* ---------------------------------------------------------------------------
   P9 Estate and stock control

   Three additions, and nothing that duplicates a primitive that already exists.
   The row action column reuses .table--pinned, which was built for exactly this
   and had one caller. The meter is a second channel beside a percentage that is
   always printed next to it, never a replacement for it.
   --------------------------------------------------------------------------- */

/* Row actions live in the pinned last column. Right aligned so the buttons line
   up down the table rather than tracking the width of whatever precedes them. */
.row-actions {
    display: flex;
    gap: var(--space-1);
    justify-content: flex-end;
    align-items: center;
}
.row-actions form { margin: 0; }

/* A utilisation bar. NOT A CHART.
   The number is always printed beside it, because a length is a channel a viewer
   has to estimate and DESIGN 10.4 does not accept an estimate as the only reading
   of a figure. The fill width arrives as a scoped custom property from the server,
   which is the sanctioned way to pass a measured value into the stylesheet. */
.meter {
    display: inline-block;
    width: 72px;
    height: 6px;
    border-radius: var(--radius-full);
    background: var(--color-gray-200);
    overflow: hidden;
    vertical-align: middle;
}
.meter__fill {
    display: block;
    height: 100%;
    width: var(--meter, 0%);
    background: var(--color-primary-dark);
    border-radius: var(--radius-full);
}
.meter__value {
    margin-left: var(--space-2);
    font-size: var(--text-xs);
}

/* The zone tint, beside its code. Same two-channel rule as the client chip: the
   colour finds it, the code identifies it, and both always ship together. */
.zone-swatch {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: var(--radius-sm);
    background: var(--zone-colour, var(--color-gray-400));
    margin-right: var(--space-2);
    vertical-align: baseline;
}

/* A form inside a card. Two columns above 720px so a nine-field rack builder is
   one screen rather than a scroll, one column below it. */
.form-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 0 var(--space-4);
    align-items: start;
}
.form-grid__actions {
    grid-column: 1 / -1;
    display: flex;
    gap: var(--space-2);
    align-items: center;
    margin-top: var(--space-2);
}

/* Location and licence plate states. Every one of these is rendered by
   _cell.html as badge--{{ value|lower }}, so a value with no rule here draws as an
   unstyled grey box. badge--bonded was already in that position before this
   section existed: the stock list has rendered a customs status since P1. */
.badge--bin, .badge--floor { background: var(--color-gray-200); color: var(--color-gray-700); }
.badge--rail        { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--dock,
.badge--staging     { background: var(--color-gray-200);      color: var(--color-gray-700); }
.badge--virtual     { background: var(--color-gray-100);      color: var(--color-gray-600); }
.badge--blocked     { background: var(--color-danger-light);  color: var(--color-danger-text); }
.badge--counting    { background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--inactive    { background: var(--color-gray-200);      color: var(--color-gray-600); }
.badge--pick_face   { background: var(--color-success-light); color: var(--color-success-text); }
.badge--restricted  { background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--recalled    { background: var(--color-danger-light);  color: var(--color-danger-text); }
.badge--blocked_stock { background: var(--color-danger-light); color: var(--color-danger-text); }

/* Customs status. Three values, and the bonded one has to READ as a constraint:
   despatch is refused while goods are under customs control, so a grey badge
   understates it. */
.badge--bonded           { background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--duty_paid        { background: var(--color-success-light); color: var(--color-success-text); }
.badge--free_circulation { background: var(--color-gray-200);      color: var(--color-gray-700); }

/* Temperature bands, on a zone header. */
.badge--ambient          { background: var(--color-gray-200);      color: var(--color-gray-700); }
.badge--air_conditioned  { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--cool,
.badge--chilled          { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--frozen           { background: var(--color-info-light);    color: var(--color-info-text); }

/* Licence plate kinds and numbering schemes. */
.badge--pallet, .badge--carton, .badge--tote, .badge--cage,
.badge--goh_trolley, .badge--rail_kit,
.badge--internal, .badge--sscc18 {
    background: var(--color-gray-200); color: var(--color-gray-700);
}
.badge--despatched { background: var(--color-gray-200); color: var(--color-gray-600); }

/* ---------------------------------------------------------------------------
   THE RAIL ON A PROJECTOR.

   A 1280x720 projector leaves the sidebar 656 px, and check_p7_presentation
   demands forty of those spare, because "it fits exactly" is a state that lasts
   until somebody adds a screen and the thing that falls off the bottom is whatever
   was last. The rail was tightened once already on that measurement; this is the
   second time it has been reached, and the phases still to come add more sections.

   So the desktop keeps its comfortable rail and the projector gets a tighter one.
   Two pixels per row and a step of group margin, which measured at roughly eighty
   pixels across a twenty-item rail: enough headroom for the sections still to be
   built rather than enough for this one.

   Font size is untouched. DESIGN 2 sets the body floor at 12 px and this rail is
   already there; what compresses is the space around the words, not the words.
   --------------------------------------------------------------------------- */
@media (max-height: 800px) {
    /* THIS BLOCK IS TIGHTER THAN IT WAS, AND THE SIGNATURE PAID FOR PART OF IT.

       The rail did not fit a 720p projector before the signature existed: measured,
       it needed 633 px of the 656 the projector leaves, which is 23 px of headroom
       against the 40 check_p7_presentation asks for. That was a standing failure
       nobody had measured since the screens added at P9 to P13.

       Taking 46 px for the signature would have deepened it to minus 21, so the
       three numbers below give back more than the signature takes. None of them
       touches a font size: DESIGN 2 floors the body face at 12 px and this rail is
       already there. What compresses is a pixel of row height, the space above a
       group heading, and the leading of the heading itself. */
    .sidebar { padding: var(--space-1) var(--space-2); }
    .sidebar__group { margin: 0; line-height: 1.05; }
    .sidebar__link { min-height: 19px; }

    /* THE SIGNATURE COMPRESSES WITH THE RAIL, because it is spending the rail's
       pixels and the rail is what does not fit here. What compresses is the artwork
       and the space around the words. NOTHING IS HIDDEN: a signature that drops its
       address or its copyright on a projector is a signature missing from the one
       screen a room full of people is looking at. */
    :root { --footer-height: 46px; }
    .site-footer { gap: 0; padding: var(--space-1) var(--space-2); }
    .site-footer__logo { height: 18px; }
    .site-footer__legal { line-height: 1; }
}

/* ---------------------------------------------------------------------------
   P10 Value added services

   The work order states and the three line roles. Every one of these is rendered
   by _cell.html as badge--{{ value|lower }}, so a value with no rule here draws as
   an unstyled grey box.
   --------------------------------------------------------------------------- */
.badge--suggested   { background: var(--color-ai-tint);       color: var(--color-ai-dark); }
.badge--build       { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--break       { background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--convert     { background: var(--color-info-light);    color: var(--color-info-text); }
.badge--handle      { background: var(--color-gray-200);      color: var(--color-gray-700); }
.badge--input       { background: var(--color-warning-light); color: var(--color-warning-text); }
.badge--output      { background: var(--color-success-light); color: var(--color-success-text); }
.badge--handled     { background: var(--color-gray-200);      color: var(--color-gray-700); }

/* A definition list read across rather than down, for the two figures a suggestion
   is based on. Inside the violet panel only, where there are never more than three. */
.detail--inline {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-4);
    align-items: baseline;
    margin: var(--space-2) 0 0;
}
.detail--inline dt {
    font-size: var(--text-xs);
    color: var(--color-text-muted);
}
.detail--inline dd {
    margin: 0 var(--space-3) 0 0;
    font-weight: var(--font-semibold);
}
.proposal-note__actions {
    display: flex;
    gap: var(--space-2);
    margin-top: var(--space-3);
}

/* ---------------------------------------------------------------------------
   P11 Directed work

   The bulk bar above a work list, and the bare checkbox that sits inside an
   identifier cell without pushing the code out of alignment.
   --------------------------------------------------------------------------- */
.bulk-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    margin-bottom: var(--space-3);
    background: var(--color-surface-sunken);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
}
.bulk-bar__actions {
    display: flex;
    gap: var(--space-2);
    align-items: center;
}
/* Inside a table cell the checkbox is the affordance and the code is the label, so
   the pair reads as one thing rather than as a control beside an identifier. */
.check--bare {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
}

/* The slotting review's two findings. They are different problems, so they get
   different tones: a fast line in the back is a cost, a slow line on a face is a
   waste, and reading them as one list hides that. */
.finding-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
    gap: var(--space-4);
}

/* ---------------------------------------------------------------------------
   P12.2. The transport handover.

   Two classes, both earning their place. No colour literal: every value resolves
   through a token, per DESIGN 1.
   --------------------------------------------------------------------------- */

/* The exact payload, in a disclosure on the consignment page. Monospace because it
   is a machine message and a proportional font makes a JSON object unreadable;
   scrolls in its own box so a long address cannot push the page sideways. */
.payload {
    margin: 0;
    padding: var(--space-3) var(--space-4);
    background: var(--color-surface-sunken);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--color-text);
    max-height: 26rem;
    overflow: auto;
    white-space: pre;
}

/* The reverse of the run: what goes on the trailer first. Numbered, because the
   whole point is the order. */
.loading-order {
    margin: 0;
    padding: 0 0 0 var(--space-6);
    display: grid;
    gap: var(--space-2);
    font-size: var(--text-sm);
}
.loading-order li { padding-left: var(--space-1); }

/* ---------------------------------------------------------------------------
   P13.1. Reports: the chart, its legend, and the filter bar's window control.

   THE SERIES COLOUR ARRIVES AS A SCOPED CUSTOM PROPERTY, --series, set inline by
   the chart tag from a solved token. That is the one shape the design linter
   recognises for a per-element colour, and it means no colour literal appears here
   and no per-series class has to be written. DESIGN 1 holds.
   --------------------------------------------------------------------------- */
.chart-wrap { display: grid; gap: var(--space-3); }
.chart {
    width: 100%;
    height: auto;
    display: block;
    overflow: visible;
}
.chart__grid {
    stroke: var(--color-border);
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
}
.chart__axis {
    fill: var(--color-text-muted);
    font-family: var(--font-mono);
    font-size: 11px;
}
.chart__bar { fill: var(--series); }
.chart__line {
    fill: none;
    stroke: var(--series);
    stroke-width: 2;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
}
.chart-legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
    font-size: var(--text-xs);
    color: var(--color-text-muted);
}
.chart-legend__item { display: inline-flex; align-items: center; gap: var(--space-2); }
.chart-legend__swatch {
    width: 12px; height: 12px;
    border-radius: 2px;
    background: var(--series);
    flex-shrink: 0;
}
.chart-legend__unit { margin-left: auto; font-variant-numeric: tabular-nums; }

/* The report catalogue: one card per group, one row per report. */
.report-groups { display: grid; gap: var(--space-5); }
.report-list { display: grid; gap: var(--space-1); }
.report-list__row {
    display: grid;
    grid-template-columns: minmax(0, 22ch) minmax(0, 1fr) auto;
    gap: var(--space-3);
    align-items: baseline;
    padding: var(--space-2) 0;
    border-bottom: 1px solid var(--color-border);
}
.report-list__row:last-child { border-bottom: 0; }
@media (max-width: 720px) {
    .report-list__row { grid-template-columns: 1fr; gap: var(--space-1); }
}
