/* ===================================================================
   common/_components.css — the components that exist on more than one
   portal, defined once.

   Before this file, each portal grew its own copy: status colours were
   forked eight ways (.chip--*, .appt-status--*, .request-status-pill--*,
   .activity-status-pill--*, .activity-mini-pill--*), and a class only
   worked on the pages that happened to link the stylesheet it lived in —
   so .chip rendered as bare text on org/notifications, .empty-state had
   no styling on three org pages, and the customer topbar's unread badge
   was styled on 2 of the 37 pages that render it.

   Load order: after common/_tokens.css (this file uses its values) and
   BEFORE the portal's own stylesheet, so a portal that genuinely needs a
   different value can still override one — contractor's tighter
   .empty-state padding is the intended kind of override.

   Only put something here when a second portal needs it. A component
   that belongs to one surface belongs in that surface's stylesheet.
   =================================================================== */

/* ── Status colour map ───────────────────────────────────────────────
   One meaning, one colour, everywhere. Add a status by pointing it at a
   bucket below, never by inventing a new colour: the same word rendering
   amber on one screen and teal on the next is how customers ended up
   seeing a scheduled visit two different colours in one session.

   Buckets, in the order a job moves through them:
     new       something has arrived, nobody has acted yet
     accent    acknowledged / open — a person has picked it up
     wait      scheduled, pending, awaiting-someone — time will resolve it
     progress  actively being worked, or money in flight
     good      finished, active, paid — the happy terminal state
     bad       failed, overdue, inactive, emergency — needs rescue
     idle      cancelled, closed — over, but not a win

   THE -fg VALUES ARE DARKENED, AND HAVE TO BE. A bucket's own shade on a
   10–15% tint of itself is not text: it is the same hue at two lightnesses,
   and four of the seven buckets failed WCAG AA in every theme —

     good  #2CB34A on #E6F6E9   2.45:1     bad   #EF4444 on #FDE9E9   3.23:1
     wait  #FFB020 on #FFF4E0   1.55:1     idle  #6E7B87 on #F0F2F3   3.71:1

   with accent failing on the default theme too (3.50:1). Two of them had
   already been noticed and answered with a pinned literal (#B87A00,
   #6B7F8C) — the right instinct, but a literal stops following a rebrand,
   and neither literal was actually dark enough.

   So each -fg mixes its own bucket colour toward --ink by the LEAST amount
   that clears 4.5:1 in all five themes. Measured, not guessed, and the
   percentages differ because the hues do: amber has to give up more than
   red to reach the same contrast. Everything still resolves from
   _tokens.css, so a rebrand lands here and stays legible — which the two
   literals could not promise.

     new 4.75  ·  accent 4.64–8.06  ·  wait 4.72–5.46
     good 4.89–5.44  ·  bad 4.60–4.86  ·  idle 4.56–4.72  ·  progress 11.55+

   Changing a bucket's colour or its tint percentage changes these, so
   re-measure rather than assuming the headroom is still there. */
:root {
  /* Re-pointed at the bucket tokens in _tokens.css. These used to be
     derivations, and two of them were wrong in a way only an org with an
     unusual brand would ever see:

       · `accent` mixed var(--teal) and var(--teal-deep) — THE ORG'S OWN
         BRAND COLOUR. A red-branded org rendered an acknowledged job in
         red, i.e. as if it had failed. The app hit this and fixed it in
         P1; this is the same fix. A scheduled job has to look the same
         in every org — that is the entire point of having a bucket map.
       · `progress` was plain var(--ink), so "In Progress" was the only
         bucket with no colour at all and read as ordinary body text.

     They also all mixed with var(--ink), which flips near-white in dark
     mode — every one of them would have washed out to a pastel. Explicit
     per-scheme values sidestep that. */
  --status-new-bg:      var(--status-new-fill);
  --status-new-fg:      var(--status-new-ink);
  --status-accent-bg:   var(--status-accent-fill);
  --status-accent-fg:   var(--status-accent-ink);
  --status-wait-bg:     var(--status-wait-fill);
  --status-wait-fg:     var(--status-wait-ink);
  --status-progress-bg: var(--status-progress-fill);
  --status-progress-fg: var(--status-progress-ink);
  --status-good-bg:     var(--status-good-fill);
  --status-good-fg:     var(--status-good-ink);
  --status-bad-bg:      var(--status-bad-fill);
  --status-bad-fg:      var(--status-bad-ink);
  --status-idle-bg:     var(--status-idle-fill);
  --status-idle-fg:     var(--status-idle-ink);
}

/* Each family keeps its own geometry (a dashboard chip is not a mobile
   pill) and takes only its colour from here. .status is the class new
   markup should use; the rest are the families already in the app. */
.status--new      { background: var(--status-new-bg); color: var(--status-new-fg); }
.status--accent   { background: var(--status-accent-bg); color: var(--status-accent-fg); }
.status--wait     { background: var(--status-wait-bg); color: var(--status-wait-fg); }
.status--progress { background: var(--status-progress-bg); color: var(--status-progress-fg); }
.status--good     { background: var(--status-good-bg); color: var(--status-good-fg); }
.status--bad      { background: var(--status-bad-bg); color: var(--status-bad-fg); }
.status--idle     { background: var(--status-idle-bg); color: var(--status-idle-fg); }

/* Bare geometry for new markup — the families above already have theirs. */
.status {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 9px;
  border-radius: 20px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.3px;
  white-space: nowrap;
  text-transform: capitalize;
}

/* ── Chip ────────────────────────────────────────────────────────────
   The dashboard status token. Was defined identically in
   org/dashboard.css and platform/dashboard.css (and a borderless third
   time in platform/organisations.css), which is why pages outside those
   three — org/notifications, contractor/notification_preferences — drew
   their chips as unstyled text. */
.chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 4px 11px;
  border-radius: 20px;
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  border: 1px solid transparent;
  transition: all 0.15s ease;
}
.chip .ti { font-size: 12px; }
/* The border is the chip's own idea (pills elsewhere are fill-only), so
   it's derived from the text colour rather than listed per status. */
/* A toned chip tints its border from its own text colour; an untoned
   one keeps the neutral border above. Keyed on the tone class now, since
   that is what carries the colour. */
.chip[class*="status--"] { border-color: color-mix(in srgb, currentColor 22%, transparent); }

/* Not a status — a vendor mark, so it keeps Stripe's own indigo, which is
   declared in common/_tokens.css beside the other vendor colours rather
   than here: the tint below is translucent, so it composites onto
   --surface and flips, and the ink needs a dark value that this sheet has
   no dark region to hold. */
.chip--stripe    { background: rgba(99,102,241,0.12); color: var(--chip-stripe-ink); }
.chip--no-stripe { background: var(--status-bad-bg);  color: var(--status-bad-fg); }

/* ── Empty state ─────────────────────────────────────────────────────
   Lived in contractor/_layout.css and platform/organisations.css only,
   so org/contractor_directory, org/supplier_invoices and org/warranties
   rendered theirs unstyled. This is the admin (roomier) version;
   contractor/_layout.css keeps its tighter override, loading later. */
.empty-state {
  text-align: center;
  padding: 60px 24px;
  color: var(--text-secondary);
}
.empty-state .ti { font-size: 40px; display: block; margin-bottom: 12px; opacity: 0.4; }
.empty-state p { margin: 0 0 20px; font-size: 14px; }

/* ── Unread badge ────────────────────────────────────────────────────
   Rendered by the customer/tenant topbar on every page, but defined in
   customer/notifications.css — so on 35 of 37 pages the count sat in the
   topbar as a loose number instead of a badge. */
.bell-badge {
  position: absolute;
  top: 2px;
  inset-inline-end: 2px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--danger-fill);
  color: var(--white);
  font-size: 10px;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  /* It sits on top of the bell's own link — clicks belong to the bell. */
  pointer-events: none;
}

/* ── A sidebar row's count ───────────────────────────────────────────
   The admin shells' "needs attention" count (api/core/org_nav_badges.py,
   live-bumped by org_realtime_toasts.js) and the contractor rail's unread
   count (nav_bubbles.js) — one pill at a row's end. */
.admin-nav-badge,
.side-nav-badge {
  flex-shrink: 0;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--danger-fill);
  color: var(--white);
  font-size: 11px;
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* Both flash partials render this span; only the admin/contractor sheets
   used to style it. */
.flash-text { flex: 1; }

/* The optional leading icon (partials/_flash_messages.html renders it
   when a layout opts in with flash_icons) — defined here because the
   partial is shared, so every portal that might opt in resolves it. */
.flash-icon { font-size: 18px; flex-shrink: 0; }

/* ── Focus ring ──────────────────────────────────────────────────────
   org/_layout.css and platform/_layout.css carried no focus styling at
   all, and no stylesheet used :focus-visible — a keyboard user had
   nothing to follow. Inputs are left alone: they already answer focus
   with a border colour change, and every portal suppresses their outline
   deliberately. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[role="button"]:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--teal);
  outline-offset: 2px;
  border-radius: 4px;
}

/* ── Shared page and card furniture ──────────────────────────────────
   Every one of these is rendered by more than one portal but used to be
   defined in exactly one, so the others rendered it bare: .page-header-left
   was platform-only while 24 org pages used it, .topbar-actions was
   contractor-only while the tenant topbar used it (and had been patched
   over with an inline flex), .card-title/.card-sub/.page-title had no
   definition anywhere the customer portal loads.

   These are the neutral versions. A portal that wants its own — the
   contractor app runs a tighter type scale, the customer app a rounder
   one — keeps its rule in its own stylesheet, which loads after this. */
/* The neutral one. .page-title/.page-sub have been shared for a while but
   their container was only ever defined in the contractor, org and platform
   sheets — so on a customer or tenant page .page-header styled nothing at
   all, which is how the notifications list ended up depending on a
   redefinition inside css/customer/notifications.css. Each of those portals
   keeps its own margin below (org and platform want 28px, platform wants a
   row); this is the value the rest inherit. */
.page-header { margin-bottom: 18px; }
.page-header-left { flex: 1; min-width: 0; }

.page-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 22px;
  color: var(--ink);
  margin: 0 0 4px;
}
.page-sub { font-size: 13.5px; color: var(--text-secondary); margin: 0; }

.card-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 15px;
  color: var(--ink);
  margin: 0 0 4px;
}
.card-sub { font-size: 13px; color: var(--text-secondary); margin: 0 0 20px; line-height: 1.5; }

/* A card's header row — title on the left, status badge hard right. Two
   portals' evidence panels used it and no stylesheet defined it, so the
   badge dropped onto its own line under the title on all three pages. */
.card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 6px;
}
.card-head .card-title { margin-bottom: 0; }

.section-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 14px;
  color: var(--ink);
  margin: 24px 2px 10px;
}
.section-title:first-child { margin-top: 2px; }

/* Back affordances: .page-back sits inside the content (admin), .topbar-back
   in the app bar (contractor), .subpage-back beside a subpage title
   (customer/tenant). Three placements, so three classes — but all three were
   stranded in one portal's stylesheet. */
/* text-decoration on all three: they're icon-only <a>s, and the contractor
   portal has no blanket `a { text-decoration: none }` the way the customer
   and tenant shells do — so without it the browser draws a line straight
   through the glyph. That's what put a bar under the topbar bell. */
.page-back {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  margin-block: 0 6px; margin-inline: -4px 0;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  font-size: 17px;
  text-decoration: none;
}
.page-back:hover { background: var(--paper, var(--bg-subtle)); color: var(--ink); }

.topbar-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
/* A back arrow beside the title, not stranded on a line of its own above
   it — which is where 18 pages had put it (each with its own copy of
   `style="position:static; display:inline-flex; margin:0 0 8px -8px"`),
   while the customer and tenant portals seated theirs in the header row.
   Two contractor pages had already hand-rolled this layout with inline
   flex, which is a fair sign of which one was wanted.

   Grid rather than flex so it works whatever the header holds: the arrow
   takes column 1, and everything else — title, sub, a status chip — stacks
   down column 2, so the arrow lines up with the TITLE rather than the
   centre of the whole block. */
.page-header--with-back {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  column-gap: 6px;
}
.page-header--with-back > .topbar-back { grid-column: 1; grid-row: 1; margin-inline-start: -8px; }
.page-header--with-back > :not(.topbar-back) { grid-column: 2; }
.topbar-back {
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  color: var(--ink);
  font-size: 20px;
  text-decoration: none;
  flex-shrink: 0;
}
.topbar-back:hover { background: var(--paper, var(--bg-subtle)); }

/* ── The notifications list ────────────────────────────────────────────
   All three portals answer their bell with the same screen: the list, and
   one Preferences link out to the per-channel settings. It was
   customer-only (css/customer/notifications.css), so the tenant's and
   contractor's bells had nowhere to go but the settings page — a bell that
   opened a form and never showed you a notification.

   Hexes became tokens on the way in: #1a1a1a/#666/#aaa/#ccc/#fafafa were a
   second grey scale beside the real one, and a shared sheet is the last
   place that should carry one. */
