/* The shell.
   Load order: colours.css -> shell.css -> the current space's stylesheet.

   Follows ui-guidelines.md: flat, no shadows anywhere, white cards on a
   gray-50 page, gray-200 borders, gray-100 dividers, 12px radius, SVG icons
   at gray-400. The four brand tokens come from colours.css; the neutrals are
   the documented gray scale. */

:root {
  /* Neutrals — ui-guidelines.md colour palette */
  --page-bg: #f9fafb;      /* gray-50   app background */
  --card-bg: #ffffff;      /* white     all tiles */
  --border: #e5e7eb;       /* gray-200  tile borders */
  --divider: #f3f4f6;      /* gray-100  internal separators */
  --text: #374151;         /* gray-700  body text */
  --text-strong: #1f2937;  /* gray-800  emphasised inline values */
  --text-muted: #9ca3af;   /* gray-400  labels, icons, secondary info */
  /* gray-300. The one grey that is a SURFACE rather than a line or a
     letter: the fill of a control that is switched off. It has to be
     darker than both `--page-bg` and `--card-bg`, because a disabled
     button still has to read as a button and not as a hole in the page. */
  --disabled-bg: #d1d5db;
  --text-mid: #6b7280; 
  --icon : #fff;    /* gray-500  section-header icons */
  /* The name over a value on a detail surface — a `dt`, a card's
     caption. Blue and 600 so a reader can find the field they want
     without reading every value on the page; darker than --sky-blue,
     which is reserved for what can be clicked. It was already this
     hex twice in backoffice.css, hardcoded both times. */
  --label-blue: #1560e0;

  /* Brand, from colours.css */
  --primary: var(--sky-blue);        /* #1640FF actions, links, active */
  --brand-dark: var(--night-blue);   /* #02023A headings, sidebar bg */

  /* The brand gradient, for a page that wants a full-colour moment instead
     of the flat page background — currently the launcher. Same palette as
     the sign-in hero panel (--night-blue through an orange glow), on the
     diagonal rather than sign-in's near-vertical: the launcher's hero is a
     wide, short box, not a tall narrow one, and a near-vertical angle reads
     as flat horizontal bands at that aspect ratio instead of a diagonal. */
  --brand-gradient: linear-gradient(225deg, rgba(242, 107, 17, 0.85) 0%,
    rgba(9, 5, 92, 0.95) 55%, rgba(2, 2, 58, 1) 100%);

  /* A second full-colour moment, for the People screens and the two
     "set up a company" forms — same night-blue-and-orange palette as
     --brand-gradient, plus a cyan highlight. */
  --gradient-alt: linear-gradient(5deg, rgba(9, 5, 92, 1) 56%,
    rgba(242, 107, 17, 0.82) 100%, rgba(0, 212, 255, 1) 50%);

  /* Tints, built from the documented chip recipe */
  --primary-wash: #eff6ff;
  --primary-edge: #bfdbfe;
  --focus-soft: 0 0 0 2px #1640ff33;
  --focus-hard: 0 0 0 2px #1640ff;

  /* Three semantic tones, each a background / ink / edge triplet, taken
     from ui-guidelines.md's status and priority chip recipes. Everything
     that has to say "this is fine", "look at this" or "this is wrong" —
     a banner, a chip, a validator tick — reads from these and never picks
     its own red. */
  --warn-bg: #fffbeb;
  --warn-ink: #b45309;
  --warn-edge: #fde68a;
  --bad-bg: #fef2f2;
  --bad-ink: #dc2626;
  --bad-edge: #fecaca;
  --good-bg: #f0fdf4;
  --good-ink: #059669;
  --good-edge: #bbf7d0;
  /* The same "this is settled" meaning, FILLED rather than tinted. A
     stepper's done mark is a solid circle with a white tick on it, and
     --good-ink is chosen to be read as text on a pale background, which
     is a different job and too dark for this one. */
  --good-strong: #06b00e;

  /* A floating panel — the account menu, the application drawer. The one
     place in the product with a shadow, and it is not elevation for
     decoration: a panel that overlaps the page it is drawn on has to be
     separable from it, which ui-guidelines.md's no-shadow rule is about
     tiles and cards rather than transient overlays. */
  --panel-float: 0 12px 32px rgba(2, 2, 58, 0.14);

  /* Overridden per space on <body>. Platform pages keep brand dark. */
  --app-accent: var(--night-blue);
  --app-accent-rgb: var(--night-blue-rgb);

  --radius: 12px;          /* rounded-xl */
  --radius-sm: 8px;        /* rounded-lg — buttons, chips */
  --rail-width: 15rem;
  --rail-width-collapsed: 4.25rem;
  --topbar-height: 3.25rem;
  --measure: 68ch;

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

/* --- Reset ------------------------------------------------------------- */

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

/* No scrollbar is ever drawn, anywhere - a page, a panel, a table in its
   own box, the sidebar. Scrolling itself is untouched: wheel, trackpad,
   touch, keyboard, and dragging a selection past the edge all behave
   exactly as before. Only the bar is not painted.

   Three declarations because three engines answer to three different
   things, not because there is a choice between them: Firefox reads
   `scrollbar-width`, legacy Edge reads `-ms-overflow-style`, and Chrome
   and Safari paint the bar as a pseudo-element that has to be switched
   off. `.shell-rail` carried exactly this trio privately; it is the
   default for everything now, so the rail no longer repeats it.

   Known cost, accepted deliberately: a scrollbar is the only thing that
   tells a reader there is more content below or to the right, and the
   only way to reach it with a mouse that has no wheel tilt. A surface
   that scrolls and matters has to say so some other way - a count, a
   "showing N of M", a stacked layout on a phone - because the bar will
   not say it any more. */
*,
*::before,
*::after {
  scrollbar-width: none;      /* Firefox */
  -ms-overflow-style: none;   /* legacy Edge */
}

*::-webkit-scrollbar { display: none; }  /* Chrome, Safari */

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

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: 0.875rem;
  line-height: 1.55;
  color: var(--text);
  background: var(--page-bg);
}

h1,
h2,
h3 {
  color: var(--brand-dark);
  line-height: 1.25;
  margin: 0 0 0.5rem;
  font-weight: 700;
}

h1 { font-size: 1.5rem; letter-spacing: -0.01em; }
h2 { font-size: 0.875rem; }
h3 { font-size: 0.8125rem; }

p { margin: 0 0 0.875rem; max-width: var(--measure); }

a { color: var(--primary); text-decoration: none; }
a:hover { text-decoration: underline; }

/* --- Procurement does not underline ----------------------------------------

   Procurement's own decision, scoped to Procurement and inert anywhere
   else: `data-application` is written on `<body>` by base.html from the
   application register, so these rules cannot match a CRM, HR, Projects
   or platform page. The shared rule above is NOT changed - every other
   application keeps the underline it has always had.

   Why here and not in procurement.css, which is where an application's
   own rules belong: procurement.css is pulled in by `{% block extra_css %}`
   and only four of Procurement's forty-eight pages do that, so a rule
   there would reach the dashboard, the compare page and the two public
   ones and miss the other forty-four. shell.css is the only stylesheet
   all of them load. Making procurement.css load everywhere is the right
   answer and is NOT a styling fix - it redefines `.panel__lede`
   differently from the kit, so turning it on app-wide would restyle
   pages that never asked. Flagged rather than done.

   The underline was the only hover feedback a plain link had, so it is
   replaced and not merely deleted; focus keeps the soft ring from the
   rule below, which is what makes dropping the underline on
   :focus-visible safe. */
body[data-application="procurement"] a:hover,
body[data-application="procurement"] a:focus-visible,
body[data-application="procurement"] a:active {
  text-decoration: none;
}

/* The one exception, and it is the carve-out rather than an oversight.
   A standing warning's link takes `color: inherit`, so inside that
   paragraph the underline is the ONLY thing separating a link from the
   sentence around it. Taking it away on hover would mean the marker
   disappearing at the moment somebody reached for it. Underlined at
   rest, underlined on hover, underlined on focus - unchanged from
   every other application. */
body[data-application="procurement"] .banner a,
body[data-application="procurement"] .banner a:hover,
body[data-application="procurement"] .banner a:focus-visible {
  text-decoration: underline;
}

/* Blue at rest, so the change is to the dark end. These are the links
   whose ONLY hover signal was the underline - everything with a hover
   rule of its own (a button, a row action, a card menu item, a
   dashboard tile) already had one and is untouched. */
body[data-application="procurement"] a:not([class]):hover,
body[data-application="procurement"] .filter-bar__clear:hover,
body[data-application="procurement"] .person-row__action:hover,
body[data-application="procurement"] .panel__header-action:hover,
body[data-application="procurement"] .timeline__title:hover,
body[data-application="procurement"] .timeline__action:hover,
body[data-application="procurement"] .timeline__action:focus-visible {
  color: var(--brand-dark);
}

/* Dark or muted at rest - a grid's record name, a chart's legend - so
   the change is the other way, to the colour the product uses for "you
   can click this". After the block above on purpose: the chart links
   are classless anchors inside a classed span, so both selectors match
   them at the same specificity and the later one is meant to win. */
body[data-application="procurement"] .grid__name:hover,
body[data-application="procurement"] .grid__qualifier:hover,
body[data-application="procurement"] .donut__name a:hover,
body[data-application="procurement"] .bar-chart__label a:hover,
body[data-application="procurement"] .column-chart__label a:hover {
  color: var(--primary);
}

/* Soft ring is the default. ui-guidelines.md reserves the hard ring for
   prominent forms and modals — see .centred__card input below. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: none;
  box-shadow: var(--focus-soft);
  border-radius: var(--radius-sm);
}

/* Section label. Sentence case at gray-400 — ui-guidelines.md rules out
   uppercase tracking-wide labels. */
.eyebrow {
  display: block;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 0.25rem;
}

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

/* SVG icons: 13px inline, 14px in a tile header, gray-400, never emoji. */
.icon {
  flex: 0 0 auto;
  color: var(--icon);
  vertical-align: -0.125em;
}

/* --- Topbar ------------------------------------------------------------ */

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 0.875rem;
  height: var(--topbar-height);
  padding: 0 1rem;
  background: var(--card-bg);
  border-bottom: 1px solid var(--border);
}

/* Opens the mobile sidebar overlay. Only ever visible below the tablet
   breakpoint (see Responsive, below) — the desktop rail is always on
   screen, so there is nothing for this button to do there. */
.topbar__menu-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-mid);
  cursor: pointer;
}

.topbar__menu-toggle:hover { background: var(--page-bg); color: var(--brand-dark); }
.topbar__menu-toggle .icon { color: currentColor; }

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  font-weight: 700;
  font-size: 0.9375rem;
  color: var(--brand-dark);
}

.brand:hover { text-decoration: none; }

.brand-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  flex: 0 0 auto;
  padding: 0.2rem;
  border-radius: 6px;
  background: var(--white);
  border: 1px solid var(--border);
}

.brand-mark img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* The pill beside the mark: which company's data you are looking at, or —
   on a surface that is not a company's — which surface it is. Named for the
   shape rather than for the company, because the backoffice puts its own
   word in it and a class called `__tenant` holding "Backoffice" is a lie
   the next reader has to check. */
.brand__pill {
  display: inline-flex;
  align-items: center;
  font-weight: 600;
  font-size: 0.75rem;
  color: var(--text-mid);
  padding: 0.2rem 0.6rem;
  margin-left: 0.25rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--page-bg);
}

.topbar__spacer { flex: 1 1 auto; }

/* --- Topbar controls --------------------------------------------------- */

/* Two controls, and the same two at every width. What replaced the flat
   application row, its narrow-screen duplicate, the "your name" link and
   the "Sign out" button — see templates/includes/_topbar.html for what went
   and why. */

.topbar__button,
.topbar__avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.topbar__button {
  width: 2.125rem;
  height: 2.125rem;
  border-radius: var(--radius-sm);
  color: var(--text-mid);
}

.topbar__button:hover,
.topbar__button[aria-expanded="true"] {
  background: var(--primary-wash);
  color: var(--primary);
}

.topbar__button .icon { color: currentColor; }

/* The avatar is round, so its open/focus state is a ring rather than a
   filled tile — a rounded-square wash behind a circle reads as a mistake. */
.topbar__avatar { border-radius: 50%; }

.topbar__avatar[aria-expanded="true"] {
  box-shadow: 0 0 0 2px var(--card-bg), 0 0 0 4px var(--primary);
}

.topbar__avatar:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--card-bg), 0 0 0 4px var(--primary);
}

/* Signed out: sign in, and create a company. */
.topbar__link {
  padding: 0.3rem 0.7rem;
  border-radius: var(--radius-sm);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-mid);
  white-space: nowrap;
}

.topbar__link:hover {
  background: var(--page-bg);
  color: var(--brand-dark);
  text-decoration: none;
}

/* Each application's own bright, mutually distinct colour, for its drawer
   tile and its sidebar. Deliberately not `Application.accent`
   (apps/core/applications.py), which is one of the three fixed brand tokens
   and is reused across more than one application by design (see
   ui-guidelines.md). This is a visual aid for telling them apart at a
   glance, keyed by `data-app`. */
[data-app="crm"] .icon { color: #1640ff; }
[data-app="hr"] .icon { color: #f69400; }
[data-app="recruitment"] .icon { color: #ec4899; }
[data-app="procurement"] .icon { color: #10b981; }
[data-app="projects"] .icon { color: #8b5cf6; }
[data-app="sandbox"] .icon { color: #06b6d4; }

/* Alpine is deferred, so x-show has not run on first paint. Without this a
   panel is briefly visible on every page load — and the drawer is a
   300px card in the middle of the screen, so "briefly" is very noticeable. */
[x-cloak] { display: none !important; }

/* --- Body layout ------------------------------------------------------- */

.shell-body {
  display: grid;
  grid-template-columns: var(--rail-width) minmax(0, 1fr);
  align-items: stretch;
  min-height: calc(100vh - var(--topbar-height));
}

.shell-body--bare { grid-template-columns: minmax(0, 1fr); }

/* Sidebar background is brand dark, per the colour palette table. Fixed to
   the full height of the viewport below the topbar — not just as tall as
   its own content — so it reads as a permanent rail rather than a box that
   happens to float above the page background. It scrolls internally once
   its content outgrows that height, with no bar drawn — see the Reset,
   which hides every scrollbar in the product — so it does not compete
   with the topbar's own hairline border. */
.shell-rail {
  position: sticky;
  top: var(--topbar-height);
  align-self: stretch;
  height: calc(100vh - var(--topbar-height));
  overflow-y: auto;
  padding: 1rem 0.625rem;
  background: var(--brand-dark);
}

.shell-content {
  padding: 1.5rem;
  min-width: 0;
}

/* Full-bleed brand gradient behind a page's content, rather than the flat
   page background — the launcher is the one page that gets this. The
   content inside stays exactly as wide as everywhere else (.content-wrap
   is unaffected); only the colour behind it changes, edge to edge. */
.shell-content--gradient { background: var(--brand-gradient); }
.shell-content--gradient-alt { background: var(--gradient-alt); }

.content-wrap { max-width: 72rem; margin: 0 auto; }

/* The launcher family (Your applications, People, Applications settings)
   sets its own, wider max-width via .launcher — this stops the generic
   content wrapper from re-capping it back down to 72rem. Every other page
   (an application's own placeholder or panel content) never has a
   .launcher inside it, so this is scoped to exactly the pages that need
   it rather than widening the whole shell. */
.content-wrap:has(.launcher) { max-width: 96rem; }

/* --- Sidebar ----------------------------------------------------------- */

.rail__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: rgb(var(--white-rgb) / 0.45);
  cursor: pointer;
}

.rail__toggle:hover { background: rgb(var(--white-rgb) / 0.07); color: var(--white); }
.rail__toggle .icon { color: currentColor; }

/* Collapsing points the arrow left (fold this way); collapsed, it points
   right (unfold this way). */
.rail__toggle .icon { transform: scaleX(-1); }
body.sidebar-collapsed .rail__toggle .icon { transform: none; }

/* The Dashboard link: every application's own home page, always the very
   first row, styled exactly like any other row in the rail — a .rail__link
   like any other. The collapse toggle shares this row, pinned to the far
   right, so it never takes a row of its own. */
.rail__home-row {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  padding-bottom: 0.875rem;
  margin-bottom: 0.625rem;
  border-bottom: 1px solid rgb(var(--white-rgb) / 0.08);
}

.rail__home-row .rail__link { flex: 1 1 auto; min-width: 0; }

/* Closes the mobile sidebar overlay from inside it. Desktop already has the
   fold-away arrow (.rail__toggle) for the same row; this is its mobile-only
   counterpart, shown instead of that button below the tablet breakpoint. */
.rail__close {
  display: none;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: rgb(var(--white-rgb) / 0.45);
  cursor: pointer;
}

.rail__close:hover { background: rgb(var(--white-rgb) / 0.07); color: var(--white); }
.rail__close .icon { color: currentColor; }

/* Every menu is a section: an uppercase label, then its pages. ui-guidelines.md
   rules out uppercase tracking-wide
   labels everywhere else; the sidebar section label is the one deliberate
   exception, so it reads at a glance against the dark rail. */
.rail__section + .rail__section { margin-top: 1.125rem; }

.rail__section-label {
  margin: 0 0 0.375rem;
  padding: 0 0.625rem;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgb(var(--mustard-yellow-rgb) / 0.75);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rail__section-items { list-style: none; margin: 0; padding: 0; }

.rail__link {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.4rem 0.625rem;
  font-size: 0.8125rem;
  color: rgb(var(--white-rgb) / 0.65);
  border-radius: var(--radius-sm);
}

/* Every application shares this one sidebar renderer (_sidebar.html), so one
   rule here covers every application's menu icons. */
.rail__link .icon { color: rgb(17 230 250 / 45%); }

.rail__link:hover {
  color: var(--white);
  background: rgb(var(--white-rgb) / 0.07);
  text-decoration: none;
}

/* The current page, in every application's sidebar alike — fixed to the
   brand's mustard yellow rather than the per-app accent, unlike the rest
   of the chrome (§2.6 normally puts the app's own accent here). */
.rail__link[aria-current="page"] {
  color: var(--brand-dark);
  font-weight: 600;
  background: var(--mustard-yellow);
}

.rail__link[aria-current="page"] .icon { color: var(--brand-dark); }

/* How many things on a page are waiting for somebody, drawn on the menu
   item itself. Mustard, which is the rail's one attention colour already —
   the section labels use it — so a count reads as "look here" without
   introducing a colour the product does not have.

   Pushed to the far end by `margin-left:auto` rather than sitting against
   the label, so counts on different rows line up in a column instead of
   ragging with the text above them. */
.rail__badge {
  margin-left: auto;
  flex: 0 0 auto;
  min-width: 1.125rem;
  padding: 0 0.3125rem;
  border-radius: 999px;
  background: var(--mustard-yellow);
  color: var(--brand-dark);
  font-size: 0.6875rem;
  font-weight: 700;
  line-height: 1.125rem;
  text-align: center;
}

/* On the current row the background is already mustard, so a mustard chip
   on it would be invisible. Invert it there. */
.rail__link[aria-current="page"] .rail__badge {
  background: var(--brand-dark);
  color: var(--mustard-yellow);
}

/* --- Collapsed sidebar --------------------------------------------------
   Collapses to the left: the rail column shrinks, the content column takes
   the freed space. Every row is already flat, so collapsing only drops the
   text and centres the icon — nothing needs to hide behind a disclosure.
   Desktop only — below the breakpoint the rail is a full-screen overlay
   (see Responsive, below), so collapsing it would fight that instead.

   Collapsing narrows the rail; it never redraws the columns. The flag lives
   in localStorage and so outlives the page that set it — collapse Admin,
   open the launcher, and the launcher still carries it. A page with no rail
   (.shell-body--bare) has no rail column to narrow, so it cannot be
   affected. When this rule set the columns itself, it outranked the bare
   one and squeezed the launcher into a 4.25rem strip. */

@media (min-width: 64.0625rem) {
  body.sidebar-collapsed { --rail-width: var(--rail-width-collapsed); }

  /* The home row's link and toggle stack instead of sitting side by side —
     there is no room for both at icon width. */
  body.sidebar-collapsed .rail__home-row { flex-direction: column; gap: 0.375rem; }

  body.sidebar-collapsed .rail__section-label,
  body.sidebar-collapsed .rail__link-label { display: none; }

  body.sidebar-collapsed .rail__link { justify-content: center; padding: 0.55rem 0.25rem; }

  /* Collapsed there is no label to push away from, so the count sits beside
     the icon rather than at the far edge of a rail that is barely wider
     than one. It stays visible: a number nobody can see until they unfold
     the sidebar is not a notification. */
  body.sidebar-collapsed .rail__badge { margin-left: 0; }
}

/* --- Panels and rows --------------------------------------------------- */

/* A page's own heading beside its one primary action — "People" and "Add
   somebody", "Applications" and "Save". Wraps below the heading on a narrow
   viewport rather than squeezing the button against the text. */
.page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 1.5rem;
}

.page-head__text { min-width: 0; }

/* A record's status pill sits on the title line, right after its name and
   reference - "Reception furniture QR-0015 (Draft)" - not over with the
   buttons. The pill keeps its own size; the heading's does not reach it. */
.page-head__text h1 .status-chip {
  margin-left: 0.5rem;
  vertical-align: middle;
  position: relative;
  top: -0.15em;
}
.page-head__text h1 .grid__person-inner .status-chip { top: 0; }
.page-head__text .launcher__lede { margin-bottom: 0; }

/* A small colour-washed icon tile in a panel header, in place of the plain
   inline icon — a record-detail page's sections read as distinct blocks,
   not a settings list. */
.panel__icon-box {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1.5px solid var(--primary-edge);
  background: var(--primary-wash);
  color: var(--primary);
}
.panel__icon-box .icon { stroke-width: 2; color: #3b5cc6; }

/* A read-only value, styled like a disabled input — a record-detail page
   reading data back, not asking for it. */
.info-field { display: flex; flex-direction: column; gap: 0.35rem; }

.info-field__label {
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-muted);
}

.info-field__value {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.55rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--page-bg);
  font-size: 0.8125rem;
  color: var(--text);
}

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

.info-field__value--muted { color: var(--text-muted); font-style: italic; }

.panel {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.panel__header {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.875rem 1.25rem;
  border-bottom: 1px solid var(--divider);
}

.panel__header .icon { color: var(--text-mid); }
.panel__header h2 { margin: 0; }

/* The right-hand end of a panel header: the one control, or the one fact,
   that belongs opposite the title rather than beside it. Two classes and
   not one because a button and a line of text want different sizes, and a
   header only ever has one or the other. */
.panel__header-action { margin-left: auto; }
.panel__header-note { margin-left: auto; font-size: 0.75rem; }

/* How many things are in the panel, beside its title rather than opposite
   it — "Items 2". The same pill as a tab's count, because it is the same
   fact in a different place; `.panel__header` is already a flex row with a
   gap, so it needs no margin of its own. */
.panel__header-count {
  padding: 0 0.4rem;
  border-radius: var(--radius-sm);
  background: var(--divider);
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-mid);
}

.panel__body { padding: 1.25rem; }

/* A sentence under whatever the body already said — a count, a caveat, an
   empty state below a lede. Its own top margin rather than the previous
   element's bottom one, so it reads the same wherever it is added. */
.panel__note { margin: 0.75rem 0 0; }

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

/* A heading that reads as a label for the box it sits in, not a toolbar
   over a divider — a form's section heading, rather than a panel with
   actions of its own. */
.panel--seamless .panel__header { border-bottom: 0; padding-bottom: 0; margin-bottom: 1.25rem; }
.panel--seamless .panel__header h2 { color: #243fba; }
.panel--seamless .panel__body { padding-top: 0; }

/* A panel that should sit FORWARD of the page rather than blend into it —
   the summary cards at the top of a record, where white on near-white
   left the card edge doing all the work. The tint and a stronger border
   define it; still no shadow (ui-guidelines.md). */
.panel--tinted {
  background: var(--primary-wash);
  border-color: var(--primary-edge);
}


/* A left-aligned run of buttons — "submit" beside "cancel" — as opposed to
   .signup__actions, which spreads a submit button from a note on its right. */
.form-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 0.5rem;
}

/* Procurement puts a form's buttons on the RIGHT, where the eye finishes
   reading the form rather than where it starts. Scoped to the one
   application that asked for it rather than changed in the base rule,
   because every other application's forms are left-aligned today and a
   silent product-wide shift is not this branch's to make.

   Keyed off `data-application`, which is what base.html actually renders.
   (The `[data-app="…"]` icon rules higher up this file match nothing -
   see the PR body.) On a phone the buttons go full width, so alignment
   stops meaning anything and this is simply inert there. */
[data-application="procurement"] .form-actions { justify-content: flex-end; }

/* Procurement's icons are its own deep blue, which is what identifies the
   application (rule 14: an app is an accent colour and an icon, never a
   different-looking product). The colour is stated once, here, as a pair
   of custom properties on the same element base.html already sets
   `--app-accent` on, so the hex appears exactly once and the alphas below
   are the only other thing said about it. It is not in colours.css
   because that file is the Identity Guidelines' palette and this is not
   one of them.

   Scoped to the icons that MARK something - a section's header, a record
   card's badge, a row's item box - and deliberately not to:

     * `.button .icon`, which is white because every live button has a
       blue fill and a dark glyph on it would be unreadable;
     * the rail, whose icons have their own palette on the night-blue and
       where a deep blue would disappear entirely;
     * status chips and `--text-muted` icons, which carry meaning by
       colour already and would start lying if they all went navy.

   The icon boxes are re-tinted with them. A deep-blue glyph left sitting
   in a wash mixed for a different colour reads as a mistake rather than
   as an accent, so the wash and its edge follow the glyph. */
[data-application="procurement"] {
  --procurement-icon: #06387e;
  --procurement-icon-rgb: 6 56 126;
}

[data-application="procurement"] .panel__header .icon,
[data-application="procurement"] .panel__icon-box .icon,
[data-application="procurement"] .grid__icon-box .icon,
[data-application="procurement"] .page-head .icon,
[data-application="procurement"] .eyebrow .icon,
[data-application="procurement"] .k-facts .icon,
[data-application="procurement"] .k-reach .icon {
  color: var(--procurement-icon);
}

[data-application="procurement"] .panel__icon-box,
[data-application="procurement"] .grid__icon-box {
  background: rgb(var(--procurement-icon-rgb) / 12%);
  color: var(--procurement-icon);
}

[data-application="procurement"] .panel__icon-box {
  border-color: rgb(var(--procurement-icon-rgb) / 35%);
}

/* Usually a <ul>: the reset keeps the browser's list indent from pushing
   every row in past the panel's own edge. */
.rows { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--divider); }
.rows > * + * { border-top: 1px solid var(--divider); }

/* Server-side pagination — see includes/_pagination.html. Sits at the foot
   of the panel it paginates, not floating below it. */
.pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem;
  padding: 0.875rem 1.25rem;
  border-top: 1px solid var(--divider);
}

.pagination__summary { margin: 0; font-size: 0.75rem; color: var(--text-muted); }

.pagination__controls {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.pagination__page { font-size: 0.75rem; color: var(--text-mid); }

.pagination__link {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--primary);
}

.pagination__link:hover { text-decoration: none; color: var(--brand-dark); }
.pagination__link .icon { color: currentColor; }

.pagination__link--disabled {
  color: var(--text-muted);
  cursor: default;
}

/* Both link states share one arrow-right icon; the "previous" one is just
   that icon mirrored, the same trick the sidebar's collapse toggle uses. */
.pagination__arrow--prev { display: inline-flex; transform: scaleX(-1); }

/* --- Messages ---------------------------------------------------------- */

.messages { list-style: none; margin: 0 0 1rem; padding: 0; }

.messages li {
  padding: 0.625rem 0.875rem;
  border: 1px solid var(--primary-edge);
  border-radius: var(--radius-sm);
  background: var(--primary-wash);
  color: var(--primary);
  font-weight: 600;
  font-size: 0.8125rem;
  margin-bottom: 0.5rem;
}

/* Warning / error chip colours, from ui-guidelines.md */
.messages li.error,
.messages li.warning {
  border-color: #fecaca;
  background: #fef2f2;
  color: #dc2626;
}

.messages li.success {
  border-color: #bbf7d0;
  background: #f0fdf4;
  color: #059669;
}

.errorlist {
  list-style: none;
  margin: 0 0 0.875rem;
  padding: 0.625rem 0.875rem;
  border: 1px solid #fecaca;
  background: #fef2f2;
  color: #dc2626;
  border-radius: var(--radius-sm);
  font-size: 0.8125rem;
  font-weight: 600;
}

.errorlist li + li { margin-top: 0.25rem; }

/* --- Forms ------------------------------------------------------------- */

.field { margin-bottom: 0.875rem; }

.field label {
  display: block;
  font-size: 0.75rem;
  font-weight: 600;
  color: #000;
  margin-bottom: 0.3rem;
}

/* The star that says a field has to be filled in (`includes/_required.html`).

   Red, because it is the one mark on a form that predicts a refusal, and
   every other colour here means something else - blue is an action,
   mustard is a warning that is not about this field. It is `aria-hidden`
   in the markup: the control carries `required` and a screen reader says
   so, so this is for the eye only.

   `--bad-ink` rather than a red of its own: a required field and the error
   it causes are the same colour, which is what makes the second one read
   as the consequence of the first. */
.field__required {
  margin-left: 0.2rem;
  color: var(--bad-ink);
  font-weight: 700;
}

.field input {
  width: 100%;
  font: inherit;
  font-size: 0.8125rem;
  padding: 0.5rem 0.7rem;
  color: var(--text);
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.field input:disabled,
.field textarea:disabled {
  background-color: var(--page-bg);
  color: var(--text-muted);
  cursor: not-allowed;
}

/* A native date input inside a field. `.field input` above already gives it
   the box; these three lines stop the browser's own widget adding a second
   one inside it, the same fight `.filter-bar__date input` picks lower down.
   Kept here rather than in a date-specific component because a date field
   IS a field — `_field.html` renders it with no special case, and the day
   one needs a special case is the day the two drift. */
.field input[type="date"] { appearance: none; -webkit-appearance: none; }
.field input[type="date"]::-webkit-datetime-edit { padding: 0; }
.field input[type="date"]::-webkit-calendar-picker-indicator {
  opacity: 0.45;
  cursor: pointer;
}

.field input[type="date"]:hover::-webkit-calendar-picker-indicator { opacity: 0.9; }

/* A multi-line field. The same box as `.field input`; the only difference
   is that it can grow. `.code-input` is the variant for a field holding
   markup rather than prose — an email template's body — where a
   proportional face hides a stray tag and a wrapped line hides the
   indentation that makes the markup readable. */
.field textarea {
  width: 100%;
  font: inherit;
  font-size: 0.8125rem;
  padding: 0.5rem 0.7rem;
  color: var(--text);
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  resize: vertical;
  min-height: 8rem;
}

.field textarea.code-input {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  line-height: 1.55;
  white-space: pre;
  overflow-x: auto;
  tab-size: 2;
}

.field .helptext {
  display: block;
  font-size: 0.75rem;
  color: var(--text-muted);
  margin-top: 0.25rem;
}

/* A compound "https://<slug>.<host>" input — prefix and suffix flank the
   one part the visitor actually types. The inner input drops its own
   border so the group reads as one field. */
.field-domain {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--card-bg);
  overflow: hidden;
}

.field-domain:focus-within { border-color: var(--primary); }

.field-domain__prefix {
  display: flex;
  align-items: center;
  padding-left: 0.7rem;
  color: var(--text-muted);
  font-size: 0.8125rem;
  white-space: nowrap;
}

.field-domain input {
  flex: 1 1 auto;
  min-width: 0;
  width: auto;
  border: 0;
  background: transparent;
  padding: 0.5rem 0.4rem;
  font: inherit;
  font-size: 0.8125rem;
  color: var(--text);
}

.field .field-domain input:focus-visible { outline: none; box-shadow: none; }

.field-domain__suffix {
  display: flex;
  align-items: center;
  padding: 0 0.7rem;
  background: var(--page-bg);
  border-left: 1px solid var(--border);
  color: var(--text-muted);
  font-size: 0.8125rem;
  font-weight: 600;
  white-space: nowrap;
}

/* A preview standing in for a text input — click anywhere in the column to
   open the file picker. */
.photo-input {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 0.75rem;
  cursor: pointer;
}

.photo-input__frame {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  aspect-ratio: 1;
  border-radius: var(--radius);
  border: 1px dashed var(--border);
  background: var(--page-bg);
  color: var(--text-muted);
  overflow: hidden;
}

.photo-input__frame img { width: 100%; height: 100%; object-fit: cover; }

.photo-input:hover .photo-input__frame { border-color: var(--primary); color: var(--primary); }

.photo-input__text { display: flex; flex-direction: column; align-items: center; }

.photo-input__action {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--primary);
}

.photo-input__action .icon { color: inherit; }

/* Primary action */
.button {
  display: inline-block;
  font: inherit;
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.5rem 0.75rem;
  color: var(--white);
  background: var(--primary);
  border: 1px solid var(--primary);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.button:hover { filter: brightness(1.06); text-decoration: none; }

/* Ghost / secondary — same fill as the primary action, per the button
   colour rule: every button is #1640FF with white text. */
.button--ghost {
  color: var(--white);
  background: var(--primary);
  border-color: var(--primary);
}

.button--ghost:hover { background: var(--primary); filter: brightness(1.06); }

.button--block { display: block; width: 100%; text-align: center; }

/* --- Centred pages: login, errors -------------------------------------- */

.centred {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: 2rem 1.25rem;
  background: var(--page-bg);
}

.centred__brand-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 700;
  font-size: 1rem;
  color: var(--brand-dark);
  margin-bottom: 1.5rem;
}

/* Signup and "add a company" sit on --gradient-alt instead of the flat
   page background; the brand row is directly on it, outside the card, so
   it needs the same light-on-dark treatment the gradient pages use
   elsewhere (see .shell-content--gradient-alt in launcher.css). */
.centred--gradient-alt { background: var(--gradient-alt); }
.centred--gradient-alt .centred__brand-row { color: var(--white); }

.centred__brand-row .brand-mark { width: 2.25rem; height: 2.25rem; }

/* --- Sign in: brand panel beside the form, not a floating card --------- */

.split {
  display: flex;
  min-height: 100vh;
}

/* The gradient lives on the left panel only — the form side carries no
   background of its own, so it stays whatever the page behind it is. */
.split__hero {
  flex: 1 1 75%;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3rem;
  text-align: center;
  color: var(--white);
  background: #09055c;
  background: linear-gradient(4deg, rgba(9, 5, 92, 1) 33%, rgba(242, 107, 17, 0.82) 100%, rgba(0, 212, 255, 1) 50%);
}

.split__hero-inner { max-width: 26rem; }

.split__hero h1 {
  color: var(--white);
  font-size: 2rem;
  letter-spacing: -0.02em;
}

.split__hero p {
  color: rgb(var(--white-rgb) / 0.78);
  margin-bottom: 0;
}

.split__form {
  flex: 0 0 25%;
  min-width: 22rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3rem 1.5rem;
}

.split__form-inner { width: 100%; max-width: 20rem; }

/* Hard focus ring: this is a prominent form, same treatment as the other
   standalone pages' .centred__card. */
.split__form-inner input:focus-visible { box-shadow: var(--focus-hard); }

.split__title { margin-bottom: 0.375rem; }

.split__subtitle {
  color: var(--text-mid);
  font-size: 0.8125rem;
  margin-bottom: 1.5rem;
}

/* A secondary way out of a form, sitting above its main action rather than
   below it — "forgotten your password?" is read while looking at the
   password field, not after deciding to submit. Right-aligned so it reads as
   attached to the field above rather than as a second heading. */
.split__aside {
  margin: -0.375rem 0 1rem;
  text-align: right;
  font-size: 0.75rem;
}

/* A label icon inside the field, left of the text — email and password on
   the sign-in form only. Composes with the reveal toggle shell.js adds on
   the right: that wraps the input in its own .reveal, this stays outside
   it, so both sit in the same relatively-positioned .field-icon.

   Two things kept the password field's padlock off the screen, and both
   are fixed here. Direct child only: the reveal toggle's own eye icon is a
   descendant of .field-icon too, and a plain `.field-icon .icon` dragged
   it over to the left edge on top of the padlock. And a z-index: .reveal
   is positioned and comes after this icon in the document, so without one
   the wrapped input's own background painted straight over it. */
.field-icon { position: relative; }

.field-icon > .icon {
  position: absolute;
  z-index: 1;
  top: 50%;
  left: 0.75rem;
  transform: translateY(-50%);
  color: var(--text-muted);
  pointer-events: none;
}

.field-icon input { padding-left: 2.25rem; }

@media (max-width: 64rem) {
  .split { flex-direction: column; min-height: 0; }
  .split__hero { flex: 0 0 auto; padding: 2.5rem 1.5rem; }
  .split__form { flex: 1 1 auto; min-width: 0; }
}

.centred__card {
  width: 100%;
  max-width: 23rem;
  background: var(--card-bg);
  border: 1px solid #7e859357;
  border-radius: var(--radius);
  padding: 1.75rem;
}

/* An outsider's page that holds a table — a supplier pricing a purchase
   request's lines (CRM P4-05): the same card, wide enough for the lines.
   The table scrolls in its own box below that. Nothing else changes. */
.centred__card--wide { max-width: 48rem; }

/* The letterhead under an outsider's card: the buying company's own
   details, set apart from the form above them. */
.centred__paper {
  margin-top: 1.75rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--divider);
}

