/* ============================================================================
   RCS Care — portal design system
   ----------------------------------------------------------------------------
   Subject: pest control for food manufacturing and corporate estates. The
   buyer is a QA head or facility manager who has to survive an FSSAI / AIB /
   BRC audit. So the product is not "spraying" — it is the evidence trail:
   which chemical, what quantity, which technician, confirmed by whom, when.

   COLOUR IS NOT DECIDED HERE. assets/PALETTE.md is the contract; site.css
   (marketing) and this file (portals) both implement it verbatim so that a
   client who clicks "Sign in" does not land in what looks like a different
   company's product. Do not invent a token or change a value below without
   changing PALETTE.md first — the two files desynchronise instantly.

   Palette v2, "colour as light": a gradient mesh of blue → violet → cyan over
   a deep ground, with glass-style surfaces. --moss therefore still holds a
   BLUE; the name is kept because site.css re-points the same token and
   renaming it here would fork the two files.

   Semantics that survive the recolour, because they are meaning and not
   decoration:
     --moss    accent / confirmed / done          (brand blue)
     --signal  inspection tag, live, awaiting     (gold)
     --alert   live sighting, overdue, destructive(red)
     --info    neutral informational              (cyan — deliberately NOT the
               brand blue, since the two sit side by side on metric tiles)

   WHERE THE MESH IS ALLOWED. A marketing page is looked at; a portal is
   operated. Mesh and glow are chrome only — the sign-in ground, the page
   header band, the rail's edge, the field app's top bar, the console strip,
   the client hero, metric tiles, modal scrims, focus rings. They never sit
   behind data. There is no mesh under a table, a job list, the operator
   queue, the chat log or any list that scrolls in bulk: it is a legibility
   problem first and a frame-rate problem second, on a 2 vCPU ARM box serving
   mid-range Android phones.

   WHERE backdrop-filter IS ALLOWED. Three surfaces in the whole portal set,
   each static and each with an opaque fallback:
     .card-in     the sign-in card            (login.html)
     .modal-scrim the onboarding/job wizard   (admin.html)
     .scrim       the site drawer             (customer.html)
   Nothing that scrolls, and nothing with a list under it. The field app's tab
   bar used to be a blurred surface pinned over a scrolling queue, which is the
   single worst case for a compositor on a phone; it is opaque now.
   ========================================================================= */

/* Faces are linked per page — fonts-app.css for the portals, fonts-site.css
   for the public site. Both now request the same three families; the split is
   kept so a page can be moved between them without editing its <head>. */

:root {
  /* ── the mesh: three stops, constant across both themes ───────────────
     Grounds change; the light does not. Everything gradient-led is derived
     from these, which is what keeps the portal and the marketing site the
     same product under two different ground colours. */
  --mesh-1: #1B3BFF;
  /* blue   — the brand's centre of gravity */
  --mesh-2: #7C3AED;
  /* violet — the shift */
  --mesh-3: #06B6D4;
  /* cyan   — the lift */

  /* dark is the default ground — control rooms and field phones at dusk */
  --ink: #05071A;
  --surface: #0B0F2A;
  --surface-2: #131A3D;
  --surface-3: #1C2550;
  --line: #2A3468;
  --line-soft: #1A2148;

  --moss: #3B6BFF;
  --moss-lift: #7FA5FF;
  --moss-dim: #1B3BFF;

  --navy-flat: #1B3BFF;
  /* section rule — also the <meta> theme-color */

  --signal: #FFC53D;
  /* inspection tag / licence mark — the loud thing */
  --alert: #FF6B85;
  /* live sighting / overdue only */
  --info: #22D3EE;
  /* informational — cyan, must not read as brand */

  --text: #EEF1FF;
  --muted: #A9B1DC;
  --faint: #7A83B8;

  --glass: rgba(255, 255, 255, .055);
  --glass-line: rgba(255, 255, 255, .11);
  --glass-blur: 14px;

  /* ── derivations, not palette ─────────────────────────────────────────
     These three are portal-local recipes off contract tokens rather than new
     colours: the console needs a soft border tint and a legible type colour
     on a gold fill, and the marketing site does not. They are written as
     color-mix so they re-derive themselves when the theme flips, and so they
     can never drift from --signal / --alert. */
  --signal-dim: color-mix(in srgb, var(--signal) 42%, var(--ink));
  --alert-dim: color-mix(in srgb, var(--alert) 42%, var(--ink));
  --signal-ink: var(--ink);
  /* type ON a --signal fill: 12.6:1 on #FFC53D */

  /* Strength of a mesh wash, per theme. Dark grounds swallow a gradient, so
     they take a lot; a near-white ground shows 13% of a saturated blue very
     clearly, and anything stronger drops the gold .eyebrow that sits over the
     page header band below 4.5:1. Measured, not guessed — see PALETTE.md §1. */
  --mesh-o: .55;

  /* Fill percentage for a status pill or badge tint. Lower in the light
     theme: --alert type on a 16% --alert tint over white is 4.36:1, which is
     a fail; 11% is 4.78:1. The token is the recipe, not the palette. */
  --tint: 16%;

  /* The primary action is the mesh itself. Both stops carry white at ≥5.7:1,
     in BOTH themes, because the mesh stops are theme-constant — so one
     declaration serves light and dark. */
  --fill-brand: linear-gradient(135deg, var(--mesh-1), var(--mesh-2));

  /* The wash. Declared once, painted by .topbar / .ph-top / .cbar / the
     sign-in ground / the client hero. Never by anything that holds a list. */
  --mesh-wash:
    radial-gradient(78% 130% at 6% -20%,
      color-mix(in srgb, var(--mesh-1) 82%, transparent) 0%, transparent 62%),
    radial-gradient(62% 115% at 88% -8%,
      color-mix(in srgb, var(--mesh-2) 68%, transparent) 0%, transparent 60%),
    radial-gradient(64% 130% at 56% 118%,
      color-mix(in srgb, var(--mesh-3) 50%, transparent) 0%, transparent 64%);

  --radius: 14px;
  --radius-sm: 9px;
  --radius-lg: 22px;

  --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 8px 28px -12px rgba(0, 0, 0, .7);
  --shadow-lg: 0 2px 4px rgba(0, 0, 0, .3), 0 30px 70px -24px rgba(0, 0, 0, .8);

  --display: 'Newsreader', Georgia, 'Times New Roman', serif;
  --body: 'Public Sans', ui-sans-serif, system-ui, -apple-system, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;

  --ease: cubic-bezier(.22, .9, .28, 1);
  --ease-in: cubic-bezier(.5, 0, .75, 0);
}