.page-header--row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.notif-prefs-link {
  font-size: 13px;
  color: var(--accent-deep);
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
}
.notif-list { padding: 0 0 8px; }
.notif-item {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
  background: var(--white);
  color: var(--ink);
  text-decoration: none;
  position: relative;
  transition: background 0.15s;
}
.notif-item:last-child { border-bottom: none; }
.notif-item:hover { background: var(--paper); }
.notif-item--linked { cursor: pointer; }
.notif-icon-wrap {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 12%, var(--white));
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 2px;
}
.notif-icon { font-size: 16px; color: var(--accent); }
.notif-body { flex: 1; min-width: 0; }
.notif-title {
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
  margin: 0 0 4px;
  line-height: 1.4;
}
.notif-sub {
  font-size: 13px;
  color: var(--text-secondary);
  margin: 0 0 4px;
  line-height: 1.4;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.notif-time { font-size: 12px; color: var(--text-tertiary); margin: 0; }
.notif-chevron {
  flex-shrink: 0;
  color: var(--border);
  display: flex;
  align-items: center;
  padding: 4px;
  margin-top: 4px;
  font-size: 16px;
}
/* Unread is the row's own state, not a badge bolted onto it: the list is
   where you go to read them, so it marks everything read on open and the
   tint is the record of what was new when you arrived. */
.notif-item--unread { background: color-mix(in srgb, var(--accent) 6%, var(--white)); }
.notif-item--unread .notif-title { font-weight: 600; }

/* ── Notification preference rows ──────────────────────────────────────
   A group heading over switch rows: label, one line of explanation, and the
   control on the right. All three portals' preference pages are built from
   these, and two of them had their own copy — contractor/_layout.css and
   customer/notifications.css, drifted on every value (42 vs 44px track, 20
   vs 18px thumb, radius 24 vs 12, --mist vs #ddd, label weight 600 vs 500,
   and the customer's hid the row icon outright). The tenant page had no copy
   at all, so it linked the customer portal's stylesheet from the tenant
   portal to borrow one. This is the contractor's, which was the version
   written in tokens.

   Padding assumes the rows sit inside an ordinary padded .card. The customer
   page's unpadded full-bleed card adds its own inset — see
   css/customer/notifications.css. */
.pref-section { border-top: 1px solid var(--mist); }
.pref-section:first-child { border-top: none; }
.pref-section-title {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--text-secondary);
  margin: 14px 2px 0;
}
.pref-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 2px;
  cursor: pointer;
}
.pref-toggle-label {
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
}
.pref-toggle-label .ti { color: var(--accent-deep); margin-inline-end: 6px; }
.pref-toggle-sub { font-size: 12px; font-weight: 400; color: var(--text-secondary); }
.pref-checkbox { position: absolute; opacity: 0; width: 0; height: 0; }
.pref-switch {
  position: relative;
  flex-shrink: 0;
  width: 42px;
  height: 24px;
  border-radius: 24px;
  background: var(--mist);
  transition: background 150ms ease;
  cursor: pointer;
}
.pref-switch::after {
  content: "";
  position: absolute;
  top: 2px;
  inset-inline-start: 2px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--white);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
  transition: transform 150ms ease;
}
.pref-checkbox:checked + .pref-switch { background: var(--accent); }
.pref-checkbox:checked + .pref-switch::after { transform: translateX(18px); }
/* Switches ship disabled and pref_auto_save.js enables them, so this is what
   you see when the script that saves them hasn't arrived — dimmed and inert,
   rather than a control that moves and stores nothing. Normally it's on
   screen for the moment between parse and the deferred script running. */
.pref-checkbox:disabled + .pref-switch { opacity: 0.45; }
.pref-toggle:has(.pref-checkbox:disabled) { cursor: default; }

/* ── The "More" menu ───────────────────────────────────────────────────
   A card of tappable rows: accent icon, label over a one-line explanation,
   chevron. The customer and tenant portals both end their tab bar on a
   More screen built out of these, and each had grown its own version —
   tenant/.menu-row (two lines, accent icons) and customer/.profile-menu-item
   (one line, grey icons), so the same menu read as two different products
   depending on which login you held. This is the tenant's, promoted.

   Works as an <a> or a <button>: install-app is a button on both screens,
   and every reset a button needs is spelled out below. */
.menu-card {
  background: var(--white);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  overflow: hidden;
  padding: 0;
}
.menu-row {
  display: flex;
  align-items: center;
  gap: 13px;
  width: 100%;
  min-height: 58px;
  text-align: start;
  background: none;
  border: none;
  border-bottom: 1px solid var(--mist);
  padding: 8px 18px;
  cursor: pointer;
  font-family: var(--font-body);
  color: var(--ink);
  text-decoration: none;
}
.menu-row:last-child { border-bottom: none; }
.menu-row:hover { background: var(--paper); }
.menu-row > .ti {
  font-size: 21px;
  color: var(--accent-deep);
  width: 24px;
  text-align: center;
  flex-shrink: 0;
}
.menu-row-body { flex: 1; min-width: 0; }
.menu-row-label { display: block; font-size: 14.5px; font-weight: 600; }
.menu-row-sub { display: block; font-size: 12px; color: var(--text-secondary); }
.menu-row > .ti-chevron-right { color: var(--border); font-size: 16px; width: auto; }
.menu-row--danger { color: var(--danger-deep); }
.menu-row--danger > .ti { color: var(--danger-deep); }

/* ── The Appearance picker ────────────────────────────────────────────
   Rendered by partials/_appearance.html on all five More/settings
   screens, so it lives here per CLAUDE.md §2 rather than in any one
   portal's sheet.

   .menu-row--static is the row that is not a link. Every other .menu-row
   is an <a> going somewhere; this one holds a control, so it drops the
   pointer cursor and the hover fill — a row that lights up under the
   mouse but only responds on three small targets inside it reads as
   broken.

   The segmented control is the dark-chip pattern §5 reserves for exactly
   this: a small set of mutually exclusive options that are not a
   status filter. It is deliberately NOT .appt-tabs, which is the
   underline bar for filtering a list. */
.menu-row--static { cursor: default; }
.menu-row--static:hover { background: none; }

.appearance-seg {
  display: inline-flex;
  flex-shrink: 0;
  gap: 2px;
  padding: 2px;
  border-radius: 999px;
  background: var(--mist);
}

.appearance-opt {
  border: none;
  background: none;
  cursor: pointer;
  padding: 5px 11px;
  border-radius: 999px;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 600;
  color: var(--text-secondary);
  transition: background 120ms ease, color 120ms ease;
}
.appearance-opt:hover { color: var(--ink); }
.appearance-opt:focus-visible { outline: 2px solid var(--accent, var(--teal)); outline-offset: 2px; }

/* --white, not --hero-ground: the selected chip is a raised surface on a
   grey track and has to inVERT with the scheme, which is the opposite of
   what the hero family does. In dark that is the card colour lifted off
   the darker track, and --ink comes up to near-white to sit on it. */
.appearance-opt--active {
  background: var(--white);
  color: var(--ink);
  box-shadow: 0 1px 2px color-mix(in srgb, var(--bg-subtle) 35%, transparent);
}

/* ── The Appearance picker, sidebar copy ──────────────────────────────
   The modifier for drawing the control on a permanently-dark rail: the
   org and platform shells' .admin-sidebar, and the contractor desk's
   .side-nav from 900px (§17.8 S6). Those rails are the hero ground, dark
   in BOTH schemes, so the control cannot inherit the page's track and
   chip — --mist and --white flip with the scheme and would render a light
   rectangle on a navy column either way round. It uses the same
   rgba-white chrome the rest of those rails are built from instead.

   Here rather than in _shell.css because three shells load this sheet and
   only two load that one — the contractor shell keeps its own chrome and
   would otherwise have needed the whole .admin-* sheet for one control.

   Stacked, not a single row: label + Auto/Light/Dark side by side is
   wider than a 220–240px rail, so Dark was clipped by the main column's
   edge. The chips stretch to fill the rail the same way the 480px
   More-screen copy below does. */
.appearance-sidebar {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
  padding: 10px 20px 4px;
}

.appearance-sidebar-label {
  font-size: 11.5px;
  font-weight: 600;
  color: var(--hero-ink-muted);
  white-space: nowrap;
}

.appearance-seg--sidebar {
  display: flex;
  width: 100%;
  background: rgba(255,255,255,0.10);
}
.appearance-seg--sidebar .appearance-opt {
  flex: 1;
  padding: 4px 9px;
  font-size: 11px;
  color: var(--hero-ink-muted);
}
.appearance-seg--sidebar .appearance-opt:hover { color: var(--hero-ink); }
.appearance-seg--sidebar .appearance-opt--active {
  background: var(--hero-ink);
  color: var(--hero-ground);
  box-shadow: none;
}

/* ── The reader's time zone (§17.49 S3), beside Appearance ─────────────
   A native <select>: ~430 zones is a list the platform already knows how
   to scroll and type-ahead through, on every device. The rail copy takes
   the same rgba-white chrome as the Appearance chips next to it, and
   `color-scheme: dark` so the OPEN list is drawn dark too — the options
   are the platform's, not ours, and would otherwise be white-on-white. */
.zone-select {
  max-width: 100%;
  min-width: 0;
  padding: 5px 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: 12px;
}
.zone-select--row { max-width: 55%; }
/* The org's own "You" settings page: a card, not a row. */
.zone-select--card { width: 100%; max-width: 360px; font-size: 14px; }
.zone-select--sidebar {
  width: 100%;
  border-color: transparent;
  background: rgba(255,255,255,0.10);
  color: var(--hero-ink);
  color-scheme: dark;
}
.zone-select:focus-visible { outline: 2px solid var(--accent, var(--teal)); outline-offset: 2px; }

@media (max-width: 480px) {
  /* Three chips plus a two-line label do not fit a phone row, so the
     control drops below the label rather than squeezing the words. */
  .menu-row--static { flex-wrap: wrap; }
  .appearance-seg { width: 100%; justify-content: space-between; }
  .appearance-opt { flex: 1; }
}

/* Signing out is one POST route for every portal now (index.logout), so
   every "Sign out" control is a form. display:contents keeps the button
   itself as the layout child, which is what both placements need: inside
   .menu-card the row must still match .menu-row:last-child, and inside
   .sidebar-footer it must still be the flex item. Here rather than in a
   portal sheet because all four portals render one. */
.logout-form { display: contents; }

.subpage-header { display: flex; align-items: center; gap: 12px; }
.subpage-header h1 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 22px;
  color: var(--ink);
  margin: 0;
}
.subpage-back {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink);
  border-radius: var(--radius-sm);
  flex-shrink: 0;
  font-size: 20px;
  text-decoration: none;
}
.subpage-back:hover { background: var(--mist, var(--mist)); }

/* ── Form furniture ──────────────────────────────────────────────────
   .form-hint and .field-hint are the same component under two names —
   78 uses of one, 72 of the other, split roughly by portal, each invisible
   on the portals that only knew the other name. New markup should use
   .form-hint; .field-hint stays an alias because renaming 72 call sites
   across five portals is churn without a user-visible payoff. */
.form-hint,
.field-hint {
  margin: 5px 0 0;
  font-size: 12px;
  color: var(--text-secondary);
  line-height: 1.4;
}
.form-hint code,
.field-hint code {
  background: color-mix(in srgb, var(--ink) 6%, transparent);
  border-radius: 4px;
  padding: 1px 5px;
  font-size: 11.5px;
  font-family: ui-monospace, monospace;
}
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
/* Parts entry on a billing/correction form (org/_job_cards.html,
   contractor/job_detail.html's correction dialog) — one row per part with
   its own arithmetic shown beside it, where the page renders one. Common
   because both portals now build a part-row form; it started org-only in
   org/services.css. */
.billing-part-row {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    align-items: center;
    margin-bottom: 6px;
}
.billing-part-row [name="part_description"] { flex: 2 1 140px; }
.billing-part-row [name="part_quantity"] { flex: 0 1 70px; }
.billing-part-row [name="part_unit_cost"] { flex: 1 1 110px; }
.billing-part-row [data-part-line-total] { flex: 1 0 auto; margin: 0; text-align: end; }
.billing-part-del {
    flex: 0 0 auto;
    border: none;
    background: none;
    color: var(--text-secondary);
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
    padding: 6px;
}
.billing-part-del:hover { color: var(--danger); }
.form-control--sm { padding: 9px 10px; font-size: 14px; }

/* Procurement rows keep the part brief readable while the irreversible
   supplier decision sits in its own compact action panel. */
.materials-order-queue-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px;
    padding: 12px 0;
}
.materials-order-queue-row--ruled { border-bottom: 1px solid var(--border); }
.materials-order-queue-detail { min-width: 0; flex: 1 1 280px; }
.materials-order-place-form {
    display: flex;
    align-items: end;
    /* Wraps because the send-to-customer form has one control more than
       the place-order form — two fields and a button inside 360px left
       each field about 110px, which clips "0.00" behind the number
       spinner and folds the button's label. Wrapping costs the
       two-control form nothing and gives the three-control one a second
       line instead of three crushed columns. */
    flex-wrap: wrap;
    gap: 8px;
    flex: 0 1 360px;
    min-width: 280px;
    padding: 9px;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--bg-subtle);
}
/* A basis rather than a bare flex:1 — `flex: 1` is `1 1 0%`, which lets a
   field shrink past the width of its own control. */
.materials-order-place-field { min-width: 0; flex: 1 1 140px; }
.materials-order-place-hint {
    flex-basis: 100%;
    margin: 0;
    color: var(--text-secondary);
    font-size: 11.5px;
    line-height: 1.4;
}
.materials-order-place-field label {
    display: block;
    margin-bottom: 4px;
    color: var(--text-secondary);
    font-size: 12px;
    font-weight: 700;
}
.materials-order-place-field .form-control { width: 100%; margin: 0; }
.materials-order-place-button { white-space: nowrap; }
.materials-order-place-button .ti { margin-inline-end: 4px; }
.materials-order-place-empty {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--text-secondary);
    font-size: 13px;
}
@media (max-width: 640px) {
    .materials-order-queue-row { align-items: stretch; flex-direction: column; }
    .materials-order-place-form { width: 100%; min-width: 0; }
}
/* The asterisk beside a required label, and the quiet "(optional)" note. */
.required { color: var(--danger); }
.form-optional,
.optional { font-weight: 400; color: var(--text-secondary); font-size: 11px; }

/* ── Disclosure section inside a card ────────────────────────────────
   The service catalogue's inner accordion, also rendered by the
   contractor client page. See accordion.js for the open/close behaviour. */
.svc-inner-details {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.svc-inner-body { padding: 14px; border-top: 1px solid var(--border); background: var(--white); }
.svc-inner-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 9px 14px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-secondary);
  cursor: pointer;
  list-style: none;
  background: var(--paper, var(--bg-subtle));
  user-select: none;
}
.svc-inner-toggle::-webkit-details-marker { display: none; }
.svc-inner-toggle:hover { color: var(--teal-deep); }
.svc-page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
}
.svc-badge {
  display: inline-flex;
  align-items: center;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.3px;
  padding: 2px 8px;
  border-radius: 20px;
}
.svc-badge--active { background: var(--status-good-bg); color: var(--status-good-fg); }
.svc-badge--hidden { background: var(--status-idle-bg); color: var(--status-idle-fg); }

/* ── Tab bar ─────────────────────────────────────────────────────────
   The underline tab row: exclusive status/filter bars app-wide (dark
   chips are for toggles and segmented controls, a different thing). org
   and platform each had a byte-identical copy apart from the active
   colour, so this one is written against --accent and each portal's own
   alias colours it. */
