/* ==========================================================================
   Application styles. Hand-written, no build step, tokens from tokens.css.

   ⚠️ THE VISUAL LANGUAGE IS design/palette.html, AND IT IS NOT ONLY COLOUR.
   That file also settles the type scale, the table treatment, the chip shape
   and the density. This stylesheet implements it. Where the two disagree,
   palette.html wins and this file gets fixed (CLAUDE.md).

   Three things everything here is built around:

   • The coordinator wants a hundred rows on screen, not twelve cards
     (design/04 P3). Dense by default; whitespace is spent on hierarchy, not
     on padding.
   • The technician is one-handed, sometimes gloved, on a roof. Nothing
     tappable is smaller than --touch-target.
   • Colour never carries meaning alone. Every state carries a word, and the
     chips carry a dot as a second non-colour signal.
   ========================================================================== */

*,
*::before,
*::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  font-family: var(--sans);

  /* 15px, per palette.html. The denser scale is what stops a screen of forty
     rows turning into a screen of twelve. */
  font-size: 15px;
  line-height: 1.55;
  color: var(--ink);
  background: var(--page);
  -webkit-font-smoothing: antialiased;

  /* ❗ THE FOOTER BELONGS TO THE BOTTOM OF THE PAGE, NOT TO THE BOTTOM OF
     THE CONTENT. Most screens here are short — an empty client list is three
     sentences and a button — so the footer's rule landed a third of the way
     down the window with a field of --page below it, which reads as the page
     having stopped loading rather than having ended.

     A column flex body and a <main> that takes the slack (below) puts the
     footer on the bottom edge when the content is short, and leaves it exactly
     where it falls when the content is long.

     ⚠️ dvh, not vh. On a phone vh is the viewport at its TALLEST — address
     bar retracted — so 100vh on a page that already fits adds the bar's height
     and makes the whole thing scroll for nothing. The vh line above it is the
     fallback for an engine that does not know dvh, the same pair auth.css
     already uses with svh. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

/* ── Type ─────────────────────────────────────────────────────────────────
   Sizes and tracking from palette.html. The negative tracking on the large
   sizes is what stops a heading reading as "the body text but bigger". */

h1, h2, h3, h4 { margin: 0; font-weight: 650; }

h1 { font-size: 1.9375rem; line-height: 1.2;  letter-spacing: -0.02em; }
h2 { font-size: 1.3125rem; line-height: 1.25; letter-spacing: -0.015em; }

/* h3 is a SECTION LABEL, not a small heading. Uppercase and quiet, so it
   organises a card without competing with the title above it. */
h3 {
  font-size: 0.8125rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-weight: 700;
}

h4 { font-size: 0.9375rem; }

p { margin: 0; }

/* Prose is capped at a readable measure. A paragraph running the full 1120px
   is genuinely hard to read — the eye loses the line coming back. */
.prose { max-width: 72ch; color: var(--ink-2); }

.eyebrow {
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--navy-500);
  font-weight: 700;
}

.muted { color: var(--ink-3); }
.small { font-size: 0.8125rem; }

/* A code, a job number, a reference read back over the phone. */
.mono {
  font-family: var(--mono);
  font-size: 0.8125rem;
  letter-spacing: -0.01em;
}

/* A table narrow enough to read as a figure list rather than a data grid. */
.table--auto { width: auto; max-width: 22rem; }

/* Money and any figure in a column. Tabular figures are the whole point:
   without them 2 500.50 and 195.00 do not line up, and a money column that
   does not line up is a money column nobody scans. */
.num {
  font-variant-numeric: tabular-nums;
  text-align: right;
}

a { color: var(--navy-600); text-underline-offset: 2px; }
a:hover { color: var(--navy-700); }

/* Never remove a focus ring. A keyboard-only coordinator lives on it. */
:focus-visible {
  outline: 2px solid var(--navy-500);
  outline-offset: 2px;
  border-radius: 3px;
}

/* ⚠️ TRANSITIONS AND ANIMATIONS ARE TWO PROPERTIES, AND ONLY ONE WAS HERE.
   Flattening transition-duration does nothing to a @keyframes animation, so
   the sign-in screen's entrance would have played in full for somebody who
   has asked the operating system for no motion. Both are switched off, and
   the iteration count with them — a 0.01ms animation repeated for ever is
   still a repaint every frame. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

/* ── Layout ───────────────────────────────────────────────────────────────
   ONE container width, shared by the header, the page and the footer. */

.container {
  width: 100%;
  max-width: var(--container);
  margin: 0 auto;
  padding-inline: var(--gutter);
}

/* Hidden until it is focused, then the first thing on the page. The
   coordinator lives on the keyboard once the dispatch board exists. */
.skip-link {
  position: absolute;
  left: var(--space-3);
  top: -3rem;
  z-index: 30;
  background: var(--surface);
  color: var(--navy-700);
  font-weight: 650;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
  transition: top 120ms ease;
}

.skip-link:focus { top: var(--space-3); }

.app-header {
  background: var(--navy-900);
  color: #fff;
  box-shadow: var(--shadow-2);
  position: sticky;
  top: 0;
  z-index: 20;
}

/* ❗ THE CHROME BANDS ARE FULL-BLEED. THE PAGE IS NOT.
   The header's contents used to sit in the same 1120px container as the page
   below, so the brand lined up with the first column of the table under it.
   On a wide monitor that put the brand and the Sign out button several hundred
   pixels in from either end of a navy bar spanning the whole screen, and the
   empty navy at both ends read as the header failing to fill.

   The bar now runs edge to edge; only the gutter holds its contents off the
   glass, because text against the very edge of the screen is its own fault.

   ⚠️ The page below deliberately does NOT follow. A table stretched across
   1920px is a line the eye loses its place on halfway across, and the reading
   measure is the reason .container exists. Losing the brand / first-column
   alignment is the price of this, and it is the cheaper of the two.

   It carries its own width rather than borrowing .container, so no class list
   can say one thing while the rule says another. */
/* ❗ THREE COLUMNS, SO THE CENTRE IS THE CENTRE OF THE WINDOW.
   The obvious way to centre the navigation is margin-inline: auto on it, which
   centres it in whatever space the brand and the account menu leave over -
   and the account menu is a PERSON'S NAME. "Jo Smit" and "Thandiwe
   Mahlangu-Petersen" are eighty pixels apart, so the whole navigation would
   sit in a different place depending on who was signed in, and it would move
   under somebody the day they were renamed. The same fault as the ragged chips
   on the jobs list: a position that depends on the width of the thing next to
   it is not a position.

   1fr auto 1fr is what fixes it. Grid keeps the two fr tracks EQUAL - the
   wider side sets both - so the middle column is centred on the bar itself and
   the name has no say in it.

   ⚠️ Both outer children name their column, because the client-facing header
   has a brand and an account block and NO navigation. Left to auto-placement
   its account block would drop into the middle column and sit centred. */
.app-header__inner {
  width: 100%;
  padding-inline: var(--gutter);
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--space-5);
  min-height: 56px;
  padding-block: var(--space-2);
}