/* --- A two-column form grid, for a page with many short fields --------- */

.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.5rem;
  margin-bottom: 3rem;
}

.form-grid .field { min-width: 0; margin-bottom: 0; }

@media (max-width: 34rem) {
  .form-grid { grid-template-columns: 1fr; }
}

/* Hard focus ring: this is a prominent form. */
.centred__card input:focus-visible { box-shadow: var(--focus-hard); }

.centred__brand {
  font-size: 1.25rem;
  font-weight: 700;
  color: #d3dff7;
  margin: 0 0 0.25rem;
}

/* --- Responsive --------------------------------------------------------
   Breakpoint scale: https://stackoverflow.com/a/7354648 — 320 / 481 / 641 /
   961 / 1025 / 1281. "Big landscape tablets, laptops and desktops" starts
   at 1025px, so that is where the persistent desktop rail starts too: a
   landscape iPad (1024px) still gets the phone's full-screen overlay, not
   the desktop column. */

@media (max-width: 64rem) {
  .shell-body { grid-template-columns: minmax(0, 1fr); }

  .shell-content { padding: 1rem; }

  .rail__toggle { display: none; }

  /* Nothing in the bar is swapped out here any more. The drawer and the
     avatar ARE the narrow-screen design, kept at every width — what used
     to happen at this breakpoint was a second copy of the same navigation
     being switched in, and the two drifted. */
  .topbar__menu-toggle { display: inline-flex; }

  /* The desktop rail sits in the grid and is always on screen; here it
     becomes a full-screen overlay instead — off canvas and taking no space
     at all while closed, covering the whole viewport below the topbar the
     moment it opens. `visibility` is delayed on the way in and applied at
     once on the way out, so a closed drawer's links are never tab-reachable
     even though the transform alone would already put them off screen. */
  .shell-rail {
    position: fixed;
    top: var(--topbar-height);
    left: 0;
    right: 0;
    bottom: 0;
    width: auto;
    height: auto;
    z-index: 30;
    transform: translateX(-100%);
    visibility: hidden;
    transition: transform 220ms ease, visibility 0s linear 220ms;
  }

  body.mobile-sidebar-open .shell-rail {
    transform: translateX(0);
    visibility: visible;
    transition: transform 220ms ease;
  }

  .rail__close { display: inline-flex; }
}

@media (prefers-reduced-motion: reduce) {
  .shell-rail { transition-duration: 0.01ms; }
}

/* The bar is four controls at most: the menu button, the brand, the drawer
   and the avatar. It must fit as it is — the topbar never scrolls sideways
   (a control you have to swipe to reach is a control most people never
   find) and never wraps (that would grow the bar taller and push every page
   down). Only the brand flexes: it is the one item that can give up width,
   and it truncates rather than pushing a control off the end. */
@media (max-width: 64rem) {
  .topbar__menu-toggle,
  .topbar__button,
  .topbar__avatar { flex-shrink: 0; }

  .brand { min-width: 0; flex: 0 1 auto; overflow: hidden; }

  .brand__name,
  .brand__pill {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    display: inline-block;
  }
}

/* Phone. The company name goes — it is in the account panel, and the page
   itself names where you are — leaving the mark, the drawer and the
   avatar. */
@media (max-width: 40rem) {
  .topbar { padding: 0 0.75rem; gap: 0.5rem; }
  .brand__pill { display: none; }
}


/* --- Admin surface primitives ------------------------------------------ */

/* A row of headline numbers, at the top of Admin → Overview. Flat tiles on
   the page background, like everything else — no shadow, ui-guidelines.md. */
.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.75rem;
  /* 1.25rem, the spacing every panel on a page gives every other one.
     It was 1rem, so a row of tiles sat closer to what followed it than
     two cards sit to each other, which reads as the tiles belonging to
     the thing underneath. */
  margin-bottom: 1.25rem;
}

/* A row of tiles under a card - the filter bar on Spend, say - had NO
   top margin at all, so the tiles butted straight onto the card above
   with the two not even separated by the page background. `.panel +
   .panel` could not see them, because a stat row is not a panel. */
.panel + .stat-row,
.stat-row + .panel,
.stat-row + .stat-row { margin-top: 1.25rem; }

.stat {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 0.875rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
}

/* The same tile with its icon beside the figure rather than a label
   above it: for a count that needs a glance, not a reading. */
.stat--figure { flex-direction: row; align-items: center; gap: 0.85rem; }
.stat--figure .stat__text { display: grid; min-width: 0; }

.stat--figure .stat__figure {
  font-size: 1.375rem;
  font-weight: 700;
  line-height: 1.2;
  color: var(--brand-dark);
}

/* gray-500, not gray-400. At 11px these are the smallest text on the
   tile and they NAME the number under them - gray-400 on white is about
   2.5:1, under half what WCAG AA asks. The icon beside it stays the
   lighter grey, because an icon is not text. */
.stat__label {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-mid);
}

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

.stat__value {
  font-size: 1.5rem;
  font-weight: 800;
  color: var(--brand-dark);
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}

/* What the figure is made of - "3 orders received", "2 of 2 asked".
   It is the sentence that lets somebody argue with the number, so it
   has to be readable (same reasoning as `.stat__label`). */
.stat__note { font-size: 0.6875rem; color: var(--text-mid); }

/* A contract's three figures. Narrower than `.stat-row`'s 13rem floor,
   because these sit in one card inside a column and not across a whole
   page, and three of them have to stay on one line down to tablet width
   or the middle one reads as belonging to a different group. */
.stat-row--spend {
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  margin-bottom: 1.25rem;
}

/* The bar under them, with what it is a share of said once beneath it
   rather than repeated on every tile. */
.spend-meter { margin: 0 0 1rem; }
.spend-meter .progress { margin-bottom: 0.4rem; }

.spend-meter__caption {
  margin: 0;
  font-size: 0.75rem;
  color: var(--text-mid);
}

/* A row of figures ABOUT something, rather than counts of it — a
   supplier's answer rate, lead time, rating, spend. The same `.stat` tile
   with three additions: a mark in the corner saying whether the figure
   could be worked out at all, a unit beside the number, and a meter under
   it. Flat and tinted, no shadow (ui-guidelines.md).

   The meter is a proportion of something already on the tile, never a
   score out of a target nobody set: "3.0 / 5.0" fills three fifths, "1 of
   1 asked" fills completely. A figure with no proportion behind it draws
   no meter rather than an empty track, which would read as zero. */
.stat-row--metrics {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin-bottom: 0;
}

.stat--tinted {
  border-color: transparent;
  background: var(--primary-wash);
}

.stat__top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.5rem;
}

.stat__top .stat__label { margin: 0; }

/* The corner mark. It says whether this figure COULD be worked out, and
   nothing about whether the number is good: what counts as a good answer
   rate is a buying rule nobody has set yet (PR-8.2), and a page that
   decided one on its own would be marking suppliers against a rule
   nobody agreed. */
.stat__mark { flex: 0 0 auto; line-height: 0; }
.stat__mark .icon { color: var(--text-muted); }
.stat__mark--unknown .icon { color: var(--text-muted); opacity: 0.65; }

/* The bar under a figure is the shared `.progress` component, not one of
   this tile's own: `<div class="progress"><progress class="progress__bar">`.
   Native <progress> carries the value in an attribute rather than an
   inline width, so a screen reader announces it and nothing is styled
   from the template. */

/* A figure that could not be worked out. The reason takes the number's
   place rather than a dash or a 0: "no orders yet" and 0% are different
   facts, and the second is a lie (PR-5.2-R07). */
.stat__value--none {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text-muted);
}

.stat__unit {
  margin-left: 0.2rem;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--text-muted);
  letter-spacing: 0;
}

@media (max-width: 64rem) {
  .stat-row--metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 40rem) {
  .stat-row--metrics { grid-template-columns: minmax(0, 1fr); }
}

/* One line of a list inside a panel — an application and its seat count. */
.row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.75rem 1.25rem;
}

.row__mark {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 2px;
  flex: 0 0 auto;
}

.row__name { font-weight: 600; color: var(--brand-dark); }

.row__meta {
  font-size: 0.75rem;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.row__value {
  margin-left: auto;
  padding-left: 0.75rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-mid);
  white-space: nowrap;
}

/* A link in a panel header, pushed to the right of the title. */
.panel__action {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
}

.panel__action .icon { color: currentColor; }

/* Facts laid out in pairs — a read-only record. */
.info-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 1rem;
}

.info-grid strong { color: var(--brand-dark); }

/* Rules that are satisfied by definition — the password policy as it
   stands, rather than a checklist someone is working through. */
.rule-list {
  list-style: none;
  margin: 0 0 0.875rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  font-size: 0.8125rem;
}

.rule-list li {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
}

.rule-list .icon { color: var(--good-ink); margin-top: 0.2rem; }

/* Panels stack rather than sitting in a grid — and they keep the same gap
   however they are nested.

   `.panel + .panel` alone could not see across a `<form>`, so a page that
   wrapped two of its panels in one (every create and edit screen) had its
   cards touching: the gap appeared between the panels INSIDE the form and
   nowhere else. The sibling list below covers the four shapes that
   actually occur - panel, form, panel, form - rather than putting a
   bottom margin on `.panel`, which would also push the last one away from
   the foot of whatever contains it. */
.panel + .panel,
.panel + form,
form + .panel,
form + form { margin-top: 1.25rem; }

/* A run of buttons directly after the FIELDS it submits stays tight to
   them (`.form-actions` own margin). One that follows a whole panel is a
   separate block and is spaced like one, or it reads as part of the card
   above it. */
.panel + .form-actions { margin-top: 1.25rem; }
.form-actions + .panel { margin-top: 1.25rem; }

/* Panels that travel together inside one wrapper - a block HTMX swaps as
   a whole, say. The wrapper is spaced as a panel would be, from whatever
   is either side of it, and the panels inside it are already spaced by
   `.panel + .panel`. Without it the first card inside butts straight
   onto the card above, because the wrapper breaks `.panel + .panel`. */
.panel + .panel-group,
.panel-group + .panel,
.panel-group + .form-actions,
.inline-note + .panel-group { margin-top: 1.25rem; }

/* A remark between two cards - "Fewer than three suppliers" - belongs to
   neither, so it is spaced off the card above it rather than tucked under
   its edge the way a note under a field is. */
.panel + .inline-note { margin-top: 1.25rem; }

/* Two panels side by side in a grid are COLUMNS, not a stack. Without
   this the stacking margin above would push the second one down and out
   of line with the first — the grid's own `gap` is already the spacing. */
.form-grid > .panel + .panel { margin-top: 0; }

/* Two cards that belong beside each other rather than under each other —
   where a delivery goes and what has been attached about it. `.form-grid`
   was doing this job on pages that had one, but it is named for a form's
   fields and carries a form's spacing; this says what it is.

   `align-items: start` so a short card keeps its own height instead of
   stretching to match a tall one beside it, and the panels inside are
   columns rather than a stack, same as the rule above. Below 1025px they
   go one under the other — phone territory on the scale in CLAUDE.md,
   where two columns of card is two half-width cards. */
.panel-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.25rem;
  align-items: start;
}

.panel-pair > .panel + .panel { margin-top: 0; }

.panel + .panel-pair,
.panel-pair + .panel,
.panel-pair + .panel-pair,
form + .panel-pair,
.panel-pair + form { margin-top: 1.25rem; }

/* One of the two is three short lines and the other is a list that
   grows with a form under it, so an even split left the first mostly
   empty. A quarter against three quarters, both the same height, and
   whichever overflows scrolls inside its own card rather than making
   the row taller than the screen.

   The height is the TALLER CARD'S, not a number. `align-items: stretch`
   already makes a grid row as tall as its tallest item and the other
   card match it, so `max-height` is the only figure needed: a cap, for
   the day the attachments are a long list. Setting `height` instead
   pinned both cards at the cap whatever was in them, which on an order
   with nothing attached was most of a screen of white. */
.panel-pair--quarter {
  grid-template-columns: 1fr 3fr;
  align-items: stretch;
}

.panel-pair--quarter > .panel {
  max-height: 30rem;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.panel-pair--quarter > .panel > .panel__header { flex: 0 0 auto; }

.panel-pair--quarter > .panel > .panel__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

@media (max-width: 64rem) {
  .panel-pair { grid-template-columns: 1fr; }
  /* Stacked, they are no longer a row, so there is no height to match -
     and a scroll box inside a page that already scrolls is the worse of
     the two readings. */
  .panel-pair--quarter > .panel { max-height: none; }
  .panel-pair--quarter > .panel > .panel__body { overflow-y: visible; }
}

/* --- One table's paging, as one control -------------------------------- */

/* Where you are, how many at a time, and the way back and forward, on
   one line under the table they belong to (`_table_controls.html`). It
   sits inside the card rather than under it, because it is about that
   table and not about the page. */
.table-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.75rem;
  padding: 0.75rem 1.25rem;
  border-top: 1px solid var(--divider);
  font-size: 0.75rem;
  color: var(--text-muted);
}

.table-controls__range { margin: 0; }
.table-controls__size { margin: 0; }

/* The steps sit opposite the range, so the eye finds them in the same
   place on every table. */
.table-controls__pages {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-left: auto;
}

.table-controls__step {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-weight: 600;
  color: var(--text-mid);
  text-decoration: none;
}

a.table-controls__step:hover { border-color: var(--primary); color: var(--primary); }
.table-controls__step .icon { color: currentColor; }

/* The end of the run: still drawn, so the control keeps its shape and
   nothing jumps as somebody pages through it. */
.table-controls__step--off { opacity: 0.45; }

.table-controls__where { white-space: nowrap; }

@media (max-width: 40rem) {
  .table-controls__pages { margin-left: 0; }
}

/* The way out of a record's own page, above its header and clear of it:
   a link back that sits under the last card reads as part of that card.
   Spaced from the page head below it rather than tucked against it. */
.back-link { margin: 0 0 1rem; }

/* --- A card that folds away ---------------------------------------------- */

/* `<details>` and `<summary>`, so the fold is the browser's and needs no
   script: the header IS the control, it keeps the keyboard behaviour it
   would have had, and two of them on one page fold independently with
   nothing coordinating them.

   The marker is ours, on the right, because the default triangle sits
   left of the icon and reads as a second bullet beside it. */
.panel--foldable > summary {
  cursor: pointer;
  list-style: none;
}

.panel--foldable > summary::-webkit-details-marker { display: none; }

.panel--foldable > summary::after {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  margin-left: auto;
  border-right: 2px solid var(--text-muted);
  border-bottom: 2px solid var(--text-muted);
  transform: rotate(45deg) translate(-0.125rem, -0.125rem);
  transition: transform 0.15s ease;
}

.panel--foldable[open] > summary::after {
  transform: rotate(225deg) translate(-0.125rem, -0.125rem);
}

/* Folded, the header is the whole card, so it should not keep the line
   that separated it from a body that is no longer there. */
.panel--foldable:not([open]) > summary { border-bottom: 0; }

.panel--foldable > summary:focus-visible {
  outline: 2px solid var(--sky-blue);
  outline-offset: -2px;
}

/* A short list of records inside a card - the files attached to an
   order, the entries in its activity. It had no rules at all and was
   rendering as a raw bulleted list with its qualifier run on behind it;
   this is the same shape the grids already use, one row per line with
   the detail under the name rather than beside it. */
.k-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.6rem;
}

.k-list > li {
  display: grid;
  gap: 0.1rem;
  font-size: 0.8125rem;
}

.k-list > li > .muted { font-size: 0.75rem; }

/* Where a delivery goes. An address is read as a block, so it is set as
   one - its own lines, not a paragraph with breaks in it - and the
   destination's name leads, because that is what somebody checks first. */
.delivery {
  display: grid;
  gap: 0.1rem;
  font-style: normal;
  font-size: 0.875rem;
  color: var(--text);
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--page-bg);
}

.delivery__who { font-weight: 600; color: var(--text-strong); }

/* What kind of destination it is - a one-off address, a site of the
   company's, or its own registered address because nobody said. The
   card is otherwise three lines that look the same whichever it is. */
.delivery__source {
  margin: 0.5rem 0 0;
  font-size: 0.75rem;
  color: var(--text-muted);
}

/* When it is due, or when it arrived, under the address it applies to. */
.delivery__when { margin-top: 0.75rem; }

/* --- Avatars ----------------------------------------------------------- */

/* One person, round, with their initials when there is no photo. Six tints,
   picked from the account id by `avatar_tone` (apps/core/templatetags/nav.py)
   so a person keeps one colour across every page and every session.

   Backgrounds are tints of the three brand tokens plus three neighbours
   that stay distinguishable from them; every pairing below clears 4.5:1
   against its ink, because initials are text. */
.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  border-radius: 50%;
  object-fit: cover;
  font-weight: 700;
  letter-spacing: 0.01em;
  line-height: 1;
  text-transform: uppercase;
  user-select: none;
  background: var(--page-bg);
  color: var(--text-mid);
}

.avatar--s { width: 1.75rem; height: 1.75rem; font-size: 0.6875rem; }
.avatar--m { width: 2rem; height: 2rem; font-size: 0.75rem; }
.avatar--l { width: 2.5rem; height: 2.5rem; font-size: 0.875rem; }