.appt-tabs {
  display: flex;
  gap: 6px;
  margin: 20px 0;
  border-bottom: 1px solid var(--border);
  /* On a narrow screen a full tab row scrolls rather than wrapping into
     a second line that pushes the content down. */
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.appt-tabs::-webkit-scrollbar { display: none; }
.appt-tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 14px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-secondary);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  white-space: nowrap;
}
.appt-tab:hover { color: var(--ink); }
.appt-tab--active { color: var(--accent-deep, var(--teal-deep)); border-bottom-color: var(--accent, var(--teal)); }
.appt-tab-count {
  font-size: 11px;
  font-weight: 700;
  background: var(--paper, var(--bg-subtle));
  color: var(--text-secondary);
  border-radius: 20px;
  padding: 1px 7px;
}
.appt-tab--active .appt-tab-count {
  background: color-mix(in srgb, var(--accent, var(--teal)) 14%, transparent);
  color: var(--accent-deep, var(--teal-deep));
}

/* Title and subtitle inside an .empty-state, so a portal doesn't have to
   invent .wizard-empty-title to say the same thing. */
.empty-state-title {
  margin: 0 0 6px;
  font-size: 16px;
  font-weight: 700;
  color: var(--ink);
  font-family: var(--font-display);
}
.empty-state-sub { margin: 0; font-size: 13.5px; color: var(--text-secondary); }

/* ── Inline alert ────────────────────────────────────────────────────
   A banner inside the page (the flash stack is for messages about an
   action that just happened). Six of these were already in the markup —
   supplier invoice review on both portals, the materials panel, data
   plate review — carrying Bootstrap's class names, which this app has
   never had, so each one rendered as an unboxed paragraph. */
.alert {
  padding: 10px 14px;
  border-radius: var(--radius-md);
  border: 1px solid transparent;
  font-size: 13px;
  line-height: 1.5;
}
.alert-warning {
  background: var(--status-wait-bg);
  color: var(--status-wait-fg);
  border-color: color-mix(in srgb, var(--warning) 30%, transparent);
}
.alert-info {
  background: var(--status-new-bg);
  color: var(--status-new-fg);
  border-color: color-mix(in srgb, var(--secondary-blue) 25%, transparent);
}
.alert-danger {
  background: var(--status-bad-bg);
  color: var(--status-bad-fg);
  border-color: color-mix(in srgb, var(--danger) 30%, transparent);
}
/* .alert carries no margins — the six banners that predate this one all sit in
   a container that spaces them. A login form has none, and the two portals
   that render one space their fields differently (contractor's .form-group has
   margin-bottom, customer's is a flex column with a gap), so the error needs
   to carry its own or it butts against the first label. */
.alert--form { margin-bottom: 14px; }

/* ── "Stripe isn't connected" prompt ─────────────────────────────────
   partials/_stripe_not_ready.html, rendered wherever money is about to
   happen for an owner that cannot take it: org Settings → Payments (both
   the Stripe card and the Invoicing one), the job invoice review card,
   and the contractor's own Payouts page. Three portals, so it lives here
   (§2) rather than in any one _layout.css.

   Only the layout — the colour is `.alert-warning` above, so this prompt
   and every other inline banner stay one component. */
.stripe-not-ready {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  margin-top: 12px;
}
.stripe-not-ready-line { margin: 0; }

/* ── Password field helpers ──────────────────────────────────────────
   The strength meter and the show/hide toggle, rendered by every
   portal's sign-in and password-reset pages.

   The whole component, not half of it. This block used to hold three
   rules — weak, good, and the toggle — while `.strength-bar`, the other
   two fill states and every `.strength-text` state sat duplicated in
   common/_shell.css and customer/login.css. The two copies had already
   drifted: admin filled the bar with --strength-fair/--strength-strong
   and customer with --amber/--green, so one meter said #F0A000 and the
   other #FFB020 for the same password. The purpose-named pair wins here
   — and #2E7D32 over #2CB34A is the readable direction for the text
   states, which sit on white.

   Splitting a component across sheets also hid a real bug: an org
   password-reset page is a standalone document and did not load this
   file at all, so its eye toggle lost its absolute positioning and its
   bar had no weak or good state. `unstyled` cannot see that — every one
   of those class NAMES is defined by some sheet the page does load. */
.toggle-password { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); font-size: 17px; color: var(--text-secondary); cursor: pointer; user-select: none; }
.strength-bar { height: 4px; border-radius: 2px; background: var(--mist); margin-top: 8px; overflow: hidden; }
.strength-fill { height: 100%; width: 0%; border-radius: 2px; background: var(--strength-fair); transition: width 200ms ease, background 200ms ease; }
.strength-fill.weak   { width: 25%;  background: var(--danger-muted); }
.strength-fill.fair   { width: 50%;  background: var(--strength-fair); }
.strength-fill.good   { width: 75%;  background: var(--sky-light); }
.strength-fill.strong { width: 100%; background: var(--strength-strong); }
.strength-text { display: block; margin-top: 5px; font-size: 11.5px; color: var(--text-secondary); }
.strength-text.weak   { color: var(--danger-muted); }
.strength-text.fair   { color: var(--strength-fair); }
.strength-text.good   { color: var(--sky); }
.strength-text.strong { color: var(--strength-strong); }
.match-text { display: block; margin-top: 6px; font-size: 11.5px; }
.match-text.match    { color: var(--strength-strong); }
.match-text.no-match { color: var(--danger-muted); }

/* ── Before-we-visit evidence ────────────────────────────────────────
   partials/_evidence_form.html asks the questions (customer and tenant);
   org/_srd_evidence.html reports the answers back to whoever dispatches.
   The form shipped with ten class names and not one rule anywhere, so
   the questions rendered as a bare run of labels and inputs — the one
   screen whose whole job is to look answerable. */
.evidence-item {
  padding: 14px 0;
  border-top: 1px solid var(--border);
}
.evidence-item:first-of-type { border-top: 0; padding-top: 4px; }
.evidence-label {
  display: block;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink);
  margin-bottom: 6px;
}
.evidence-tick { color: var(--success); margin-inline-start: 4px; }
.evidence-answered {
  margin: 0 0 8px;
  font-size: 12.5px;
  color: var(--success);
}
/* The photo ask is a camera button, not a bare file control — same
   reasoning as the choices below: a phone, one hand, in front of the
   appliance. The input itself is [hidden]; the label opens the picker. */
.evidence-photo-btn {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 14px 16px;
  background: var(--paper, var(--bg-subtle));
  border: 1.5px dashed var(--border);
  border-radius: var(--radius-md);
  cursor: pointer;
  color: var(--text-secondary);
  font-size: 13.5px;
  font-weight: 500;
  transition: border-color 0.15s;
}
.evidence-photo-btn:hover { border-color: var(--accent, var(--teal)); }
.evidence-photo-btn .ti { font-size: 20px; color: var(--accent, var(--teal)); }

/* Intake photos on customer/tenant request detail: one strip, add tile
   sized like a thumbnail — not a second gallery + full-width Upload CTA. */
.sr-photo-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 8px 0 0;
}
.sr-photo-strip img {
  width: 56px;
  height: 56px;
  object-fit: cover;
  border-radius: 6px;
  border: 1px solid var(--border);
  display: block;
}
.sr-photo-add-form { margin: 0; }
.sr-photo-add {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  width: 56px;
  height: 56px;
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  background: var(--paper, var(--bg-subtle));
  border: 1.5px dashed var(--border);
  border-radius: 6px;
  cursor: pointer;
  color: var(--text-secondary);
  font-size: 11px;
  font-weight: 600;
  line-height: 1.1;
  transition: border-color 0.15s;
}
.sr-photo-add:hover { border-color: var(--accent, var(--teal)); }
.sr-photo-add .ti {
  font-size: 18px;
  color: var(--accent, var(--teal));
}
.sr-photo-add-text { color: var(--text-secondary); }

/* Yes/No is two tap targets, not two radio dots: this is answered on a
   phone, often one-handed, by someone standing in front of the appliance. */
.evidence-choices { display: flex; gap: 10px; }
.evidence-choice {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-size: 14px;
  cursor: pointer;
}
.evidence-choice:has(input:checked) {
  border-color: var(--accent, var(--teal));
  background: color-mix(in srgb, var(--accent, var(--teal)) 8%, transparent);
}
.evidence-footer { margin-top: 18px; }
.evidence-footer .form-hint { margin-top: 10px; }

/* A checkbox and its sentence, as one row and one tap target. Three
   portals had three names for it — .checkbox-row (contractor),
   .form-group--check (org services), .form-check-label (org products) —
   and a rule behind none of them, so all three rendered as a 13px box
   with text running under it. */
.checkbox-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 13.5px;
  line-height: 1.45;
  cursor: pointer;
}
.checkbox-row input[type="checkbox"],
.checkbox-row input[type="radio"] { width: 17px; height: 17px; flex-shrink: 0; margin-top: 1px; }

/* Not a status — an identifier (a plan name, a domain, a provider). Takes
   the idle bucket so it reads as a fact rather than a state. */
.chip--id { background: var(--status-idle-bg); color: var(--status-idle-fg); }

/* ── Rules two families were each keeping their own copy of ──────────
   Byte-identical in common/_shell.css (org + platform) and one of
   contractor/_layout.css or customer/_layout.css — which couldn't just
   read _shell.css, because it's the admin sheet and they don't load it.
   This file loads before all three, so a portal that genuinely wants a
   different value still states it and still wins. */
.flash--success { background: var(--success-fill); color: var(--white); }
.flash--danger  { background: var(--danger-fill);  color: var(--white); }

.btn-danger { background: var(--danger-fill); color: var(--white); }
/* The fill is already --danger-deep in light, so a hover steps toward the
   ink: darker in light, lighter in dark, and its label gains either way. */
.btn-danger:not(:disabled):hover { background: color-mix(in srgb, var(--danger-fill) 85%, var(--ink)); }
.btn-primary:not(:disabled):hover { background: var(--accent-deep); }

.form-group label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  margin-bottom: 6px;
}

input[type="file"]:not([hidden]) {
  display: block;
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--text-secondary);
}

/* A file input the page has replaced with its own control — the
   `.file-pick-btn` <label> opens the picker, so the native input must not
   render beside it.

   It has to carry the type selector. The rule above is (0,2,1), a bare
   `.file-input-hidden` is (0,1,0), and specificity does not care which
   sheet loaded last — so the class lost, and every branding upload row
   shipped TWO controls: the styled "Choose file…" button and the browser's
   own "Choose file / No file chosen" next to it. This is the shape
   `unstyled` structurally cannot see, the same as `btn-base`: the class IS
   defined in a sheet the page loads, and is overruled anyway.

   Here rather than in a portal sheet because the rule it has to outrank is
   here (§2) — a copy of this living in org/settings.css is a copy that
   silently stops working the day this file changes, which is how it got
   found. Ties on specificity and wins on order, so the two must stay in
   this order and in this file. */
input[type="file"].file-input-hidden { display: none; }

/* The chevron is a data: URI rather than a token, so it can't theme — it
   is the one place a literal grey is unavoidable in this file. */
select.form-control {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'%3E%3Cpath fill='%236B7F8C' d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  padding-inline-end: 32px;
}

/* The plan comparison grid — the customer signup funnel and the platform
   plan editor render the same table of the same features. */
.feature-matrix-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13.5px;
}

/* ===================================================================
   One dialog

   CLAUDE.md rule 11 sends every Add control here, and a rule that says
   "use a dialog" without a dialog to use is a rule that produces one
   more spelling per page. There were already five — .plans-modal,
   .job-dispatch-modal, .job-hold-modal, .insp-detail-modal and
   .pricing-confirm — and comparing them is what this block is: they
   agreed on nearly everything (width: min(94vw, N), margin auto,
   padding 0, no border, that same 20px/50px shadow, a 28px round close
   button pinned top-right, a blurred scrim) and disagreed only on the
   width, the radius and the scrim's opacity. So the width is the knob
   and the rest is settled.

   In _components.css because it is used by more than one portal, which
   is what that means — and because every portal layout already links
   this file, so a dialog cannot render unstyled on the one page nobody
   checked.

   **The tokens below are chosen, not reached for.** --radius-lg,
   --font-display, --paper and --mist are declared by exactly three
   sheets — contractor/_layout, customer/_layout and common/_shell — so a
   component that must work on every portal cannot read them bare: on a
   tenant page nothing declares them and the rule renders off nothing.
   The ones without a fallback here (--ink, --border, --bg-subtle,
   --text-secondary) are declared in _tokens.css, which loads before this
   file everywhere. The two that carry a fallback are CLAUDE.md's
   sanctioned hedge, for the same reason it names: a shared partial draws
   on shells that do not all declare the same palette.

   Markup: partials/_modal.html. Behaviour: static/js/modal.js. Neither
   is optional — `showModal()` is what makes a <dialog> modal, and a
   <dialog> shown any other way is a div with a bad reputation.
   =================================================================== */

.modal {
  /* The knob. 460 suits a form of a few fields; the variants below are
     the two other sizes the existing five had settled on. */
  width: min(94vw, var(--modal-w, 460px));
  max-height: 88vh;
  margin: auto;
  padding: 0;
  border: none;
  border-radius: var(--radius-lg);
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
}

.modal--narrow { --modal-w: 360px; }
.modal--wide   { --modal-w: 560px; }

/* Conversation is an on-demand tool, not permanent page furniture. The
   launcher stays at the screen edge while the request remains readable;
   related threads can stack by setting --conversation-offset. */