/* Light grounds, PALETTE.md §Light, implemented verbatim. --signal-ink flips
   to paper because the light --signal (#8A5A06) is a dark gold that carries
   white at 5.9:1 and would swallow near-black type. */
@media (prefers-color-scheme: light) {
  :root {
    --ink: #F4F6FF;
    --surface: #FFFFFF;
    --surface-2: #EDF1FF;
    --surface-3: #E0E7FF;
    --line: #CFD8F5;
    --line-soft: #E4E9FB;
    --moss: #2647D8;
    --moss-lift: #1B3BFF;
    --moss-dim: #C9D6FF;
    --signal: #8A5A06;
    --alert: #C0304C;
    --info: #0E7490;
    --text: #0A1030;
    --muted: #454E80;
    --faint: #5C6598;
    --navy-flat: #2647D8;
    --glass: rgba(255, 255, 255, .72);
    --glass-line: rgba(16, 24, 64, .10);
    --signal-ink: #FFFFFF;
    --mesh-o: .13;
    --tint: 11%;
    --shadow: 0 1px 2px rgba(10, 16, 48, .06), 0 8px 24px -14px rgba(10, 16, 48, .22);
    --shadow-lg: 0 2px 6px rgba(10, 16, 48, .07), 0 28px 60px -28px rgba(10, 16, 48, .3);
  }
}

/* The viewer's explicit toggle must beat the OS preference in both
   directions, so both are restated at token level. */
:root[data-theme="dark"] {
  --ink: #05071A;
  --surface: #0B0F2A;
  --surface-2: #131A3D;
  --surface-3: #1C2550;
  --line: #2A3468;
  --line-soft: #1A2148;
  --moss: #3B6BFF;
  --moss-lift: #7FA5FF;
  --moss-dim: #1B3BFF;
  --navy-flat: #1B3BFF;
  --signal: #FFC53D;
  --alert: #FF6B85;
  --info: #22D3EE;
  --text: #EEF1FF;
  --muted: #A9B1DC;
  --faint: #7A83B8;
  --glass: rgba(255, 255, 255, .055);
  --glass-line: rgba(255, 255, 255, .11);
  --signal-ink: var(--ink);
  --mesh-o: .55;
  --tint: 16%;
  --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 8px 28px -12px rgba(0, 0, 0, .7);
  --shadow-lg: 0 2px 4px rgba(0, 0, 0, .3), 0 30px 70px -24px rgba(0, 0, 0, .8);
}

:root[data-theme="light"] {
  --ink: #F4F6FF;
  --surface: #FFFFFF;
  --surface-2: #EDF1FF;
  --surface-3: #E0E7FF;
  --line: #CFD8F5;
  --line-soft: #E4E9FB;
  --moss: #2647D8;
  --moss-lift: #1B3BFF;
  --moss-dim: #C9D6FF;
  --navy-flat: #2647D8;
  --signal: #8A5A06;
  --alert: #C0304C;
  --info: #0E7490;
  --text: #0A1030;
  --muted: #454E80;
  --faint: #5C6598;
  --glass: rgba(255, 255, 255, .72);
  --glass-line: rgba(16, 24, 64, .10);
  --signal-ink: #FFFFFF;
  --mesh-o: .13;
  --tint: 11%;
  --shadow: 0 1px 2px rgba(10, 16, 48, .06), 0 8px 24px -14px rgba(10, 16, 48, .22);
  --shadow-lg: 0 2px 6px rgba(10, 16, 48, .07), 0 28px 60px -28px rgba(10, 16, 48, .3);
}

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

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

body {
  margin: 0;
  background: var(--ink);
  color: var(--text);
  font-family: var(--body);
  font-size: 15px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

/* Newsreader is a transitional serif, not the grotesque this system was first
   scaled for. It wants looser tracking and more leading, and its variable
   range tops out at 700 — asking for 800 buys nothing but a clamp. Same
   treatment site.css gives the public pages, so a heading is the same object
   on both sides of the sign-in. */
h1,
h2,
h3,
h4 {
  font-family: var(--display);
  font-weight: 600;
  letter-spacing: -.015em;
  line-height: 1.12;
  margin: 0;
  text-wrap: balance;
}

p {
  margin: 0;
}

a {
  color: inherit;
  text-decoration: none;
}

img {
  max-width: 100%;
  display: block;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

/* The focus ring carries the brand light rather than the licence gold: gold is
   semantic (inspection tag) and was doing double duty here. --moss-lift is the
   accent-as-type value in both themes, so the ring is 8.9:1 on the dark ground
   and 6.7:1 on the light one, with a mesh halo behind it. */
:focus-visible {
  outline: 2px solid var(--moss-lift);
  outline-offset: 3px;
  border-radius: 4px;
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--mesh-1) 26%, transparent);
}

.mono {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
}

.eyebrow {
  font-family: var(--mono);
  font-size: .68rem;
  text-transform: uppercase;
  letter-spacing: .18em;
  color: var(--signal);
  font-weight: 500;
}

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

.wrap {
  width: min(1240px, 100% - 3rem);
  margin-inline: auto;
}

/* ── the mesh, and the one rule about it ───────────────────────────────────
   .mesh paints --mesh-wash on a pseudo-element behind its host's content. It
   is opt-in: no element gets it by inheritance, and in particular `body` never
   carries it, because receipt.html and audit.html load this stylesheet and are
   sheets of paper.

   The drift animates `transform` only — a composited property, so the layer is
   rasterised once and moved, rather than repainting three radial gradients
   every frame. Static under prefers-reduced-motion. */