.avatar--tone-1 { background: #dbe3ff; color: #1233c9; }
.avatar--tone-2 { background: #ffe9c7; color: #8a4b00; }
.avatar--tone-3 { background: #d9f2e3; color: #136b37; }
.avatar--tone-4 { background: #f1dcff; color: #6b21a8; }
.avatar--tone-5 { background: #ffe0e0; color: #b01616; }
.avatar--tone-6 { background: #d5eef5; color: #0e6579; }

/* A photo withheld during a support session. Not the empty state — somebody
   HAS a photo here and it is deliberately not being served. Dashed, so it
   reads as a gap that is meant to be there rather than as a failed load. */
.avatar--withheld {
  background: var(--page-bg);
  border: 1px dashed var(--border);
  color: var(--text-soft);
}

.avatar--withheld .icon { width: 0.875rem; height: 0.875rem; }

/* --- A record's facts, as label and value ------------------------------ */

/* For the header of a detail page: a handful of fields about one record,
   read rather than edited. Not a `.grid` — a table of one row with six
   columns is a table pretending, and it wraps badly. Shared rather than
   written into the one page that needed it first (rule 14). */
.detail-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.875rem 1.5rem;
  margin: 0;
}

.detail-list > div { min-width: 0; }

.detail-list dt {
  margin: 0 0 0.2rem;
  color: var(--label-blue);
  font-size: 0.75rem;
  font-weight: 600;
}

.detail-list dd {
  margin: 0;
  color: var(--text);
  font-size: 0.8125rem;
  overflow-wrap: anywhere;
}

/* Detail cards: icon-box + label/value, laid out in a grid. Each card is a
   flex row with the icon box on the left and the text stacked on the right.
   Used on record detail pages (PR-1.2, SB-1.2). */
.detail-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem 1.25rem;
  margin: 0;
}

.detail-cards__item {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  min-width: 0;
}

.detail-cards__text { min-width: 0; }

.detail-cards__label {
  display: block;
  margin: 0 0 0.25rem;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-muted);
}

.detail-cards__value {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text);
  overflow-wrap: anywhere;
}

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

/* Two columns rather than three, for a `.detail-cards` inside a card that
   is itself half the page wide. */
.detail-cards--two { grid-template-columns: repeat(2, 1fr); }

@media (max-width: 64rem) {
  .detail-cards { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 40rem) {
  .detail-cards { grid-template-columns: 1fr; }
}

/* --- A record's status, and who holds it -------------------------------

   Three small primitives for a record's "where it has got to" card: the
   status restated with how far along it is and what it is waiting on,
   who is holding it (an avatar, their name, whether they are still
   active, a one-line caption and one action), and a row of small counts.
   Built for the request page (PR-1.2-R01), but none of them mentions a
   request: a hiring manager, an opportunity's owner and a supplier
   contact answer the same "who, and how far" shape.

   The lines between the parts are `--border`, not `--divider`: on a
   tinted panel gray-100 does not show at all. */

.status-block {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem;
}

/* THE ONE UPPERCASE LABEL. ui-guidelines.md and rule 13 rule out
   uppercase tracking-wide labels everywhere, and `.eyebrow` stays
   sentence case for that reason. This card was asked for with this
   label by name (October 2026), so it is an exception with an address,
   not a new default: do not reach for this class anywhere else, and do
   not turn `.eyebrow` into it. The text in the template stays sentence
   case, so it still reads as words to a screen reader and when copied. */
.status-block__label {
  display: block;
  margin-bottom: 0.35rem;
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.status-line {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.status-line__value {
  margin: 0;
  font-size: 1.125rem;
  font-weight: 800;
  line-height: 1.2;
  color: var(--brand-dark);
}

.person-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
}

.person-row__text { min-width: 0; flex: 1 1 auto; }

.person-row__name {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text-strong);
}

.person-row__caption {
  margin: 0.1rem 0 0;
  font-size: 0.6875rem;
  color: var(--text-muted);
}

.person-row__action { flex: 0 0 auto; font-size: 0.75rem; white-space: nowrap; }

/* A row of small counts about a record: one flat white tile each, the
   label and its icon over the number, centred. Smaller than `.stat`,
   which is for headline numbers worth a card of their own. */
.fact-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.6rem;
  margin-top: 1rem;
}

.fact-row__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  min-width: 0;
  padding: 0.6rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--page-bg);
  text-align: center;
}

.fact-row__label {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-muted);
}

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

.fact-row__value {
  font-size: 1rem;
  font-weight: 700;
  color: var(--brand-dark);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 40rem) {
  .fact-row { grid-template-columns: minmax(0, 1fr); }
}

/* A record's own facts, two to a row: a small icon and a label over the
   value, a line between rows. A `<dl>`, because that is what it is. The
   columns have no gap of their own - the left cell's padding makes it -
   so the line between rows runs the full width rather than breaking at
   the gutter. */
.fact-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  row-gap: 1rem;
  margin: 0;
}

.fact-grid__item { min-width: 0; padding-right: 1rem; }
.fact-grid__item:nth-child(even) { padding-right: 0; }

.fact-grid__item:nth-child(n + 3) {
  padding-top: 1rem;
  border-top: 1px solid var(--border);
}

.fact-grid__label {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  margin: 0 0 0.3rem;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-muted);
}

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

.fact-grid__value {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text-strong);
  overflow-wrap: anywhere;
}

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

.fact-grid__sub {
  display: block;
  margin-top: 0.15rem;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-muted);
}

@media (max-width: 40rem) {
  .fact-grid { grid-template-columns: minmax(0, 1fr); }
  .fact-grid__item { padding-right: 0; }
  .fact-grid__item:nth-child(n + 2) {
    padding-top: 1rem;
    border-top: 1px solid var(--border);
  }
}

/* Item icon box: smaller, muted variant for table rows. */
.grid__icon-box {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  flex-shrink: 0;
  border-radius: var(--radius-sm);
  background: var(--primary-wash);
  color: var(--primary);
}

/* Three-dot actions menu for grid rows. */
.grid__more {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}

.grid__more:hover { background: var(--divider); color: var(--text); }

/* --- The support session bar ------------------------------------------- */

/* Drawn under the topbar, on every page, for the whole of a live session,
   and only for the person holding it. Standing rather than dismissible: it
   is true until the session ends, and it carries the clock the session is
   running against. Full-bleed and above the rail, because it is a statement
   about the entire window rather than about the page inside it. */
.support-bar {
  position: sticky;
  top: var(--topbar-height);
  z-index: 19;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem 0.875rem;
  padding: 0.5rem 1rem;
  border-bottom: 1px solid var(--warn-edge);
  background: var(--warn-bg);
  color: var(--warn-ink);
  font-size: 0.8125rem;
}

.support-bar .icon { color: currentColor; }
.support-bar strong { font-weight: 700; }
.support-bar__note { color: inherit; opacity: 0.85; }
.support-bar__spacer { flex: 1 1 auto; }

/* --- Floating menus ---------------------------------------------------- */

/* The account menu and the application drawer.

   Positioned by CSS against their own `.topbar__menu` box, and NOT by
   JavaScript measuring the button. The measuring version bound `:style` on
   the same element as `x-show`; Alpine re-applies a `:style` binding by
   rewriting the element's inline style, which wiped the `display: none`
   x-show had set — so opening either panel revealed both. Nothing but
   `x-show` touches `display` now.

   Anchored to the RIGHT edge: these hang off the last two controls in the
   bar, and a panel growing rightwards from there runs off the screen. */
.topbar__menu { position: relative; display: inline-flex; flex: 0 0 auto; }

.menu-panel {
  position: absolute;
  top: calc(100% + 0.375rem);
  right: 0;
  z-index: 40;
  min-width: 15rem;
  max-width: calc(100vw - 1rem);
  max-height: calc(100vh - var(--topbar-height) - 1.5rem);
  overflow-y: auto;
  padding: 0.35rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
  box-shadow: var(--panel-float);
}

.menu-panel__who {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.6rem 0.65rem 0.5rem;
  max-width: 17rem;
}

.menu-panel__who-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.menu-panel__who strong {
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--brand-dark);
  overflow: hidden;
  text-overflow: ellipsis;
}

.menu-panel__who span {
  font-size: 0.6875rem;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Which company, and what this person is in it. A row rather than a line of
   the block above, because "you are in Acme, as an Owner" is the thing
   somebody opens this menu to check. */
.menu-panel__company {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin: 0 0.65rem 0.4rem;
  padding: 0.3rem 0.5rem;
  border: 1px solid var(--divider);
  border-radius: var(--radius-sm);
  background: var(--page-bg);
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-mid);
}

.menu-panel__company b { color: var(--brand-dark); font-weight: 700; }

.menu-item {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  width: 100%;
  padding: 0.45rem 0.6rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text);
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}

.menu-item:hover {
  background: var(--page-bg);
  color: var(--brand-dark);
  text-decoration: none;
}

.menu-item .icon { color: var(--text-muted); }
.menu-item:hover .icon { color: var(--brand-dark); }

/* Signing out is the one destructive row, and it is the one people reach
   for by colour rather than by reading. */
.menu-item--danger { color: var(--bad-ink); }
.menu-item--danger .icon,
.menu-item--danger:hover .icon { color: var(--bad-ink); }
.menu-item--danger:hover { background: var(--bad-bg); color: var(--bad-ink); }

/* A count or a hint pushed to the right of a row: "2 companies". */
.menu-item__hint {
  margin-left: auto;
  padding-left: 0.5rem;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-muted);
}

.menu-sep {
  height: 1px;
  margin: 0.3rem 0.2rem;
  background: var(--divider);
}

.menu-form { margin: 0; }

/* --- The application drawer -------------------------------------------- */

/* Every application this person can open, as tiles. What is NOT here is the
   point: an application the company has not bought, or that this person
   holds no seat in, is absent rather than greyed out (§2.5). */
.drawer { width: 19rem; padding: 0.875rem; }

.drawer__title {
  margin: 0 0 0.6rem;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-muted);
}

.drawer__tiles {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.35rem;
}

.drawer__tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  padding: 0.65rem 0.25rem;
  border-radius: var(--radius-sm);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--brand-dark);
  text-align: center;
  line-height: 1.2;
}

.drawer__tile:hover { background: var(--page-bg); text-decoration: none; }

.drawer__tile[aria-current="true"] {
  background: var(--primary-wash);
  color: var(--primary);
}

/* The tile's mark carries the application's accent — §2.6's "identified by
   an accent colour and an icon", at the one moment somebody is choosing
   between applications. */
.drawer__mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.375rem;
  height: 2.375rem;
  border-radius: 10px;
  background: var(--tile-accent, var(--brand-dark));
  color: var(--white);
}

.drawer__mark .icon { color: currentColor; }

/* Administering the company is not one of the applications it bought, so
   its tile is marked out rather than sitting in the row as an equal. */
.drawer__tile--platform .drawer__mark {
  background: var(--card-bg);
  color: var(--brand-dark);
  border: 1px solid var(--border);
}

.drawer__foot {
  margin: 0.7rem 0 0;
  padding-top: 0.6rem;
  border-top: 1px solid var(--divider);
  font-size: 0.6875rem;
  color: var(--text-muted);
  max-width: none;
}

/* Below the phone breakpoint the panel is the width of the screen rather
   than a card floating near one corner of it. */
/* On a phone the panel is the width of the screen rather than a card
   floating in one corner of it — so it leaves its button's box and is
   measured against the viewport instead. */
@media (max-width: 30rem) {
  .drawer { width: auto; }

  .menu-panel {
    position: fixed;
    top: calc(var(--topbar-height) + 0.375rem);
    left: 0.5rem;
    right: 0.5rem;
    min-width: 0;
  }

  .drawer__tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* --- Banners ----------------------------------------------------------- */

/* A standing condition the page wants read before anything else — "this
   company has only one administrator". Not a flash message: those are
   about what just happened and go away. */
.banner {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  margin: 0 0 1rem;
  padding: 0.75rem 0.875rem;
  border: 1px solid var(--warn-edge);
  border-radius: var(--radius);
  background: var(--warn-bg);
  color: var(--warn-ink);
  font-size: 0.8125rem;
}

/* The gap under it is the one every card gives the next thing, so a
   banner above a card is spaced from it exactly as two cards are. It
   was 1rem, which read as the banner being attached to what followed. */
.banner { margin-bottom: 1.25rem; }

/* `max-width: none` and not merely the absence of a cap: the global
   `p` rule sets the 68ch reading measure on EVERY paragraph, so
   taking this one's own cap away quietly handed it back. A message
   in a box fills the box - the frame has already decided the width,
   and a sentence stopping a third of the way across it reads as the
   text having failed to lay out rather than as a measure. */
.banner__body p,
.banner__body strong { max-width: none; }

.banner .icon { color: currentColor; margin-top: 0.15rem; }
.banner__body { min-width: 0; }
.banner__body strong { display: block; color: var(--warn-ink); }
/* No `--measure` here. A banner is a BOX, and the box is already as
   wide as the column it sits in; capping the sentence inside it at 68ch
   left two thirds of a full-width card empty and read as the text
   having failed to lay out. The reading measure is for running prose on
   an open page, not for a two-line notice in a frame that has already
   set its own width (`.content-wrap` caps that at 72rem). */
.banner__body p { margin: 0.15rem 0 0; }
/* A banner that is all body and no bold line - an intro sentence in its
   box - has nothing above that paragraph to be spaced from. */
.banner__body > p:first-child { margin-top: 0; }
.banner a { color: inherit; text-decoration: underline; font-weight: 600; }

/* A banner leading a page needs no space above it; one following
   something does, or it reads as attached to whatever is above - the
   same spacing panels give each other. */
.panel + .banner,
.banner + .banner,
.panel-pair + .banner,
.k-reach + .banner,
.stat-row + .banner,
.page-head + .banner,
form + .banner { margin-top: 1.25rem; }

.banner--bad {
  border-color: var(--bad-edge);
  background: var(--bad-bg);
  color: var(--bad-ink);
}

.banner--bad .banner__body strong { color: var(--bad-ink); }

.banner--good {
  border-color: var(--good-edge);
  background: var(--good-bg);
  color: var(--good-ink);
}

.banner--good .banner__body strong { color: var(--good-ink); }

/* --- Chips ------------------------------------------------------------- */

/* A small factual label: a tenant role, an entitlement. Not a status. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.15rem 0.45rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--page-bg);
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-mid);
  white-space: nowrap;
}

/* The company role a person holds, wherever it is shown — the companies
   page, the account menu, the People list. Owner reads as the strongest
   because it is the only one that cannot be taken away by somebody else. */
.chip--owner {
  background: var(--primary-wash);
  border-color: var(--primary-edge);
  color: var(--primary);
}

.chip--admin {
  background: var(--warn-bg);
  border-color: var(--warn-edge);
  color: var(--warn-ink);
}

/* Whether an application is ready to be switched on — the backoffice's
   Applications screen and, for a company let into one, its own. Preview
   reads as "look at this" and released as "this is fine", from the same
   semantic triplets a banner uses; neither picks its own colour. */
.chip--preview {
  background: var(--warn-bg);
  border-color: var(--warn-edge);
  color: var(--warn-ink);
}

.chip--released {
  background: var(--good-bg);
  border-color: var(--good-edge);
  color: var(--good-ink);
}

/* A circumstantial fact in amber - "waiting for something", never an
   error. The same palette `.chip--admin` and `.chip--preview` happen to
   share for their own reasons; this is the generic one for everywhere
   else a status note needs the colour (PR-1.2-R01's "Awaiting quote
   requests", beside the status it explains). */
.chip--warn {
  background: var(--warn-bg);
  border-color: var(--warn-edge);
  color: var(--warn-ink);
}

.chip--warn .icon { color: currentColor; }

/* A position, not a judgement - "Stage 1 of 4". The same blue wash an
   owner chip uses, because it says where something is rather than
   whether that is good or bad. */
.chip--primary {
  background: var(--primary-wash);
  border-color: var(--primary-edge);
  color: var(--primary);
}

.launcher__foot {
  margin-top: 1.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
  font-size: 0.8125rem;
  color: var(--text-muted);
  max-width: var(--measure);
}

/* --- Password reveal --------------------------------------------------- */

.reveal { position: relative; display: block; }

.reveal input { padding-right: 2.5rem; }

.reveal__toggle {
  position: absolute;
  top: 50%;
  right: 0.35rem;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}

.reveal__toggle:hover { color: var(--brand-dark); background: var(--page-bg); }
.reveal__toggle .icon { color: currentColor; }

/* --- Password checklist ------------------------------------------------ */

/* Built by shell.js under any field marked data-password-rules. Mirrors the
   validators; the server is still the one that refuses. */
.pw-checks {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.2rem 0.75rem;
  margin: 0.45rem 0 0;
  padding: 0;
  font-size: 0.6875rem;
}

.pw-checks li {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  color: var(--text-muted);
}

.pw-checks li.is-ok { color: var(--good-ink); }

.pw-checks i {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 0.875rem;
  height: 0.875rem;
  flex: 0 0 auto;
  border: 1.5px solid currentColor;
  border-radius: 50%;
}

.pw-checks li.is-ok i {
  background: var(--good-ink);
  border-color: var(--good-ink);
  color: var(--white);
}

.pw-checks .icon { color: currentColor; }

@media (max-width: 30rem) {
  .pw-checks { grid-template-columns: minmax(0, 1fr); }
}

/* --- Busy state ------------------------------------------------------- */

/* Signing up provisions a schema, so the wait is real. A disabled button
   that says what it is doing beats a dead one that invites a second click. */
.button.is-busy {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  opacity: 0.75;
  cursor: progress;
}

/* A button nothing will happen on. Grey and flat, deliberately NOT the
   brand blue: every live button in the product is #1640FF with white
   text, so a refused one that keeps that fill reads as an action
   somebody can take, and gets pressed. The Requests list's "Merge
   ticked" is the case this was written for - it is disabled until two
   rows are ticked (`[data-merge-gate]` in shell.js), which is most of
   the time somebody is looking at it.

   The fill is `--disabled-bg`, gray-300, which is DARKER than either
   surface a button sits on - the gray-50 page behind
   `.page-head__actions` and a white panel - so the control still reads
   as a button rather than as a hole in the page. Gray-50 was the first
   attempt and was exactly the page's own colour; gray-200 was a step
   most people would not see. The label is gray-700 on it, 7:1, and the
   edge gray-400, so the shape is legible from either side.

   Only the colours change. The shape, the size and the padding stay, so
   the control does not move or reflow under the pointer at the moment it
   becomes live.

   `.is-busy` is excluded on purpose. A busy button is disabled too - for
   the second click - but it is WORKING rather than refusing, and it says
   so beside its own spinner, so it keeps its colour. */
.button:disabled { cursor: not-allowed; }

.button:disabled:not(.is-busy) {
  color: var(--text);
  background: var(--disabled-bg);
  border-color: var(--text-muted);
}

/* `--icon` is white, for the blue fill every live button has. On the grey
   one it would be invisible, so the icon follows the label instead. */
.button:disabled:not(.is-busy) .icon { color: currentColor; }

.button:disabled:not(.is-busy):hover { filter: none; background: var(--disabled-bg); }

.spinner {
  display: inline-block;
  width: 0.75rem;
  height: 0.75rem;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 600ms linear infinite;
  flex: 0 0 auto;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .spinner { animation-duration: 2s; }
}

/* While a form is busy, stop the rest of it looking clickable. */
form.is-busy .picker__label,
form.is-busy .field { opacity: 0.65; pointer-events: none; }

/* --- A credential shown once ------------------------------------------ */

/* The temporary password an admin reads out. Deliberately loud: it is on
   screen once and never again. */
.credential {
  border: 1px solid var(--primary-edge);
  background: var(--primary-wash);
  border-radius: var(--radius);
  padding: 1.15rem 1.25rem;
  margin-bottom: 0.75rem;
}

.credential h2 {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}

.credential__pair {
  display: grid;
  gap: 0.4rem;
  margin: 0.75rem 0;
}

.credential__row {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  flex-wrap: wrap;
}

.credential__key {
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-mid);
  min-width: 5.5rem;
}

.credential__value {
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--brand-dark);
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.2rem 0.5rem;
  user-select: all;
}

/* A button that reads as a link. For a destructive-free action that must be
   a POST, like signing out. */
.linkish {
  font: inherit;
  font-size: inherit;
  padding: 0;
  border: 0;
  background: none;
  color: var(--primary);
  cursor: pointer;
  text-decoration: underline;
}

/* --- Status chips ------------------------------------------------------ */

/* "Seat active", "Access ended", "No record here yet". A state, not a
   label — .chip is the one for a fact that does not change. */

.status-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  font-size: 0.6875rem;
  font-weight: 600;
  border: 1px solid #bbf7d0;
  background: #f0fdf4;
  color: #059669;
}

.status-chip__dot { width: 0.4rem; height: 0.4rem; border-radius: 999px; background: #059669; }

/* gray-500 on gray-50 clears AA; gray-400 on it was 2.4:1, and this is
   the chip that says Cancelled, Ended, Access ended - the states a
   reader most needs to be sure they read correctly. The dot stays the
   lighter grey: it is a shape, not text. */
.status-chip--off { border-color: var(--border); background: var(--page-bg); color: var(--text-mid); }
.status-chip--off .status-chip__dot { background: var(--text-muted); }

/* A chip that is a NOTE ON SOMETHING ELSE rather than a column of its
   own - the "came from" mark beside a request's title, where the title
   is what a reader is scanning and the chip only says where the row
   arrived from. At the full size it competed with the title it was
   annotating; here it is the kit's smallest type (0.625rem is already
   the floor in five other places), with the padding and the dot
   brought down in step so the pill keeps its shape. Nothing else
   changes: same component, same colours, said quieter. */
.status-chip--small {
  gap: 0.25rem;
  padding: 0.1rem 0.45rem;
  font-size: 0.625rem;
}
.status-chip--small .status-chip__dot { width: 0.3rem; height: 0.3rem; }

/* --- A server-side filter bar, and the access grid ---------------------
   Shared components, not the launcher's own furniture: the Admin
   application's People and Activity screens are built from them, and an
   application that needed a filtered table would be too. They live here
   rather than in launcher.css for the reason every shared component does —
   one design system, and an application never writes its own. */

/* --- A server-side filter bar: search, a multi-select, a page-size choice */

.filter-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.45rem;
  padding: 0.75rem 1.25rem;
  border-bottom: 1px solid var(--divider);
  background: var(--page-bg);
}

.filter-bar__search {
  position: relative;
  flex: 1 1 11rem;
  min-width: 8rem;
  display: flex;
}

.filter-bar__input {
  width: 100%;
  font: inherit;
  font-size: 0.75rem;
  padding: 0.35rem 2.1rem 0.35rem 0.6rem;
  color: var(--text);
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.filter-bar__search-btn {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 2.1rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: var(--primary);
  color: var(--white);
  cursor: pointer;
}

.filter-bar__search-btn:hover { filter: brightness(1.06); }
.filter-bar__search-btn .icon { color: inherit; }

.filter-bar__field { position: relative; flex: 0 0 auto; }

.filter-bar__select {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font: inherit;
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.35rem 0.55rem;
  color: var(--text);
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  white-space: nowrap;
}

.filter-bar__select:hover { border-color: var(--primary-edge); }

/* A flat, bordered popover — no box-shadow anywhere, per ui-guidelines.md. */
.filter-bar__panel {
  position: absolute;
  z-index: 10;
  top: calc(100% + 0.35rem);
  left: 0;
  min-width: 13rem;
  padding: 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
}

.filter-bar__option {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  /* It is a <label> on most pages and a <p> on one — the "no applications"
     note in the backoffice's people filter — so the reset belongs here
     rather than on that one template. */
  margin: 0;
  padding: 0.4rem 0.3rem;
  border-radius: var(--radius-sm);
  font-size: 0.8125rem;
  cursor: pointer;
}

.filter-bar__option:hover { background: var(--page-bg); }

.filter-bar__panel-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-top: 0.4rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--divider);
}

.filter-bar__apply {
  font: inherit;
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.35rem 0.7rem;
  color: var(--white);
  background: var(--primary);
  border: 1px solid var(--primary);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.filter-bar__apply:hover { filter: brightness(1.06); }

/* A date filter in a filter bar: a word saying which date, and the input.

   The pill is on the INPUT rather than on a wrapper around both, which is
   the second attempt at this. A wrapper holding the border while the input
   goes borderless is the tidier markup and it is also one rule away from
   the failure it was drawn to prevent: if anything stops that pair of rules
   applying together, what is left is the browser's own square box sitting
   inside a rounded one. Styling the element that has a native box means the
   worst case is a control that is merely plain, never one that is wrong. */
.filter-bar__date {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  gap: 0.4rem;
}

.filter-bar__date-label {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text-muted);
  white-space: nowrap;
}

/* The same shape as .filter-bar__select beside it — padding, weight, radius
   and border all match, so the row reads as one set of controls rather than
   pills next to boxes. `appearance: none` first: a date input is a native
   widget and every engine gives it chrome of its own (Chrome a bordered
   field, Safari a rounded stepper, Firefox its own padding), which is what
   puts square corners on it however the border is set. */
.filter-bar__date input[type="date"] {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.35rem 0.55rem;
  color: var(--text);
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  line-height: 1.25;
}

.filter-bar__date input[type="date"]:hover { border-color: var(--primary-edge); }
.filter-bar__date input[type="date"]:focus { border-color: var(--primary); }

/* Chrome lays the segments out in a box that carries its own padding on top
   of ours; Safari indents the stepper. Both are the widget asserting a
   shape the rule above has already set. */
.filter-bar__date input[type="date"]::-webkit-datetime-edit { padding: 0; }
.filter-bar__date input[type="date"]::-webkit-inner-spin-button { display: none; }

.filter-bar__date input[type="date"]::-webkit-calendar-picker-indicator {
  margin: 0 0 0 0.25rem;
  padding: 0;
  opacity: 0.45;
  cursor: pointer;
}

.filter-bar__date input[type="date"]:hover::-webkit-calendar-picker-indicator,
.filter-bar__date input[type="date"]:focus::-webkit-calendar-picker-indicator {
  opacity: 0.9;
}

.filter-bar__clear { font-size: 0.75rem; font-weight: 600; }

.filter-bar__reset {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-muted);
  white-space: nowrap;
}

.filter-bar__reset:hover { color: var(--brand-dark); text-decoration: none; }
.filter-bar__reset .icon { color: inherit; }

/* --- One row of filters, and one row only -------------------------------

   Above the collapse breakpoint every filter is shown inline, and the
   bar wrapped them onto a second and a third line as pages grew a
   fourth and fifth picker. That pushed the table down the screen, and
   gave two pages with the same controls bars of different heights
   depending on how long a company happened to be called.

   The row does not wrap now. The controls shrink and truncate instead,
   the search giving up its space first - what somebody types in it is
   short, where what a select says is a whole sentence ("Buying for:
   Acme Manufacturing Ltd"). A select keeps a floor so it never shrinks
   to something nobody can read, and the Reset link never shrinks at
   all: it is the way out.

   Below the breakpoint none of this applies - the bar collapses behind
   the Filters toggle there, which is the phone answer and already
   one row.                                                            */