.conversation-launcher {
  position: fixed;
  inset-inline-end: 18px;
  bottom: calc(20px + env(safe-area-inset-bottom, 0px) + var(--conversation-offset, 0px));
  z-index: 40;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 1px;
  width: 48px;
  height: 48px;
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: 999px;
  background: var(--white, var(--bg-subtle));
  box-shadow: 0 4px 14px color-mix(in srgb, var(--ink) 14%, transparent);
  color: var(--accent-deep);
  font-family: var(--font-body, inherit);
  font-size: 0;
  font-weight: 700;
  cursor: pointer;
}
.conversation-launcher .ti { font-size: 19px; }
.conversation-launcher .ti + .ti { font-size: 15px; margin-inline-start: -5px; align-self: flex-end; margin-bottom: 9px; }
.conversation-launcher:hover, .conversation-launcher:focus-visible { background: color-mix(in srgb, var(--accent) 7%, var(--white, var(--bg-subtle))); outline: none; }
.modal--conversation { --modal-w: 560px; }
.modal--conversation .modal-inner { padding-bottom: 20px; }
@media (max-width: 640px) {
  .conversation-launcher { bottom: calc(76px + env(safe-area-inset-bottom, 0px) + var(--conversation-offset, 0px)); }
  .modal--conversation { width: min(100vw, var(--modal-w)); max-height: 92vh; margin: auto auto 0; border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
  .modal--conversation .modal-inner { max-height: 92vh; }
}

/* Tinted from --ink rather than a fourth hand-mixed rgba: the three that
   existed used 0.45, 0.45 and 0.55, which is not a decision anybody made
   twice. */
.modal::backdrop {
  background: color-mix(in srgb, var(--ink) 45%, transparent);
  backdrop-filter: blur(3px);
}

/* The scroll lives here, not on the <dialog>, so the close button can be
   pinned against the dialog's edge and stay put while the body scrolls. */
.modal-inner {
  position: relative;
  padding: 22px 24px 24px;
  overflow-y: auto;
  max-height: 88vh;
}

.modal-close {
  position: absolute;
  top: 14px;
  inset-inline-end: 14px;
  width: 28px;
  height: 28px;
  border: none;
  border-radius: 50%;
  background: var(--bg-subtle);
  color: var(--text-secondary);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}
.modal-close:hover { background: var(--border); color: var(--ink); }

/* Padded clear of the close button, so a long title wraps above it
   rather than under it. */
.modal-title {
  margin: 0 0 4px;
  padding-inline-end: 34px;
  font-family: var(--font-display, inherit);
  font-size: 17px;
}

.modal-sub {
  margin: 0 0 16px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-secondary);
}

/* Confirm on the right, and anything destructive at the other end of the
   row — rule 10 does not stop applying because the buttons are in a
   dialog. */
.modal-actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 10px;
  margin-top: 18px;
}
.modal-actions--split { justify-content: space-between; }

/* Mobile and tablet dialogs are sheets, regardless of whether a page uses
   the shared macro or a specialised native dialog. Desktop keeps the
   centred dialog. The first child is the established inner wrapper on all
   dialogs and becomes the scroll region, keeping close/actions reachable. */
@media (max-width: 768px) {
  dialog {
    width: 100vw !important;
    max-width: 100vw !important;
    max-height: 94dvh !important;
    margin: auto auto 0 !important;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0 !important;
  }
  dialog > :first-child {
    max-height: 94dvh;
    overflow-y: auto;
  }
}

/* ── Document style picker ────────────────────────────────────────────
   Two radio grids — layout and palette — for the printed documents an org
   or a contractor sends (api/core/branding.py's DOC_LAYOUTS/DOC_PALETTES,
   rendered by common/print_document.css).

   Here rather than in org/settings.css because BOTH portals render it:
   the org picks the style of its work orders and remittance advices, the
   contractor picks the style of its invoices. org/settings.css already
   owns a nearly identical control (.theme-option, for the portal theme)
   and that one is correctly org-only — a contractor has no portal theme.
   Copying it across would have been the second spelling of one idea, and
   using it from the contractor page would have rendered an unstyled row,
   which is what the `unstyled` check exists to catch.

   The palette swatches and the layout wireframes are deliberately
   different SHAPES, not two rows of the same chip: the two pickers are
   independent axes and the control should say so before the labels do. */
.docstyle-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 14px;
}

.docstyle-option {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  cursor: pointer;
}
/* The options are <label>s inside a .form-group, and `.form-group label`
   (block, 600, a bottom margin — the field-name style) outranks a single
   class: the cards stacked radio over wireframe over a bold note, and the
   swatches, inline spans with no box, rendered as nothing. */
.docstyle-grid > .docstyle-option { display: flex; margin: 0; font-size: inherit; font-weight: 400; color: inherit; }
.docstyle-option:hover { border-color: var(--accent, var(--teal)); }
.docstyle-option:focus-within { outline: 2px solid var(--accent, var(--teal)); outline-offset: 2px; }
.docstyle-option--selected {
  border-color: var(--accent, var(--teal));
  box-shadow: inset 0 0 0 1px var(--accent, var(--teal));
}
.docstyle-option input[type="radio"] { margin: 0; accent-color: var(--accent, var(--teal)); }

.docstyle-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.docstyle-name { font-size: 14px; font-weight: 600; }
.docstyle-note { font-size: 12px; color: var(--text-secondary); }

/* Palette: a round two-tone chip, ink against accent, exactly as the
   document draws them. Values from the flat --brand-* map so this chip and
   the printed page cannot disagree. */
.docstyle-swatch {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid var(--border);
  flex: none;
}
.docstyle-swatch--follow  { background: linear-gradient(135deg, var(--ink) 50%, var(--accent, var(--teal)) 50%); }
.docstyle-swatch--classic { background: linear-gradient(135deg, var(--brand-classic-ink) 50%, var(--brand-classic-accent) 50%); }
.docstyle-swatch--moby    { background: linear-gradient(135deg, var(--brand-moby-ink) 50%, var(--brand-moby-accent) 50%); }
.docstyle-swatch--slate   { background: linear-gradient(135deg, var(--brand-slate-ink) 50%, var(--brand-slate-accent) 50%); }
.docstyle-swatch--meadow  { background: linear-gradient(135deg, var(--brand-meadow-ink) 50%, var(--brand-meadow-accent) 50%); }
.docstyle-swatch--clay    { background: linear-gradient(135deg, var(--brand-clay-ink) 50%, var(--brand-clay-accent) 50%); }

/* Layout: a page-shaped wireframe. Greyscale on purpose — the layout axis
   never carries a colour, and a coloured preview here would imply the two
   pickers are one. The ::before is the letterhead, the ::after the rule
   under it; each layout moves them the way the real document does. */
.docstyle-mini {
  position: relative;
  width: 26px;
  height: 34px;
  border: 1px solid var(--border);
  border-radius: 2px;
  background: var(--surface);
  flex: none;
  overflow: hidden;
}
.docstyle-mini::before,
.docstyle-mini::after {
  content: "";
  position: absolute;
  inset-inline-start: 4px;
  inset-inline-end: 4px;
  background: var(--text-tertiary);
}
.docstyle-mini::before { top: 5px; height: 3px; }
.docstyle-mini::after { top: 12px; height: 1px; }

/* classic — centred, narrow wordmark over a ruled band. */
.docstyle-mini--classic::before { inset-inline-start: 8px; inset-inline-end: 8px; }
.docstyle-mini--classic::after { height: 2px; }

/* modern — a filled band across the top, rule gone. */
.docstyle-mini--modern::before { inset-inline-start: 0; inset-inline-end: 0; top: 0; height: 8px; }
.docstyle-mini--modern::after { top: 13px; }

/* minimal — everything hairline, wordmark left. */
.docstyle-mini--minimal::before { inset-inline-end: 14px; height: 2px; }
.docstyle-mini--minimal::after { top: 11px; }

/* bold — a heavy rule under an oversized title. */
.docstyle-mini--bold::before { inset-inline-end: 10px; height: 5px; }
.docstyle-mini--bold::after { top: 14px; height: 3px; }

/* The picker and its live preview, side by side where there is room. The
   preview is an iframe of the real document renderer (see
   partials/_document_preview.html) — scaled down rather than redesigned,
   so what a person approves here is what their client receives. */
.docstyle-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: 24px;
  align-items: start;
}
.docstyle-controls { min-width: 0; }

.docstyle-preview { min-width: 0; }
.docstyle-preview-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin: 0 0 8px;
}
/* Fixed box, and the document inside it is rendered at its real width and
   scaled — the alternative, letting it reflow into 300px, would preview
   the phone breakpoint rather than the A4 page these documents are
   printed on. */
.docstyle-preview-frame {
  position: relative;
  width: 100%;
  height: 380px;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
}
.docstyle-preview-frame iframe {
  position: absolute;
  top: 0;
  inset-inline-start: 0;
  width: 820px;
  height: 1040px;
  border: 0;
  transform: scale(0.365);
  transform-origin: 0 0;
  /* Nothing in the preview is clickable — it is a picture of a document,
     and a stray click inside a scaled frame lands nowhere useful. */
  pointer-events: none;
}

/* The "cards" variant (partials/_document_style_picker.html): the page
   draws the preview itself, so the pickers take the whole width — layouts
   as a grid of cards with their one-line notes, palettes as round swatches
   whose name sits under them and whose ring says which is chosen. */
.docstyle-layout--cards { display: block; }
.docstyle-layout--cards .docstyle-grid--layouts { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
.docstyle-layout--cards .docstyle-grid--layouts .docstyle-option { min-height: 64px; align-items: center; }
.docstyle-grid--swatches { gap: 6px 10px; }
.docstyle-grid > .docstyle-option--swatch {
  position: relative;
  flex-direction: column;
  gap: 6px;
  min-width: 64px;
  min-height: 44px;
  padding: 6px 4px;
  border-color: transparent;
  background: none;
}
.docstyle-option--swatch input[type="radio"] { position: absolute; opacity: 0; width: 1px; height: 1px; }
.docstyle-option--swatch .docstyle-swatch { width: 36px; height: 36px; }
.docstyle-option--swatch .docstyle-name { font-size: 12px; font-weight: 500; color: var(--text-secondary); }
.docstyle-option--swatch.docstyle-option--selected { border-color: transparent; box-shadow: none; }
.docstyle-option--swatch.docstyle-option--selected .docstyle-swatch {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent, var(--teal));
}
.docstyle-option--swatch.docstyle-option--selected .docstyle-name { color: var(--ink); font-weight: 600; }

/* Below the two-column step the preview goes under the controls rather
   than beside them, and gets its full width back. */
@media (max-width: 900px) {
  .docstyle-layout { grid-template-columns: minmax(0, 1fr); }
  .docstyle-preview-frame { height: 300px; }
}

/* ── A business's settings: the save bar (§17.58 P2) ────────────────────
   partials/_save_bar.html and static/js/business_settings.js. Here rather
   than in a portal sheet because either kind of business's settings can
   draw it. Sticky at the foot of the content, a nav bar's height up where
   the portal has one, so it rides over the cards it is about. */
.bs-bar {
  position: sticky;
  bottom: calc(var(--bottomnav-total-h, 0px) + 12px);
  z-index: 20;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 16px;
  margin-top: 16px;
  padding: 10px 12px 10px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--ink);
  box-shadow: 0 6px 24px rgba(14, 31, 45, 0.16);
  animation: bs-bar-in 200ms cubic-bezier(0.23, 1, 0.32, 1);
}
.bs-bar[hidden] { display: none; }
@keyframes bs-bar-in { from { transform: translateY(8px); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .bs-bar { animation: none; } }
.bs-bar-text { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 14px; font-weight: 600; }
.bs-bar-actions { display: flex; gap: 8px; margin-inline-start: auto; }
/* 44px: a thumb's target, whatever height the portal's buttons are. */
.bs-bar-actions button { min-height: 44px; margin: 0; width: auto; }
.bs-bar--saving { cursor: progress; }

/* The unsaved dot — in the bar, and on the section's own tab. */
.bs-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--warning, var(--accent)); flex: none; }
/* Positioned so its .sr-only text is held inside the tab row's own scroll
   box: unpositioned, the absolute span escaped it and widened the page by
   the row's overflow on a phone. */
.bs-tab-note { position: relative; display: inline-flex; align-items: center; margin-inline-start: 6px; vertical-align: middle; }

/* A card with unsaved changes, and one whose save was refused. */
.bs-card--dirty { box-shadow: inset 3px 0 0 var(--accent, var(--teal)); }
.bs-card--failed { border-color: var(--danger); }
.bs-error {
  margin: 12px 0 0;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: var(--status-bad-bg);
  color: var(--status-bad-fg);
  font-size: 13px;
  line-height: 1.45;
}
.bs-error[hidden] { display: none; }
/* The control a refusal named (an instant form's `field`), marked while the
   reader is walked to it, cleared with their next edit. */
.bs-field--invalid { outline: 2px solid var(--danger); outline-offset: 2px; border-radius: 6px; }
/* The search card's keys (partials/_knowledge_search.html, drawn by more
   than one portal) behind "Use your own AI keys" on a business's AI
   settings (§17.58 P5) — from the switch's own state, so it holds with JS
   off. Its Save is the card's JS-off path, hidden once the bar takes over;
   the button's own inline-flex outranks the browser's [hidden]. */
.bs-keys:not(:has(.bs-keys-toggle:checked)) .bs-keys-fields { display: none; }
.bs-keys-fields { margin-top: 8px; }
.bs-keys .form-control { min-height: 44px; max-width: 100%; }
form.bs-keys > [data-bs-card-save][hidden], .cs-card form.bs-keys > [data-bs-card-save][hidden] { display: none; }

/* A select with an "Other…" that opens a box beside it. The box shows only
   while Other is chosen — from the select's own state, so no script has to
   keep it in step, and a browser without :has() simply shows it. */
.bs-pick:not(:has(option[data-bs-other-option]:checked)) .bs-pick-other { display: none; }

/* ── Stock: the shelf, the quantity, the signed movement ─────────────────
 *
 * Here rather than in a portal sheet because two portals render them —
 * an org's store and a contractor's van are the same thing to the ledger
 * (see api/core/stock.py), so the page over it is the same page twice and
 * a second copy of these rules is what `duplicate-rules` exists to stop.
 * Everything structural is a shared component already (.chip,
 * .empty-state); what is here is the grid those sit in and the one thing
 * this feature has that nothing else does — a quantity read at a glance.
 */

.stock-locations {
  display: grid;
  gap: 16px;
  /* Two shelves side by side only where each keeps a readable part
   * column; narrower than that, one under the other beats two that wrap
   * every name. */
  grid-template-columns: repeat(auto-fit, minmax(600px, 1fr));
  /* Each card is as tall as its shelf: an empty van beside a full one is
   * a short card, not a tall blank. */
  align-items: start;
  margin-bottom: 16px;
}

.stock-location {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
}

.stock-location-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}

.stock-location-name {
  margin: 0;
  font-size: 15px;
  font-weight: 600;
}