.mesh {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

.mesh::before {
  content: '';
  position: absolute;
  inset: -45% -25%;
  z-index: -1;
  pointer-events: none;
  background-image: var(--mesh-wash);
  opacity: var(--mesh-o);
  animation: mesh-drift 34s var(--ease) infinite alternate;
}

@keyframes mesh-drift {
  from {
    transform: translate3d(-2%, -3%, 0) scale(1);
  }

  to {
    transform: translate3d(3%, 4%, 0) scale(1.12);
  }
}

/* Glass is a treatment, not a default. Three hosts use it; every one of them
   states an opaque fallback so a browser without backdrop-filter — or a
   compositor that has given up — still gets a readable card. */
@supports not (backdrop-filter: blur(1px)) {
  .glass {
    backdrop-filter: none !important;
    background: var(--surface) !important;
  }
}

/* ── buttons ───────────────────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  padding: .72rem 1.25rem;
  border-radius: 999px;
  border: 1px solid transparent;
  font-weight: 600;
  font-size: .875rem;
  cursor: pointer;
  transition: transform .18s var(--ease), background .18s, border-color .18s,
    box-shadow .18s, filter .18s;
  background: transparent;
}

.btn:hover {
  transform: translateY(-1px);
}

.btn:active {
  transform: translateY(0);
}

/* White on a flat --moss is 4.44:1 in the dark theme — a fail at this size.
   The mesh gradient carries it at 6.7:1 (blue stop) and 5.7:1 (violet stop),
   and is the same object in both themes because the stops are constant. */
.btn-primary {
  background: var(--fill-brand);
  color: #fff;
  box-shadow: 0 6px 20px -10px var(--mesh-1);
}

.btn-primary:hover {
  filter: brightness(1.12);
  box-shadow: 0 8px 26px -10px var(--mesh-2);
}

.btn-signal {
  background: var(--signal);
  color: var(--signal-ink);
}

.btn-signal:hover {
  filter: brightness(1.08);
}

.btn-ghost {
  border-color: var(--line);
  color: var(--text);
}

.btn-ghost:hover {
  border-color: var(--moss);
  background: color-mix(in srgb, var(--moss) 10%, transparent);
}

.btn-sm {
  padding: .45rem .85rem;
  font-size: .8rem;
}

/* The theme toggle. admin/customer/operator all carry .icon-btn but only
   site.css defined it, and the portals do not load site.css — so the control
   that switches light and dark was itself an unstyled UA button: a white
   rectangle on a navy topbar. Defined here so the portals have it; site.css
   loads after app.css on the public pages and keeps its own copy. */
.icon-btn {
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border-radius: 9px;
  border: 1px solid var(--line);
  background: transparent;
  cursor: pointer;
  color: var(--muted);
  transition: all .18s;
}

.icon-btn:hover {
  color: var(--text);
  border-color: var(--moss);
}

/* ── surfaces ──────────────────────────────────────────────────────────── */
/* Opaque, deliberately. A card is where the work is, and the work needs a
   ground that does not move under it. */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.pad {
  padding: 1.25rem;
}

/* ── status pills: state must read as shape + colour, not colour alone ─── */
.pill {
  display: inline-flex;
  align-items: center;
  gap: .38rem;
  padding: .2rem .6rem .22rem;
  border-radius: 999px;
  font-family: var(--mono);
  font-size: .68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .08em;
  white-space: nowrap;
}

/* The tint mixes into --surface rather than into transparent. A pill is a
   status badge that has to be readable wherever it lands, and the field app
   puts one in the mesh-lit top bar: over the light theme's wash, gold on a
   transparent tint fell to 4.1:1. Backing it with the card ground makes the
   pill's contrast a property of the pill instead of a property of whatever
   happens to be behind it. */
.pill::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 2px;
  background: currentColor;
}

.pill-done {
  color: var(--moss-lift);
  background: color-mix(in srgb, var(--moss) var(--tint), var(--surface));
}

.pill-live {
  color: var(--signal);
  background: color-mix(in srgb, var(--signal) var(--tint), var(--surface));
}

.pill-live::before {
  border-radius: 999px;
  animation: blip 1.6s infinite;
}

.pill-wait {
  color: var(--muted);
  background: color-mix(in srgb, var(--muted) var(--tint), var(--surface));
}

.pill-alert {
  color: var(--alert);
  background: color-mix(in srgb, var(--alert) var(--tint), var(--surface));
}

.pill-alert::before {
  border-radius: 999px;
}

@keyframes blip {

  0%,
  100% {
    opacity: 1;
    transform: scale(1);
  }

  50% {
    opacity: .35;
    transform: scale(.7);
  }
}

/* ── scroll reveal ─────────────────────────────────────────────────────── */
/* Scroll-reveal hides content until JS unhides it, so it is gated on JS being
   alive. Ungated, a script error or a blocked file leaves a marketing page
   blank rather than merely unanimated — the visitor sees nothing at all. */
.js .reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity .7s var(--ease), transform .7s var(--ease);
}

.js .reveal.in {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .js .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ── shared app chrome (portals) ───────────────────────────────────────── */
.app {
  display: grid;
  grid-template-columns: 244px 1fr;
  min-height: 100vh;
}

/* The rail is dense navigation that is on screen the whole time, and its
   labels are --faint. No wash goes behind it: --faint over a mesh is 3.9:1.
   It gets the mesh as a 1.5px lit edge instead — painted, not composited, and
   the one place the identity can appear here for free. */
.rail {
  background-color: var(--surface);
  background-image: linear-gradient(180deg, var(--mesh-1), var(--mesh-2) 52%, var(--mesh-3));
  background-repeat: no-repeat;
  background-size: 1.5px 100%;
  background-position: right top;
  border-right: 0;
  padding: 1.4rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}

.rail-nav {
  display: flex;
  flex-direction: column;
  gap: .15rem;
}

.rail-nav a {
  display: flex;
  align-items: center;
  gap: .7rem;
  padding: .58rem .7rem;
  border-radius: var(--radius-sm);
  color: var(--muted);
  font-size: .875rem;
  font-weight: 500;
  transition: background .16s, color .16s;
}

.rail-nav a:hover {
  background: var(--surface-2);
  color: var(--text);
}

.rail-nav a.on {
  background: color-mix(in srgb, var(--moss) 18%, transparent);
  color: var(--text);
  font-weight: 600;
}

.rail-nav a.on .ic {
  color: var(--moss-lift);
}

/* Nav icons are inline SVG, sized in one place so a stroke tweak does not
   need eleven edits. currentColor lets the .on state recolour them. */
.ic {
  width: 18px;
  height: 18px;
  color: var(--faint);
  flex: none;
}

.rail-label {
  font-family: var(--mono);
  font-size: .62rem;
  text-transform: uppercase;
  letter-spacing: .16em;
  color: var(--faint);
  padding: 0 .7rem;
  margin-bottom: .4rem;
}

.main {
  padding: 1.6rem 2rem 4rem;
  min-width: 0;
}

/* The page header band. This is the portal's hero: it holds an eyebrow, an
   h1, and the page's controls, and nothing else — no list, no table, no faint
   type. Self-contained rather than a wash pinned to the top of .main, so it
   cannot creep under whatever the next section happens to be. */
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.6rem;
  flex-wrap: wrap;
  padding: 1.15rem 1.3rem;
  border: 1px solid var(--glass-line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

.topbar::before {
  content: '';
  position: absolute;
  inset: -45% -25%;
  z-index: -1;
  pointer-events: none;
  background-image: var(--mesh-wash);
  opacity: var(--mesh-o);
  animation: mesh-drift 34s var(--ease) infinite alternate;
}

/* The wash cannot go stronger than --mesh-o in the light theme: the gold
   .eyebrow that sits in this band needs the ground to stay above L .748, and
   .13 already puts it at 4.66:1. So the light theme gets its identity from a
   lit edge instead of a deeper tint — the same device as the rail's edge and
   the console tab underline, and it costs one paint. */
.topbar::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 2px;
  pointer-events: none;
  background: linear-gradient(90deg, var(--mesh-1), var(--mesh-2) 55%, var(--mesh-3));
}

/* ── brand mark ────────────────────────────────────────────────────────── */
.brand {
  display: inline-flex;
  align-items: center;
  gap: .6rem;
  font-family: var(--display);
  font-weight: 700;
  letter-spacing: -.015em;
  font-size: 1.1rem;
}

/* The mark is the mesh, so it is the same object in both themes and cannot
   repeat the old failure where the light theme's --moss-dim was a pale tint
   and white "RCS" vanished into the bottom-right corner. */
.brand-mark {
  width: 30px;
  height: 30px;
  border-radius: 9px;
  flex: none;
  background: var(--fill-brand);
  display: grid;
  place-items: center;
  color: #fff;
  font-size: .78rem;
  font-family: var(--mono);
  font-weight: 600;
  letter-spacing: -.04em;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .28), 0 4px 14px -6px var(--mesh-1);
}