@media (min-width: 64.0625rem) {
  .filter-bar { flex-wrap: nowrap; }

  /* Nothing may claim its full natural width at the row's expense. */
  .filter-bar > * { min-width: 0; }

  /* It takes the slack, at twice the rate of anything else, and is
     still the first to give room back when there is none. A search
     box too narrow to show what was typed in it is the one control
     on the bar whose whole job is reading back. */
  .filter-bar__search { flex: 2 1 14rem; min-width: 8rem; }

  /* **A `<select>` is as wide as its LONGEST OPTION, not as its
     chosen one.** "Buying for: All" was taking the room "Buying for:
     Acme Manufacturing Ltd" would need, on every page where a company
     picker exists, and the search was paying for it. Capped, so the
     closed control is sized for reading an answer rather than for the
     longest one it could ever hold; the full name is there when it
     opens.

     The floor is sized for the worst bar in the product - six selects
     and a search beside a Reset, which is Requests and Contracts - so
     the row still fits at the narrowest width this query covers with
     the rail open. Between the two they take their natural width. */
  .filter-bar__select,
  .filter-bar__field {
    flex: 0 1 auto;
    min-width: 4.5rem;
    max-width: 14rem;
  }

  /* `white-space: nowrap` is already on the select; these two are what
     turn the overflow into an ellipsis rather than a clipped word. */
  .filter-bar__select {
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .filter-bar__reset { flex: 0 0 auto; }
}

/* --- The seat grid ----------------------------------------------------- */

.grid-scroll { overflow-x: auto; }

/* --- A table that becomes a list on a phone -----------------------------

   A data table cannot be made narrow; below about 640px the honest
   choices are a sideways scroll or a stack, and a sideways scroll on a
   list people live in is a list they stop reading. `.grid--stack` turns
   each ROW into a block: the row's own `<th scope="row">` is its title,
   and every cell becomes a line labelled from `data-label`.

   **Opt-in, by the table.** `.grid` is shared by every application, and a
   table whose cells carry no `data-label` would stack into a column of
   unexplained values - worse than the scroll it replaced. So nothing
   changes for anybody until they add the class, and `includes/_th.html`
   stays the single source of a column's name either way.

   A cell with no label still stacks, and simply shows its value: the
   component degrades to "readable" rather than to "broken" while a
   screen is being labelled up.                                           */

@media (max-width: 40rem) {
  .grid--stack,
  .grid--stack thead,
  .grid--stack tbody,
  .grid--stack tr,
  .grid--stack th,
  .grid--stack td {
    display: block;
  }

  /* The header row is the labels' source, not something to draw: each
     cell repeats its own column name beside the value instead. Hidden
     accessibly - a screen reader still gets the real header. */
  .grid--stack thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .grid--stack tr {
    padding: 0.85rem 0;
    border-top: 1px solid var(--divider);
  }

  .grid--stack tbody tr:first-child { border-top: 0; }

  .grid--stack th,
  .grid--stack td {
    padding: 0.2rem 0;
    border-top: 0;
  }

  /* The row's own heading cell is the block's title. */
  .grid--stack th[scope="row"] {
    font-size: 0.875rem;
    font-weight: 700;
    color: var(--brand-dark);
  }

  /* Label left, value right - the shape a phone reads fastest, and the
     same one `.k-facts` uses, so the two do not look like two systems. */
  .grid--stack td[data-label] {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
  }

  .grid--stack td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 auto;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--text-muted);
  }

  /* A numeric column is right-aligned in a table because its neighbours
     are; alone on a line it has nothing to line up with. */
  .grid--stack .grid__num,
  .grid--stack .line-items__num { text-align: left; }

  /* An empty cell would otherwise draw its label against nothing. */
  .grid--stack td:empty { display: none; }

  /* A stacked block has the width a phone has, so a headline wraps here
     rather than being cut: cutting it would hide the only words on the
     block, and there is no column to keep it out of. */
  .grid--stack td.grid__cell--headline {
    max-width: none;
    white-space: normal;
    overflow: visible;
    text-align: right;
  }

  /* Alone on a line, the actions have nothing to line up against, and
     the icon column is a line of its own with its label beside it. */
  .grid--stack th.grid__th--actions,
  .grid--stack td.grid__cell--actions,
  .grid--stack td.grid__cell--channel {
    width: auto;
    text-align: left;
    white-space: normal;
  }

  .grid--stack td.grid__cell--actions .row-actions { justify-content: flex-start; }

  /* The action cell sits at the foot of its block, not squeezed beside a
     label. */
  .grid--stack td:last-child:not([data-label]) { padding-top: 0.5rem; }

  /* A line-item row that is EDITED stacks too, and its inputs take the
     width a phone actually has rather than the column width they were
     sized for. */
  .grid--stack .line-items__input { width: 100%; }

  /* The line-item tables carry their own cell padding for a dense grid;
     stacked, that density is what makes the block unreadable. */
  .grid--stack.line-items th,
  .grid--stack.line-items td { padding: 0.2rem 0; }

  /* Nothing to scroll once it is a list. */
  .grid-scroll:has(.grid--stack) { overflow-x: visible; }
}

.grid { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }

.grid th,
.grid td {
  text-align: left;
  padding: 0.6rem 0.85rem;
  border-top: 1px solid var(--divider);
  vertical-align: middle;
}

/* --- Two rows that are not one of the list ------------------------------

   A grid is a list of like things. Two rows on Budgets are not: the
   Total is a sum OF the rows, and Uncategorised is the remainder that
   can never carry a budget. Both were drawn exactly like the
   categories between them, so the eye had to read the first cell to
   tell them apart. Their own ground says it before anything is read.

   Tints, not borders: a heavier rule would read as the table ending,
   and Uncategorised sits in the middle of it.                        */
.grid__row--total > th,
.grid__row--total > td {
  background: var(--primary-wash);
  border-top: 2px solid var(--primary-edge);
  font-weight: 700;
  color: var(--text-strong);
}

.grid__row--aside > th,
.grid__row--aside > td { background: var(--page-bg); }

.grid thead th {
  background: var(--page-bg);
  border-top: 0;
  border-bottom: 1px solid var(--border);
}

/* A column header that explains itself on hover — includes/_th.html.
   CSS only: the bubble is in the document, hidden, and shown by :hover and
   :focus-within, so it needs no script and works the same for a pointer and
   a keyboard. `aria-describedby` on the trigger is what a screen reader
   reads instead of hovering, so the explanation is not pointer-only.

   Dotted underline rather than an info icon: a row of icons across a header
   competes with the data underneath, and every header here has a tip, so
   the icon would be decoration repeated N times rather than a signal. */
.grid__th { position: relative; }

/* One checkbox that ticks or clears a whole column of row checkboxes at
   once (shell.js's data-tick-all). Its own cursor, ahead of the label so
   it never lands between the label and its tooltip - see _th.html. */
.grid__select-all {
  margin-right: 0.5rem;
  cursor: pointer;
  vertical-align: middle;
}

/* What a select-all is reaching beyond the page somebody can see. Shown
   only while the header is fully ticked, by shell.js - a selection that
   includes rows nobody is looking at has to say so.

   **A line of its own, above the table.** It used to be `inline-block`
   with a left margin, which put it hard against the header row with
   nothing between them: the table read as though it had scrolled up
   underneath the sentence. It is a statement about the whole selection,
   so it gets its own line and its own space.

   **And `display` here is what made `hidden` stop working.** shell.js
   hides it with the attribute (`saying.hidden = !everything`), and the
   browser's own `[hidden] { display: none }` is a UA rule that any
   author rule setting `display` beats - so this sentence was on screen
   on every one of these pages whether anything was ticked or not. The
   `[hidden]` line below is what gives the attribute back its meaning.
   `.topbar__badge` and `.ai-slot__action` carry the same line for the
   same reason; a kit-wide `[hidden] { display: none !important; }`
   would retire all four and is worth doing deliberately rather than in
   passing. */
.tick-all-note {
  display: block;
  margin: 0 0 0.5rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--primary);
}

.tick-all-note[hidden] { display: none; }

.grid__th-label {
  cursor: help;
}
.grid__th-label:focus-visible {
  outline: 2px solid var(--sky-blue);
  outline-offset: 2px;
  border-radius: 2px;
}
.grid__tip {
  position: absolute;
  top: calc(100% + 0.35rem);
  left: 0.85rem;
  z-index: 20;
  width: max-content;
  max-width: 16rem;
  padding: 0.5rem 0.65rem;
  border: 1px solid color-mix(in srgb, var(--mustard-yellow) 30%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--mustard-yellow) 10%, var(--card-bg));
  color: var(--text);
  font-size: 0.75rem;
  font-weight: 400;
  line-height: 1.45;
  white-space: normal;
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.12s ease;
}
/* Each tip is shown by its OWN control, never by anything focused
   inside the header. It was `.grid__th:focus-within .grid__tip`, which
   is right while a header holds one tip and wrong the moment it holds
   two - a header with an action beside its label (the plus that adds a
   category) drew both bubbles, stacked on each other, as soon as
   either was reached. The label is `tabindex="0"`, so naming it keeps
   the keyboard exactly as it was. */
.grid__th-label:hover + .grid__tip,
.grid__th-label:focus + .grid__tip,
.grid__th-action:hover + .grid__tip,
.grid__th-action:focus-visible + .grid__tip {
  visibility: visible;
  opacity: 1;
}

/* An action that belongs to a COLUMN rather than to a row: a quiet
   icon beside the header's words, same size as the kit's other inline
   icons. It is a real link to a page that does the same thing, so it
   still works with no script (see _th.html's `action`). */
.grid__th-action {
  display: inline-flex;
  align-items: center;
  margin-left: 0.35rem;
  padding: 0.1rem;
  border-radius: var(--radius-sm);
  color: var(--primary);
  vertical-align: middle;
}
/* `.icon` sets a colour of its own, and on an application's page the
   accent rule (`[data-app="procurement"] .icon`) sets another - so an
   icon does NOT inherit the colour of the thing it sits in unless it
   is told to. This is an action, and every action in the product is
   brand blue (rule 13), not the application's accent. Same shape as
   `.topbar__button .icon`. */
.grid__th-action .icon { color: currentColor; }
.grid__th-action:hover {
  background: color-mix(in srgb, var(--primary) 10%, transparent);
}
.grid__th-action:focus-visible {
  outline: 2px solid var(--sky-blue);
  outline-offset: 1px;
}

/* The last columns would push their bubble past the table's right edge. */
.grid thead th:last-child .grid__tip,
.grid thead th:nth-last-child(2) .grid__tip {
  left: auto;
  right: 0.85rem;
}

.grid__merge-wrap {
  position: relative;
  display: inline-flex;
  cursor: not-allowed;
}
.grid__merge-tip {
  position: absolute;
  top: calc(100% + 0.35rem);
  left: 0;
  z-index: 20;
  width: max-content;
  max-width: 16rem;
  padding: 0.5rem 0.65rem;
  border: 1px solid color-mix(in srgb, var(--mustard-yellow) 30%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--mustard-yellow) 10%, var(--card-bg));
  color: var(--text);
  font-size: 0.75rem;
  font-weight: 400;
  line-height: 1.45;
  white-space: normal;
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.12s ease;
}
.grid__merge-wrap:hover .grid__merge-tip,
.grid__merge-wrap:focus-within .grid__merge-tip {
  visibility: visible;
  opacity: 1;
}

.grid__app {
  white-space: nowrap;
  font-size: 0.75rem;
  color: var(--text-mid);
}

.grid__mark {
  display: inline-block;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 2px;
  margin-right: 0.3rem;
}

.grid__person { min-width: 12rem; }
.grid__person-inner { display: flex; align-items: center; gap: 0.6rem; }
.grid__name { display: flex; align-items: center; gap: 0.5rem; font-weight: 600; color: var(--brand-dark); }
.grid__person code { font-size: 0.6875rem; color: var(--text-muted); }
/* A word under a cell's value saying what KIND of value it is, not a
   second value: "requested", where the Started column is showing the
   request's own date because access never began. */
.grid__qualifier { display: block; font-size: 0.6875rem; color: var(--text-muted); }

/* A second line under a cell's value — a link into the record, a note about
   it. Same scale as the qualifier above, with a gap, because this one sits
   under a value rather than reading as part of it. */
.grid__cell-sub { display: block; font-size: 0.6875rem; margin-top: 0.25rem; }
.grid__cell form { margin: 0; }

/* A person in a grid row is `.avatar` now — see includes/_avatar.html. The
   `.grid__avatar` pair that used to live here went with the person-icon
   fallback it existed to style. */

.grid__actions { display: inline-flex; align-items: center; gap: 0.4rem; }

/* A button inside a table row is at the row's scale, not the page's. At the
   full .button size a two-word label like "End now" wraps, and a wrapped
   button makes every row in the table taller than any of them needs to be.
   `nowrap` is the half that actually fixes it — the smaller type is so that
   it does not have to be wide to avoid wrapping. */
.grid__actions .button {
  font-size: 0.6875rem;
  padding: 0.3rem 0.6rem;
  white-space: nowrap;
}

/* Same reasoning for the state chip beside them: "Active · until 13:01"
   broken over two lines is a taller row saying nothing more. */
.grid__cell .status-chip { white-space: nowrap; }

/* A row still worth showing, but not one of the ones being acted on — a
   ticked request the merge page has to leave out, and whatever else later
   needs to say "listed, not included". Dimmed rather than removed, because
   somebody ticked it and is owed the reason; the cell that carries the
   reason stays at full strength so it can still be read. */
.grid__row--muted .grid__cell,
.grid__row--muted th.grid__cell { opacity: 0.55; }

.grid__row--muted .grid__cell:has(.status-chip) { opacity: 1; }

/* Free text in a table needs a ceiling or it takes the width the columns
   either side of it need. The template still truncates — this is the
   column's share of the table, that is the length of the sentence, and a
   cell wants both: without the ceiling one long unbroken word still pushes
   the row, and without the truncation a paragraph wraps to six lines
   inside the ceiling. */
.grid__cell--reason,
.grid th.grid__cell--reason {
  max-width: 20rem;
  min-width: 10rem;
}

/* --- The actions column ------------------------------------------------

   The last column of a list is the row's own verbs - Open, Review, Set,
   Retire - and it is now named rather than left as an empty header that
   a reader has to infer from the buttons under it. Right-aligned and as
   narrow as its content, so the data columns keep the width and the
   buttons sit against the edge they are read from.

   `width: 1%` is the table trick for "as narrow as this will go": an
   auto-layout table gives a percentage column its content width when the
   percentage is smaller than the content needs, which is always here. */
.grid th.grid__th--actions,
.grid td.grid__cell--actions {
  text-align: right;
  width: 1%;
  white-space: nowrap;
}

/* A cell of several chips still packs them to the right. */
.grid td.grid__cell--actions .row-actions { justify-content: flex-end; }

/* --- A column of one line, cut at the column's width -------------------

   A table of paragraphs is not a table. The template already caps what
   it puts in the HTML; this caps what is DRAWN, at whatever width the
   column actually gets, and the whole text is on the cell's `title`. */
.grid td.grid__cell--headline {
  max-width: 26rem;
  min-width: 8rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- A column that is one icon ----------------------------------------

   Narrow and fixed: the icon is the whole content, so the column must
   not take a share of the table as though it held words. */
.grid th.grid__th--channel,
.grid td.grid__cell--channel {
  width: 2.75rem;
  text-align: center;
  padding-left: 0.5rem;
  padding-right: 0.5rem;
}

/* The icon itself, in the ink the rest of the row is read in. Shape
   carries the meaning - a speech bubble against an envelope - so this
   never becomes a colour code (rule 13). */
.channel-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
}

.channel-mark .icon { color: inherit; }

.grid__view-link {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.6875rem;
  font-weight: 600;
  padding: 0.25rem 0.55rem;
  border: 1px solid var(--primary-edge);
  border-radius: var(--radius-sm);
  color: var(--primary);
  white-space: nowrap;
}

.grid__view-link:hover {
  background: var(--primary-wash);
  color: var(--primary);
  text-decoration: none;
}

.grid__view-link .icon { color: inherit; }

.seat {
  font: inherit;
  font-size: 0.6875rem;
  font-weight: 600;
  padding: 0.25rem 0.5rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  white-space: nowrap;
}

.seat:hover { border-style: solid; color: var(--brand-dark); }

.seat--on {
  border-style: solid;
  border-color: var(--primary-edge);
  background: var(--primary-wash);
  color: var(--primary);
}

.seat--on .icon { color: var(--primary); }

/* A seat cell for somebody whose access has ended. There is nothing to
   click — the seats were released — so it reads as a placeholder rather
   than a button that refuses. */
.seat--none {
  border-style: solid;
  border-color: transparent;
  cursor: default;
  color: var(--text-muted);
}

/* The status chip sits under the name in the people grid, where it needs
   to start a line of its own rather than trail the email address. */
.grid__person .status-chip { margin-top: 0.3rem; }

.plain-list { list-style: none; margin: 0; padding: 0; font-size: 0.8125rem; }
.plain-list li { padding: 0.3rem 0; border-top: 1px solid var(--divider); }
.plain-list li:first-child { border-top: 0; }

.field--error input { border-color: #fecaca; }

.centred__note {
  margin: 0.75rem 0 0;
  font-size: 0.75rem;
  color: var(--text-muted);
  text-align: center;
}


/* --- The access grid's role cells --------------------------------------- */

/* One dropdown per cell, in a form of its own that submits on change. No
   Save button anywhere on the grid: a page of thirty dropdowns and one Save
   is a page where somebody changes four things and loses three of them. Each
   change is its own POST and its own audit entry.

   `.cell-form` exists only to stop the form's own box adding a line. */
.cell-form { margin: 0; display: block; }

.role-select {
  font: inherit;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--brand-dark);
  background-color: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.3rem 1.6rem 0.3rem 0.5rem;
  cursor: pointer;
  max-width: 100%;
  /* The chevron, drawn in CSS so there is no second image to load and no
     emoji anywhere near a control. */
  appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--text-muted) 50%),
    linear-gradient(135deg, var(--text-muted) 50%, transparent 50%);
  background-position: calc(100% - 0.8rem) 50%, calc(100% - 0.55rem) 50%;
  background-size: 0.25rem 0.25rem;
  background-repeat: no-repeat;
}

.role-select:hover { border-color: var(--primary-edge); }

/* "No access" is the empty state of a cell, and it should read as one —
   dashed and grey, so a row of them is visibly a row of nothing rather than
   a row of settings. */
.role-select--none {
  color: var(--text-muted);
  border-style: dashed;
}

/* A role somebody may not change: the last owner, or a column the viewer
   does not hold the permission for. Plain text, not a disabled control —
   a greyed-out dropdown invites somebody to work out how to un-grey it. */
.role-fixed {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-mid);
}

.role-fixed--muted { color: var(--text-muted); font-weight: 400; }
.role-fixed__why .icon { color: var(--text-muted); }

/* The Admin column is about the company rather than an application, so it
   is separated from the application columns rather than sitting among them
   as an equal. */
.grid__app--company,
.grid__cell--company {
  background: var(--primary-wash);
  border-right: 1px solid var(--primary-edge);
}

.grid thead th.grid__app--company { color: var(--primary); }

.grid__app small {
  display: block;
  font-weight: 400;
  font-size: 0.6875rem;
  color: var(--text-muted);
  text-transform: none;
  letter-spacing: 0;
}

/* Somebody who belongs to the company but has no record inside it yet —
   a state to fix, not a failure. */
.status-chip--warn {
  border-color: var(--warn-edge);
  background: var(--warn-bg);
  color: var(--warn-ink);
}

.status-chip--warn .status-chip__dot { background: var(--warn-ink); }

/* Buttons at the right of a page heading. */
.page-head__actions {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  flex-wrap: wrap;
}

/* The one tick box that costs money: "give me a seat in anything I add",
   on Admin → Applications. Deliberately heavier than a plain checkbox row,
   because it is a decision rather than a preference. */
.seat-me {
  display: flex;
  align-items: flex-start;
  gap: 0.65rem;
  margin: 1rem 0 0;
  padding: 0.875rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
  cursor: pointer;
}

.seat-me:hover { border-color: var(--primary-edge); }
.seat-me input { margin-top: 0.15rem; flex: 0 0 auto; }

.seat-me span {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  font-size: 0.75rem;
  color: var(--text-muted);
}

.seat-me strong { font-size: 0.8125rem; color: var(--brand-dark); }

/* --- Two-factor authentication ----------------------------------------- */

/* The enrolment page: QR beside the typed-out secret, because a desktop
   authenticator and a phone with a broken camera both need the second one
   and a QR nobody can read is otherwise a dead end. */
.mfa-setup {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 1.5rem;
  align-items: start;
}

.mfa-setup__qr {
  flex: 0 0 auto;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--white);
  line-height: 0;
}

.mfa-setup__qr svg { display: block; }

.mfa-setup__manual { min-width: 0; }

/* The shared secret, typed by hand into an app. Spaced and monospaced,
   because every character of it has to survive being read off a screen. */
.mfa-setup__secret {
  display: block;
  margin: 0.35rem 0 0.75rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--page-bg);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  color: var(--brand-dark);
  word-break: break-all;
}

@media (max-width: 40rem) {
  .mfa-setup { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
}

/* Recovery codes, shown once. Two columns of monospace so they can be read
   down a page and copied without losing a line. */
.code-list {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 0.35rem;
  margin: 0 0 0.875rem;
  padding: 0;
}

.code-list li {
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--page-bg);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  letter-spacing: 0.04em;
  color: var(--brand-dark);
  text-align: center;
}

/* The same panel, for an outcome that is good news rather than a secret to
   copy down. Nothing here disappears on reload, so it does not need to
   shout — green, and calm, against the blue one above it that does. */
.credential--calm {
  border-color: var(--good-edge);
  background: var(--good-bg);
}

.credential--calm h2 { color: var(--good-ink); }
.credential--calm h2 .icon { color: var(--good-ink); }

/* --- Notifications ------------------------------------------------------ */

/* The bell, its badge, its panel, and the full list page. One set of row
   styles for both, because a notification should not look like two
   different things depending on where it is read.

   No new component was invented for any of this: the panel is `.menu-panel`,
   the kind markers are `.chip`, the filter is the `.filter-bar` select the
   People screen uses, and the button is `.button--ghost`. What is here is
   what genuinely did not exist — a count on a control, and a row. */

/* A screen-reader-only label. A utility rather than a component, and the
   first one here: the badge is a number with no word beside it, which reads
   as "3" and nothing else without this. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The bell needs to be the positioning context for its own badge, and
   `.topbar__menu` cannot be: that box is what the PANEL is positioned
   against, and a badge anchored to it would sit at the panel's corner
   rather than the button's. */
.topbar__button--bell { position: relative; }

/* An application's own count beside the bell (apps/core/chrome.py). It is
   a link rather than a button, because the page it opens is the list — so
   it needs the anchor reset the buttons never did, and the same
   positioning context for the same reason as the bell above. */
.topbar__button--count {
  position: relative;
  text-decoration: none;
}

/* Mustard, not red. Red is for something that has gone wrong; an approval
   waiting on somebody is work, not a fault — and the same colour carries
   the approval chip below, so the badge and the rows it stands for agree. */
.topbar__badge {
  position: absolute;
  top: 0.15rem;
  right: 0.1rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1rem;
  height: 1rem;
  padding: 0 0.22rem;
  border-radius: 999px;
  background: var(--mustard-yellow);
  color: #fff;
  font-size: 0.625rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  pointer-events: none;
}

/* `hidden` has to win over the flex above, or a nought-count badge is an
   empty mustard dot on every page. */
.topbar__badge[hidden] { display: none; }

/* Wider than the account menu: a subject line needs room to be read, and
   truncating the one sentence that says what is waiting defeats the panel. */
.menu-panel--notifications { min-width: 21rem; }

.menu-panel__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.5rem 0.65rem 0.4rem;
}

.menu-panel__head h4 {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--brand-dark);
}

.menu-panel__head-note {
  font-size: 0.6875rem;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- Rows -------------------------------------------------------------- */

.notif-list {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.notif-list--page {
  gap: 0;
  padding: 0.25rem 0;
}

.notif {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 0.55rem 0.65rem;
  border-radius: var(--radius-sm);
}

.notif-list--page .notif {
  border-radius: 0;
  padding: 0.85rem 1.25rem;
  border-bottom: 1px solid var(--divider);
}

.notif-list--page .notif:last-child { border-bottom: 0; }

/* Unread is a separate axis from kind — either kind can be either — so it
   is a ground and a weight rather than a third chip colour. */
.notif--unread { background: var(--primary-wash); }
.notif--unread .notif__subject { font-weight: 700; color: var(--brand-dark); }

/* An approval request is work waiting on somebody, so on the full list it
   also carries a stripe: scanning a long page for "what needs me" should
   not mean reading every chip. Not in the panel, where ten rows of stripe
   is noise rather than signal. */
.notif-list--page .notif--approval {
  border-left: 3px solid var(--mustard-yellow);
  padding-left: calc(1.25rem - 3px);
}

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

.notif__when {
  font-size: 0.6875rem;
  color: var(--text-muted);
  white-space: nowrap;
}

.notif__subject {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text);
}

.notif__body {
  margin: 0;
  font-size: 0.75rem;
  color: var(--text-muted);
}

/* Pushed to the end of the head row, so the whole list's marks line up
   however long a subject runs. */
.notif__form { margin: 0 0 0 auto; }

.notif__mark {
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--primary);
  cursor: pointer;
}

.notif__mark:hover { text-decoration: underline; }

.notif-clear {
  margin: 0;
  padding: 0 0.65rem 0.4rem;
  display: flex;
  justify-content: flex-end;
}

.notif-clear__button {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  border: 0;
  background: none;
  padding: 0.2rem 0.25rem;
  font: inherit;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--primary);
  cursor: pointer;
}

.notif-clear__button:hover { text-decoration: underline; }
.notif-clear__button .icon { color: currentColor; }

.notif-empty {
  margin: 0;
  padding: 1rem 1.25rem;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

/* Kind markers. Both are `.chip` — only the palette differs, because the
   two mean different things rather than being different shapes. */
.chip--approval {
  background: var(--warn-bg);
  border-color: var(--warn-edge);
  color: var(--warn-ink);
}

.chip--notice {
  background: var(--primary-wash);
  border-color: var(--primary-edge);
  color: var(--primary);
}

.chip--approval .icon,
.chip--notice .icon { color: currentColor; }

/* Tighter rows inside the bell's panel than on the full list. The panel is a
   glance at five, the page is a record of everything — the same row, read at
   two different distances.

   The mark-read control moved into the head row (see
   _notification_rows.html), which took a line off every row; these trim the
   space that line used to justify. */
.menu-panel--notifications .notif { padding: 0.45rem 0.6rem; gap: 0.15rem; }

/* One line, ellipsised. The body is a transition label or a comment —
   enough to recognise, and the subject above it is the thing being read.
   The full list shows it whole. */
.menu-panel--notifications .notif__body {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Five rows fit well inside the viewport, so this only matters on a short
   window — and there it keeps the "See all notifications" link at the foot
   reachable rather than pushed off the bottom. */
.menu-panel--notifications { max-height: min(32rem, calc(100vh - var(--topbar-height) - 2rem)); }

/* === The Step 0 shared kit ================================================
   Everything from here to the end of the file is the shared kit: the
   components every application would otherwise invent for itself (rule 14).
   Each is a whole section of its own, appended rather than interleaved, so
   parallel branches never touch the same lines.

   Media queries below this marker are held to the fixed scale by
   apps/core/tests/test_stylesheets.py: 320 / 481 / 641 / 961 / 1025 / 1281,
   in rem at the root 16px. A `min-width` lands ON the threshold (1025px ->
   64.0625rem) and a `max-width` one pixel below it (1024px -> 64rem), so
   the two never collide. Desktop is the baseline; a query only ever adds
   the narrower behaviour on top of it. */

/* --- Tabs ------------------------------------------------------------- */

/* Sections of one record or one screen. Each tab is its own address, so the
   current one is a real `aria-current="page"` rather than a class toggled by
   script, and a link to it can be sent — the same decision the backoffice's
   company tabs made. Wraps rather than scrolls: a tab you have to swipe to
   find is a tab most people never find. */
.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin: 0 0 1rem;
  padding: 0;
  list-style: none;
  border-bottom: 1px solid var(--border);
}

.tabs__tab {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: -1px;
  padding: 0.55rem 0.75rem;
  border: none;
  border-bottom: 2px solid transparent;
  background: none;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text-mid);
  white-space: nowrap;
  cursor: pointer;
}

.tabs__tab:hover { color: var(--brand-dark); text-decoration: none; }

.tabs__tab[aria-current="page"] {
  color: var(--primary);
  border-bottom-color: var(--primary);
}

.tabs__tab[aria-current="page"] .icon { color: currentColor; }

.tabs__count {
  padding: 0 0.4rem;
  border-radius: var(--radius-sm);
  background: var(--divider);
  font-size: 0.6875rem;
  color: var(--text-mid);
}

/* --- Board ------------------------------------------------------------ */

/* Columns of cards, one column per workflow stage. A board is genuinely
   wide, so on a desktop it is the one thing besides a wide table allowed to
   scroll sideways, inside its own box; below 1025px the columns stack.

   The terminal rail is where finished stages go — Done, Won, Lost — as a
   narrow column of counts rather than full columns of cards nobody is
   working on any more. */
.board {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  overflow-x: auto;
  padding-bottom: 0.25rem;
}