/* "12 lines" / "empty", quietly, between the name and its kind. */
.stock-location-meta {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 12px;
  color: var(--text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.stock-empty {
  margin: 0;
  padding: 14px 16px;
  font-size: 13px;
  color: var(--text-secondary);
}

/* What the shelves are worth: three figures in a row, the margin in the
 * accent so the eye lands on the one that is the point of pricing stock. */
.stock-worth {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
  margin-bottom: 16px;
}
.stock-worth__stat {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  min-width: 0;
}
.stock-worth__label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.stock-worth__value {
  font-size: 20px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.stock-worth__value--margin { color: var(--teal-deep); }
.stock-worth__sub { font-size: 11px; color: var(--text-secondary); }

/* One row shape, four columns, on every shelf and on the catalogue: the
 * part takes the room, the three figures sit in fixed columns so a page
 * of them reads down as well as across. Baseline-aligned, so the number
 * and the name share a line whatever wraps under either. */
.stock-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 80px 80px 100px;
  align-items: baseline;
  column-gap: 10px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--border);
}

.stock-row:last-child { border-bottom: 0; }

.stock-row--head {
  padding-top: 8px;
  padding-bottom: 8px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
  background: var(--paper);
}

.stock-row-part { min-width: 0; }

.stock-row-part a {
  color: var(--text-primary);
  text-decoration: none;
  font-weight: 500;
}

@media (hover: hover) and (pointer: fine) {
  .stock-row-part a:hover { text-decoration: underline; }
}

.stock-row-sku {
  display: block;
  font-size: 12px;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

.stock-cell--num {
  text-align: end;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.stock-cell { font-size: 13px; color: var(--text-secondary); }
/* A price the shelf does not have yet: a dash where a number would be,
 * so the column stays a column. */
.stock-cell--none { color: var(--text-secondary); opacity: 0.6; }
.stock-cell--qty { color: var(--text-primary); }

/* The number is what a person came to this page for, so it is the only
 * thing on the row allowed to be big. Tabular figures because a column of
 * quantities that does not line up is a column nobody scans. */
.stock-qty {
  font-size: 17px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.stock-qty--low { color: var(--danger); }

.stock-qty-unit {
  font-size: 12px;
  font-weight: 400;
  color: var(--text-secondary);
}

.stock-reorder {
  display: block;
  font-size: 11px;
  color: var(--text-secondary);
  text-align: end;
}

/* "Not held anywhere yet" in the quantity's column, in the quantity's
 * register minus the weight — a fact about the part, not an error. */
.stock-row-note {
  display: block;
  font-size: 12px;
  color: var(--text-secondary);
  white-space: normal;
  text-align: end;
}

.stock-catalogue { padding: 0; overflow: hidden; }
.stock-catalogue__head { flex-wrap: wrap; }
.stock-row--catalogue { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) 80px 80px 100px; }

/* The places as a filter: chips in a row, the one in view filled. A
 * press is answered; nothing animates on a change of shelf — this is
 * navigation, and it should feel like a page, not a transition. */
.stock-filter { display: flex; flex-wrap: wrap; gap: 6px; margin-inline-start: auto; }
.stock-filter__chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 11px;
  border-radius: 20px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-primary);
  font-size: 12px;
  font-weight: 600;
  text-decoration: none;
  transition: transform 160ms cubic-bezier(0.23, 1, 0.32, 1), background 120ms ease;
}
.stock-filter__chip:active { transform: scale(0.97); }
.stock-filter__chip--on { background: var(--accent); border-color: var(--accent); color: var(--white); }
.stock-filter__count { font-weight: 500; opacity: 0.75; font-variant-numeric: tabular-nums; }
@media (hover: hover) and (pointer: fine) {
  .stock-filter__chip:not(.stock-filter__chip--on):hover { background: var(--paper); }
}

/* Where a part is: one small link per place, with how many there. */
.stock-cell--where { display: flex; flex-wrap: wrap; gap: 4px 6px; align-items: baseline; min-width: 0; }
.stock-where {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  padding: 2px 8px;
  border-radius: 6px;
  background: var(--paper);
  color: var(--text-primary);
  font-size: 12px;
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
}
.stock-where__qty { font-variant-numeric: tabular-nums; color: var(--text-secondary); }
/* The bin inside the place — "Van 1 · A13": a code the owner named. */
.stock-where__bin,
.stock-place__bin {
  display: inline-block;
  padding: 0 5px;
  border-radius: 4px;
  border: 1px solid var(--border);
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  color: var(--text-secondary);
  vertical-align: 1px;
}

/* The bins of the place in view: one chip each, removable, plus Add. */
.stock-bins {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 8px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--border);
  background: var(--paper);
}
.stock-bins__label { font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-inline-end: 4px; }
.stock-bin {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin: 0;
  padding: 3px 4px 3px 10px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--surface);
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.stock-bin__code { line-height: 1.4; }
.stock-bin__remove {
  appearance: none;
  border: 0;
  background: transparent;
  width: 20px;
  height: 20px;
  border-radius: 4px;
  color: var(--text-secondary);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  transition: transform 160ms cubic-bezier(0.23, 1, 0.32, 1), background 120ms ease;
}
.stock-bin__remove:active { transform: scale(0.97); }
@media (hover: hover) and (pointer: fine) {
  .stock-bin__remove:hover { background: var(--paper); color: var(--text-primary); }
}
.stock-bins .btn-sm { margin-inline-start: auto; }
@media (hover: hover) and (pointer: fine) { .stock-where:hover { background: var(--border); } }

/* ── One part's page ─────────────────────────────────────────────────
 * The facts strip with its one door beside it; a card per place with
 * the threshold that makes it low; the ledger as the same row shape the
 * shelves use, five columns. */
.stock-facts {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 20px;
}
.stock-facts > .stock-worth { flex: 1 1 auto; margin-bottom: 0; }
.stock-worth--facts { grid-template-columns: repeat(5, minmax(0, 1fr)); }
/* The one fact that is a form: the part's reorder threshold, edited in place. */
.stock-worth__stat--form { margin: 0; }
.stock-worth__stat--form .stock-worth__label { margin: 0; }
.stock-worth__field { display: flex; gap: 6px; align-items: center; min-width: 0; }
.stock-worth__field .form-control { flex: 1 1 60px; min-width: 0; height: 34px; padding-block: 0; font-size: 16px; font-weight: 600; font-variant-numeric: tabular-nums; }
.stock-worth__field .btn-sm { flex: 0 0 auto; }
/* The product's facts, one chip each — brand, model, "8 kg wash capacity". */
.stock-product-facts { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 14px; }
.stock-product-fact {
  display: inline-block;
  padding: 3px 9px;
  border-radius: 6px;
  background: var(--paper);
  border: 1px solid var(--border);
  font-size: 12px;
  font-weight: 500;
  color: var(--text-primary);
}
.stock-specs-box { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; resize: vertical; }
.stock-worth__order,
.stock-low__order { font-weight: 600; color: var(--accent-deep); text-decoration: none; }
@media (hover: hover) and (pointer: fine) {
  .stock-worth__order:hover, .stock-low__order:hover { text-decoration: underline; }
}
.stock-worth__order { font-size: 12px; }
.stock-worth__stat--low { border-color: var(--danger); }
.stock-worth__stat--low .stock-worth__sub { color: var(--danger); }
.stock-facts__actions { flex: 0 0 auto; padding-top: 12px; }

.stock-places {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  gap: 12px;
  margin-bottom: 20px;
}
.stock-place {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.stock-place__head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.stock-place__name { font-size: 14px; font-weight: 600; }
.stock-place__qty { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.stock-place__qty .stock-qty { font-size: 22px; }
/* The total first (Talha 2026-09-20): one card across the whole row, the
 * number a size up — it is the answer the cards under it break down. The
 * same surface as a place, so it reads as one of them, summed. */
.stock-place--total { grid-column: 1 / -1; }
.stock-place--total .stock-qty { font-size: 28px; }
/* The threshold, in a row: label · box · Save, level on one baseline. */
.stock-place__reorder {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}
.stock-place__reorder label { font-size: 12px; color: var(--text-secondary); white-space: nowrap; margin: 0; }
.stock-place__reorder .form-control { flex: 1 1 80px; min-width: 0; height: 34px; padding-block: 0; font-size: 14px; }
.stock-place__reorder .btn-sm { flex: 0 0 auto; }
/* The move row: quantity, then where — the select takes what the box
 * leaves. */
.stock-place__move { flex-wrap: wrap; }
.stock-place__move .form-control[name="quantity"] { flex: 0 0 72px; }
.stock-place__move select { flex: 1 1 120px; min-width: 120px; }
.stock-place__bin-form select { flex: 1 1 100px; min-width: 100px; }
.stock-place__bin-form [data-bin-code] { flex: 1 1 80px; }

.stock-row--history { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.4fr) 88px 80px 80px; }
.stock-cell--for { color: var(--text-primary); min-width: 0; overflow-wrap: anywhere; }
.stock-cell--for a { color: var(--accent-deep); text-decoration: none; }
@media (hover: hover) and (pointer: fine) { .stock-cell--for a:hover { text-decoration: underline; } }

/* Two boxes side by side in a dialog, each with its own label above. */
.form-row--pair > div { display: flex; flex-direction: column; gap: 4px; min-width: 0; }

/* Narrow: the part takes the first line, the three figures share the
 * second under their own labels, so nothing is lost and nothing wraps
 * mid-number. */
@media (max-width: 640px) {
  .stock-locations { grid-template-columns: 1fr; }
  /* Three figures still in a row on a phone — a glance, not a scroll —
   * at a size that fits "£12,345.67" in a third of 400px. */
  .stock-worth { gap: 8px; }
  .stock-worth__stat { padding: 10px 12px; }
  .stock-worth__value { font-size: 15px; }
  /* The counts under the figures are a desk-width detail; a phone gets
   * the three numbers and nothing cut mid-word. */
  .stock-worth__sub { display: none; }
  .stock-row--head { display: none; }
  .stock-row {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    row-gap: 6px;
  }
  .stock-row-part { grid-column: 1 / -1; }
  .stock-cell--num { text-align: start; }
  .stock-cell[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-secondary);
  }
  .stock-cell--qty { text-align: end; }
  .stock-row-note { text-align: start; }
  .stock-row--catalogue { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .stock-row--catalogue .stock-row-part,
  .stock-row--catalogue .stock-cell--where { grid-column: 1 / -1; }
  .stock-filter { margin-inline-start: 0; }
  .stock-facts { flex-direction: column; }
  .stock-facts__actions { padding-top: 0; }
  .stock-worth--facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .stock-worth__stat--form { grid-column: 1 / -1; }
  .stock-row--history { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .stock-row--history .stock-row-part,
  .stock-row--history .stock-cell--for { grid-column: 1 / -1; }
}

/* Demand from direct-to-job purchases is useful only when it reads as a
   decision: keep this part, or leave it as a just-in-time purchase. These
   rows retain the audit signals without making operators scan a wide table. */
.stock-opportunities { padding: 0; overflow: hidden; }
.stock-opportunities-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  padding: 18px 18px 4px;
}
.stock-opportunities-head h2 { margin: 0 0 5px; }
.stock-opportunities-head .card-sub { margin-bottom: 10px; }
.stock-opportunities-list { border-top: 1px solid var(--border); }
.stock-opportunity {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--border);
}
.stock-opportunity:last-child { border-bottom: 0; }
.stock-opportunity-main { min-width: 0; }
.stock-opportunity-name {
  display: inline-block;
  margin-bottom: 7px;
  color: var(--text-primary);
  font-weight: 650;
  text-decoration: none;
}
.stock-opportunity-name:hover { text-decoration: underline; }
.stock-opportunity-metrics { display: flex; flex-wrap: wrap; gap: 4px 12px; color: var(--text-secondary); font-size: 12px; }
.stock-opportunity-metrics strong { color: var(--text-primary); font-variant-numeric: tabular-nums; }
.stock-opportunity-action { flex: 0 0 auto; white-space: nowrap; }

@media (max-width: 640px) {
  .stock-opportunities-head { padding: 16px 14px 4px; }
  .stock-opportunities-head .chip { display: none; }
  .stock-opportunity { align-items: flex-start; flex-direction: column; padding: 14px; }
  .stock-opportunity-action { width: 100%; }
}

/* A wrapping row of page-level buttons, above the thing they act on. Named
   for the shape and not for the first page that wanted it: it lived here as
   `.stock-actions` while three pages used it, two of which were not about
   stock, and a shared sheet naming a component after one caller is how the
   next person concludes it is not theirs to use. */
.page-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 18px;
}
/* The first button after a destructive one (§10: never beside Save): it and
   everything after it go to the far end of the row. */
.page-actions__far { margin-inline-start: auto; }

/* On the contractor and customer portals a button IS the row — `.btn-primary`
   there carries `width: 100%`, which is right on a phone and turns a row of
   three into three stacked bars on a desk. So they shrink to their content
   once there is width to put them side by side, and only then: below the
   step, full width is still the thing a thumb wants. Admin portals are
   unaffected — `.btn` carries the shape there and was never 100%. */
@media (min-width: 640px) {
  .page-actions > .btn-primary,
  .page-actions > .btn-secondary,
  .page-actions > .btn-outline,
  .page-actions > .btn-danger { width: auto; }
}

/* Movement ledger — a signed number reads as a direction before it reads
 * as a value, so the sign carries the colour and the column is tabular.
 *
 * The -ink half of the status pair, not a bare --status-good: the map in
 * _tokens.css declares -ink (the text) and -fill (the ground) and nothing
 * in between, and ui_lint caught the invented middle name. */
.stock-delta {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
}

.stock-delta--in { color: var(--status-good-ink); }
.stock-delta--out { color: var(--status-bad-ink); }

/* ── Table scroll ────────────────────────────────────────────────────
 * A table wider than its column scrolls inside its own box rather than
 * pushing the page sideways. Lived in common/_shell.css until 2026-08-20,
 * where only the two admin portals could see it — see the note there.
 *
 * New table? Wrap it in this. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* ── Certificates ────────────────────────────────────────────────────
 * Rendered by four portals — org, contractor, customer and tenant all
 * read the same artefact — so the rules live here rather than in any one
 * portal's sheet (§2). The row data comes from
 * `api/core/certificate_view.rows`, which is what stops "expired" meaning
 * two different things on two screens.
 *
 * Logical properties throughout: `text-align: start`, `margin-inline-*`.
 * The `direction` check enforces it, and RTL is a `LOCALES` row away. */
