/* ===================================================================
   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.
   =================================================================== */

/* Colours this sheet alone uses, named so none of them is a bare hex.
   Same values as before — this changed no pixel. */
:root {
  --chip-stripe-ink: #4F46E5;
}

/* ── 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. */
.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;
  right: 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: 0 0 6px -4px;
  border-radius: var(--radius-sm, 6px);
  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-left: -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-right: 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;
  left: 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: left;
  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, 6px);
  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: right; }
.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; }
/* 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, 10px);
  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, 10px);
  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);
}

/* ── 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-left: 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, 10px);
  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)); }

/* 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, 10px);
  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:hover { background: var(--danger-deep); }
.btn-primary: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);
}

/* 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-right: 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, 14px);
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
}

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

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

/* A phone gets the sheet it expects: full width, off the bottom edge, and
   square where it meets it. */
@media (max-width: 480px) {
  .modal {
    width: 100vw;
    max-width: 100vw;
    max-height: 92vh;
    margin: auto auto 0;
    border-radius: var(--radius-lg, 14px) var(--radius-lg, 14px) 0 0;
  }
  .modal-inner { max-height: 92vh; }
}

/* ── 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, 8px);
  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;
  left: 4px;
  right: 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 { left: 8px; right: 8px; }
.docstyle-mini--classic::after { height: 2px; }

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

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

/* bold — a heavy rule under an oversized title. */
.docstyle-mini--bold::before { right: 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, 8px);
  background: var(--surface);
}
.docstyle-preview-frame iframe {
  position: absolute;
  top: 0;
  left: 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: right;
}

.stock-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 18px;
}

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