/* Components, written only against the tokens in tokens.css.
 *
 * Only what this app actually has: shell, card, table, button, form field,
 * chip, tab bar, pager, alert, tree group, skeleton - and, with the design
 * system, the stat tile, the hint banner, the avatar and the thumbnail. No
 * grid system, no utility classes, no components nobody uses.
 *
 * Never put a literal colour in this file. If a component needs a colour that
 * is not in tokens.css, the token is missing - add it there.
 *
 * The look is the Stock of the Earth design system (Figma). docs/
 * design-system.md says which class is which of its components, and how a
 * page is assembled from them.
 */

/* ---- legacy variable aliases ---------------------------------------------
 * 176 inline `style="... var(--muted) ..."` uses are spread across the
 * templates. Aliasing the old names onto the M3 roles keeps every one of them
 * working, and makes them dark-mode aware for free - which editing 176 call
 * sites by hand would not.
 *
 * These are a migration bridge. New code uses the --md-sys-* roles directly,
 * and this block shrinks as templates are converted.
 */
:root {
  --ink: var(--md-sys-color-on-surface);
  --muted: var(--md-sys-color-on-surface-variant);
  --line: var(--md-sys-color-outline-variant);
  --accent: var(--md-sys-color-primary);
  --accent-dim: var(--md-sys-color-primary-container);
  --bg: var(--md-sys-color-surface);
  --card: var(--md-sys-color-surface-container-low);
  --danger: var(--md-sys-color-error);
  --danger-dim: var(--md-sys-color-error-container);
  --warn: var(--md-sys-color-warning);
  --warn-dim: var(--md-sys-color-warning-container);
}

/* ---- base ---------------------------------------------------------------- */

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: var(--md-sys-typescale-font);
  font-size: var(--md-sys-typescale-body-size);
  line-height: var(--md-sys-typescale-body-line);
  letter-spacing: var(--md-sys-typescale-body-tracking);
  color: var(--md-sys-color-on-surface);
  background: var(--md-sys-color-surface);
  /* Inter has true tabular figures and a straight-sided 1; asking for them
     everywhere keeps a column of numbers lined up without each table having
     to remember to. */
  font-feature-settings: "cv05" 1, "tnum" 1;
}

/* The sidebar shell is opt-in, not the default for every page that loads this
   stylesheet: the examples app and the login page stack normally and scroll
   the document, and a flexed, overflow-hidden body would break both. */
html:has(body.app-shell),
body.app-shell {
  height: 100%;
}

/* The photograph the page sheet floats on: the design's sky by day, its
   night sky after dark. On the body, fixed, so it never scrolls - the sheet
   in front of it does. The plain surface colour underneath is what shows
   while the image loads, and is near enough the sky's own that the swap is
   hard to see. */
body.app-shell {
  display: flex;
  overflow: hidden;
  background-color: var(--md-sys-color-surface);
  background-image: var(--app-backdrop-light);
  background-size: cover;
  background-position: center;
  background-attachment: fixed;
}

/* light-dark() takes colours and nothing else, so the image follows the
   scheme the same three ways the scheme itself is decided: pinned dark,
   pinned light, or the operating system's when nothing is pinned. */
:root[data-theme="dark"] body.app-shell {
  background-image: var(--app-backdrop-dark);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body.app-shell {
    background-image: var(--app-backdrop-dark);
  }
}

a {
  color: var(--md-sys-color-primary);
}

/* The design's three heading steps, in the condensed display face and the
   heading colour. Line heights tighter than the mock's 36px, which is the
   frame Figma drew the text in rather than a leading anybody would set: on a
   two-line heading it puts a blank line between them. */
h1,
h2,
h3 {
  font-family: var(--md-sys-typescale-display-font);
  font-weight: var(--md-sys-typescale-headline-weight);
  color: var(--md-sys-color-heading);
  letter-spacing: 0;
  /* Asap Condensed's word space is narrower than Antique Olive's, enough that
     "Stock of the Earth" reads as one word at 18px. */
  word-spacing: 0.08em;
  line-height: 1.15;
}

h1 {
  font-size: var(--md-sys-typescale-headline-size);
  letter-spacing: var(--md-sys-typescale-headline-tracking);
  margin: var(--md-space-1) 0 var(--md-space-5);
}

h2 {
  font-size: var(--md-sys-typescale-headline-small-size);
}

h1 .muted,
h1 small,
h2 .muted,
h2 small,
h3 .muted,
h3 small {
  font-family: var(--md-sys-typescale-font);
  font-size: var(--md-sys-typescale-body-large-size);
  font-weight: 500;
  word-spacing: normal;
  letter-spacing: var(--md-sys-typescale-body-tracking);
}

h3 {
  font-size: var(--md-sys-typescale-title-size);
}

.muted {
  color: var(--md-sys-color-on-surface-variant);
}

/* What a quantity amounts to in packs, under the quantity itself. Quiet and on
 * its own line: the number is what a screen is read for, the packs are what
 * they mean once somebody walks to the shelf. Normal weight even inside a
 * <strong> total, so a bold row does not promote the breakdown with it. */
.pack-breakdown {
  display: block;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: 400;
  color: var(--md-sys-color-on-surface-variant);
  white-space: nowrap;
}

/* ---- app shell -----------------------------------------------------------
 * The design's Nav Bar on the photograph, beside a rounded page sheet that is
 * the scrolling pane. The sidebar has no panel of its own: it is words and
 * icon tiles straight on the sky, which is why its text sits on the darkest
 * ink the theme has rather than a muted one. */

.sidebar {
  width: calc(var(--sidebar-w) + var(--md-space-3));
  flex: 0 0 auto;
  height: 100%;
  padding: 0 0 var(--md-space-6) var(--md-space-3);
  display: flex;
  flex-direction: column;
  gap: var(--md-space-4);
  transition: width var(--md-sys-motion-duration-medium)
    var(--md-sys-motion-easing-standard);
}

/* The wordmark and the three tools on one line, as the design draws them. */
.sidebar-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--md-space-2);
  min-height: 2.75rem;
  padding: var(--md-space-1) 0 0 var(--md-space-3);
}

.brand {
  font-family: var(--md-sys-typescale-display-font);
  font-weight: 700;
  font-size: var(--md-sys-typescale-title-size);
  line-height: 1.2;
  letter-spacing: 0;
  color: var(--md-sys-color-heading);
  text-decoration: none;
  white-space: nowrap;
  word-spacing: 0.08em;
}

.sidebar-tools {
  display: flex;
  align-items: center;
  gap: var(--md-space-1);
  /* The design keeps the last icon on the column's right edge, where the nav
     items' chevrons line up under it. */
  margin-right: calc(-1 * var(--md-space-2));
}

/* An icon-only button: the 16px glyph in a 28px target. The glyph is the
   design's teal by day and its grey at night - the heading colour - and the
   button carries the accessible name (aria-label), never the glyph. */
.icon-btn {
  display: inline-grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--md-sys-shape-corner-sm);
  background: transparent;
  color: var(--md-sys-color-heading);
  font-size: 1rem; /* the icon is 1em */
  line-height: 1;
}

.icon-btn:not(:disabled):hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-surface) var(--md-sys-state-hover-opacity),
    transparent
  );
}

.sidebar nav {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--md-space-1);
  /* Room for the focus ring, which the scroll container would otherwise clip
     on the left edge. */
  padding: 2px;
  margin: -2px;
}

/* A destination, and the toggle of a group - the same box, so a link and a
   collapsible group sit at the same height with their icons on the same
   vertical line and the same gap to the label. They drifted the moment the
   toggle became a flex row and the link stayed inline. */
.sidebar .nav-link,
.nav-group-toggle {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  width: 100%;
  min-height: 2.125rem;
  padding: var(--md-space-2) var(--md-space-4) var(--md-space-2)
    var(--md-space-3);
  border: 0;
  border-radius: var(--md-sys-shape-corner-md);
  background: transparent;
  color: var(--md-sys-color-on-surface);
  font: inherit;
  font-size: var(--md-sys-typescale-label-size);
  font-weight: var(--md-sys-typescale-label-weight);
  line-height: 1.3;
  letter-spacing: -0.005em;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.sidebar .nav-link:hover,
.nav-group-toggle:hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-nav-highlight) 70%,
    transparent
  );
}

/* The current destination: the design's nav highlight - near-white on the sky,
   black on the stars. The fill alone marks it; aria-current tells a screen
   reader the same. */
.sidebar .nav-link.active {
  background: var(--md-sys-color-nav-highlight);
}

.nav-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The section icon: the design's 18px tile in the section's highlight, the
   glyph in dark ink on it in both themes. The tile is drawn here rather than
   in the sprite so the colour-vision palette can repaint it. */
.icon.sec-icon {
  width: 18px;
  height: 18px;
  padding: 0;
  border-radius: var(--md-sys-shape-corner-xs);
  background: var(--section);
  color: var(--md-sys-color-on-highlight);
  vertical-align: middle;
}

/* A collapsible group of nav destinations. Alpine-driven (x-show +
   x-transition), not <details> - a native details/summary toggle can't
   animate open/closed (browsers hide its content via `display: none`,
   which no stable CSS transition can animate), so this uses the same
   x-data/x-show pattern as .user-menu below instead.

   Open, it becomes the design's highlighted panel: the toggle, a rule, and
   the sub-pages indented under it, all on the nav highlight. */
.nav-group {
  border-radius: var(--md-sys-shape-corner-md);
}

.nav-group.is-open {
  background: var(--md-sys-color-nav-highlight);
  padding-bottom: var(--md-space-3);
}

.nav-group.is-open > .nav-group-toggle:hover {
  background: transparent;
}

/* The chevron belongs to the row, not to the label, so it is pushed to the
   far edge; it points down closed and up open, as the design draws it. The
   shared one (_chevron.html) points right, hence the quarter turns. */
.nav-group-toggle .chev,
.user-trigger .chev {
  display: inline-flex;
  transform: rotate(90deg);
  transition: transform var(--md-sys-motion-duration-short)
    var(--md-sys-motion-easing-standard);
}

.nav-group-toggle .chev-icon,
.user-trigger .chev-icon {
  width: 16px;
  height: 16px;
  stroke-width: 1.5;
}

.nav-group.is-open > .nav-group-toggle .chev {
  transform: rotate(-90deg);
}

.nav-group-children {
  display: flex;
  flex-direction: column;
  gap: var(--md-space-2);
  margin: 0 var(--md-space-4) 0 var(--md-space-3);
  padding: var(--md-space-2) 0 0 var(--md-space-5);
  border-top: 1px solid var(--md-sys-color-nav-divider);
}

/* A sub-page: text only, as the design draws it - the group's icon already
   says where you are. The current one takes the section's highlight let down
   into the panel (--section-soft) and a heavier weight, so it is not marked by
   colour alone. */
.nav-group-children a {
  display: flex;
  align-items: center;
  min-height: 1.625rem;
  padding: 0 var(--md-space-6) 0 var(--md-space-2);
  border-radius: var(--md-sys-shape-corner-xs);
  color: var(--md-sys-color-on-surface);
  font-size: var(--md-sys-typescale-label-size);
  font-weight: var(--md-sys-typescale-label-weight);
  line-height: 1.3;
  text-decoration: none;
}

.nav-group-children a:hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-surface) var(--md-sys-state-hover-opacity),
    transparent
  );
}

.nav-group-children a.active {
  background: var(--section-soft);
  color: var(--section-soft-ink);
  font-weight: 600;
}

/* The rule between the destinations and the links about the app. */
.nav-divider {
  height: 1px;
  margin: var(--md-space-3) 0;
  background: var(--md-sys-color-nav-divider);
}

/* The account, at the foot: the design's User Avatar - a filled bar like the
   current destination, so it reads as a control rather than a caption. */
.sidebar .user-menu {
  position: relative;
}

.user-trigger {
  width: 100%;
  min-height: 3.1875rem;
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  padding: var(--md-space-2) var(--md-space-4) var(--md-space-2)
    var(--md-space-3);
  background: var(--md-sys-color-nav-highlight);
  border: 0;
  border-radius: var(--md-sys-shape-corner-md);
  font: inherit;
  font-size: var(--md-sys-typescale-label-size);
  font-weight: var(--md-sys-typescale-label-weight);
  color: var(--md-sys-color-on-surface);
  cursor: pointer;
  text-align: left;
}

.user-trigger:hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-surface) var(--md-sys-state-hover-opacity),
    var(--md-sys-color-nav-highlight)
  );
}

.user-trigger .email {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The shared chevron points right; this menu's closed state points down. */
.chev-down {
  transform: rotate(90deg);
}

.user-trigger[aria-expanded="true"] .chev {
  transform: rotate(-90deg);
}

/* A menu floats over the page, which is the one place the design's soft tile
   shadow gives way to the heavier floating one. */
.user-dropdown {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(100% + var(--md-space-2));
  background: var(--md-sys-color-surface-container-high);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-panel);
  box-shadow: var(--md-sys-elevation-3);
  padding: var(--md-space-2);
  display: flex;
  flex-direction: column;
  gap: 2px;
  z-index: 20;
  /* The open/close animation is Alpine's x-transition in base.html, not a rule
     here: x-show toggles display, and there is no transition out of
     display:none for CSS to hook. */
}

/* The theme buttons are a row of three, not menu items. */
.user-dropdown .theme-switch button {
  width: auto;
  justify-content: center;
}

.user-dropdown a,
.user-dropdown button {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  width: 100%;
  padding: var(--md-space-2) var(--md-space-3);
  border-radius: var(--md-sys-shape-corner-sm);
  font-size: var(--md-sys-typescale-body-size);
  color: var(--md-sys-color-on-surface);
  text-decoration: none;
  background: none;
  border: 0;
  font-family: inherit;
  font-weight: 400;
  text-align: left;
  cursor: pointer;
}

.user-dropdown a:hover,
.user-dropdown button:hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-surface) var(--md-sys-state-hover-opacity),
    transparent
  );
  color: var(--md-sys-color-on-surface);
}

.user-dropdown .divider {
  height: 1px;
  background: var(--md-sys-color-outline-variant);
  margin: var(--md-space-1) 2px;
}

.user-dropdown .tenant-name {
  padding: var(--md-space-2) var(--md-space-3);
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  color: var(--md-sys-color-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The running commit. It inherits the dropdown's button rule above - full
   width, the same padding and hover - so it reads as one of the menu's items
   rather than as a caption wedged between them. Muted, because it is reference
   rather than an action somebody came here to take. */
.user-dropdown .menu-version {
  font-size: var(--md-sys-typescale-label-small-size);
  color: var(--md-sys-color-on-surface-variant);
  white-space: nowrap;
}

/* Monospace only on the value, and not uppercased: it is read character by
   character and may well be retyped, so the letterforms have to be
   distinguishable and the case has to be the real one. `user-select: all` so a
   drag selects the whole hash, for anyone who would rather not trust a
   clipboard button. */
.user-dropdown .menu-version__value {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  user-select: all;
}

/* Pushed to the right and given its own colour so the confirmation is not
   mistaken for part of the value. It occupies no space when empty, which is why
   the label and value do not shift when it appears. */
.user-dropdown .menu-version__state {
  margin-left: auto;
  color: var(--md-sys-color-primary);
}

/* Theme switch: the three choices are radio-like - the design's segmented
   pills, the chosen one raised on a field-white fill. */
.theme-switch {
  display: flex;
  gap: 2px;
  margin: var(--md-space-1) var(--md-space-2) var(--md-space-2);
  padding: 3px;
  background: var(--md-sys-color-surface-container);
  border-radius: var(--md-sys-shape-corner-md);
}

.theme-switch button {
  flex: 1;
  text-align: center;
  padding: var(--md-space-1) 0;
  font-size: var(--md-sys-typescale-label-small-size);
  border: 1px solid transparent;
  border-radius: var(--md-sys-shape-corner-sm);
  color: var(--md-sys-color-on-surface);
  background: none;
  cursor: pointer;
}

.theme-switch button[aria-pressed="true"] {
  background: var(--md-sys-color-surface-container-lowest);
  border-color: var(--md-sys-color-field-outline);
  font-weight: 600;
}

[x-cloak] {
  display: none !important;
}

/* ---- the collapsed rail ---------------------------------------------------
 * The sidebar folded to its icons (the first of the three tools). Desktop
 * only: below the breakpoint the sidebar is a drawer and is either all there
 * or not there at all, so the narrow-screen block undoes all of this.
 *
 * Labels are hidden visually, not removed - each destination keeps its name
 * for a screen reader, and the wordmark keeps its link. */
@media (min-width: 881px) {
  .is-railed .sidebar {
    width: calc(var(--sidebar-rail-w) + var(--md-space-3));
  }

  .is-railed .sidebar-head {
    flex-direction: column;
    align-items: stretch;
    padding-left: 0;
  }

  .is-railed .sidebar-head .brand,
  .is-railed .sidebar .search-box,
  .is-railed .sidebar .nav-label,
  .is-railed .sidebar .nav-group-toggle .chev,
  .is-railed .sidebar .user-trigger .email,
  .is-railed .sidebar .user-trigger .chev {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  .is-railed .sidebar-tools {
    flex-direction: column;
    margin: 0;
  }

  .is-railed .nav-group-children {
    display: none !important;
  }

  .is-railed .nav-group.is-open {
    background: transparent;
    padding-bottom: 0;
  }

  .is-railed .sidebar .nav-link,
  .is-railed .nav-group-toggle,
  .is-railed .user-trigger {
    justify-content: center;
    padding-left: 0;
    padding-right: 0;
  }

  /* A group whose page is open is still the current section; with its
     children hidden, the toggle is where that has to show. */
  .is-railed .nav-group:has(.nav-group-children a.active) > .nav-group-toggle {
    background: var(--md-sys-color-nav-highlight);
  }

  /* The user menu opens upward from a 68px bar; it needs the room of the
     full sidebar to be readable. */
  .is-railed .user-dropdown {
    right: auto;
    width: var(--sidebar-w);
  }
}

/* ---- icons ---------------------------------------------------------------
 * The symbols in _icon_sprite.html carry no colour, so these few lines are
 * what makes the whole set look like a set. Changing the weight of every
 * original icon in the app is `stroke-width` here; the design system's own
 * icons carry their weights per path (see the sprite's header).
 *
 * `1em` and `currentColor`: an icon is a glyph in the text beside it, so it
 * takes that text's size and colour without either being restated per use. */

.icon {
  width: 1em;
  height: 1em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex-shrink: 0;
  /* Optical centring: a cap-height glyph sits slightly above the baseline that
     a 1em box would put it on. */
  vertical-align: -0.125em;
}

/* The sprite itself is markup, not a picture: it must be in the DOM for <use>
   to resolve and must never take up space or be read out. `display: none`
   would stop the symbols resolving in some engines, hence the clip. */
.icon-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* ---- mobile nav: topbar, toggle, scrim -----------------------------------
 * All three are display:none until the breakpoint at the foot of this file
 * turns them on. They are declared here, with the shell they belong to, rather
 * than inside that block. */

.topbar {
  display: none;
  align-items: center;
  gap: var(--md-space-3);
  padding: var(--md-space-2) var(--md-space-3);
  background: var(--md-sys-color-surface);
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
}

.topbar .brand {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.nav-toggle {
  flex: 0 0 auto;
  width: 2.5rem;
  height: 2.5rem;
  display: grid;
  place-items: center;
  padding: 0;
  font-size: 1.15rem; /* the icon is 1em */
  background: var(--md-sys-color-surface-container-lowest);
  border: 1px solid var(--md-sys-color-field-outline);
  border-radius: var(--md-sys-shape-corner-md);
  color: var(--md-sys-color-on-surface);
  cursor: pointer;
}

.scrim {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 30;
  background: color-mix(
    in srgb,
    var(--md-sys-color-inverse-surface) 45%,
    transparent
  );
}

/* The page sheet: the design's rounded panel over the photograph, and the
   pane that scrolls. 30px clear of the top and the right edge as drawn; the
   gap to the sidebar shrinks with the window, because the mock's 63px is a
   1920px screen's luxury and a laptop wants the width for its tables. Its
   foot runs off the bottom of the window, square, so the content scrolls
   under the edge rather than ending in a rounded lip. */
.app-main {
  flex: 1;
  min-width: 0;
  height: calc(100% - var(--md-space-7));
  margin: var(--md-space-7) var(--md-space-7) 0 clamp(var(--md-space-4), 3.3vw, 63px);
  overflow-y: auto;
  background: var(--md-sys-color-surface);
  border-radius: var(--md-sys-shape-corner-page) var(--md-sys-shape-corner-page)
    0 0;
  /* The containing block for anything absolutely positioned in the page, so
     the pane's overflow clips it. Without it such an element - the
     visually-hidden <select> of every autocomplete picker, .visually-hidden -
     is placed against the document instead, sits at its spot in the scrolled
     content, and makes the *document* taller than the window. The body's
     overflow:hidden then hides the scrollbar but not the scroll: a window
     left scrolled down (by the narrow layout, which scrolls the document, or
     by focus) stays scrolled down with no way back up (#416). */
  position: relative;

  /* The content gutter, as variables because .page-header below has to cancel
     it exactly - it bleeds to the pane edges with negative margins and then
     re-applies the same padding. Two literals that must stay equal is a pair
     that eventually will not, and the mobile override only has to change these
     two rather than six values. 34px across, as the mock sets its content in
     from the sheet's edge. */
  --main-pad-y: var(--md-space-8);
  --main-pad-x: 2.125rem;
}

main {
  padding: var(--main-pad-y) var(--main-pad-x);
}

/* Pinned page header: stays put while the content below scrolls. The sheet's
   own colour, so what scrolls under it disappears rather than showing
   through. */
.page-header {
  position: sticky;
  top: calc(-1 * var(--main-pad-y));
  z-index: 5;
  background: var(--md-sys-color-surface);
  margin: calc(-1 * var(--main-pad-y)) calc(-1 * var(--main-pad-x))
    var(--md-space-5);
  padding: var(--main-pad-y) var(--main-pad-x) var(--md-space-2);
}

.page-header h1 {
  margin: var(--md-space-1) 0 0;
}

/* The way back up: "‹ Catalog" above a record's title. The design runs it
   along the top of the sheet with a rule under it the full width of the
   sheet - so inside a page header it bleeds to the edges the header itself
   bleeds to, and the rule is the header's top edge. The arrow is the text's
   own (&larr; in 70 templates), so nothing here adds one. */
.back-link {
  margin: 0 0 var(--md-space-4);
  font-size: var(--md-sys-typescale-body-small-size);
  font-weight: var(--md-sys-typescale-label-weight);
}

.back-link a {
  color: var(--md-sys-color-on-surface);
  text-decoration: none;
}

.back-link a:hover {
  text-decoration: underline;
}

.page-header:has(> .back-link) {
  padding-top: var(--md-space-2);
}

.page-header .back-link {
  margin: 0 calc(-1 * var(--main-pad-x)) var(--md-space-5);
  padding: var(--md-space-2) var(--main-pad-x);
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
}

/* A title with its actions: `<h1 class="row-between">Title <span>buttons
   </span></h1>` is how ~90 pages already write it, and the design puts the
   buttons on the title's line, right-aligned, 10px apart. The words of a
   button inside an h1 must not take the heading's face. */
h1.row-between,
.page-title {
  flex-wrap: wrap;
  row-gap: var(--md-space-3);
}

h1.row-between > span,
.page-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  font-family: var(--md-sys-typescale-font);
  font-size: var(--md-sys-typescale-body-size);
  font-weight: 400;
  letter-spacing: var(--md-sys-typescale-body-tracking);
}

/* The action row under a record's title rather than beside it - the
   customer page draws Edit and its five neighbours on their own line, left
   to right from Edit, because six buttons beside a long name leave the name
   no room. Wraps on a narrow screen instead of running off it. */
.page-actions--start {
  justify-content: flex-start;
  margin-top: var(--md-space-4);
}

/* The line under a record's title saying what it is - "Aloe & Lime
   Deodorant Spray 100ml" under its SKU. p1 Medium in the body ink. */
.page-subtitle {
  margin: var(--md-space-2) 0 var(--md-space-4);
  font-size: var(--md-sys-typescale-body-large-size);
  font-weight: 500;
}

/* A row's own link before its Edit - "Prices  Edit". */
.row-action {
  margin-right: var(--md-space-2);
}

/* A heading that opens a new part of a long page, clear of the card above. */
.section-heading {
  margin-top: var(--md-space-8);
}

/* A sentence under the title saying what the page is asking: the card note's
   type, a little under the title. */
.page-header > .card-note {
  margin: var(--md-space-2) 0 0;
}

/* Prose kept to a readable line length, wherever it sits. */
.measure {
  max-width: 60ch;
}

/* A notice in the page header - "packed, but must not leave yet" - sits under
   the title rather than flush against it. */
.page-header > .card {
  margin: var(--md-space-3) 0 0;
}

/* ---- tabs ----------------------------------------------------------------
 * The design's segmented pills: a sunken track the width of the page, the
 * current tab raised on it as a white field. The current one also takes a
 * heavier weight and aria-selected / aria-current from tabs.js and the
 * templates, so it is not marked by its fill alone. */

.tab-bar {
  display: flex;
  gap: 2px;
  margin-top: var(--md-space-4);
  padding: 9px 10px;
  background: var(--md-sys-color-surface-container);
  border-radius: var(--md-sys-shape-corner-panel);
  overflow-x: auto;
}

.tab-bar button,
.tab-bar a {
  display: inline-flex;
  align-items: center;
  gap: var(--md-space-2);
  min-height: 1.75rem;
  padding: 0 10px;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--md-sys-shape-corner-sm);
  font-family: inherit;
  font-size: var(--md-sys-typescale-body-small-size);
  font-weight: var(--md-sys-typescale-label-weight);
  color: var(--md-sys-color-on-surface);
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
}

.tab-bar button:hover,
.tab-bar a:hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-surface) var(--md-sys-state-hover-opacity),
    transparent
  );
}

.tab-bar button.active,
.tab-bar a.active {
  background: var(--md-sys-color-surface-container-lowest);
  border-color: var(--md-sys-color-field-outline);
  font-weight: 600;
}

/* ---- surfaces ------------------------------------------------------------
 * A card is the design's tile: 24px corners, the tile fill, and the soft
 * shadow. One that holds a table is the secondary tile instead - the beige
 * well the design sets its lists in, where each row is a white strip of its
 * own (see tables, below). It needs no shadow: it is cut into the page, not
 * lifted off it. */

.card {
  background: var(--md-sys-color-surface-container-low);
  border: 0;
  border-radius: var(--md-sys-shape-corner-tile);
  box-shadow: var(--md-sys-elevation-1);
  padding: var(--md-space-6) var(--md-space-7);
  margin-bottom: var(--md-space-4);
}

.card:has(> table, > form > table),
.card--well {
  background: var(--md-sys-color-surface-container);
  box-shadow: none;
}

/* A card around a table too wide for a laptop - the portal preview's twelve
   columns - scrolls sideways inside its own corners. Only on cards that hold
   no picker: overflow on an ancestor clips an autocomplete's dropdown. */
.card--scroll {
  overflow-x: auto;
}

/* `--error` is what 18 templates hand-roll as style="border-color:var(--danger)",
   reaching for a legacy alias besides. `--warn` is the "nothing is broken, but
   look at this" tone the status chips already have. Both are a 2px edge as
   well as a colour, so the difference survives a reader who cannot see it. */

.card--error {
  border: 2px solid var(--md-sys-color-error);
}

.card--warn {
  border: 2px solid var(--md-sys-color-warning);
  background: var(--md-sys-color-hint-container);
}

/* A card that is one paragraph - a warning - is the card's padding around the
   words, not the paragraph's margins on top of it. */
.card > p:only-child {
  margin: 0;
}

/* ---- the four things a card is made of ------------------------------------
 * Extracted from 148 inline `style` attributes that were each re-typing one of
 * these four. That is what an un-named component does: it gets re-typed
 * slightly differently every time, which is why the card title had four
 * spellings and the field label two.
 *
 * They also unblock a real CSP: `style-src 'unsafe-inline'` is most of the way
 * to having no policy at all, and the portal (#47) is public traffic. See #45.
 *
 * Sizes come from the typescale rather than the literals the templates used
 * (0.95rem, 0.72rem), so titles and labels land on the same few steps as
 * everything else. The field label is typographically identical to a `th`,
 * which is what it always was semantically. */