.app-header__brand { grid-column: 1; }
.app-nav { grid-column: 2; }
.app-header__actions { grid-column: 3; }

/* ⚠️ Below a laptop the three columns have nothing to share out - the brand
   and the account menu are close to the whole width - and squeezing the
   navigation into what is left wraps it into four rows inside a STICKY header.
   Back to the row that wraps, where the navigation takes a line of its own.
   Nothing is centred at that size because there is no centre to be in. */
@media (max-width: 64rem) {
  .app-header__inner {
    display: flex;
    flex-wrap: wrap;
  }

  /* The navigation takes a line of its own rather than shouldering the account
     menu onto one. Left to wrap on its own the menu was what dropped, so the
     one control whose position people learn moved on a narrow window while the
     links stayed put - backwards. */
  .app-nav {
    order: 3;
    flex-basis: 100%;
  }
}

.app-header__brand {
  font-weight: 700;
  font-size: 0.9375rem;
  letter-spacing: -0.01em;
  color: #fff;
  text-decoration: none;
  white-space: nowrap;
}

.app-header__actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-left: auto;
}

.app-header__user {
  font-size: 0.8125rem;
  color: var(--navy-200);
  white-space: nowrap;
}

.app-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  flex-wrap: wrap;
}

/* ❗ THE DIRECT CHILD COMBINATOR IS LOAD-BEARING.
   These rules paint white-on-navy. The grouped areas put their links inside a
   pale panel hanging below the bar, and as a descendant selector every rule
   here reached into it - pale blue text and a white-on-white hover, on a
   surface. > keeps the bar's styling in the bar. */
.app-nav > a,
.nav-group > summary {
  color: var(--navy-200);
  text-decoration: none;
  font-size: 0.875rem;
  font-weight: 600;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius);
  line-height: 1.2;
  white-space: nowrap;
}