.board__column {
  display: flex;
  flex: 1 0 15rem;
  flex-direction: column;
  min-width: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--page-bg);
}

.board__column-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 0.875rem;
  border-bottom: 1px solid var(--divider);
}

.board__column-title { margin: 0; font-size: 0.8125rem; }

.board__count {
  margin-left: auto;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-muted);
}

.board__cards {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-height: 4rem;
  margin: 0;
  padding: 0.625rem;
  list-style: none;
}

.board__card {
  padding: 0.625rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
}

.board__card:focus-within { box-shadow: var(--focus-soft); }

.board__card-title {
  display: block;
  font-weight: 600;
  color: var(--text-strong);
  overflow-wrap: anywhere;
}

.board__card-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.4rem;
  font-size: 0.6875rem;
  color: var(--text-muted);
}

/* An empty column still says it is a column. Without this a stage with no
   cards reads as a rendering fault. */
.board__empty {
  margin: 0;
  padding: 0.75rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  font-size: 0.75rem;
  color: var(--text-muted);
  text-align: center;
}

.board__rail {
  display: flex;
  flex: 0 0 11rem;
  flex-direction: column;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.board__rail-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-mid);
}

a.board__rail-item:hover {
  border-color: var(--primary-edge);
  color: var(--primary);
  text-decoration: none;
}

@media (max-width: 64rem) {
  .board { flex-direction: column; align-items: stretch; overflow-x: visible; }
  .board__column,
  .board__rail { flex: none; width: 100%; }
}

/* --- Stepper ---------------------------------------------------------- */

/* Where a multi-step flow is up to. A step's number comes from a CSS
   counter, so the template writes the steps in order and nothing else.

   The steps SHARE THE ROW and are JOINED by a line, so the thing reads as
   one run rather than five chips that happen to sit near each other. The
   line is the step's own ::after rather than a span in the template, which
   keeps the markup a plain <ol> of <li> and means a flow with four steps
   or seven needs nothing added. */
.stepper {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 0;
  margin: 0 0 1.25rem;
  padding: 0;
  list-style: none;
  counter-reset: stepper;
}

.stepper__step {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex: 1 1 0;
  min-width: 9rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text-muted);
  counter-increment: stepper;
}

/* The line to the next step. The last one has nothing to join to, so it
   neither grows nor draws one. `min-width` keeps a connector visible when
   the labels are long enough to use up the row. */
.stepper__step::after {
  content: "";
  flex: 1 1 auto;
  min-width: 1rem;
  height: 2px;
  margin-left: 0.75rem;
  border-radius: 999px;
  background: var(--border);
}

.stepper__step:last-child { flex: 0 1 auto; }
.stepper__step:last-child::after { content: none; }

/* A step behind you is joined by a line behind you too. */
.stepper__step--done::after { background: var(--good-strong); }

.stepper__marker {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--card-bg);
  font-size: 0.8125rem;
}

.stepper__marker:empty::before { content: counter(stepper); }

/* The only two markers carrying an icon are the current one (blue) and a
   done one (green), and the mark reads white on both. */
.stepper__marker .icon { color: var(--white); }

.stepper__step--current { color: var(--brand-dark); }

.stepper__step--current .stepper__marker {
  border-color: var(--primary);
  background: var(--primary);
  color: var(--white);
}

.stepper__step--done { color: var(--text-strong); }

.stepper__step--done .stepper__marker {
  border-color: var(--good-edge);
  background: var(--good-strong);
  color: var(--text-strong);
}

/* A flow that stopped short — CRM's lost opportunity — rather than one
   still under way or finished. */
.stepper__step--bad { color: var(--bad-ink); }

.stepper__step--bad .stepper__marker {
  border-color: var(--bad-edge);
  background: var(--bad-bg);
  color: var(--bad-ink);
}

/* --- Checklist -------------------------------------------------------- */

/* Things to do, some done. Not the password checklist (.pw-checks), which
   is a live validator rather than work somebody ticks off. */
.checklist { margin: 0; padding: 0; list-style: none; }

.checklist__summary {
  margin: 0 0 0.5rem;
  font-size: 0.75rem;
  color: var(--text-mid);
}

.checklist__item {
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  padding: 0.6rem 0;
  border-top: 1px solid var(--divider);
}

.checklist__item:first-child { border-top: 0; }

.checklist__box {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 1rem;
  height: 1rem;
  margin-top: 0.15rem;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--card-bg);
  accent-color: var(--primary);
}

.checklist__box .icon { color: currentColor; }

.checklist__label { flex: 1; min-width: 0; overflow-wrap: anywhere; }

.checklist__meta {
  font-size: 0.6875rem;
  color: var(--text-muted);
  white-space: nowrap;
}

.checklist__item--done .checklist__box {
  border-color: var(--good-edge);
  background: var(--good-bg);
  color: var(--good-ink);
}

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

/* --- Activity trail --------------------------------------------------- */

/* A record's history, newest first, drawn from the audit log it already
   has — includes/_activity_trail.html reads what audit.resolve_actors
   gives it and writes nothing of its own. */
.timeline { margin: 0; padding: 0; list-style: none; }

.timeline__entry {
  position: relative;
  display: flex;
  gap: 0.75rem;
  padding: 0 0 1rem;
}

.timeline__entry:not(:last-child)::before {
  content: "";
  position: absolute;
  top: 1.5rem;
  bottom: 0;
  left: 0.6875rem;
  width: 1px;
  background: var(--border);
}

.timeline__marker {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 1.375rem;
  height: 1.375rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--page-bg);
}

.timeline__body { flex: 1; min-width: 0; font-size: 0.8125rem; }

.timeline__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.25rem 0.5rem;
}

.timeline__actor { font-weight: 600; color: var(--text-strong); }

.timeline__when {
  margin-left: auto;
  font-size: 0.6875rem;
  color: var(--text-muted);
  white-space: nowrap;
}

.timeline__detail {
  margin: 0.25rem 0 0;
  color: var(--text-mid);
  overflow-wrap: anywhere;
}

/* One of ours, during a support session — marked, never hidden. */
.timeline__entry--support .timeline__marker {
  border-color: var(--warn-edge);
  background: var(--warn-bg);
  color: var(--warn-ink);
}

.timeline__entry--support .timeline__marker .icon { color: currentColor; }

/* Logged by a person — a call, a meeting — rather than written by the
   system. The same timeline; the marker says which kind of entry it is. */
.timeline__entry--by-hand .timeline__marker {
  border-color: var(--primary-edge);
  background: var(--primary-wash);
  color: var(--primary);
}

.timeline__entry--by-hand .timeline__marker .icon { color: currentColor; }

/* What was written, as typed. */
.timeline__notes {
  margin: 0.375rem 0 0;
  color: var(--text-strong);
  overflow-wrap: anywhere;
}

.timeline__meta {
  margin: 0.25rem 0 0;
  font-size: 0.6875rem;
  color: var(--text-muted);
}

.timeline__action {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.6875rem;
  font-weight: 600;
  text-decoration: none;
}

.timeline__action:hover,
.timeline__action:focus-visible { text-decoration: underline; }

/* --- Progress --------------------------------------------------------- */

/* A native <progress>, so the value is an attribute and never an inline
   width — rule 33 forbids the latter, and a screen reader already knows
   what a <progress> is. */
.progress {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  font-size: 0.75rem;
}

.progress__bar {
  flex: 1;
  min-width: 4rem;
  height: 0.5rem;
  overflow: hidden;
  border: 0;
  border-radius: 999px;
  background: var(--divider);
  color: var(--primary);
  -webkit-appearance: none;
  appearance: none;
}

.progress__bar::-webkit-progress-bar { border-radius: 999px; background: var(--divider); }
.progress__bar::-webkit-progress-value { border-radius: 999px; background: var(--primary); }
.progress__bar::-moz-progress-bar { border-radius: 999px; background: var(--primary); }

.progress__label { font-weight: 600; color: var(--text-mid); white-space: nowrap; }

.progress--good .progress__bar { color: var(--good-ink); }
.progress--good .progress__bar::-webkit-progress-value { background: var(--good-ink); }
.progress--good .progress__bar::-moz-progress-bar { background: var(--good-ink); }

.progress--bad .progress__bar { color: var(--bad-ink); }
.progress--bad .progress__bar::-webkit-progress-value { background: var(--bad-ink); }
.progress--bad .progress__bar::-moz-progress-bar { background: var(--bad-ink); }

/* --- Modal ------------------------------------------------------------ */

/* A native <dialog>, opened by shell.js from any `[data-modal-open="id"]`.
   It overlaps the page it is drawn on, so it takes --panel-float like the
   floating menus do — the no-shadow rule is about tiles and cards. Its
   inputs get the hard focus ring, which ui-guidelines.md reserves for
   modals. Full screen on a phone. */
.modal {
  width: min(32rem, calc(100vw - 2rem));
  max-height: calc(100vh - 2rem);
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
  color: var(--text);
  box-shadow: var(--panel-float);
}

/* A dialog that is READ, not answered: two panels side by side, where
   32rem would put four words on a line. It widens the box and changes
   nothing else, and below 1025px - phone territory on the scale in
   CLAUDE.md - it is exactly the dialog above, because at that width the
   panels inside have stacked anyway. */
.modal--wide { width: min(62rem, calc(100vw - 2rem)); }

.modal::backdrop { background: rgb(var(--night-blue-rgb) / 0.4); }

.modal__header {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--divider);
}

.modal__title { margin: 0; font-size: 0.9375rem; }

.modal__close {
  display: inline-flex;
  margin-left: auto;
  padding: 0.25rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}

.modal__close:hover { color: var(--primary); }
.modal__close .icon { color: currentColor; }

.modal__body { padding: 1.25rem; overflow-y: auto; }
.modal__body > p:last-child { margin-bottom: 0; }

.modal__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.75rem;
  padding: 0.875rem 1.25rem;
  border-top: 1px solid var(--divider);
  background: var(--page-bg);
}

.modal input:focus-visible,
.modal select:focus-visible,
.modal textarea:focus-visible { box-shadow: var(--focus-hard); }

/* A dialog holding a longer form — `_modal.html` with `form_template` —
   keeps its heading and its buttons in view and scrolls the fields
   between them, rather than scrolling the buttons off the bottom. A short
   confirm is unchanged: it never fills the height. */
.modal[open] { display: flex; flex-direction: column; }
.modal > form { display: flex; flex-direction: column; min-height: 0; }
.modal__body { min-height: 0; }

@media (max-width: 40rem) {
  .modal {
    width: 100vw;
    max-width: 100vw;
    height: 100%;
    max-height: 100vh;
    margin: 0;
    border-radius: 0;
  }
}

/* --- Status chip tones ------------------------------------------------ */

/* The two tones .status-chip did not have. "Info" is the documented `open`
   recipe (sky-blue wash); "bad" is the red triplet a banner already uses. */
.status-chip--info {
  border-color: var(--primary-edge);
  background: var(--primary-wash);
  color: var(--primary);
}

.status-chip--info .status-chip__dot { background: var(--primary); }

.status-chip--bad {
  border-color: var(--bad-edge);
  background: var(--bad-bg);
  color: var(--bad-ink);
}

.status-chip--bad .status-chip__dot { background: var(--bad-ink); }

/* --- Drop zone -------------------------------------------------------- */

/* A file input the size of the box. The input itself covers the zone at
   zero opacity, so dropping a file onto it is the browser's own behaviour
   and works without script; shell.js only adds the hover state while
   something is dragged over it and lists what was chosen. */
.drop-zone {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  padding: 1.5rem 1rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--page-bg);
  text-align: center;
}

.drop-zone:hover,
.drop-zone.is-dragover {
  border-color: var(--primary);
  background: var(--primary-wash);
}

.drop-zone:focus-within { box-shadow: var(--focus-soft); }

.drop-zone__input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}

.drop-zone__label { font-size: 0.8125rem; font-weight: 600; color: var(--primary); }
.drop-zone__hint { font-size: 0.6875rem; color: var(--text-muted); }

.drop-zone__files {
  margin: 0.25rem 0 0;
  padding: 0;
  list-style: none;
  font-size: 0.75rem;
  color: var(--text);
  overflow-wrap: anywhere;
}

/* --- Inline note ------------------------------------------------------ */

/* One sentence beside the thing it is about — under a field, in a row.
   Smaller than a banner and never at the top of a page: a banner is a
   standing condition of the page, this is a remark about one control. */
.inline-note {
  display: flex;
  align-items: flex-start;
  gap: 0.4rem;
  /* Was capped at the 68ch reading measure, for the same reason the
     banner was, and wrong for the same reason: it sits inside a card
     whose width is already the decision. */
  max-width: none;
  margin: 0.35rem 0 0;
  font-size: 0.75rem;
  color: var(--text-mid);
}

/* The flex child, or the sentence inside it keeps the global `p`
   measure and the note is boxed wide around narrow text. */
.inline-note__body { flex: 1 1 auto; min-width: 0; max-width: none; }

.inline-note .icon { margin-top: 0.15rem; }
.inline-note__body { min-width: 0; }

.inline-note--warn { color: var(--warn-ink); }
.inline-note--bad { color: var(--bad-ink); }
.inline-note--good { color: var(--good-ink); }

.inline-note--warn .icon,
.inline-note--bad .icon,
.inline-note--good .icon { color: currentColor; }

/* --- What you're seeing ----------------------------------------------- */

/* The line over a list that says how much of it is on screen and why:
   "12 of 240 widgets · filtered by Status, Owner · Clear". A filtered list
   that does not say so reads as a list with things missing from it. */
.result-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem 0.625rem;
  padding: 0.625rem 1.25rem;
  border-bottom: 1px solid var(--divider);
  font-size: 0.75rem;
  color: var(--text-mid);
}

.result-summary__count { font-weight: 600; color: var(--text-strong); }

.result-summary__filters {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.result-summary__clear { margin-left: auto; font-weight: 600; }

/* --- Whose view this is ------------------------------------------------
   One quiet line saying what the viewer's reach covers (SB-S03): "Showing
   Acme Construction Ltd and Acme Leisure Ltd, plus requests you're named
   on". Under a page's heading, it tucks up to it; inside a form it sits
   where it is put. */
.k-reach {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.75rem;
  color: var(--text-mid);
}

.k-reach b { color: var(--brand-dark); }
.page-head + .k-reach { margin: -0.75rem 0 1rem; }
/* A small control at the end of the line — Procurement's "Mark me
   unavailable" (PR-1.2a-R05). A form, so it posts; laid out as text. */
.k-reach__action { display: inline-flex; align-items: center; gap: 0.4rem; margin: 0; }

/* A date in a grid that needs attention: coming up, or already gone.
   Coloured text, never a chip, so nothing else on the row has to shout. */
.grid__due { color: var(--warn-ink); }
.grid__due--past { color: var(--bad-ink); }

/* --- Empty state ------------------------------------------------------ */

/* Two different things, and they must never look alike. First run —
   nothing has ever been added — offers the create action, and only to
   somebody who may use it. Filtered to nothing offers the way back, never
   create: the rows exist, this page simply cannot see them. */
/* A record's header facts — a purchase order's status, total, dates and
   who approved it. A definition list, styled as a ledger rather than as
   cards: labels in their own column so every value starts on the same
   line, hairlines between rows, nothing boxed. Two columns on a wide
   panel because a single stack of six facts is a page of scrolling for
   no reason.

   `<dl><div><dt>…<dd>…</div></dl>` — the wrapping div is what makes each
   pair one grid item; a bare dt/dd pair cannot be, and the label/value
   alignment is the whole point of the component. */
.k-facts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: 2.5rem;
  margin: 0;
}

.k-facts > div {
  display: grid;
  grid-template-columns: 8.5rem minmax(0, 1fr);
  align-items: baseline;
  gap: 0.25rem 1rem;
  padding: 0.6rem 0;
  border-top: 1px solid var(--divider);
  min-width: 0;
}

/* The first row of each column has nothing above it to be divided from. */
.k-facts > div:nth-child(-n + 2) { border-top: 0; }

/* gray-500, not gray-400. These are LABELS - Status, Placed, Approved -
   and gray-400 on white is about 2.5:1, under half what WCAG AA asks of
   text. --text-muted keeps its job as the colour of icons and of a
   qualifier nobody has to read; a word naming the value beside it is
   not that. */
.k-facts dt {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-mid);
}

.k-facts dd {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--text);
  overflow-wrap: anywhere;
}

/* The qualifier after a value - "by Hana Admin", "nothing can be sent
   until they answer" - is secondary, and still has to be readable. */
.k-facts dd .muted { font-size: 0.75rem; color: var(--text-mid); }

@media (max-width: 40rem) {
  .k-facts { grid-template-columns: minmax(0, 1fr); }
  .k-facts > div:nth-child(2) { border-top: 1px solid var(--divider); }
  .k-facts > div:first-child { border-top: 0; }
}

/* --- A list you can add to, from inside it ------------------------------

   A dropdown whose first row opens a box: name the thing, press
   Enter, and it is on the list and chosen. Built on the popover the
   filter bars already use - a button, a panel, rows in it - because
   that is the product's dropdown and this is one.

   The native `<select>` it wraps is still in the markup and still the
   control with script off; see `_category_picker.html`.            */
.picker { position: relative; display: block; }

/* An open picker sits above the rows under it: a row further down the
   table paints later, so the panel needs a stacking context of its own
   now that nothing clips it. Only while open, or every closed picker
   on the page would be competing for the same layer. */
.picker--open { position: relative; z-index: 20; }

/* The card and the table box used to be unclipped here so this panel
   could escape them. Both clips are back, and the panel escapes by
   being shown in the top layer instead - see "A layer that escapes the
   card" at the end of this file. */

/* The category picker IS a native select now (see
   `_category_picker.html`), so it takes the design system's select -
   the same box, the same gray-400 chevron - rather than the browser's
   own control sitting in a table cell beside fields that were styled.
   The look is declared once, on `.field select` below, and this is the
   second selector on it. */

.picker__button {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  padding: 0.5rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--card-bg);
  font: inherit;
  font-size: 0.8125rem;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}

.picker__button:hover { border-color: var(--primary-edge); }

.picker__button:focus-visible {
  outline: 2px solid var(--sky-blue);
  outline-offset: 1px;
}

.picker__value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.picker__button .icon { flex: 0 0 auto; color: var(--text-mid); }

/* Flat and bordered, no shadow — ui-guidelines.md, the same as every
   other popover in the kit. */
.picker__panel {
  position: absolute;
  z-index: 10;
  top: calc(100% + 0.25rem);
  left: 0;
  right: 0;
  min-width: 12rem;
  padding: 0.35rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
}

/* The top row, and it reads as an action rather than as one of the
   things on the list - it is the one row that does not choose. */
.picker__new {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  width: 100%;
  padding: 0.4rem 0.45rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  font: inherit;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--primary);
  cursor: pointer;
}

.picker__new:hover { background: var(--primary-wash); }
.picker__new .icon { color: currentColor; }

/* "New category" is a <details> under the select rather than a row
   inside a panel, so the disclosure is the browser's and needs no
   script to open. The marker goes: the row already says what it does,
   and a triangle beside a + is two arrows about one thing. */
.picker__add { margin-top: 0.3rem; }
.picker__add > summary { list-style: none; }
.picker__add > summary::-webkit-details-marker { display: none; }
.picker__add > summary::marker { content: ""; }
.picker__add > summary:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

.picker__adder { display: flex; gap: 0.35rem; padding: 0.25rem; }

.picker__input {
  flex: 1 1 auto;
  min-width: 0;
  font: inherit;
  font-size: 0.8125rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--card-bg);
  color: var(--text);
}

.picker__said { margin: 0.2rem 0.45rem 0.35rem; font-size: 0.75rem; }
.picker__said--bad { color: var(--bad-ink); }
.picker__said--good { color: var(--good-ink); }

/* The add row is the panel's header, not the first row of the list -
   it is the one row that does not choose. It carries the card's own
   background and sticks to the top, so the options scroll UNDER it
   instead of appearing through it. A border rather than an `<hr>`,
   because the rule used to float between two things that were not
   separated by anything else. */
.picker__head {
  position: sticky;
  top: -0.35rem;              /* cancels the panel's own padding */
  z-index: 1;
  margin: -0.35rem -0.35rem 0.35rem;
  padding: 0.35rem 0.35rem 0.35rem;
  border-bottom: 1px solid var(--divider);
  border-radius: var(--radius) var(--radius) 0 0;
  background: var(--card-bg);
}

/* A long list scrolls inside the panel rather than past the bottom of
   the window - the add row at the top must stay reachable. */
.picker__options { max-height: 15rem; overflow-y: auto; overscroll-behavior: contain; }

.picker__option {
  display: block;
  width: 100%;
  padding: 0.4rem 0.45rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  font: inherit;
  font-size: 0.8125rem;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}

.picker__option:hover { background: var(--page-bg); }

.picker__option--on {
  background: var(--primary-wash);
  color: var(--primary);
  font-weight: 600;
}

/* --- "It isn't on the list" ---------------------------------------------

   A category added from the form that wanted one, beside the picker
   rather than on another screen. A row, because the box and the button
   are one act; it wraps on a phone and in the narrow cell of an item
   row, where the two stack and each takes the width.                  */
.category-add {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.4rem;
}

.category-add input { flex: 1 1 7rem; min-width: 0; }

/* --- A message, laid out as the email it becomes -------------------------

   The covering note on a purchase order is an email, so the page says
   so: the headers as label-and-value rows with a hairline between them,
   the writing area under them, and what is attached at the foot. It was
   a stack of ordinary fields, which told nobody that To, the message
   and the PDF leave together as one message.

   Nothing here is a new control. Every field is still `_field.html`'s,
   with the product's own input and its own label; this is the
   arrangement around them, which is what an application is allowed to
   do (rule 14).                                                        */
.compose__row { border-bottom: 1px solid var(--divider); }

.compose__row > .field,
.compose__row > .compose__pair {
  display: grid;
  grid-template-columns: 4.5rem minmax(0, 1fr);
  align-items: baseline;
  gap: 0.2rem 1rem;
  margin: 0;
  padding: 0.55rem 0;
}

.compose__row > .field > label,
.compose__label {
  margin: 0;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-mid);
}

/* Help and errors belong under the value, not under the label. */
.compose__row .helptext,
.compose__row .errorlist { grid-column: 2; margin: 0; }

.compose__value {
  min-width: 0;
  font-size: 0.8125rem;
  color: var(--text);
  overflow-wrap: anywhere;
}

.compose__value .muted { font-size: 0.75rem; }

/* A tick that reads as part of the row it is on, rather than as a field
   of its own with a second label above it. */
.compose__check {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8125rem;
  color: var(--text);
}

.compose__body { padding-top: 0.9rem; }
.compose__body > .field { margin: 0; }
.compose__body textarea { min-height: 10rem; }

/* What goes with it. A chip rather than a sentence: it is a file, and a
   file on an email is a thing you can point at. */
.compose__attachment {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0.9rem 0 0;
  padding: 0.35rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--page-bg);
  font-size: 0.75rem;
  color: var(--text-mid);
}

.compose__attachment .icon { color: var(--text-mid); }
.compose__attachment strong { color: var(--text); font-weight: 600; }

@media (max-width: 40rem) {
  /* A 4.5rem label column and an address field do not share a phone.
     The label goes over its value, as every other field on a phone
     already does. */
  .compose__row > .field,
  .compose__row > .compose__pair { grid-template-columns: minmax(0, 1fr); }
  .compose__row .helptext,
  .compose__row .errorlist { grid-column: 1; }
}

/* --- A purchase order's own summary card ---------------------------------

   The reference and its state lead, the money sits opposite them, and
   the facts follow underneath as a ledger. A reader who has scrolled
   past the page head should not have to scroll back to answer "which
   order, what state, how much".                                        */
.order-card__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
  padding-bottom: 0.9rem;
  margin-bottom: 0.25rem;
  border-bottom: 1px solid var(--border);
}

.order-card__id {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.6rem;
  min-width: 0;
}

.order-card__ref {
  margin: 0;
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.2;
  color: var(--brand-dark);
}

.order-card__money { text-align: right; min-width: 0; }

/* Tabular figures, so a column of amounts lines up on the decimal
   wherever this card is read. */
.order-card__total {
  margin: 0;
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.2;
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
}

/* What the number above means about VAT (SB-S12). It is part of the
   amount, not a footnote to it, so it sits with it. */
.order-card__vat {
  margin: 0.15rem 0 0;
  font-size: 0.75rem;
  color: var(--text-mid);
}

@media (max-width: 40rem) {
  .order-card__head { flex-direction: column; }
  .order-card__money { text-align: left; }
}

/* --- Phone behaviour for the shared page furniture ----------------------

   Three components had no phone rules at all: the page head, the tabs and
   the pagination. Each is on nearly every screen in the product, so each
   was wrong on nearly every screen at phone width.

   A touch target is 44px by the usual accessibility floor, and a 0.75rem
   button at 0.5rem padding is about 30. On a pointer that is fine; under
   a thumb it is not, so buttons grow rather than the whole type scale
   changing.                                                             */
@media (max-width: 40rem) {
  /* The actions drop under the title and fill the width, so two of them
     are two full-width rows rather than two half-squashed ones. */
  .page-head { gap: 0.75rem; }
  .page-head__actions { width: 100%; }
  .page-head__actions > .button,
  .page-head__actions > form > .button {
    flex: 1 1 auto;
    text-align: center;
    padding: 0.7rem 0.75rem;
  }

  /* Tabs WRAP; they do not scroll. SB-S06 allows exactly one sideways
     scroller in the product and it is the board - a scrolling tab strip
     hides tabs behind an edge with nothing to say they are there, which
     is the reason for the rule. Wrapping shows all of them, so the gap
     tightens instead. */
  .tabs { gap: 0.2rem; }
  .tabs__tab { padding-inline: 0.6rem; }

  .pagination {
    justify-content: center;
    padding: 0.875rem 1rem;
  }

  /* Anything a thumb presses, not just the ones in a page head. */
  .button { padding: 0.65rem 0.85rem; }

  /* A run of form buttons stacks rather than sitting two-up at half
     width each. */
  .form-actions > .button { flex: 1 1 auto; text-align: center; }

  /* Panels lose the side padding that a phone cannot spare. */
  .panel__body { padding: 1rem; }
  .panel__header { padding: 0.875rem 1rem; }
}

.k-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding: 2.5rem 1.25rem;
  text-align: center;
}

.k-empty__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--radius);
  background: var(--primary-wash);
  color: var(--primary);
}

.k-empty__icon .icon { color: currentColor; }

.k-empty--filtered .k-empty__icon {
  background: var(--divider);
  color: var(--text-mid);
}

.k-empty__title { margin: 0; font-size: 0.9375rem; color: var(--brand-dark); }
.k-empty__body { max-width: 42ch; margin: 0; color: var(--text-mid); }

.k-empty__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.75rem;
  margin-top: 0.5rem;
}

/* --- Line items ------------------------------------------------------- */

/* A modifier on .grid for the table quotes, invoices, costings and
   purchase orders all need: rows of description, quantity, price and
   amount, with a totals footer. Numbers right-aligned in tabular figures so
   a column of amounts lines up on its decimal point. Wide on a phone, so
   it sits in a .grid-scroll box and never stacks — a costing read as cards
   cannot be added up. */
.line-items .line-items__num,
.grid .grid__num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.line-items__input {
  width: 100%;
  min-width: 4rem;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--card-bg);
  font: inherit;
  font-size: 0.8125rem;
  color: var(--text);
}

.line-items__input--num { text-align: right; font-variant-numeric: tabular-nums; }

/* --- What each column of an item row is worth --------------------------

   An auto table hands every column holding a `width: 100%` input an
   equal share, so a quantity was as wide as the description beside it
   and the category picker was left with what a `<select>` showing an
   em dash needs. These say what each is for; the two unsized columns
   (`--wide`) take whatever is left, because they are the ones somebody
   writes a sentence into.

   On a `<col>`, so each is stated once rather than on a `th` and a
   `td` apiece - and below 1025px `.grid--stack` turns every row into a
   block and they stop applying, which is correct there.            */