.card-title {
  margin-top: 0;
  margin-bottom: var(--md-space-4);
  font-family: var(--md-sys-typescale-display-font);
  font-size: var(--md-sys-typescale-title-size);
  font-weight: var(--md-sys-typescale-title-weight);
  color: var(--md-sys-color-heading);
  /* Flex even with nothing beside the text: a title with a badge or a pill
     after it was five of the six spellings, and a lone text node in a flex
     container renders exactly as it does in a block. One rule, no modifier.

     `wrap` is not optional here, and the dashboard is where that shows: without
     it, "Stock over time" next to "(last 90 days, Main Warehouse)" in a 340px
     card squeezes the *title* into two lines rather than moving the span to the
     next row - flex shrinks its items before it wraps them. Caught by looking at
     the page, not by reading the rule. */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2);
}

/* The words beside a title - "(FY 2026/27, net of credits)" - are the body
   face, not the heading's. */
.card-title > .muted,
.card-title > small {
  font-family: var(--md-sys-typescale-font);
  font-size: var(--md-sys-typescale-body-size);
  font-weight: 400;
  letter-spacing: var(--md-sys-typescale-body-tracking);
}

/* The buttons at the far end of a card's title - "Edit packaging", "Edit" -
   or a figure that sits there ("372 unit on hand"). Pushed right by the auto
   margin, and in the body face like the words above. Replaces the
   `style="margin-left:auto; display:flex; gap:0.4rem;"` span the product
   pages carried on every card. */
.card-title > .card-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2);
  margin-left: auto;
  font-family: var(--md-sys-typescale-font);
  font-size: var(--md-sys-typescale-body-size);
  font-weight: 400;
  letter-spacing: var(--md-sys-typescale-body-tracking);
}

/* A paragraph that opens or closes a card - a summary line, a warning, the
   sentence under a table - takes its air from the card's padding, not from
   its own margins as well. */
.card > p:first-child {
  margin-top: 0;
}

.card > p:last-child {
  margin-bottom: 0;
}

/* A card whose title is its own disclosure. `.card-title` sets `display: flex`,
   which takes a <summary> off `list-item` and so removes the marker the browser
   would have drawn - without one the row reads as a heading rather than as
   something to click. Same ::before triangle as `.loc-group summary`, and the
   bottom margin an <h3> would have had from the browser. */
summary.card-title {
  cursor: pointer;
  margin-bottom: 0;
}

details[open] > summary.card-title {
  margin-bottom: var(--md-space-4);
}

summary.card-title::-webkit-details-marker {
  display: none;
}

summary.card-title::before {
  content: "\25B8";
  color: var(--md-sys-color-on-surface-variant);
  transition: transform 120ms ease;
}

details[open] > summary.card-title::before {
  transform: rotate(90deg);
}

@media (prefers-reduced-motion: reduce) {
  summary.card-title::before {
    transition: none;
  }
}

/* The sentence under a card title saying what the card is for. The fourth thing
   a card is made of, and the one that was still being hand-rolled:
   `class="muted" style="margin:0 0 0.9rem; font-size:0.85rem;"`, in that exact
   spelling, in a few dozen templates. Same note as above `.card-title` applies -
   an un-named component gets re-typed slightly differently each time. */
.card-note {
  margin: 0 0 var(--md-space-4);
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

/* Last thing in the card rather than under the title - the footnote form, for a
   caveat that only makes sense once you have seen the fields. */
.card-note--foot {
  margin: var(--md-space-4) 0 0;
}

/* A note that is the last thing in its card - an empty state, a footnote -
   has nothing under it to keep away from. */
.card-note:last-child {
}

/* A second footnote follows the first closely - they are one afterword. */
.card-note--foot + .card-note {
  margin-top: var(--md-space-2);
}

/* A card's padding is its margin: the paragraph or list a notice card is made
   of does not add its own above and below. Replaces `style="margin:0"` on the
   one <p> of a warning card, in a few dozen places. */
.card > :first-child {
  margin-top: 0;
}

.card > :last-child {
  margin-bottom: 0;
}

/* The reasons in a notice: a short bulleted list, indented by its bullets
   only. */
.reason-list {
  margin: 0;
  padding-left: 1.1rem;
}

/* A card title with actions at the far end - "Lines" and its "Group by
   format / Sort by SKU". The row carries the title's bottom margin. */
.card-title-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--md-space-2);
  margin-bottom: var(--md-space-4);
}

.card-title-row > .card-title {
  margin-bottom: 0;
}

/* A banner card's headline and the sentence under it - "You don't have access
   to any location yet." then what to do about it. The note sits close under
   its bold line rather than a title's distance away; ~40 templates hand-roll
   this as `style="margin:0.4rem 0 0; font-size:0.85rem;"`. */
.card > strong + .card-note {
  margin: var(--md-space-1) 0 0;
}

/* The design's field label: 12px semibold in the accent, above its value.
   No longer uppercased - the design sets labels in sentence case, and the
   letter-spacing token that opened the capitals out is zero now. */
.field-label {
  display: block;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  letter-spacing: var(--md-sys-typescale-label-small-tracking);
  color: var(--md-sys-color-primary);
}

/* The detail grid every record page opens with: as many 150px columns as fit,
   each a label over its value in p1 - the design's "Type / Recovered" pairs. */
.detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--md-space-6) var(--md-space-4);
  font-size: var(--md-sys-typescale-body-large-size);
}

.detail-grid .field-label {
  margin-bottom: var(--md-space-1);
}

/* A long value - an email address with no space in it - breaks inside its
   own column rather than running over the next one. */
.detail-grid > * {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* A qualifier under a value: the group discounts under the blanket one. */
.detail-grid small {
  display: block;
  font-size: var(--md-sys-typescale-body-small-size);
}

/* A chip button under the value it acts on ("Set prices"), on its own line. */
.detail-grid .btn-xs {
  display: flex;
  width: fit-content;
  margin-top: var(--md-space-1);
}

.row-between {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--md-space-4);
}

/* The bar above a grid that acts on a selection - the sales order list's bulk
   invoice print (#60). It lives inside the htmx-swapped partial along with the
   table, so the ticks and the count of them come and go together. It is the
   design's instruction banner ("Tick orders to print..."), so it wears it -
   see `.hint-banner` below for the component. `flex-wrap` because the
   sentence explaining that is longer than a button. */
.bulk-bar {
  flex-wrap: wrap;
  margin-bottom: var(--md-space-3);
  padding: 10px 17px 10px var(--md-space-5);
  background: var(--md-sys-color-hint-container);
  border: 1px solid var(--md-sys-color-hint-outline);
  border-radius: var(--md-sys-shape-corner-panel);
  color: var(--md-sys-color-on-hint-container);
}

.bulk-bar p,
.bulk-bar .muted {
  margin: 0;
  font-size: var(--md-sys-typescale-body-size);
  font-weight: 500;
  color: var(--md-sys-color-on-hint-container);
}

/* Cards side by side, for a form with more sections than one column of them can
 * carry without becoming a scroll. Two things are chosen because these hold
 * fields:
 *
 *   - The row gap is zero. `.card` already carries `margin-bottom`, and a gap
 *     on top of it puts twice as much air between two stacked rows of cards as
 *     between two stacked cards, which reads as a break in the page.
 *   - `align-items: start`, against the grid default. Stretched to a common
 *     height, the card with two fields in it gets a hand's width of empty
 *     surface under them, which reads as a section somebody forgot to finish -
 *     worse than the ragged bottom edge it was buying. Cards end where their
 *     content does.
 */
.card-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--card-col, 19rem), 1fr));
  gap: 0 var(--md-space-4);
  align-items: start;
}

/* Two cards, the first one half again as wide: what is filled in every time
   beside what is usually left alone. Explicit tracks rather than `auto-fit`,
   because the ratio is the whole point and auto-fit cannot express one. It only
   applies from 60rem - the same floor `.workbench` uses - since below that the
   narrow card is too narrow to hold a select, and the base rule above is
   already a single column at that width. */
.card-row--lead {
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 60rem) {
  .card-row--lead {
    grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
  }
}

/* ---- the dashboard's grid (#37, #70, #662) --------------------------------
 * One grid for the whole page, never a band of cards followed by a second
 * grid: two independent grids cannot be locked together in CSS, and their
 * column edges coincided under half the time (#70). Every component of the
 * person's layout - a built-in card or a pinned report element - is a child
 * of this one.
 *
 * Twelve columns and fixed-height rows. A layout entry says how many of each
 * a component spans (`dash-w-*` in twelfths: 3, 4, 6, 8 or 12; `dash-h-*`
 * in rows: 1 to 4; see dashboard/layout.py), so a card is sized to what it
 * holds rather than to its neighbour - the point of #37, where a single
 * number got a chart's 340px of empty space. The person picks the size now,
 * never below what the card says it reads at (`min_w`, `min_h`).
 *
 * `row dense` packing: components go in the layout's order, but a small one
 * may move up into a gap a wide one left at the end of a row. Free placement
 * is out of scope (#658); this is what "they pack in order" buys.
 *
 * `minmax(0, 1fr)` rather than `1fr`, and `min-width: 0` on the cards: a
 * track's minimum is otherwise its content's, and one long table cell would
 * widen its column and push the whole grid sideways off the page.
 *
 * A card is a flex column - its head, then a body taking the rest - and
 * clips at its row height. The body scrolls rather than stretching the row:
 * a table of `h * 3 - 1` rows fits the height it was cut for, and anything
 * longer (a wrapped note, a bigger font) stays inside its own card.
 *
 * A row is two half-rows of 48px: a section heading (`.dash-h-half`) takes
 * one, a card two per row of its height - 2 x 48 + 16 is the 112px a row has
 * always been, so no card's size changed when headings came in. */

.dash-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  grid-auto-rows: 48px;
  grid-auto-flow: row dense;
  gap: var(--md-space-4);
}

.dash-card {
  display: flex;
  flex-direction: column;
  gap: var(--md-space-3);
  min-width: 0;
  margin: 0;
  padding: var(--md-space-4) var(--md-space-5);
  overflow: hidden;
}

.dash-w-3 { grid-column: span 3; }
.dash-w-4 { grid-column: span 4; }
.dash-w-6 { grid-column: span 6; }
.dash-w-8 { grid-column: span 8; }
.dash-w-12 { grid-column: span 12; }

.dash-h-half { grid-row: span 1; }
.dash-h-1 { grid-row: span 2; }
.dash-h-2 { grid-row: span 4; }
.dash-h-3 { grid-row: span 6; }
.dash-h-4 { grid-row: span 8; }

/* A section heading - the design's "Trends & performance": h2 type on the
   page itself, no tile, sitting on the bottom of its half-row so it reads
   as the title of what follows. Always a row of its own, so the dense
   packing can never set it beside a card or another heading. */
.dash-heading {
  grid-column: 1 / -1;
  display: flex;
  align-items: flex-end;
  min-width: 0;
  margin: 0;
  padding: 0 0 2px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

h2.dash-heading {
  display: block;
  align-self: end;
}

/* In the editor it is a component like a card, all on one line: the
   handle, its words in a field, its width and move controls, Take off. */
.dash-heading.dash-card {
  flex-direction: row;
  align-items: center;
  gap: var(--md-space-2);
  padding: var(--md-space-2) var(--md-space-4);
}

.dash-heading .dash-heading__field {
  flex: 1 1 10rem;
  min-width: 0;
  font-family: var(--md-sys-typescale-display-font);
  font-size: var(--md-sys-typescale-headline-small-size);
  font-weight: var(--md-sys-typescale-headline-weight);
  color: var(--md-sys-color-heading);
}

.dash-heading.dash-card .dash-edit__foot {
  padding-top: 0;
  border-top: 0;
}

/* The design's tile heading: the title in Inter Bold 16 and the ink, then
   what it covers in brackets, regular 14, on the same line - "Sales (1 Apr
   2026 to 31 Mar 2027, completed orders)". One line, cut with an ellipsis
   rather than wrapped: a wrapped title takes a row a table had counted on.
   The subtitle gives way first. The badge and any action sit to the right
   and never shrink. A top-five's title is its section pill (`.section-pill`
   inside it). */
.dash-card__head {
  display: flex;
  align-items: flex-start;
  gap: var(--md-space-2);
  min-width: 0;
  flex: none;
}

.dash-card__titles {
  display: flex;
  align-items: baseline;
  gap: var(--md-space-2);
  flex: 1 1 auto;
  min-width: 0;
}

.dash-card__title,
.dash-card__sub {
  margin: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dash-card__title {
  flex: 0 1 auto;
  min-width: 0;
  font-family: var(--md-sys-typescale-font);
  font-size: var(--md-sys-typescale-stat-label-size);
  font-weight: 700;
  line-height: 24px;
  letter-spacing: -0.02em;
  word-spacing: normal;
  color: var(--md-sys-color-on-surface);
}

.dash-card__title a {
  color: inherit;
}

/* Gives way long before the title does: a narrow card keeps its name and
   loses the brackets. */
.dash-card__sub {
  flex: 0 1000 auto;
  min-width: 0;
  font-size: var(--md-sys-typescale-body-size);
  line-height: 24px;
  letter-spacing: var(--md-sys-typescale-body-tracking);
  color: var(--md-sys-color-on-surface);
}

/* The brackets on a built-in card, whose subtitle is always there. A pinned
   tile's may be empty, and "()" would say something went missing. */
.dash-card:not(.pin-tile) .dash-card__sub::before {
  content: "(";
}

.dash-card:not(.pin-tile) .dash-card__sub::after {
  content: ")";
}

/* One row has room for a title and the figures, not a subtitle as well.
   Not on a phone, where rows are as tall as what is in them. */
@media (min-width: 761px) {
  .dash-h-1 .dash-card__sub {
    display: none;
  }
}

.dash-card__action {
  flex: none;
}

.dash-card__body {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
}

.dash-card__body > p:last-child {
  margin-bottom: 0;
}

/* A chart fills what its card leaves it, so a taller card is a taller chart.
   Flex sizes the box and Plotly measures it as it draws; `responsive` (in
   charts.js) re-measures on a window resize. Basis zero and no minimum, or
   the plot's own drawn height would hold the box open. */
.dash-card .chart {
  flex: 1 1 0;
  min-height: 0;
  height: auto;
}

/* A table on a card is cut to the rows its height holds (`h * 3 - 1`), so a
   row is one line: a cell too long for its column ends in an ellipsis rather
   than wrapping into a second line the count did not allow for. Fixed layout
   is what lets a cell be cut at all; the first column, usually the name of
   the thing, gets the most room. */
.dash-card table {
  table-layout: fixed;
}

.dash-card th,
.dash-card td {
  padding: var(--md-space-1) var(--md-space-2) var(--md-space-1) 0;
  font-size: var(--md-sys-typescale-body-small-size);
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dash-card th:first-child {
  width: 40%;
}

/* The period toggle on the Sales card: two links, see its partial. */
.dash-period {
  display: flex;
  flex-wrap: wrap;
  gap: var(--md-space-2);
  margin: 0 0 var(--md-space-3);
}

/* The same toggle set as a table's filter, outside the dashboard - the product
   page's Allocations tab (#604). Links, so the choice is in the URL. */
.filter-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--md-space-2);
  margin: 0 0 var(--md-space-3);
}

/* ---- the top-five cards: a picture in front of each name -----------------
 * The design's "Top 5 products" and "Top 5 customers": a product's
 * thumbnail or a customer's avatar, the name in Inter Medium 14, a hairline
 * under each row (the generic card table). The code follows the name,
 * quieter; the figures the list is ranked on keep their columns. The rows'
 * height is what `ranking_rows` in dashboard/cards.py counts on. */
.dash-rank td {
  padding-block: 6px;
  font-size: var(--md-sys-typescale-body-size);
  vertical-align: middle;
}

.dash-rank th:first-child {
  width: 58%;
}

.dash-rank__who {
  display: flex;
  align-items: center;
  gap: var(--md-space-3);
  min-width: 0;
}

.dash-rank__name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  font-weight: 500;
}

.dash-rank__code {
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: 400;
  color: var(--md-sys-color-on-surface-variant);
}

.dash-rank__thumb {
  flex: none;
  width: 40px;
  height: 40px;
  object-fit: contain;
  border-radius: var(--md-sys-shape-corner-sm);
  background: var(--md-sys-color-paper);
}

/* No picture on record: the Inventory icon on the field fill, so the names
   still line up. */
.dash-rank__thumb--none {
  display: inline-grid;
  place-items: center;
  background: var(--md-sys-color-surface-container);
  color: var(--md-sys-color-on-surface-variant);
  font-size: 1.25rem;
}

/* ---- the stat tile on the grid -------------------------------------------
 * "At a glance" is the design's green stat tile (`.stat-tile--finance`). On
 * the grid it is sized by its row like any card, so the tile's own margin
 * and minimum go; its padding stays the tile's. In the editor its controls
 * sit on the gradient, so they take its dark ink. */
.dash-card.stat-tile {
  margin: 0;
  min-height: 0;
  padding: var(--md-space-6);
}

.dash-card.stat-tile--finance :is(.dash-card__title, .dash-card__sub, .dash-edit__handle, .dash-edit__remove, .dash-edit__value) {
  color: var(--md-sys-color-on-highlight);
}

.dash-card.stat-tile--finance .dash-edit__foot {
  border-top-color: var(--md-sys-color-on-highlight);
}

/* The "Got ideas?" banner under the grid, at the width the design gives it:
   two thirds of the page. */
.dash-ideas {
  max-width: 61rem;
  margin-top: var(--md-space-8);
}

/* A caveat under the figures it qualifies. */
.dash-note {
  margin: var(--md-space-3) 0 0;
}

.dash-card .kpi-strip {
  gap: var(--md-space-2) var(--md-space-6);
}

/* The figures of a one-row card are its whole body; the zero hint is a third
   line it has no room for, and "0" beside its label says it already. */
@media (min-width: 761px) {
  .dash-h-1 .kpi-hint {
    display: none;
  }
}

.dash-card .kpi {
  gap: 0;
}

/* Who a scripted card's figures come from (#662): the person's own report,
   a report shipped with the app, or a colleague's, in that order of tone. */
.dash-badge {
  flex: none;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: 600;
  padding: 2px var(--md-space-2);
  border-radius: var(--md-sys-shape-corner-full);
  white-space: nowrap;
}

.dash-badge--mine {
  color: var(--md-sys-color-mine);
  background: var(--md-sys-color-mine-container);
}

.dash-badge--script {
  color: var(--md-sys-color-script);
  background: var(--md-sys-color-script-container);
}

.dash-badge--other {
  color: var(--md-sys-color-on-surface-variant);
  background: var(--md-sys-color-surface-variant);
}

/* A pinned element is rendered by reports/elements as a card of its own;
   inside a tile it is the tile's body, not a second box. It is a flex column
   too, so a plot in it fills the tile the way a built-in chart does. */
.pin-tile .dash-card__body > .card {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  margin: 0;
  padding: 0;
  border: 0;
  box-shadow: none;
  background: transparent;
}

/* Its own title repeats the report's heading above it often enough; kept,
   but quieter. */
.pin-tile .dash-card__body > .card > .card-title {
  font-size: var(--md-sys-typescale-body-size);
  margin-bottom: var(--md-space-2);
}

/* The sources' caveats, one quiet line until opened; opened, they scroll
   inside the tile rather than push the figure out of it. */
.pin-tile .pin-notes {
  flex: 0 0 auto;
  margin-bottom: var(--md-space-2);
  font-size: var(--md-sys-typescale-label-size);
  color: var(--md-sys-color-on-surface-variant);
}
.pin-tile .pin-notes > summary {
  cursor: pointer;
}
.pin-tile .pin-notes[open] {
  max-height: 50%;
  overflow-y: auto;
}

/* Said under the grid, not in it: there is no place to say it in. */
.pin-hidden {
  margin-top: var(--md-space-4);
}

/* ---- the dashboard's head and its editor (#664) ---------------------------
 * The page's title row: "Edit dashboard" in view mode; in the editor a pill
 * saying so, and Reset, Cancel and Save. dashboard/js/editor.js does the
 * editing; this is how it looks, after the mockup's edit mode with the app's
 * square corners.
 *
 * `[hidden]` is restated wherever a rule here sets `display`: an author
 * `display` beats the browser's own `[hidden] { display: none }`, and this
 * stylesheet has no reset for it (see `.composer__send`). */

.dash-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--md-space-3);
  flex-wrap: wrap;
  margin: var(--md-space-1) 0 var(--md-space-5);
}

.dash-head__title {
  display: flex;
  align-items: baseline;
  gap: var(--md-space-3);
  flex-wrap: wrap;
}

.dash-head__title h1 {
  margin: 0;
}

/* "Editing your layout": the Dashboard's own highlight, as a section pill. */
.dash-head__pill {
  padding: 1px var(--md-space-3) 3px;
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--section);
  color: var(--md-sys-color-on-highlight);
  font-family: var(--md-sys-typescale-font);
  font-size: var(--md-sys-typescale-body-size);
  font-weight: 700;
  word-spacing: normal;
}

.dash-head__note {
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

.dash-head__actions {
  display: flex;
  gap: var(--md-space-2);
  flex-wrap: wrap;
}

.dash-editor-error {
  margin: 0 0 var(--md-space-4);
  padding: 10px 14px;
  border: 1px solid var(--md-sys-color-error);
  border-radius: var(--md-sys-shape-corner-panel);
  background: var(--md-sys-color-error-container);
  color: var(--md-sys-color-on-error-container);
  font-size: var(--md-sys-typescale-body-size);
}

/* The grid and the Components panel share a row, the grid taking nearly all
   of it; under about 960px the panel wraps below the grid. */
.dash-editor {
  display: flex;
  flex-wrap: wrap;
  gap: var(--md-space-5);
  align-items: flex-start;
}

.dash-editor > .dash-grid {
  flex: 999 1 640px;
  min-width: 0;
}

/* Rows a footer taller while editing: the controls take about 48px of every
   card, and at 112px a one-row card would be nothing but its controls, its
   figures scrolled out of sight. The sizes stay in rows and twelfths, which
   is what is saved; only the row is taller. Not on a phone, where rows are
   as tall as what is in them already. */
@media (min-width: 761px) {
  .dash-grid.is-editing {
    /* Two half-rows of 72px: 160px a row, as before. */
    grid-auto-rows: 72px;
  }
}

/* A component being edited: dashed, and its controls on it. */
.dash-grid.is-editing > .dash-card {
  border: 1.5px dashed var(--md-sys-color-primary);
}

.dash-grid.is-editing > .dash-card.is-dragging {
  opacity: 0.45;
}

.dash-grid.is-editing > .is-drop-target {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

.dash-edit__handle {
  flex: none;
  padding-top: 2px;
  color: var(--md-sys-color-on-surface-variant);
  cursor: grab;
}

.dash-edit__remove {
  flex: none;
  margin: -2px -6px 0 0;
  color: var(--md-sys-color-on-surface-variant);
}

.dash-edit__foot {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  padding-top: var(--md-space-2);
  border-top: 1px dashed var(--md-sys-color-outline-variant);
  font-size: 0.75rem;
}

.dash-edit__group {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}

.dash-edit__move {
  margin-left: auto;
}

.dash-edit__step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-sm);
  background: var(--md-sys-color-surface-container-lowest);
  color: var(--md-sys-color-on-surface);
  line-height: 1;
}

.dash-edit__step:not([aria-disabled="true"]):hover {
  background: var(--md-sys-color-surface-container-highest);
}

/* Still focusable, so focus never drops out of a card at the end of a
   range; pressing it says why nothing moved. */
.dash-edit__step[aria-disabled="true"] {
  opacity: 0.38;
  cursor: not-allowed;
}

.dash-edit__value {
  min-width: 54px;
  text-align: center;
  color: var(--md-sys-color-on-surface-variant);
  font-variant-numeric: tabular-nums;
}

.dash-edit__value[data-edit-h] {
  min-width: 36px;
}

.dash-drop {
  grid-column: span 12;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 96px;
  padding: var(--md-space-3);
  border: 2px dashed var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-tile);
  color: var(--md-sys-color-on-surface-variant);
  font-size: var(--md-sys-typescale-body-size);
  text-align: center;
}

.dash-grid.is-editing > .dash-drop.is-drop-target {
  outline: none;
  border-color: var(--md-sys-color-primary);
}

/* The Components panel. Beside the grid it stays in view while the page
   scrolls, so Add is always at hand; below it, it simply follows. */
.dash-library {
  flex: 1 1 300px;
  max-width: 100%;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: var(--md-space-3);
  padding: var(--md-space-5);
  background: var(--md-sys-color-surface-container-low);
  border-radius: var(--md-sys-shape-corner-tile);
  box-shadow: var(--md-sys-elevation-1);
}

@media (min-width: 1100px) {
  .dash-library {
    position: sticky;
    top: var(--md-space-4);
    max-height: calc(100vh - 2 * var(--md-space-4));
    overflow: auto;
  }
}

.dash-library__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--md-space-2);
}

.dash-library__head h2 {
  margin: 0;
  font-size: var(--md-sys-typescale-title-size);
}

.dash-library__count,
.dash-library__note,
.dash-library__meta,
.dash-library__unrun {
  font-size: var(--md-sys-typescale-label-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

/* Its label as a field's: 12px semibold, the accent. */
.dash-library__search {
  display: flex;
  flex-direction: column;
  gap: var(--md-space-1);
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  color: var(--md-sys-color-primary);
}

/* The filters are a .btn-tonal set: the chosen one is mint. */
.dash-library__filters {
  display: flex;
  gap: var(--md-space-2);
  flex-wrap: wrap;
}

.dash-library__filters button,
.dash-library__toggle {
  flex: none;
}

.dash-library__groups {
  display: flex;
  flex-direction: column;
  gap: var(--md-space-3);
}

.dash-library__group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.dash-library__group[hidden] {
  display: none;
}

.dash-library__title {
  margin: var(--md-space-1) 0 0;
  font-family: var(--md-sys-typescale-font);
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  word-spacing: normal;
  color: var(--md-sys-color-primary);
}

.dash-library__note,
.dash-library__unrun {
  margin: 0;
}

.dash-library__items {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Each component a rounded strip on the well, as a list row. */
.dash-library__item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: var(--md-space-2) 10px;
  border-radius: var(--md-sys-shape-corner-md);
  background: var(--md-sys-color-surface-container);
}

.dash-library__words {
  flex: 1 1 auto;
  min-width: 0;
}

.dash-library__name,
.dash-library__meta {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dash-library__name {
  font-size: var(--md-sys-typescale-body-small-size);
  font-weight: 600;
}

/* Add is the primary button as it is; On is the neutral one (.btn-ghost's
   look), and takes the component off. */
.dash-library__toggle.is-on,
.dash-library__toggle.is-on:not(:disabled):hover {
  background: transparent;
  border-color: var(--md-sys-color-outline);
  color: var(--md-sys-color-on-surface);
}

.dash-library__empty {
  margin: 0;
}

.dash-library__new-form {
  margin: 0;
}

.dash-library__new {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--md-space-2);
  width: 100%;
  min-height: 40px;
  border: 1px dashed var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-md);
  background: transparent;
  color: var(--md-sys-color-primary);
  font-size: var(--md-sys-typescale-body-small-size);
  font-weight: 600;
}

.dash-library__new:not(:disabled):hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-primary) var(--md-sys-state-hover-opacity),
    transparent
  );
}

/* A phone gets one column, as wide as the screen: twelfths of 390px are
   too narrow for anything. Rows size to their content again, with a floor so
   a chart still has room, and a chart takes the fixed height it has
   everywhere else in the app rather than filling a card that now has none. */
@media (max-width: 760px) {
  .dash-grid {
    grid-template-columns: minmax(0, 1fr);
    grid-auto-rows: auto;
  }

  .dash-grid > .dash-card {
    grid-column: auto;
    grid-row: auto;
    min-height: 180px;
  }

  .dash-grid > .dash-drop,
  .dash-grid > .dash-heading {
    grid-column: auto;
  }

  .dash-grid > .dash-heading {
    grid-row: auto;
    min-height: 0;
  }

  .dash-heading.dash-card {
    flex-wrap: wrap;
  }

  /* A top five fits the phone rather than scrolling sideways as other
     tables do there: its names give way, its figures stay in view. */
  .dash-card.dash-rank table {
    display: table;
    width: 100%;
  }

  .dash-rank th:first-child {
    width: 46%;
  }

  .dash-card .chart {
    flex: none;
    height: 260px;
  }
}

