/* ===================================================================
   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);
  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;
}

/* 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);
  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);
}

@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); color: var(--white); }
.flash--danger  { background: var(--danger);  color: var(--white); }

.btn-danger { background: var(--danger); color: var(--white); }
.btn-danger:not(:disabled):hover { background: var(--danger-deep); }
.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);
  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;
}
.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;
}

/* 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; }
}

/* ── 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;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
}

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

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

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

.stock-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 11px 16px;
  border-bottom: 1px solid var(--border);
}

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

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

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

.stock-row-part a:hover { text-decoration: underline; }

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

/* 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;
}

/* 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;
}

/* 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); }

@media (max-width: 640px) {
  .stock-locations { grid-template-columns: 1fr; }
}

/* ── 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; }
}

/* 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 stays scoped to `.admin-content`, which the org and
   platform shells render and common/_shell.css defines — so the office
   and the org get the two-column reading, and the field app at every
   width keeps the stacked page it already was.
   =================================================================== */

.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. */
.review-lines { margin: 0 -18px; }
.review-line { padding: 12px 18px; }
.review-line:nth-child(even) { background: var(--paper); }
/* 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. */

@media (min-width: 1200px) {
    /* 1200 is the scale's top step and the same one the SRD rail
       collapses at. The office shell spends 220px of it on the sidebar
       and 64px on .admin-content's padding, so the arithmetic here is
       1200 - 284 - 360 - 20 = a 536px form column at the narrowest this
       ever applies, growing with the window. Under that the form is
       tighter than the field app it came from, which is why this does
       not reach further down the scale. */
    .admin-content .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 { grid-column: 1; grid-row: 1; }
    .admin-content .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;
    }
    /* 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 { max-height: min(60vh, 520px); }
    .admin-content .review-pair { grid-template-columns: 1fr 1fr; }
}

/* ── 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); 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; }