.cert-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
}

.cert-table th {
  text-align: start;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

.cert-table td {
  padding: 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}

.cert-table tr:last-child td { border-bottom: none; }

.cert-name {
  display: block;
  font-weight: 600;
  color: var(--ink);
}

/* The scheme number under the form's name. Its own line and not a second
 * column: it is how a person finds one certificate among a year of them,
 * and a column of mostly-blank references reads as missing data — the
 * field is free text because every scheme numbers its own way, and plenty
 * of forms carry no number at all. */
.cert-ref {
  display: block;
  margin-top: 2px;
  font-size: 12px;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

/* The date sits UNDER the status pill rather than beside it. The pill is
 * the answer ("Expiring soon"); the date is the evidence, and a reader
 * scanning a column of dates for the one that has passed is doing the
 * work the pill exists to save them. */
.cert-expiry {
  display: block;
  margin-top: 4px;
  font-size: 12px;
  color: var(--text-secondary);
  white-space: nowrap;
}

.cert-actions { text-align: end; white-space: nowrap; }

/* Deliberately not a `.btn` variant. `.btn` carries the shape on org and
 * platform and does not exist on contractor or customer (§3), so a
 * component shared by all four cannot wear one and be right anywhere. */
.cert-open {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 13px;
  font-weight: 600;
  color: var(--accent-deep, var(--accent));
  text-decoration: none;
  background: var(--surface);
}

.cert-open:hover { background: var(--bg-subtle); }

.cert-pending {
  font-size: 12.5px;
  color: var(--text-secondary);
  font-style: italic;
}

/* Picking one of the forms we ship, inside the New-form dialog. Here and
 * not in a portal sheet for the reason above: the org designer is unbuilt
 * rather than declined, and this list is what it will use too. */
.cert-pick { display: flex; flex-direction: column; gap: 8px; }

.cert-pick-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  cursor: pointer;
  background: var(--surface);
}

.cert-pick-row:hover { background: var(--bg-subtle); }

/* `:has()` rather than a class a script toggles — the radio already holds
 * the state, and a second copy of it in the DOM is a second thing to keep
 * in step. */
.cert-pick-row:has(input:checked) {
  border-color: var(--accent);
  background: var(--bg-subtle);
}

/* The focus ring lands on the radio, which is 16px in the corner of a
 * 60px row — easy to lose. Lift it onto the whole row. */
.cert-pick-row:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.cert-pick-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.cert-pick-name { font-size: 14px; font-weight: 600; color: var(--ink); }

.cert-pick-sub { font-size: 12.5px; color: var(--text-secondary); }

.cert-pick-meta {
  font-size: 12px;
  color: var(--text-secondary);
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  margin-block-start: 2px;
}

@media (max-width: 640px) {
  /* The issuer is the first thing to go: on a phone the reader already
   * knows whose screen they are on, and the name is the longest cell in
   * the row. The property follows it on the narrowest screens — a tenant
   * and a landlord looking at one home do not need to be told which. */
  .cert-table th:nth-child(3), .cert-table td:nth-child(3) { display: none; }
}

@media (max-width: 480px) {
  .cert-table th:nth-child(2), .cert-table td:nth-child(2) { display: none; }
  .cert-table td, .cert-table th { padding: 10px 8px; }
}

/* ── Visit time offers ───────────────────────────────────────────────────
 * One side says when it is free; the other picks a time inside it. Both
 * portals render both halves — the office offers windows and accepts the
 * customer's, the customer does the mirror — so these live here rather
 * than in either portal's sheet (§2). A second copy would be two
 * vocabularies for one conversation.
 */

/* A date and a from/to pair on one line, because that is one thought. It
 * collapses to stacked rows below 480 rather than shrinking three controls
 * a thumb has to hit: a 90px-wide time input is a control nobody can use. */
.win-row {
  display: grid;
  grid-template-columns: 1.4fr 1fr 1fr;
  gap: 6px;
  margin-block-end: 6px;
}

@media (max-width: 480px) {
  .win-row { grid-template-columns: 1fr; }
}

/* The times themselves. A wrapping row of buttons rather than a select:
 * the whole value of a window is seeing how much choice there is, and a
 * closed dropdown hides exactly that. */
.offer-times {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-block: 8px 4px;
}

.offer-time {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--paper);
  color: var(--ink);
  padding: 7px 11px;
  font-size: 13px;
  cursor: pointer;
}

.offer-time:hover {
  border-color: var(--accent);
}

.offer-time--none {
  color: var(--text-secondary);
  font-size: 13px;
  margin-block: 6px;
}

.offer-message--none {
  font-size: 13px;
  color: var(--text-secondary);
  margin-block: 6px;
}

/* A two-stage choice keeps a long customer availability list legible: pick
 * the day first, then one of that day's genuinely bookable times. */
.offer-slot-picker__days {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding-block: 2px;
}

.offer-slot-picker { margin-block: 10px; }

.offer-slot-picker__day {
  flex: 0 0 auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--paper);
  color: var(--ink);
  padding: 7px 10px;
  font-size: 12px;
  cursor: pointer;
}

.offer-slot-picker__day:hover,
.offer-slot-picker__day.is-selected {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--white);
}

/* The material-deposit form is intentionally one compact row on desktop;
 * narrow screens stack it without changing the controls or submission. */
.srd-inline-fields {
  display: grid;
  grid-template-columns: minmax(0, .7fr) minmax(0, 1.4fr) minmax(0, 1fr) auto;
  gap: 8px;
  align-items: end;
}

@media (max-width: 640px) {
  .srd-inline-fields { grid-template-columns: 1fr; }
}

/* One issued deposit and the controls that act on THAT one — the figure
 * reads first and its actions sit beside it, so a job holding several
 * deposits never offers a button whose row is ambiguous. Wraps rather than
 * scrolls on a narrow screen. */
.srd-deposit-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  padding: 6px 0;
}

.srd-deposit-row form { margin: 0; }

/* Live status regions must be invisible without disappearing from the
 * accessibility tree. Shared because org pages use the same pattern. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* The window a set of times belongs to, so "Tuesday morning" and
 * "Thursday afternoon" read as two answers and not one long list. */
.offer-window-label {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-secondary);
  margin-block: 10px 0;
}

/* A short column of money lines with a rule under the figure they add
   up to — what a claim bills, what the materials come to, what the
   customer ends up paying.

   Lived in css/org/service_request_detail.css until the customer's own
   request page needed the same list for a material it is being asked to
   pay for. Two portals means here (§2), and the name lost the org
   page's prefix on the way: a component in common/ named after one
   portal's screen is a component the next person will not look for.

   Originally: The office was being asked to set a commission against a single
   subtotal with the lines behind it rendered nowhere at all — so a
   labelled, ruled list rather than prose: the point is scanning "4.5
   hours, two parts, a call-out" against what the job was, and a
   paragraph makes that a reading exercise. */
.money-lines {
    list-style: none;
    margin: 0 0 10px;
    padding: 0;
}

.money-lines li {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    padding: 5px 0;
    border-bottom: 1px solid var(--hairline, var(--border));
}

.money-lines li:last-child {
    border-bottom: 0;
}

/* `display: flex` above beats the UA's own `[hidden] { display: none }`
   — any display declaration does, since the UA sheet is the weakest
   origin — so a row hidden by the attribute stayed on screen. The review
   card's live discount line shipped as a permanent "Discount −$0.00"
   because of it. Anything that sets `display` on an element it also
   hides has to say this. */
.money-lines li[hidden] { display: none; }

.money-lines li > span:last-child {
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    white-space: nowrap;
}

/* The figure a group adds up to. A rule ABOVE rather than heavier text:
   the list already bolds every amount, so weight alone cannot say which
   one is the sum. */
.money-lines li.money-lines__sum {
    border-top: 2px solid var(--border);
    border-bottom: 0;
    margin-block-start: 4px;
    font-weight: 600;
}

/* ── Filed payroll ───────────────────────────────────────────────────
   partials/_filed_payroll.html, rendered in BOTH the org portal (a
   payroll run's page) and the contractor office (a filed period). Here
   rather than in either portal's sheet for exactly that reason (§2) —
   the org page's own <style> defines .rn-card/.rn-table, and a partial
   reaching for those renders as bare text on the other side. */
.fp-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  margin-bottom: 18px;
  overflow: hidden;
}
.fp-table { width: 100%; border-collapse: collapse; }
.fp-table td,
.fp-table th {
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
  vertical-align: middle;
  text-align: start;
}
.fp-table tr:last-child td { border-bottom: none; }
.fp-total td { font-weight: 600; }
.fp-muted { color: var(--text-secondary); }
.fp-empty {
  text-align: center;
  color: var(--text-secondary);
  padding: 36px 16px;
}

/* ── An account's own WhatsApp number ────────────────────────────────
   partials/_whatsapp_sender.html, rendered in BOTH the org portal's
   Communications settings and the contractor's business settings. Here
   rather than in either portal's sheet for exactly that reason (§2):
   the org page dresses its fields with .form-control and the contractor
   page with its own page-local rules, so a partial reaching for either
   renders as bare browser chrome on the other side. */
.wa-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
  margin-bottom: 16px;
}
@media (max-width: 640px) {
  .wa-grid { grid-template-columns: 1fr; }
}
.wa-field label {
  display: block;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--text-secondary);
  margin-bottom: 6px;
}
.wa-field input {
  width: 100%;
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 14px;
  font-family: inherit;
  background: var(--surface);
  color: inherit;
}
.wa-saved {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
  color: var(--text-muted);
}
.wa-hint {
  font-size: 12px;
  color: var(--text-secondary);
  line-height: 1.5;
  margin: 6px 0 0;
}
/* The callback is the one value on this form the account does not type —
   we mint it — so it reads as output rather than as another empty box. */
.wa-callback {
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px 16px;
  margin-bottom: 16px;
}
.wa-callback-label {
  display: block;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--text-secondary);
  margin-bottom: 6px;
}
.wa-callback-url {
  display: block;
  font-size: 13px;
  word-break: break-all;
  color: var(--text-primary);
}


/* ===================================================================
   Checking a form against the document it was read out of

   Here rather than in a portal sheet because BOTH review screens are now
   rendered by two portals: the contractor office and field shells, and
   the org portal's own supplier-invoice review, which used to draw the
   same page a second way (CLAUDE.md §13). A class defined in
   contractor/_layout.css and rendered on an org page is the failure §1
   opens with.

   The desk behaviour is scoped to the two content wrappers that HAVE a
   desk: `.admin-content` (the org and platform shells, common/_shell.css)
   and `.contractor-main` (the contractor shell, which grew a wide mode in
   §17.8 S1). Everything narrower than the breakpoint below keeps the
   stacked page, which is what a phone gets and what these pages were
   before either desk existed.

   `.contractor-main` was added by §17.8 S2 and is not a widening of the
   rule — it is the same rule following its pages. Both review screens
   rendered in the contractor OFFICE shell, which is `.admin-content`; S2
   retired that shell, so without this they would have silently dropped to
   the stacked reading on the one screen the two-column layout was built
   for, while every test kept passing. `tests/app/test_contractor_review_
   pages_render.py` is what caught it, by asserting the shell it renders
   in rather than trusting the page to be in the right one.
   =================================================================== */

.review-grid { display: grid; gap: 16px; align-items: start; }
/* Each column spaces its own children, the way .srd-main does — so the
   two-column and the stacked rendering keep one rhythm between them. */
.review-main, .review-doc {
    display: flex;
    flex-direction: column;
    gap: 14px;
    min-width: 0;
}
/* .card ships a bottom margin for pages that stack it; inside a column
   with its own gap that would double up. Direct children only: the
   supplier form's inner cards stack normally and are not the column's
   business. */
.review-main > *, .review-doc > * { margin-bottom: 0; }

/* The text a document was read into, as a bounded scroll box. Bounded
   because the alternative is a page as long as the invoice: the supplier
   review had no cap at all, and in the rail it has to stay shorter than
   the viewport it is sticky against. */
.doc-text {
    white-space: pre-wrap;
    font-size: 13px;
    max-height: 340px;
    overflow: auto;
    margin: 0;
    padding: 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    /* The text is here to be selected and pasted into the field the
       reader got wrong — never retyped off an image. */
    user-select: text;
}


/* A pair of fields that share a row once there is room for two. Inert
   below the breakpoint, so on a phone every field keeps the full width
   it needs. */
.review-pair { display: grid; gap: 0 14px; }


/* ── The lines on a supplier invoice ──────────────────────────────────
   Ten controls per line across three rows, every line the image of the
   one above it, and a hairline between them doing all the work of saying
   where one ended. Reading it meant counting rows.

   Striped instead, with the pattern this portal already has — the
   inspection card's .insp-item, same tint — so a line reads as one block
   rather than three rows that happen to be adjacent. Full-bleed via
   negative margins cancelling .card's 18px padding: a stripe that stops
   short of the card edge reads as a floating box, which is the thing it
   is trying not to be. */
/* The whole bill onto one job in one move — above the lines, on both
   review pages (partials/_bill_whole.html). A ruled band rather than a
   card of its own: it is a control over the list, not another line. */
.bill-whole { padding: 0 0 12px; margin: 0 0 12px; border-bottom: 1px solid var(--border); }
.bill-whole__row {
    display: grid;
    grid-template-columns: minmax(180px, 2fr) minmax(90px, 1fr) auto;
    gap: 8px;
    align-items: end;
}

/* ── The figures and the footing ─────────────────────────────────────
   Number, date, due on one row; net, VAT, gross on the next; then the
   footing — what the lines come to against what the merchant printed,
   recomputed on every keystroke (partials/_bill_totals.html). The
   footing is the one thing on this screen a reviewer must not miss, so
   it is a band, not a sentence, and its verdict is a status pill from the
   fixed bucket map (§4) — "Adds up" reads the same on every firm. */