/* The KPI figures' own layout - a flex row, not a grid of small cards (see
   above). Also retires the last hand-rolled `font-size:1.9rem;
   font-weight:700` inline style on the dashboard - the pattern the note above
   `.card-title` describes, and one less thing between here and a real CSP
   (#45). */

.kpi-strip {
  display: flex;
  flex-wrap: wrap;
  /* Room enough to read as separate figures rather than a sentence. Wraps
     rather than squeezing, so a narrow window stacks them. */
  gap: var(--md-space-5) var(--md-space-8);
}

/* Each figure is still its own link - the number is what people aim at. */
.kpi {
  display: flex;
  flex-direction: column;
  gap: var(--md-space-1);
  color: inherit;
  text-decoration: none;
  /* No border of its own: the bar is the card. A hover needs somewhere to
     land, so the label takes the accent instead. */
  min-width: 8rem;
}

.kpi:hover .kpi-label,
.kpi:hover .kpi-value {
  color: var(--md-sys-color-primary);
}

/* The design's Stat (Inter Bold 32) over its Label (Inter Bold 16), with a
   12px note under both when there is one. */
.kpi-value {
  font-size: var(--md-sys-typescale-stat-size);
  font-weight: var(--md-sys-typescale-stat-weight);
  line-height: 1;
  letter-spacing: 0.002em;
}

.kpi-label {
  font-size: var(--md-sys-typescale-stat-label-size);
  font-weight: 700;
  line-height: 1.2;
  color: var(--md-sys-color-on-surface);
}

/* Only rendered at zero, where the number alone reads as a failure to load
   rather than as good news. */
.kpi-hint {
  font-size: var(--md-sys-typescale-label-small-size);
}

/* The strip is also the glance on a record page (#58), where a figure switches
   tab instead of navigating - so it has to survive being a <button>. A link
   with href="#" would do the same job while telling the browser, the keyboard
   and a screen reader that it goes somewhere, which it does not. The button
   rules below are filled and padded, so they are undone here rather than
   reordered: `.kpi` is a figure whatever element carries it. */
button.kpi {
  padding: 0;
  border: 0;
  background: none;
  text-align: left;
  align-items: flex-start;
  justify-content: flex-start;
  min-height: 0;
  font-family: inherit;
}

/* ---- images and attached files (#58) --------------------------------------
 * The product page's own gallery, moved onto Overview and out of the inline
 * styles it used to carry - one less thing between here and a real CSP (#45).
 * 140px tracks: four to a row on a laptop, and a square crop so a portrait
 * bottle and a landscape carton line up. */

.image-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--md-space-3);
  margin-bottom: var(--md-space-4);
}

.image-tile,
.image-tile > a {
  position: relative;
  display: block;
}

.image-tile img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border: 1px solid var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-lg);
}

/* The default image says so on the image, not in a caption under it. */
.image-tile .badge {
  position: absolute;
  top: var(--md-space-2);
  left: var(--md-space-2);
}

/* Reorder / default / remove, under the tile they act on rather than in a table
 * beside it - reordering is a question about the picture, so the buttons belong
 * where the picture is. One <form> per button (each is its own POST), hence the
 * flex row: they would otherwise stack. */
.image-tile-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--md-space-1);
  margin-top: var(--md-space-2);
}

.image-tile-actions button {
  padding: 0 var(--md-space-2);
  font-size: var(--md-sys-typescale-label-small-size);
  min-height: 1.75rem;
}

.image-upload {
  margin-top: var(--md-space-3);
  align-items: end;
}

/* The image picker doubles as a drop target (#437). A dashed outline says so
 * before anything is dragged; it fills in while files are over it, so the
 * user can see the drop will land here and not open the file in the tab. */
.dropzone {
  padding: var(--md-space-3);
  border: 2px dashed var(--line);
  border-radius: var(--md-sys-shape-corner-sm);
  transition: border-color 120ms ease, background-color 120ms ease;
}

.dropzone.is-dragover {
  border-color: var(--accent);
  background: var(--accent-dim);
}

.dropzone-hint {
  margin: var(--md-space-1) 0 0;
  font-size: var(--md-sys-typescale-label-small-size);
}

/* dropzone.js's refusal of a batch too large to send (#559). Full width, so
   it reads under the row of fields in a Files card. */
.dropzone-error {
  flex-basis: 100%;
  margin: var(--md-space-1) 0 0;
  color: var(--danger);
}

/* ---- the product gallery: reorder and the Add tile ------------------------
 * What gallery.js needs on top of the design system's `.thumb` tiles and
 * `.hint-banner` (docs/design-system.md): the arrows row under a tile for a
 * browser without JavaScript, what a tile being dragged looks like, and the
 * focus ring on the Add tile's picker. Placement only.
 *
 * `[hidden]` is restated, for the reason given above `.filters`: `.thumb`,
 * `.hint-banner` and the tools' buttons set `display`, which beats the
 * browser's own `[hidden] { display: none }` - and the handles, the Add tile
 * and the hint start hidden until the script has run. Two classes, so it
 * outranks the component rules that come later in this file. */
.thumb-grid.image-gallery [hidden],
.hint-banner[data-gallery-hint][hidden] {
  display: none;
}

.image-gallery .image-tile-actions {
  display: flex;
  gap: var(--md-space-1);
  margin-top: var(--md-space-2);
}

.image-gallery .image-tile.is-dragging {
  opacity: 0.4;
}

.image-gallery.is-enhanced [draggable="true"] {
  cursor: grab;
}

/* The picker inside the label is visually hidden, so the label carries the
   focus ring the input would have drawn. */
.image-add-button:focus-within {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

/* ---- delivery photographs (#327) -------------------------------------------
 * The goods receipt pages reuse .image-grid for the tiles themselves. What is
 * here is the two things a photograph off the warehouse floor needs and a
 * product shot does not: a caption saying who took it and when, and a
 * thumbnail small enough to sit in a list row. */

.photo-caption {
  margin: var(--md-space-1) 0 0;
  font-size: var(--md-sys-typescale-label-small-size);
  line-height: 1.3;
}

/* The list's own cell: one thumbnail and the count beside it, as a single
 * link into the delivery. 2.5rem so the row keeps the height of every other
 * row on the page - a list of deliveries is scanned, and a taller row on the
 * photographed ones would make "has photos" the thing the eye follows. */
.photo-cell {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  text-decoration: none;
}

.photo-thumb {
  width: 2.5rem;
  height: 2.5rem;
  object-fit: cover;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-sm);
}

.file-list {
  margin: 0;
  padding-left: var(--md-space-5);
}

.file-list .badge {
  margin-left: var(--md-space-2);
}

/* ---- inventory and catalog pages ------------------------------------------
 * What the Flows "Product" frame (63:963) draws that no generic component
 * covers, and the few shapes the other inventory pages kept re-typing as
 * inline styles. Everything else on those pages is the generic components. */

/* A record's title with its chips on the same line - "CRYS100AL-C  Finished
   good  Batch tracked  Sellable". The chips keep the body face (see the
   heading rules); the gap is the mock's, a little wider before the first chip
   than between them. */
.title-chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2) var(--md-space-4);
}

.title-chips > .chips {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}

/* Overview's first row: the gradient stat tile beside the catalog facts, the
   two the same height as the mock draws them. Stacks when there is not room
   for both side by side; a product with no figures to show gets the facts
   card the full width. */
.product-glance {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(26rem, 100%), 1fr));
  gap: 0 var(--md-space-4);
  align-items: stretch;
}

.product-glance > .stat-tile {
  display: flex;
  align-items: center;
}

/* The tile's figures are buttons that open the tab holding the detail, so a
   keyboard user needs to see which one has focus on the gradient as well. */
.product-glance .kpi:focus-visible {
  outline-color: var(--md-sys-color-on-highlight);
}

/* "Images & attachments": the mock sets this card's title one step up (h2),
   and closes the card with a ruled-off "Add images & attachments" section. */
.product-media > .card-title {
  font-size: var(--md-sys-typescale-headline-small-size);
}

.product-media__add {
  margin: var(--md-space-6) calc(-1 * var(--md-space-7)) 0;
  padding: var(--md-space-4) var(--md-space-7) 0;
  border-top: 1px solid var(--md-sys-color-outline-variant);
}

.product-media__add > h3 {
  margin: 0 0 var(--md-space-3);
}

.product-media__forms {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
  gap: var(--md-space-4) var(--md-space-8);
}

/* One upload: the picker over its button, as the mock stacks "Choose File /
   No file chosen" over "Upload". */
.upload-form {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--md-space-2);
}

.upload-form .field-label {
  margin-bottom: 0;
}

/* The picker below the gallery stays a drop target, but the mock draws it as
   a plain picker - the dashed square belongs to the gallery's Add image tile.
   It outlines only while files are over it. */
.upload-form.dropzone {
  margin: calc(-1 * var(--md-space-3));
  border-color: transparent;
}

.upload-form.dropzone.is-dragover {
  border-color: var(--md-sys-color-primary);
}

/* A field that would otherwise stretch the width of the page - a short
   select on its own row. */
.field-narrow {
  max-width: 22rem;
}

/* A labelled group of tickboxes inside a .form-grid - "Used for", "Handling",
   "Certifications". A fieldset so the label names the group for a screen
   reader; none of the browser's frame. */
.flag-group {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.flag-group > legend {
  padding: 0 0 0 3px;
  margin-bottom: 3px;
}

.flag-group > .form-flags {
  margin-top: 0;
}

.form-flags--nowrap label {
  white-space: nowrap;
}

/* `.form-grid input` stretches every control to its column; a tickbox is not
   a field to stretch. */
.form-flags input[type="checkbox"] {
  width: auto;
}

/* A lone tickbox in a .form-grid row, sat level with the fields beside it
   rather than with their labels: pushed down by a label's height. */
.form-grid > .form-flags {
  align-self: start;
  align-items: center;
  margin-top: 0;
  padding-top: 1.6rem;
}

/* The column floor of a .form-grid, for the forms that want narrower (the
   product form's many short fields) or wider (a single location picker). */
.form-grid--narrow {
  --form-col: 180px;
}

.form-grid--wide {
  --form-col: 260px;
}

/* The picker and Apply at the end of the catalog's bulk bar. */
.bulk-bar__actions {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
}

/* A wide table that scrolls inside its card rather than widening the page -
   the packaging components' eight columns. */
.table-scroll {
  overflow-x: auto;
}

/* A component's recycling instruction, on its own row under the component. */
td.sub-row {
  padding-top: 0;
  font-size: var(--md-sys-typescale-label-small-size);
}

/* The tick and thumbnail columns of a list. */
th.col-tick {
  width: 2rem;
}

th.col-thumb {
  width: 2.4rem;
}

/* A yes/no column: a tick or a muted cross, centred under its heading. */
.flag-col {
  text-align: center;
}

.flag-col .flag-on {
  color: var(--md-sys-color-primary);
}

/* The line formsets (adjustment, transfer): fixed columns so a picker
   opening does not reflow the table under the pointer. */
.lines-fixed {
  table-layout: fixed;
}

.lines-fixed .col-product {
  width: 38%;
}

.lines-fixed .col-qty {
  width: 16%;
}

.lines-fixed .col-batch {
  width: 36%;
}

.lines-fixed .col-remove {
  width: 10%;
}

/* "+ Add line" under the rows it adds to. */
.add-row {
  margin-top: var(--md-space-4);
}

/* Units by month on the product's Stats tab: a bar per row, drawn by a
   <progress> so the width is the value and no style attribute carries it. */
.trend-cell {
  width: 45%;
}

.trend-bar {
  display: block;
  width: 100%;
  height: 0.5rem;
  border: 0;
  border-radius: 3px;
  background: var(--md-sys-color-surface-container);
  color: var(--md-sys-color-primary);
  appearance: none;
  overflow: hidden;
}

.trend-bar::-webkit-progress-bar {
  background: var(--md-sys-color-surface-container);
  border-radius: 3px;
}

.trend-bar::-webkit-progress-value {
  background: var(--md-sys-color-primary);
  border-radius: 3px;
}

.trend-bar::-moz-progress-bar {
  background: var(--md-sys-color-primary);
  border-radius: 3px;
}

/* A warning card listing several findings: one line each, close together. */
.note-lines > p {
  margin: var(--md-space-1) 0 0;
}

/* A block followed by more of the same card - the packaging figures over the
   component table. */
.section-gap {
  margin-bottom: var(--md-space-4);
}

/* Two tables in one card (pack levels, then pallets). */
.card > table + table {
  margin-top: var(--md-space-4);
}

/* A loss: the error tone, and the minus sign in front of it says the same
   thing to someone who cannot see the colour. */
.figure-negative {
  color: var(--md-sys-color-error);
}

/* A figure leading a detail-grid cell - "41.988 g" - over its quiet unit. */
.figure-lg {
  font-size: var(--md-sys-typescale-title-size);
  font-weight: 700;
}

/* A quiet second line under a value or a cell. */
.sub-line {
  display: block;
  font-size: var(--md-sys-typescale-label-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

/* The findings under a product on the weight data report. */
.findings {
  margin: 0;
  padding-left: var(--md-space-5);
}

/* The location tree's add-sublocation form, inline in the node's row. */
.inline-form {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2);
}

.btn.btn-sm,
button.btn-sm {
  min-height: 1.75rem;
  padding: 0 var(--md-space-2);
  font-size: var(--md-sys-typescale-label-small-size);
}

/* ---- buttons -------------------------------------------------------------
 * The design's two button families, on the classes 700 templates already
 * use:
 *
 *   button, .btn     Primary - the mint action, dark ink on it in both
 *                    themes. Filled is the default because a bare <button>
 *                    is filled in 85 templates.
 *   .btn-ghost       Neutral - an outline and no fill: Channels, Price
 *                    lists, every secondary action in a title row.
 *   .btn-tonal       Ghost - a field-coloured fill: the unselected half of a
 *                    toggle pair such as "This financial year / Rolling 12
 *                    Months", where the selected half is the primary.
 *   .btn-lg          The big 42px primary - "Share ideas", a form's submit
 *                    when it is the only thing on the page to press.
 *   .icon-btn        A square, icon only (see the shell above); the button
 *                    carries the accessible name.
 *
 * 32px tall, 6px corners, 14px Inter Medium, an icon at 14px with 5px to its
 * label - the component library's Secondary Icon Button. */

button,
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  /* 32px from padding and line height rather than a min-height: the dozens of
     rules that turn a <button> into something else (a disclosure, a figure,
     a close cross) reset its padding, and a min-height they never heard of
     would keep them 32px tall. */
  padding: 6px var(--md-space-3);
  border: 1px solid var(--md-sys-color-action-outline);
  border-radius: var(--md-sys-shape-corner-sm);
  background: var(--md-sys-color-action);
  color: var(--md-sys-color-on-action);
  /* Named rather than inherited: a button in a title row would otherwise
     take the heading's condensed face. */
  font-family: var(--md-sys-typescale-font);
  word-spacing: normal;
  font-size: var(--md-sys-typescale-label-size);
  font-weight: var(--md-sys-typescale-label-weight);
  line-height: 18px;
  letter-spacing: 0;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  vertical-align: middle;
  transition: background-color var(--md-sys-motion-duration-short)
    var(--md-sys-motion-easing-standard);
}

button .icon,
.btn .icon {
  width: 14px;
  height: 14px;
}

/* Only an enabled button answers the pointer, so a disabled one keeps its own
   fill whatever its variant. Inside :where() so this weighs no more than the
   bare `button:hover` it replaces: every variant's own hover (.btn-ghost,
   .btn-tonal, .icon-btn, the shell's toggles) still wins over it. */
:where(button, .btn):where(:not(:disabled, [aria-disabled="true"])):hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-action) var(--md-sys-state-hover-opacity),
    var(--md-sys-color-action)
  );
}

/* The design dims a button that cannot be pressed yet (Print, before anything
   is ticked) rather than greying it, so it keeps its place in the row. The
   cursor and the attribute say the rest. */
button:disabled,
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
}

.btn-ghost {
  background: transparent;
  color: var(--md-sys-color-on-surface);
  border-color: var(--md-sys-color-outline);
}

.btn-ghost:not(:disabled):hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-surface) var(--md-sys-state-hover-opacity),
    transparent
  );
}

.btn-tonal {
  background: var(--md-sys-color-surface-container-lowest);
  color: var(--md-sys-color-on-surface);
  border-color: var(--md-sys-color-field-outline);
}

.btn-tonal:not(:disabled):hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-surface) var(--md-sys-state-hover-opacity),
    var(--md-sys-color-surface-container-lowest)
  );
}

/* One of a set that is currently chosen - the selected period, the current
   filter preset. Primary-filled, with aria-pressed or aria-current saying so;
   for a toggle pair written as two .btn-tonal. */
.btn-tonal[aria-pressed="true"],
.btn-tonal[aria-current],
.btn-tonal[aria-pressed="true"]:not(:disabled):hover,
.btn-tonal[aria-current]:not(:disabled):hover {
  background: var(--md-sys-color-action);
  border-color: var(--md-sys-color-action-outline);
  color: var(--md-sys-color-on-action);
}

.btn-lg {
  padding: 11px 21px;
  border-radius: var(--md-sys-shape-corner-md);
  font-size: var(--md-sys-typescale-body-large-size);
}

/* The design's chip-sized button: "Set prices" under the value it acts on
   in a detail grid. 20px tall, 11px words, 4px corners - a button that sits
   in a line of text without making the line taller than its neighbours.
   Primary by default; combine with .btn-ghost for a neutral one. */
.btn-xs {
  padding: 1px var(--md-space-2);
  border-radius: var(--md-sys-shape-corner-xs);
  font-size: var(--md-sys-typescale-caption-size);
  line-height: 16px;
}

/* The row that closes a full-page form: submit, plus a Cancel that leaves
   without saving. Cancel is an <a> to the page you came from, never a
   <button> - a button inside a <form> submits it, which is what the sales
   order form's type="cancel" was silently doing. */
.form-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* Submit straight under a row of tick boxes or a note gets the air a field's
   own margin would have given it. */
:is(.form-flags, .card-note--foot, .choice-group) + .form-actions {
  margin-top: var(--md-space-4);
}

/* The focus ring: the accent, 2px, clear of the control. On every element
   that takes focus from the keyboard, and only then - a mouse click on a
   button does not draw it. */
:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}

/* ---- form controls -------------------------------------------------------
 * Styled by element, not by class, so a template that renders {{ form.sku }}
 * directly looks right without being touched. That is what lets the ~68
 * templates migrate one at a time instead of all at once. */

input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="date"],
input[type="datetime-local"],
input[type="search"],
input[type="url"],
input[type="tel"],
select,
textarea {
  /* The design's Filter Input: 32px, 8px corners, a field-white fill with a
     light edge, 14px Inter Medium. 14px of air on the left, 10 on the right
     where a select's chevron or a date's calendar sits. */
  min-height: 2rem;
  padding: 5px 10px 5px 14px;
  border: 1px solid var(--md-sys-color-field-outline);
  border-radius: var(--md-sys-shape-corner-md);
  font: inherit;
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: -0.005em;
  background-color: var(--md-sys-color-surface-container-lowest);
  color: var(--md-sys-color-on-surface);
  min-width: 9rem;
  /* A native <select> sizes itself to its widest option, so one customer named
     "Holland & Barrett Retail Limited" makes the field wider than a phone and
     takes the whole document with it. min-width is not what does that - the
     content is - so this is the cap that stops it. */
  max-width: 100%;
}

textarea {
  min-height: 4.5rem;
  resize: vertical;
  width: 100%;
}

/* A select draws the design's chevron rather than the platform's arrow: two
   gradients, each a thin diagonal stroke in the text colour, meeting at the
   bottom - a "\" and a "/". It follows the theme and needs no image, where an
   SVG here would be a colour no token reaches. A multiple select is a list
   box, not a dropdown, and keeps its own look. */
select:not([multiple]):not([size]) {
  appearance: none;
  padding-right: 2.25rem;
  background-image: linear-gradient(
      45deg,
      transparent 40%,
      currentColor 40%,
      currentColor 60%,
      transparent 60%
    ),
    linear-gradient(
      -45deg,
      transparent 40%,
      currentColor 40%,
      currentColor 60%,
      transparent 60%
    );
  background-position: calc(100% - 17px) 55%, calc(100% - 12px) 55%;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}

/* The native date field keeps its own calendar button - it is the picker -
   and the scheme in force (tokens.css) is what keeps that button legible at
   night. */
input[type="date"],
input[type="datetime-local"] {
  min-width: 10rem;
}

input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 1px;
  border-color: var(--md-sys-color-primary);
}

/* Errors must not rely on colour (WCAG 1.4.1): an operator who cannot see red
   saw neither the red border nor the red text. The words are in the markup
   (common/_field_errors.html, forms/errors_ul.html); this only weights them,
   and makes an invalid control's border change shape as well as colour. */
input[aria-invalid="true"],
select[aria-invalid="true"],
textarea[aria-invalid="true"] {
  border-color: var(--md-sys-color-error);
  border-width: 2px;
  border-style: dashed;
}

.field-errors__prefix {
  font-weight: 600;
}

.error-summary__title {
  margin: 0 0 var(--md-space-2);
}

input[type="checkbox"],
input[type="radio"] {
  accent-color: var(--md-sys-color-primary);
  min-width: 0;
}

/* Field structure emitted by the project form renderer (see
   stockOfTheEarth/templates/forms/). Hand-written templates that build their
   own label/control markup keep working through the element rules above. */
.md-field {
  margin-bottom: var(--md-space-4);
}

.md-field > label,
.filters label {
  display: block;
  padding-left: 3px;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  letter-spacing: var(--md-sys-typescale-label-small-tracking);
  color: var(--md-sys-color-primary);
  margin-bottom: 3px;
}

/* A choice field the renderer wraps in a fieldset (radio buttons, ticked
   choices): its legend is the field's label, and none of the browser's
   frame round it. */
.md-field > fieldset {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.md-field > fieldset > legend {
  padding: 0 0 0 3px;
  margin-bottom: 3px;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  letter-spacing: var(--md-sys-typescale-label-small-tracking);
  color: var(--md-sys-color-primary);
}

.md-field input,
.md-field select,
.md-field textarea {
  width: 100%;
}

.md-field input[type="checkbox"],
.md-field input[type="radio"] {
  width: auto;
}

/* Also under a hand-written field in a .form-grid, which is how the older
   form pages lay theirs out. */
.md-field .helptext,
.form-grid .helptext {
  display: block;
  margin: var(--md-space-1) 0 0;
  font-size: var(--md-sys-typescale-label-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

/* .field-errors is the legacy hand-written class, still in 75 places; both
   render identically so a half-migrated page stays coherent. */
.md-field .errorlist,
.field-errors {
  margin: var(--md-space-1) 0 0;
  padding: 0;
  list-style: none;
  color: var(--md-sys-color-error);
  font-size: var(--md-sys-typescale-body-small-size);
}

/* A table row the form rejected. Where a page tells you to look at the
   highlighted rows, this is the highlight: a .field-errors note under one cell
   is not findable in a table that runs to hundreds of rows.

   On the cells rather than the row, because a <td> with its own background
   paints over one set on the <tr>. */
.row--error > td {
  /* The error tone itself, faint, rather than its container: the containers
     are pastels in both themes now, and a pastel wash under the light words
     of the dark theme would cost them their contrast. */
  background: color-mix(in srgb, var(--md-sys-color-error) 14%, transparent);
}

/* Why a formset row was rejected, spanning the table under the row it is
   about - common/_row_errors.html. The rule that normally separates the two
   goes, so the pair reads as one rejected line rather than as a stray row of
   red text that could belong to either neighbour. */
.row--error:has(+ .row--error-note) > td {
  border-bottom: 0;
}

.row--error-note > td {
  padding-top: 0;
}

/* A filter bar: labelled controls in a row, bottom-aligned. */
/* Two classes, one layout, on purpose. `.filters` narrows a list, and filters.js
   collapses it behind a toggle that counts what is applied. `.controls` chooses
   what a page is about - a report's period and cut always have a value, so that
   count would read "3" on a page where nothing has been narrowed, and the
   controls would hide behind a toggle labelled Filters. Same row of fields,
   no script. */
.filters,
.controls {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: var(--md-space-3);
  margin-bottom: var(--md-space-4);
}

/* The animated collapse filters.js wraps this form in.
 *
 * Height in pixels, measured and set by that script. Two tidier-looking
 * approaches were tried against a real browser and neither survived:
 *
 *   - the `hidden` attribute cannot animate at all, and worse, `.filters {
 *     display: flex }` above beats the browser's own `[hidden] { display: none
 *     }`, so it did not even hide the form - it sat fully visible with
 *     aria-expanded="false", collapsed for a screen reader and open to
 *     everyone else;
 *   - `grid-template-rows: 1fr -> 0fr`, the usual "animate to auto height"
 *     trick, runs as a transition but does not interpolate in pixels here: the
 *     used height went 77.5px -> 17px -> done in two frames, because in an
 *     auto-height grid every positive `fr` resolves to the same content size
 *     and only exactly `0fr` differs. A step function, not an animation.
 *
 * The margin belongs to the shell rather than the form for a related reason: a
 * margin sits outside the content box, so on the form it survived the collapse
 * and left the row propped open at 16px. Padding does the same. Only a box that
 * is entirely inside the animated height can close to nothing.
 *
 * visibility, not just height: a zero-height box still holds focusable fields,
 * so tabbing would walk into a form nobody can see. It is delayed out and
 * instant in, so the fields are reachable as soon as it starts opening and only
 * leave the tab order once it has finished closing. */
.filters-collapse {
  overflow: hidden;
  height: 0;
  visibility: hidden;
}

.filters-collapse.is-animated {
  transition:
    height var(--md-sys-motion-duration-medium)
      var(--md-sys-motion-easing-emphasized),
    visibility 0s linear var(--md-sys-motion-duration-medium);
}

.filters-collapse.is-open {
  visibility: visible;
}

.filters-collapse.is-animated.is-open {
  transition:
    height var(--md-sys-motion-duration-medium)
      var(--md-sys-motion-easing-emphasized),
    visibility 0s;
}

.filters-collapse > .filters {
  margin-bottom: 0;
}

/* The collapse toggle filters.js puts in front of every .filters form. Six
   filters on the movements page is most of a phone screen before any data. */
/* The design's Filters panel, which filters.js builds around every list
   page's filter form: a sunken panel, its head row - the toggle and Reset
   filter - and the fields under it. The panel's padding is on the head and
   the form rather than on the panel, for the reason given above: only a box
   entirely inside the animated height can close to nothing. */
.filters-panel {
  margin-bottom: var(--md-space-4);
  background: var(--md-sys-color-surface-container);
  border-radius: var(--md-sys-shape-corner-panel);
}

.filters-panel__head {
  display: flex;
  align-items: center;
  gap: var(--md-space-5);
  padding: var(--md-space-3) var(--md-space-4);
}

.filters-panel .filters {
  padding: 0 var(--md-space-4) var(--md-space-4);
}

/* "Filters" and its chevron: Label Desktop - Inter Bold - in the accent, at
   the panel's head. Six filters on the movements page is most of a phone
   screen before any data, which is why it folds. */
.filters-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--md-space-2);
  min-height: 1.75rem;
  padding: 0 var(--md-space-1);
  background: none;
  border: 0;
  color: var(--md-sys-color-primary);
  font-size: 1.125rem;
  font-weight: 700;
  letter-spacing: -0.005em;
}

.filters-toggle:hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-surface) var(--md-sys-state-hover-opacity),
    transparent
  );
}

/* Points down closed and up open, like the nav groups' chevron. */
.filters-toggle .chev {
  width: 14px;
  height: 14px;
  transition: transform var(--md-sys-motion-duration-short)
    var(--md-sys-motion-easing-standard);
}

.filters-toggle[aria-expanded="true"] .chev {
  transform: rotate(180deg);
}

/* How many filters are actually applied. Without it, a collapsed form hides the
   reason the list looks short. */