.app-nav > a:hover,
.nav-group > summary:hover { color: #fff; background: rgba(255, 255, 255, 0.08); }

/* WHERE YOU ARE, said twice over: filled on the exact screen, underscored on
   the area holding it. They have to be different marks, because with a group
   collapsed the area badge is the only one on screen - and if it looked like
   the page badge it would claim to be a screen you are not on. */
.app-nav > a[aria-current="page"] {
  color: #fff;
  background: rgba(255, 255, 255, 0.12);
}

/* ── Grouped navigation ───────────────────────────────────────────
   <details>, and not a scripted menu. The disclosure, the Enter and Space
   keys, the focus order and the screen-reader announcement all arrive with the
   element; site.js adds only what the element genuinely lacks, which is
   closing. A menu built out of buttons and JavaScript would have been three
   times the code to arrive at the same keyboard behaviour, and would have
   broken with a script error rather than degrading to an open list. */

.nav-group { position: relative; }

.nav-group > summary {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  cursor: pointer;

  /* Both are needed: the shorthand for Firefox and Chrome, the pseudo-element
     for Safari, which still ships the older marker. */
  list-style: none;
}

.nav-group > summary::-webkit-details-marker { display: none; }

/* Our own chevron. A character rather than an SVG or a background image - the
   CSP allows no external asset and a border-triangle cannot be given the
   optical weight of the label beside it. It rotates on open, which is the only
   moving thing in the header. */
.nav-group > summary::after {
  content: "\25BE";
  font-size: 0.7em;
  opacity: 0.7;
  transition: transform 120ms ease;
}

.nav-group[open] > summary { color: #fff; background: rgba(255, 255, 255, 0.12); }
.nav-group[open] > summary::after { transform: rotate(180deg); }

/* The area badge. An underscore, not a fill - see the note above. */
.nav-group > summary.is-here {
  color: #fff;
  box-shadow: inset 0 -2px 0 var(--blue-300);
}

.nav-group__menu {
  position: absolute;
  top: calc(100% + var(--space-2));
  left: 0;
  z-index: 30;
  min-width: 12.5rem;

  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: var(--space-2);

  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2);
}

/* ⚠️ The last group sits nearest the window edge, so its panel opens to the
   LEFT. Anchored at left:0 it ran off the side of the window and the items at
   the bottom of it were unreachable - which looks exactly like a panel that
   works, until somebody needs the last item in it. */
.nav-group--end .nav-group__menu { left: auto; right: 0; }

/* ⚠️ A LINK AND A SUBMIT BUTTON, STYLED AS ONE THING.
   Sign out is a form, not a link, and always has been - a GET sign-out can be
   fired by an image tag on any page a person visits. So the account menu holds
   one anchor and one button, and if they are styled separately the two rows
   sit at different heights with different type, which reads as one of them
   being broken. */
.nav-group__menu a,
.nav-group__menu button {
  display: flex;
  align-items: center;
  min-height: 2.125rem;
  padding: var(--space-2) var(--space-3);

  color: var(--ink-2);
  text-decoration: none;
  font-size: 0.875rem;
  font-weight: 600;
  white-space: nowrap;
  border-radius: var(--radius);
}

/* A button is not an anchor and brings a browser's own chrome with it. */
.nav-group__menu button {
  width: 100%;
  border: 0;
  background: none;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

/* The panel is a flex column with a gap; a form wrapping one item would
   otherwise introduce a second box with margins of its own. */
.nav-group__menu form { margin: 0; }

.nav-group__menu a:hover,
.nav-group__menu button:hover { background: var(--navy-100); color: var(--navy-700); }

.nav-group__menu a[aria-current="page"] {
  background: var(--navy-100);
  color: var(--navy-700);
  box-shadow: inset 2px 0 0 var(--navy-500);
}

/* ── The account menu ─────────────────────────────────────────
   The bar carries a NAME. The sign-in address is one tap down, because "which
   account am I in" is a real question and an email address on every screen of
   a shared-desk system is half a credential on display. */
.nav-group__meta {
  margin: 0;
  padding: var(--space-2) var(--space-3) var(--space-3);
  font-size: 0.75rem;
  color: var(--ink-3);
  white-space: nowrap;
}

/* Not a border on the button: sign out is the destructive item and wants a
   beat before it, not a box drawn round it. */
.nav-group__rule {
  height: 0;
  margin: var(--space-1) 0;
  border: 0;
  border-top: 1px solid var(--line-soft);
}

/* ⚠️ A name is somebody else's data and can be any length. Unbounded, one
   long one pushes the whole navigation into a second row - so it truncates,
   and the menu below it shows the address in full anyway.

   min-width: 0 is the load-bearing half: a flex item will not shrink below its
   content without it, and text-overflow then never fires. */
.nav-group--account > summary { max-width: 16rem; }

.nav-group__who {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-main {
  padding-block: var(--space-6) var(--space-7);

  /* Takes the leftover height, which is what pushes the footer down. 1 0 auto
     rather than 1 1 auto: <main> may grow past its content, never shrink below
     it — shrinking is how a long page loses its last rows. */
  flex: 1 0 auto;
}

/* ❗ A CARD CARRIES NO MARGIN, and several pages put their sections straight
   into <main> rather than inside a .stack — so consecutive cards butted
   together with a zero-pixel gap and the boundary between them disappeared.
   The job card, the invoice, the review screen, a stock location and the PPM
   site plan all read as one undivided slab.

   Rather than remembering a wrapper on every page, the separation lives here.
   Same discipline as SouthAfrica.Moment and the input selector above: when a
   correct call needs something somebody has to remember, move it inside.

   .page-header is excluded because the breadcrumb above it must stay tight to
   the title — its own margin-bottom is the gap there, and this rule provides
   the one below it (which is why page-header's margin-bottom is now zero). */
.app-main > * + *:not(.page-header) { margin-top: var(--space-5); }

/* The count belongs to the table above it, not to the page. */
.app-main > .list-count { margin-top: var(--space-3); }

.app-footer {
  border-top: 1px solid var(--line);
  padding-block: var(--space-5) var(--space-6);
  margin-top: var(--space-7);
  color: var(--ink-3);
  font-size: 0.8125rem;
}

/* The footer is the header's opposite number and takes the same edges. A band
   whose text started 400px in, under a brand that starts at the gutter, would
   read as two different left margins on the same piece of chrome. */
.app-footer__inner {
  width: 100%;
  padding-inline: var(--gutter);
}

/* ── The page header ──────────────────────────────────────────────────────
   Every screen opens the same way: where am I, what is this, what can I do.
   It exists as a component so no page hand-rolls it with a margin-left:auto
   and drifts a few pixels from the last one. */

/* ❗ A GRID, NOT A WRAPPING FLEX ROW.
   As flex with margin-left:auto, the action group dropped to its own line the
   moment the title was long and then right-aligned itself — leaving a large
   notch under the title, and on the parts list putting the search field and
   its Search button on two separate lines in the middle of the page.
   Two columns: the text takes what is left, the actions size to content, and
   they stack cleanly rather than wrapping into an L. */
.page-header {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: var(--space-4);

  /* The gap below comes from .app-main's stacking rule above, so that a page
     which puts a breadcrumb, a header and three cards straight into <main>
     gets one consistent rhythm rather than two that add up. */
  margin-bottom: 0;
}

@media (max-width: 48rem) {
  .page-header { grid-template-columns: minmax(0, 1fr); }
}

.page-header__text { min-width: 0; }

/* ⚠️ BASELINE, not centre. A title is almost always followed by a small
   qualifier — a site code, a count, "excluding VAT" — and centring sits that
   text a few pixels low against a much larger heading, so it reads as dropped
   rather than paired. It is small and it is on most detail pages, and
   misalignment is the specific thing that reads as unfinished. */
.page-header__title {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.page-header__sub {
  color: var(--ink-3);
  font-size: 0.875rem;
  margin-top: var(--space-1);
}

.page-header__actions {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  justify-self: end;
  flex-wrap: wrap;
}

@media (max-width: 48rem) {
  .page-header__actions { justify-self: start; }
}

/* A boundary inside the action group: reading a client and creating a
   franchise are not the same kind of act, and six buttons at one weight said
   they were. */
.page-header__actions .action-divider {
  width: 1px;
  align-self: stretch;
  background: var(--line);
  margin-inline: var(--space-1);
}

/* ── Toolbar ──────────────────────────────────────────────────────────────
   Controls that act on the LIST, not on the page. Search lived in the page
   header beside "New site", which made a filter and a create look like a pair
   and left the pair to wrap. It belongs above the thing it filters. */

.toolbar {
  display: flex;
  align-items: flex-end;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.toolbar form { display: flex; gap: var(--space-2); align-items: flex-end; }
.toolbar__end { margin-left: auto; }

/* ❗ A FIELD'S BOTTOM MARGIN IS FOR THE FIELD UNDER IT, AND IN A ROW THERE IS
   NONE. .field carries margin-bottom so stacked fields in a form breathe. Flex
   aligns MARGIN boxes, so in a toolbar that margin sat below the input as part
   of the item — and flex-end then lined the bottom of the MARGIN up with the
   bottom of the button beside it. The button rendered a full --space-4 lower
   than the input it belongs to, on the Search of every admin list.

   flex-end stays: it is what keeps the controls level when one of them carries
   a visible label above it and the next one does not. The margin is the bug. */
.toolbar .field { margin-bottom: 0; }

/* A trail rather than a full breadcrumb bar: three levels deep is the whole
   system, so anything more elaborate would be scaffolding for depth that
   does not exist. */
.breadcrumb {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.8125rem;
  color: var(--ink-3);
  margin-bottom: var(--space-2);
  flex-wrap: wrap;
}

.breadcrumb a { color: var(--ink-3); text-decoration: none; font-weight: 600; }
.breadcrumb a:hover { color: var(--navy-600); text-decoration: underline; }
.breadcrumb__sep { color: var(--ink-4); }

/* ── Surfaces ─────────────────────────────────────────────────────────── */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  box-shadow: var(--shadow-1);
}

.card--flush { padding: 0; overflow: hidden; }

/* ⚠️ The region an irreversible action lives in, and nothing else goes here.
   A void, a scrap, a closure. The border is the separation the red button used
   to have to carry on its own while sitting inline beside a text field in a
   card it shared with an ordinary correction. */
.card--danger { border-color: #e6c3c0; }
.card--danger > .card__header { border-bottom-color: #f0d7d4; background: var(--critical-soft); }
.card--danger > .card__header .card__title { color: var(--critical); }

/* Baseline for the same reason as .page-header__title — "On the shelf 2" and
   "Mkhize Holdings CC R64 837.50" both sat a few pixels out of true. */
.card__header {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);

  /* The band, matching the table header — so a card and a table read as the
     same object at a glance, which they are: a heading over rows. A hairline
     on its own left the header indistinguishable from the first row of the
     body, and on .card--flush that is literally what it was. */
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
  flex-wrap: wrap;
}

/* .card--flush clips its children, but a padded card does not — without this
   the tinted band squares off the two top corners it is supposed to follow. */
.card--flush > .card__header:first-child {
  border-top-left-radius: calc(var(--radius-lg) - 1px);
  border-top-right-radius: calc(var(--radius-lg) - 1px);
}

/* A control is not text: it centres against the title rather than sitting its
   own text on the title's baseline, which would lift the whole button. */
.card__header > :last-child.card__header-end { margin-left: auto; align-self: center; }
.card__header .button { align-self: center; }
.card__title { font-size: 1rem; font-weight: 650; letter-spacing: -0.01em; }
.card__body { padding: var(--space-4) var(--space-5); }

/* A callout, from palette.html. Used where a screen has something to say
   about the data rather than about the form. */
.note {
  background: var(--navy-100);
  border: 1px solid #cfdeee;
  border-left: 3px solid var(--navy-500);
  border-radius: var(--radius-lg);
  padding: var(--space-3) var(--space-4);
  font-size: 0.875rem;
  color: var(--ink-2);
}

.note--warning {
  background: var(--warning-soft);
  border-color: #ecd9b0;
  border-left-color: var(--warning);
}

/* The one place a note says something has gone right. Used where the absence
   of a step is the good news — a job inside standing authority needs no
   approval at all — so it must not look like the warnings around it. */
.note--good {
  background: var(--good-soft);
  border-color: #c3e0cb;
  border-left-color: var(--good);
}

.note b { color: var(--navy-700); }

/* A photograph of a fault, on a screen a coordinator reads on a laptop. Capped
   rather than full width: the useful thing is what is wrong, not how large the
   camera's sensor is, and a 2400px-wide image pushes everything else off the
   fold. */
.job-photo {
  display: block;
  max-width: min(100%, 480px);
  height: auto;
  border-radius: var(--radius-lg);
  border: 1px solid var(--line);
}

/* ── About this screen ────────────────────────────────────────────────────
   Where a screen explains a DECISION rather than telling somebody what to do.

   ⚠️ The explanations are one of the better things about this system and none
   of them should be deleted — the point is placement. At body size, at the foot
   of a working screen, a paragraph about not becoming an ERP reads as an
   instruction that never resolves into an action, and the reader stops trusting
   that the sentences on a screen are for them.

   Closed by default. What stays inline is anything that changes what somebody
   should DO — "a credit note reduces what is owed and is not a payment" earns
   its place; the scope rationale does not. */

.aside {
  border-top: 1px solid var(--line-soft);
  padding-top: var(--space-3);
}

.aside > summary {
  font-size: 0.8125rem;
  font-weight: 650;
  color: var(--navy-600);
  cursor: pointer;
  /* The 44px target is for the technician's phone, and this is a desk control —
     but it is a disclosure somebody clicks, so it gets more than its text. */
  padding-block: var(--space-1);
}

.aside > summary:hover { color: var(--navy-700); }
.aside[open] > summary { margin-bottom: var(--space-3); }
.aside .prose { font-size: 0.875rem; }
.aside .prose + .prose { margin-top: var(--space-3); }

/* ── Empty states ─────────────────────────────────────────────────────────
   "Nothing here" is a screen a user meets on their first day, so it says
   what to do rather than only what is absent. */

.empty {
  text-align: center;
  padding: var(--space-6) var(--space-5);
  color: var(--ink-3);
}

.empty__title { color: var(--ink-2); font-weight: 650; margin-bottom: var(--space-1); }
.empty__hint { font-size: 0.875rem; margin-bottom: var(--space-4); }

/* ── Chips ────────────────────────────────────────────────────────────────
   STATUS is a soft chip. PRIORITY is solid or outlined. They must never look
   like the same thing (palette.html, CLAUDE.md).

   The dot is the second non-colour signal, and the word is the first. Roughly
   8% of men cannot separate red from green, and the technicians are almost
   all men. */

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 4px 10px;
  border-radius: 20px;
  font-size: 0.71875rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  white-space: nowrap;
  line-height: 1.4;
}

.chip__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}

.chip--status            { background: var(--neutral-soft); color: var(--ink-3); }
.chip--status.is-info    { background: var(--info-soft);    color: var(--info); }
.chip--status.is-good    { background: var(--good-soft);    color: var(--good); }
.chip--status.is-warning { background: var(--warning-soft); color: var(--warning); }
.chip--status.is-serious { background: var(--serious-soft); color: var(--serious); }
.chip--status.is-critical{ background: var(--critical-soft);color: var(--critical); }
.chip--status.is-progress{ background: var(--navy-700);     color: #fff; }
.chip--status.is-invoiced{ background: var(--navy-100);     color: var(--navy-700); }
/* ⚠️ --ink-3, NOT --ink-4. Closed is the quietest status in the set and it is
   still a status: it was 2.3:1 on this background, which is not "quiet", it is
   unreadable. Quiet comes from the grey hue, never from the contrast
   (palette.html rule 5). */
.chip--status.is-closed  { background: var(--neutral-soft); color: var(--ink-3); }

/* Routine is outlined; urgent and emergency are solid. An inset shadow rather
   than a border, so an outlined chip is exactly the same size as a solid one
   and a column of them does not jitter. */
.chip--priority {
  background: transparent;
  color: var(--ink-3);
  box-shadow: inset 0 0 0 1.5px var(--line);
  text-transform: uppercase;
}

.chip--priority.is-ppm       { color: var(--info); box-shadow: inset 0 0 0 1.5px var(--navy-200); }
.chip--priority.is-urgent    { background: var(--serious);  color: #fff; box-shadow: none; }
.chip--priority.is-emergency { background: var(--critical); color: #fff; box-shadow: none; }

/* ── Controls ─────────────────────────────────────────────────────────── */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--touch-target);
  padding: 0 var(--space-4);
  border: 1px solid var(--navy-600);
  border-radius: var(--radius);
  background: var(--navy-600);
  color: #fff;
  font: inherit;
  font-size: 0.875rem;
  font-weight: 650;
  line-height: 1;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color 120ms ease, border-color 120ms ease;
}

.button:hover { background: var(--navy-700); border-color: var(--navy-700); color: #fff; }
.button:active { background: var(--navy-900); }

.button--secondary {
  background: var(--surface);
  color: var(--navy-700);
  border-color: var(--line);
}

.button--secondary:hover {
  background: var(--navy-100);
  border-color: var(--navy-200);
  color: var(--navy-700);
}

/* On the navy header, where a white-bordered button would glare. */
.button--onDark {
  background: rgba(255, 255, 255, 0.1);
  border-color: rgba(255, 255, 255, 0.25);
  color: #fff;
}

.button--onDark:hover { background: rgba(255, 255, 255, 0.18); border-color: rgba(255, 255, 255, 0.4); }

/* ⚠️ Below the 44px touch target ON PURPOSE, and only for a row action on a
   dense desktop table, where a full-height button beside 13px text is the
   thing that makes an admin screen look clumsy. WCAG 2.2 AA asks for 24px;
   this is 36. Never use it on anything a technician taps. */
.button--sm {
  min-height: 36px;
  padding: 0 var(--space-3);
  font-size: 0.8125rem;
}

/* Large — the one action on a technician's screen, thumb-sized. */
.button--lg {
  min-height: 52px;
  padding: 0 var(--space-5);
  font-size: 1rem;
}

/* A third weight, below secondary: an action that is available but not being
   recommended. Carries no border until you reach for it. */
.button--quiet {
  background: transparent;
  border-color: transparent;
  color: var(--navy-600);
}

.button--quiet:hover { background: var(--navy-100); border-color: transparent; color: var(--navy-700); }

/* ⚠️ Destructive, and deliberately NOT the default styling for anything.
   Voiding a credit note or closing a site should take a moment's thought, and
   a red button is the last signal before it — never the first one you meet. */
.button--danger {
  background: var(--critical);
  border-color: var(--critical);
  color: #fff;
}

.button--danger:hover { background: #96201a; border-color: #96201a; color: #fff; }

.button[disabled],
.button[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: none;
}

.button--block { width: 100%; }

/* A count riding on a button — "Approve (3)". Sits in the button's own
   colour at reduced opacity so it never reads as a separate control. */
.button__count {
  font-variant-numeric: tabular-nums;
  opacity: 0.72;
  font-weight: 600;
}

/* Buttons that act as one control. The join is a shared border, so it reads
   as a segmented control rather than three buttons that happen to touch. */
.button-group { display: inline-flex; }
.button-group .button { border-radius: 0; margin-left: -1px; }
.button-group .button:first-child { border-radius: var(--radius) 0 0 var(--radius); margin-left: 0; }
.button-group .button:last-child { border-radius: 0 var(--radius) var(--radius) 0; }
.button-group .button:focus-visible { position: relative; z-index: 1; }

/* ── Forms ────────────────────────────────────────────────────────────── */

.field { margin-bottom: var(--space-4); }

.field label,
label {
  display: block;
  font-size: 0.8125rem;
  font-weight: 650;
  color: var(--ink-2);
  margin-bottom: var(--space-1);
}

/* ❗ MATCHED BY EXCLUSION, NOT BY LISTING THE TYPES.
   An <input> written without a type attribute is still a text field, and
   input[type="text"] does NOT match it — so a list of types leaves those
   fields with the browser's default box: no 44px target, no 16px font (so
   iOS zooms the page on focus), no width, no border. Twenty-nine of them
   were rendering that way, including four required "why" fields on money
   screens and the skip reason on a technician's phone.

   Listing the types also loses every type nobody thought of: date, search,
   datetime-local and url were all missing. So the rule is inverted — style
   everything except the controls that are genuinely not text boxes. A new
   input type is then styled by default rather than forgotten.

   Same discipline as SouthAfrica.Moment: when a correct call needs something
   somebody has to remember, move it inside. */
input:not([type="checkbox"], [type="radio"], [type="range"], [type="color"],
           [type="file"], [type="submit"], [type="button"], [type="reset"],
           [type="image"], [type="hidden"]),
select,
textarea {
  width: 100%;
  min-height: var(--touch-target);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--line);
  border-radius: var(--radius);

  /* 16px, not the 15px body size. Below 16px iOS zooms the page on focus,
     which on the technician's phone is a jarring lurch mid-form. */
  font-family: inherit;
  font-size: 16px;
  color: var(--ink);
  background: var(--surface);
}

textarea { min-height: 5rem; line-height: 1.5; resize: vertical; padding-block: var(--space-3); }

input:hover, select:hover, textarea:hover { border-color: var(--ink-4); }

input:focus, select:focus, textarea:focus {
  border-color: var(--navy-500);
  outline: 2px solid var(--navy-500);
  outline-offset: -1px;
}

/* ⚠️ --ink-3. A placeholder is read — several of these carry a worked example
   (JC-04821, VAN-07, "Roof hatch locked, nobody had the key") and one of them
   is read on a phone in a plant room. --ink-4 is 2.7:1 and carries no text
   (palette.html rule 5). */
input::placeholder, textarea::placeholder { color: var(--ink-3); }

/* A field is only as wide as its content deserves. A postal code in a box
   the width of the form is the detail that reads as unfinished.

   ❗ AND EVERY FIELD SAYS WHICH IT IS, INCLUDING THE FULL-WIDTH ONES.

   --full is a no-op in CSS and the point of it is that it exists: without it,
   a .field carrying no width class meant both "the whole column is right
   here" and "nobody chose", and nothing could tell the two apart. The result
   was rows like the stock location's — a 320px select, two 110px numbers and
   a 175px note beside each other — and a PPM screen giving "Why", which takes
   a sentence, the narrowest box on the page.

   An architecture test refuses a .field with no width now, so the choice has
   to be made rather than defaulted into. */
.field--xs input, .field--xs select { max-width: 7rem; }
.field--sm input, .field--sm select { max-width: 12rem; }
.field--md input, .field--md select { max-width: 20rem; }
.field--full input, .field--full select, .field--full textarea { max-width: none; }

.field__hint {
  display: block;
  font-size: 0.8125rem;
  color: var(--ink-3);
  margin-top: var(--space-1);
  font-weight: 400;
}

/* A hint that is a gap rather than guidance — the setting is on and something
   it depends on is missing. Not .field__error: nothing the person typed is
   wrong, and colouring it as an error would imply the form will refuse to save.
   The wording carries the meaning; the colour only weights it. */
.field__hint--warn {
  color: var(--warning);
  font-weight: 600;
}

.field__error {
  display: block;
  color: var(--critical);
  font-size: 0.8125rem;
  font-weight: 600;
  margin-top: var(--space-1);
}

.field__error:empty { display: none; }

/* A tick box is not a text field: it must not stretch to the width of the
   form, and its label sits beside it rather than above. */
.field--check { display: flex; align-items: flex-start; gap: var(--space-3); }

.field--check input[type="checkbox"] {
  width: 1.125rem;
  height: 1.125rem;
  min-height: 0;
  margin: 0;
  flex: 0 0 auto;
  accent-color: var(--navy-600);
  align-self: center;
}

.field--check label { margin: 0; font-size: 0.9375rem; color: var(--ink); }

input[type="checkbox"] { width: auto; min-height: 0; }

/* Grouping related fields is an accessibility affordance, not decoration — a
   screen reader announces the legend with every field inside it.

   It is also what separates two operations that share a card. Where a screen
   offers several — book in, send, correct — the rule and the legend are the
   boundary; without them the first field's label has to do that job, and
   "Book in" reads as the name of a part picker. */
fieldset {
  border: 0;
  border-top: 1px solid var(--line-soft);
  padding: var(--space-4) 0 0;
  margin: 0 0 var(--space-5);
  min-width: 0;
}

legend {
  font-size: 0.75rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--ink-3);
  padding: 0;
  margin-bottom: var(--space-4);
}

/* The card header is already a rule. A second one directly under it reads as a
   mistake rather than as a boundary. */
.card__body > form:first-child > fieldset:first-child,
.card__body > fieldset:first-child { border-top: 0; padding-top: 0; }

/* The last group in a card does not need to push the card open. */
.card__body > form:last-child > fieldset:last-child,
.card__body > fieldset:last-child { margin-bottom: 0; }

/* Two fields on one line where they belong together — suburb and city, city
   and postal code. Collapses to one column on a phone. */
.field-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 0 var(--space-4);
}

.form-actions {
  display: flex;
  gap: var(--space-3);
  align-items: center;
  padding-top: var(--space-4);
  border-top: 1px solid var(--line-soft);
  flex-wrap: wrap;
}

.search-input { max-width: 16rem; }

/* An explanatory line at the top of a fieldset, before its first field. */
.fieldset-intro { margin-bottom: var(--space-4); }

/* A label over a group of checkboxes. Not a <legend>, because a fieldset
   inside a fieldset reads badly to a screen reader and nests visually for no
   reason — this is one field whose control happens to be several boxes. */
.field__legend {
  display: block;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: var(--space-2);
}

.form-narrow { max-width: 34rem; }
.form-centred { max-width: 25rem; margin-inline: auto; }

/* Sign-in is the first screen anyone meets, and for a while it was also the
   only one. It gets a little air above it rather than being pinned to the
   header. */
.signin-mark { text-align: center; padding-top: var(--space-6); }
.signin-submit { width: 100%; }

/* ── Tables ────────────────────────────────────────────────────────────────
   Treatment from palette.html: quiet uppercase micro-headers on a tinted
   band, hairline row rules, no zebra. Dense enough for a hundred rows. */

.table-wrap {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-1);
}

.table-scroll { overflow-x: auto; }

/* ❗ WIDE CONTENT SCROLLS INSIDE ITS OWN BOX, NEVER ON THE BODY.
   The job list's eleven status filters are a 980px segmented control with
   nothing allowed to wrap. Below about 900px the DOCUMENT went that wide
   instead: the whole page scrolled sideways, header and footer with it, and
   seven of the eleven filters were off the edge of a phone.

   Wrapping is not the answer for this one — a segmented control has square
   inner corners and rounded outer ones, so a wrapped row gets its radii in
   the wrong places. It scrolls, and the segments stay a control.

   The padding is for the focus ring: an overflow container clips it, and a
   coordinator on the keyboard lives on that ring. */
.scroll-x {
  overflow-x: auto;
  overflow-y: hidden;
  padding-block: var(--space-1);
  margin-block: calc(var(--space-1) * -1);
  scrollbar-width: thin;
  overscroll-behavior-inline: contain;
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875rem;
}

.table th {
  text-align: left;
  font-size: 0.65625rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-weight: 700;
  padding: var(--space-3) var(--space-4);

  /* ⚠️ THE BAND IS A FILL AND TAKES --surface-2, NOT --line-soft.
     It borrowed the hairline colour for years, which is why the row rule and
     the header band could never be adjusted independently — and why the
     header ended up the same value as the page it sat two planes above. */
  background: var(--surface-2);
  white-space: nowrap;

  /* The header stays put while a long list scrolls. */
  position: sticky;
  top: 0;
  z-index: 1;
}

.table th a {
  color: inherit;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.table th a:hover { color: var(--navy-600); }
.table th[aria-sort="ascending"], .table th[aria-sort="descending"] { color: var(--navy-600); }

.table td {
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--line-soft);
  color: var(--ink-2);
  vertical-align: middle;
}

.table tbody tr:hover td { background: var(--navy-100); }

/* The column that identifies the row carries the weight; everything else
   steps back. Without this every cell shouts equally and the eye has nowhere
   to land. */
.table td.is-primary { color: var(--ink); font-weight: 600; }
.table td.is-code { font-family: var(--mono); font-size: 0.8125rem; color: var(--ink-3); white-space: nowrap; }
.table td.num, .table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.table td.is-actions { text-align: right; white-space: nowrap; }

.table td.is-actions a {
  font-size: 0.8125rem;
  font-weight: 650;
  text-decoration: none;
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius);
}

.table td.is-actions a:hover { background: var(--navy-100); text-decoration: none; }

/* ── Data rows ────────────────────────────────────────────────────────────
   palette.html's job row, generalised: a fixed monospace reference column, a
   name that carries the weight, and one quiet line underneath. It is how a
   site reads on the structure screen and how a job will read on the board. */

/* ❗ THE LIST CARRIES ITS OWN SURFACE.
   Fifteen screens put a .data-list straight into <main> and eight wrapped it in
   a .card first, so half the lists in the system were a bounded white object
   and half were rows floating on the page grey with no left or right edge at
   all - nothing saying "this is one table" rather than "these are some
   paragraphs". The jobs list was one of the bare ones, and deepening --page
   made it obvious rather than making it wrong; it had always been wrong.

   ⚠️ Not fixed by wrapping fifteen views. Same discipline as .app-main's
   stacking rule and SouthAfrica.Moment: when being correct depends on every
   call site remembering a wrapper, move the wrapper inside. A list is a
   surface, and one already inside a card gives its surface back below. */
.data-list {
  list-style: none;
  margin: 0;
  padding: 0;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
  overflow: hidden;
}

/* Already on a surface. The card is the object; the list is its contents. */
.card .data-list,
.table-wrap .data-list {
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

/* ❗ BASELINE, NOT CENTRE.
   The row is a reference, a two-line block and some chips. Centred, the ref and
   the chips sat against the middle of the two-line block - which is BETWEEN the
   title and the line under it, aligned to neither, and it reads as though every
   row is a few pixels out of true. It is worse where a title wraps, because the
   block grows and the ref drifts further down while the title stays put.

   A flex baseline resolves against the first line of the two-line block, which
   is the title - so the reference now sits on the title's own baseline, which
   is the thing it names. Same fix as .page-header__title and .card__header. */
.data-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--line-soft);
  text-decoration: none;
  color: inherit;
}