.bill-figures {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 10px;
}
.bill-figures { margin-bottom: 12px; }
.bill-figures__at { font-weight: 400; color: var(--text-secondary); }
/* The unlock reads as a sentence with a link in it, not a fourth button. */
.bill-figures__unlock {
    display: inline;
    border: 0;
    padding: 0;
    margin-inline-start: 4px;
    background: none;
    font: inherit;
    color: var(--teal-deep);
    text-decoration: underline;
    cursor: pointer;
}
.bill-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 22px;
    margin-top: 14px;
    padding: 12px 14px;
    border-radius: var(--radius-md, 10px);
    background: var(--paper);
    border: 1px solid transparent;
}
.bill-foot--off { border-color: var(--status-bad-fg); }
.bill-foot__cell { display: flex; flex-direction: column; gap: 2px; }
.bill-foot__cell--verdict { margin-inline-start: auto; }
.bill-foot__key { font-size: 11px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-secondary); }
.bill-foot__val { font-family: var(--font-display); font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ink); }

/* A number box reads as a number: right-aligned, tabular digits. */
.form-control.num { text-align: end; font-variant-numeric: tabular-nums; }

/* The lines card's head — title on the left, the two facts on the right. */
.bill-lines__bar {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 4px 16px;
    margin-bottom: 12px;
}

/* ── One line ─────────────────────────────────────────────────────────
   A HEAD that reads like the ledger — number, description, what it comes
   to — over a BODY of three named groups (partials/_bill_line.html). The
   description is the only big control on the row; the total beside it is
   text the page computes. Numbered with a CSS counter so a line the Add
   dialog appends is counted too. Each line is its own card with air
   between them (Talha, 2026-09-18 — the stripes that preceded this ran
   the lines together), and the line under the pointer or the keyboard
   takes the accent so a reviewer tabbing through twelve boxes can tell
   which line they are on. */
.review-lines { display: flex; flex-direction: column; gap: 12px; counter-reset: bill-line; }
.review-line {
    padding: 14px 16px 14px;
    counter-increment: bill-line;
    background: var(--surface);
    border: 1px solid var(--border);
    border-inline-start: 3px solid var(--border);
    border-radius: var(--radius-md, 10px);
}
.review-line:hover, .review-line:focus-within { border-inline-start-color: var(--teal); }
.review-line--gone { display: none; }
.review-line__head {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 10px;
}
.review-line__index {
    flex: 0 0 auto;
    min-width: 24px;
    height: 24px;
    padding: 0 6px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 12px;
    background: var(--surface);
    border: 1px solid var(--border);
    font-size: 11px;
    font-weight: 700;
    color: var(--text-secondary);
    font-variant-numeric: tabular-nums;
}
.review-line__index::before { content: counter(bill-line); }
.review-line__desc { flex: 1 1 200px; min-width: 0; font-weight: 600; font-size: 15px; }
.review-line__total {
    flex: 0 0 auto;
    font-family: var(--font-display);
    font-size: 17px;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
    min-width: 5ch;
    text-align: end;
}
/* A button reading as an icon — full reset, the way .link-action is
   built on the contractor sheet, here because both portals render it. */