.filters-toggle .count {
  padding: 0 var(--md-space-2);
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  font-size: var(--md-sys-typescale-label-small-size);
}

/* Reset filter: the error tone, small, with the design's circular arrows -
   it throws away what you narrowed, which is worth a second look before the
   click, but it is not destructive enough for a button. */
.filters-reset {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--md-sys-typescale-label-small-size);
  color: var(--md-sys-color-error);
  text-decoration: none;
}

.filters-reset:hover {
  text-decoration: underline;
}

/* The header block of a form page. Replaces six near-identical *-form-grid
   classes (po-, so-, prod-, loc-, tf-, asm-, gr-) that differed only in the
   column floor - which is now the --form-col property. */
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--form-col, 220px), 1fr));
  gap: var(--md-space-4);
}

.form-grid > .span-all {
  grid-column: 1 / -1;
}

/* Column floors as classes, for the grids that hold shorter or longer fields
   than the 220px default - the address row's seven short boxes, the notes'
   two long ones. They set the same --form-col property a page could set
   itself, without a style attribute in the markup. */
.form-grid--col-xs {
  --form-col: 180px;
}

.form-grid--col-s {
  --form-col: 12rem;
}

.form-grid--col-m {
  --form-col: 13rem;
}

.form-grid--col-l {
  --form-col: 16rem;
}

.form-grid--col-xl {
  --form-col: 20rem;
}

/* `.md-field` carries its own bottom margin, so a grid row gap on top of it
   puts twice as much air between two rows of fields as there is between two
   stacked fields outside a grid. Zero it for the renderer's fields only: the
   hand-written `<div>` children the older form pages put in this grid have no
   margin of their own and still need the gap to separate their rows. */
.form-grid > .md-field {
  margin-bottom: 0;
}

/* `auto-fill` instead of `auto-fit`: keep the empty columns rather than letting
   the fields that are there share the whole width out between them. For the
   short rows - a product group and a percentage - where `auto-fit` gives two
   half-page-wide boxes to hold "5.00". The floor becomes the width, not the
   minimum. */
.form-grid--fill {
  grid-template-columns: repeat(auto-fill, minmax(var(--form-col, 220px), 1fr));
}

/* The column floors the sales forms were setting with `style="--form-col:..."`:
   the six boxes of a postal address typed in place, and a file picker. */

.form-grid--address {
  --form-col: 12rem;
}

.form-grid--file {
  --form-col: 280px;
}

.form-grid label {
  display: block;
  padding-left: 3px;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  letter-spacing: var(--md-sys-typescale-label-small-tracking);
  color: var(--md-sys-color-primary);
  margin-bottom: 3px;
}

.form-grid input,
.form-grid select,
.form-grid textarea {
  width: 100%;
}

/* A row of checkboxes under a grid: box first, label beside it in body case -
   the micro-caps of a field label read as a heading and are wrong next to a
   tickbox. */
.form-flags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--md-space-4);
  margin-top: var(--md-space-2);
}

.form-flags label {
  display: flex;
  align-items: center;
  gap: var(--md-space-1);
  text-transform: none;
  letter-spacing: normal;
  font-size: var(--md-sys-typescale-body-size);
  font-weight: 400;
  color: var(--md-sys-color-on-surface);
  margin: 0;
}

/* A row of choices above a grid - the shared address's "who is this change
   for" - is kept apart from the grid's first labels. */
.form-flags + .form-grid {
  margin-top: var(--md-space-4);
}

/* A set of tick boxes answering one question - "They may order in" over the
   pack sizes. A fieldset for the legend a screen reader announces first,
   with none of the browser's frame: the field label is the heading. */
.choice-group {
  margin: var(--md-space-4) 0 0;
  padding: 0;
  border: 0;
  min-width: 0;
}

.choice-group > legend {
  padding: 0 0 0 3px;
}

.choice-group > .form-flags {
  margin-top: var(--md-space-1);
}

/* A box or a dot keeps its own size inside a form grid: `.form-grid input`
   stretches every input to the column, which stood a radio alone in the middle
   of its row with its words wrapped beside it (the order form's "Bill to"). */
.form-grid input[type="radio"],
.form-grid input[type="checkbox"] {
  width: auto;
}

/* A row of choices that is a fieldset: no browser frame, and the legend is a
   field label like every other one in the grid. Inside a grid it is a cell
   like its neighbours, so it starts level with them rather than a row of
   flags' gap lower (the order form's "Bill to", Bugbot on #704). */
fieldset.form-flags {
  border: 0;
  padding: 0;
  margin-inline: 0;
}

.form-grid fieldset.form-flags {
  margin-top: 0;
}

fieldset.form-flags > legend,
.choice-list > legend {
  padding: 0 0 0 3px;
  margin-bottom: 3px;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  color: var(--md-sys-color-primary);
}

/* A stack of radios (or ticks) under a label - the print dialog's "Document"
   and "Consignee" - is the `.choice-list` below, as a fieldset: no browser
   frame. `__check` is a lone tick outside a list, `__field` a control in the
   stack that is not a choice (a select under its own label), `__nested` one
   that belongs to the choice above it and so is indented under its words,
   `__stack` a column of such controls. */
fieldset.choice-list {
  min-width: 0;
  padding: 0;
  border: 0;
  margin-inline: 0;
}

.choice-list__check {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  font-size: var(--md-sys-typescale-body-size);
  color: var(--md-sys-color-on-surface);
  margin-bottom: var(--md-space-4);
}

.choice-list__field {
  display: block;
  margin: var(--md-space-1) 0 var(--md-space-4);
}

.choice-list__nested {
  margin-left: 1.4rem;
}

.choice-list__stack {
  display: grid;
  gap: var(--md-space-2);
  align-self: stretch;
}

/* One repeating row of a formset card - an address, a contact, a discount.
   A card inside a card, closer to its neighbours than a standalone card is:
   the outer card's own padding is already the air on the other three sides,
   and the default margin on top of it made three addresses look like three
   sections. Replaces the `style="margin-bottom:0.8rem"` each of these rows
   was carrying. */
.card > .form-row-card {
  margin-bottom: var(--md-space-3);
}

/* Line-item formset rows. A class rather than the #line-rows id it replaces:
   htmx still targets the id, but a page may carry more than one of these and
   ids have to stay unique. */
.line-rows td {
  vertical-align: middle;
  padding-top: var(--md-space-2);
  padding-bottom: var(--md-space-2);
}

.line-rows input,
.line-rows select {
  width: 100%;
  height: 2.3rem;
}

.line-rows input[type="number"] {
  min-width: 7rem;
}

.line-rows input[type="checkbox"] {
  width: auto;
  height: auto;
}

/* The sales line's tax code, under the product it belongs to rather than in a
   column of its own - see common/_order_line_row.html for why. Laid out
   sideways because the picker is narrow and the label is one word. */
.line-tax {
  display: flex;
  gap: var(--md-space-2);
  align-items: center;
  margin-top: var(--md-space-1);
}

.line-tax label {
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  letter-spacing: var(--md-sys-typescale-label-small-tracking);
  color: var(--md-sys-color-primary);
}

/* ---- autocomplete pickers ------------------------------------------------
 * Every picker in the app is a django-autocomplete-light <autocomplete-select>
 * web component. This replaces the select2 skin that used to live here, and
 * dal_alight's own stylesheet, which base.html deliberately does not load: it
 * hard-codes a grey light/dark palette, rounded corners and a drop shadow, and
 * declares them at a specificity our light-dark() tokens would have to fight -
 * which is why the order-form pickers stayed white in dark mode.
 *
 * Structure the component renders:
 *   <autocomplete-select>
 *     <select slot="select">      <- the real field, visually hidden
 *     <span slot="deck">          <- the chosen value, as a chip
 *     <autocomplete-select-input> <- the text input you type in
 *   and, appended to <body> when open, .autocomplete-light-box
 */
autocomplete-select {
  display: block;
}

/* The real <select> carries the value that gets submitted, so it must stay in
   the accessibility tree - hidden the way a skip-link is, not display:none. */
autocomplete-select select[slot="select"] {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
}

autocomplete-select input {
  width: 100%;
}

/* The current value. Sits above the input, so a picker with something chosen
   is two rows tall - deliberate: it makes "already set" unmissable in a
   formset where the input itself always reads empty. */
autocomplete-select [slot="deck"] {
  display: block;
}

autocomplete-select [slot="deck"] [data-value] {
  display: inline-flex;
  align-items: center;
  gap: var(--md-space-1);
  margin-bottom: var(--md-space-1);
  padding: 0 var(--md-space-2);
  min-height: 1.8rem;
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  border-radius: var(--md-sys-shape-corner-xs);
  font-size: var(--md-sys-typescale-body-small-size);
  cursor: pointer;
}

autocomplete-select [slot="deck"] .clear {
  padding-left: var(--md-space-1);
  font-weight: 600;
  color: var(--md-sys-color-error);
}

autocomplete-select-input[loading] input {
  opacity: 0.6;
  cursor: wait;
}

.autocomplete-light-box {
  position: absolute;
  z-index: 1000;
  /* autocomplete.js caps max-height to the room left on screen; the list then
     scrolls inside itself rather than running off the page (#414). The 50vh
     is only the fallback for before the script has measured anything. */
  max-height: 50vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: var(--md-space-1) 0;
  background: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface);
  border: 1px solid var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-xs);
}

/* The component positions the box from getBoundingClientRect() plus
   window.scrollY, and appends it to <body>. In the app shell the body never
   scrolls (.app-main does), so scrollY is always 0 and those are viewport
   coordinates - fixed is what they actually mean. Absolute would have the
   body's own overflow:hidden clip any dropdown near the foot of a long form.
   The login and examples pages do scroll the body, and carry no pickers. */
body.app-shell > .autocomplete-light-box {
  position: fixed;
}

/* The shell is a flex row, so *anything* a script appends to <body> becomes a
   third flex item and squeezes the sidebar and the content sideways - which is
   exactly what the dropdown above does if it ever loses its positioning. The
   shell has two panes and a stack of overlays, so make "not in the flow"
   structural rather than something each overlay has to remember.

   .topbar is the one exception, and it has to be named here rather than opting
   out from its own rule: this selector is (0,2,1) and `.topbar` is (0,1,0), so
   a media query saying `position: sticky` loses to it no matter where it sits
   in the file. Left in, the mobile topbar shrink-wraps to its content and
   leaves the flow - which reads as a bar that neither spans the width nor
   pushes the page down, and is exactly how it first shipped. */
body.app-shell > :not(.sidebar, .app-main, .topbar) {
  position: fixed;
}

/* Wrapped rather than cut off with an ellipsis: the box is as wide as its
   field, and on a phone two locations differing only in their last segment
   ("UK/A1/B3/S1" and "UK/A1/B3/S2") read as the same option once truncated. */
.autocomplete-light-box [data-value] {
  padding: var(--md-space-2) var(--md-space-3);
  font-size: var(--md-sys-typescale-body-small-size);
  white-space: normal;
  overflow-wrap: anywhere;
  cursor: pointer;
}

/* The grant picker's field is otherwise as narrow as its placeholder, which
   is what a long location path then has to fit in. */
.location-access-picker {
  min-width: min(20rem, 100%);
  flex: 1 1 20rem;
}

/* A product row in the order-line picker (#470): thumbnail on the left, the
   SKU and name over the small print on the right. `contain` for the same
   reason .line-thumb uses it - these are pack shots of every shape, and
   cropping a bottle square hides the label that makes it recognisable. */
.picker-product {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
}

.picker-thumb {
  flex: none;
  width: 2.5rem;
  height: 2.5rem;
  object-fit: contain;
  background: var(--md-sys-color-surface-container-low);
}

.picker-product__text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.picker-product__text small {
  color: var(--md-sys-color-on-surface-variant);
  font-size: var(--md-sys-typescale-label-small-size);
}

.autocomplete-light-box .hilight {
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
}

.autocomplete-light-no-results,
.autocomplete-light-box [data-next-page] {
  padding: var(--md-space-2) var(--md-space-3);
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
  font-style: italic;
  cursor: default;
}

.autocomplete-light-box [data-next-page] {
  text-align: center;
  font-style: normal;
  cursor: pointer;
  border-top: 1px solid var(--md-sys-color-outline-variant);
}

/* ---- tables --------------------------------------------------------------
 * Dense by design: this app is mostly stock tables.
 *
 * The design draws a list two ways, and which one a table gets depends on
 * what it sits on, not on a class the template has to remember:
 *
 *   strips  each row its own white strip, 4px corners, 2px apart - the
 *           sales order list on the page, a customer's addresses in a
 *           sunken card. The default: on the page, and in a .card that
 *           holds a table (which turns that card into the well, above).
 *   rules   no fill, a hairline under each row - the dashboard's top five
 *           lists, inside a white tile, where white strips would vanish.
 *           Inside any other .card, and in a dialog.
 *
 * Two custom properties carry the choice, so every rule below is written
 * once. A row is styled through its cells, and only a row that has a <td>:
 * the header row is bare <th>s - most tables here have no <thead> - and a
 * key/value row's <th> takes the strip with its <td>.
 */

table {
  --row-fill: var(--md-sys-color-surface-container-low);
  --row-rule: transparent;
  border-collapse: separate;
  border-spacing: 0 2px;
  width: 100%;
}

.card table,
dialog table {
  --row-fill: transparent;
  --row-rule: var(--md-sys-color-outline-variant);
}

.card:has(> table, > form > table) table,
.card--well table {
  --row-fill: var(--md-sys-color-surface-container-low);
  --row-rule: transparent;
}

th,
td {
  text-align: left;
  padding: 10px 10px 10px 0;
  font-size: var(--md-sys-typescale-body-small-size);
  font-weight: 500;
  line-height: 18px;
  vertical-align: middle;
}

/* A column heading: the design's 12px semibold in the accent, sentence case,
   sitting just above the first strip. */
th {
  padding-top: var(--md-space-2);
  padding-bottom: var(--md-space-1);
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  letter-spacing: var(--md-sys-typescale-label-small-tracking);
  color: var(--md-sys-color-primary);
}

/* The first column lines its heading up with the text in the strips under
   it, which start 15px in. */
tr > :first-child {
  padding-left: 15px;
}

tr:has(> td) > * {
  background: var(--row-fill);
  border-bottom: 1px solid var(--row-rule);
}

tr:has(> td) > :first-child {
  border-radius: var(--md-sys-shape-corner-xs) 0 0 var(--md-sys-shape-corner-xs);
}

tr:has(> td) > :last-child {
  border-radius: 0 var(--md-sys-shape-corner-xs) var(--md-sys-shape-corner-xs) 0;
}

tr:has(> td) > :only-child {
  border-radius: var(--md-sys-shape-corner-xs);
}

/* A row heading - the label of a key/value table - keeps the body ink and
   weight of the strip it is in; only the column headings are accents. */
tr:has(> td) > th {
  color: var(--md-sys-color-on-surface);
  font-size: var(--md-sys-typescale-body-small-size);
}

/* Not `td.num`, which is what this was: 14 templates wrote
   `<th style="text-align:right">` above a `td.num` column because the element
   selector did not reach the header, so a right-aligned column had a
   left-aligned heading over it. Unscoped, one class does both ends. */
.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}


/* Two lists in one card - an order's invoices, then its credit notes - stand
   apart. */
table + table {
  margin-top: var(--md-space-4);
}

/* The figures under a document's lines: Subtotal, Tax, Total, at the right
   edge and only as wide as they need. Hairlines rather than strips even in a
   well, because they are one block of arithmetic, not three records; the
   total row is the answer and carries the weight. */
.totals {
  display: flex;
  justify-content: flex-end;
  margin-top: var(--md-space-4);
}

/* `.card .totals` too: a card holding the lines is a well, and its rule for
   the tables in it is heavier than a bare `.totals table`. */
.totals table,
.card .totals table {
  --row-fill: transparent;
  --row-rule: var(--md-sys-color-outline-variant);
  width: auto;
  min-width: 14rem;
}

.totals tr.totals__total > * {
  font-weight: 700;
}

.totals .totals__note {
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: 400;
  color: var(--md-sys-color-on-surface-variant);
}

/* The row highlight fades rather than snapping. On a 25-row table the eye is
   tracking across a line of figures, and an instant full-width colour change
   under a moving cursor reads as a flicker. */
td {
  transition: background var(--md-sys-motion-duration-short)
    var(--md-sys-motion-easing-standard);
}

tr:has(> td):hover > * {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-surface) 5%,
    var(--row-fill)
  );
}

/* A row that needs attention - the short lines on a part-available order. Weak
   on purpose: the badge in the row says what is wrong, and this only has to make
   the two rows out of twenty findable without reading. A left edge as well as a
   tint, so it survives being printed and does not rely on colour alone. The
   error tone itself, faint - its container is a pastel in both themes, and a
   pastel wash would take the dark theme's light words down with it. */
tr.row--attention > * {
  background: color-mix(in srgb, var(--md-sys-color-error) 10%, var(--row-fill));
}

tr.row--attention > :first-child {
  box-shadow: inset 3px 0 0 var(--md-sys-color-error);
}

/* Otherwise the class beats the hover rule on specificity and the row somebody
   is pointing at is the one row that does not respond. */
tr.row--attention:hover > * {
  background: color-mix(in srgb, var(--md-sys-color-error) 16%, var(--row-fill));
}

/* A quiet second line in a cell - the product's name under its SKU, why a
   product is hidden under the Hidden chip. Its own line, so the cell needs
   no <br>, and a step smaller than the row so the first line still leads. */
.cell-note {
  display: block;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: 400;
  color: var(--md-sys-color-on-surface-variant);
}

/* The actions at the end of a row - Edit, and a Remove that has to be a
   POST form. A flex row, so the form sits beside the link without being
   made inline, and the pair stays right-aligned under a .num heading. */
.row-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--md-space-3);
}

.row-actions form {
  margin: 0;
}

/* Product thumbnails in a lines table. `contain` because these are supplier
   pack shots of every shape, and cropping a bottle to a square hides the label
   that makes it recognisable - which is the whole point of the column. */
.line-thumb {
  display: block;
  width: 2.4rem;
  height: 2.4rem;
  object-fit: contain;
  background: var(--md-sys-color-paper);
  border-radius: var(--md-sys-shape-corner-xs);
}

/* ---- chips ---------------------------------------------------------------
 * The design's pills: 11px words on a pastel, fully rounded. A plain chip
 * (a product's "Sellable") is a field-white pill; the three that mean
 * something take their status container; a tenant's own tone is that tone
 * let down into paper. Every one of those fills is a pastel in both themes,
 * and every one carries the same dark ink. */

.badge {
  display: inline-flex;
  align-items: center;
  font-family: var(--md-sys-typescale-font);
  word-spacing: normal;
  font-size: var(--md-sys-typescale-caption-size);
  font-weight: 500;
  line-height: 16.5px;
  letter-spacing: 0.006em;
  padding: 1px var(--md-space-2);
  border-radius: var(--md-sys-shape-corner-full);
  /* A hairline the status and tone chips drop: a white chip on a white tile
     would otherwise be words floating with no chip at all. */
  border: 1px solid var(--md-sys-color-field-outline);
  background: var(--md-sys-color-surface-container-lowest);
  color: var(--md-sys-color-on-surface);
  white-space: nowrap;
  vertical-align: middle;
}

.badge.ok {
  font-weight: 600;
  border-color: transparent;
  color: var(--md-sys-color-on-success-container);
  background: var(--md-sys-color-success-container);
}

.badge.warn {
  font-weight: 600;
  border-color: transparent;
  color: var(--md-sys-color-on-warning-container);
  background: var(--md-sys-color-warning-container);
}

.badge.danger {
  font-weight: 600;
  border-color: transparent;
  color: var(--md-sys-color-on-error-container);
  background: var(--md-sys-color-error-container);
}

/* A shape ahead of the word on the three chips that mean something - a dot
   for ok, a triangle for a warning, a diamond for an error - so the meaning
   survives anyone who cannot tell the fills apart, and a black and white
   printout. Drawn with clip-path rather than a glyph so it takes the text
   colour and needs no font to have the character. */
.badge.ok::before,
.badge.warn::before,
.badge.danger::before {
  content: "";
  display: inline-block;
  width: 0.55em;
  height: 0.55em;
  margin-right: 0.35em;
  background: currentColor;
}

.badge.ok::before {
  border-radius: 50%;
}

.badge.warn::before {
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
}

.badge.danger::before {
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}

/* A pill in a tone the tenant picked for a status, a label or a
   certification - see common/tones.py. The tone let down into paper, the
   design's pastel, with the dark highlight ink on it: the same in both
   themes, as the design keeps its chips. test_tokens.py reads the share from
   this rule and measures every tone's pill in every mode. */
.badge.tone {
  color: var(--md-sys-color-on-highlight);
  background: color-mix(in srgb, var(--tone) 30%, var(--md-sys-color-paper));
  border-color: transparent;
  font-weight: 600;
}

.tone-grey { --tone: var(--pal-grey); }
.tone-blue { --tone: var(--pal-blue); }
.tone-teal { --tone: var(--pal-teal); }
.tone-green { --tone: var(--pal-green); }
.tone-amber { --tone: var(--pal-amber); }
.tone-orange { --tone: var(--pal-orange); }
.tone-red { --tone: var(--pal-red); }
.tone-purple { --tone: var(--pal-purple); }

/* The design's Tile Highlight: a card heading set in a pill of its section's
   colour - "Top 5 products" in Inventory's, "Top 5 customers" in Customers'.
   Label Desktop type (Inter Bold 16) in the highlight ink. Which section is
   the element's own (.section-<name>) or the page's (data-section). */
.section-pill {
  display: inline-flex;
  align-items: center;
  padding: 1px var(--md-space-3) 3px;
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--section);
  color: var(--md-sys-color-on-highlight);
  font-family: var(--md-sys-typescale-font);
  font-size: var(--md-sys-typescale-stat-label-size);
  font-weight: 700;
  line-height: 24px;
  letter-spacing: -0.02em;
  white-space: nowrap;
}

/* stockOfTheEarth.forms.ToneSelect: the pills themselves are the options.
   The radio stays in the page for the keyboard and the form post, just out
   of sight; the chosen pill gets a ring in its own colour. */
.tone-select {
  display: flex;
  flex-wrap: wrap;
  gap: var(--md-space-2);
  padding: var(--md-space-1) 0;
}

.tone-select__option {
  position: relative;
  margin: 0;
  cursor: pointer;
}

.tone-select__option input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.tone-select__option .badge {
  font-size: var(--md-sys-typescale-label-size);
  padding: var(--md-space-1) var(--md-space-3);
}

.tone-select__option input:checked + .badge {
  outline: 2px solid var(--tone);
  outline-offset: 2px;
}

.tone-select__option input:focus-visible + .badge {
  outline: 2px dashed var(--md-sys-color-on-surface);
  outline-offset: 2px;
}

/* ---- appearance ----------------------------------------------------------
 * Settings -> General: the theme and colour-vision choices, with the chosen
 * palette drawn beside them. Side by side so the preview changes next to the
 * control that changed it; stacked on a phone.
 *
 * The preview is a small piece of the app in the chosen mode: the sidebar's
 * section tiles, a card heading's section pill, the status chips, the tone
 * pills, the two gradient stat tiles and a chart's series. Each part is the
 * real component, so it repaints from the palette the figure's own
 * data-vision selects (tokens.css), and from the theme its
 * data-preview-theme pins - light-dark() resolves on the element that uses
 * it, so the figure's color-scheme is enough. */

.appearance {
  display: grid;
  grid-template-columns: minmax(14rem, 22rem) minmax(0, 1fr);
  gap: var(--md-space-6);
  align-items: start;
}

@media (max-width: 720px) {
  .appearance {
    grid-template-columns: minmax(0, 1fr);
  }
}

.vision-preview {
  margin: 0;
  padding: var(--md-space-4);
  display: grid;
  grid-template-columns: minmax(10rem, 12rem) minmax(0, 1fr);
  gap: var(--md-space-4);
  color: var(--md-sys-color-on-surface);
  background: var(--md-sys-color-surface-container);
  border-radius: var(--md-sys-shape-corner-panel);
}

.vision-preview[data-preview-theme="light"] {
  color-scheme: light;
}

.vision-preview[data-preview-theme="dark"] {
  color-scheme: dark;
}

@media (max-width: 720px) {
  .vision-preview {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* The sidebar, in miniature: each section's icon tile beside its name. */
.vision-preview__nav {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  align-content: start;
  gap: var(--md-space-1);
  font-size: var(--md-sys-typescale-body-small-size);
}

.vision-preview__nav li {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  padding: var(--md-space-1) var(--md-space-2);
  border-radius: var(--md-sys-shape-corner-xs);
}

/* The current sub-page: the highlight let down into the surface, as the nav
   draws it, with the heavier weight that says so without the colour. */
.vision-preview__nav li.is-current {
  background: var(--section-soft);
  color: var(--section-soft-ink);
  font-weight: 600;
}

.vision-preview__body {
  display: grid;
  align-content: start;
  gap: var(--md-space-3);
  min-width: 0;
}

.vision-preview__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--md-space-2);
}

/* The gradient tiles at a third of their size: the colours are the point, not
   the figures. */
.vision-preview__tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--md-space-3);
}

.vision-preview__tiles .stat-tile {
  min-height: 0;
  margin: 0;
  padding: var(--md-space-3) var(--md-space-4);
  border-radius: var(--md-sys-shape-corner-panel);
}

.vision-preview__tiles .kpi {
  min-width: 0;
}

.vision-preview__tiles .kpi-value {
  font-size: var(--md-sys-typescale-headline-small-size);
}

.vision-preview__tiles .kpi-label {
  font-size: var(--md-sys-typescale-body-small-size);
}

.vision-preview__bars {
  display: flex;
  align-items: flex-end;
  gap: var(--md-space-1);
  height: 4rem;
}

.vision-preview__bars span {
  flex: 1;
  height: 55%;
  background: var(--tone);
  border-radius: var(--md-sys-shape-corner-xs) var(--md-sys-shape-corner-xs) 0 0;
}

/* A chart's bars at made-up heights, so neighbouring series are told apart by
   colour and not by being level with each other. */
.vision-preview__bars span:nth-child(2) { height: 80%; }
.vision-preview__bars span:nth-child(3) { height: 40%; }
.vision-preview__bars span:nth-child(4) { height: 95%; }
.vision-preview__bars span:nth-child(5) { height: 65%; }
.vision-preview__bars span:nth-child(6) { height: 30%; }
.vision-preview__bars span:nth-child(7) { height: 75%; }
.vision-preview__bars span:nth-child(8) { height: 50%; }

.vision-preview figcaption {
  grid-column: 1 / -1;
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

/* The data quality page's per-area overview: one row per check, its title on
   the left and its count (or OK) as a badge on the right. */
.check-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.check-list > li {
  padding: var(--md-space-2) 0;
  border-top: 1px solid var(--md-sys-color-outline-variant);
}

.check-list > li:first-child {
  border-top: 0;
}

/* ---- help hints ----------------------------------------------------------
 * A question-mark disc beside a heading that reveals an explanation on hover
 * or focus. For a rule too long to sit in a label but too important to leave
 * only in the docs - the order-status categories being the case it was built
 * for. See templates/_help_hint.html.
 *
 * A <button>, not a hover-only span: the text inside is sometimes the only
 * place a rule is written down in the app, so it has to be reachable from the
 * keyboard and by a screen reader. */

.help-hint {
  position: relative;
  display: inline-flex;
  vertical-align: middle;
}

.help-hint > button {
  display: inline-flex;
  align-items: center;
  padding: 0;
  border: 0;
  background: none;
  color: var(--md-sys-color-on-surface-variant);
  cursor: help;
}

.help-hint > button:hover,
.help-hint > button:focus-visible {
  color: var(--md-sys-color-primary);
}

/* Sized against the page rather than the heading it sits in. `.icon` is 1em, so
   a hint beside an <h1> drew a question mark as tall as the title - and these
   now sit next to headings of three different sizes. */
.help-hint > button .icon {
  width: 1.05rem;
  height: 1.05rem;
}

/* It floats over the page, so it takes the floating shadow (the elevation
   note in tokens.css). The typography is reset because this sits inside a
   heading, in the display face - inheriting that would make a paragraph
   unreadable. */
.help-hint-bubble {
  position: absolute;
  top: calc(100% + var(--md-space-2));
  left: 0;
  z-index: 30;
  width: max-content;
  max-width: min(26rem, 70vw);
  padding: var(--md-space-3);
  background: var(--md-sys-color-surface-container-high);
  box-shadow: var(--md-sys-elevation-2);
  font-family: var(--md-sys-typescale-font);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-sm);
  color: var(--md-sys-color-on-surface);
  font-size: var(--md-sys-typescale-body-small-size);
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: normal;
  text-align: left;
  text-transform: none;
  white-space: normal;
}