/* ⚠️ A picture has no useful baseline - it would hang its BOTTOM edge on the
   title's baseline and push the row open. The asset list puts a 56px thumbnail
   in the reference column, so that column centres itself instead. */
.data-row__ref:has(.thumb) { align-self: center; }

.data-list > li:first-child .data-row { border-top: 0; }

/* ── The column header ───────────────────────────────────────────
   Opt-in, for a list long enough that somebody has to be told what the columns
   are. Deliberately identical to .table th - the band, the micro-caps, the
   colour - because a list of rows and a table of rows should not be two
   different-looking objects when they are doing the same job.

   ⚠️ It is a <li>, because a <ul> may not contain a <div>. */
.data-list__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-5);
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
  font-size: 0.65625rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-weight: 700;
}

/* The header's own bottom rule is the separator; the first row must not add a
   second one under it. */
.data-list__head + li .data-row { border-top: 0; }
a.data-row:hover { background: var(--navy-100); }

.data-row__ref {
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--ink-3);
  width: 7rem;
  flex: 0 0 auto;
}

.data-row__main { flex: 1; min-width: 0; }

.data-row__name {
  font-size: 0.90625rem;
  font-weight: 650;
  color: var(--ink);
  letter-spacing: -0.01em;
}

.data-row__sub {
  display: block;
  font-weight: 400;
  color: var(--ink-3);
  font-size: 0.78125rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.data-row__end { margin-left: auto; flex: 0 0 auto; display: flex; gap: var(--space-2); align-items: baseline; }

/* ❗ TWO FIXED COLUMNS, BECAUSE A RIGHT-ALIGNED PAIR IS A RAGGED PAIR.
   The priority chip and the status chip were laid out as a group pushed to the
   right-hand margin, so where each one STARTED depended on how wide the chip
   beside it happened to be. Down five rows the priority chips began at five
   different x positions - the single loudest reason the list looked wrong. The
   right-hand edge was the only straight line on the screen, and it is the edge
   nobody reads down.

   ⚠️ The priority chip is absent on a Routine job, which is why the status
   chip names its column explicitly rather than trusting auto-placement to put
   it second. Without that, an ordinary job's status slides into the priority
   column and the raggedness comes back on exactly the rows that had no chip to
   blame. */
.data-row__end--columns {
  display: grid;
  grid-template-columns: 6rem minmax(0, 12rem);
  gap: var(--space-2);
  justify-items: start;
}

.data-row__end--columns .chip--status { grid-column: 2; }

/* ── Detail grid ──────────────────────────────────────────────────────────
   Label-over-value pairs for a record you read rather than edit. Two or three
   columns depending on width, and the value carries the weight because that is
   what the eye is looking for. Money sits in tabular figures so a column of
   amounts lines up on the decimal point.

   ❗ THE PAIR IS THE GRID CELL — <div><dt/><dd/></div>, which HTML allows
   inside a <dl> precisely for this. Without the wrapper the dt and the dd are
   two consecutive cells, so the "label over value" this file describes never
   happened: the label sat in one column and its value in the NEXT one, and on
   the job card "Indicative" ended up beside "Triage estimate" rather than
   above its own figure.

   ⚠️ Four of the seven pages that use this already wrapped their pairs and
   three did not, which is the more useful fact: the correct construct was
   known and there was nothing to make it happen.

   ⚠️ This class is for a RECORD. A total and its name is .figure-row below;
   a term and a sentence is .term-row. They were all one class, and the same
   auto-fit collapse that broke the pairs here put a money total nine hundred
   pixels from the word naming it. */

.detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: var(--space-4) var(--space-6);
  margin: 0;
}