/* The console header carries the real wordmark. Theme swap copied from
   site.css verbatim — element+class on both sides, so the two variants can
   never tie on specificity and render at once. */
.brand img { height: 34px; width: auto; }
/* The rail stretches the anchor to its full width; without this the wordmark
   hugs the left edge instead of sitting centred over the nav column. */
.rail .brand { justify-content: center; }
img.logo-dark  { display: none; }
@media (prefers-color-scheme: dark) {
  img.logo-light { display: none; }
  img.logo-dark  { display: block; }
}
[data-theme="dark"]  img.logo-light { display: none; }
[data-theme="dark"]  img.logo-dark  { display: block; }
[data-theme="light"] img.logo-light { display: block; }
[data-theme="light"] img.logo-dark  { display: none; }

/* ── data table ────────────────────────────────────────────────────────── */
/* Flat and opaque, on purpose. 97 rows over a gradient is a legibility
   problem and a repaint-per-scroll problem at the same time. */
.tablewrap {
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: .85rem;
  min-width: 640px;
}

th {
  text-align: left;
  padding: .7rem .9rem;
  font-family: var(--mono);
  font-weight: 500;
  font-size: .66rem;
  text-transform: uppercase;
  letter-spacing: .12em;
  color: var(--faint);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

td {
  padding: .78rem .9rem;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: middle;
}

tbody tr:last-child td {
  border-bottom: none;
}

tbody tr {
  transition: background .14s;
}

tbody tr:hover {
  background: var(--surface-2);
}

td.num,
th.num {
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}

/* ── metric tiles ──────────────────────────────────────────────────────── */
/* Opaque card, with the tile's own semantic accent bled into the bottom-right
   corner as light. The glow is kept out of the top-left, where .k sits in
   --faint; --muted and the display numeral are the only things it reaches. */
.metrics {
  display: grid;
  gap: .9rem;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}

.metric {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem 1.1rem;
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

.metric::before {
  content: '';
  position: absolute;
  inset: auto -10% -30% auto;
  z-index: -1;
  width: 190px;
  height: 130px;
  pointer-events: none;
  background: radial-gradient(60% 70% at 100% 100%,
      color-mix(in srgb, var(--accent-bar, var(--moss)) 18%, transparent), transparent 72%);
}

.metric::after {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  background: linear-gradient(180deg,
      var(--accent-bar, var(--moss)),
      color-mix(in srgb, var(--accent-bar, var(--moss)) 55%, transparent));
}

.metric .k {
  font-family: var(--mono);
  font-size: .64rem;
  text-transform: uppercase;
  letter-spacing: .13em;
  color: var(--faint);
}

.metric .v {
  font-family: var(--display);
  font-size: 2rem;
  font-weight: 700;
  letter-spacing: -.02em;
  margin-top: .3rem;
  font-variant-numeric: tabular-nums;
}

.metric .d {
  font-size: .76rem;
  color: var(--muted);
  margin-top: .15rem;
}

/* ── forms ─────────────────────────────────────────────────────────────── */
.field {
  display: flex;
  flex-direction: column;
  gap: .35rem;
}

.field label {
  font-size: .78rem;
  font-weight: 600;
  color: var(--muted);
}

.input,
select.input,
textarea.input {
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--text);
  border-radius: var(--radius-sm);
  padding: .6rem .75rem;
  font-size: .875rem;
  transition: border-color .16s, background .16s, box-shadow .16s;
  width: 100%;
}

.input:focus {
  border-color: var(--moss);
  background: var(--surface);
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--mesh-1) 22%, transparent);
}

.input::placeholder {
  color: var(--faint);
}

/* ── service code (OTP) ────────────────────────────────────────────────── */
.otp {
  display: flex;
  gap: .5rem;
}

.otp input {
  width: 46px;
  height: 56px;
  text-align: center;
  font-family: var(--mono);
  font-size: 1.35rem;
  font-weight: 600;
  background: var(--surface-2);
  border: 1.5px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--text);
  transition: border-color .18s, transform .18s var(--ease), background .18s;
}

.otp input:focus {
  outline: none;
  border-color: var(--signal);
  background: var(--surface);
}

.otp input.filled {
  border-color: var(--moss);
  background: color-mix(in srgb, var(--moss) 12%, var(--surface-2));
  transform: translateY(-2px);
}