/* ---- snackbars (django messages) -----------------------------------------
 * Floating rather than inline, for a structural reason: .page-header is
 * sticky with a negative top margin that pulls it to the edge of <main>, so
 * anything rendered above it in the flow gets overlapped and clipped. Messages
 * were exactly that. Fixed positioning sidesteps it, and a snackbar is M3's
 * own component for transient feedback.
 *
 * Bottom right, not bottom left where M3 would put it: the sidebar's user menu
 * opens upward into that corner. */

.snackbars {
  position: fixed;
  bottom: var(--md-space-4);
  right: var(--md-space-4);
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: var(--md-space-2);
  max-width: min(32rem, calc(100vw - 2 * var(--md-space-4)));
}

/* Inverse surface is M3's snackbar container: deliberately the opposite tone
   to the page so it reads as an overlay rather than part of the content. */
.snackbar {
  display: flex;
  align-items: flex-start;
  gap: var(--md-space-3);
  padding: var(--md-space-3) var(--md-space-4);
  border-radius: var(--md-sys-shape-corner-panel);
  box-shadow: var(--md-sys-elevation-2);
  border-left: 4px solid transparent;
  background: var(--md-sys-color-inverse-surface);
  color: var(--md-sys-color-inverse-on-surface);
  font-size: var(--md-sys-typescale-body-size);
}

.snackbar-success {
  border-left-color: var(--md-sys-color-primary);
}

.snackbar-warning {
  border-left-color: var(--md-sys-color-warning);
}

.snackbar-error {
  border-left-color: var(--md-sys-color-error);
}

.snackbar button {
  flex-shrink: 0;
  background: none;
  border: 0;
  padding: 0 var(--md-space-1);
  margin: -2px 0 0;
  font-size: 1.1rem;
  line-height: 1.2;
  color: inherit;
  opacity: 0.7;
  cursor: pointer;
}

.snackbar button:hover {
  background: none;
  opacity: 1;
}

/* ---- pagination ----------------------------------------------------------
 * The row count on the left, the controls on the right: `space-between` needs
 * something to push against, and "26-50 of 312" is the thing a person reading
 * a list wanted to know anyway.
 *
 * Every target is 2rem square. The old pager was two text links whose hit area
 * was the width of the word "Prev", which is a small target with a mouse and a
 * miss with a thumb. See _pager.html for the markup this expects. */

.pager {
  /* The design centres the controls under the list and keeps the count at
     the left: three columns, the outer two equal, so the middle one is the
     page's middle whatever the count says. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: var(--md-space-2);
  margin-top: var(--md-space-4);
  font-size: var(--md-sys-typescale-body-size);
}

.pager-count {
  margin: 0;
}

.pager-count {
  color: var(--md-sys-color-on-surface-variant);
  /* Tabular figures so the count does not jitter as the digits change under a
     cursor that is sitting still on Next. */
  font-variant-numeric: tabular-nums;
}

/* One bordered bar, as the design draws it: the steps at either end, the
   page numbers between, a hairline after First and Prev and before Next and
   Last. */
.pager-controls {
  display: flex;
  align-items: center;
  gap: 2px;
  min-height: 2rem;
  padding: 0 var(--md-space-1);
  border: 1px solid var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-sm);
  /* It is a <ul> for the screen reader's "list of 9 items"; the bullets and
     the indent are not wanted. */
  list-style: none;
  margin: 0;
  padding: 0;
}

.pager-controls > li:nth-child(2),
.pager-controls > li:nth-child(3),
.pager-controls > li:nth-last-child(2),
.pager-controls > li:last-child {
  border-left: 1px solid var(--md-sys-color-outline);
  padding-left: 2px;
}

.pager-page,
.pager-step,
.pager-gap {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.75rem;
  height: 1.625rem;
  padding: 0 var(--md-space-1);
  border-radius: var(--md-sys-shape-corner-xs);
  color: var(--md-sys-color-on-surface);
  font-size: var(--md-sys-typescale-body-small-size);
  font-weight: 500;
  text-decoration: none;
  font-variant-numeric: tabular-nums;
}

/* The anchors carry href, so the pointer comes for free - but the current page
   and the disabled steps are spans, and a span that looks identical to its
   neighbours should not offer a hand. */
.pager a {
  color: var(--md-sys-color-on-surface);
  transition:
    background var(--md-sys-motion-duration-short)
      var(--md-sys-motion-easing-standard),
    color var(--md-sys-motion-duration-short)
      var(--md-sys-motion-easing-standard);
}

.pager a:hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-primary) var(--md-sys-state-hover-opacity),
    transparent
  );
}

.pager a:active {
  background: color-mix(
    in srgb,
    var(--md-sys-color-primary) var(--md-sys-state-pressed-opacity),
    transparent
  );
}

/* Filled, not merely bold: on a row of nine near-identical targets, weight
   alone is not enough to find where you are at a glance. */
.pager-page.is-current {
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  font-weight: 600;
}

.pager-step.is-disabled {
  color: var(--md-sys-color-on-surface);
  opacity: 0.38;
}

.pager-gap {
  /* Narrower than a page number: an ellipsis is not a target, and giving it the
     same 2rem box leaves a visible hole in the row. */
  min-width: 1rem;
}

@media (max-width: 880px) {
  /* The count above the controls rather than beside them - at 390px the two
     together wrap into a ragged two lines anyway. */
  .pager {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
  }

  .pager-count {
    text-align: center;
  }
}

/* ---- collapsible location / batch groups --------------------------------- */

.loc-group {
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-sm);
  margin-bottom: var(--md-space-2);
  overflow: hidden;
}

.loc-group + .loc-group {
  margin-top: var(--md-space-2);
}

.loc-group summary {
  display: flex;
  align-items: center;
  gap: var(--md-space-3);
  padding: var(--md-space-2) var(--md-space-3);
  cursor: pointer;
  font-weight: 600;
  list-style: none;
}

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

.loc-group summary::before {
  content: "\25B8";
  color: var(--md-sys-color-on-surface-variant);
  transition: transform 120ms ease;
}

.loc-group[open] summary::before {
  transform: rotate(90deg);
}

.loc-group summary span:first-of-type {
  flex: 1;
}

.loc-group summary:hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-surface) var(--md-sys-state-hover-opacity),
    transparent
  );
}

.loc-group .loc-group-body {
  padding: 0 var(--md-space-3) var(--md-space-3);
}

/* The right-hand side of a tree node: figures then actions. It must not shrink
   or wrap - a long location name would otherwise fold "+ Add sublocation" onto
   two lines and leave every row a different height. */

.location-actions {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  flex-shrink: 0;
  white-space: nowrap;
}

/* The stock figures on a tree node. Two fixed-width right-aligned boxes rather
   than text in the flow: a nested <details> indents its whole row, so anything
   flow-positioned drifts right with the depth and never forms a column. */

.location-stock {
  display: flex;
  gap: var(--md-space-3);
  flex-shrink: 0;
  font-size: var(--md-sys-typescale-label-small-size);
  color: var(--md-sys-color-on-surface-variant);
  text-decoration: none;
}

.location-stock:hover {
  color: var(--md-sys-color-primary);
}

.location-stock > span {
  width: 5rem;
  text-align: right;
}

.location-stock .here {
  width: 4.5rem;
  color: var(--md-sys-color-on-surface-variant);
}

/* ---- location tree -------------------------------------------------------- */

/* No indent of its own: the whole indent comes from .location-children below,
   so there is one place that decides how far a level steps in and one place
   that decides where the guide line falls. */
.location-node {
  margin-left: 0;
}

.location-node > summary {
  cursor: pointer;
  padding: 2px var(--md-space-2);
  border-radius: var(--md-sys-shape-corner-xs);
  /* The marker is gone anyway - display:flex on a summary removes it - so this
     only stops Firefox drawing one before the flex layout applies. */
  list-style: none;
}

.location-node > summary::-webkit-details-marker {
  display: none;
}

.tree-controls {
  display: flex;
  gap: var(--md-space-2);
  margin: 0 0 var(--md-space-3);
}

/* The disclosure chevron, shown only for a node that actually has children.
   `:has` rather than a template flag so a child appended by htmx makes its
   parent foldable with no re-render and no JS. */

.location-label {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  min-width: 0;
}

/* One chevron for the nav groups, the user menu and the tree - see
   templates/_chevron.html for why it is drawn rather than typed. */

.chev-icon {
  width: 0.9rem;
  height: 0.9rem;
  flex-shrink: 0;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.tree-chev {
  visibility: hidden;
  color: var(--md-sys-color-on-surface-variant);
  transition: transform 120ms ease;
}

.location-node:has(> .location-children > *) > summary .tree-chev {
  visibility: visible;
}

.location-node[open]:has(> .location-children > *) > summary .tree-chev {
  transform: rotate(90deg);
}

/* A leaf folds nothing, so it should not offer to. */
.location-node:not(:has(> .location-children > *)) > summary {
  cursor: default;
}

.location-node > summary:hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-surface) var(--md-sys-state-hover-opacity),
    transparent
  );
}

.location-node > summary a,
.location-node > summary form {
  margin-left: var(--md-space-2);
}

/* The guide line drops from the centre of the parent's chevron, so a branch
   visibly descends from the control that folds it. That centre is the summary's
   own left padding plus half the chevron; anything else leaves the line beside
   the chevron rather than under it. */
.location-children {
  margin-left: calc(var(--md-space-2) + 0.45rem);
  border-left: 1px solid var(--md-sys-color-outline-variant);
  padding-left: var(--md-space-3);
}

/* ---- dialog ---------------------------------------------------------------
 * A tile lifted off the page: the tile's corners and the floating shadow. The
 * backdrop is the scrim role, which darkens whatever is behind it in either
 * theme.
 *
 * `dialog.modal`, not one class per modal: this was `dialog.movement-modal` and
 * its only user had been removed, leaving styling for a dialog that no longer
 * existed. The lot-assignment modal (#136) is the second dialog to want exactly
 * this, which is one more than a per-feature class was ever worth. Wide, because
 * what is in it is a table of shelves and quantities. */

dialog.modal {
  background: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-tile);
  box-shadow: var(--md-sys-elevation-3);
  padding: var(--md-space-6) var(--md-space-7);
  width: min(860px, 94vw);
  /* The body scrolls, not the page behind it: a product in twenty lots is a
     long table, and a dialog taller than the window cannot be dismissed. */
  max-height: 88vh;
  overflow-y: auto;
}

dialog.modal::backdrop {
  background: var(--md-sys-color-scrim);
}

/* The bar a dialog's form ends with: the running total on one side, the buttons
   on the other. Sticky inside the dialog's own scroll, because a form long
   enough to need a total is long enough to scroll the total off the screen. The
   background is the dialog's, so rows pass behind it rather than through it. */
.modal-actions {
  position: sticky;
  bottom: calc(var(--md-space-5) * -1);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--md-space-3) var(--md-space-5);
  margin-top: var(--md-space-4);
  padding: var(--md-space-4) 0;
  background: var(--md-sys-color-surface-container-high);
  border-top: 1px solid var(--md-sys-color-outline-variant);
}

.sum-readout {
  margin: 0;
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

.sum-readout output {
  font-weight: 700;
  font-size: var(--md-sys-typescale-title-size);
  font-variant-numeric: tabular-nums;
  color: var(--md-sys-color-on-surface);
}

/* All three notes are in the markup and the scope's class picks one, so the JS
   toggles a class and never writes wording - the language stays in the template
   with the rest of the page's.

   The remainder is the default state rather than a third special case: most of
   the time there is some left to assign, and that is the figure being worked
   towards. `is-over` and `is-exact` replace it. */
.sum-note {
  display: none;
  margin-left: var(--md-space-1);
}

.sum-note--remaining {
  display: inline;
}

[data-sum-scope].is-over .sum-note--remaining,
[data-sum-scope].is-exact .sum-note--remaining {
  display: none;
}

[data-sum-scope].is-over .sum-note--over,
[data-sum-scope].is-exact .sum-note--exact {
  display: inline;
}

[data-sum-scope].is-over .sum-note--over,
[data-sum-scope].is-over output {
  color: var(--md-sys-color-error);
}

[data-sum-scope].is-exact .sum-note--exact {
  color: var(--md-sys-color-primary);
}

/* A figure that is also a control: it has to read as one of a column of numbers
   first and as clickable second, so it keeps the column's alignment and tabular
   figures and takes a dotted underline rather than a button's container. */
.fill-link {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-variant-numeric: tabular-nums;
  color: var(--md-sys-color-primary);
  cursor: pointer;
  text-decoration: underline dotted;
  text-underline-offset: 3px;
}

.fill-link:hover {
  text-decoration: underline solid;
}

/* A label that has to exist for the input beside it but would only repeat the
   column heading on screen. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* M3's text button: no container until hovered. */
.btn-link {
  background: none;
  border: none;
  color: var(--md-sys-color-primary);
  cursor: pointer;
  padding: 0;
  font-size: var(--md-sys-typescale-body-small-size);
  text-decoration: underline;
}

.btn-link:hover {
  background: none;
  color: var(--md-sys-color-primary);
}

/* ---- loading states ------------------------------------------------------ */

.skeleton {
  background: linear-gradient(
    90deg,
    var(--md-sys-color-surface-container) 25%,
    var(--md-sys-color-surface-container-high) 37%,
    var(--md-sys-color-surface-container) 63%
  );
  background-size: 400% 100%;
  animation: shimmer 1.4s ease infinite;
  border-radius: var(--md-sys-shape-corner-xs);
  display: inline-block;
}

@keyframes shimmer {
  0% {
    background-position: 100% 0;
  }
  100% {
    background-position: 0 0;
  }
}

/* A pane being replaced by htmx dims and stops taking clicks.
 *
 * `.swap-target` rather than the `#results` id this used to key on. htmx puts
 * `htmx-request` on the element that *issued* the request - the filter form -
 * not on the target it swaps into, so `#results.htmx-request` could never
 * match anything and the rule had never once fired. The panes now name
 * themselves with a class and the triggers point `hx-indicator` at them, which
 * is what actually moves the class onto the pane. */
/* The transition belongs on the pane, not inside the `.htmx-request` rule where
   it was. Declared there it only existed while the class was present, so the
   pane faded out and then snapped back at full brightness the instant the new
   rows arrived - the half of the movement that tells you the page you asked for
   has landed. Both directions now. */
.swap-target {
  transition: opacity var(--md-sys-motion-duration-short)
    var(--md-sys-motion-easing-standard);
}

.swap-target.htmx-request {
  opacity: 0.55;
  pointer-events: none;
}

/* ---- busy forms ----------------------------------------------------------
 * A submit that moves stock can take a moment, and until now it looked
 * identical to a click that missed. busy.js adds .is-busy on submit; these
 * rules are the whole visible difference. */

.is-busy button[type="submit"],
.is-busy button:not([type]),
.is-busy input[type="submit"] {
  position: relative;
  padding-left: 2rem;
  cursor: progress;
}

.is-busy button[type="submit"]::before,
.is-busy button:not([type])::before,
.is-busy input[type="submit"]::before {
  content: "";
  position: absolute;
  left: 0.75rem;
  top: 50%;
  width: 0.85em;
  height: 0.85em;
  margin-top: -0.425em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 620ms linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(1turn);
  }
}

/* A spinner says "working"; it does not say "for how long". A form whose
 * submit takes long enough that a person starts wondering - creating a tenant
 * migrates a whole schema and takes about half a minute (QA-0922A) - carries a
 * note saying so, hidden until the submit that makes it true.
 *
 * Hidden by `display`, not by the `hidden` attribute, so it is out of the
 * accessibility tree until it applies and in it the moment it does. busy.js
 * needs no change for this - the class it already puts on the form is the
 * whole switch. */
[data-busy-note] {
  display: none;
}

.is-busy [data-busy-note] {
  display: block;
}

/* ---- dashboard charts ----------------------------------------------------
 * One height for every chart, so the tiles line up whatever is in them. It was
 * six copies of the same inline style. */

/* The entrance is paired with the grow-from-zero animation the chart script
 * runs. Two deliberate choices in four lines:
 *
 * The hidden state is here rather than added by that script, because a class
 * applied from JS lands after the browser has already painted the element once
 * - which is a flash of the finished chart before it fades in. Nothing is lost
 * if the script never runs: the box would be empty either way.
 *
 * Opacity only, no transform. Plotly measures the element with
 * getBoundingClientRect as it draws, and a transform moves that box, so the
 * plot lays itself out against the shifted rect and stays offset inside its
 * container once the transform settles. The movement in this entrance is the
 * bars growing, which is Plotly's own animation and does not touch layout. */

.chart {
  width: 100%;
  height: 260px;
  opacity: 0;
  transition: opacity 420ms ease;
}

.chart.is-revealed {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }

  /* Two exceptions, both feedback rather than decoration. Frozen, they would
     leave a user who asked for less motion with no indication that anything is
     happening at all - which is not what that preference asks for. */
  .is-busy button::before,
  .skeleton {
    animation-duration: 900ms !important;
  }
}

/* ---- design system components -------------------------------------------
 * The generic pieces the design's pages are assembled from that the app had
 * no class for. docs/design-system.md has the markup for each. */

/* The instruction banner - the design's pale yellow "Tick orders to print
   their paperwork..." and "Drag to reorder...": how the thing beside it
   works, said once, in place. An optional icon first and an optional dismiss
   last; `role="note"` on the element, because it is commentary on the page
   rather than an alert about it.

     <div class="hint-banner" role="note">
       {% icon "drag" %}<p>Drag to reorder...</p>
       <button type="button" class="icon-btn" aria-label="Dismiss">{% icon "close" %}</button>
     </div>
*/
.hint-banner {
  display: flex;
  align-items: center;
  gap: var(--md-space-3);
  min-height: 2.625rem;
  margin: 0 0 var(--md-space-4);
  padding: 5px var(--md-space-3) 5px var(--md-space-4);
  background: var(--md-sys-color-hint-container);
  border: 1px solid var(--md-sys-color-hint-outline);
  border-radius: var(--md-sys-shape-corner-panel);
  color: var(--md-sys-color-on-hint-container);
  font-size: var(--md-sys-typescale-body-size);
  font-weight: 500;
}

.hint-banner > p {
  flex: 1;
  margin: 0;
}

.hint-banner > .icon {
  width: 16px;
  height: 16px;
}

.hint-banner .icon-btn {
  color: var(--md-sys-color-on-hint-container);
}

/* The stat tile: a card holding a row of big figures - the design's "0 Open
   sales orders" and "39.815 g Packaging per unit". The figures are the
   existing .kpi-strip (below), so a page that already has one only needs the
   tile around it.

     <section class="stat-tile stat-tile--inventory">
       <div class="kpi-strip"> <a class="kpi" href="...">
         <span class="kpi-value">0</span><span class="kpi-label">Unit on hand</span>
       </a> ... </div>
     </section>

   Plain, it is a white tile like a .card. With --finance or --inventory it is
   the design's gradient with its grain: the gradient and the grain's colour
   are palette entries, so a colour-vision mode repaints both (tokens.css),
   and the words on it are the dark highlight ink in both themes. */
.stat-tile {
  position: relative;
  isolation: isolate;
  min-height: 6.5rem;
  margin-bottom: var(--md-space-4);
  padding: var(--md-space-6) var(--md-space-7);
  background: var(--md-sys-color-surface-container-low);
  border-radius: var(--md-sys-shape-corner-tile);
  box-shadow: var(--md-sys-elevation-1);
  overflow: hidden;
}

.stat-tile--finance,
.stat-tile--inventory {
  box-shadow: none;
  color: var(--md-sys-color-on-highlight);
}

.stat-tile--finance {
  --grad-from: var(--pal-grad-finance-from);
  --grad-to: var(--pal-grad-finance-to);
  --grad-noise: var(--pal-grad-finance-noise);
}

.stat-tile--inventory {
  --grad-from: var(--pal-grad-inventory-from);
  --grad-to: var(--pal-grad-inventory-to);
  --grad-noise: var(--pal-grad-inventory-noise);
}

.stat-tile--finance,
.stat-tile--inventory {
  background: linear-gradient(to bottom, var(--grad-from), var(--grad-to));
}

/* The grain: the exported noise mask, painted in the tile's noise colour and
   laid under the words (z-index -1 inside the tile's own stacking context,
   which `isolation` gives it). */
.stat-tile--finance::before,
.stat-tile--inventory::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--grad-noise);
  mask-image: var(--app-noise);
  mask-repeat: repeat;
  pointer-events: none;
}

/* The dark ink in both themes, hovered or not: `.kpi:hover` turns a figure
   the accent colour, which on these pastels is the wrong colour in light and
   unreadable in dark. A hovered figure underlines its label instead. */
.stat-tile--finance .kpi :is(.kpi-value, .kpi-label, .kpi-hint),
.stat-tile--inventory .kpi :is(.kpi-value, .kpi-label, .kpi-hint),
.stat-tile--finance .kpi:hover :is(.kpi-value, .kpi-label),
.stat-tile--inventory .kpi:hover :is(.kpi-value, .kpi-label) {
  color: var(--md-sys-color-on-highlight);
}

.stat-tile--finance a.kpi:hover .kpi-label,
.stat-tile--inventory a.kpi:hover .kpi-label {
  text-decoration: underline;
}

/* The photographic banner - the dashboard's "Got ideas?": a tile of the
   design's blurred photograph, 45px corners, with a white tile set into it
   carrying the words. The photograph is decoration (a CSS background, so it
   has no alt to get wrong). */
.photo-banner {
  padding: 26px;
  border-radius: var(--md-sys-shape-corner-banner);
  background-color: var(--md-sys-color-surface-container);
  background-image: var(--app-ideas-photo);
  background-size: cover;
  background-position: center;
}

.photo-banner__card {
  max-width: 28rem;
  padding: var(--md-space-6) var(--md-space-7);
  background: var(--md-sys-color-surface-container-low);
  border-radius: var(--md-sys-shape-corner-tile);
  font-size: var(--md-sys-typescale-body-large-size);
}

.photo-banner__card h2 {
  margin: 0 0 var(--md-space-4);
}

/* An avatar: a person's picture or a company's logo over their initials (see
   _avatar.html for why the letters are underneath). The design's two: the
   24px rounded square beside the account email, and the round 32px "PA"
   beside a customer. The initials take the section's highlight, so a
   customer's sit on Customers' lime. */
.avatar {
  position: relative;
  display: inline-grid;
  place-items: center;
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  overflow: hidden;
  border-radius: var(--md-sys-shape-corner-xs);
  background: var(--section);
  color: var(--md-sys-color-on-highlight);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0;
  line-height: 1;
  vertical-align: middle;
}

.avatar img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.avatar--lg {
  width: 32px;
  height: 32px;
  font-size: 0.875rem;
}

.avatar--round {
  border-radius: 50%;
}

/* The image thumbnail - the design's product gallery tile: 170px square,
   12px corners, its place in the order top right, "Default" bottom left.
   Hovered or focused, the image dims and the tools appear: the drag handle
   top left, Make default bottom left, delete bottom right. Shown on
   focus-within too, so a keyboard reaches them the same way a pointer does.

     <figure class="thumb">
       <img src="..." alt="...">
       <span class="thumb__index">1</span>
       <span class="thumb__tag">Default</span>
       <div class="thumb__tools"> ... buttons ... </div>
     </figure>
*/
.thumb {
  position: relative;
  width: 100%;
  max-width: 170px;
  aspect-ratio: 1;
  margin: 0;
  border: 1px solid var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-lg);
  overflow: hidden;
  background: var(--md-sys-color-paper);
}

.thumb > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.thumb::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--md-sys-color-scrim);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--md-sys-motion-duration-short)
    var(--md-sys-motion-easing-standard);
}

.thumb:hover::after,
.thumb:focus-within::after {
  opacity: 0.45;
}

/* The chips on an image are always paper-white with dark words, whatever the
   theme: they sit on a photograph, not on the page. */
.thumb__index,
.thumb__tag,
.thumb__tools button,
.thumb__tools .btn {
  position: absolute;
  z-index: 1;
  display: inline-grid;
  place-items: center;
  min-width: 22px;
  height: 22px;
  padding: 0 var(--md-space-2);
  border: 1px solid var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-sm);
  background: var(--md-sys-color-paper);
  color: var(--md-sys-color-on-highlight);
  font-size: var(--md-sys-typescale-caption-size);
  font-weight: 500;
  line-height: 1;
}

.thumb__index {
  top: var(--md-space-2);
  right: var(--md-space-2);
  padding: 0 4px;
  font-weight: 800;
}

.thumb__tag {
  bottom: var(--md-space-2);
  left: var(--md-space-2);
}

.thumb__tools {
  position: absolute;
  inset: 0;
  z-index: 1;
  opacity: 0;
  transition: opacity var(--md-sys-motion-duration-short)
    var(--md-sys-motion-easing-standard);
}

.thumb:hover .thumb__tools,
.thumb:focus-within .thumb__tools {
  opacity: 1;
}

/* The square tools: 30px, a 16px icon. */
.thumb__tools .thumb__drag,
.thumb__tools .thumb__delete {
  width: 30px;
  height: 30px;
  padding: 0;
}

.thumb__tools .thumb__drag {
  top: var(--md-space-2);
  left: var(--md-space-2);
  cursor: grab;
}

.thumb__tools .thumb__delete {
  bottom: var(--md-space-2);
  right: var(--md-space-2);
}

.thumb__tools .thumb__make-default {
  bottom: var(--md-space-2);
  left: var(--md-space-2);
}

/* The last tile of a gallery: where a new image goes - a dashed square with
   the Add button and "or drop files here" in it. */
.thumb--add {
  display: grid;
  place-content: center;
  gap: var(--md-space-3);
  justify-items: center;
  border: 2px dashed var(--md-sys-color-outline);
  background: transparent;
  font-size: var(--md-sys-typescale-label-small-size);
}

.thumb--add::after {
  display: none;
}

/* A gallery of them: as many 170px tiles as fit, 16px apart. */
.thumb-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(140px, 100%), 170px));
  gap: var(--md-space-4);
}

@media (prefers-reduced-motion: reduce) {
  .thumb::after,
  .thumb__tools {
    transition: none;
  }
}

/* The gallery page itself (examples/components.html): rows of specimens and
   the icon sheet. Layout only - every specimen is the real component. */
.ds-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: var(--md-space-4);
}

.ds-section {
  display: inline-flex;
  align-items: center;
  gap: var(--md-space-2);
}