.detail-grid > div { min-width: 0; }

/* ── A figure and what it is ──────────────────────────────────────────────
   ❗ The pair stays together AT EVERY WIDTH, which is the entire point.

   These rows were .detail-grid, whose auto-fit collapses to two full-width
   tracks when there are two children — so on the exec pack five management
   figures sat about 900px from the words naming them with nothing between,
   and below about 460px the pair stacked. "Total" then appeared above
   R21 562.50, which appeared above "Received", and every amount read as
   belonging to the row beneath it. On an invoice.

   Capped rather than full width: a total a person cannot pair with its label
   in one glance is a total they will check somewhere else. */

.figure-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-4);
  max-width: 30rem;
  padding-block: var(--space-1);
}

.figure-row > :first-child { color: var(--ink-2); min-width: 0; }

.figure-row > :last-child {
  margin-left: auto;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* A figure never breaks across a line. "R18" above "750.00" on a phone is two
   numbers to anybody scanning, and the label is free to wrap instead. Applied
   to .num rather than to the last child, because one of these values is prose
   — the reporting pack says "Not available — see …" where a figure would be. */
.figure-row > .num { white-space: nowrap; }

/* A sum of the rows above it, and it says so with a rule rather than only
   with weight. Used where the figure genuinely IS a total — not merely where
   one figure matters more than its neighbours. */
.figure-row--total {
  border-top: 1px solid var(--line);
  margin-top: var(--space-2);
  padding-top: var(--space-3);
}

/* ── A term and a sentence about it ───────────────────────────────────────
   Not a figure: the value is prose, so it takes the room and the term stays
   narrow. Stacks below a phone width, where two columns of this leave the
   sentence four words wide. */

.term-row {
  display: grid;
  grid-template-columns: minmax(7rem, 11rem) minmax(0, 1fr);
  gap: var(--space-2) var(--space-5);
  align-items: baseline;
}

.term-row > :first-child {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.term-row > :last-child { color: var(--ink-2); }

@media (max-width: 34rem) {
  .term-row { grid-template-columns: minmax(0, 1fr); gap: var(--space-1); }
}

.detail-grid dt {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: var(--space-1);
}

.detail-grid dd {
  margin: 0;
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* The consequence of the number, beside the number. A ceiling of R0 means
   something, and the figure alone does not say what. */
.detail-grid__note {
  display: block;
  font-size: 0.78125rem;
  font-weight: 400;
  color: var(--ink-3);
  margin-top: var(--space-1);
}

@media (max-width: 40rem) {
  .data-row { flex-wrap: wrap; gap: var(--space-2); padding-inline: var(--space-4); }
  .data-row__ref { width: auto; }
  .data-row__main { flex-basis: 100%; }

  /* ❗ The action group has to come down too. It did not, and .card--flush sets
     overflow: hidden — so the dispatch board's "Send somebody" button was cut
     in half by the card edge and could not be reached at all, because there
     was nothing to scroll to. Chips and the action now sit on their own line
     under the fault description. */
  .data-row__end { margin-left: 0; flex-basis: 100%; flex-wrap: wrap; }

  /* Fixed columns are for a wide screen. On a phone the row has already
     stacked, and an 18rem grid inside a 22rem viewport would push the status
     chip off the side. */
  .data-row__end--columns { display: flex; }

  /* The header names columns that no longer exist once the row has stacked. */
  .data-list__head { display: none; }
}

/* ── The client structure tree ─────────────────────────────────────────────
   design/07 §1. Three levels, never more, so this needs no recursion and no
   indentation machinery — an area is a card and its sites are the rows in it.
   That reads as a hierarchy without a single guide line. */

.tree { display: flex; flex-direction: column; gap: var(--space-4); }

.tree__group { }

.tree__count {
  font-size: 0.8125rem;
  color: var(--ink-3);
  font-weight: 600;
}

/* The count under a list. Says how much you are looking at, which matters
   once a client has forty sites and the table is capped. */
.list-count { margin-top: var(--space-3); font-size: 0.8125rem; color: var(--ink-3); }

/* ── The design lab ───────────────────────────────────────────────────────
   /design. A panel of live token controls beside every component the system
   has. Not shipped to a client's screen — staff only, and everything on it is
   invented data. */

.lab {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-5);
}

@media (min-width: 64rem) {
  .lab { grid-template-columns: 17rem minmax(0, 1fr); align-items: start; }
  .lab__panel { position: sticky; top: calc(56px + var(--space-4)); }
}

.lab__control label,
.lab__swatch label {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font-size: 0.8125rem;
}

.lab__control output {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--ink-3);
  font-weight: 400;
}

.lab__control input[type="range"] { width: 100%; min-height: 0; accent-color: var(--navy-600); }

.lab__swatch { display: flex; align-items: center; gap: var(--space-3); }
.lab__swatch label { flex: 1; margin: 0; }

.lab__swatch input[type="color"] {
  width: 2.5rem;
  height: 2rem;
  min-height: 0;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  flex: 0 0 auto;
}

.lab__output {
  font-family: var(--mono);
  font-size: 0.6875rem;
  line-height: 1.6;
  background: var(--ink);
  color: #E8F0F8;
  padding: var(--space-3);
  border-radius: var(--radius);
  overflow-x: auto;
  margin: 0 0 var(--space-3);
  white-space: pre;
}

/* A phone-width frame, so a control meant for the technician is judged at the
   size he will actually meet it rather than across a desktop card. */
.lab__phone {
  max-width: 20rem;
  padding: var(--space-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--page);
}

/* ── Utilities ────────────────────────────────────────────────────────── */

.stack { display: flex; flex-direction: column; gap: var(--space-4); }
.stack--tight { gap: var(--space-2); }
.stack--loose { gap: var(--space-6); }

.row { display: flex; gap: var(--space-3); align-items: center; flex-wrap: wrap; }
.row--end { margin-left: auto; }

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

code {
  font-family: var(--mono);
  font-size: 0.78125rem;
  background: var(--surface-2);
  padding: 1px 5px;
  border-radius: 4px;
  color: var(--ink-2);
}

/* HTMX sets this while a request is in flight. */
.htmx-request { opacity: 0.6; }

/* ── Print ────────────────────────────────────────────────────────────────
   The base sheet PDFs inherit. Chrome renders them headless, so what the
   screen hides here is what the job card and the invoice will not show. */

@media print {
  body { background: #fff; }
  .app-header, .app-footer, .page-header__actions, .form-actions { display: none; }
  .card, .table-wrap { box-shadow: none; border-color: #ccc; break-inside: avoid; }
  .app-main { padding: 0; }
}

/* ── Readiness (design/07 §5) ─────────────────────────────────────────────
   A configuration gap is not an error, so it must not look like one: the
   panel is a list of cards a coordinator chooses to open, in the same
   vocabulary as every other list in the admin shell. */

.note--serious {
  background: var(--serious-soft);
  border-color: #eccdc7;
  border-left-color: var(--serious);
}

/* The consequence, never the headline on its own. A warning whose cost is
   unstated reads as pedantry and gets dismissed. */
.readiness__why { font-size: 0.875rem; color: var(--ink-3); max-width: 68ch; }

.readiness .data-row__end { color: var(--navy-600); font-weight: 600; font-size: 0.8125rem; }

/* ── Places, the one tree that nests (design/03 §2.1) ────────────────────
   Depth is a class, never an inline style: the CSP is style-src 'self' with
   no 'unsafe-inline', so a style attribute is silently discarded and the
   tree renders flat. Six steps, matching LocationTree.MaxDepth. */

.place--d0 { padding-left: 0; }
.place--d1 { padding-left: var(--space-5); }
.place--d2 { padding-left: calc(var(--space-5) * 2); }
.place--d3 { padding-left: calc(var(--space-5) * 3); }
.place--d4 { padding-left: calc(var(--space-5) * 4); }
.place--d5 { padding-left: calc(var(--space-5) * 5); }

/* The guide line is what makes depth readable at a glance; the indent alone
   reads as accidental ragging once there are more than a few rows. */
.place--d1, .place--d2, .place--d3, .place--d4, .place--d5 {
  border-left: 2px solid var(--navy-100);
}

/* ── Asset photographs ────────────────────────────────────────────────────
   A fixed box with cover cropping, so a portrait phone photo and a landscape
   one sit on the same line without the rows jumping about. */

.thumb {
  width: 40px;
  height: 40px;
  border-radius: var(--radius);
  object-fit: cover;
  background: var(--neutral-soft);
  display: block;
}

.data-row__ref .thumb { width: 56px; height: 56px; }

/* ── Setting a client up ──────────────────────────────────────────────────
   A numbered list that reads as a sequence, on a screen whose whole argument
   is that there IS one. Everything here is a class: an inline style attribute
   is silently discarded under style-src 'self', so the bar is a native
   <progress> rather than a div somebody widths at render time. */

.setup-progress {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.setup-progress__bar {
  flex: 1;
  min-width: 0;
  height: 8px;
  appearance: none;
  border: 0;
  border-radius: 999px;
  background: var(--neutral-soft);
  overflow: hidden;
}

/* Both pseudo-elements, because Chrome and Firefox disagree about which one
   paints the track and which the fill, and a bar that is invisible in one of
   them is worse than no bar. */
.setup-progress__bar::-webkit-progress-bar { background: var(--neutral-soft); border-radius: 999px; }
.setup-progress__bar::-webkit-progress-value { background: var(--navy-600); border-radius: 999px; }
.setup-progress__bar::-moz-progress-bar { background: var(--navy-600); border-radius: 999px; }

.setup-progress__count {
  font-size: 0.8125rem;
  color: var(--ink-3);
  white-space: nowrap;
}

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.step {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--space-4);

  /* Baseline, not centre: centred, the number sits against the middle of a
     four-line block and is level with nothing. The same fault the data rows
     had. */
  align-items: start;

  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  box-shadow: var(--shadow-1);
}

/* The one step to do now, and it should be findable without reading. */
.step--next {
  border-color: var(--navy-500);
  box-shadow: var(--shadow-2);
}

/* Nothing is hidden or disabled — a later step is still a link that works.
   It is simply quieter, so the eye lands on the one that is next. */
.step--waiting,
.step--optional { background: var(--surface-2); }

.step__marker {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: 999px;
  background: var(--neutral-soft);
  color: var(--ink-3);
  font-weight: 700;
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
}

.step--done .step__marker { background: var(--good-soft); color: var(--good); }
.step--next .step__marker { background: var(--navy-600); color: #fff; }

.step__body { min-width: 0; }

.step__head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-1);
}

.step__title { font-size: 1rem; font-weight: 650; margin: 0; }

.step__why {
  font-size: 0.875rem;
  color: var(--ink-3);
  max-width: 68ch;
  margin: 0;
}

.step__detail {
  font-size: 0.8125rem;
  color: var(--ink-2);
  font-weight: 600;
  margin: var(--space-2) 0 0;
}

.step__problems {
  margin: var(--space-3) 0 0;
  padding: var(--space-3);
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  background: var(--warning-soft);
  border-radius: var(--radius);
  font-size: 0.8125rem;
  color: var(--ink-2);
}

.step__subjects {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-1);
}

.step__action { display: grid; gap: var(--space-2); justify-items: end; }

/* Not a greyed-out control: a sentence naming who to ask. A button that
   refuses and says nothing wastes an afternoon. */
.step__blocked {
  font-size: 0.8125rem;
  color: var(--ink-3);
  margin: 0;
  max-width: 18ch;
  text-align: right;
}

/* Below a phone's width the three columns stop being three: the marker keeps
   its place beside the title, and the action drops under the text rather than
   squeezing the sentence into a column two words wide. */
@media (max-width: 40rem) {
  .step { grid-template-columns: auto minmax(0, 1fr); }
  .step__action { grid-column: 1 / -1; justify-items: stretch; }
  .step__blocked { max-width: none; text-align: left; }
}

/* ── The honeypot ─────────────────────────────────────────────────────────
   _BotTrap.cshtml's wrapper, on the two anonymous forms. It lives in app.css
   rather than auth.css because _LayoutAuth loads both and the client's
   magic-link page loads only this one.

   ❗ OFF THE SCREEN, NOT display:none. A bot written any time this century
   skips a hidden field; one positioned off the canvas is still a field it
   parses and fills, which is the entire signal. The partial adds tabindex="-1"
   and aria-hidden so nobody can reach it by tabbing or hear it read out.

   ⚠️ left, never right. Overflow past the LEADING edge is clipped; past the
   trailing edge it lengthens the document and the whole page scrolls
   sideways, which is the failure this system already knows by heart. */
.bot-trap {
  position: absolute;
  left: -9999px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