.review-line__remove {
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--text-secondary);
    font: inherit;
    cursor: pointer;
    transition: transform 160ms cubic-bezier(0.23, 1, 0.32, 1), background-color 120ms ease, color 120ms ease;
}
.review-line__remove:active { transform: scale(0.92); }
@media (hover: hover) and (pointer: fine) {
    .review-line__remove:hover { background: var(--status-bad-bg); color: var(--status-bad-fg); }
}
.review-line__remove:focus-visible { background: var(--status-bad-bg); color: var(--status-bad-fg); }
.review-line__body {
    display: grid;
    gap: 10px 18px;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
/* Three groups, three surfaces (Talha, 2026-09-19: "make cost and client
   sections distinct"): Cost sits on the paper, Product on the card, and
   Client — what the customer is charged — under a rule of its own. Cost
   is a 2×2 so "Auto (20%)" and a four-figure unit cost each get a box
   wide enough to read. */
/* Cost and Product are two equal panels — the same paper, the same
   padding, labels on one line — so the eye reads a pair, not an island
   beside loose boxes. Client is the answer beneath them: no panel, a rule,
   and the figure it produces at the row's end where the line total sits. */
.review-group--cost, .review-group--product { padding: 8px 10px 10px; border-radius: var(--radius-sm); background: var(--paper); }
.review-group--client { grid-column: 1 / -1; padding-top: 10px; border-top: 1px solid var(--border); }
/* A group is a column whose fields sit at its foot, so Cost's boxes and
   Product's boxes share one baseline however their labels wrap. */
.review-group { min-width: 0; display: flex; flex-direction: column; }
.review-group__fields { margin-top: auto; }
/* One height for every box in a row — a select and an input do not
   agree on padding, and the row read as a staircase. */
.review-group__fields .form-control,
.bill-figures .form-control { height: 38px; padding-top: 0; padding-bottom: 0; }
.review-group__label {
    display: block;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-secondary);
    margin-bottom: 6px;
}
.review-group__fields { display: grid; gap: 8px; align-items: end; }
.review-group__fields--cost { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* A derived figure reads as one: on the paper, not the surface. */
.review-line .form-control[readonly],
.bill-figures .form-control[readonly] { background: var(--paper); color: var(--text-secondary); }
/* The VAT the picker derives, as text beside it — the box itself only
   opens under "Amount…" (data-vat-cash-group is hidden otherwise). */
/* The derived VAT is a figure in a box's place: the same 38px, centred,
   so the row reads level with the picker beside it. */
.review-group__derived {
    align-self: end;
    height: 38px;
    display: flex;
    align-items: center;
    padding: 0 10px;
    font-size: 14px;
    font-weight: 600;
    color: var(--text-secondary);
    font-variant-numeric: tabular-nums;
}
/* Model numbers are long and warranties short: the model takes the top
   row, stock no. and warranty share the second, and the 2×2 is full. */
.review-group__fields--product { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }
.review-group__model { grid-column: 1 / -1; }
/* The product's facts (§17.38), folded under the numbers: open when the
   line has any, a one-line summary when it has none. Brand and the two
   dates on one row, the specs box full width. */
.review-group--facts { grid-column: 1 / -1; margin: 0; padding: 8px 10px 10px; border-radius: var(--radius-sm); background: var(--paper); }
.review-group__facts-summary {
    cursor: pointer;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-secondary);
    list-style: none;
    display: flex;
    align-items: center;
    gap: 6px;
}
.review-group__facts-summary::-webkit-details-marker { display: none; }
.review-group__facts-summary::before {
    content: "";
    width: 6px; height: 6px;
    border-inline-end: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: rotate(-45deg);
    transition: transform 160ms cubic-bezier(0.23, 1, 0.32, 1);
}
.review-group--facts[open] > .review-group__facts-summary::before { transform: rotate(45deg); }
.review-group__fields--facts { margin-top: 8px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.review-group__specs { grid-column: 1 / -1; }
.review-group__specs textarea { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; min-height: 0; resize: vertical; }
/* "Look it up" (§17.38 U6) under the specs box, with the sentence that
   says what the merchant's listing had beside it. */
.review-group__lookup { grid-column: 1 / -1; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.review-group__lookup-note { font-size: 12px; color: var(--text-secondary); }
.form-hint-inline { font-weight: 400; color: var(--text-secondary); }
.form-hint-inline code { font-size: 11px; }
/* A box the selection has switched off: `hidden` must beat the flex
   column above, or the attribute does nothing. */
.review-group__fields .form-group[hidden],
.review-group__derived[hidden] { display: none; }
/* A row, not a grid: a box the mode switches off leaves no hole behind. */
.review-group__fields--client { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px 10px; }
.review-group__fields--client > .form-group { flex: 0 1 140px; min-width: 0; }
/* Every box in a row lands on one baseline: a label that wraps
   ("Warranty (months)") pushes its own text up, never its box down. */
.review-group__fields .form-group,
.bill-figures .form-group {
    margin-bottom: 0;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
}
.review-group__fields .form-group label { font-size: 12px; font-weight: 500; color: var(--text-secondary); margin-bottom: 4px; }
.review-group__fields--client > .review-group__job { flex: 1 1 200px; }
/* The client's boxes, grouped for the markup: `display: contents` keeps
   each box a child of the flex row. */
.review-group__when-job { display: contents; }
.review-group__pays {
    flex: 0 0 auto;
    margin-inline-start: auto;
    align-self: flex-end;
    height: 38px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    font-size: 14px;
    font-weight: 600;
    color: var(--teal-deep);
    font-variant-numeric: tabular-nums;
}
.review-line__tie { margin-top: 10px; }
/* There was a `.review-line--new` here — the always-blank row, dashed so
   it wasn't counted as a line. It is a dialog now (CLAUDE.md rule 11);
   marking the odd one out was treating the symptom of it being in the
   list at all. */

/* ── The document, framed ─────────────────────────────────────────────
   Was contractor/_layout.css's .doc-frame, moved here when the bill
   review — rendered by both portals — started framing its PDF in place
   (partials/_bill_document.html). The works order review still uses it. */
.doc-frame {
    width: 100%;
    height: 420px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}
.bill-doc__bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
.bill-doc__actions { display: flex; align-items: center; gap: 6px; }
.bill-doc__actions form { display: inline; margin: 0; }
.bill-doc__frame { height: 60vh; min-height: 420px; background: var(--paper); }
.bill-doc__image { display: block; max-width: 100%; border: 1px solid var(--border); border-radius: var(--radius-sm); }

@media (min-width: 1200px) {
    /* 1200 is the scale's top step and the same one the SRD rail
       collapses at. Both shells arrive at the same form column at the
       narrowest this ever applies, which is why one breakpoint serves
       both: the admin shell spends 220px on its sidebar and 64px on
       .admin-content's padding (1200 - 284 - 360 - 20 = 536px), and the
       contractor shell caps at min(1200px, 96vw) — 1152px at this
       viewport — then spends 232px on the rail and 32px on
       .contractor-main's padding (1152 - 264 - 360 - 20 = 508px). Under
       this step the form is tighter than the field app it came from,
       which is why the rule does not reach further down the scale. */
    .admin-content .review-grid,
    .contractor-main .review-grid {
        grid-template-columns: minmax(0, 1fr) 360px;
        gap: 20px;
    }
    /* Explicit placement, by coordinates rather than by source order —
       which is the point. The two pages disagree about where the document
       belongs when the page is stacked (the works order shows it before
       the form, the supplier invoice after), and both are right for their
       own phone rendering. Naming the columns here lets each keep the
       markup order it wants while the desk puts the work beside the nav
       and the reference material outboard of it, the way round the SR
       detail rail already reads. */
    .admin-content .review-main,
    .contractor-main .review-main { grid-column: 1; grid-row: 1; }
    .admin-content .review-doc,
    .contractor-main .review-doc {
        grid-column: 2;
        grid-row: 1;
        position: sticky;
        /* Clears .admin-content's own top padding, so the card lands
           where the page header left off rather than under it. */
        top: 20px;
    }
    /* The contractor shell's topbar is sticky and the admin shell's is
       not, so a card pinned at 20px there slides under it here. Same sum
       the rail and the flash stack pin at, plus the 20px above.

       Both tokens carry a fallback because this sheet is loaded by 163
       pages that never load contractor/_layout.css, which is where
       --topbar-total-h is declared — ui_lint's `tokens` check reports
       exactly that, and it is right to: an undeclared custom property
       resolves to nothing and the whole `calc` is discarded. The fallback
       is unreachable in practice (`.contractor-main` exists only on the
       shell that declares it) and is what makes the rule true of the
       sheet it is written in. */
    .contractor-main .review-doc {
        top: calc(var(--topbar-total-h, 0px) + var(--env-banner-h, 0px) + var(--impersonation-banner-h, 0px) + 20px);
    }
    /* Tall enough to be worth keeping open beside the form, bounded so a
       long invoice cannot push the rail past the fold it is sticky for. */
    .admin-content .doc-text,
    .contractor-main .doc-text { max-height: min(60vh, 520px); }
    .admin-content .review-pair,
    .contractor-main .review-pair { grid-template-columns: 1fr 1fr; }
    /* On a desk: cost and product side by side, the client group full
       width beneath with the job picker taking half of it. */
    .admin-content .review-line__body,
    .contractor-main .review-line__body { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }

    .admin-content .bill-doc__frame,
    .contractor-main .bill-doc__frame { height: calc(100vh - 220px); }
}

/* ── The audit log ────────────────────────────────────────────────────
   One read-only table, rendered by two portals (partials/_audit_log.html).
   These rules were a <style> block on each page, and the two had drifted:
   the contractor's was rewritten onto tokens while the org's kept a
   hardcoded indigo for .aud-action. Neither check that exists for that
   could see it — `hex` blanks a template's own <style> region, and
   `duplicate-rules` only ever compares stylesheets to stylesheets. Here
   both of them can. */
.aud-card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
.aud-toolbar { display: flex; gap: 12px; align-items: center; padding: 14px 18px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
.aud-toolbar label { font-size: 12px; font-weight: 600; color: var(--text-muted); }
.aud-toolbar select { padding: 8px 11px; border: 1px solid var(--border); border-radius: 8px; font: inherit; background: var(--surface); color: inherit; }
.aud-toolbar .muted { margin-inline-start: auto; font-size: 13px; color: var(--text-muted); }
.aud-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.aud-table th { text-align: start; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); padding: 10px 14px; border-bottom: 1px solid var(--border); white-space: nowrap; }
.aud-table td { padding: 11px 14px; border-bottom: 1px solid var(--border); vertical-align: top; }
.aud-when { white-space: nowrap; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.aud-action { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12px; background: var(--bg-subtle); color: var(--text-secondary); padding: 2px 7px; border-radius: 5px; white-space: nowrap; }
.aud-actor { white-space: nowrap; }
/* A system actor is not a person, and italic is what says so without a
   second column for it. */
.aud-actor .sys { color: var(--text-muted); font-style: italic; }
.aud-reasons { margin: 6px 0 0; padding-inline-start: 18px; color: var(--text-muted); }
.aud-reasons li { margin-block-start: 2px; }
.aud-empty { text-align: center; color: var(--text-muted); padding: 40px 14px; }
/* Its own scroller rather than .table-scroll: that one is defined in
   common/_shell.css, the ADMIN shell, which the contractor portal never
   loads — the class would render as nothing and the table would push the
   page sideways (§1). */
.aud-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
/* The "Open the job" link on a money row. It had no rule at all: `dead`
   excuses a class that looks like the root of a block whose parts are
   defined, and while these lived in the page's own <style> block
   `.aud-jump` sat under that excuse next to eleven defined `.aud-*`
   siblings. It rendered as body-coloured text with no space above it. */
.aud-jump { display: inline-block; margin-block-start: 6px; font-size: 12.5px; font-weight: 600; color: var(--accent-deep); text-decoration: none; }
.aud-jump:hover { text-decoration: underline; }

/* ── A part's movement ledger ─────────────────────────────────────────
   Rendered by both stock item pages (partials/_stock_item_body.html).
   These were contractor/stock.css, which the org portal does not load —
   the org page drew the same six facts as a `<table class="table">`
   instead, and that class is defined in the ADMIN shell, so neither
   spelling worked in both places. §1. */
/* Stock, contractor side — the movement ledger's own rows.
 *
 * The shelf components this feature shares with the org page
 * (.stock-locations, .stock-row, .stock-qty, .stock-delta) live in
 * common/_components.css by CLAUDE.md §2: more than one portal renders
 * them, and a second copy here is what the `duplicate-rules` check exists
 * to stop. What is left in this file is the one thing only this portal
 * has — a ledger drawn as rows instead of a table, because .table lives
 * in common/_shell.css and the field shell does not load it (see the
 * template's own note). That sentence was written before it was true:
 * `.table` defined nothing anywhere until 2026-08-23, so the rows here
 * were the right call for a reason one step over from the stated one.
 */

.stock-moves {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.stock-move {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 11px 14px;
}

.stock-move-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
}

.stock-move-what {
    font-size: 14px;
    font-weight: 600;
    color: var(--text-primary);
}

.stock-move-sub {
    margin-top: 2px;
    font-size: 12px;
    color: var(--text-secondary);
}

.stock-move-note {
    margin: 6px 0 0;
    font-size: 13px;
    color: var(--text-primary);
}

/* ── The processing-queue row ─────────────────────────────────────────
   partials/_queue_row.html, rendered by both portals: works-order and
   supplier-invoice imports on the contractor side, the bills list on
   the org side. It was contractor/_layout.css, which the org portal
   does not load — which is why the org list had drawn the same row by
   hand instead (§1, §13). */
.queue-row { display: flex; align-items: center; gap: 12px; padding: 12px 0; }
.queue-row + .queue-row { border-top: 1px solid var(--mist); }
.queue-row-icon {
    display: flex; align-items: center; justify-content: center;
    width: 34px; height: 34px; flex-shrink: 0; border-radius: 9px;
    background: var(--teal-pale); color: var(--teal-deep); font-size: 17px;
}
.queue-row-body { flex: 1; min-width: 0; }
.queue-row-title {
    margin: 0; font-size: 14px; font-weight: 600; color: var(--ink);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.queue-row-sub { margin: 2px 0 0; font-size: 12.5px; color: var(--text-secondary); line-height: 1.45; }
.queue-row-action { flex-shrink: 0; }
/* "Reading…" — a state, not a control. This used to be class="badge",
   which has never existed in this stylesheet, so it rendered as bare text. */
.queue-row-note {
    flex-shrink: 0; font-size: 12px; font-weight: 600;
    color: var(--text-secondary); font-style: italic;
}


/* ── A section heading in a list ──────────────────────────────────────
   partials/_section_heading.html. Rendered by both portals since the
   bills list stopped being drawn twice: the org's copy was a
   `<p class="section-title">Needs review (4)</p>`, the count in the label
   where nothing could style it. Was contractor/_layout.css, which the org
   portal does not load (§1, §13). */
.list-section-head {
    display: flex; align-items: center; gap: 8px;
    margin: 22px 2px 10px;
}
.list-section-head:first-child { margin-top: 4px; }
.list-section-title {
    margin: 0; font-family: var(--font-display); font-weight: 700;
    font-size: 12px; text-transform: uppercase; letter-spacing: 0.6px;
    color: var(--text-secondary);
}
.list-section-count {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 20px; height: 20px; padding: 0 6px; border-radius: 10px;
    background: var(--mist); color: var(--text-secondary);
    font-size: 11.5px; font-weight: 700; font-variant-numeric: tabular-nums;
}
/* Late work is the one thing on this page that should catch the eye
   before it's read. Nothing else gets to use this. */
.list-section-count--urgent {
    background: color-mix(in srgb, var(--danger) 14%, transparent);
    color: var(--danger-deep);
}

/* ── The richer empty state's icon tile ───────────────────────────────
   Moved out of contractor/_layout.css when the bills queue became one
   partial for both portals (partials/_bill_rows.html): its "No bills yet"
   state carries this, and on the org page the class was defined nowhere
   the page loads — a 64px tile rendering as an unstyled glyph, which is
   §1 and what `unstyled` reported the moment the partial was shared.

   The plain `.empty-state .ti` (a bare 34px mist icon) stays the default
   for every simpler "nothing here yet" list; these rules only apply where
   a page opts into the bigger tile. Same specificity as `.empty-state
   .ti`, so it has to come LATER in the cascade than that rule — it does,
   ~700 lines below it in this file. */
.empty-state-icon {
    width: 64px;
    height: 64px;
    margin: 0 auto 16px;
    border-radius: var(--radius-lg);
    background: color-mix(in srgb, var(--teal) 12%, transparent);
    color: var(--teal-deep);
    display: flex;
    align-items: center;
    justify-content: center;
}
.empty-state-icon .ti { font-size: 30px; color: inherit; margin: 0; display: block; }

/* Shared assistant setup in organisation and contractor Connections. */
.assistant-intro,
.knowledge-search,
.assistant-setup { min-inline-size: 0; overflow-wrap: anywhere; }
.assistant-intro { margin-block-end: 24px; }
.assistant-intro h3 { margin-block: 16px 8px; font-size: 1rem; }
.assistant-intro-prompts,
.assistant-intro-start { padding-inline-start: 24px; margin-block: 0 12px; }
.assistant-intro-prompts > li { margin-block: 4px; }
.assistant-locked { margin-block-start: 16px; }
.assistant-setup { margin-block-end: 24px; }
.assistant-setup select, .assistant-setup input:not([type="hidden"]) { inline-size: 100%; box-sizing: border-box; }
.assistant-setup-stage { margin-block-start: 24px; }
.assistant-setup-steps { padding-inline-start: 24px; }
.assistant-setup-steps > li { margin-block: 20px; padding-inline-start: 4px; }
.assistant-setup h3 { margin-block: 24px 12px; }
.assistant-setup h4 { margin-block: 0 8px; }
.assistant-setup code, .assistant-setup p { overflow-wrap: anywhere; }
.assistant-setup pre { white-space: pre-wrap; overflow-wrap: anywhere; padding: 12px; background: var(--bg-subtle); border-radius: var(--radius-sm); }
.assistant-setup dt { font-weight: 600; margin-block-start: 12px; }
.assistant-setup dd { margin-inline-start: 0; }
.assistant-setup-button { display: inline-flex; align-items: center; justify-content: center; padding: 10px 16px; border: 0; border-radius: var(--radius-sm); background: var(--teal-deep); color: var(--white); font: inherit; cursor: pointer; }
.assistant-setup-advanced { list-style: none; margin-inline-start: -24px; }
.assistant-setup-advanced summary { cursor: pointer; font-weight: 600; }
.knowledge-search form { margin-block-start: 16px; }
.copy-field {
    display: flex; align-items: center; gap: 10px; width: 100%;
    box-sizing: border-box;
    margin-block-start: 8px; padding: 11px 14px; border-radius: var(--radius-md);
    border: 1.5px solid var(--border); background: var(--paper);
    font-family: inherit; font-size: 13.5px; color: var(--ink);
    text-align: start; cursor: pointer;
}
.copy-field:hover { border-color: var(--teal); }
.copy-field-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.copy-field .ti { flex-shrink: 0; font-size: 17px; color: var(--teal-deep); }
.copy-field--done { border-color: var(--success); }
.copy-field--done .ti { color: var(--success); }
.copy-field--done .copy-field-value::after { content: " — copied"; color: var(--success); font-weight: 600; }

/* ── Labour terms — the one form (§17.27 D3) ────────────────────────────
   partials/_labour_terms.html, drawn on the contractor AND org portals,
   which is why it lives here (CLAUDE.md §2). A segmented "Priced as"
   control over the PricingType words, then only the figures that kind
   needs; labour-terms.js moves the hidden/disabled state and asks the
   server for the "The client sees:" line. Grounds and inks are token
   pairs that both move (dark-pair). */
.lt { display: grid; gap: 12px; }
/* A lone visible figure (hourly, fixed) fills the row rather than sitting
   in half of it beside a hidden sibling. */
.lt .form-row { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
.lt-kind { border: 0; padding: 0; margin: 0; min-width: 0; }
.lt-legend { font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
.lt-kind-row {
  display: flex;
  gap: 4px;
  padding: 3px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-subtle, var(--surface));
  overflow-x: auto;
  scrollbar-width: none;
}
.lt-kind-row::-webkit-scrollbar { display: none; }
.lt-kind-option {
  flex: 1 1 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 8px 10px;
  border-radius: 8px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-secondary);
  white-space: nowrap;
  cursor: pointer;
}
.lt-kind-option input { position: absolute; opacity: 0; width: 0; height: 0; }
.lt-kind-option--active { background: var(--surface); color: var(--ink); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); }
.lt-kind-option:focus-within { outline: 2px solid var(--accent, var(--teal)); outline-offset: 1px; }
.lt-hint { margin: 6px 0 0; }
.lt-inclusion > summary { cursor: pointer; list-style: none; }
.lt-inclusion > summary::-webkit-details-marker { display: none; }
.lt-inclusion > summary::before { content: "▸ "; }
.lt-inclusion[open] > summary::before { content: "▾ "; }
.lt-inclusion .form-row { margin-top: 8px; }
.lt-preview { margin: 0; font-size: 13px; color: var(--ink); }
.lt-preview-label { color: var(--text-secondary); margin-inline-end: 4px; }
.lt-preview--problem [data-lt-sentence] { color: var(--danger, var(--text-secondary)); }

/* ── Hours and minutes ──────────────────────────────────────────────────────
   Two whole numbers instead of a decimal with a quarter-hour step, because
   the value comes off a clock: a 1 h 57 m visit is 1.95, which nobody should
   have to work out and which `step="0.25"` called invalid (§17.35 Q41). */
.duration-field { display: flex; align-items: end; gap: 8px; }
.duration-field__part { display: flex; align-items: center; gap: 6px; min-width: 0; }
.duration-field__part .form-control { margin: 0; width: 5.5em; min-width: 0; }
.duration-field__unit { font-size: 13px; color: var(--text-secondary); }

/* ── Sales channels card (partials/_sales_channels.html) ─────────────
   One block per channel. A channel that is not set up is one row — its
   name, one line, its button — and a connected one keeps its actions in
   view and its settings behind a disclosure. A channel's refusals are one
   closed line per reason, opening on the products, each linked to its
   editor (Talha 2026-10-04: sixty "sku: error" lines were a wall). The
   disclosures inherit currentColor, so they read inside an .alert too. */
.channel-block, .channel-row {
  padding-block: 14px;
  border-top: 1px solid var(--border);
}
.channel-block:first-of-type, .channel-row:first-of-type { border-top: 0; }
.channel-block > * + * { margin-top: 8px; }
.channel-row { display: flex; align-items: center; gap: 12px; }
.channel-row-main { flex: 1; min-width: 0; }
.channel-row-main > p { margin: 0; }
.channel-row > form { flex: none; margin: 0; }
.channel-name {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-weight: 600;
  color: var(--ink);
}
.channel-problems > * + * { margin-top: 6px; }
.channel-problems-head { margin: 0; }
.channel-refusal-toggle, .channel-settings-toggle {
  display: flex;
  align-items: baseline;
  gap: 8px;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.channel-refusal-toggle::-webkit-details-marker,
.channel-settings-toggle::-webkit-details-marker { display: none; }
.channel-refusal-caret { flex: none; align-self: flex-start; margin-top: 0.2em; transition: transform 0.15s ease; }
details[open] > summary > .channel-refusal-caret { transform: rotate(90deg); }
.channel-refusal-count {
  flex: none;
  min-width: 2.2em;
  text-align: center;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.channel-refusal-why { min-width: 0; overflow-wrap: anywhere; }
.channel-refusal-next { margin: 6px 0 0; font-weight: 600; }
.channel-refusal-words { margin: 4px 0 0; opacity: 0.8; overflow-wrap: anywhere; }
.channel-refusal-list { margin: 6px 0 4px; padding-inline-start: 22px; }
.channel-refusal-list li { margin-bottom: 2px; }

.channel-refusal-sku { opacity: 0.7; font-variant-numeric: tabular-nums; }
.channel-settings-toggle { font-weight: 600; color: var(--text-secondary); padding-block: 4px; }
.channel-settings[open] > .channel-settings-toggle { margin-bottom: 8px; }
.channel-push { font-size: 13px; color: var(--text-secondary); }
.channel-push > p { margin: 0; }
@media (max-width: 480px) {
  .channel-row { flex-wrap: wrap; }
  .channel-row > form { flex-basis: 100%; }
}
.channel-problems a { color: inherit; text-decoration: underline; }

/* The channels overview's filter bar and count (§17.62 M2): one row on a
   desk, the search on its own line and the rest two-up on a phone. */
.channel-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  margin: 4px 0 12px;
}
.channel-filters > .form-control { flex: 0 1 auto; width: auto; min-width: 0; }
.channel-filters > .channel-filters-search { flex: 1 1 220px; }
.channel-filters-check { margin: 0; white-space: nowrap; }
.channel-filters-go { display: inline-flex; align-items: center; gap: 12px; }
.channel-overview-count {
  margin: 0 0 8px;
  font-size: 13px;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
/* A state reads "Chosen, not sent yet", so its column is wider than stock's. */
.stock-row--channels { grid-template-columns: minmax(0, 1fr) 150px 80px 90px; }
.channel-bulk {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}
.channel-bulk-scope { display: grid; gap: 6px; margin: 12px 0; padding: 0; border: 0; }
.channel-bulk-scope > legend { padding: 0; margin-bottom: 6px; }
@media (max-width: 640px) {
  .channel-filters > .form-control { flex: 1 1 40%; }
  .channel-filters > .channel-filters-search { flex-basis: 100%; }
  .stock-row--channels { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .channel-bulk { grid-template-columns: minmax(0, 1fr); }
}