.ds-icons {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: var(--md-space-2) var(--md-space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.ds-icons li {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  font-size: var(--md-sys-typescale-body-small-size);
}

.ds-icons .icon {
  font-size: 1.25rem;
}

/* ---- narrow screens ------------------------------------------------------
 * One block, at the foot of the file, because it touches the shell, the content
 * gutter and every table - splitting it across those sections would hide it.
 *
 * 880px is the width at which the 15rem sidebar stops being affordable next to
 * a table. It is a literal because a custom property cannot be used in a media
 * query.
 *
 * The desktop shell is a flex row of fixed-height panes with the *pane*
 * scrolling and the document not. All three of those choices are wrong here:
 * the sidebar becomes a drawer, so the row has one child, and the document
 * scrolls normally.
 */
@media (max-width: 880px) {
  html:has(body.app-shell),
  body.app-shell {
    height: auto;
  }

  body.app-shell {
    display: block;
    /* Was overflow:hidden, which on a narrow screen does not merely look wrong
       - it makes anything overflowing unreachable, because the pane that used
       to scroll instead of the document is now the document. */
    overflow: visible;
  }

  /* The sheet fills the screen: on a phone the photograph would only be a
     frame of a few pixels round the edge, bought with width the tables need. */
  .app-main {
    height: auto;
    margin: 0;
    border-radius: 0;
    overflow-y: visible;
    --main-pad-y: var(--md-space-4);
    --main-pad-x: var(--md-space-4);
  }

  .topbar {
    display: flex;
    position: sticky;
    top: 0;
    z-index: 20;
  }

  /* Off-canvas, and out of flow so the topbar and content lay out as if it
     were not there. */
  .sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 40;
    height: auto;
    /* The drawer slides over the page, so it needs the page's own ground
       under the words - the sky stays behind the sheet. Rounded on the edge
       it opens from, like the sheet's corners. */
    width: min(calc(var(--sidebar-w) + var(--md-space-6)), 88vw);
    padding: var(--md-space-4) var(--md-space-3);
    background: var(--md-sys-color-surface);
    border-radius: 0 var(--md-sys-shape-corner-page)
      var(--md-sys-shape-corner-page) 0;
    box-shadow: var(--md-sys-elevation-3);
    transform: translateX(-100%);
    transition: transform var(--md-sys-motion-duration-medium)
      var(--md-sys-motion-easing-emphasized);
  }

  .sidebar.is-open {
    transform: none;
  }

  .scrim {
    display: block;
  }

  /* The sidebar's own brand is redundant once the topbar carries one, and a
     drawer has nothing to fold into a rail. */
  .sidebar-head .brand,
  .sidebar-tools .icon-btn:first-child {
    display: none;
  }

  .sidebar-head {
    justify-content: flex-end;
  }

  /* Tables scroll instead of squashing, without touching the ~100 templates
     that hold a bare <table>.
     - `display:block` makes the table itself the scroll container; the rows
       inside are re-wrapped in an anonymous table box and keep their layout.
     - `nowrap` is what makes the scroll engage at all: allowed to wrap, cells
       compress to fit and there is nothing to scroll to, just unreadably
       narrow columns.
     Deliberately not `overflow` on .card - app.css already carries a note
     about ancestor overflow clipping the autocomplete dropdowns. */
  table {
    display: block;
    overflow-x: auto;
    white-space: nowrap;
  }

  /* Two sticky headers stacking would eat a phone screen, so the topbar wins -
     it holds the only way back to the nav.
     Dropping `position` is not enough on its own, though: the negative margins
     exist to bleed the header to the edges of a pane it is pinned inside, and
     left in place with the pin gone they pull the heading up underneath the
     topbar. That is the overlap. It goes back to being an ordinary block. */
  .page-header {
    position: static;
    margin: 0 0 var(--md-space-4);
    padding: 0;
  }

  /* Filter fields take the full width rather than sitting two-and-a-bit to a
     row, which is what the flex-wrap does unaided at this width. */
  .filters > div {
    flex: 1 1 100%;
  }

  .filters input,
  .filters select {
    width: 100%;
  }

  /* A 1.5rem headline is a lot of a 390px screen, and the topbar's brand is
     furniture rather than content. */
  h1 {
    font-size: 1.625rem;
  }

  h2 {
    font-size: 1.25rem;
  }
}

/* Sortable column headings (#52 follow-up).
 *
 * The heading itself is the control, so it takes the heading's own colour
 * rather than looking like a link in a row of labels - the chevron is what says
 * "this one is sorted", and it only appears on the column in force. An
 * underline on hover is the affordance; a blue heading would read as a link to
 * somewhere else.
 *
 * 0.95em, not the 1em `.icon` default, which was measured against body text: at
 * the heading's 11px uppercase a full-size chevron sits taller than the word
 * beside it. */
.sort-th a {
  color: inherit;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  white-space: nowrap;
}

.sort-th a:hover { text-decoration: underline; }

/* Right-aligned headings put the chevron on the correct side of the word. */
.sort-th.num a { flex-direction: row-reverse; }

.sort-icon {
  width: 0.95em;
  height: 0.95em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Checkbox popover for a multi-value filter (sales order Channel).
 *
 * A native <select multiple> needs ctrl-click to pick a second row, gives no
 * hint that it can, and takes a paragraph of vertical space to show three
 * options. This is the same Alpine x-show shape as .user-dropdown, opening
 * downwards from its trigger and sized like the selects beside it so the
 * filter bar stays on one line. */
.check-menu {
  position: relative;
}

/* Dressed as the select beside it - the design's Filter Input - because to
   the person using the bar it is one: a field showing what is chosen. */
.check-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--md-space-2);
  width: 100%;
  min-height: 2rem;
  padding: 5px 10px 5px 14px;
  background: var(--md-sys-color-surface-container-lowest);
  border: 1px solid var(--md-sys-color-field-outline);
  border-radius: var(--md-sys-shape-corner-md);
  color: var(--md-sys-color-on-surface);
  font: inherit;
  font-size: var(--md-sys-typescale-body-size);
  font-weight: 500;
  text-align: left;
  cursor: pointer;
}

.check-trigger:hover {
  border-color: var(--md-sys-color-outline);
}

.check-dropdown {
  /* Fixed, not absolute: `.filters-collapse` is overflow:hidden to animate
   * its collapse, and an absolutely-positioned child is clipped by it as soon
   * as it extends past the filter bar. Coordinates come from the trigger, set
   * by the Alpine `place()` in sales/list.html. */
  position: fixed;
  /* Wider than the trigger, which is sized to the filter bar rather than to
   * the longest channel name. */
  min-width: 14rem;
  /* Tall enough for the whole list without scrolling — the point of showing
   * checkboxes is seeing every option at once, and 16rem cut a seven-channel
   * tenant off at five. Still capped against the viewport so a tenant with
   * thirty channels scrolls rather than running off the screen. */
  max-height: min(28rem, 60vh);
  overflow-y: auto;
  background: var(--md-sys-color-surface-container-high);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-panel);
  box-shadow: var(--md-sys-elevation-2);
  padding: var(--md-space-2);
  display: flex;
  flex-direction: column;
  gap: 2px;
  z-index: 20;
  box-shadow: var(--md-sys-elevation-2);
}

.check-dropdown label {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  padding: var(--md-space-2) var(--md-space-3);
  border-radius: var(--md-sys-shape-corner-sm);
  font-size: var(--md-sys-typescale-body-small-size);
  /* Both overrides are load-bearing: `.filters label` is the uppercase,
   * bolded caption style used for the field labels above each control, and
   * these are option rows, not captions. */
  font-weight: normal;
  text-transform: none;
  letter-spacing: normal;
  color: var(--md-sys-color-on-surface);
  cursor: pointer;
  white-space: nowrap;
}

.check-dropdown label:hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-surface) var(--md-sys-state-hover-opacity),
    transparent
  );
}

.check-dropdown .divider {
  height: 1px;
  margin: var(--md-space-1) 0;
  background: var(--md-sys-color-outline-variant);
}

/* The report workbench: the document beside the conversation.
 *
 * Two columns on a wide screen, and one on a narrow one with the report first -
 * the report is what somebody came to read, and a chat log above it would push
 * it off the screen on a phone.
 *
 * **On a wide screen the two panes fill the viewport and scroll separately.**
 * They scrolled the page as one before, which meant reading down a long report
 * carried the composer off the bottom of the screen - so answering a question
 * about what you had just read began with scrolling back up to find the box.
 * Now the report scrolls inside its own frame and the conversation keeps its
 * composer pinned, which is the arrangement every chat beside a document
 * settles on for the same reason.
 *
 * `dvh` rather than `vh`, with a `vh` fallback for anything that lacks it: on a
 * phone `vh` is the height the viewport would have if the browser's own bars
 * were hidden, so a `100vh` pane is taller than the space it is in and the
 * bottom of it - here, the composer - sits under the address bar.
 *
 * The report keeps a paper-white surface in both themes rather than following
 * the theme switch. That is a decision rather than an omission: every export
 * and every print of this document is on white, and a report that looked one
 * way on screen and another on paper would be two documents. */
.workbench {
  display: grid;
  gap: 1.25rem;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}

.workbench__report {
  background: var(--md-sys-color-paper);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--radius-md, 8px);
  /* Clips the document's own corners to the border radius. Vertically this
     costs nothing while the box grows with its content, which is the narrow-
     screen case; the wide-screen rule below turns the y axis into a scroller. */
  overflow: hidden;
}

/* A snapshot report from the retired report builder (#628): its archived page
   in a sandboxed frame, so its own stylesheet cannot reach the app's. Paper
   white in both themes, like every print of it. */
.report-snapshot {
  display: block;
  width: 100%;
  height: calc(100vh - 13rem);
  height: calc(100dvh - 13rem);
  min-height: 28rem;
  background: var(--md-sys-color-paper);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-tile);
  box-shadow: var(--md-sys-elevation-1);
}

/* The two bounds on the agent, as bars.
 *
 * Side by side on anything but a phone, because they are read together - "how
 * much of the month is left" and "how many questions today" answer the same
 * question from different directions, and stacked they read as two unrelated
 * facts.
 *
 * Shaped by the same tokens as everything else (the shape block in
 * tokens.css). The width is inline on the fill, which is the only place a dynamic bar can
 * carry it. */
.usage-bars {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--md-space-4);
  margin-bottom: var(--md-space-4);
}

.usage__label {
  display: flex;
  justify-content: space-between;
  gap: var(--md-space-2);
  margin: 0 0 var(--md-space-1);
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  color: var(--md-sys-color-primary);
}

.usage__label span:last-child {
  font-variant-numeric: tabular-nums;
  color: var(--md-sys-color-on-surface);
}

.usage__track {
  height: 0.4rem;
  background: var(--md-sys-color-surface-container-highest);
  border-radius: var(--md-sys-shape-corner-full);
  overflow: hidden;
}

.usage__track > span {
  display: block;
  height: 100%;
  /* `min-width` so a bar that is a fraction of a percent along is still a
     visible sliver rather than nothing - the same reason `percent_used` never
     rounds down to zero. */
  min-width: 2px;
  background: var(--md-sys-color-primary);
}

/* Spent. Not the error colour: running out of today's messages is the system
   working as configured, not a fault, and it clears itself within hours. */
.usage__track--full > span {
  background: var(--md-sys-color-outline);
}

.usage__note {
  margin: var(--md-space-1) 0 0;
  font-size: var(--md-sys-typescale-label-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

/* The conversation fills its column: the log takes whatever is left and the
   composer sits on the bottom edge. */
.workbench__chat {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  min-height: 0;
}

#chat-log {
  display: flex;
  flex-direction: column;
  /* Tight. A transcript is read as one thing, and 0.6rem between every line
     plus the activity lines' own padding turned four exchanges into a page of
     scrolling. The turns are separated by their own backgrounds rather than by
     air. */
  gap: 0.15rem;
  overflow-y: auto;
  /* On a narrow screen the panes are not pinned, so the log needs a ceiling of
     its own or a long conversation buries the report below it. The wide-screen
     rule below replaces this with "whatever is left". */
  max-height: 26rem;
  min-height: 0;
}

/* **This block comes after the base rules on purpose.** Both set `overflow` on
   `.workbench__report` at the same specificity, so whichever is written last
   wins - and with the media query first, the base `overflow: hidden` clobbered
   `overflow-y: auto` and the report did not scroll at all. */
@media (min-width: 60rem) {
  .workbench {
    grid-template-columns: minmax(0, 3fr) minmax(22rem, 2fr);
    /* What the page header, the settings tabs and the page's own padding take
       off the top. A variable rather than a number inline, because it is a
       guess about other components and somebody adjusting it should find one
       place to do it. */
    --workbench-chrome: 13rem;
    height: calc(100vh - var(--workbench-chrome));
    height: calc(100dvh - var(--workbench-chrome));
    /* Below this there is no point pinning anything: a 12rem chat pane is
       worse than a scrolling page. */
    min-height: 28rem;
    align-items: stretch;
  }

  /* `min-height: 0` on both, and it is not decoration. A grid item's default
     minimum is its content, so a long report or a long chat log would push the
     row taller than the height above and scroll the page anyway - which is the
     bug this whole block exists to fix. */
  .workbench__report,
  .workbench__chat {
    min-height: 0;
    height: 100%;
  }

  .workbench__report { overflow-y: auto; }

  #chat-log {
    flex: 1 1 auto;
    max-height: none;
  }
}

/* The composer: one row, with the button inside the field.
 *
 * Two buttons stacked under a textarea is the shape of a form somebody fills
 * in and submits. This is a conversation, and what people do to a conversation
 * is press return - so return sends, shift+return makes a newline, and the
 * button is a small affordance in the corner of the field rather than the
 * primary way in.
 *
 * Send and Stop share the slot. Only one of them is ever the thing to do, and
 * a permanently visible Stop is a control that does nothing for all but a few
 * seconds of a session. */
.composer {
  position: relative;
  display: flex;
  flex: none;
  /* Shared by the field's floor and the button's box, so the two cannot drift
     apart - see the note on the min-height below. */
  --composer-button: 2rem;
  --composer-inset: 0.35rem;
}

.composer textarea {
  width: 100%;
  /* Room for the button, plus the padding it sits in. */
  padding-right: 3rem;
  resize: none;
  /* **The base `textarea` rule sets `min-height: 4.5rem`**, which is the right
     default for a form field and wrong for a chat box: it meant `rows="1"` had
     no effect at all and the send button, anchored to the bottom edge, sat
     three empty lines below the text it belongs to. That was the misalignment.
     The floor here is instead exactly what the button needs - its own height
     plus its inset, twice - so at rest the button is centred in the field by
     construction rather than by a magic number. */
  min-height: calc(var(--composer-button) + 2 * var(--composer-inset));
  /* Grown by the script as somebody types, up to a ceiling - a composer that
     can eat the whole pane is one that hides the conversation it is part of. */
  max-height: 12rem;
  overflow-y: auto;
}

.composer__send {
  position: absolute;
  right: var(--composer-inset);
  bottom: var(--composer-inset);
  /* **`:not([hidden])`, and it is load-bearing.** An author `display` beats the
     browser's own `[hidden] { display: none }`, and this stylesheet has no
     `[hidden]` reset of its own (see the note on `.filters` above, which was
     bitten by the same thing) - so without this, Stop rendered on top of Send
     from the moment the page loaded, before anybody had asked anything. */
  display: none;
  align-items: center;
  justify-content: center;
  width: var(--composer-button);
  height: var(--composer-button);
  padding: 0;
  border-radius: var(--md-sys-shape-corner-sm);
}

.composer__send:not([hidden]) { display: inline-flex; }

.composer__send .icon { width: 1.05rem; height: 1.05rem; }

/* Stop is not an error, it is a choice - so not the red one. But the secondary
   container is a near neighbour of this app's primary green, and a mint circle
   where a green circle had been a moment earlier read as the same button
   failing to do anything. A plain surface with a visible edge is legible as a
   different control at a glance, which is the whole job of the swap. */
.composer__stop {
  background: var(--md-sys-color-surface-container-highest);
  color: var(--md-sys-color-on-surface-variant);
  border-color: var(--md-sys-color-outline);
}

/* The activity lines in the report chat.
 *
 * Quieter than the conversation on purpose: they are there to be scanned past
 * unless somebody is checking what the agent looked at, which is most of the
 * time. Monospace numerals so a column of row counts lines up when several
 * queries run in one step. */
/* A turn in the conversation.
 *
 * Bubbles, sided: yours on the right in the primary tint, the agent's on the
 * left on a raised surface. That is the arrangement every messaging app uses
 * and it carries the one fact a transcript has to carry at a glance - who said
 * which of these - without a name label on every line.
 *
 * Neither side takes the full width. A bubble that runs the whole column has
 * no side to be on, and the ragged edge is what makes the alternation
 * readable; 85% rather than a fixed measure so it still works in a 22rem pane.
 *
 * `align-self` rather than `margin-left: auto`, because the log is a flex
 * column and this is the property that column understands. */
.chat-line {
  margin: 0;
  max-width: 85%;
  padding: 0.4rem 0.65rem;
  /* The small corner every chip-sized thing takes (the shape block in
     tokens.css). Sides and backgrounds carry who-said-what here; rounding is
     not doing that work. */
  border-radius: var(--md-sys-shape-corner-sm);
  /* The model writes newlines and means them - a short list in a reply is
     lines, not one wrapped paragraph. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.chat-line--you {
  align-self: flex-end;
  background: var(--md-sys-color-primary-container);
  color: var(--md-sys-color-on-primary-container);
  /* The one place air is worth spending: a new question is the start of an
     exchange, and the gap is what separates exchanges from each other. */
  margin-top: 0.5rem;
}

.chat-line--agent {
  align-self: flex-start;
  background: var(--md-sys-color-surface-container-high);
  color: var(--md-sys-color-on-surface);
}

/* Not a bubble: nobody said it. Errors, and the note after a Stop - so it runs
   the width of the log as a notice, quieter than either speaker. */
.chat-line--system {
  align-self: stretch;
  max-width: none;
  background: var(--md-sys-color-surface-container);
  color: var(--md-sys-color-on-surface-variant);
  border: 1px solid var(--md-sys-color-outline-variant);
  font-size: 0.85rem;
}

/* The activity lines are annotations on the agent's turn rather than turns of
   their own, so they keep the agent's side and the full width of it. */
.chat-line--tool,
.chat-line--thinking {
  align-self: stretch;
  max-width: none;
}

.chat-line--tool {
  padding: 0.15rem 0.6rem;
  font-size: 0.8rem;
  color: var(--md-sys-color-on-surface-variant);
  font-variant-numeric: tabular-nums;
  border-left: 2px solid var(--md-sys-color-outline-variant);
  border-radius: 0;
  background: none;
}

/* A tool that refused. Not the error colour - most of what lands here is the
 * model asking for something it may not have, which it recovers from in one
 * step, and painting that red would train people to distrust a working
 * conversation. */
.chat-line--tool-error {
  border-left-color: var(--md-sys-color-warning);
}

/* "Thinking… 4s".
 *
 * The timer is what keeps it honest: a pending request proves we are waiting,
 * not that the model is making progress, so the panel says how long it has
 * been waiting rather than implying anything about the far end.
 *
 * The pulse is on opacity rather than a spinner because it sits in a text log,
 * and it stops entirely under prefers-reduced-motion - the seconds counting up
 * already carry the information the movement is decorating. */
.chat-line--thinking {
  font-size: 0.85rem;
  color: var(--md-sys-color-on-surface-variant);
  background: none;
  padding: 0.2rem 0.6rem;
  animation: chat-pulse 1.4s ease-in-out infinite;
}

@keyframes chat-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}

@media (prefers-reduced-motion: reduce) {
  .chat-line--thinking { animation: none; }
}

/* The report's version history, folded away above the conversation.
 *
 * Shut by default and quiet when open. It answers a question somebody asks
 * occasionally - "what did that last message actually do to my report" - and a
 * permanent list of it would sit between the conversation and the composer for
 * the whole of every session, which is exactly the complaint that put sharing
 * behind the Actions menu.
 *
 * Typed smaller than the chat and in the muted colour, because it is a record
 * rather than a voice: nobody said any of this, and bubbles would make the
 * agent look like it had spoken twice.
 *
 * The list has a ceiling of its own. The pane is already a column of panes
 * that have to share a fixed height on a wide screen, and a report with forty
 * versions in it would otherwise push the log and the composer off the bottom
 * the moment somebody opened the history out of curiosity. */
.version-history {
  font-size: 0.85rem;
  /* It must not take height from the log when the pane is a flex column: the
     conversation is what the column is for. */
  flex: none;
}

.version-history > summary {
  cursor: pointer;
  display: flex;
  gap: 0.4rem;
  align-items: baseline;
  padding: 0.25rem 0;
  color: var(--md-sys-color-on-surface-variant);
}

.version-history > summary span {
  font-size: 0.8rem;
}

.version-history__list {
  list-style: none;
  margin: 0.2rem 0 0;
  padding: 0;
  max-height: 16rem;
  overflow-y: auto;
}

/* The same left rule the activity lines use, for the same reason: these are
   annotations on the conversation rather than part of it. */
.version-history__item {
  border-left: 2px solid var(--md-sys-color-outline-variant);
  padding: 0.25rem 0 0.35rem 0.6rem;
}

.version-history__item + .version-history__item {
  margin-top: 0.35rem;
}

.version-history__message {
  margin: 0;
  color: var(--md-sys-color-on-surface);
}

.version-history__changes {
  margin: 0.15rem 0 0;
  padding-left: 1.1rem;
  color: var(--md-sys-color-on-surface-variant);
}

.version-history__meta {
  margin: 0.15rem 0 0;
  font-size: 0.75rem;
  color: var(--md-sys-color-on-surface-variant);
  font-variant-numeric: tabular-nums;
}

/* The integrations index.
 *
 * A grid rather than a column: six integrations stacked was a page of
 * scrolling to compare states that are meant to be read at a glance, and a
 * badge you have to scroll to find is a badge nobody checks. `auto-fill` with
 * a minimum rather than a fixed column count, so it becomes one column on a
 * phone without a breakpoint saying so. */
.integration-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
  gap: 1rem;
  align-items: stretch;
}

/* A column inside each card, so the action sits on the bottom edge whatever
 * length the description is - otherwise the buttons wander up and down the row
 * and the eye has to hunt for them. */
.integration-card {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin: 0;
}

.integration-card__head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.integration-card__head .card-title { margin: 0; }

/* The icon is decorative - the name is right beside it - so it is quiet and
 * carries no label of its own. */
.integration-card__head .icon {
  color: var(--md-sys-color-on-surface-variant);
  flex: none;
}

/* The badge to the far end of the head row, so badges line up across a row of
 * cards regardless of how long each name is. */
.integration-card__head .badge { margin-left: auto; }

.integration-card__blurb,
.integration-card__detail {
  margin: 0;
  font-size: var(--md-sys-typescale-body-small-size);
}

/* `auto` top margin is what pins the action to the bottom of the flex column. */
.integration-card__action { margin-top: auto; align-self: flex-start; }

/* ---- settings and integrations ---------------------------------------------
 * What Settings (accounts/templates/accounts/) and the integration pages
 * (integrations/templates/) need beyond the generic components. Each of these
 * replaces a style="" those templates used to re-type page by page. */

/* The settings tabs under a page's own title - Currencies, an integration -
   rather than under "Settings": the title says where you are, the tabs where
   else you can go. */
.page-header > .tab-bar--settings {
  margin-top: var(--md-space-2);
}

/* A title that carries chips beside it as well as actions on the right: the
   title and its chips in `.page-title`, which keeps the heading's face that
   `h1.row-between > span` hands the actions' side. */
h1.row-between > .page-title {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-start;
  gap: var(--md-space-3);
  font: inherit;
  letter-spacing: inherit;
}

/* A card's closing sentence sits on the card's own padding, not on its
   paragraph margin as well. Generic, but found here: the settings cards end
   on a note more often than anything else does. */
.card > p:last-child {
  margin-bottom: 0;
}

/* The two-factor invitation above every page: its buttons wrap under the
   words on a phone rather than squeezing them. */
.mfa-banner,
.mfa-banner > .row-between {
  flex-wrap: wrap;
}

.mfa-banner > p {
  flex: 1 1 16rem;
}

/* The sentence after an info grid, before the card's buttons. */
.detail-grid + .card-note {
  margin-top: var(--md-space-5);
}

/* A row of actions on one thing, where each button may be its own POST form
   (Test connection, Pull now, Disconnect): the forms lay out as if they were
   the buttons. Also the bar a card ends with. */
.action-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-top: var(--md-space-4);
}

.action-row > form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2);
  margin: 0;
}

/* The quiet sentence beside a button - "Re-checks the SKUs first". */
.action-note {
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

/* A form that is only a button, inside a table cell or a sentence. */
.inline-form {
  display: inline-flex;
  align-items: center;
  gap: var(--md-space-2);
  margin: 0;
}

/* The last cell of a row: its actions on one line. */
.cell-actions {
  white-space: nowrap;
}

.cell-actions > * + * {
  margin-left: var(--md-space-2);
}

.nowrap {
  white-space: nowrap;
}

/* A second, quieter line under a cell's value - "by ana@...", a product's
   name under its SKU. */
.cell-sub {
  display: block;
  margin-top: 2px;
  font-size: var(--md-sys-typescale-label-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

/* An identifier somebody reads character by character or copies: a shop
   domain, a SKU, a Xero tenant id, a recovery code. */
.code-id {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--md-sys-typescale-body-small-size);
  overflow-wrap: anywhere;
}

/* The sentence under a warning card's bold first line. */
.notice-detail {
  margin: var(--md-space-1) 0 0;
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

/* A warning inside a card about the thing the card is - "Not a sales order
   yet". The status pastel and a left bar, so it reads as a warning in every
   colour-vision mode and without the colour too. */
.inline-warn {
  margin: 0 0 var(--md-space-4);
  padding: var(--md-space-2) var(--md-space-3);
  border-left: 4px solid var(--md-sys-color-warning);
  border-radius: var(--md-sys-shape-corner-sm);
  background: var(--md-sys-color-warning-container);
  color: var(--md-sys-color-on-warning-container);
  font-size: var(--md-sys-typescale-body-small-size);
}

/* A heading inside a card, under its title: "Give access to every location". */
.card-subhead {
  margin: var(--md-space-6) 0 var(--md-space-2);
  font-size: var(--md-sys-typescale-body-size);
  font-weight: 700;
  color: var(--md-sys-color-on-surface);
}

/* A labelled select in a `.controls` row, with the count it governs beside
   it on the field's baseline. */
.controls > .md-field {
  margin-bottom: 0;
}

.controls__note {
  padding-bottom: var(--md-space-2);
}

/* A field that takes a row of its own inside a .filters row - eight tone
   pills do not fit beside a name. */
.filters > .filters__full {
  flex-basis: 100%;
}

/* A text box that grows beside its button: an abandon reason, a shop address,
   a shop SKU. */
.inline-fields {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2);
  margin: var(--md-space-2) 0 0;
}

.inline-fields > input[type="text"] {
  flex: 1 1 16rem;
  max-width: 28rem;
}

.inline-fields input[type="number"] {
  width: 5rem;
}

/* Field grids with a narrower floor than the default 220px - the letterhead's
   phone / email / website, a country beside its VAT number. */
.form-grid--narrow {
  --form-col: 13rem;
}

.form-grid--medium {
  --form-col: 16rem;
}

/* Checkboxes one per line: the roles a user holds, a group's permissions. */
.choice-list {
  display: grid;
  gap: var(--md-space-2);
  margin: 0 0 var(--md-space-4);
}

.choice-list label {
  display: flex;
  align-items: baseline;
  gap: var(--md-space-2);
}

td > .choice-list {
  margin: 0;
}

/* The permission groups' bulk buttons. */
.perm-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2);
  margin-bottom: var(--md-space-3);
}

/* A stored or about-to-be-stored letterhead image: on paper, because that is
   what it prints on, whatever the theme. */
.stored-image {
  display: block;
  max-width: 16rem;
  max-height: 5rem;
  padding: var(--md-space-2);
  background: var(--md-sys-color-paper);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-xs);
}

.stored-image--short {
  max-height: 3rem;
}

/* The narrow column the sign-in steps sit in: change password, the two-factor
   code, accepting an invite. */
.auth-card {
  max-width: 26rem;
  margin: var(--md-space-8) auto 0;
}

.auth-card h1 {
  margin-top: 0;
  font-size: var(--md-sys-typescale-headline-small-size);
}

.auth-card__form {
  display: flex;
  flex-direction: column;
  gap: var(--md-space-4);
}

.auth-card__form .md-field {
  margin: 0;
}

.auth-card__form button[type="submit"] {
  width: 100%;
}

.auth-card__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2);
  margin: var(--md-space-4) 0 0;
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

.auth-card__foot form {
  margin: 0;
}

/* Two-factor set-up: the QR code beside the key for an app that cannot scan.
   The code is on paper in both themes - a scanner wants dark on light. */
.mfa-setup {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--md-space-6);
}

.mfa-qr {
  width: 12rem;
  max-width: 100%;
  padding: var(--md-space-2);
  background: var(--md-sys-color-paper);
  border-radius: var(--md-sys-shape-corner-sm);
}

.mfa-qr svg {
  display: block;
  width: 100%;
  height: auto;
}

.mfa-setup__key {
  flex: 1;
  min-width: 14rem;
}

.mfa-setup__key .code-id,
.recovery-codes .code-id {
  font-size: var(--md-sys-typescale-body-large-size);
  letter-spacing: 0.05em;
}

.recovery-codes {
  columns: 2;
  margin: 0;
  padding: 0;
  list-style: none;
  line-height: 2;
}