.line-items__col--qty { width: 6.5rem; }
.line-items__col--unit { width: 7rem; }
.line-items__col--pick { width: 13rem; }
.line-items__col--file { width: 13rem; }
.line-items__col--act { width: 3rem; }

/* A picker fills the cell it is given, like the text inputs either
   side of it, rather than sitting at the width of its own widest
   option. */
.line-items .picker,
.line-items select.line-items__input { width: 100%; }

.line-items__input[type="file"] {
  padding: 0;
  border: 0;
  background: none;
  font-size: 0.75rem;
  color: var(--text-muted);
}

.line-items__input[type="file"]::file-selector-button {
  padding: 0.35rem 0.65rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--card-bg);
  font: inherit;
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--text);
  cursor: pointer;
  margin-right: 0.5rem;
}

.line-items__input[type="file"]::file-selector-button:hover {
  background: var(--page-bg);
}

.line-items__remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  position: relative;
  padding: 0.35rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--bad-ink);
  cursor: pointer;
}

.line-items__remove:hover { background: var(--bad-bg); }
.line-items__remove input[type="checkbox"] {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
.line-items__remove .icon { color: currentColor; }

/* Ticked means "this row goes when you save". Say so: the control fills in,
   and the row it belongs to fades, so the decision is visible before the
   save rather than only after it. */
.line-items__remove:has(input:checked) {
  background: var(--bad-ink);
  color: #fff;
}

.line-items__remove input:focus-visible + .icon {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.line-items tbody tr:has(.line-items__remove input:checked)
  .grid__cell:not(:has(.line-items__remove)) {
  opacity: 0.45;
}

.line-items__add {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--primary);
  cursor: pointer;
}

.line-items__add .icon { color: currentColor; }

/* Where a line prints, as a number to change (a formset with `can_order`):
   only as wide as a two- or three-digit place needs. */
.line-items__input--order { width: 4rem; min-width: 3.5rem; }

/* The words of a line, given room to be read in a table that scrolls
   sideways on a phone rather than squeezed to the width of a number. */
.line-items__input--wide { min-width: 12rem; }

/* A heading line (`headings`): a title over the lines beneath it, carrying
   no price. Read-only, it spans the table; in a formset, its box sits under
   the description, and ticking it quietens the figures beside it, so the
   decision shows before the save rather than only after it. */
.line-items .line-items__heading th[scope="row"] {
  padding-top: 0.9rem;
  font-weight: 700;
  color: var(--brand-dark);
}

.line-items__flag {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.35rem;
  font-size: 0.75rem;
  color: var(--text-muted);
  white-space: nowrap;
  cursor: pointer;
}

.line-items tr:has(.line-items__flag input:checked)
  .line-items__input--num:not(.line-items__input--order),
.line-items tr:has(.line-items__flag input:checked) select.line-items__input {
  opacity: 0.45;
}

/* --- A list in groups -------------------------------------------------- */

/* One heading row over each group's rows — CRM's quotes by client company
   (CRM-3.1-R04) — with how many it holds and its totals beside the name.
   A row of the table rather than a table per group, so the columns line
   up down the whole list and paging still counts rows. */
.grid .grid__group th {
  background: var(--page-bg);
  font-weight: 700;
  color: var(--brand-dark);
}

.grid__group-meta {
  margin-left: 0.5rem;
  font-weight: 500;
  color: var(--text-muted);
}

.line-items tfoot th,
.line-items tfoot td { background: var(--page-bg); }

.line-items .line-items__total-label {
  text-align: right;
  font-weight: 600;
  color: var(--text-mid);
}

.line-items .line-items__total--grand th,
.line-items .line-items__total--grand td {
  border-top: 1px solid var(--border);
  font-weight: 700;
  color: var(--brand-dark);
}

/* --- Card menu -------------------------------------------------------- */

/* The "more" control on a card or a row: a <details> whose summary is the
   three dots. Native, so it opens with a keyboard and with script off;
   shell.js only closes it on an outside click or Escape. Floats over the
   page, so it takes --panel-float like the other floating menus. */
.card-menu { position: relative; display: inline-flex; }

.card-menu__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  list-style: none;
  cursor: pointer;
}

.card-menu__toggle::-webkit-details-marker { display: none; }
.card-menu__toggle:hover { background: var(--divider); color: var(--primary); }
.card-menu__toggle .icon { color: currentColor; }

.card-menu__panel {
  position: absolute;
  top: calc(100% + 0.25rem);
  right: 0;
  z-index: 40;
  min-width: 11rem;
  margin: 0;
  padding: 0.35rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
  box-shadow: var(--panel-float);
  list-style: none;
}

.card-menu__item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.45rem 0.6rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  font: inherit;
  font-size: 0.8125rem;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}

.card-menu__item:hover { background: var(--page-bg); color: var(--primary); text-decoration: none; }
.card-menu__item--danger { color: var(--bad-ink); }
.card-menu__item--danger:hover { background: var(--bad-bg); color: var(--bad-ink); }

/* --- Refusals --------------------------------------------------------- */

/* Not found, no access to this application, something went wrong — drawn
   inside the shell with the sidebar still working, so a refusal never
   strands somebody on a page with no way on. The reference is a quotable
   string, selectable in one click. */
.refusal {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  max-width: 34rem;
  margin: 3rem auto;
  text-align: center;
}

.refusal__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius);
  background: var(--divider);
  color: var(--text-mid);
}

.refusal__icon .icon { color: currentColor; }

.refusal--error .refusal__icon { background: var(--bad-bg); color: var(--bad-ink); }

.refusal__title { margin: 0; font-size: 1.25rem; }
.refusal__body { margin: 0; color: var(--text-mid); }

.refusal__ref {
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--page-bg);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  user-select: all;
}

.refusal__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.75rem;
  margin-top: 0.5rem;
}

/* --- Loading ---------------------------------------------------------- */

/* A placeholder the shape of what is coming, for an HTMX region that has
   not answered yet. Still under reduced motion. */
.skeleton {
  display: block;
  height: 0.75rem;
  margin: 0 0 0.5rem;
  border-radius: var(--radius-sm);
  background: var(--divider);
  animation: skeleton-pulse 1.4s ease-in-out infinite;
}

.skeleton--title { width: 40%; height: 1rem; }
.skeleton--short { width: 60%; }

@keyframes skeleton-pulse {
  50% { opacity: 0.5; }
}

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

/* --- The phone: tables, filters, the primary action ------------------- */

/* A table below 1025px becomes a stack of cards, each headed by the row's
   first column — the `<th scope="row">` — and every other cell labelled
   from its own `data-label`. Opt-in with .grid--stack, because a genuinely
   wide table (line items, the seat grid) stays a table in its .grid-scroll
   box: that is the one thing on a phone allowed to scroll sideways. The
   header row is hidden visually but kept for a screen reader. */
@media (max-width: 64rem) {
  .grid--stack thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  .grid--stack,
  .grid--stack tbody,
  .grid--stack tr,
  .grid--stack th,
  .grid--stack td { display: block; width: 100%; }

  .grid--stack tbody { padding: 0.75rem; }

  .grid--stack tbody tr {
    margin: 0 0 0.75rem;
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--card-bg);
  }

  .grid--stack tbody tr:last-child { margin-bottom: 0; }

  .grid--stack tbody th[scope="row"] {
    border-top: 0;
    border-bottom: 1px solid var(--divider);
    background: var(--page-bg);
  }

  /* `overflow-wrap: anywhere` lets a value with no space in it — an email
     address, a URL — break inside the card instead of running past its
     edge, where the row's `overflow: hidden` would cut it off unread. */
  .grid--stack td {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    overflow-wrap: anywhere;
  }

  .grid--stack tbody th[scope="row"] + td { border-top: 0; }

  .grid--stack td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 40%;
    font-size: 0.6875rem;
    font-weight: 600;
    color: var(--label-blue);
  }

  .grid--stack .grid__num { text-align: right; }
}

/* Filters behind one control on a phone, and the control says how many are
   on. shell.js marks a `[data-filter-collapse]` bar collapsible when it
   starts, so with script off every filter is simply shown — nothing is ever
   hidden behind a button that cannot open. */
.filter-bar__toggle { display: none; }

.filter-bar__count {
  padding: 0 0.4rem;
  border-radius: var(--radius-sm);
  background: var(--primary);
  font-size: 0.6875rem;
  color: var(--white);
}

@media (max-width: 64rem) {
  .filter-bar.is-collapsible .filter-bar__toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.45rem 0.7rem;
    border: 1px solid var(--primary);
    border-radius: var(--radius-sm);
    background: var(--primary);
    font: inherit;
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--white);
    cursor: pointer;
  }

  .filter-bar.is-collapsible .filter-bar__toggle .icon { color: currentColor; }

  /* On the blue toggle, the count reads as a white pill rather than blue on
     blue. */
  .filter-bar.is-collapsible .filter-bar__toggle .filter-bar__count {
    background: var(--white);
    color: var(--primary);
  }

  .filter-bar.is-collapsible:not(.is-open) > :not(.filter-bar__toggle):not(.filter-bar__search) {
    display: none;
  }
}

/* The primary action stays in reach at the foot of the screen on a long
   form, rather than at the foot of the form. It sticks inside its own
   form, so it travels with the form and is gone once the form scrolls
   past. Below a form's panels it sits on the page; as the last thing in a
   panel's body it runs out to the panel's edges, over the panel's white. */
@media (max-width: 64rem) {
  .form-actions--sticky {
    position: sticky;
    bottom: 0;
    z-index: 5;
    padding: 0.75rem 0;
    border-top: 1px solid var(--divider);
    background: var(--page-bg);
  }

  .panel__body .form-actions--sticky {
    margin: 0 -1.25rem -1.25rem;
    padding: 0.75rem 1.25rem;
    background: var(--card-bg);
  }
}

@media (max-width: 40rem) {
  .panel__body .form-actions--sticky {
    margin: 0 -1rem -1rem;
    padding: 0.75rem 1rem;
  }
}

/* --- Inline panel (k-modal) ------------------------------------------- */

/* A task opened ON a page rather than over it — Sandbox's "Give access":
   a <div> in the page flow at its own address (/access/give/), opened by a
   link, closed by a link back, with no backdrop and no focus held inside
   it. Not `.modal`, which is the native <dialog> that floats over the page
   and is opened by script. Two different components, two different
   classes: one name cannot be both in the flow and over it.

   It wraps a `.panel`, which draws the box. This only sets it apart from
   the page around it — a sky-blue edge, the hard focus ring a prominent
   form takes — and lands it below the topbar when its address is opened.
   Flat, in the flow, and no different on a phone. */
.k-modal {
  margin: 0 0 1.5rem;
  scroll-margin-top: calc(var(--topbar-height) + 1rem);
}

.k-modal > .panel { border-color: var(--primary-edge); }

.k-modal input:focus-visible,
.k-modal select:focus-visible,
.k-modal textarea:focus-visible { box-shadow: var(--focus-hard); }

/* --- Select fields ---------------------------------------------------- */

/* A <select> inside a `.field` — every choice `_field.html` renders. The
   Forms section above gives `input` and `textarea` their box and never
   gave one to `select`, so every dropdown in the product fell back to the
   browser's own control beside fields that were styled. Same box as
   `.field input`; the native arrow is replaced by a gray-400 chevron (the
   `chevron-down` glyph from icons.py, stroke 2.5) so the control looks the
   same in every browser, and the space it needs is kept clear on the right.

   A multi-select is a list, not a dropdown: it keeps the box but draws no
   chevron and grows to show its options. */
.field select,
.picker__native {
  width: 100%;
  font: inherit;
  font-size: 0.8125rem;
  line-height: 1.4;
  padding: 0.5rem 2.25rem 0.5rem 0.7rem;
  color: var(--text);
  background-color: var(--card-bg);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.7rem center;
  background-size: 0.8125rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.field select:hover,
.picker__native:hover { border-color: var(--primary-edge); }

.field select:disabled,
.picker__native:disabled {
  background-color: var(--page-bg);
  color: var(--text-muted);
  cursor: not-allowed;
}

.field select[multiple] {
  padding-right: 0.7rem;
  background-image: none;
  cursor: default;
}

/* --- File, checkbox and radio fields ---------------------------------- */

/* The rest of the inputs `.field input` catches by accident. That rule is a
   text box — full width, padded, bordered — and every input type inside a
   `.field` gets it, which is right for text and wrong for three others.

   A file input keeps the box, but the "Choose file" button inside it was
   the browser's own grey one. It becomes a `.button`: #1640FF with white
   text and the 8px radius, because every button in the product is that —
   the same rule `.button--ghost` follows.

   A checkbox or a radio is not a text box at all: under the same rule it
   was stretched full width with a border round it. Django draws each
   option of a RadioSelect or CheckboxSelectMultiple as an input INSIDE its
   own <label>, and `.field label` makes that label a bold block heading —
   so an option read as a heading with a bar under it. Here the input is a
   plain 1rem control in the brand blue, and a label that holds one is a
   row: control, then its text, at body weight. A lone checkbox (a
   BooleanField) sits under its label as a control, not a bar. The switch
   in launcher.css is deliberately outside `.field` and is untouched. */
/* `max-width`, because a file input's Choose File button has an
   intrinsic width and will push the control past its container rather
   than shrink - which is what hung the button out of the box in a
   narrow column. Bounded here so no caller has to remember. */
.field input[type="file"] {
  padding: 0.3rem;
  max-width: 100%;
  min-width: 0;
  cursor: pointer;
}

.field input[type="file"]:hover { border-color: var(--primary-edge); }

.field input[type="file"]::file-selector-button {
  margin-right: 0.75rem;
  padding: 0.35rem 0.75rem;
  border: 1px solid var(--primary);
  border-radius: var(--radius-sm);
  background: var(--primary);
  font: inherit;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--white);
  cursor: pointer;
}

.field input[type="file"]::file-selector-button:hover { filter: brightness(1.06); }

.field input[type="checkbox"],
.field input[type="radio"] {
  flex: 0 0 auto;
  width: 1rem;
  height: 1rem;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  accent-color: var(--primary);
  cursor: pointer;
}

.field label:has(> input[type="checkbox"]),
.field label:has(> input[type="radio"]) {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  padding: 0.25rem 0;
  font-size: 0.8125rem;
  font-weight: 400;
  color: var(--text);
  cursor: pointer;
}

/* --- Filter-bar dropdowns --------------------------------------------- */

/* `.filter-bar__select` (the filter bar section above) gives a control its
   border, radius and padding, and was written for the <button> that opens
   a multi-select popover — which draws its own chevron-down icon. Put on a
   native <select> — the stage, sort and page-size choices on the widget
   list, Admin → Activity, notifications, the support filters and the
   backoffice — it left the browser's own arrow and chrome inside that
   border, the same gap `.field select` had. So a SELECT with the class gets
   what `.field select` gets: no native appearance, and the same gray-400
   chevron, with room kept for it. The button is not touched. */
select.filter-bar__select {
  padding-right: 1.9rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.55rem center;
  background-size: 0.75rem;
  -webkit-appearance: none;
  appearance: none;
}

/* --- Company structure ------------------------------------------------ */

/* Admin → Company structure (CS-1.1) and Where people sit (CS-1.2).

   What a unit is, as a chip in the words a customer uses (CS-1.1-R03).
   Three kinds and the customer itself, each told apart by the semantic
   triplets the rest of the kit already uses — none picks its own colour.
   The customer reads strongest, as the owner chip does: it is the one row
   that is not a part of something else. */
.chip--customer {
  background: var(--primary);
  border-color: var(--primary);
  color: var(--white);
}

.chip--company {
  background: var(--primary-wash);
  border-color: var(--primary-edge);
  color: var(--primary);
}

.chip--department {
  background: var(--page-bg);
  border-color: var(--border);
  color: var(--text-mid);
}

.chip--place {
  background: var(--good-bg);
  border-color: var(--good-edge);
  color: var(--good-ink);
}

.chip--customer .icon,
.chip--company .icon,
.chip--department .icon,
.chip--place .icon { color: currentColor; }

/* The tree is a table whose first cell is indented by how deep the unit
   sits — `style="--depth: N"` on the name, the one interpolated style the
   pattern check allows. Capped, so a deep branch never pushes the name out
   of its column. */
.structure-grid__name {
  display: block;
  padding-left: calc(min(var(--depth, 0), 6) * 1.25rem);
}

/* A retired unit stays on the list, greyed, so somebody looking for a name
   that has gone can find it (CS-1.1-R06). */
.structure-grid__row--retired .grid__name,
.structure-grid__row--retired .grid__cell { color: var(--text-muted); }

.structure-grid__details {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.75rem;
}

/* The kind chip and the counts at the top of the edit panel
   (CS-1.1b-R02), and the links under it. */
.structure-facts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 1rem;
  font-size: 0.8125rem;
}

.structure-links { margin-top: 1.25rem; }

/* Moving several people at once (CS-1.2-R05): the destination, above the
   rows whose boxes are ticked. */
.structure-bulk {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 1.25rem;
  border-bottom: 1px solid var(--divider);
}

.structure-bulk__label {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text-mid);
}

/* An add form with legal details is two panels in one form; the one
   inside the inline panel gets the same edge as one standing directly
   in it. */
.k-modal > form > .panel { border-color: var(--primary-edge); }

/* --- Groups ------------------------------------------------------------ */

/* Settings → Groups in every application, and Admin → Groups (GRP-6,
   GRP-7). A retired group stays on its list, greyed, so somebody looking
   for a name that has gone can find it and restore it. */
.groups-grid__row--retired .grid__name,
.groups-grid__row--retired .grid__cell { color: var(--text-muted); }

/* What a group prints, a few short lines in one cell. */
.groups-grid__details {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.75rem;
}

/* --- Merge requisitions (Procurement) ---------------------------------- */

/* PR-1.1b's merge page, drawn to its own supplied design rather than to the
   shared archetypes. Everything here is scoped under `.merge-page` on
   purpose, so nothing else can pick it up.

   Its labels are sentence case like every other screen's: the uppercase
   tracking-wide labels this page carried briefly were an exception to rule
   13 that was never agreed, and they have been taken back out.

   Still the shared system in every other respect: the brand blue for what is
   active, 12px on cards and 8px on chips and buttons, borders and focus
   rings rather than shadows, and the tokens for every colour. */

.merge-page .eyebrow {
  color: var(--primary);
}

.merge-page .page-head__actions { display: flex; gap: 0.5rem; }

/* A section of the page: one bordered card, a header strip and a body. */
.merge-panel {
  margin-bottom: 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
}

.merge-panel__header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.875rem 1.25rem;
  border-bottom: 1px solid var(--divider);
}

.merge-panel__header .icon { color: var(--primary); }

.merge-panel__title {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--brand-dark);
}

/* A count in the header's own strip, in the same key as the title. */
.merge-panel__title-count {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.merge-panel__flag {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  margin-left: auto;
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--good-edge);
  border-radius: var(--radius-sm);
  background: var(--good-bg);
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--good-ink);
}

.merge-panel__flag .icon { color: currentColor; }

.merge-panel__body { padding: 1.25rem; }

.merge-panel__lede {
  margin: 0 0 1rem;
  font-size: 0.75rem;
  color: var(--text-muted);
}

/* The table of what was ticked. Column headers read like every other grid's. */
.merge-page .grid thead th {
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--text-muted);
}

.merge-page .grid thead th .grid__th-label { font: inherit; }

/* A request's reference, as a chip beside what it is for. */
.merge-ref {
  display: inline-block;
  padding: 0.1rem 0.4rem;
  border-radius: 0.25rem;
  background: var(--primary-wash);
  font-family: var(--font-mono);
  font-size: 0.625rem;
  font-weight: 600;
  color: var(--primary);
}

/* The second line of a cell: a ledger code, a date, a VAT number. */
.merge-sub {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.6875rem;
  color: var(--text-muted);
}

.merge-entity-name { font-weight: 600; color: var(--brand-dark); }

.merge-count {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--page-bg);
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text);
}

.merge-person { display: inline-flex; align-items: center; gap: 0.45rem; }

/* The note under the table: what merging across companies will do. */
.merge-notice {
  display: flex;
  gap: 0.6rem;
  margin: 0 1.25rem 1.25rem;
  padding: 0.75rem 0.875rem;
  border: 1px solid var(--warn-edge);
  border-radius: var(--radius-sm);
  background: var(--warn-bg);
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--warn-ink);
}

.merge-notice .icon { flex: 0 0 auto; color: currentColor; margin-top: 0.1rem; }
.merge-notice strong { font-weight: 600; }

/* The two ways, side by side. */
.merge-strategy {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.75rem;
}

.merge-strategy__card {
  display: block;
  margin: 0;
  padding: 1rem;
  border: 1.5px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
  font-weight: 400;
  cursor: pointer;
}

.merge-strategy__card:hover { border-color: var(--primary-edge); }

.merge-strategy__card:has(input:checked) {
  border-color: var(--primary);
  background: var(--primary-wash);
}

.merge-strategy__card:has(input:focus-visible) { box-shadow: var(--focus-soft); }

.merge-strategy__top {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}

.merge-strategy__name {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--brand-dark);
}

.merge-strategy__card:has(input:checked) .merge-strategy__name { color: var(--primary); }

.merge-strategy__note {
  display: block;
  margin-left: 1.5rem;
  font-size: 0.75rem;
  line-height: 1.5;
  color: var(--text-muted);
}

.merge-strategy__foot {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  margin: 0.75rem 0 0 1.5rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--divider);
  font-size: 0.6875rem;
  color: var(--text-muted);
}

.merge-badge {
  padding: 0.1rem 0.45rem;
  border-radius: 0.25rem;
  background: var(--primary);
  font-size: 0.6875rem;
  font-weight: 700;
  color: var(--white);
}

/* What the chosen way then asks for: a box inside the panel. */
.merge-detail {
  margin-top: 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--page-bg);
}

.merge-detail__header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--border);
}

.merge-detail__note { margin-left: auto; font-size: 0.6875rem; color: var(--text-muted); }

.merge-detail__body { padding: 1rem; }

.merge-detail__body .field { margin-bottom: 1rem; }

/* One candidate parent, or one billing entity: a row that is a radio. */
.merge-option {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 0.5rem;
  padding: 0.75rem 0.875rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--card-bg);
  font-weight: 400;
  cursor: pointer;
}

.merge-option:last-child { margin-bottom: 0; }
.merge-option:hover { border-color: var(--primary-edge); }

.merge-option:has(input:checked) {
  border-color: var(--primary);
  background: var(--primary-wash);
}

.merge-option:has(input:focus-visible) { box-shadow: var(--focus-soft); }

.merge-option__text { min-width: 0; }

.merge-option__name {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--brand-dark);
}

.merge-option__fate {
  margin-left: auto;
  padding-left: 0.75rem;
  font-size: 0.6875rem;
  color: var(--warn-ink);
  white-space: nowrap;
}

.merge-option__tag {
  margin-left: auto;
  padding: 0.15rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 0.25rem;
  font-size: 0.625rem;
  font-weight: 600;
  color: var(--text-muted);
  white-space: nowrap;
}

.merge-option--split { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; }

/* The bar along the bottom: what is about to happen, and the button that
   does it. Sticky, because the decision is made at the top of a long page
   and the button must not be a scroll away from it. */
.merge-bar {
  position: sticky;
  bottom: 0;
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  margin-top: 1rem;
  padding: 0.75rem 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
}

.merge-bar__state {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.75rem;
  color: var(--text-muted);
}

.merge-bar__dot {
  width: 0.5rem;
  height: 0.5rem;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--good-ink);
}

.merge-bar__state strong { font-weight: 600; color: var(--text); }
.merge-bar__state .merge-ref { margin: 0 0.1rem; }

.merge-bar__actions { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }

/* Below the desktop rail: one column everywhere, and the bar stops being
   sticky so it cannot cover a field on a short screen. */
@media (max-width: 64rem) {
  .merge-strategy,
  .merge-option--split { grid-template-columns: 1fr; }

  .merge-bar { position: static; }
  .merge-bar__actions { margin-left: 0; }
}

/* --- Editing in place, in a grid row ------------------------------------ */

/* A row's own value changed where it stands — a stage's name, a list
   entry's, a probability — with the input and its one button on one line,
   so a grid of them still reads as a grid rather than a column of stacked
   forms. Each is its own POST, like `.cell-form`.

     <form class="cell-edit" method="post" action="…">
       {% csrf_token %}
       <div class="field"><input type="text" name="label" …></div>
       <button type="submit" class="linkish">Rename</button>
     </form>

   `--narrow` for a short value such as a number. The width is a ceiling,
   not a size: in a narrow cell the input gives way and the button stays. */
.cell-edit {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  max-width: 26rem;
  margin: 0;
}

.cell-edit--narrow { max-width: 11rem; }

.cell-edit > .field { flex: 1 1 auto; min-width: 0; margin: 0; }
.cell-edit > .linkish,
.cell-edit > .button { flex: none; white-space: nowrap; }

/* A chip under the value it qualifies — "Built in". */
.cell-edit + .chip { margin-top: 0.4rem; }

/* In a phone's stacked card (.grid--stack below 1025px) a cell is one line,
   its label on the left and its value on the right, so the editor takes the
   whole value side. A cell with no label of its own heads the card: there
   the editor takes the full width and a chip under it starts its own line,
   instead of squeezing the input to nothing. */
@media (max-width: 64rem) {
  .grid--stack td > .cell-edit { flex: 1 1 0; max-width: none; }
  .grid--stack td:not([data-label]) { flex-wrap: wrap; row-gap: 0.4rem; }
  .grid--stack td:not([data-label]) > .cell-edit { flex-basis: 100%; }
  .grid--stack td > .cell-edit + .chip { margin-top: 0; max-width: 100%; white-space: normal; }

  /* A labelled cell holding an editor puts its label on a line of its own
     and gives the editor the card's width — beside a 40% label the input
     shrank to a few letters of the stage it renames. */
  .grid--stack td[data-label]:has(> .cell-edit) { flex-wrap: wrap; row-gap: 0.4rem; }
  .grid--stack td[data-label]:has(> .cell-edit)::before { flex-basis: 100%; }
  .grid--stack td[data-label] > .cell-edit { flex-basis: 100%; }
}

/* A `.row` — a person, what they hold, an action — wraps on a phone rather
   than pushing its last item past the panel's edge. */
@media (max-width: 64rem) {
  .row { flex-wrap: wrap; }
}

/* --- Gutters around a note, a form and the panels beside them ----------- */

/* A note straight under a panel's header, about everything in the panel —
   what a list is for, where its rows come from — with no body around it.
   It takes the panel's gutter, so it lines up with the title above it
   instead of touching the border. */
.panel__header + .inline-note {
  margin: 0;
  padding: 0.75rem 1.25rem;
  max-width: calc(var(--measure) + 2.5rem);
}

/* `.panel + .panel` spaces panels that touch. A form wrapping panels, the
   form's own buttons below them, and a note standing over a panel all
   break that adjacency; these keep the same rhythm across them. */
form + .panel,
.panel + .form-actions,
.inline-note + .panel { margin-top: 1rem; }

/* A full-width field after a two-column grid, at the grid's own gap. */
.form-grid + .field { margin-top: 1.5rem; }

/* --- The lines over a list ------------------------------------------------ */

/* "12 of 240 companies, filtered by …" is a bar across the panel, not a
   paragraph: `p`'s reading measure stopped its rule half way across. The
   page range under it, and a note about the rows, take the panel's gutter
   rather than touching its border. */
.result-summary { max-width: none; }

.panel > .pagination__summary { padding: 0.5rem 1.25rem 0; }

.result-summary + .inline-note {
  margin: 0;
  padding: 0.5rem 1.25rem 0;
  max-width: calc(var(--measure) + 2.5rem);
}