@media (prefers-reduced-motion: reduce) {

  .mesh::before,
  .topbar::before,
  .ph-top::before,
  .cbar::before,
  .hero-strip::before,
  .sign-ground::before,
  .sign-glow {
    animation: none !important;
  }

  *,
  *::before,
  *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── modal ──────────────────────────────────────────────────────────────────
   These lived only in admin.html's inline <style>, so no other portal could
   open a dialog — which is why the field app was still calling
   window.confirm(). Declared here for assets/confirm.js, which builds its node
   from these class names and adds no markup to any page.

   Colours are existing tokens only; nothing new is introduced, so PALETTE.md
   stays the contract. The scrim is one of the three surfaces the header block
   above sanctions for backdrop-filter: it is static, and nothing scrolls
   underneath it while it is up.

   Its own responsive rules travel with it rather than living in the shared
   breakpoint sections below. A dialog that has lost its mobile layout is a
   confirm button a technician cannot reach, and this component is loaded by
   pages that do not otherwise opt into those sections. */
.modal-scrim { position: fixed; inset: 0; background: rgba(0,0,0,.55); backdrop-filter: blur(4px);
               opacity: 0; pointer-events: none; transition: opacity .28s; z-index: 300;
               display: grid; place-items: center; padding: 1.5rem; }
.modal-scrim.on { opacity: 1; pointer-events: auto; }
.modal {
  width: min(660px, 100%); max-height: 88vh; overflow-y: auto; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
  transform: translateY(16px) scale(.98); transition: transform .3s var(--ease);
}
.modal-scrim.on .modal { transform: none; }
.m-head { padding: 1.3rem 1.5rem 1rem; border-bottom: 1px solid var(--line);
          display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; }
.m-body { padding: 1.4rem 1.5rem; }
.m-foot { padding: 1rem 1.5rem; border-top: 1px solid var(--line); background: var(--surface-2);
          display: flex; justify-content: space-between; gap: 1rem; align-items: center;
          border-radius: 0 0 var(--radius-lg) var(--radius-lg); }

/* Phone and small tablet: near full-bleed and bottom-anchored, because 1.5rem
   of scrim each side on a 360px screen is a fifth of the width spent on
   nothing, and a dialog by the thumb beats one in the middle of the screen. */
@media (max-width: 720px) {
  .modal-scrim { padding: .6rem; align-items: end; }
  .modal { width: 100%; max-height: 92vh; max-height: 92dvh; }
  .m-head, .m-body, .m-foot { padding-left: 1rem; padding-right: 1rem; }
  /* Both actions go full width and stack. Cancel is drawn first, so on a
     phone the harmless one sits above the destructive one. */
  .m-foot { flex-wrap: wrap; }
  .m-foot .btn { flex: 1 1 auto; }
}

/* A tap target below about 40px is a miss waiting to happen, and a miss here
   either dismisses a dialog or confirms it. */
@media (pointer: coarse) {
  .m-foot .btn, .m-head .dw-close { min-height: 40px; }
}

/* ── responsive: the rail ───────────────────────────────────────────────────
   Below the breakpoint the rail leaves the grid and becomes a drawer. It used
   to stay in flow as a horizontally-scrolling strip, which put whichever
   section you wanted just past the right-hand edge. Paired with
   assets/responsive.js, which supplies the toggle and the scrim. */
.nav-toggle {
  display: none;
}

@media (max-width: 900px) {
  .app {
    grid-template-columns: 1fr;
  }

  .rail {
    position: static;
    height: auto;
    flex-direction: row;
    align-items: center;
    overflow-x: auto;
    border-right: none;
    border-bottom: 1px solid var(--line);
  }

  .rail-nav {
    flex-direction: row;
  }

  .rail-label,
  .rail .only-wide {
    display: none;
  }

  .main {
    padding: 1.2rem 1rem 3rem;
  }
}
/* ============================================================================
   PRINT — PALETTE.md §4
   ----------------------------------------------------------------------------
   receipt.html and audit.html are physical service records that go into a
   client's compliance file. They declare `color-scheme: light`, which does NOT
   pin this stylesheet's tokens: those follow the reader's OS theme, so a QA
   head printing from a dark-themed laptop used to get slate-grey column heads
   (`th` in --faint) and a navy band under the pointer (`tbody tr:hover` in
   --surface-2) on a black-ink document. Both leaked before and both are pinned
   here rather than only in the two pages, so the next document added to the
   portal set inherits paper by default instead of rediscovering the bug.

   Everything this block touches is stated as a fact about paper: white ground,
   near-black ink, no mesh, no glass, no tint, no shadow, opacity 1.
   ========================================================================= */
@media print {
  /* All three selectors, and in this order: a reader who has ever used the
     theme toggle carries data-theme on <html> for the rest of the session, and
     `:root[data-theme="dark"]` outranks a bare `:root` even inside a later
     media block. Pinning only `:root` would leave a toggled-dark reader
     printing dark tokens — the exact failure this block exists to stop. */
  :root, :root[data-theme="dark"], :root[data-theme="light"] {
    --ink: #FFFFFF; --surface: #FFFFFF; --surface-2: #FFFFFF; --surface-3: #FFFFFF;
    --line: #111111; --line-soft: #666666;
    --text: #111111; --muted: #333333; --faint: #111111;
    --moss: #2647D8; --moss-lift: #2647D8; --moss-dim: #FFFFFF;
    --signal: #111111; --signal-ink: #FFFFFF; --signal-dim: #666666;
    --alert: #C0304C; --alert-dim: #C0304C;
    --info: #111111; --navy-flat: #111111;
    --glass: #FFFFFF; --glass-line: #111111; --glass-blur: 0px;
    --mesh-o: 0; --tint: 0%;
    --shadow: none; --shadow-lg: none;
    --fill-brand: none;
    color-scheme: light;
  }
  html, body { background: #fff !important; color: #111 !important; }
  /* No decorative layer may reach the sheet, whichever host painted it. */
  .mesh::before, .topbar::before, .topbar::after, .ph-top::before, .cbar::before,
  .hero-strip::before, .hero-strip::after, .metric::before,
  .sign-ground::before, .sign-grid, .sign-glow,
  .modal-scrim, .scrim, .toolbar, .ph-tabs {
    display: none !important;
  }
  /* Glass, blur and translucency are screen effects. On paper they are either
     invisible or a grey box, and a grey box on a service record is a defect.
     .wm is the one exemption: the 5% "RCS CARE" watermark is furniture on the
     issued audit PDF, it is meant to be faint, and forcing it opaque would lay
     solid black lettering across every finding. It is excluded from the reset
     rather than hidden, because dropping it would change the document. */
  *:not(.wm), *::before, *::after {
    backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
    filter: none !important; box-shadow: none !important; text-shadow: none !important;
    opacity: 1 !important;
  }
  .wm { opacity: .05 !important; filter: none !important; }
  .wm span { color: #111 !important; }
  /* The two base table rules that actually leaked, pinned at source. Only the
     colour: app.css's base `th` sets no background, so there is nothing to
     leak there — and forcing one would strip receipt.html's own #ECEFF4
     header band, which is part of the paper document rather than console
     chrome. Verified in a print render: the band is still on the sheet. */
  th { color: #111 !important; }
  td { color: inherit; }
  tbody tr, tbody tr:hover { background: transparent !important; }
  .tablewrap, .card, .metric, .pad { background: #fff !important; box-shadow: none !important; }
  .pill { background: transparent !important; color: #111 !important;
          border: 1px solid #111; }
  .eyebrow, .muted, .mono { color: #111 !important; }
  .brand-mark { background: #fff !important; color: #111 !important; border: 1px solid #111; }
  :focus-visible { outline: none !important; box-shadow: none !important; }
  /* Nothing scrolls on a printed page, so without this the whole document
     comes out blank. */
  .js .reveal { opacity: 1 !important; transform: none !important; }
  a { color: #111 !important; text-decoration: none; }
}

/* ── third-party assistant widget ──────────────────────────────────────────
   The IntelloWork widget renders into #exu-widget-root inside this document —
   no iframe, no shadow root — so this sheet reaches it, and it is the only
   place the fix can live: the markup is built by their loader, not by us.

   Their close button carries an inline `color: #0F172A` (slate-900). Their
   panel header is dark navy in BOTH themes — only the body below it follows
   the theme we hand the loader — so that slate is navy-on-navy and the button
   disappears. It is still there and still works: screen readers announce it,
   Tab reaches it, a click closes the panel. A contrast bug, not a missing
   control.

   Painted against its own header rather than the page theme, because the
   header does not change with the theme and a light/dark pair here would be
   wrong half the time. An opaque pale disc carries the dark glyph on the navy
   header; on a pale header the ring keeps the disc defined. Inline styles
   outrank external rules, hence !important. Scoped to the aria-label, so if
   their markup renames it this override silently stops applying and the
   button goes back to being invisible — that is the thing to check first if
   it ever vanishes again.

   Geometry, measured off the rendered panel rather than guessed: their header
   is 60px tall and reserves a 48px column on the right for this button, but
   pins it with `top: 8px` at 28px square — so it rides 8px above the header's
   centre line and reads small against the "New chat" pill beside it. 32px
   centred in both axes (top: 15px sits it on the centre line; right: 9px
   keeps the centre where theirs was) fills the reserved column without
   crowding it, and is a kinder touch target. Those two offsets are tied to
   their 60px header — if they change its height, these drift. */
#exu-widget-root button[aria-label="Close chat"] {
  color: #0F172A !important;
  background: #F1F5F9 !important;
  border: 1px solid rgba(15, 23, 42, .12) !important;
  top: 15px !important;
  right: 9px !important;
  width: 32px !important;
  height: 32px !important;
  font-size: 22px !important;
  line-height: 1 !important;
}

/* ── touch targets and micro-type on phones ────────────────────────────────
   The modal block above already establishes the rule — a tap target below
   about 40px is a miss waiting to happen — but it only ever covered the
   dialog chrome. The site chrome was never brought in line: the theme toggle
   and the menu button render 34x34, the brand mark 30x30, and the toolbar
   buttons 37px tall. Fine under a mouse, a miss under a thumb.

   The icon buttons grow their hit area with a centred pseudo-element rather
   than their own box, because they sit in a tight row beside the logo and the
   nav: growing the ink would re-space the header on every page, and at 320px
   there is no room to absorb it. Height goes to 44 because the header has
   vertical room to spare; width only stretches to 44 when the control is
   already close, so two adjacent buttons cannot overlap and swap taps.
   Buttons that carry a label grow properly — 37 to 44 costs nothing in a
   toolbar that has already stacked.

   The type floor is separate: on a phone the mono micro-labels under the
   figures and on the tab strip land at 9.6-9.9px, which is below what anyone
   reads comfortably at arm's length. Lifted to 11.2px on phone widths only,
   so the desktop typography is untouched. */
@media (pointer: coarse) {
  .icon-btn, #burger, .brand, .dw-close { position: relative; }
  .icon-btn::after, #burger::after, .brand::after, .dw-close::after {
    content: '';
    position: absolute; top: 50%; left: 50%; translate: -50% -50%;
    width: 100%; min-width: 44px; height: 44px;
  }
  .btn { min-height: 44px; }
  /* The dialog block above settled on 40px and its selectors are more
     specific than the plain .btn rule, so it wins and those controls stay
     40. Restated here at 44 to bring them to the same standard rather than
     leaving two different answers in one stylesheet. .mq-pause is not a
     .btn at all, which is why it was missed entirely. */
  .m-foot .btn, .m-head .dw-close, .mq-pause { min-height: 44px; }
}

@media (max-width: 500px) {
  .metric .k { font-size: .7rem; }
}

/* ── data tables: long unbreakable values ─────────────────────────────────
   Records carry values with no break opportunity in them — machine-generated
   addresses, job refs, audit-log payloads, hashes. A cell holding one of those
   demands its full width, the table's natural width runs past its wrapper, and
   what gets clipped is whichever column came first: usually the identity of
   the row. The audit log did this even on a 1440 desktop.

   Applied per column, NOT to every cell. Blanket `overflow-wrap: anywhere`
   was tried and is wrong: it lowers the min-content width of every cell, so
   the table hands its space to whichever column has the longest text and
   snaps the short atomic ones — "RCS-284065" came out as "RCS-" over
   "284065", and "19 Aug, 13:24" broke across two lines. A reference or a
   timestamp is a single value and must never break; only genuinely
   unbreakable long tokens get to wrap, and they are marked where they are
   rendered. */
/* ── data tables: what may wrap, and what may not ─────────────────────────
   Per column, never blanket. `overflow-wrap: anywhere` on every cell was
   tried and is wrong: it lowers the min-content width of all of them, so the
   table hands its space to whichever column has the longest text and snaps
   the short atomic ones — "RCS-284065" came out as "RCS-" over "284065", and
   "19 Aug, 13:32" as three lines. A reference or a timestamp is one value and
   must never break; a JSON payload has no break opportunity at all and is the
   only thing that should.

   The action column is matched by what it holds, not by being last. It was
   written as td:last-child, which is true on the tables whose final column is
   a row of buttons and false on the audit log, where the last column is the
   detail text — and nowrap on a JSON blob is exactly the wrong answer. */
.tablewrap td:has(.btn) { white-space: nowrap; }
.tablewrap td.ref, .tablewrap td.when, .tablewrap td.num { white-space: nowrap; }
.tablewrap td.wrap { overflow-wrap: anywhere; white-space: normal; }

/* ── the filter bar ────────────────────────────────────────────────────────
   assets/filters.js has always emitted .flt-bar / .flt-item / .flt-grow /
   .flt-actions, and nothing in either stylesheet ever styled them. Unstyled
   divs are blocks, so every control rendered full width, one per row: the
   work queue opened as eleven stacked fields and the table it filters was
   pushed a screen and a half below the fold. It reads as a broken page
   because functionally it is one — the data is not on screen.

   A grid rather than a flex row, so the controls line up in columns instead
   of ragging, and auto-fit so the same bar serves a phone (one column), a
   tablet (two or three) and a console (as many as fit) without a breakpoint
   per page. Search spans two columns where there is room, because a ref or a
   site name needs the width. Clear sits at the end of the row on its own
   baseline, aligned with the inputs rather than their labels. */
.flt-bar {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: .7rem .8rem;
  align-items: end;
  margin-bottom: 1rem;
}
.flt-item { display: flex; flex-direction: column; gap: .3rem; min-width: 0; }
.flt-item .input { width: 100%; }
.flt-label {
  font-family: var(--mono);
  font-size: .64rem;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: var(--faint);
}
.flt-grow { grid-column: span 2; }
.flt-actions { justify-content: flex-end; }
.flt-actions .btn { width: 100%; }

/* One column on a phone, and the search box stops claiming two of them. */
@media (max-width: 560px) {
  .flt-bar { grid-template-columns: 1fr; }
  .flt-grow { grid-column: auto; }
}

/* ── pagination ────────────────────────────────────────────────────────────
   Same story as the filter bar: assets/pager.js emits .pg / .pg-count /
   .pg-nav / .pg-btn / .pg-gap and not one of them was ever styled, so the
   pager under every table in every portal rendered as a bare run of unstyled
   buttons wrapping into the page. It is the control that says which slice of
   the record you are looking at, so it has to read as a control. */
.pg {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: .7rem 1rem;
  margin-top: .9rem;
}
.pg-count { font-size: .74rem; color: var(--faint); }
.pg-nav { display: flex; align-items: center; gap: .3rem; flex-wrap: wrap; }
.pg-btn {
  min-width: 36px;
  padding: .42rem .6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: .78rem;
  cursor: pointer;
  transition: background .14s, border-color .14s, color .14s;
}
.pg-btn:hover:not(:disabled) { border-color: var(--moss); color: var(--moss-lift); }
/* The current page is a state, not a destination — it does not invite a click. */
.pg-btn.on {
  background: var(--fill-brand);
  border-color: transparent;
  color: #fff;
  font-weight: 600;
  cursor: default;
}
.pg-btn:disabled { opacity: .45; cursor: not-allowed; }
.pg-gap { padding: 0 .2rem; color: var(--faint); }

/* ── a field that failed validation ────────────────────────────────────────
   validate.js sets .invalid and aria-invalid together and writes the message
   into its own node. The message and the ARIA state were landing; the field
   itself was not marked, so on a long form the text said something was wrong
   and nothing said which box. Colour is not the only signal — the border
   weight changes too, for anyone who cannot separate the hue. */
.input.invalid,
.invalid {
  border-color: var(--alert) !important;
  box-shadow: 0 0 0 1px var(--alert);
}
.input.invalid:focus,
.invalid:focus {
  outline-color: var(--alert);
}

/* responsive.js sets this on <body> while the rail is open as a drawer, to
   stop the page underneath scrolling with a dragging finger. Nothing acted
   on it, so the page behind the drawer scrolled anyway. */
body.nav-open { overflow: hidden; }

/* The one size below btn-sm. Used on the client portal's inline actions. */
.btn-xs {
  padding: .3rem .6rem;
  font-size: .74rem;
  border-radius: var(--radius-sm);
}

/* The dialog footer holds the commit — Schedule job, Issue audit, Save. The
   panel already scrolls at max-height 88vh, so nothing was unreachable, but
   on a 360x640 phone the New audit form is 842px against a 561px panel and
   the action sits below the fold on open: the dialog reads as if it has no
   way to finish. Pinned to the bottom of the scrolling panel so the commit is
   always on screen and the form scrolls behind it. It already carries its own
   background and top border, so it stays opaque over the content it covers. */
.modal .m-foot {
  position: sticky;
  bottom: 0;
  z-index: 1;
}

/* ── collapsible rail groups ───────────────────────────────────────────────
   Thirteen destinations in a flat list is more than the eye sorts at a
   glance, and only the first eight are daily work: Platform holds the
   reference shelf — sightings, analytics, notifications, the audit log and
   integrations — which is looked at occasionally and never during a shift.

   Nothing is removed; the group is folded. Operations and Directory open,
   Platform closed, and the choice is remembered per browser, so an admin who
   lives in the audit log is not re-opening it every morning. The label was a
   div and is now a button, because it does something. */
.rail-group { margin-bottom: .25rem; }
.rail-group + .rail-group { margin-top: .9rem; }

button.rail-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  width: 100%;
  background: none;
  border: 0;
  cursor: pointer;
  text-align: left;
  border-radius: var(--radius-sm);
  padding: .32rem .7rem;
}
button.rail-label:hover { color: var(--muted); background: var(--surface-2); }

.rail-caret {
  font-size: .7rem;
  line-height: 1;
  transition: transform .2s var(--ease);
}
.rail-group.collapsed .rail-caret { transform: rotate(-90deg); }

/* Folded with display:none rather than an animated height. The grid 0fr
   trick was tried first and silently did nothing here: it collapses the one
   explicit row, and .rail-items holds five links as direct children, so the
   other four sized themselves as implicit auto rows and stayed on screen and
   clickable. Correctness over the transition. */
.rail-group.collapsed .rail-items { display: none; }

/* On the icon-only rail there is no label to press, so nothing may be
   folded — every destination stays reachable. */
@media (max-width: 1100px) {
  .rail-group.collapsed .rail-items { display: block; }
}

/* ── forms ─────────────────────────────────────────────────────────────────
   Every dialog and panel in the product is a form, and until now a field was
   a label plus an input and nothing else: no way to mark what is required, no
   place for the sentence that explains a field, and — because .field-err was
   never styled — a failed validation printed as an unstyled paragraph in body
   type. The person filling the form could not tell which boxes were optional,
   why a value was rejected, or where the error was.

   The pieces below are the vocabulary a form needs. Everything is opt-in, so
   existing markup keeps working and gains the parts it asks for. */

/* Optional is the exception worth marking, not required. Marking six of seven
   fields with an asterisk teaches nothing; marking the one you may skip does.
   Both are provided — use .req only on forms that are mostly optional. */
.field label { display: flex; align-items: baseline; gap: .4rem; }
.field.req > label::after {
  content: 'Required';
  font-family: var(--mono);
  font-size: .58rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--alert);
}
.field.opt > label::after {
  content: 'Optional';
  font-family: var(--mono);
  font-size: .58rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--faint);
}

/* The sentence that stops someone guessing. Sits under the control, because
   read-after-you-look-at-the-box is the order people actually use it in. */
.field .hint {
  font-size: .74rem;
  line-height: 1.45;
  color: var(--faint);
  margin: 0;
}

/* validate.js inserts this after the field it belongs to and gives it
   role="alert". It had no styling at all. */
.field-err {
  margin: .1rem 0 0;
  font-size: .74rem;
  line-height: 1.45;
  color: var(--alert);
  display: flex;
  gap: .35rem;
}
.field-err::before { content: '!'; font-weight: 700; flex: none; }

/* A native select renders its arrow differently on every platform and sits
   flush against the right padding. One chevron, drawn here, same everywhere.
   The right padding leaves room for it so a long option cannot run underneath. */
select.input {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 2.1rem;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: right 1.05rem center, right .8rem center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
select.input:disabled { opacity: .55; cursor: not-allowed; }

/* A control you have to hit with a thumb. 40px was the old height; the rest
   of the product now settles at 44. */
.input, select.input { min-height: 44px; }
textarea.input { min-height: 76px; resize: vertical; line-height: 1.5; }

/* A titled block inside a long form, so a wizard step reads as two or three
   short forms rather than one wall of fields. */
.form-section + .form-section { margin-top: 1.4rem; }
.form-section > .form-legend {
  font-family: var(--mono);
  font-size: .62rem;
  text-transform: uppercase;
  letter-spacing: .14em;
  color: var(--faint);
  padding-bottom: .5rem;
  margin-bottom: .9rem;
  border-bottom: 1px solid var(--line-soft);
}

/* Two fields to a row where there is room, one where there is not. Replaces
   per-form grid-template-columns, which did not collapse on a phone. */
.grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .95rem; }
.grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .95rem; }
@media (max-width: 620px) {
  .grid-2, .grid-3 { grid-template-columns: 1fr; }
}

/* The note that applies to the whole form rather than one field — a rule the
   API will enforce, a consequence worth knowing before pressing the button. */
.form-note {
  display: flex;
  gap: .55rem;
  padding: .7rem .85rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-soft);
  background: var(--surface-2);
  font-size: .78rem;
  line-height: 1.5;
  color: var(--muted);
}
.form-note::before { content: 'i'; font-family: var(--mono); font-weight: 700; color: var(--info); flex: none; }

/* ── dialog and drawer close button ────────────────────────────────────────
   Declared once here because it was declared eight times before: seven copies
   of the same eight inline properties across admin.html's modals, plus a
   local rule in customer.html. Eight places to keep in step is how a control
   ends up looking slightly different on every screen, and it is why the
   admin copies never picked up the hover the client portal had.

   The hue on hover is --alert because this discards a half-filled form. */
.dw-close {
  flex: none;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--muted);
  font-size: .9rem;
  line-height: 1;
  cursor: pointer;
  transition: color .14s, border-color .14s, background .14s;
}
.dw-close:hover {
  color: var(--alert);
  border-color: var(--alert);
  background: color-mix(in srgb, var(--alert) 8%, transparent);
}

/* ── dialog polish ─────────────────────────────────────────────────────────
   The head and foot are fixed furniture and the body is the part that moves,
   so the two edges stay put while a long form scrolls between them. Without
   this the header scrolled away and a half-filled dialog lost the one line
   saying what it was. */
.modal .m-head {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.m-head .eyebrow { display: block; }
.m-head h2 { line-height: 1.25; }

/* A dialog that has scrolled shows a hairline under its header, so the edge
   between fixed furniture and moving content stays legible. */
.modal .m-head::after {
  content: '';
  position: absolute;
  inset: auto 0 -1px 0;
  height: 1px;
  background: var(--line);
}

/* ── metric rows that do not leave a widow ─────────────────────────────────
   auto-fit packs as many tiles as fit and drops the remainder onto a new row
   on their own. With six tiles at 1440 that means five across and one alone
   with a two-thirds-empty row beside it, which reads as a page that failed to
   finish loading rather than as a deliberate layout.

   The column count is chosen from the number of tiles instead, so a row
   always divides evenly. :has with :nth-child(n):last-child matches "exactly
   n children" — the only way to count them in CSS. Pages with four or fewer
   keep auto-fit, which already divides cleanly. */
@media (min-width: 1000px) {
  /* exactly 6 → two rows of three, never 5 + 1 */
  .metrics:has(> :nth-child(6):last-child) { grid-template-columns: repeat(3, 1fr); }
  /* exactly 5 → one row, tighter than auto-fit would allow */
  .metrics:has(> :nth-child(5):last-child) { grid-template-columns: repeat(5, 1fr); }
}
@media (min-width: 620px) and (max-width: 999px) {
  .metrics:has(> :nth-child(5):last-child),
  .metrics:has(> :nth-child(6):last-child) { grid-template-columns: repeat(2, 1fr); }
}

/* ── the operator directory's summary strip ────────────────────────────────
   Four counts above the rows. The one that matters operationally is the
   lapsed-certificate figure: the API refuses to assign those people, so a
   route built without checking it produces a job that bounces. It was
   previously legible only as small red text on individual rows, which means
   you had to read thirty rows to learn a number.

   Tone is carried by a left rule and the figure's colour, not by a filled
   panel — this sits directly above a table and a block of colour there
   competes with the data. */
.ops-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: .7rem;
  margin-bottom: 1.1rem;
}
.ops-stat {
  border: 1px solid var(--line);
  border-left: 3px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  padding: .65rem .85rem;
  display: flex;
  flex-direction: column;
  gap: .1rem;
  min-width: 0;
}
.ops-stat .n {
  font-family: var(--mono);
  font-size: 1.45rem;
  font-weight: 600;
  line-height: 1.1;
  color: var(--text);
}
.ops-stat .l { font-size: .74rem; line-height: 1.35; color: var(--faint); }
.ops-stat.good { border-left-color: var(--moss); }
.ops-stat.good .n { color: var(--moss); }
.ops-stat.bad { border-left-color: var(--alert); }
.ops-stat.bad .n { color: var(--alert); }
.ops-stat.filtered { border-left-color: var(--info); }
.ops-stat.filtered .n { color: var(--info); }