/* An occasional action or a detail folded under a quiet line: "Connect
   another account", "3 recorded attempts". The summary is a link in look, so
   it reads as something to open rather than as a caption. */
.disclosure {
  margin-top: var(--md-space-4);
}

.disclosure > summary {
  cursor: pointer;
  width: fit-content;
  font-size: var(--md-sys-typescale-body-small-size);
  font-weight: 600;
  color: var(--md-sys-color-primary);
}

.disclosure__body,
.disclosure > summary + * {
  margin-top: var(--md-space-3);
}

/* What became of an Amazon purchase order: its chip, the reason and the
   button that acts on it, on one wrapping line. */
.order-outcome {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2);
  margin: 0 0 var(--md-space-4);
}

/* A bulleted list in a table cell - what a Xero flow still needs. */
.cell-list {
  margin: 0;
  padding-left: var(--md-space-4);
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

/* A table that follows something else in the same card. */
.table-gap {
  margin-top: var(--md-space-4);
}

/* The report workbench's Actions menu.
 *
 * Sharing, labelling, archiving and deleting behind one button, top right. They
 * were a panel below the document, which put a block of controls somebody uses
 * once between the report and the conversation for the whole of every session.
 *
 * Positioned against the page header, so it sits with the title rather than
 * floating over the report. */
.report-menu { position: relative; margin-left: auto; }

.report-menu__panel {
  position: absolute;
  right: 0;
  z-index: 20;
  min-width: 20rem;
  max-width: min(24rem, calc(100vw - 2rem));
  margin-top: var(--md-space-1);
  padding: var(--md-space-1);
  background: var(--md-sys-color-surface-container-high);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-panel);
  box-shadow: var(--md-sys-elevation-3);
}

.report-menu__group {
  display: flex;
  flex-direction: column;
  gap: var(--md-space-2);
  padding: var(--md-space-3);
}

/* A field and its button, or a pair of buttons, on one line: a menu of
   stacked full-width buttons reads as a form to fill in from the top. Every
   button here is the neutral one - nothing in the menu is the page's main
   action. */
.report-menu__row,
.report-menu__group.report-menu__row {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2);
}

.report-menu__row input {
  flex: 1 1 8rem;
  min-width: 0;
}

/* A rule between groups rather than around each: three bordered boxes in a
 * dropdown reads as three dropdowns. */
.report-menu__group + .report-menu__group {
  border-top: 1px solid var(--md-sys-color-outline-variant);
}

/* A group's name: the design's field label - 12px semibold in the accent,
   sentence case. It was uppercase micro-caps, which the design retired. */
.report-menu__label {
  margin: 0;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  letter-spacing: var(--md-sys-typescale-label-small-tracking);
  color: var(--md-sys-color-primary);
}

.report-menu__status {
  margin: 0;
  font-size: var(--md-sys-typescale-body-small-size);
  word-break: break-all;
}

.report-menu__shares {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--md-sys-typescale-body-small-size);
}
.report-menu__shares li {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  flex-wrap: wrap;
  padding: var(--md-space-1) 0;
}
.report-menu__shares li + li {
  border-top: 1px solid var(--md-sys-color-outline-variant);
}

/* The one action that cannot be undone, coloured as such. Everything else in
 * this menu is reversible and is not. A neutral button with the error's edge
 * and words rather than a red fill, so it does not shout from a menu that is
 * mostly reversible things - and the word says what it does either way. */
.report-menu__danger {
  background: transparent;
  color: var(--md-sys-color-error);
  border-color: var(--md-sys-color-error);
}

/* ---- reports and data quality ---------------------------------------------
 * The few things the Reports pages, the public link to a report and Data
 * quality need that no generic component gives. The scripted report's own
 * layout is reports/static/reports/css/script_editor.css; this is what is
 * shared with the pages that do not load it. */

/* A report's labels, under the title they belong to rather than in a column
   of their own: they are what the title is filed under. */
.report-list__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--md-space-1);
  margin-top: var(--md-space-1);
}

/* The question on the title's line, where a list's "New ..." button sits:
   wide enough to read a question as it is typed, not the whole row. */
.report-ask input[type="text"] {
  width: min(26rem, 60vw);
}

/* On a phone the title row wraps, and the question gets a line of its own
   with the button beside it rather than a stub of a field pushed right. */
@media (max-width: 880px) {
  .report-ask {
    flex: 1 1 100%;
    justify-content: flex-start;
  }

  .report-ask input[type="text"] {
    flex: 1 1 10rem;
    width: auto;
  }
}

/* What the tenant is told about the agent's allowance (_allowance.html). The
   words take whatever is left of the banner's row. */
.allowance-banner__body {
  flex: 1;
}

.allowance-banner__note {
  margin: var(--md-space-1) 0 0;
  font-size: var(--md-sys-typescale-body-small-size);
  font-weight: 400;
}

.allowance-banner__ask {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-3);
  margin-top: var(--md-space-3);
}

.allowance-banner__ask .allowance-banner__note {
  margin: 0;
}

/* A public link to a report (shared_public.html): no shell, so the page sheet
   is the body, and its header does not pin - there is nothing to scroll past
   it to. */
body.shared-report {
  background: var(--md-sys-color-surface);
  color: var(--md-sys-color-on-surface);
}

.shared-report__sheet {
  max-width: 72rem;
  margin: 0 auto;
  padding: var(--md-space-8) var(--md-space-4);
}

.shared-report .page-header {
  position: static;
  margin: 0 0 var(--md-space-5);
  padding: 0;
}

/* Data quality: the heading over the cards of findings, set apart from the
   overview above it as the dashboard sets "Trends & performance" apart. */
.data-quality__heading {
  margin: var(--md-space-6) 0 var(--md-space-4);
}

.data-quality__areas .card-title {
  margin-bottom: var(--md-space-3);
}

/* ---- global search ------------------------------------------------------ */

/* The box at the top of the sidebar. `position: relative` is load bearing: the
   suggestion panel is absolute against this, and the sidebar itself scrolls its
   nav - anchoring to that would slide the panel away from the box. */
.search-box {
  position: relative;
}

.search-box__form {
  position: relative;
  display: flex;
  align-items: center;
}

/* Inside the field rather than beside it, so the field itself is as wide as the
   sidebar allows - 15rem is not much to search in. */
.search-box__form .icon {
  position: absolute;
  left: var(--md-space-2);
  width: 16px;
  height: 16px;
  color: var(--md-sys-color-on-surface-variant);
  pointer-events: none;
}

.search-box__form input[type="search"] {
  width: 100%;
  /* Overrides the 9rem floor every other field carries: this one is inside a
     15rem rail, and a minimum wider than its container would push out of it. */
  min-width: 0;
  padding-left: calc(var(--md-space-3) + 16px);
  padding-right: 2rem;
  font-size: var(--md-sys-typescale-body-small-size);
}

/* The shortcut hint. Muted and small enough to be scenery until wanted, and out
   of the way of the text: pointer-events off so clicking it focuses the field
   underneath rather than nothing. */
.search-box__key {
  position: absolute;
  right: var(--md-space-2);
  padding: 1px 5px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-xs);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--md-sys-typescale-label-small-size);
  color: var(--md-sys-color-on-surface-variant);
  pointer-events: none;
}

/* Out of the way once there is something to clear: the browser puts its own
   clear button in that corner, and the two together read as clutter. The hint
   has done its job by then anyway - the field is already focused. */
.search-box__form input[type="search"]:not(:placeholder-shown) ~ .search-box__key {
  display: none;
}

/* Hidden until search.js says otherwise - which it does only once a swap has
   actually produced rows, so there is never an empty panel hanging open. */
.search-panel {
  display: none;
  position: absolute;
  top: 100%;
  left: var(--md-space-3);
  /* Wider than the rail it hangs off, because order numbers and product names
     do not fit in 15rem. Capped against the viewport so it cannot run off the
     side of a phone, where the sidebar is a drawer over the page. */
  width: min(24rem, calc(100vw - 2rem));
  max-height: min(28rem, 70vh);
  overflow-y: auto;
  background: var(--md-sys-color-surface-container-high);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-sm);
  padding: var(--md-space-2);
  /* Above the sidebar's own stacking, and above the topbar on a narrow screen
     where the drawer sits at 40. */
  z-index: 50;
  box-shadow: var(--md-sys-elevation-2);
}

.search-box.is-open .search-panel {
  display: block;
}

.search-panel__note,
.search-panel__all {
  display: block;
  padding: var(--md-space-2) var(--md-space-3);
  font-size: var(--md-sys-typescale-body-small-size);
}

.search-panel__all {
  border-top: 1px solid var(--md-sys-color-outline-variant);
  margin-top: var(--md-space-1);
  color: var(--md-sys-color-primary);
  text-decoration: none;
}

.search-panel__all:hover {
  text-decoration: underline;
}

.search-group + .search-group {
  margin-top: var(--md-space-3);
}

/* The heading is a label for a set, not a title somebody reads - the design's
   field label (12px semibold, accent, sentence case), like the tenant name in
   the profile menu. */
.search-group__label {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  margin: 0 0 var(--md-space-1);
  padding: 0 var(--md-space-2);
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  letter-spacing: var(--md-sys-typescale-label-small-tracking);
  color: var(--md-sys-color-primary);
}

.search-group__label .icon {
  width: 14px;
  height: 14px;
}

.search-hits {
  list-style: none;
  margin: 0;
  padding: 0;
}

.search-hits a {
  display: flex;
  align-items: baseline;
  gap: var(--md-space-2);
  padding: var(--md-space-2) var(--md-space-3);
  border-radius: var(--md-sys-shape-corner-sm);
  color: var(--md-sys-color-on-surface);
  text-decoration: none;
}

.search-hits a:hover,
.search-hits a:focus-visible {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-surface) var(--md-sys-state-hover-opacity),
    transparent
  );
}

/* The code, in the letterforms a code is read in - these get retyped and read
   aloud over a phone, so 0 and O have to be different shapes. */
.search-hit__title {
  flex: 0 0 auto;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--md-sys-typescale-body-small-size);
}

/* Takes the slack and truncates, so a long product name cannot widen the panel
   or wrap a row into two. */
.search-hit__subtitle {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

/* Why the row matched, when that was an EAN or an ASIN rather than anything
   else on the row. Tinted rather than muted: it is the answer to "why is this
   here", so it has to survive a glance. */
.search-hit__hint {
  /* Does not yield: the subtitle above it does. In a 24rem panel the two
     compete for the same slack, and shrinking this one first truncated "EAN
     5025452001868" to "EAN ..." - dropping the only part of the row that
     explains why it is in the results at all.

     Still capped and clipped, because a tenant may keep a whole trading name in
     a partner's code field - and one does. Written code-first, so what survives
     the ellipsis is the code that matched. */
  flex: 0 0 auto;
  max-width: 11rem;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  padding: 1px var(--md-space-2);
  border-radius: var(--md-sys-shape-corner-xs);
  background: var(--md-sys-color-secondary-container);
  color: var(--md-sys-color-on-secondary-container);
  font-size: var(--md-sys-typescale-label-small-size);
}

.search-group__more {
  margin: var(--md-space-1) 0 0;
  padding: 0 var(--md-space-3);
  font-size: var(--md-sys-typescale-label-small-size);
}

/* The results page. The field is the page's own, so a linked-to search can be
   corrected where it is read. */
.search-page__form {
  display: flex;
  gap: var(--md-space-2);
  align-items: center;
}

.search-page__form input[type="search"] {
  flex: 1 1 auto;
}

/* ---- signed-out pages, error pages, the landing page, the platform ---------
 * The pages around the shell: what somebody sees before they are signed in,
 * when something has gone wrong, on the bare host, and on the platform
 * console. None of them is mocked; each is built from the shell's own parts -
 * the photograph, the wordmark, the heading face, a tile, the library's
 * fields and buttons. */

/* Signed out (base.html adds the class): no sidebar to sit beside, so the
   sheet steps aside - no fill, no corners, the whole window - and the page's
   own card stands on the photograph. On a phone too: the photograph is the
   one thing that makes a lone form look like this app. */
.app-shell--signed-out .app-main {
  margin: 0;
  height: 100%;
  background: transparent;
  border-radius: 0;
}

/* The sign-in's column: the wordmark over one tile, centred in the window. */
.auth {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--md-space-5);
  min-height: calc(100dvh - 2 * var(--main-pad-y));
}

/* The same card inside a signed-in sheet (an expired password on the
   platform) is not alone in the window, so it is not centred in it. */
.auth--in-sheet {
  min-height: 0;
  padding-top: var(--md-space-8);
}

/* The wordmark at the size of a page title: on the sign-in it is the page's
   only furniture, and at the sidebar's 18px it read as a caption. */
.auth__brand {
  display: flex;
  align-items: center;
  gap: var(--md-space-3);
  margin: 0;
  font-size: var(--md-sys-typescale-headline-size);
}

/* The card's title with the sun/moon switch on its right (_theme_toggle.html). */
.auth-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--md-space-3);
  margin-bottom: var(--md-space-4);
}

.auth-card__head h1 {
  margin: 0;
}

/* Under the wordmark in the centred column, the card is placed by the column
   rather than by its own margin. */
.auth > .auth-card {
  width: min(26rem, 100%);
  margin: 0;
}

.auth-card > .field-errors {
  margin: 0 0 var(--md-space-4);
}

/* The shell's announcements (_page_notices.html), which {% place_notices %}
   puts under the page's back link, else at the top of its header, else above
   the content. A compact strip each, in the instruction banner's colours -
   a warning keeps its 2px warning edge (.card--warn) so the two differ in
   shape as well as colour - with the action on the right and wrapping under
   the words on a phone. */
.page-notices {
  display: flex;
  flex-direction: column;
  gap: var(--md-space-2);
  margin: 0 0 var(--md-space-4);
}

.page-notices > .card,
.page-notices > .hint-banner,
.page-notices > .mfa-banner {
  margin: 0;
}

.page-notices > .card {
  padding: 10px 17px 10px var(--md-space-5);
  border-radius: var(--md-sys-shape-corner-panel);
  box-shadow: none;
  color: var(--md-sys-color-on-hint-container);
}

.page-notices > .card:not(.card--warn) {
  background: var(--md-sys-color-hint-container);
  border: 1px solid var(--md-sys-color-hint-outline);
}

.page-notices > .card .muted {
  color: var(--md-sys-color-on-hint-container);
}

.recovery-notice {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--md-space-1) var(--md-space-3);
}

.recovery-notice__reason {
  font-size: var(--md-sys-typescale-body-small-size);
}

/* errors/403.html and 404.html, and the standalone 400, CSRF and 500 pages:
   one sentence in a tile. */
.error-card > p {
  margin: 0;
}

.error-card > p + p {
  margin-top: var(--md-space-3);
}

.error-card > .form-actions {
  margin-top: var(--md-space-5);
}

/* The public landing page (urls_public.landing): the photograph, the
   wordmark and a status chip along the top, one sheet with the pitch and the
   four tiles, and a quiet footer. It scrolls the document - it is not the app
   shell - so the photograph is set here rather than inherited. */
body.landing {
  min-height: 100vh;
  padding: 0 var(--md-space-7);
  background-color: var(--md-sys-color-surface);
  background-image: var(--app-backdrop-light);
  background-size: cover;
  background-position: center;
  background-attachment: fixed;
}

:root[data-theme="dark"] body.landing {
  background-image: var(--app-backdrop-dark);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) body.landing {
    background-image: var(--app-backdrop-dark);
  }
}

.landing__bar,
.landing__sheet,
.landing__foot {
  max-width: 72rem;
  margin: 0 auto;
}

.landing__bar,
.landing__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--md-space-3);
  padding: var(--md-space-6) var(--md-space-2);
}

.landing__brand {
  font-size: var(--md-sys-typescale-headline-small-size);
}

.landing__sheet {
  padding: var(--md-space-8) var(--md-space-8) var(--md-space-6);
  background: var(--md-sys-color-surface);
  border-radius: var(--md-sys-shape-corner-page);
  box-shadow: var(--md-sys-elevation-1);
}

.landing__hero {
  max-width: 44rem;
  margin: 0 auto var(--md-space-8);
  text-align: center;
}

.landing__hero h1 {
  margin: 0 0 var(--md-space-4);
  font-size: clamp(2rem, 5vw, 3.25rem);
  line-height: 1.05;
}

.landing__eyebrow {
  margin: 0 0 var(--md-space-3);
  color: var(--md-sys-color-primary);
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
}

.landing__lede {
  margin: 0 0 var(--md-space-6);
  font-size: var(--md-sys-typescale-body-large-size);
  color: var(--md-sys-color-on-surface-variant);
}

.landing__workspace {
  display: flex;
  align-items: flex-start;
  gap: var(--md-space-4);
  text-align: left;
}

.landing__workspace > .icon {
  width: 22px;
  height: 22px;
  margin-top: 2px;
  color: var(--md-sys-color-heading);
}

.landing__workspace .card-title {
  margin-bottom: var(--md-space-1);
}

.landing__workspace p,
.landing__grid p {
  margin: 0;
}

.landing__features > h2,
.landing__features > p {
  margin: 0 0 var(--md-space-2);
  text-align: center;
}

.landing__features > p {
  margin-bottom: var(--md-space-6);
}

.landing__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--md-space-4);
}

.landing__grid .card {
  margin: 0;
}

.landing__foot {
  color: var(--md-sys-color-on-surface);
  font-size: var(--md-sys-typescale-body-small-size);
}

@media (max-width: 880px) {
  body.landing {
    padding: 0 var(--md-space-3);
  }

  .landing__sheet {
    padding: var(--md-space-6) var(--md-space-4);
  }
}

/* The platform console (tenants/templates/platform/base.html): the tenant
   shell's sidebar and sheet, made unmistakably somewhere else. The same sky
   with its colour drained out (the image's light, the surface's hue - which is
   near enough none), a "Platform" tag on the wordmark, a line saying no tenant
   is open, and bare glyphs where a tenant's nav has coloured section tiles.
   Three of the four are shape and words, so it holds in every colour-vision
   mode. The selector is :root-qualified to outweigh the theme's own photo
   rules above, which name `body.app-shell` the same way. */
:root body.app-shell.platform {
  background-blend-mode: luminosity;
}

/* Black on paper by day, paper on black by night: the strongest pill the
   tokens have, and nothing a tenant page ever shows. */
.platform-tag {
  display: inline-flex;
  align-items: center;
  padding: 1px var(--md-space-2);
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-inverse-surface);
  color: var(--md-sys-color-inverse-on-surface);
  font-family: var(--md-sys-typescale-font);
  font-size: var(--md-sys-typescale-caption-size);
  font-weight: 700;
  line-height: 16.5px;
  letter-spacing: 0.02em;
  word-spacing: normal;
  white-space: nowrap;
}

.platform .sidebar-head {
  justify-content: flex-start;
}

/* Signed out there is no sidebar, so the wordmark and tag sit where its head
   would, over the photograph and out of the centred card's way. */
.platform-signed-out-mark {
  position: absolute;
  top: var(--md-space-4);
  left: var(--md-space-6);
  z-index: 1;
}

.platform-where {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  margin: 0;
  padding: var(--md-space-2) var(--md-space-3);
  border: 1px dashed var(--md-sys-color-nav-divider);
  border-radius: var(--md-sys-shape-corner-md);
  color: var(--md-sys-color-on-surface);
  font-size: var(--md-sys-typescale-body-small-size);
  font-weight: 500;
}

.platform .sidebar .nav-link .icon,
.platform-where .icon {
  width: 18px;
  height: 18px;
  color: var(--md-sys-color-heading);
}

.platform .topbar .platform-tag {
  margin-left: auto;
}

.platform-message > p,
.platform .app-main .card > p:only-child {
  margin: 0;
}

/* A row's actions, right-aligned in the last cell: links, ghost buttons, the
   odd status chip, and a POST form or two among them. Generic - the platform's
   tables and the shared reference-data list use it - and the replacement for
   the `style="white-space:nowrap"` cell and `style="display:inline;
   margin-left:0.5rem"` form that dozens of templates hand-roll.
   `.row-actions` is a wrapper inside the cell; `.cell-actions` the cell
   itself, for a cell whose children are inline. */
.row-actions {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--md-space-2);
}

.row-actions form,
.cell-actions form {
  margin: 0;
}

/* `.inline-form` and `.cell-actions` themselves are the settings section's
   (#706); this only right-aligns a row's last-cell actions. */
td.cell-actions:last-child {
  text-align: right;
}

td:has(> .row-actions) {
  text-align: right;
}

/* In a cell they stay on one line: a phone scrolls the table sideways
   rather than stacking the buttons into a row three times as tall. */
td > .row-actions {
  flex-wrap: nowrap;
}

/* The console's own pieces, shared by its pages. */

/* A schema name or a job's name under the thing it belongs to. */
.platform-code {
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: 400;
}

/* The checkbox column of the features table: as narrow as the box. */
.platform-tick {
  width: 2rem;
}

/* A block that follows a card or a form rather than sitting in one. */
.platform-follow {
  margin-top: var(--md-space-4);
}

/* A tenant's own words, quoted under their request. */
.platform-quote {
  margin: var(--md-space-3) 0 0;
  padding-left: var(--md-space-3);
  border-left: 2px solid var(--md-sys-color-hint-outline);
}

/* The state line on Settings: a status chip and the sentence about it. */
.platform-state {
  margin: 0 0 var(--md-space-2);
}

/* Said in the error ink, always beside words that say it failed. */
.platform-error {
  color: var(--md-sys-color-error);
}

p.platform-error {
  margin: 0;
}

/* Tenant cost: the rest of a tenant's largest tables, under the biggest. */
.platform-tables {
  margin: var(--md-space-2) 0 0;
  padding: 0;
  list-style: none;
}

/* The layout editor: source and preview side by side, the whole width, the
   split dragged by the person (the splitter sets --split, kept with the
   editor's other settings so it holds between visits). */
.layout-editor {
  display: grid;
  grid-template-columns: minmax(20rem, var(--split, 50%)) 8px minmax(20rem, 1fr);
  gap: 0 var(--md-space-2);
  align-items: start;
}

.layout-splitter {
  cursor: col-resize;
  align-self: stretch;
  border-radius: var(--md-sys-shape-corner-xs);
  background: var(--md-sys-color-outline-variant);
  touch-action: none;
}

.layout-splitter:hover,
.layout-splitter.is-dragging {
  background: var(--md-sys-color-primary);
}

@media (max-width: 1100px) {
  .layout-editor {
    grid-template-columns: 1fr;
  }

  .layout-splitter {
    display: none;
  }
}

.layout-editor .code-editor,
.layout-editor textarea {
  height: calc(100vh - 17rem);
  min-height: 24rem;
}

.layout-source {
  width: 100%;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

/* The preview stays white whatever the page theme: it is a sheet of paper,
   and the point of looking at it is to see what will print. */
.layout-preview {
  height: calc(100vh - 17rem);
  min-height: 24rem;
  width: 100%;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-sm);
  background: var(--md-sys-color-paper);
}

.layout-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2);
  margin-bottom: var(--md-space-2);
}

.layout-toolbar--foot {
  margin: var(--md-space-2) 0 0;
}

.layout-toolbar label {
  display: flex;
  align-items: center;
  gap: var(--md-space-1);
}

.layout-note {
  flex: 1;
  min-width: 16rem;
}

.layout-error {
  white-space: pre-wrap;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--md-sys-typescale-body-small-size);
}

/* A layout's history: the server's line diff, until Monaco's replaces it. */
.layout-diff-editor {
  height: 70vh;
}

.diff {
  width: 100%;
  border-collapse: collapse;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--md-sys-typescale-label-small-size);
}

.diff td {
  padding: 0 var(--md-space-2);
  white-space: pre;
  vertical-align: top;
}

.diff .num {
  width: 3rem;
  color: var(--md-sys-color-on-surface-variant);
  text-align: right;
  user-select: none;
}

.diff .add {
  background: color-mix(in srgb, var(--md-sys-color-success) 18%, transparent);
}

.diff .del {
  background: color-mix(in srgb, var(--md-sys-color-error) 18%, transparent);
}

.diff .hunk {
  color: var(--md-sys-color-on-surface-variant);
  background: var(--md-sys-color-surface-container-high);
}

/* The manual for writing a layout. */
.ref code {
  font-size: var(--md-sys-typescale-body-small-size);
}

.ref td {
  vertical-align: top;
}

.ref .path {
  white-space: nowrap;
}

/* A product's handling labels (_product_labels.html): a row of tone chips
   that wraps beside whatever it follows. */
.label-pills {
  display: inline-flex;
  flex-wrap: wrap;
  gap: var(--md-space-1);
  vertical-align: middle;
}

/* ---- the shared record cards (common/templates/common) --------------------
 * Files, Comments and History sit on most record pages. Built from the tile
 * and its title; what is here is only what they add. */

/* A second part of a card under a rule the card's full width - the product
   mock's "Add images & attachments" under its gallery. Generic: any card
   whose add-form follows its list. The negative margins are the card's own
   padding (.card above). */
.card-section {
  margin: var(--md-space-6) calc(-1 * var(--md-space-7)) 0;
  padding: var(--md-space-5) var(--md-space-7) 0;
  border-top: 1px solid var(--md-sys-color-outline-variant);
}

.card-section__title {
  margin: 0 0 var(--md-space-3);
  font-family: var(--md-sys-typescale-display-font);
  font-size: var(--md-sys-typescale-stat-label-size);
  font-weight: var(--md-sys-typescale-title-weight);
  color: var(--md-sys-color-heading);
}

/* The one line a card shows when it has nothing to list. */
.card-empty {
  margin: 0;
  color: var(--md-sys-color-on-surface-variant);
}

.attachments-card__form {
  align-items: end;
}

/* A file picker's button, as the mock's "Choose File": the Neutral button,
   with "No file chosen" beside it in the quiet ink. Generic - every upload
   field in the app is a bare <input type="file">. */
input[type="file"] {
  font: inherit;
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

input[type="file"]::file-selector-button {
  margin-right: var(--md-space-3);
  padding: 6px var(--md-space-4);
  border: 1px solid var(--md-sys-color-outline);
  border-radius: var(--md-sys-shape-corner-sm);
  background: transparent;
  color: var(--md-sys-color-on-surface);
  font: inherit;
  font-size: var(--md-sys-typescale-label-size);
  font-weight: var(--md-sys-typescale-label-weight);
  cursor: pointer;
}

input[type="file"]::file-selector-button:hover {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-surface) var(--md-sys-state-hover-opacity),
    transparent
  );
}

.history-card__when {
  width: 10rem;
}

.history-card__who {
  width: 14rem;
}

/* Comments: a list of who-and-when over what they said, a hairline between. */
.note-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.note + .note {
  margin-top: var(--md-space-4);
  padding-top: var(--md-space-4);
  border-top: 1px solid var(--md-sys-color-outline-variant);
}

.note__meta {
  margin: 0 0 var(--md-space-1);
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  color: var(--md-sys-color-primary);
}

.note__body > p {
  margin: 0 0 var(--md-space-2);
}

.note__body > p:last-child {
  margin-bottom: 0;
}

/* A sales line's comment, under the product it is about. */
.line-comment {
  margin-top: var(--md-space-1);
}

.row--error-note .field-errors {
  margin: 0;
}

/* ---- search results ------------------------------------------------------
 * The results page draws each group as a tile with its own title, where the
 * sidebar's dropdown keeps them as plain stacked lists (same partial). */
.search-results {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
  gap: var(--md-space-4);
  align-items: start;
}

.search-results .search-group {
  padding: var(--md-space-5) var(--md-space-5) var(--md-space-4);
  background: var(--md-sys-color-surface-container-low);
  border-radius: var(--md-sys-shape-corner-tile);
  box-shadow: var(--md-sys-elevation-1);
}

.search-results .search-group + .search-group {
  margin-top: 0;
}

.search-results .search-group__label {
  margin-bottom: var(--md-space-2);
  padding: 0;
  font-family: var(--md-sys-typescale-display-font);
  font-size: var(--md-sys-typescale-title-size);
  font-weight: var(--md-sys-typescale-title-weight);
  letter-spacing: 0;
  color: var(--md-sys-color-heading);
}

.search-results .search-group__label .icon {
  width: 18px;
  height: 18px;
}