/* --- Saved views inside a list's panel ------------------------------------ */

/* Saved views drawn as tabs under a list's filters (CRM's pipeline): the
   bar (`_tabs.html`, a <nav> round a `.tabs` list) takes the panel's
   gutter instead of touching its border, and the note saying what the
   chosen view means lines up under it. A board after the panel holding
   them keeps the gap panels keep between themselves. */
.panel > nav > .tabs {
  margin: 0;
  padding: 0.5rem 1.25rem 0;
}

.panel > nav + .inline-note {
  margin: 0;
  padding: 0.75rem 1.25rem;
  max-width: calc(var(--measure) + 2.5rem);
}

.panel + .board,
.panel + .k-empty { margin-top: 1rem; }

/* --- Board: what a column holds, and a board that stays a board --------- */

/* The line under a column's head: what its cards are worth — one total per
   currency, never converted — and, quieter, what the stage counts for. */
.board__sum {
  margin: 0;
  padding: 0.4rem 0.875rem;
  border-bottom: 1px solid var(--divider);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-mid);
}

.board__sum-note {
  font-size: 0.6875rem;
  font-weight: 400;
  color: var(--text-muted);
}

.board__rail-item .board__sum-note { display: block; }
.board__rail > .board__sum-note { margin: 0; padding: 0 0.25rem; }


/* A board whose columns stay side by side at every width, scrolling
   sideways in its own box instead of stacking — the one screen that does
   on a phone (CRM-S52, the pipeline). Opt in with `.board--scroll`. */
@media (max-width: 64rem) {
  .board--scroll { flex-direction: row; align-items: flex-start; overflow-x: auto; }
  .board--scroll .board__column { flex: 0 0 16rem; width: auto; }
  .board--scroll .board__rail { flex: 0 0 11rem; width: auto; }
}

/* --- Board: picking a card up and putting it down ------------------------ */

/* `[data-board-drag]` in shell.js. A card that may move says so under the
   pointer; while one is in the hand nothing on the page selects as text
   and the pointer stays a closed hand wherever it goes. */
[data-board-drag] .board__card[data-move-url] {
  cursor: grab;
  user-select: none;
  -webkit-touch-callout: none;
}

/* Where a card will land is measured from its column, so the column is
   what its cards' positions are counted from. */
[data-board-drag] .board__cards { position: relative; }

body.is-board-dragging,
body.is-board-dragging * {
  cursor: grabbing;
  user-select: none;
}

/* The card in the hand: a copy, tilted, following the pointer. It floats
   over the page, so it takes the float the card menu and the modal take —
   rule 13's one exception — and nothing else on the board does. */
.board__card--lifted {
  position: fixed;
  z-index: 60;
  margin: 0;
  pointer-events: none;
  border-color: var(--primary-edge);
  box-shadow: var(--panel-float);
  transform: rotate(3deg) scale(1.02);
  transition: transform 120ms ease;
}

/* Let go: it straightens and glides to where it lands, or back home. */
.board__card--settling {
  transform: none;
  transition: left 160ms ease, top 160ms ease, transform 160ms ease;
}

/* Where it will land — a gap the size of the card, wherever the pointer
   is between two cards. */
.board__card--placeholder {
  border-style: dashed;
  border-color: var(--primary-edge);
  background: var(--primary-wash);
}

.board__card--placeholder > * { visibility: hidden; }
.board__card--placeholder ~ .board__empty { display: none; }

/* The column, or the rail's Won or Lost, it would drop into. */
.board__cards--over {
  border-radius: var(--radius-sm);
  background: var(--primary-wash);
}

.board__rail-item--over {
  border-color: var(--primary);
  background: var(--primary-wash);
  color: var(--primary);
}

/* Dropped, and on its way to the server: where it was put, until the board
   comes back redrawn. */
.board__card--saving { opacity: 0.6; }

/* Arrived: a moment's wash where it now is, then nothing. */
@keyframes board-card-landed {
  from { background: var(--primary-wash); border-color: var(--primary-edge); }
  to { background: var(--card-bg); border-color: var(--border); }
}

.board__card--landed { animation: board-card-landed 1.4s ease-out; }

/* --- One person, read back -------------------------------------------- */

/* A record page about one person — Admin's person, HR's employee: the
   card on the left, everything else beside it. Moved here from
   launcher.css so every application can use it. */

.profile-layout {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
}

.profile-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.profile-card {
  flex: 0 0 16rem;
  padding: 1.5rem;
  text-align: center;
}

.profile-card__photo {
  position: relative;
  width: 6rem;
  height: 6rem;
  margin: 0 auto 0.875rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius);
  background: var(--page-bg);
  color: var(--text-muted);
  overflow: hidden;
}

.profile-card__photo img { width: 100%; height: 100%; object-fit: cover; }

/* The shared avatar, filling the card's photo well rather than sitting in
   it at its own 40px. A record page is the one place a person is shown
   large, and initials should be legible there too. */
.profile-card__photo .avatar {
  width: 100%;
  height: 100%;
  border-radius: var(--radius);
  font-size: 1.75rem;
}

.profile-card__status-dot {
  position: absolute;
  right: 0.4rem;
  bottom: 0.4rem;
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 999px;
  background: #22c55e;
  border: 2px solid var(--card-bg);
}

.profile-card__status-dot--off { background: var(--text-muted); }

.profile-card__name {
  font-weight: 700;
  font-size: 1rem;
  color: var(--brand-dark);
  margin: 0 0 0.15rem;
}

.profile-card__role {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--primary);
  margin: 0 0 0.6rem;
}

.profile-card__rule { border: 0; border-top: 1px solid var(--divider); margin: 1rem 0; }

.profile-card__mail {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-sm);
  background: var(--primary-wash);
  color: var(--primary);
}

.profile-card__mail:hover { filter: brightness(1.06); text-decoration: none; }

/* Facts under the rule — "Started 3 Mar 2024" — each with its icon, read
   left to right while the name above stays centred. */
.profile-card__facts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.45rem;
  text-align: left;
  font-size: 0.8125rem;
  color: var(--text-mid);
}

.profile-card__facts li { display: flex; align-items: flex-start; gap: 0.45rem; min-width: 0; overflow-wrap: anywhere; }

.profile-card__facts .icon { flex: 0 0 auto; margin-top: 0.15rem; color: var(--text-muted); }

.profile-card__actions { display: grid; gap: 0.5rem; }

@media (max-width: 40rem) {
  .profile-layout { flex-direction: column; }
  .profile-card { flex: 1 1 auto; width: 100%; }
}

/* --- A record beside its sidebar --------------------------------------- */

/* Two columns where the second is a fixed, narrow rail: a record's own
   panels on the left, and the things ABOUT it - documents, people, a
   short list - on the right. The pack draws PR-6.2 this way and so does
   every record page that has something to put beside the terms.

   The rail is 18rem because that is what holds a file name, a date and a
   download without wrapping; the first column is `minmax(0, 1fr)` so a
   long word in a table inside it shrinks the column rather than pushing
   the rail off the page.

   Below 1025px it is one column, the rail last: at that width there is
   no room for two, and the rail is the supporting half. */
.k-two-col {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 18rem;
  align-items: start;
  gap: 1rem;
}

/* --- An org chart --------------------------------------------------------- */

/* HR's org chart (HR-3.3), and any other tree of people: each level a
   centred row, joined to the box above by thin connector lines. A box is a
   chip; the top one reads as the strongest, an open position and a folded
   branch are faded, and a box outside the viewer's reach has no name. */
.org-chart { overflow-x: auto; }
/* Fold chips wrap; the filter bar's one-row squeeze would empty them. */
.org-fold { display: flex; flex-wrap: wrap; align-items: center; gap: 0.45rem; margin-bottom: 1rem; }

.org-chart__hint { margin: 0 0 1rem; padding: 0.5rem 0.75rem; border: 1px dashed var(--border); border-radius: var(--radius-sm); text-align: center; }

.org-tree,
.org-tree__level {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  justify-content: center;
  gap: 0;
}

/* As wide as it needs to be and centred while it fits; wider, it scrolls
   from its left edge rather than centring part of itself out of view. */
.org-tree { width: max-content; min-width: 100%; }

.org-tree__level { position: relative; padding-top: 1rem; }

/* The line down from the box above to this level. */
.org-tree__level::before {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  height: 1rem;
  border-left: 1px solid var(--border);
}

.org-tree__item {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 1rem 0.5rem 0;
}

.org-tree > .org-tree__item { padding-top: 0; }

/* The line across to the siblings, and down to this box. */
.org-tree__level > .org-tree__item::before,
.org-tree__level > .org-tree__item::after {
  content: "";
  position: absolute;
  top: 0;
  right: 50%;
  width: 50%;
  height: 1rem;
  border-top: 1px solid var(--border);
}

.org-tree__level > .org-tree__item::after { right: auto; left: 50%; border-left: 1px solid var(--border); }
.org-tree__level > .org-tree__item:first-child::before,
.org-tree__level > .org-tree__item:last-child::after { border-top: 0; }
.org-tree__level > .org-tree__item:only-child::before { display: none; }
.org-tree__level > .org-tree__item:only-child::after { border-top: 0; }

.org-node {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.35rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--page-bg);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-mid);
  white-space: nowrap;
}

.org-node--top { background: var(--primary-wash); border-color: var(--primary-edge); color: var(--primary); }
.org-node--open,
.org-node--folded { opacity: 0.65; }
.org-node--hidden { color: var(--text-muted); }
.org-node--folded { font-weight: 500; color: var(--text-muted); }
.org-node[draggable="true"] { cursor: grab; }

.org-node__name { color: var(--brand-dark); }
a.org-node__name:hover { color: var(--primary); }
.org-node__where { font-weight: 500; color: var(--text-muted); }

.org-node__toggle,
.org-node__edit {
  display: inline-flex;
  align-items: center;
  padding: 0.1rem;
  border: 0;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}

.org-node__toggle:hover,
.org-node__edit:hover { color: var(--primary); }

/* --- Row editor ------------------------------------------------------- */

/* Several rows of the same few inputs, added and removed in place: a
   template's tasks. Used with `.field`, so its inputs and selects are the
   field's own; each row is a grid of them, and below 641px a row stacks. */
.row-editor { margin-top: 0.5rem; }

.row-editor__title {
  margin: 0 0 0.3rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: #000;
}

.row-editor__head,
.row-editor__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 11rem 6rem 4rem;
  gap: 0.5rem;
  align-items: center;
}

.row-editor__head {
  font-size: 0.6875rem;
  color: var(--text-muted);
  margin-bottom: 0.25rem;
}

.row-editor__row { margin-bottom: 0.5rem; }

@media (max-width: 40rem) {
  .row-editor__head { display: none; }
  .row-editor__row {
    grid-template-columns: minmax(0, 1fr) 5rem;
    padding-bottom: 0.5rem;
    border-bottom: 1px solid var(--divider);
  }
  .row-editor__row > :first-child { grid-column: 1 / -1; }
}

/* One input a row: a template's clauses in order. */
.row-editor--single .row-editor__row { grid-template-columns: minmax(0, 1fr) 4rem; }

/* --- Decision card ------------------------------------------------------- */

/* A decision on a page of its own — delete, archive, merge, reopen, record
   an answer — or the news that it cannot be made yet and why. One focused
   card in a reading column, not a panel stretched across the whole page
   around two sentences: the eye lands on the question, the consequences,
   and the one thing to press.

   The QUESTION is the card's title, so the page head names the record it
   is about and never repeats the question. The tone badge says what kind
   of decision it is (`decision--warn`, `--bad`, `--good`; the default is
   the brand's). The footer holds the one brand-blue button — every button
   in the product is #1640FF — and Cancel as a quiet `.cancel-link` beside
   it, so there is exactly one thing that looks pressable. Flat: no
   shadow, rule 13. Drawn by `includes/_confirm.html`; a page that needs
   more inside it (a list of what blocks it, a picker) writes the same
   markup around its own body. */
.decision {
  max-width: 40rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card-bg);
  overflow: hidden;
}

.decision--wide { max-width: 48rem; }

.decision__head {
  display: flex;
  align-items: flex-start;
  gap: 0.875rem;
  padding: 1.25rem 1.5rem 0;
}

.decision__icon {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 999px;
  background: var(--primary-wash);
  color: var(--primary);
}

.decision__icon .icon { color: currentColor; }
.decision--warn .decision__icon { background: var(--warn-bg); color: var(--warn-ink); }
.decision--bad .decision__icon { background: var(--bad-bg); color: var(--bad-ink); }
.decision--good .decision__icon { background: var(--good-bg); color: var(--good-ink); }

.decision__heading { min-width: 0; }

.decision__title {
  margin: 0.35rem 0 0;
  font-size: 1rem;
  font-weight: 700;
  line-height: 1.35;
  color: var(--brand-dark);
}

.decision__lead {
  margin: 0.35rem 0 0;
  max-width: var(--measure);
  font-size: 0.875rem;
  line-height: 1.55;
  color: var(--text-mid);
}

/* Lined up under the title, past the badge, so the card reads as one
   column of text with a mark beside it. */
.decision__body {
  padding: 1rem 1.5rem 1.25rem calc(1.5rem + 2.25rem + 0.875rem);
}

.decision__body > * + * { margin-top: 0.875rem; }
.decision__body > .inline-note { margin-top: 0.875rem; }
.decision__body > :first-child { margin-top: 0; }

/* What will change, one line each, each with its own mark. */
.decision__list {
  display: grid;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.decision__list li {
  display: flex;
  align-items: flex-start;
  gap: 0.55rem;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--text);
}

.decision__list .icon { flex: none; margin-top: 0.2rem; color: var(--text-muted); }

/* The one button, and the way out beside it. A <form> or a <div>: the
   form's own box adds nothing. */
.decision__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
  margin: 0;
  padding: 0.875rem 1.5rem;
  border-top: 1px solid var(--divider);
  background: var(--page-bg);
}

@media (max-width: 40rem) {
  .decision__head { padding: 1rem 1rem 0; }
  .decision__body { padding: 0.875rem 1rem 1rem; }
  .decision__foot { padding: 0.75rem 1rem; }
  .decision__foot > .button { flex: 1 1 auto; text-align: center; }
}

/* --- Cancel, quietly ----------------------------------------------------- */

/* The way out of a form or a decision, beside its one button. A link (or a
   <button> that closes a dialog) in the muted ink, never a second
   brand-blue button: two identical blue buttons side by side make
   "Cancel" look as much like the action as the action does. Comfortable
   to hit — the padding is the button's — and blue on hover, like any
   link. */
.cancel-link {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.5rem 0.35rem;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-mid);
  cursor: pointer;
}

.cancel-link:hover { color: var(--primary); text-decoration: none; }
.cancel-link:focus-visible { outline: none; box-shadow: var(--focus-soft); border-radius: var(--radius-sm); }

/* --- A list of records ----------------------------------------------------- */

/* A short list of records to go and look at — the open opportunities that
   stop a company being archived, the places a landing page leads. Each row
   is one link, the whole width: a reference (or an icon), the record's
   name with a line under it, something on the right — a value, a chip —
   and an arrow that moves when the row is hovered. Drawn by
   `includes/_record_list.html`.

   Bordered on its own; `record-list--flush` sits inside a panel that
   already has the border. */
.record-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--card-bg);
  overflow: hidden;
}

.record-list--flush { border: 0; border-radius: 0; }

.record-list__item + .record-list__item { border-top: 1px solid var(--divider); }

.record-list__link {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 0.875rem;
  padding: 0.75rem 1rem;
  color: var(--text);
  transition: background-color 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}

.record-list--flush .record-list__link { padding: 0.8rem 1.25rem; }

.record-list__link:hover { background: var(--primary-wash); text-decoration: none; }
.record-list__link:active { background: var(--primary-wash); transform: translateY(1px); }
.record-list__link:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

.record-list__ref {
  justify-self: start;
  padding: 0.15rem 0.45rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--page-bg);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--text-mid);
  white-space: nowrap;
}

.record-list__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-sm);
  background: var(--primary-wash);
  color: var(--primary);
}

.record-list__icon .icon { color: currentColor; }

.record-list__text { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }

.record-list__title {
  overflow: hidden;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--brand-dark);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.record-list__meta { font-size: 0.75rem; color: var(--text-muted); }

.record-list__aside {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.record-list__go { line-height: 0; }

.record-list__go .icon {
  color: var(--text-muted);
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), color 0.2s ease;
}

.record-list__link:hover .record-list__go .icon { color: var(--primary); transform: translateX(3px); }

@media (prefers-reduced-motion: reduce) {
  .record-list__link,
  .record-list__go .icon { transition: none; }
  .record-list__link:active,
  .record-list__link:hover .record-list__go .icon { transform: none; }
}

@media (max-width: 40rem) {
  .record-list__link { grid-template-columns: minmax(0, 1fr) auto; gap: 0.35rem 0.75rem; }
  .record-list__ref,
  .record-list__icon { grid-column: 1 / -1; }
  .record-list__title { white-space: normal; }
  .record-list__go { display: none; }
}

/* --- One record moving into another ----------------------------------------- */

/* "This one → that one": a merge, a hand-over. Two named tiles with the
   arrow between them, the one that stays tinted — so the direction is
   seen before it is read. Stacks on a phone, the arrow turned down. */
.move-pair {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: stretch;
  gap: 0.75rem;
}

.move-pair__side {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 0;
  padding: 0.75rem 0.875rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--page-bg);
}

.move-pair__side--stays { border-color: var(--primary-edge); background: var(--primary-wash); }

.move-pair__label { font-size: 0.6875rem; font-weight: 600; color: var(--text-muted); }
.move-pair__name { font-size: 0.875rem; font-weight: 700; color: var(--brand-dark); overflow-wrap: anywhere; }
.move-pair__meta { font-size: 0.6875rem; color: var(--text-mid); }

.move-pair__arrow { display: flex; align-items: center; line-height: 0; }
.move-pair__arrow .icon { color: var(--primary); }

@media (max-width: 40rem) {
  .move-pair { grid-template-columns: minmax(0, 1fr); }
  .move-pair__arrow { justify-content: center; }
  .move-pair__arrow .icon { transform: rotate(90deg); }
}

/* --- A focused column -------------------------------------------------------- */

/* A page that does ONE thing — mark it won, share it, send it, pick a
   company — reads down a column, not across a wide screen: a 1,100px input
   for a value or a date is a line the eye has to travel and a form that
   looks emptier than it is. `--wide` for a full record form with fields
   in two columns. */
.focus-column { max-width: 44rem; }
.focus-column--wide { max-width: 60rem; }

/* --- Actions in a row ------------------------------------------------------- */

/* Small actions on one row of a table — rename, move up, retire — as the
   kit's outlined chips ("Change…" on an access screen), not underlined
   words: they read as controls, they line up, and a row of four is a
   tidy group instead of a sentence. `--icon` is an arrow on its own (with
   an aria-label); `--danger` the one that removes or retires. Works on
   <a> and <button> alike. */
.row-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 0.375rem;
}

.row-actions > .cell-form { display: contents; }

/* The same chips at the start of a line — quick answers inside a form
   (Snooze's "Tomorrow" / "Next week"), where the form reads left to right. */
.row-actions--start { justify-content: flex-start; }

.row-action {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.25rem 0.55rem;
  border: 1px solid var(--primary-edge);
  border-radius: var(--radius-sm);
  background: var(--card-bg);
  font: inherit;
  font-size: 0.6875rem;
  font-weight: 600;
  line-height: 1.4;
  color: var(--primary);
  white-space: nowrap;
  cursor: pointer;
}

.row-action:hover { background: var(--primary-wash); color: var(--primary); text-decoration: none; }
.row-action:active { transform: translateY(1px); }
.row-action .icon { color: inherit; }

.row-action--icon { justify-content: center; width: 1.75rem; padding: 0.25rem 0; }

.row-action--danger { border-color: var(--bad-edge); color: var(--bad-ink); }
.row-action--danger:hover { background: var(--bad-bg); color: var(--bad-ink); }

.cell-edit > .row-action { flex: none; }

@media (prefers-reduced-motion: reduce) {
  .row-action:active { transform: none; }
}

/* --- What happens next, said once ------------------------------------------ */

/* The one line above a form's button that says what pressing it does —
   "OPP-0117 becomes Won at EUR 96,000.00, closed today." — set on a light
   wash so it is read as the summary of the form, not as one more field's
   help text. `_inline_note.html` with `summary=True`. */
.inline-note--summary {
  max-width: none;
  margin-top: 1.25rem;
  padding: 0.7rem 0.875rem;
  border: 1px solid var(--primary-edge);
  border-radius: var(--radius-sm);
  background: var(--primary-wash);
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--text);
}

.inline-note--summary .icon { color: var(--primary); }

/* A note followed by the field or the form it is about: a breath between
   them, so the label below does not read as the note's last line. */
.inline-note + .field,
.inline-note + form,
.inline-note + .form-grid { margin-top: 1rem; }

/* A summary that opens the form rather than closing it ("goes to the
   people you choose…") sits flush at the top. */
.inline-note--summary:first-child { margin-top: 0; }

/* --- Moving a record along its stages ---------------------------------------- */

/* Under a `.stepper`: the moves to the other open stages as the kit's row
   chips, "Move to" in front of them, and the decisions that END it — Mark
   won, Mark lost — as the buttons, on the right. A step and a decision do
   not look alike. Wraps to two lines on a narrow screen. */
.stage-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1.5rem;
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--divider);
}

.stage-actions__moves {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem;
}

.stage-actions__label { margin-right: 0.25rem; font-size: 0.75rem; color: var(--text-muted); }

.stage-actions__end { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-left: auto; }

/* --- A landing page: figures and the ways in -------------------------------- */

/* A `.stat` tile that is also the way into the list behind its number —
   the landing page's "Closing this month · 3". The whole tile is the
   link; hover lifts its border to the brand's edge, never a shadow. */
.stat--link {
  color: inherit;
  transition: border-color 0.2s ease, background-color 0.2s ease;
}

.stat--link:hover { border-color: var(--primary-edge); background: var(--primary-wash); text-decoration: none; }
.stat--link:active { transform: translateY(1px); }
.stat--link:focus-visible { outline: none; box-shadow: var(--focus-hard); }

@media (prefers-reduced-motion: reduce) {
  .stat--link { transition: none; }
  .stat--link:active { transform: none; }
}

/* One sentence under a panel's header saying what is in it, at the
   panel's own gutter, before a flush list. */
.panel__lede {
  margin: 0;
  padding: 0.75rem 1.25rem;
  max-width: calc(var(--measure) + 2.5rem);
  border-bottom: 1px solid var(--divider);
  font-size: 0.8125rem;
  color: var(--text-mid);
}

/* `.panel-pair` is declared once, further up. dev and this branch each
   invented it while the other was in flight - the landing pages' "two
   to a row, one on a narrow screen" and PR-4.2's "two cards side by
   side, the second a quarter". The one kept is the explicit
   `1fr 1fr` with the 1025px media query, for two reasons: the fixed
   breakpoint scale in CLAUDE.md rules out a content-driven collapse
   at 24rem, and `.panel-pair--quarter` (1fr 3fr) extends an explicit
   two-column base and does not survive `auto-fit`. On a desktop both
   gave two columns and on a phone both give one, so the landing pages
   read the same; the gap is 1.25rem rather than 1rem. */

/* Each column stacks its own panels, so a column with two panels in it
   does not need a wrapper with its own margins. */
.k-two-col > * { display: grid; gap: 1rem; min-width: 0; }

/* Equal halves, for two panels that answer the same kind of question —
   "where it went" beside "by supplier" on PR-7.2. */
.k-two-col--even { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* A card that runs the full width under a pair of columns, or above
   one. `.panel + .panel` cannot see across the grid wrapper, so without
   this the two butt together with nothing between them - the same gap
   `.panel + .panel-group` exists to close. */
.k-two-col + .panel,
.panel + .k-two-col,
.k-two-col + .k-two-col { margin-top: 1.25rem; }

@media (max-width: 64rem) {
  .k-two-col,
  .k-two-col--even { grid-template-columns: minmax(0, 1fr); }
}

/* --- A reference beside a record's title ------------------------------- */

/* The record's own number, set next to its name rather than under it:
   "Bulk materials 2026  CON-0031". Quieter and smaller than the heading
   it sits in, because the name is what somebody reads and the reference
   is what they quote. */
.page-head__ref {
  margin-left: 0.4rem;
  font-size: 1rem;
  font-weight: 500;
  color: var(--text-muted);
}

/* --- A meter that is nearly full --------------------------------------- */

/* The third tone the `.progress` component was missing. `--good` and
   `--bad` already say settled and wrong; this says "watch it" - a budget
   at 94% is neither of the other two, and the pack draws that row amber.
   The figure beside the bar is still what carries the fact (PR-7.1-R03);
   the tone is so somebody scanning a column knows which rows to read. */
.progress--warn .progress__bar { color: var(--warn-ink); }
.progress--warn .progress__bar::-webkit-progress-value { background: var(--warn-ink); }
.progress--warn .progress__bar::-moz-progress-bar { background: var(--warn-ink); }

/* The figure beside a meter, when the meter has run past its end. */
.progress__label--bad { color: var(--bad-ink); }

/* --- A figure that is good news ---------------------------------------- */

/* Money saved, on a stat tile and in a grid cell. The kit already had
   `--none` for a figure that could not be worked out; this is the other
   end of it. Used sparingly and never as decoration: green here means
   "this is money the company kept", and nothing else on the page may
   borrow it to mean "fine". */
.stat__value--good { color: var(--good-ink); }
.grid__cell--good { color: var(--good-ink); font-weight: 600; }

/* Its mirror, and held to the same discipline: red here means "this
   is what the choice costs over the alternative", never "wrong". The
   compare page's two warnings are the only callers - both of them say
   in words, above the table, that nothing is being refused. */
.grid__cell--bad { color: var(--bad-ink); font-weight: 600; }

.panel-pair > .panel { margin: 0; }
.result-summary + .panel-pair,
.stat-row + .panel-pair { margin-top: 1rem; }

.cancel-link .icon { color: currentColor; }

/* --- A list drawn as cards -------------------------------------------- */

/* For a list whose records are a handful of facts rather than a row of
   columns: an order template is a name, a company, an item count and how
   often it has been used, and a five-column table of that is five columns
   of mostly nothing. Rows are still right for anything you scan down one
   column of; this is for what you read across. A page that offers both
   switches with `?view=`, so each is a real address.

   `.record-cards` is the grid, `.record-cards__item` one card. The card is
   a `.panel` underneath, so it keeps the kit's radius, border and padding
   and gains no shadow (rule 13). `--off` greys a retired record without
   hiding it. Tokens only: nothing here knows which application drew it. */
.record-cards {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
  align-items: stretch;
}

/* The grid spaces its own children. Without this the kit's sibling rule
   (`.panel + .panel`) adds a top margin to every card but the first, and
   a row of cards comes out stepped down to the right - which is what the
   wrapper is here to prevent, the same way `.panel-pair` does. */
.record-cards > .panel + .panel { margin-top: 0; }

.record-cards__item {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  padding: 1.1rem 1.15rem;
  transition: border-color 0.12s ease, background 0.12s ease;
}

/* The whole card opens the record, without nesting the buttons inside a
   link - which is invalid, and which a screen reader reads as one
   enormous link with controls buried in it. The title is the only real
   anchor; its `::after` is stretched over the card, so a click anywhere
   that is not a control lands on it. */
.record-cards__item:has(.record-cards__link):hover {
  border-color: var(--primary);
  background: var(--primary-wash);
}

.record-cards__link { color: inherit; text-decoration: none; }
.record-cards__link::after { position: absolute; inset: 0; content: ""; }
.record-cards__link:hover { text-decoration: none; }
.record-cards__item:hover .record-cards__link { color: var(--primary); }

/* The keyboard gets the same highlight, on the card rather than on the
   few characters of the title. */
.record-cards__link:focus-visible { outline: none; }
.record-cards__item:has(.record-cards__link:focus-visible) {
  border-color: var(--primary);
  box-shadow: var(--focus-soft);
}

/* Anything somebody can press sits above the stretched link, or the
   card would swallow the click. */
.record-cards__aside,
.record-cards__actions,
.record-cards__item .progress { position: relative; z-index: 1; }

@media (prefers-reduced-motion: reduce) {
  .record-cards__item { transition: none; }
}

.record-cards__head {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  min-height: 2rem;
}

.record-cards__title {
  margin: 0;
  min-width: 0;
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.35;
  color: var(--brand-dark);
  overflow-wrap: anywhere;
}

.record-cards__sub {
  margin: 0.2rem 0 0;
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--text-muted);
  overflow-wrap: anywhere;
}