.search-results .search-hits a {
  padding-left: var(--md-space-2);
  padding-right: var(--md-space-2);
}

.search-results .search-hit__subtitle {
  white-space: normal;
}

/* ---- the platform's delivery console --------------------------------------
 * One <details> per tenant, so a fleet is a page somebody can actually stand in
 * front of rather than a wall of tables. Native disclosure, no library: the
 * open/closed state is a browser affordance, it survives find-in-page, and the
 * default-open rule is decided on the server where the states already are.
 *
 * `summary.card-title` above already does the marker and the spacing. What is
 * here is only what state looks like.
 */

/* A left stripe as well as the pill. The pill is a colour and a word, which is
 * two, but both are inside the row; the stripe is what lets somebody scanning a
 * column of twenty collapsed sections see where the trouble is without reading
 * any of them. Width in px, not a token: it is a rule, not a spacing step. */
.delivery-tenant {
  border-left: 4px solid var(--md-sys-color-outline-variant);
}

.delivery-tenant--overdue,
.delivery-tenant--unreadable {
  border-left-color: var(--md-sys-color-error);
}

.delivery-tenant--failed {
  border-left-color: var(--md-sys-color-warning);
}

.delivery-tenant--healthy {
  border-left-color: var(--md-sys-color-primary);
}

/* Counts on the summary row, and the times in the table under it. Tabular so a
 * column of them lines up on the digit rather than on the glyph - the whole
 * reason to put figures in a column at all. */
.delivery-counts,
.delivery-when,
.delivery-console .kpi-value {
  font-variant-numeric: tabular-nums;
}

.delivery-counts {
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
  font-weight: 400;
  /* To the far end of the summary row, so the counts line up across sections
     whatever length the tenant names are. */
  margin-left: auto;
}

.delivery-when {
  white-space: nowrap;
}

/* A schedule this tenant was never granted. Muted rather than dropped: silence
 * is how this page lies, and a shorter table cannot be told from a forgotten
 * one. Held at the same columns as the rest so it still reads as a schedule. */
tr.delivery-row--muted td {
  color: var(--md-sys-color-on-surface-variant);
}

/* The last run's own account of itself, under its schedule row. A one-line
 * summary that opens onto the captured log: monospace, wrapped rather than
 * scrolled sideways, and capped in height so a chatty run does not push the
 * next tenant off the screen. */
tr.delivery-run td {
  padding-top: 0;
  border-top: 0;
}

.delivery-run-log summary {
  cursor: pointer;
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

.delivery-run-log pre {
  margin: 0.5rem 0 0;
  padding: 0.6rem 0.8rem;
  max-height: 24rem;
  overflow: auto;
  font-size: 0.72rem;
  line-height: 1.45;
  white-space: pre-wrap;
  word-break: break-word;
  background: var(--md-sys-color-surface-container);
  border-radius: 6px;
}

/* The measure, not the console. The shell is deliberately wide for the tables;
 * a warning banner or a form set 1500px across is a different thing, and one
 * nobody can read. Capped where the sentence is, so the table beside it still
 * gets the glass. */
.platform .app-main .card > p,
.platform .app-main .card-note {
  max-width: 70ch;
}

.platform .app-main .card > form {
  max-width: 40rem;
}

/* ---- reorder plan --------------------------------------------------------
 *
 * The plan (#441) is the widest table in the app and the only one whose rows
 * open. Four things it needs that nothing else does: a bar that compares two
 * weeks figures, a row of glyph flags, a drawer under a row, and a supplier
 * group header carrying its own subtotal. Everything else on the screen is the
 * app's own card, table, badge, kpi-strip and pager.
 *
 * Tokens only, per the top of this file.
 */

.plan-table td {
  vertical-align: top;
}

/* The SKU is what gets retyped into a purchase order, so it carries the weight
   and the name sits under it rather than beside it: a column wide enough for
   both side by side would be a column of mostly whitespace. */
.plan-sku {
  font-weight: var(--md-sys-typescale-label-weight);
}

.plan-desc,
.plan-small {
  display: block;
  font-size: var(--md-sys-typescale-label-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

.plan-small {
  display: inline;
}

.plan-desc {
  max-width: 19rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ---- the cover meter -----------------------------------------------------
 *
 * The fill is weeks of cover, against how long a new order takes. Reading them
 * against each other is the whole point - "4 weeks of stock" means nothing
 * until you know whether a new one takes a fortnight or seven months.
 *
 * A native <meter> (#705): `low` is the lead time, `high` lead plus safety and
 * `optimum` the top of the scale, so the browser itself picks the segment -
 * under the lead (even-less-good: error), inside the safety margin
 * (suboptimum: warning), clear of it (optimum: primary). No inline width, and
 * the weeks are printed beside it, so colour is never the only reading.
 * Each engine names the parts differently, hence the two sets of rules.
 */

.plan-cover {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  white-space: nowrap;
}

.plan-cover-meter {
  appearance: none;
  display: block;
  flex: none;
  width: 72px;
  height: 10px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-surface-container);
  overflow: hidden;
}

.plan-cover-meter::-webkit-meter-bar {
  height: 100%;
  border: 0;
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-surface-container);
}

.plan-cover-meter::-webkit-meter-optimum-value {
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-primary);
}

.plan-cover-meter::-webkit-meter-suboptimum-value {
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-warning);
}

.plan-cover-meter::-webkit-meter-even-less-good-value {
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-error);
}

.plan-cover-meter::-moz-meter-bar {
  border-radius: var(--md-sys-shape-corner-full);
  background: var(--md-sys-color-primary);
}

.plan-cover-meter:-moz-meter-sub-optimum::-moz-meter-bar {
  background: var(--md-sys-color-warning);
}

.plan-cover-meter:-moz-meter-sub-sub-optimum::-moz-meter-bar {
  background: var(--md-sys-color-error);
}

/* ---- flags ---------------------------------------------------------------
 *
 * A row carries up to five of these and five words would be a paragraph in a
 * table cell, so each is one glyph with its sentence in the `title`. Bordered
 * rather than filled: they sit beside a status pill, and two filled shapes in
 * one row would compete for the same glance.
 */

.plan-flags {
  white-space: nowrap;
}

.plan-flag {
  display: inline-block;
  width: 18px;
  height: 18px;
  margin-right: 2px;
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-xs);
  background: var(--md-sys-color-surface-container-low);
  color: var(--md-sys-color-on-surface-variant);
  font-size: var(--md-sys-typescale-label-small-size);
  line-height: 16px;
  text-align: center;
}

.plan-flag.warn {
  border-color: var(--md-sys-color-warning);
  color: var(--md-sys-color-warning);
}

.plan-flag.danger {
  border-color: var(--md-sys-color-error);
  color: var(--md-sys-color-error);
}

/* ---- the drawer ----------------------------------------------------------
 *
 * The detail a buyer wants about the one row they are deciding on, not the
 * fifty they are scanning. Four columns because the eight pairs in it fall
 * naturally into two reading rows, and a two-column list would run off the
 * bottom of the screen for a row near the fold.
 */

.plan-row {
  cursor: pointer;
}

/* On the cells, as the table's own hover is: a strip paints its cells, so a
   colour on the <tr> never showed. The open row and its drawer read as one
   strip - the row keeps the hover tint, loses its bottom corners, and the
   drawer under it carries on in the strip's fill with the matching ones. */
.plan-row.is-open > td {
  background: color-mix(
    in srgb,
    var(--md-sys-color-on-surface) 5%,
    var(--row-fill)
  );
}

.plan-row.is-open > td:first-child {
  border-bottom-left-radius: 0;
}

.plan-row.is-open > td:last-child {
  border-bottom-right-radius: 0;
}

/* Scoped to the table so it outweighs the strip's own corner rules
   (`tr:has(> td) > :only-child`), which would otherwise round the top. */
.plan-table tr.plan-drawer > td,
.plan-table tr.plan-drawer:hover > td {
  background: var(--row-fill);
  padding: var(--md-space-3) var(--md-space-5) var(--md-space-4);
  border-radius: 0 0 var(--md-sys-shape-corner-xs) var(--md-sys-shape-corner-xs);
}

.plan-drawer-head {
  display: flex;
  justify-content: space-between;
  gap: var(--md-space-4);
  margin-bottom: var(--md-space-3);
}

.plan-detail {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--md-space-3) var(--md-space-6);
  margin: 0;
}

/* The design's field label (`.field-label`): sentence case in the accent,
   no longer the uppercase micro-caps this drawer predates the design with. */
.plan-detail dt {
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  letter-spacing: var(--md-sys-typescale-label-small-tracking);
  color: var(--md-sys-color-primary);
}

.plan-detail dd {
  margin: 0;
}

/* ---- supplier grouping --------------------------------------------------- */

/* The supplier's header strip takes the page's section colour, let down into
   the surface as the nav's current page is, so it reads as a heading over the
   white strips under it rather than as one more of them. */
.plan-group td,
.plan-group:hover td {
  background: var(--section-soft);
  color: var(--section-soft-ink);
  font-weight: var(--md-sys-typescale-label-weight);
}

/* The subtotal to the far end of the header, under the figures it totals. */
.plan-subtotal {
  float: right;
  font-variant-numeric: tabular-nums;
}

/* The two urgent counts in the strip are the two the page exists for, and the
   strip is otherwise a row of identical numbers. */
.kpi-value.plan-now {
  color: var(--md-sys-color-error);
}

.kpi-value.plan-soon {
  color: var(--md-sys-color-warning);
}

/* What the plan said before the what-if scenario moved a cell: under the
   scenario's figure, small, so the eye reads the new answer first. */
.plan-was {
  display: block;
  font-size: var(--md-sys-typescale-label-small-size);
  color: var(--md-sys-color-on-surface-variant);
  white-space: nowrap;
}

/* The what-if panel (planning.scenario), on the reorder plan and the
   production report. Closed it is one button and, with a scenario in force,
   one line saying what moved. */
.whatif {
  margin-bottom: var(--md-space-4);
}

.whatif-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2) var(--md-space-3);
}

.whatif-changes {
  min-width: 0;
}

.whatif-ignored {
  margin: var(--md-space-2) 0 0;
}

.whatif-panel {
  margin-top: var(--md-space-3);
}

.whatif-groups {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--md-space-4);
  margin-top: var(--md-space-3);
}

.whatif-group {
  border: 0;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--md-space-3);
  align-content: start;
  min-width: 0;
}

/* Each group is headed like a card title. */
.whatif-group legend {
  font-family: var(--md-sys-typescale-display-font);
  font-size: var(--md-sys-typescale-title-size);
  font-weight: var(--md-sys-typescale-title-weight);
  color: var(--md-sys-color-heading);
  margin-bottom: var(--md-space-2);
}

/* The design's field label, as on every other form. */
.whatif-field label {
  display: block;
  padding-left: 3px;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  letter-spacing: var(--md-sys-typescale-label-small-tracking);
  color: var(--md-sys-color-primary);
  margin-bottom: 3px;
}

.whatif-field input[type="number"],
.whatif-field select {
  width: 100%;
}

/* A lever the scenario moves. Every label is the accent now, so colour
   cannot say which ones moved: a heavier weight and an underline do, which
   also survive every colour-vision mode. */
.whatif-field.is-moved label {
  font-weight: 700;
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}

/* A tickbox lever reads as a sentence, in the body ink. */
.whatif-field .whatif-check {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  padding-left: 0;
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface);
}

.whatif-check input {
  margin-right: var(--md-space-1);
}

.whatif-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-3);
  margin-top: var(--md-space-4);
}

.plan-notes {
  margin: 0;
  padding-left: var(--md-space-5);
}

@media (max-width: 900px) {
  /* Four columns of detail do not fit a phone, and a drawer that scrolls
     sideways is a drawer nobody opens twice. */
  .plan-detail {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* ---- purchasing and production records ------------------------------------
 *
 * What the purchase order, goods receipt, assembly and bill-of-materials
 * pages need that no generic component gives, built from the Customer and
 * Product detail mocks. These pages used to type each of these as an inline
 * `style`; naming them is what keeps the look in one place (and the CSP work
 * in #45 moving).
 */

/* A record's title with its chips beside it - the product mock's
   "CRYS100AL-C [Finished good] [Batch tracked]". */
.record-title {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2) var(--md-space-3);
}

/* The record's actions in a row under its title, primary first - the
   customer mock's "Edit / Sales overview / Portal logins". */
.record-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin: 0 0 var(--md-space-3);
}

/* A line of context under the title: where an order came from, why an action
   is not offered yet. */
.record-note {
  margin: 0 0 var(--md-space-3);
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

/* A one-button POST form among links: no box of its own. */
.inline-form {
  display: inline;
  margin: 0;
}

/* A sentence under a cell's value - a cost's apportionment note, why a
   component is short, when a line was last checked. */
.cell-note {
  display: block;
  margin-top: 2px;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: 400;
  color: var(--md-sys-color-on-surface-variant);
}

/* Buttons side by side in a cell or a heading row. */
.cell-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2);
}

.cell-center {
  text-align: center;
}

/* An order's figures under its lines, right-aligned and lined up. */
.record-totals {
  display: grid;
  grid-template-columns: auto auto;
  justify-content: end;
  gap: var(--md-space-2) var(--md-space-6);
  margin: var(--md-space-4) 0 0;
  padding-right: 10px;
  font-variant-numeric: tabular-nums;
}

.record-totals dt {
  color: var(--md-sys-color-on-surface-variant);
}

.record-totals dd {
  margin: 0;
  text-align: right;
}

.record-totals .is-total {
  font-weight: 700;
  color: var(--md-sys-color-on-surface);
}

/* One delivery on an order: its heading row, then its lines. A rule between
   two, not a box round each - boxes inside the card would read as cards. */
.receipt-block + .receipt-block {
  margin-top: var(--md-space-5);
  padding-top: var(--md-space-5);
  border-top: 1px solid var(--md-sys-color-outline-variant);
}

.receipt-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2) var(--md-space-3);
  margin: 0 0 var(--md-space-2);
}

.receipt-head > .cell-actions {
  margin-left: auto;
}

.receipt-block .cell-note {
  margin-top: var(--md-space-2);
}

/* The add-a-cost row under the costs table: a POST form wearing `.filters`
   for its row of labelled fields, top-aligned so an error under one field
   does not push its neighbours down. */
.cost-add {
  margin: var(--md-space-4) 0 0;
  align-items: start;
}

/* "+ Add line" under a line editor. */
.lines-add {
  margin-top: var(--md-space-3);
}

/* The line editors fix their column widths, so a picker filling in does not
   resize the whole table under the cursor. Per editor, because the columns
   differ; the widths are the ones each template used to set inline. */
.lines-fixed {
  table-layout: fixed;
}

.lines-fixed--po col:nth-child(1) { width: 40%; }
.lines-fixed--po col:nth-child(n + 2) { width: 15%; }

.lines-fixed--receipt col:nth-child(1) { width: 38%; }
.lines-fixed--receipt col:nth-child(2) { width: 14%; }
.lines-fixed--receipt col:nth-child(3) { width: 20%; }
.lines-fixed--receipt col:nth-child(4) { width: 18%; }
.lines-fixed--receipt col:nth-child(5) { width: 10%; }

.lines-fixed--qc col:nth-child(1) { width: 30%; }
.lines-fixed--qc col:nth-child(2) { width: 10%; }
.lines-fixed--qc col:nth-child(3) { width: 14%; }
.lines-fixed--qc col:nth-child(4) { width: 12%; }
.lines-fixed--qc col:nth-child(5) { width: 12%; }
.lines-fixed--qc col:nth-child(6) { width: 22%; }

.lines-fixed--put-away col:nth-child(1) { width: 6%; }
.lines-fixed--put-away col:nth-child(2) { width: 30%; }
.lines-fixed--put-away col:nth-child(3) { width: 12%; }
.lines-fixed--put-away col:nth-child(4) { width: 12%; }
.lines-fixed--put-away col:nth-child(5) { width: 14%; }
.lines-fixed--put-away col:nth-child(6) { width: 26%; }

.lines-fixed--bom col:nth-child(1) { width: 46%; }
.lines-fixed--bom col:nth-child(n + 2) { width: 18%; }

/* The QC page's last column holds four labelled fields stacked; the labels
   are the design's field labels, as in a form grid. */
.qc-fields {
  display: grid;
  gap: var(--md-space-1);
}

.qc-fields label {
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  color: var(--md-sys-color-primary);
}

/* The start form: one heading row per component, then its picks. */
.pick-group > td {
  padding-top: var(--md-space-4);
}

.pick-group .sum-readout {
  margin-left: var(--md-space-2);
}

/* The purchase order's header fields, six to a row on a wide screen. */
.form-grid--compact {
  --form-col: 180px;
}

/* A single paragraph that is the whole of its card. */
.flush {
  margin: 0;
}

/* The reorder plan's "About this calculation": a heading per part. */
.plan-about .card-title:not(:first-child) {
  margin-top: var(--md-space-5);
}

/* A report's controls (`.controls`, see the filters above) set in the Filters
   panel's well, with the panel's stacked labels, but without its collapse:
   a report's period always has a value, so there is nothing to count or
   reset. Opt-in, so the other `.controls` rows keep their look until their
   pages are redesigned. */
.controls--panel {
  padding: var(--md-space-4) var(--md-space-5);
  background: var(--md-sys-color-surface-container);
  border-radius: var(--md-sys-shape-corner-panel);
}

.controls--panel label {
  display: block;
  padding-left: 3px;
  margin-bottom: 3px;
  font-size: var(--md-sys-typescale-label-small-size);
  font-weight: var(--md-sys-typescale-label-small-weight);
  letter-spacing: var(--md-sys-typescale-label-small-tracking);
  color: var(--md-sys-color-primary);
}

/* A tickbox's label is the sentence beside it, not a caption above. */
.controls--panel label:has(> input[type="checkbox"]) {
  display: flex;
  align-items: center;
  gap: var(--md-space-2);
  min-height: 2.25rem;
  margin: 0;
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface);
}

/* A table's totals row: a strip like the rows above it, in bold. */
.total-row > * {
  font-weight: 700;
}

/* ---- the sales order form's live preview (#468, #471) --------------------
 * The price and stock note under a product on the order form. Small print,
 * because it is a guide to the eye beside the boxes rather than a column of
 * its own - and `:empty` so a row nobody has chosen a product for takes no
 * height at all. */

.line-preview {
  margin-top: var(--md-space-1);
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

.line-preview:empty {
  display: none;
}

/* The running total. Sticky rather than fixed, and in the flow of the card:
   a sticky element reserves its own space, so the footer cannot come to rest
   over the row somebody is typing into (WCAG 2.4.11). */
.order-totals {
  position: sticky;
  bottom: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--md-space-2) var(--md-space-4);
  margin-top: var(--md-space-4);
  padding: var(--md-space-2) var(--md-space-3);
  border-top: 1px solid var(--md-sys-color-outline-variant);
  background: var(--md-sys-color-surface-container-low);
}

.order-totals__figures {
  display: flex;
  gap: var(--md-space-5);
  margin: 0;
  margin-left: auto;
}

/* Tabular figures, so Net, VAT and Gross line up as they are read down. */
.order-totals__figures div {
  display: flex;
  gap: var(--md-space-2);
  font-variant-numeric: tabular-nums;
}

.order-totals__figures dt {
  color: var(--md-sys-color-on-surface-variant);
}

.order-totals__figures dd {
  margin: 0;
}

.order-totals__label,
.order-totals__note {
  margin: 0;
}

/* The note belongs under the figures rather than beside them: it is about
   what they are missing. */
.order-totals__note {
  flex-basis: 100%;
}

/* ---- sales (the redesign of the sales pages) ------------------------------
 * What the sales pages need that no generic component gives. Everything
 * here is sales-only; the generic pieces this work added (.totals,
 * .card-title-row, .reason-list, .choice-list__*, .measure) sit with their
 * families above. */


/* The number and its comment count, one unit: the design sets the bubble a
   few pixels after the number, in the accent, with the count in caption
   size. Inline-flex so the two never wrap apart. */
.order-number {
  display: inline-flex;
  align-items: center;
  gap: var(--md-space-4);
  white-space: nowrap;
}

.order-number__comments {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  font-size: var(--md-sys-typescale-caption-size);
  font-weight: 600;
  color: var(--md-sys-color-primary);
  text-decoration: none;
}

.order-number__comments .icon {
  width: 16px;
  height: 16px;
}

.order-number__comments:hover {
  text-decoration: underline;
}

/* The bulk print dialog: wide enough for the sheet names. */
.print-modal {
  min-width: min(28rem, 80vw);
}

/* Why an order's stock flag says what it says, under the Lines title: the
   chip then the sentence, on one line where they fit. */
.stock-note {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2);
}

/* The quantity box in the lot dialog's table: wide enough for 0.0001
   precision, no wider. */
.lot-qty {
  width: 7rem;
}

/* The order form's lines: fixed columns, so a long product name in the
   picker cannot shove the figures sideways while it is being typed. */
.order-lines {
  table-layout: fixed;
}

.order-lines__product {
  width: 46%;
}

.order-lines__figure {
  width: 14%;
}

.order-lines__discount {
  width: 12%;
}

/* The quick-add bar above the order's lines (#469): the search wide, the
   figures narrow, Add at the end of the row, and everything wrapping onto a
   second row on a narrow screen rather than scrolling sideways. */
.quick-add {
  margin-bottom: var(--md-space-4);
  padding: var(--md-space-3) var(--md-space-4);
  background: var(--md-sys-color-surface-container);
  border-radius: var(--md-sys-shape-corner-panel);
}

.quick-add__fields {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--md-space-2) var(--md-space-3);
}

.quick-add__fields > div {
  flex: 0 1 8rem;
  min-width: 6rem;
}

.quick-add__fields > .quick-add__product {
  flex: 1 1 18rem;
}

.quick-add__fields input {
  width: 100%;
}

.quick-add__duplicate {
  margin: var(--md-space-2) 0 0;
}

.quick-add__duplicate[hidden] {
  display: none;
}

.quick-add__duplicate p {
  margin: 0 0 var(--md-space-2);
}

.quick-add__status {
  margin: var(--md-space-2) 0 0;
  font-size: var(--md-sys-typescale-body-small-size);
}

.quick-add__status:empty {
  display: none;
}

/* The words say it; the colour only repeats it. */
.quick-add__status--error {
  color: var(--md-sys-color-error);
}

.quick-add__keys {
  margin-top: var(--md-space-2);
  font-size: var(--md-sys-typescale-body-small-size);
}

.quick-add__key-list {
  display: grid;
  gap: var(--md-space-1);
  margin: var(--md-space-2) 0;
}

.quick-add__key-list div {
  display: flex;
  flex-wrap: wrap;
  gap: var(--md-space-2);
}

.quick-add__key-list dt {
  min-width: 16rem;
}

.quick-add__key-list dd {
  margin: 0;
}

/* Paste lines (#474), under the bar: the box full width, the preview a
   plain table of what each pasted row becomes. */
.paste-lines {
  margin: calc(-1 * var(--md-space-2)) 0 var(--md-space-4);
}

.paste-lines textarea {
  width: 100%;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

.paste-lines__preview:empty {
  display: none;
}

.paste-preview {
  margin: var(--md-space-2) 0;
}

.paste-preview p {
  margin: 0;
}

.paste-preview__text {
  white-space: pre-wrap;
  word-break: break-word;
}

/* The words and the sign say it; the colour only repeats it. */
.paste-preview__warning {
  margin: var(--md-space-2) 0;
  color: var(--md-sys-color-on-surface);
}

.paste-preview__warning > span[aria-hidden] {
  color: var(--md-sys-color-warning);
}

.paste-preview__ok .icon {
  width: 16px;
  height: 16px;
  vertical-align: text-bottom;
}

/* "New address" under the address picker it adds to. */
.address-pick__new {
  margin-top: var(--md-space-2);
}

/* The pallet plan: the 3D stage needs a height to render into (the element
   sizes itself to its box), the controls sit under it, and the pallet picker
   is laid out here but shown by pallet_plan.js - no `display` on it, so the
   browser's `[hidden]` still hides it. */
.pallet-stage {
  display: block;
  width: 100%;
  height: 60vh;
  min-height: 320px;
}

.pallet-controls .card-note {
  margin: 0;
}

.pallet-nav {
  gap: var(--md-space-2);
  align-items: center;
}

/* "Use this plan" starts hidden until the plan is computed, and `.action-row`
   sets `display`, which beats the browser's `[hidden]` - restated here for the
   reason given above `.filters`. */
.pallet-use[hidden] {
  display: none;
}

/* A carton's colour in the plan, ahead of its SKU in the key. The SKU is
   always beside it, so the colour is never the only way to tell them apart. */
.swatch {
  display: inline-block;
  width: 0.7rem;
  height: 0.7rem;
  margin-right: var(--md-space-2);
  border-radius: 2px;
  background: var(--tone);
  vertical-align: middle;
}


/* One consignment on an order's Shipments tab: its number, status and the
   paperwork on one line, the carrier under it, then its lines. A rule
   between consignments rather than a card each - they are one list. */
.shipment-block + .shipment-block {
  margin-top: var(--md-space-5);
  padding-top: var(--md-space-5);
  border-top: 1px solid var(--md-sys-color-outline-variant);
}

.shipment-block__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--md-space-2) var(--md-space-3);
  margin: 0 0 var(--md-space-2);
}

.shipment-block__docs {
  display: flex;
  gap: var(--md-space-2);
  margin-left: auto;
}

.shipment-block__meta {
  margin: 0 0 var(--md-space-2);
  font-size: var(--md-sys-typescale-body-small-size);
  color: var(--md-sys-color-on-surface-variant);
}

/* ---- A name with its avatar in front ---------------------------------------
   The customer's logo or initials (_avatar.html) beside their name in the
   customer page header. */
.with-avatar {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  /* Middle, not the baseline: an inline-flex box that starts with an <img>
     takes the image's bottom edge as its baseline, which pushes the line - and
     a table row - taller than the same box with initials in it. */
  vertical-align: middle;
}

/* Fixed PDF action with native disclosure and a no-popover fallback. */
.export-split { display: inline-flex; align-items: center; gap: 0.25rem; }
.export-popover-trigger, .export-menu[popover] { display: none; }
.export-fallback { position: relative; }
.export-menu {
  background: var(--md-sys-color-surface-container-high);
  border: 1px solid var(--md-sys-color-outline-variant);
  border-radius: var(--md-sys-shape-corner-panel);
  box-shadow: var(--md-sys-elevation-3);
  padding: var(--md-space-2);
  min-width: 10rem;
}
.export-menu a {
  display: block;
  padding: var(--md-space-2) var(--md-space-3);
  color: var(--md-sys-color-on-surface);
  border-radius: var(--md-sys-shape-corner-sm);
  text-decoration: none;
}
.export-menu a:hover { background: var(--md-sys-color-surface-container); }
.export-fallback .export-menu { position: absolute; top: 100%; right: 0; z-index: 20; }
@supports selector(:popover-open) and (top: anchor(bottom)) {
  .export-popover-trigger { display: inline-flex; }
  .export-fallback { display: none; }
  .export-menu:popover-open { display: block; }
  .export-menu[popover] {
    inset: auto;
    top: anchor(bottom);
    left: anchor(left);
    margin: 0.25rem 0 0;
    position-try-fallbacks: flip-inline, flip-block;
  }
}

/* The order book keeps every status and its caveats readable at small widths. */
.dash-card--order-book table {
  width: 100%;
  min-width: 0;
}
.dash-card--order-book :is(th, td, .dash-card__sub) {
  white-space: normal;
  overflow-wrap: anywhere;
}
.dash-card--order-book th:first-child { width: 34%; }
.dash-card--order-book th:nth-child(2) { width: 18%; }

/* Order entry pack shots expand in flow so a scrolling table cannot clip them. */
.line-thumb-control {
  flex: none;
  padding: 0;
  border: 0;
  border-radius: var(--md-sys-shape-corner-xs);
  background: var(--md-sys-color-paper);
  cursor: zoom-in;
}
.line-thumb-control .line-thumb { width: 40px; height: 40px; }
.line-thumb-control:hover .line-thumb,
.line-thumb-control:focus .line-thumb { width: 128px; height: 128px; }
.line-thumb-control:focus-visible {
  outline: 2px solid var(--md-sys-color-primary);
  outline-offset: 2px;
}