/* Pushed to the right of the title: a chip, a card menu, a toggle. */
.record-cards__aside {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-left: auto;
  flex: none;
}

/* The facts, two to a row, ruled off from the title above them so the
   card reads as a heading and then a record rather than one long column
   of text at four different weights. */
.record-cards__facts {
  display: grid;
  gap: 0.7rem 1rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin: 0;
  padding-top: 0.85rem;
  border-top: 1px solid var(--divider);
}

/* A fact that needs the whole width - a list of supplier names. */
.record-cards__facts--wide { grid-template-columns: minmax(0, 1fr); }
.record-cards__fact--wide { grid-column: 1 / -1; }

.record-cards__label {
  display: block;
  margin-bottom: 0.15rem;
  font-size: 0.6875rem;
  font-weight: 500;
  color: var(--text-muted);
}

.record-cards__value {
  display: block;
  min-width: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.4;
  color: var(--text);
  overflow-wrap: anywhere;
}

.record-cards__value--muted { font-weight: 500; color: var(--text-muted); }

/* The last thing in the card, held to the bottom and ruled off, so a row
   of cards of different heights still lines its buttons up. */
.record-cards__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
  margin-top: auto;
  padding-top: 0.85rem;
  border-top: 1px solid var(--divider);
}

/* A control that posts is wrapped in its own <form>, and that form was
   the flex item rather than the button inside it: it shrank below the
   button's width and the two controls drew over each other. The kit
   already solves this for a table row (`.row-actions > .cell-form`);
   the same answer here, so the button itself is the flex item. */
.record-cards__actions > form { display: contents; }

/* Two actions of equal standing, side by side and sharing the width -
   the shape a card ends in when its actions ARE the point of it, rather
   than the rare and the undoing kind that belong in the `⋯` menu. They
   wrap to their own lines on a card too narrow to hold both, which is
   what `flex-basis` here is for rather than a breakpoint. */
.record-cards__actions--split > .button {
  flex: 1 1 9rem;
  justify-content: center;
}

/* A fact that is worth lifting off the card's white - who a template
   suggests asking, which is the one thing on it somebody scans for. */
.record-cards__note {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  padding: 0.7rem 0.8rem;
  border-radius: var(--radius-sm);
  /* gray-100, not the page's gray-50: inside a white card the lighter
     tint is almost invisible, and the point of the block is that the
     eye finds it without reading. */
  background: var(--divider);
}

.record-cards__note .icon { flex: none; margin-top: 0.1rem; color: var(--text-muted); }
.record-cards__note-text { min-width: 0; }

.record-cards__item--off { background: var(--page-bg); }
.record-cards__item--off .record-cards__title { color: var(--text-mid); }

@media (max-width: 40rem) {
  .record-cards { grid-template-columns: minmax(0, 1fr); }
}


/* --- A layer that escapes the card ------------------------------------- */

/* Every menu, dropdown and tooltip in the kit opens inside a card, and a
   card clips: `.panel` carries `overflow: hidden` for its rounded
   corners, and `.grid-scroll` is a sideways scroller - which the spec
   turns into a vertical clip too, because a `visible` on one axis
   computes to `auto` when the other is not visible. So a panel opened
   near an edge was cut flat, worst in a table's last row.

   `shell.js` shows such a panel in the browser's TOP LAYER (the native
   `popover` attribute), which no ancestor's overflow can clip and no
   z-index on the page can cover - an open <dialog> included, so a
   dropdown inside a pop-up form lands above the form. It marks the
   panel `.kit-layer` and sets two custom properties for where it goes;
   every rule that reads them is here, so no styling lives in the
   script.

   Without `popover` the attribute is never added, none of these rules
   match, and each component keeps the `position: absolute` rules it has
   always had: the menu opens and works, clipped as it was before. */

.kit-layer:popover-open {
  position: fixed;
  inset: auto;
  left: var(--layer-x, 0);
  top: var(--layer-y, 0);
  margin: 0;
  /* The UA stylesheet gives a popover its own border and padding; each
     component's own rules are author styles and already win. Only the
     box's placement and its scrolling need saying here. */
  max-height: none;
  overflow: visible;
}

/* A panel that matched the width of the control it drops from keeps
   doing so: `left: 0; right: 0` cannot survive being taken out of the
   card, so the width is passed in instead. */
.picker__panel.kit-layer:popover-open { width: var(--layer-w, auto); }

/* The tooltip is hidden by `visibility` and `opacity` rather than by
   `display`, so that it can fade. Shown in the top layer it is governed
   by the popover's own open state instead. */
.grid__tip.kit-layer:popover-open { visibility: visible; opacity: 1; }

/* A popover is NOT moved in the DOM - only painted in the top layer -
   so the tooltip is still its label's sibling and the two hover rules
   above still match it. They do no harm: a closed popover is
   `display: none` by the UA stylesheet whatever its visibility says,
   and an open one is covered by the rule above. Nothing to undo.

   What does have to be undone is the nudge that pulls the last two
   columns' bubbles in from the table's right edge. It is more specific
   than `.kit-layer:popover-open`, so it would win and pin a lifted
   tooltip to the wrong side; keeping a layer inside the viewport is
   `place()`'s job now. */
.grid thead th:last-child .grid__tip.kit-layer:popover-open,
.grid thead th:nth-last-child(2) .grid__tip.kit-layer:popover-open {
  left: var(--layer-x, 0);
  right: auto;
}


/* --- An outlined button ------------------------------------------------ */

/* The brand blue, inverted: white with a blue edge, filling blue when
   somebody is about to press it. For TWO actions of equal standing,
   where one filled button beside another filled one would be two
   things shouting (rule 13 - every button is still #1640FF; what
   changes is which side of the edge the colour sits on).

   `.button--ghost`, despite its name, is filled. It is older and used
   widely, so it is left as it is rather than renamed. */
.button--outline {
  color: var(--primary);
  background: var(--card-bg);
  border-color: var(--primary-edge);
}

.button--outline:hover {
  color: var(--white);
  background: var(--primary);
  border-color: var(--primary);
  filter: none;
}

.button--outline .icon { color: currentColor; }

/* --- AI slot -------------------------------------------------------------- */

/* Where a screen will offer to draft something (CRM-S54,
   `includes/_ai_slot.html`): a dashed, quiet box — an icon, the one
   sentence saying what it drafts, and either why it is off or its one
   control. The same box in the same place on or off, so switching AI
   drafting on never moves anything around it. Dashed rather than solid so
   it never reads as a field to fill in; no fill, no shadow. */
.ai-slot {
  display: flex;
  align-items: flex-start;
  gap: 0.625rem;
  margin: 0 0 0.875rem;
  padding: 0.7rem 0.875rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  background: transparent;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--text-mid);
}

.ai-slot__icon { display: inline-flex; flex: 0 0 auto; padding-top: 0.15rem; }

/* `--icon` is white, for a button's blue fill; on this quiet box it is
   the guidelines' gray-400. */
.ai-slot__icon .icon { color: var(--text-muted); }

.ai-slot__body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.25rem;
  min-width: 0;
}

.ai-slot__label { font-weight: 600; overflow-wrap: anywhere; }

.ai-slot__state { color: var(--text-mid); }

.ai-slot__action { margin-top: 0.125rem; }

/* On: the edge and the icon take the brand's colour; the control is the
   kit's row chip, so the form's own button stays the one blue button. */
.ai-slot--on { border-color: var(--primary-edge); }
.ai-slot--on .ai-slot__icon .icon { color: var(--primary); }

/* Its control waits for the page's script (a POST through HTMX), and a
   chip's own display would otherwise win over `hidden`. */
.ai-slot__action[hidden],
.ai-slot__undo[hidden] { display: none; }

/* While a draft is being written: the chip is disabled by HTMX. */
.ai-slot__action:disabled { opacity: 0.6; cursor: progress; }

/* What came back, or why nothing did — one plain sentence. */
.ai-slot__result { color: var(--text-mid); overflow-wrap: anywhere; }

/* Drafts that belong to the slot itself (New proposal's summary and scope,
   the reports narrative, an upsell line per client): editable boxes the
   width of the slot, each under its own label. Never saved by the slot. */
.ai-slot__drafts {
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
  align-self: stretch;
  margin-top: 0.375rem;
}

.ai-slot__line { display: flex; flex-direction: column; gap: 0.25rem; }

.ai-slot__line-label { font-weight: 600; color: var(--text); overflow-wrap: anywhere; }

.ai-slot__draft {
  width: 100%;
  min-width: 0;
  padding: 0.5rem 0.625rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--card-bg);
  font: inherit;
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--text);
}

textarea.ai-slot__draft { resize: vertical; }

.ai-slot__draft:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
  border-color: var(--primary);
}

/* A step and the lines under it: a permit type's steps and their documents. */
.row-editor--steps .row-editor__head,
.row-editor--steps .row-editor__row { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) 4rem; align-items: start; }
.row-editor--steps textarea { min-height: 4.5rem; }

/* --- Charts ------------------------------------------------------------- */

/* Two charts and no library (P3-04, CRM-10.1): horizontal bars
   (`includes/_bar_chart.html`) and vertical columns
   (`includes/_column_chart.html`), their figures worked out by
   `apps/core/charts.py`. Every length is an attribute - a <progress>'s
   `value` and `max`, an SVG <rect>'s `height` - never an inline width
   (rule 33), and every chart carries its figures as text beside or under
   the picture. Flat, the brand blue, no shadow (ui-guidelines.md). An
   application never draws a chart of its own (rules 13 and 14). */
.chart { margin: 0; }

.chart + .chart { margin-top: 1.25rem; }

.chart__caption {
  margin: 0 0 0.75rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-mid);
}

/* Nothing to draw: one quiet line where the chart would be, never an
   empty axis that reads as "nought". */
.chart__empty {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--text-mid);
}

/* The figures under a chart, shown for everybody rather than only read
   out. The kit's `.grid`, a little apart from the picture. */
.chart__table { margin-top: 1rem; }

/* Horizontal bars. One row a thing counted: its label, the bar, its
   figure. The label column has the same bounds on every row, so the bars
   start in one line however long the labels are. */
.bar-chart {
  display: grid;
  gap: 0.625rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.bar-chart__row {
  display: grid;
  grid-template-columns: minmax(6rem, 12rem) minmax(4rem, 1fr) auto;
  align-items: center;
  gap: 0.75rem;
  font-size: 0.8125rem;
}

.bar-chart__label { min-width: 0; color: var(--text); overflow-wrap: anywhere; }

/* The `.progress` bar's shape, a little taller - it is the figure here,
   not a meter under one. */
.bar-chart__bar {
  width: 100%;
  min-width: 4rem;
  height: 0.625rem;
  overflow: hidden;
  border: 0;
  border-radius: 999px;
  background: var(--divider);
  color: var(--primary);
  -webkit-appearance: none;
  appearance: none;
}

.bar-chart__bar::-webkit-progress-bar { border-radius: 999px; background: var(--divider); }
.bar-chart__bar::-webkit-progress-value { border-radius: 999px; background: var(--primary); }
.bar-chart__bar::-moz-progress-bar { border-radius: 999px; background: var(--primary); }

.bar-chart__figure {
  font-weight: 600;
  color: var(--text-strong);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

.bar-chart__note {
  margin-left: 0.4rem;
  font-weight: 400;
  color: var(--text-mid);
}

.bar-chart__row--good .bar-chart__bar { color: var(--good-ink); }
.bar-chart__row--good .bar-chart__bar::-webkit-progress-value { background: var(--good-ink); }
.bar-chart__row--good .bar-chart__bar::-moz-progress-bar { background: var(--good-ink); }

.bar-chart__row--warn .bar-chart__bar { color: var(--warn-ink); }
.bar-chart__row--warn .bar-chart__bar::-webkit-progress-value { background: var(--warn-ink); }
.bar-chart__row--warn .bar-chart__bar::-moz-progress-bar { background: var(--warn-ink); }

.bar-chart__row--bad .bar-chart__bar { color: var(--bad-ink); }
.bar-chart__row--bad .bar-chart__bar::-webkit-progress-value { background: var(--bad-ink); }
.bar-chart__row--bad .bar-chart__bar::-moz-progress-bar { background: var(--bad-ink); }
.bar-chart__row--bad .bar-chart__figure { color: var(--bad-ink); }

.bar-chart__row--muted .bar-chart__bar { color: var(--text-muted); }
.bar-chart__row--muted .bar-chart__bar::-webkit-progress-value { background: var(--text-muted); }
.bar-chart__row--muted .bar-chart__bar::-moz-progress-bar { background: var(--text-muted); }

/* Vertical columns. The SVG stretches to its box, so the columns share
   the width however many there are; the labels underneath are a row of
   the same number of equal cells, so each sits under its column. A rule
   at the top of the plot carries the scale's figure. */
.column-chart {
  position: relative;
  padding-top: 1.25rem;
}

.column-chart__top {
  position: absolute;
  top: 0;
  left: 0;
  font-size: 0.6875rem;
  color: var(--text-mid);
  font-variant-numeric: tabular-nums;
}

.column-chart__plot {
  display: block;
  width: 100%;
  height: 10rem;
  overflow: visible;
}

.column-chart__column { fill: var(--primary); }
.column-chart__column--good { fill: var(--good-ink); }
.column-chart__column--warn { fill: var(--warn-ink); }
.column-chart__column--bad { fill: var(--bad-ink); }
.column-chart__column--muted { fill: var(--primary-edge); }

.column-chart__axis { stroke: var(--border); stroke-width: 1; }
.column-chart__rule { stroke: var(--border); stroke-width: 1; stroke-dasharray: 4 4; }

.column-chart__labels {
  display: flex;
  margin: 0.375rem 0 0;
  padding: 0;
  list-style: none;
}

.column-chart__label {
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  font-size: 0.6875rem;
  color: var(--text-mid);
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A phone: the label takes its own line above the bar and the figure,
   so a long reason is never squeezed down to a word a line. */
@media (max-width: 40rem) {
  .bar-chart__row { grid-template-columns: minmax(0, 1fr) auto; row-gap: 0.25rem; }
  .bar-chart__label { grid-column: 1 / -1; }
  .column-chart__plot { height: 8rem; }
}


/* --- A donut, a ring and a sparkline ---------------------------------------

   Three more shapes beside the kit's bars and columns, added with
   Procurement's dashboard and shared from that day. Server-rendered SVG:
   no charting library, no CDN, and every length an ATTRIBUTE rather than
   an inline style (rule 33).

   ONE ACCENT, IN TINTS. A donut's segments are the application's own
   accent at five strengths plus a neutral grey for "Other" - never a
   rainbow, which makes a chart look like a toy and means nothing (a hue
   per category says the categories differ in KIND, and they do not).
   Five is where tints still tell apart; `charts.TINTS` says the same
   number in Python, and the sixth category onward is the grey.

   COLOUR IS NEVER THE ONLY CARRIER. Every segment's label and figure are
   real text in the legend, every ring prints its own percentage, and a
   sparkline's arrow has a word beside it. Hide every colour and all three
   still read. */

.donut__body {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1.25rem;
}

.donut__plot {
  position: relative;
  flex: 0 0 auto;
  width: 8.5rem;
  height: 8.5rem;
}

.donut__ring { width: 100%; height: 100%; transform: rotate(-90deg); }

.donut__track,
.donut__arc {
  fill: none;
  stroke-width: 4.5;
}

.donut__track { stroke: var(--divider); }

/* The arcs butt onto each other, so a round cap would have each one
   overlap the next by half its width and the small ones would read
   bigger than they are. */
.donut__arc { stroke-linecap: butt; }

.donut__arc--1 { stroke: rgb(var(--app-accent-rgb) / 100%); }
.donut__arc--2 { stroke: rgb(var(--app-accent-rgb) / 78%); }
.donut__arc--3 { stroke: rgb(var(--app-accent-rgb) / 58%); }
.donut__arc--4 { stroke: rgb(var(--app-accent-rgb) / 40%); }
.donut__arc--5 { stroke: rgb(var(--app-accent-rgb) / 25%); }
.donut__arc--other { stroke: var(--text-muted); }

/* A tone is meaning, so it wins over the tint it was given. */
.donut__arc--good { stroke: var(--good-strong); }
.donut__arc--warn { stroke: var(--warn-ink); }
.donut__arc--bad { stroke: var(--bad-ink); }

/* The hole, and ONLY the hole. `inset: 0` covered the whole plot, so a
   centre figure wider than the hole - which any full money string is,
   at the old 17px - printed straight across the arcs. The ring's stroke
   is 4.5 of a 42-unit box, so the hole is about 65% of the plot's width
   and an inset of 18% a side lands just inside it. The text wraps and
   breaks rather than spilling, because a figure nobody can read is
   still better than one drawn over the chart. */
.donut__centre {
  position: absolute;
  inset: 18%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.1rem;
  pointer-events: none;
  text-align: center;
  overflow: hidden;
}

.donut__centre-figure {
  font-size: 0.875rem;
  font-weight: 700;
  line-height: 1.2;
  color: var(--brand-dark);
  /* A long currency amount has no space to break at. Better two lines
     inside the hole than one line across the ring. */
  overflow-wrap: anywhere;
}

.donut__centre-note {
  font-size: 0.6875rem;
  color: var(--text-mid);
}

.donut__legend {
  flex: 1 1 11rem;
  min-width: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

.donut__key {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.25rem 0;
  font-size: 0.8125rem;
}

.donut__swatch {
  width: 0.625rem;
  height: 0.625rem;
  border-radius: 0.1875rem;
  /* Aligned to the text's baseline rather than its box: a square beside
     a cap-height letter sits high otherwise. */
  transform: translateY(0.05rem);
}

.donut__swatch--1 { background: rgb(var(--app-accent-rgb) / 100%); }
.donut__swatch--2 { background: rgb(var(--app-accent-rgb) / 78%); }
.donut__swatch--3 { background: rgb(var(--app-accent-rgb) / 58%); }
.donut__swatch--4 { background: rgb(var(--app-accent-rgb) / 40%); }
.donut__swatch--5 { background: rgb(var(--app-accent-rgb) / 25%); }
.donut__swatch--other { background: var(--text-muted); }

/* A toned segment needs a toned swatch. `_donut_chart.html` puts
   `donut__arc--{{ part.tone }}` on the arc on top of its tint, and the
   swatch carries the tint alone - so a part given a tone was drawn
   green, amber or red in the ring and still keyed with the tint it
   would have had. Nothing passes a tone to a donut today, which is why
   it has not shown: this closes it before something does. The three
   values are `.donut__arc--good` / `--warn` / `--bad`'s own. */
.donut__swatch--good { background: var(--good-strong); }
.donut__swatch--warn { background: var(--warn-ink); }
.donut__swatch--bad { background: var(--bad-ink); }

.donut__name {
  min-width: 0;
  overflow: hidden;
  color: var(--text);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.donut__name a { color: inherit; }

.donut__figure {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--brand-dark);
  white-space: nowrap;
}


/* --- One figure against its own target ------------------------------------ */

.ring {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-width: 0;
}

.ring__plot {
  position: relative;
  flex: 0 0 auto;
  width: 3.75rem;
  height: 3.75rem;
}

.ring__svg { width: 100%; height: 100%; transform: rotate(-90deg); }

.ring__track,
.ring__arc {
  fill: none;
  stroke-width: 5;
}

.ring__track { stroke: var(--divider); }
.ring__arc { stroke: var(--primary); stroke-linecap: round; }

.ring--good .ring__arc { stroke: var(--good-strong); }
.ring--warn .ring__arc { stroke: var(--warn-ink); }
.ring--bad .ring__arc { stroke: var(--bad-ink); }

.ring__share {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--brand-dark);
  font-variant-numeric: tabular-nums;
}

/* A target nobody set is a dash, never a nought: "none of it used" and
   "nothing to measure against" are different answers. */
.ring__dash { color: var(--text-muted); }

.ring__text { display: grid; gap: 0.1rem; min-width: 0; }

.ring__label {
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-mid);
}

.ring__figure {
  font-size: 1.0625rem;
  font-weight: 700;
  line-height: 1.2;
  color: var(--brand-dark);
}

.ring__note { font-size: 0.6875rem; color: var(--text-mid); }

/* Over budget, said in a word beside the figure as well as in the
   colour of the arc. */
.ring__over {
  justify-self: start;
  padding: 0.05rem 0.35rem;
  border: 1px solid var(--bad-edge);
  border-radius: var(--radius-sm);
  font-size: 0.625rem;
  font-weight: 700;
  color: var(--bad-ink);
  text-transform: none;
}


/* --- A trend inside a tile ------------------------------------------------- */

.sparkline {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  min-width: 0;
}

.sparkline__plot {
  width: 4.5rem;
  height: 1.25rem;
  overflow: visible;
}

.sparkline__line {
  stroke: var(--primary);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* The viewBox is stretched to the box, so a stroke scaled with it
     would be fat one way and thin the other. */
  vector-effect: non-scaling-stroke;
}

.sparkline--good .sparkline__line { stroke: var(--good-strong); }
.sparkline--warn .sparkline__line { stroke: var(--warn-ink); }
.sparkline--bad .sparkline__line { stroke: var(--bad-ink); }

.sparkline__trend {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--text-mid);
}

.sparkline__trend .icon { color: inherit; }
.sparkline__trend--up { color: var(--good-strong); }
.sparkline__trend--down { color: var(--bad-ink); }
.sparkline__trend--flat { color: var(--text-muted); }


/* --- A chart's tooltip ----------------------------------------------------- */

/* The exact figure, on hover and on focus. Shown by `:hover` /
   `:focus-within` like a column header's, and lifted into the browser's
   top layer by `wireChartTip` in shell.js so a panel's `overflow:
   hidden` cannot cut it in half - the same `.kit-layer` mechanism, for
   the same reason.

   It is a nicety, not the chart: every figure it shows is already on the
   page in the legend beside it. */
.chart-tip {
  position: absolute;
  bottom: calc(100% + 0.25rem);
  left: 0;
  z-index: 5;
  max-width: 16rem;
  padding: 0.35rem 0.5rem;
  border-radius: var(--radius-sm);
  background: var(--brand-dark);
  color: var(--white);
  font-size: 0.6875rem;
  font-weight: 500;
  line-height: 1.35;
  white-space: normal;
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.12s ease;
  pointer-events: none;
}

.donut__key:hover .chart-tip,
.donut__key:focus-within .chart-tip {
  visibility: visible;
  opacity: 1;
}

.chart-tip.kit-layer:popover-open { visibility: visible; opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .chart-tip { transition: none; }
}


/* A phone: the ring and its legend stack, and nothing scrolls sideways. */
@media (max-width: 40rem) {
  .donut__body { gap: 0.875rem; }
  .donut__plot { width: 7rem; height: 7rem; }
  .donut__legend { flex-basis: 100%; }
}


/* A second series behind the first, to the same scale - Procurement's
   dashboard draws committed behind spent. Only a column the caller gave
   `committed_height` has one, so every chart already on the product is
   untouched. Tinted rather than hued: one accent, two strengths. */
.column-chart__column--second { fill: rgb(var(--app-accent-rgb) / 28%); }

/* --- A list whose chrome stays put --------------------------------------

   Opt in with `.sticky-list` beside `.panel`. It lives here rather
   than in an application's own sheet because the kit owns tables
   (rules 13 and 14) - and because a rule in `procurement.css` was
   invisible on nine of the ten screens that wanted it: only
   `access.html` links that sheet, so the class was on the markup
   and the CSS was never loaded. Nobody but Procurement uses it
   today; the scope is the class, not the file. The filter bar parks under
   the topbar and the column headers park under the filter bar, so a
   long list keeps both the controls and the column names on screen.

   **Two of the kit's rules have to give way, and that is the whole
   cost of it.** `position: sticky` is measured against the nearest
   ancestor that SCROLLS, so `.panel`'s `overflow: hidden` and
   `.grid-scroll`'s `overflow-x: auto` each turn a sticky header into
   one stuck to a box that is itself scrolling away - it does nothing
   at all, silently. Dropping both is what makes this work, and the
   consequence is real: a table too wide for the window now scrolls the
   PAGE sideways instead of scrolling inside its own panel.

   **Desktop only (1025px and up)** for exactly that reason. Below it
   the kit either stacks each row (`.grid--stack`) or needs the sideways
   scroll more than it needs a header that follows, and `.filter-bar`'s
   own collapse has already put the selects behind a button - a sticky
   bar there would be a toggle taking up room rather than any help.

   **The offset is measured, never guessed.** `--filter-bar-height` is
   written by `wireStickyList` in shell.js, so a bar that wraps to two
   rows on a narrower desktop still parks the headers underneath itself
   rather than behind it. The fallback is one row's worth, which is what
   it is on a full-width window.

   Backgrounds are already opaque on both (`--page-bg`), which is what
   stops the rows showing through as they pass underneath. */
@media (min-width: 64.0625rem) {
  .sticky-list,
  .sticky-list .grid-scroll { overflow: visible; }

  .sticky-list .filter-bar {
    position: sticky;
    top: var(--topbar-height);
    z-index: 3;
  }

  .sticky-list .grid thead th {
    position: sticky;
    top: calc(var(--topbar-height) + var(--filter-bar-height, 3.3rem));
    z-index: 2;
  }

  /* The panel's rounded corners were being clipped by the `overflow`
     that has just gone, so the corner pieces round themselves off
     again: whichever element is first inside the panel at the top, and
     the table's last row at the bottom. */
  .sticky-list > :first-child,
  .sticky-list .panel__header {
    border-top-left-radius: var(--radius);
    border-top-right-radius: var(--radius);
  }
  .sticky-list .grid tbody tr:last-child > *:first-child { border-bottom-left-radius: var(--radius); }
  .sticky-list .grid tbody tr:last-child > *:last-child { border-bottom-right-radius: var(--radius); }
}

/* A day calendar: one row per person, a column per day, their days filled in
   (HR's leave calendar, HR-8.1-R01). Narrow on purpose; it scrolls in its own box. */
.day-calendar { border-collapse: collapse; font-size: 0.75rem; }
.day-calendar th, .day-calendar td { border: 1px solid var(--divider); }
.day-calendar__who { padding: 0.4rem 0.6rem; text-align: left; white-space: nowrap; font-weight: 600; }
.day-calendar__day { min-width: 1.6rem; height: 1.8rem; text-align: center; color: var(--text-muted); }
.day-calendar__day--weekend { background: var(--page-bg); }
.day-calendar__day--off { background: var(--primary); }
.day-calendar__bar { padding: 0 0.15rem; }
.day-calendar__label { display: block; overflow: hidden; padding: 0.15rem 0.4rem; border-radius: var(--radius-sm); background: var(--primary); color: var(--white); font-weight: 600; text-align: left; text-overflow: ellipsis; white-space: nowrap; }

/* An AI slot's answer, under its box; "working" only while the request runs. */
.ai-slot__busy { display: none; color: var(--text-muted); font-weight: 400; }
.htmx-request .ai-slot__busy { display: inline; }
.ai-slot__result:empty { display: none; }
.ai-slot__result { margin: 0.5rem 0 1rem; }
.ai-slot__result .field textarea { width: 100%; }
