/* ===================================================================
   common/_shell.css — shared admin-shell design system
   Loaded by org/platform/_layout.html after common/_tokens.css (the
   single source for values shared with the customer app too) and each
   role's own small _layout.css (role tokens + role-specific rules).
   Every rule here is written against generic tokens (--accent,
   --accent-deep, --sidebar-bg, ...) so each role's own token file can
   swap the theme without touching this file.
   =================================================================== */

/* Colours this sheet alone uses, named so none of them is a bare hex.
   Same values as before — this changed no pixel. */
:root {
  --kpi-icon-purple-ink: #7C3AED;
}

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

:root {
  /* Admin dashboards' own design language — tighter radius scale and
     Inter-only display font, distinct from the customer app's. ink/white/
     border/text-secondary/text-tertiary/font-body come from
     common/_tokens.css and are NOT redeclared here — this sheet always
     loads after it.
     --navy aliases straight to --ink (Primary Navy) rather than its own
     hex — the brand sheet's own "Navigation bar" example uses Primary
     Navy itself for dark nav backgrounds, not a separate darker shade. */
  --navy:           var(--ink);
  /* The admin sidebar's ground. org/_layout.css, platform/_layout.css and
     contractor/office.css each declare this too and a paid brand_color
     overrides it !important (partials/_brand_theme.html) — but THIS is
     the sheet that paints the sidebar, and it was reading a token it
     never declared. It is the hero ground rather than --ink because the
     sidebar is dark in both schemes, the same permanently-dark chrome
     the customer topbar and sidebar are. */
  --sidebar-bg:     var(--hero-ground);
  --paper:          var(--bg-subtle);
  --mist:           #E6EEF2;
  --radius-sm:      6px;
  --radius-md:      10px;
  --radius-lg:      14px;
  --shadow-card:    0 1px 3px color-mix(in srgb, var(--ink) 6%, transparent), 0 4px 12px color-mix(in srgb, var(--ink) 6%, transparent);
  --font-display:   "Inter", system-ui, -apple-system, sans-serif;

  /* The card outline the dashboards draw instead of a resting shadow —
     see the .kpi-card block below for why the shadow moved to hover.
     Derived from --border rather than given a hex of its own: --border at
     full strength is the app's form-control rule and reads as a heavier
     line than a card wants, and deriving means the dark palette's
     --border/--surface pair carries it without a second declaration.
     --paper stays the INNER hairline (card headers, table rows); this is
     the outer edge, and they are deliberately two weights. */
  --card-line:      color-mix(in srgb, var(--border) 58%, var(--surface));
}

/* moby theme — see common/_tokens.css for the shared color/font block this
   layers on top of. Admin dashboards (org + platform, both loaders of this
   file) get the marketing site's condensed display headline face and its
   sharper, less-rounded card language; --paper warms slightly to match the
   theme's paper-100/-200 rather than staying a cool gray. */
:root[data-theme="moby"] {
  --font-display: "Big Shoulders Display", sans-serif;
  --paper:        #F4F1E8;
  --radius-sm:    3px;
  --radius-md:    6px;
  --radius-lg:    10px;
}

:root[data-theme="slate"] {
  --font-display: "Space Grotesk", sans-serif;
  --paper:        #EEF1F3;
  --radius-sm:    4px;
  --radius-md:    8px;
  --radius-lg:    12px;
}

:root[data-theme="meadow"] {
  --font-display: "Lora", serif;
  --paper:        #F1F3EC;
  --radius-sm:    10px;
  --radius-md:    16px;
  --radius-lg:    22px;
}

:root[data-theme="clay"] {
  --font-display: "DM Serif Display", serif;
  --paper:        #F5EFE7;
  --radius-sm:    8px;
  --radius-md:    14px;
  --radius-lg:    20px;
}

/* ── The admin shell's dark half ──────────────────────────────────────
   Last, so it outranks the four [data-theme] blocks above on source
   order — they tie at (0,2,0) and each of them pins --paper to a warm
   literal, which is the page ground here (`html, body` below reads it).
   The classic default two blocks up already says `var(--bg-subtle)` and
   so was the one shape that survived the first dark pass intact; these
   four are the ones that would have kept a moby org's #F4F1E8 linen
   under near-white body text.

   Note --paper means something DIFFERENT on this sheet than it does in
   customer/_layout.css: there it is a card, here it is the page under
   the cards. Same name, opposite end of the stack, which is why each
   sheet has to answer for its own rather than the token file answering
   for both.

   The shadow is the other thing that cannot be inherited. It is built
   from --ink, and --ink flips to near-white — so every admin card came
   out of the first pass wearing a soft white glow instead of a shadow.
   Rebuilt on --bg-subtle, which in dark is the near-black page ground,
   at the heavier alphas a dark UI needs to show depth at all. */
:root[data-appearance="dark"] {
  --paper:       var(--bg-subtle);
  --shadow-card: 0 1px 3px color-mix(in srgb, var(--bg-subtle) 45%, transparent), 0 4px 12px color-mix(in srgb, var(--bg-subtle) 60%, transparent);
}

html, body {
  margin: 0;
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* ── The Appearance picker, sidebar copy ──────────────────────────────
   The shared component lives in common/_components.css; this is the
   modifier for drawing it on the sidebar, which is --sidebar-bg — the
   hero ground, dark in BOTH schemes. So it cannot inherit the page's
   track and chip: --mist and --white flip with the scheme and would
   render a light rectangle on a navy rail either way round. It uses the
   same rgba-white chrome as the rest of this rail instead, which is
   already how .admin-brand's rule and .admin-logout-btn's border are
   drawn — those are correct at any scheme for the same reason.

   Not in _components.css because no other portal has a sidebar to put
   it on: the three mobile shells render the .menu-row copy on their More
   screen instead.

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

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

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

/* ===================================================================
   Flash messages
   =================================================================== */
.flash-stack {
  position: fixed;
  top: 16px;
  right: 16px;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-width: 380px;
}

.flash {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 12px 12px 16px;
  border-radius: var(--radius-md);
  font-size: 13.5px;
  font-weight: 500;
  box-shadow: 0 4px 16px rgba(0,0,0,0.1);
  word-break: break-word;
  opacity: 1;
  transition: opacity 250ms, transform 250ms;
}
.flash--dismissing { opacity: 0; transform: translateX(12px); }
.flash--warning  { background: var(--warning); color: var(--white); }
.flash--info     { background: var(--accent-deep); color: var(--white); }

.flash-close {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: rgba(255,255,255,0.18);
  color: inherit;
  border-radius: 50%;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  opacity: 0.85;
}
.flash-close:hover { opacity: 1; background: rgba(255,255,255,0.3); }

/* ===================================================================
   Admin shell — sidebar + main
   =================================================================== */
.admin-shell {
  display: flex;
  min-height: 100vh;
}

.admin-sidebar {
  width: var(--sidebar-w);
  flex-shrink: 0;
  background: var(--sidebar-bg);
  display: flex;
  flex-direction: column;
  padding: 24px 0;
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
}

.admin-brand {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 20px 24px;
  border-bottom: 1px solid rgba(255,255,255,0.08);
  margin-bottom: 16px;
}

.admin-brand .brand-mark {
  width: 26px;
  height: 22px;
  color: var(--accent);
  flex-shrink: 0;
}

.admin-brand .brand-word {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 14px;
  color: rgba(255,255,255,0.9);
  letter-spacing: 0.2px;
}
.admin-brand .brand-word b { font-weight: 800; color: var(--accent); }
.admin-brand .brand-logo {
  /* Uploaded logos keep their real colors — no brightness(0) invert(1),
     which used to flatten every logo to an illegible white silhouette.
     Fixed height/width (not max-*) so object-fit scales small SVGs UP
     to fill the slot instead of rendering at their tiny intrinsic size. */
  height: 52px;
  width: 172px; /* sidebar 220px − 2×20px brand padding, a hair inside */
  object-fit: contain;
  object-position: left center;
}

.admin-brand-badge {
  display: block;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--accent) 70%, transparent);
  margin-top: 1px;
}

.admin-nav {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0 10px;
  overflow-y: auto;
  min-height: 0;
}

.admin-nav-section {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: rgba(255,255,255,0.3);
  padding: 14px 12px 6px;
}

.admin-nav-link {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 38px;
  padding: 0 12px;
  border-radius: var(--radius-md);
  color: rgba(255,255,255,0.6);
  text-decoration: none;
  font-size: 13.5px;
  font-weight: 500;
  transition: background 120ms ease, color 120ms ease;
}
.admin-nav-link .ti { font-size: 17px; }
.admin-nav-link:hover { background: rgba(255,255,255,0.07); color: rgba(255,255,255,0.9); }
.admin-nav-link--active {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
}
.admin-nav-link > span:not(.admin-nav-badge) { flex: 1; }

/* Real "needs attention" count, computed on every org page load (see
   app/services/org/nav_badges.py) and live-bumped by the same realtime
   events the queue pages themselves listen for (org_realtime_toasts.js). */
.admin-nav-badge {
  flex-shrink: 0;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--danger);
  color: var(--white);
  font-size: 11px;
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.admin-logout-wrap {
  padding: 16px 10px 0;
  border-top: 1px solid rgba(255,255,255,0.08);
  margin-top: 16px;
}

.admin-logout-btn {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  height: 38px;
  padding: 0 12px;
  border: none;
  border-radius: var(--radius-md);
  background: transparent;
  color: rgba(255,255,255,0.45);
  font-family: var(--font-body);
  font-size: 13.5px;
  font-weight: 500;
  cursor: pointer;
  text-align: left;
  transition: background 120ms ease, color 120ms ease;
}
.admin-logout-btn .ti { font-size: 17px; }
.admin-logout-btn:hover { background: rgba(255,255,255,0.07); color: rgba(255,255,255,0.75); }

/* ===================================================================
   Main content area
   =================================================================== */
.admin-main {
  margin-left: var(--sidebar-w);
  flex: 1;
  min-width: 0;
}

.admin-content {
  padding: 40px 32px;
}

/* ===================================================================
   Small/narrow screens (<= 960px)
   The sidebar is fixed-width at every size; below this breakpoint it
   goes off-canvas (translated out of view) and a topbar — hidden above
   the breakpoint — takes its place, with a hamburger button that adds
   `admin-shell--nav-open` to .admin-shell (see admin-nav-toggle.js) to
   bring it back on top of the page as a drawer. Everything else here
   (tables, forms, grids) is each page's own concern; this block only
   owns the shell chrome shared by every org/platform page.
   =================================================================== */
.admin-topbar { display: none; }

@media (max-width: 900px) {
  .admin-topbar {
    display: flex;
    align-items: center;
    gap: 12px;
    height: 56px;
    padding: 0 8px 0 16px;
    background: var(--sidebar-bg);
    position: sticky;
    top: 0;
    z-index: 45;
  }
  .admin-topbar .brand-mark { width: 22px; height: 18px; color: var(--accent); flex-shrink: 0; }
  .admin-topbar .brand-word {
    font-family: var(--font-display);
    font-weight: 600;
    font-size: 14px;
    color: rgba(255,255,255,0.92);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .admin-topbar .brand-logo { height: 30px; width: auto; max-width: 140px; object-fit: contain; object-position: left center; }

  .admin-nav-toggle {
    width: 36px;
    height: 36px;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: rgba(255,255,255,0.08);
    border: none;
    border-radius: var(--radius-sm);
    color: var(--white);
    font-size: 19px;
    cursor: pointer;
  }
  .admin-nav-toggle:hover { background: rgba(255,255,255,0.16); }

  /* Sidebar becomes a fixed-position drawer, off-screen until opened. */
  .admin-sidebar {
    transform: translateX(-100%);
    transition: transform 200ms ease;
    z-index: 70;
    width: min(var(--sidebar-w), 84vw);
  }
  .admin-shell--nav-open .admin-sidebar {
    transform: translateX(0);
    box-shadow: 8px 0 28px rgba(0,0,0,0.28);
  }

  .admin-nav-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(10,14,18,0.45);
    z-index: 65;
  }
  .admin-shell--nav-open .admin-nav-backdrop { display: block; }

  /* Content no longer reserves room for a permanently-docked sidebar. */
  .admin-main { margin-left: 0; width: 100%; }
  .admin-content { padding: 20px 16px; }

  .page-title { font-size: 19px; }
}

@media (max-width: 640px) {
  .admin-content { padding: 16px 12px; }
}

/* Belt-and-suspenders: whatever triggered the drawer open shouldn't also
   let the page scroll underneath it (mobile Safari especially will
   happily scroll both at once otherwise). Toggled by JS, not by width,
   since it only matters while the drawer is actually open. */
.admin-nav-open-lock { overflow: hidden; }

/* ===================================================================
   Admin tables — the wrapper scrolls, the table stays a table

   This used to be `.admin-content table { display: block; overflow-x:
   auto }` — a safety net so a wide table scrolled instead of blowing the
   page out past the fixed sidebar. It worked for that, and quietly broke
   everything else: `display: block` on a <table> does NOT make its rows
   fill the box. The element becomes a block that stretches to the card,
   its children generate an anonymous table box, and THAT box is sized to
   its content. So every admin table narrower than its card stopped its
   header tint and row borders after the last column and left white to the
   right — the default state, not an edge case, and the reason this was
   reported on four separate pages.

   There is no CSS fix for that while the table is a block: an auto-width
   table box is shrink-to-fit by definition. The only real fix is the one
   here — the table stays `display: table` and a wrapper does the
   scrolling. Every admin table now has one (.table-scroll, or a
   page-specific card that already sets overflow-x), so the blanket rule
   and the ever-growing list of classes opting back out of it are both
   gone.

   New table? Wrap it in .table-scroll. Nothing enforces that, but the
   failure is visible immediately rather than at a wide enough window. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* ===================================================================
   Page title/subtitle — the .page-header wrapper layout itself is
   role-specific (some pages stack title+sub, some put an action button
   alongside), so it stays in each role's own _layout.css.
   =================================================================== */

/* page_header(back_href=...)'s back arrow — sits on its own line above
   the title regardless of whether the role's own .page-header/
   .page-header-left is flexed (platform) or plain block flow (org), so
   one rule works for both. */

/* ===================================================================
   Shared card
   =================================================================== */
.card {
  background: var(--white);
  border-radius: var(--radius-lg);
  padding: 24px;
  box-shadow: var(--shadow-card);
  margin-bottom: 20px;
}

/* ===================================================================
   Buttons
   =================================================================== */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 38px;
  padding: 0 18px;
  border: none;
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  transition: background 120ms ease, opacity 120ms ease;
  white-space: nowrap;
}
.btn .ti { font-size: 16px; }

.btn-primary { background: var(--accent); color: var(--white); }

.btn-outline {
  background: transparent;
  color: var(--accent);
  border: 1.5px solid color-mix(in srgb, var(--accent) 35%, transparent);
}
.btn-outline:hover { background: color-mix(in srgb, var(--accent) 7%, transparent); }

/* .btn-secondary is an alias for .btn-ghost — contractor's stylesheet
   uses that name for the same neutral/bordered treatment, and templates
   here have picked up the habit assuming it's a universal name. Keeping
   both means either spelling renders correctly everywhere. */
.btn-ghost, .btn-secondary {
  background: var(--paper);
  color: var(--ink);
  border: 1.5px solid var(--mist);
}
.btn-ghost:hover, .btn-secondary:hover { background: var(--mist); }


/* Destructive, but quiet enough to repeat down every row of a table:
   neutral weight until the pointer is actually on it. Solid .btn-danger is
   right for one decisive action on a page — a whole column of them reads
   as a warning about the table rather than about any row in it. */
.btn-danger-ghost {
  background: var(--paper);
  color: var(--danger-deep);
  border: 1.5px solid color-mix(in srgb, var(--danger) 30%, transparent);
}
.btn-danger-ghost:hover {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--white);
}

.btn-sm { height: 32px; padding: 0 14px; font-size: 12.5px; }

/* The form-footer submit. Lived in org/settings.css. */
.btn-save {
  height: 40px;
  padding: 0 20px;
  background: var(--ink);
  color: var(--white);
  border: none;
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background 120ms ease;
}
.btn-save:hover { background: var(--navy); }

/* One step smaller than .btn-sm — the action that sits inside a table cell
   or beside a heading. Lived in org/services.css, which meant every other
   page reaching for it (reports, team calendar, inspection detail, the
   platform webhook log) rendered a native browser button; it belongs here,
   where both the org and platform shells already load it. */
.btn-xs {
  height: 26px;
  padding: 0 10px;
  font-size: 12px;
  font-weight: 600;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--white);
  color: var(--text-secondary);
  cursor: pointer;
  transition: border-color 120ms, color 120ms;
  white-space: nowrap;
}
.btn-xs:hover { border-color: var(--teal); color: var(--teal-deep); }
.btn-xs--ghost { color: var(--text-secondary); }
.btn-xs--hide  { color: var(--amber-ink); border-color: rgba(180,83,9,0.25); }
.btn-xs--hide:hover { background: rgba(180,83,9,0.06); }
.btn-xs--show  { color: var(--success); border-color: color-mix(in srgb, var(--success) 25%, transparent); }
.btn-xs--show:hover { background: color-mix(in srgb, var(--success) 6%, transparent); }

/* ===================================================================
   Row action clusters
   =================================================================== */
/* The buttons in a table's last cell (platform contractors, plans, org
   detail). Grouped, because as bare siblings each <form> renders
   block-level and they stack into a ragged column of different widths —
   which is what these looked like.

   A wrapper element rather than display:flex on the <td> itself: that
   stops the cell being a table-cell at all, which survives only while it
   is the last column and breaks the row silently the moment one is added
   after it. plans.css carries the long version of that note. */
.row-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  white-space: nowrap;
}
.row-actions form { margin: 0; }

/* The destructive action goes last and set apart — a wider gap and a
   hairline, so it is never the one you hit while aiming for the neutral
   button beside it. Same instinct as keeping Deactivate off the end of a
   form's Save row, applied to a cluster instead of a footer. */
.row-actions-sep {
  margin-left: 2px;
  padding-left: 8px;
  border-left: 1px solid var(--mist);
}

/* ===================================================================
   Status + priority pills — shared across appointments, customers, dashboard
   =================================================================== */
.appt-status {
  display: inline-flex;
  align-items: center;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.3px;
  padding: 3px 9px;
  border-radius: 20px;
  text-transform: capitalize;
  white-space: nowrap;
}
/* Status colours: add `status--<tone>` alongside this class and
   common/_components.css paints it. The tone comes from
   api/core/status_tones.py via the `|status_tone` filter, which is the
   same declaration the API serves every client — so this pill, the
   customer's own pill for the same word, and the phone cannot drift apart.
   Geometry stays here. */

/* A submission is queued on a device but hasn't reached the server yet —
   sits alongside .appt-status (which still reads the record's real status,
   e.g. in_progress), not a replacement for it. Same warning tone as a
   scheduled visit since both mean "needs attention", but its own class
   since it's an orthogonal flag, not a status value. */
/* .unsubmitted-badge is its neighbour, not its twin: that one means the
   device is still working on it and staff should wait, this one means the
   answers are all in and somebody has to press Submit. Different actions,
   so different colours — but one geometry, since they sit in the same cell
   and a row carrying either should read identically. */
.pending-sync-badge,
.unsubmitted-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.3px;
  padding: 3px 9px;
  border-radius: 20px;
  white-space: nowrap;
}
.pending-sync-badge {
  background: color-mix(in srgb, var(--warning) 14%, transparent);
  color: var(--amber-ink);
}
.unsubmitted-badge {
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  color: var(--danger-deep);
}
.pending-sync-badge .ti,
.unsubmitted-badge .ti { font-size: 12px; }
/* display:inline-flex above otherwise outranks the browser's own
   [hidden] { display: none } — see the same note in contractor/_layout.css. */
.pending-sync-badge[hidden],
.unsubmitted-badge[hidden] { display: none; }

.appt-priority {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.3px;
  padding: 1px 6px;
  border-radius: 20px;
}
/* Priority colours: common/_components.css. */

/* Service request reference chip — same #id shown to the customer in the
   "Reference #123" notification copy, surfaced everywhere org staff view
   that request (list, detail, work order, invoice). Reuses the indigo
   convo-chip--sr color already established in org/chat.css. */
.sr-ref {
  display: inline-block;
  font-size: 11px;
  font-weight: 600;
  padding: 2px 9px;
  border-radius: 10px;
  background: var(--status-new-bg);
  color: var(--indigo-ink);
  white-space: nowrap;
}

/* ===================================================================
   Shared form elements
   =================================================================== */
.form-group { margin-bottom: 18px; }


.form-control {
  display: block;
  width: 100%;
  height: 40px;
  padding: 0 12px;
  background: var(--white);
  border: 1.5px solid var(--mist);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--ink);
  outline: none;
  transition: border-color 150ms ease;
}
.form-control:focus { border-color: var(--accent); }
.form-control::placeholder { color: var(--text-secondary); }

/* A textarea wearing .form-control keeps its own height — the 40px above
   is a one-line control's height, and it was flattening every multi-line
   box on an admin page to a single line regardless of `rows`. The
   contractor and customer sheets each hand-rolled this fix years apart
   (three different min-heights between them); the admin portals never
   got one, so org and platform have been showing a nine-row compose as a
   40px slot. `height: auto` is the part that matters: it hands the box
   back to `rows`, which is where the author already said how tall. */
textarea.form-control {
  height: auto;
  min-height: 80px;
  padding: 10px 12px;
  line-height: 1.5;
  resize: vertical;
}

/* Baseline chrome for any text-like input/textarea/select that ISN'T given
   .form-control or wrapped in a scoped field class (the ~dozen ".xxx-field
   input" rules across org templates all hand-roll this same padding/
   border/radius). This is the fallback so a plain <input> can never render
   as raw unstyled browser chrome again — width/height are deliberately
   left out so it doesn't fight compact inline-row forms (which size fields
   with their own width/max-width). A more specific rule (.form-control,
   a field wrapper) still wins and can override any of this. */
input:not([type]),
input[type="text"],
input[type="email"],
input[type="tel"],
input[type="number"],
input[type="date"],
input[type="month"],
input[type="week"],
input[type="time"],
input[type="datetime-local"],
input[type="password"],
input[type="search"],
input[type="url"],
input[type="color"],
textarea,
select {
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--ink);
  background: var(--white);
}
input:not([type]):focus,
input[type="text"]:focus,
input[type="email"]:focus,
input[type="tel"]:focus,
input[type="number"]:focus,
input[type="date"]:focus,
input[type="month"]:focus,
input[type="week"]:focus,
input[type="time"]:focus,
input[type="datetime-local"]:focus,
input[type="password"]:focus,
input[type="search"]:focus,
input[type="url"]:focus,
input[type="color"]:focus,
textarea:focus,
select:focus {
  outline: none;
  border-color: var(--accent);
}

/* Native file inputs render as the bare browser default - style just the
   button part via ::file-selector-button so it matches btn-secondary.
   :not([hidden]) matters — a plain type+attribute selector otherwise
   outranks the browser's own `[hidden] { display: none }` rule and pops
   the native control back into view behind a custom attach button/label
   (chat composers, before/after photo pickers, ...). */
input[type="file"]::file-selector-button {
  margin-right: 10px;
  padding: 9px 14px;
  border: 1.5px solid var(--mist);
  border-radius: var(--radius-md);
  background: var(--white);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
input[type="file"]::file-selector-button:hover { background: var(--mist); }

/* Select */

/* Checkbox row */
.form-check {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 18px;
}
.form-check input[type="checkbox"] {
  width: 16px;
  height: 16px;
  accent-color: var(--accent);
  cursor: pointer;
}
.form-check label {
  font-size: 13.5px;
  color: var(--ink);
  cursor: pointer;
}

/* ===================================================================
   Password field — toggle-eye + strength meter
   Shared by every login/signup/reset-password page across roles.
   =================================================================== */
.password-field, .pw-wrap { position: relative; }
.password-field .form-control, .pw-wrap .form-control { padding-right: 42px; }
.toggle-password:hover { color: var(--ink); }

/* The strength meter and the match text moved to common/_components.css,
   which already owned half of them. */

/* Quiet note under a rate, a calendar legend or a table cell. */
.rate-notes { display: block; font-size: 11.5px; color: var(--text-secondary); margin-top: 2px; }

/* The plain settings panel, on both admin portals. */
.settings-card {
  background: var(--white);
  border-radius: var(--radius-lg);
  padding: 24px;
  box-shadow: var(--shadow-card);
}

/* ===================================================================
   Dashboard furniture — KPI tiles, the compact table, stat rows
   org/dashboard.css and platform/dashboard.css carried byte-identical
   copies of all of this. Both shells load this file, so it lives here
   once; anything genuinely different between the two dashboards stays
   in their own stylesheets.
   =================================================================== */
/* ===================================================================
   KPI grid
   ===================================================================
   auto-fit rather than a fixed column count: these dashboards render
   anywhere from three to a dozen tiles depending on which features the
   org's plan includes, and the fixed two-up below meant a 1920px screen
   showed two cards a row with half its width empty. 230px is the
   narrowest a tile still reads at (42px icon + value + label), so this
   packs four to six across a normal desktop and degrades to two or
   three on a narrow laptop. Below 900px each dashboard decides for
   itself — org/dashboard.css turns the row into a swipe strip.
   =================================================================== */
.kpi-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 14px;
  margin-bottom: 28px;
}

/* ── The tile, stacked ────────────────────────────────────────────────
   Icon on its own line, then the number, then what the number is. It was
   icon-beside-body until now, and the number paid for that: a 42px icon
   plus a 14px gutter is 56px off the front of every value, so the eight
   figures a dashboard exists to be read at a glance were the one thing on
   the card NOT aligned to its left edge. Stacked, the values line up down
   the column and the icon becomes what it always was — a category mark,
   not a label.

   Flat and outlined rather than shadowed. A resting shadow on every tile
   plus a shadow on every card below them gave a page of eight tiles and
   five cards thirteen floating planes and no hierarchy; the shadow moves
   to :hover, where it means "this one", and the outline does the resting
   work for a fraction of the visual weight.

   position: relative is for .kpi-risk, the only thing that sits in the
   corner the icon left free. */
.kpi-card {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--card-line);
  border-radius: var(--radius-lg);
  padding: 18px 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.kpi-card:hover {
  border-color: color-mix(in srgb, var(--accent) 40%, var(--card-line));
  box-shadow: 0 4px 20px color-mix(in srgb, var(--ink) 9%, transparent);
}
.kpi-icon {
  width: 36px;
  height: 36px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 17px;
  flex-shrink: 0;
}

/* The one thing that shares the icon's line: a tile whose number is bad
   news says so, rather than leaving a reader to know that a rising
   receivables figure is the wrong direction. Absolutely positioned so it
   needs no wrapper element in the eight templates that render a tile. */
.kpi-risk {
  position: absolute;
  top: 18px;
  right: 20px;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.3px;
  background: var(--status-bad-bg);
  color: var(--status-bad-fg);
}
.kpi-icon--green { background: color-mix(in srgb, var(--success) 10%, transparent); color: var(--success); }
.kpi-icon--blue {
  background: color-mix(in srgb, var(--secondary-blue) 12%, transparent);
  color: var(--secondary-blue);
}
.kpi-icon--teal { background: color-mix(in srgb, var(--teal) 10%, transparent); color: var(--teal-deep); }
.kpi-icon--orange {
  background: color-mix(in srgb, var(--warning) 12%, transparent);
  color: var(--warning-deep);
}
.kpi-icon--red { background: color-mix(in srgb, var(--danger) 10%, transparent); color: var(--danger); }
.kpi-icon--navy { background: color-mix(in srgb, var(--navy) 8%, transparent); color: var(--navy); }
.kpi-icon--purple { background: rgba(139,92,246,0.1); color: var(--kpi-icon-purple-ink); }
.kpi-value {
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 700;
  color: var(--ink);
  line-height: 1;
  margin: 0 0 4px;
  letter-spacing: -0.5px;
  font-variant-numeric: tabular-nums;
}
/* The label is what the number IS, so it reads at full ink and lets size
   and weight do the separating; the sub-line under it is the muted one.
   Both were --text-secondary before, the sub at opacity 0.75 on top of a
   token that had only just been darkened to clear AA — which took it
   straight back under. Two levels, both readable, is the same hierarchy
   the old three levels were reaching for. */
.kpi-label { font-size: 12.5px; font-weight: 500; color: var(--text-primary); margin: 0; }
.kpi-sub { font-size: 11.5px; color: var(--text-secondary); margin: 2px 0 0; }
.dash-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 15px 18px 13px;
  border-bottom: 1px solid var(--paper);
}
.dash-card-title {
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 700;
  color: var(--ink);
  margin: 0;
}
.dash-card-link { font-size: 12.5px; font-weight: 600; color: var(--accent); text-decoration: none; }
/* =================================================================== Recent orgs / recent requests table =================================================================== */ .dash-table { width: 100%; border-collapse: collapse; font-size: 13.5px; min-width: 600px; }
.dash-table th {
  text-align: left;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--text-tertiary);
  padding: 12px 22px;
  border-bottom: 1px solid var(--paper);
  background: transparent;
}
.dash-table td {
  padding: 14px 22px;
  border-bottom: 1px solid var(--paper);
  color: var(--ink);
  vertical-align: middle;
}
.dash-table tbody tr { transition: background 0.15s ease; }
.dash-table a { color: var(--ink); text-decoration: none; font-weight: 500; }
.dash-empty { text-align: center; padding: 40px 22px; color: var(--text-secondary); font-size: 13.5px; }

/* ===================================================================
   The same rows, stacked — below 900px
   ===================================================================
   .dash-table carries a 600px floor, so on a phone every dashboard
   table scrolls sideways inside its card and the status column someone
   opened the page to check starts off-screen. A table marked
   .dash-table--stack hides itself below 900px and its .dash-cards
   sibling — the same rows, the same links, one card each — takes over.
   Both live in the same live-region partial, so the swap survives a
   realtime re-render; nothing here needs JS.

   900px is the shell's own drawer breakpoint, reused deliberately: the
   width where the sidebar stops being a sidebar is the width where a
   six-column table stops being a table.
   =================================================================== */
.dash-cards { display: none; }

@media (max-width: 900px) {
  .dash-table--stack { display: none; }
  .dash-cards { display: flex; flex-direction: column; }
}

/* Rows of one list, not cards inside a card: a hairline between them
   and no shadow of their own — the .dash-card around them has one. */
.dash-cards-item {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--paper);
}
.dash-cards-item:last-child { border-bottom: none; }

/* Row one: what the thing is on the left, the chip that was its own
   column pinned to the right edge. */
.dash-cards-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
}
.dash-cards-title { font-size: 14px; font-weight: 600; color: var(--ink); text-decoration: none; }
.dash-cards-title:hover { color: var(--accent); }

/* A secondary line — one fact at each end, each eliding on its own so a
   long name can't push the date off the card. */
.dash-cards-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  font-size: 12.5px;
  color: var(--text-secondary);
}
.dash-cards-meta > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-cards-meta--sub { font-size: 12px; }

/* Whatever columns are left after the primary chip, as one wrapping row. */
.dash-cards-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }

/* A row-level action (the org board's Send reminder) sits under the
   facts at its natural width, not stretched across the card. */
.dash-cards-action { align-self: flex-start; }
/* ===================================================================
   Stat list — a counted breakdown inside a side card
   ===================================================================
   Rows, not a ruled table. Each one is a rounded band that can carry its
   own status tint, so a panel of five subscription states reads as "four
   active, nothing else" at a glance instead of as five identical lines
   with different numbers on the end.

   The tint comes from the .status--* map in common/_components.css —
   the same seven buckets every chip in the app uses (CLAUDE.md 4). That
   is the whole reason the row takes a status class rather than getting
   .stat-row--green and friends of its own: a subscription that is Active
   here and Active on the subscriptions table has to be one colour, and
   the map is where that is decided once.

   A row with nothing in it stays untinted and quiet. The mock this came
   from kept a coloured dot on a zero row, which spends the eye's one
   strongest cue — colour — on the rows that have nothing to say.
   =================================================================== */
.stat-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px;
}
.stat-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  transition: background 0.15s ease;
}
/* Every Waiting-on-you row is a link to the queue it counts. That used to
   be six copies of the same inline style= in the template.
   The colour reset is guarded rather than flat: `a.stat-row` is one step
   MORE specific than `.status--wait`, so an unguarded `color: inherit`
   would out-rank the tint on exactly the rows that have something to say
   and hand them back the card's own ink. */
a.stat-row { text-decoration: none; }
a.stat-row:not([class*="status--"]) { color: inherit; }
/* Untinted rows only: a hover that repaints the background would wipe the
   tint off the rows carrying the information. */
.stat-row:not([class*="status--"]):hover { background: var(--paper); }
.stat-row-label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text-secondary);
}
/* A label that is itself a link — the AI panel's top spenders, each one
   a route to the org it names. */
a.stat-row-label { text-decoration: none; }
a.stat-row-label:hover { color: var(--accent); }
.stat-row-count {
  font-size: 13.5px;
  font-weight: 700;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
/* Inside a tinted row both take the bucket's own ink, which is what makes
   the row read as one object rather than as a coloured strip with black
   text sitting on it. */
.stat-row[class*="status--"] .stat-row-label,
.stat-row[class*="status--"] .stat-row-count { color: inherit; }
/* A zero is an answer, so it stays readable rather than going to
   --text-tertiary (2.2:1, which this repo treats as decoration). It
   recedes against the tinted rows by being grey where they are coloured,
   not by being too faint to read. */
.stat-row--zero .stat-row-count { color: var(--text-secondary); }

/* The bullet that carries the row's tone. currentColor, so it needs no
   per-status rule of its own — it is whatever the row already is. */
.stat-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}
/* =================================================================== Greeting header =================================================================== */ .dash-greeting {
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 700;
  color: var(--ink);
  margin: 0 0 4px;
}
.dash-greeting-sub { font-size: 13.5px; color: var(--text-secondary); margin: 0 0 28px; }

/* Sign-in page chrome. org/login.css and platform/login.css each had a
   byte-identical copy; both those pages link this file. */
.login-brand { display: flex; align-items: center; gap: 8px; margin-bottom: 28px; }
.login-brand .brand-word { font-family: var(--font-display); font-weight: 600; font-size: 15px; color: var(--ink); }
/* The org's own wordmark in that row, where portal_logo() hands one over.
   Left-aligned and auto-width because .login-brand is a flex row, unlike
   the customer portal's centred .auth-brand. Platform's own sign-in links
   this same file and never reaches this rule — portal_logo() is None on
   the platform domain. */
.login-brand .brand-logo { height: 32px; width: auto; max-width: 180px; object-fit: contain; object-position: left center; }
.login-heading { font-family: var(--font-display); font-weight: 700; font-size: 22px; color: var(--ink); margin: 0 0 6px; }
.login-sub { font-size: 13.5px; color: var(--text-secondary); margin: 0 0 24px; }

/* Branding controls: org settings and the platform's org detail render
   the same three. */
.branding-color-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
label.file-pick-btn .ti { font-size: 15px; color: var(--text-secondary); flex-shrink: 0; }

/* Channel cards inside the combined Communications accordion — the same
   card grid as the platform org page's Messaging section (one card per
   channel: notifications, text, Kixie, WhatsApp, email domain, usage) */
.svc-card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 12px;
}
@media (max-width: 480px) {
  /* auto-fit's 320px floor is wider than the smallest phones in active
     use (iPhone SE-class, 320-375px) once .admin-content's own padding
     is subtracted — drop the floor so a single card actually fits. */
  .svc-card-grid { grid-template-columns: 1fr; }
}

/* The body of a .settings-card, below its title — platform/
   organisation_detail.html was already using the name with nothing behind
   it, so the card's controls sat flush against its heading. */
.settings-card-body { margin-top: 14px; }

/* A background job's own last words, quoted inside a table cell —
   /platform/background quotes both a scheduled run's captured stdout tail
   and a failed RQ job's exception line, and the mail version of the same
   report (email/schedule_report.html) styles its copy inline because mail
   clients strip linked CSS.

   Capped and scrollable rather than clipped: a traceback is worth reading
   in full, but a 2000-character tail rendered at full height pushes every
   other row of the table off the screen — which is the table the reader
   came for. */
.job-output {
  white-space: pre-wrap;
  word-break: break-word;
  font-family: ui-monospace, monospace;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--text-secondary);
  background: var(--paper);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  margin: 6px 0 0;
  max-height: 200px;
  overflow-y: auto;
}

/* The dashboard card and KPI body, moved up here from org/dashboard.css and
   platform/dashboard.css — which held one byte-identical copy each, the
   same duplication the block above records for the rest of this furniture.
   /platform/background is what forced it: it renders both classes and loads
   neither dashboard's sheet, so both rendered as nothing there.

   Each dashboard's own sheet still loads AFTER this file, so a portal that
   genuinely needs a different value keeps overriding it — which is how
   platform/dashboard.css holds on to its overflow-y.

   Flat and outlined for the same reason .kpi-card is — see the note
   there. The two have to match: they sit on the same page, and a
   shadowed card under a row of outlined tiles reads as a different kind
   of object rather than as the next thing down the page. */
.dash-card {
  background: var(--surface);
  border: 1px solid var(--card-line);
  border-radius: var(--radius-lg);
  overflow-x: auto;
}
.kpi-body { flex: 1; min-width: 0; }

/* The job table on /platform/background — a plain-English row with its
   technical detail kept beside it rather than instead of it.

   The whole point of the layout is that the two audiences read different
   columns: an office manager reads the title and the meaning, an engineer
   reads the command and the output. Neither is hidden behind a disclosure,
   because the person who needs the command is usually the person already
   being asked about the title. */
.job-name { font-weight: 600; }
.job-command {
  display: block;
  margin-top: 2px;
  font-family: ui-monospace, monospace;
  font-size: 11px;
  color: var(--text-secondary);
}
/* Capped: these are one or two sentences, and a table cell that grows to
   the width of a paragraph pushes the status column off the card. */
.job-meaning {
  max-width: 380px;
  font-size: 12.5px;
  color: var(--text-secondary);
  line-height: 1.5;
}
.job-cadence { white-space: nowrap; font-size: 12.5px; }
.job-runs { margin-top: 2px; font-size: 11px; color: var(--text-secondary); }
/* The key under a table, explaining its own status words — this page is
   read by people who do not otherwise see the system, so the vocabulary
   has to travel with it. */
.job-legend {
  padding: 12px 22px 16px;
  margin: 0;
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-secondary);
  border-top: 1px solid var(--paper);
}
.job-legend strong { color: var(--ink); font-weight: 600; }

/* A disclosure inside an .alert — used by /platform/background's "did not
   run" list, which is one or two lines on a normal bad morning and twenty
   on a bad one.

   Not .svc-inner-details: that one brings its own border, paper background
   and teal hover, all of which fight an alert's colouring. This inherits
   `currentColor` throughout instead, so it reads correctly inside any of
   the alert variants without knowing which one it is in.

   The summary keeps the headline and the count, so what collapses is the
   detail, never the alarm. */
.alert-details-toggle {
  display: flex;
  align-items: baseline;
  gap: 6px;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.alert-details-toggle::-webkit-details-marker { display: none; }
.alert-details-caret {
  align-self: center;
  font-size: 15px;
  transition: transform 0.15s ease;
}
.alert-details[open] .alert-details-caret { transform: rotate(90deg); }
/* "Show which" is the affordance — without it a summary reads as a
   sentence, and nobody discovers there is anything behind it. */
.alert-details-hint {
  margin-left: auto;
  font-size: 12px;
  font-weight: 600;
  opacity: 0.75;
  white-space: nowrap;
}
.alert-details[open] .alert-details-hint { visibility: hidden; }
.alert-details-list { margin: 8px 0 0; padding-left: 22px; }
.alert-details-list li { margin-bottom: 3px; }

/* The action cell on /platform/background — a Run now / Retry button, or
   the reason there isn't one. Narrow and right-aligned so the column reads
   as a margin rather than as another thing to scan. */
.job-action { white-space: nowrap; text-align: right; }
/* inline-block, not block: a failed task offers Retry AND Dismiss, and two
   stacked forms would put the destructive one directly under the primary —
   the shape §10 of CLAUDE.md exists to prevent. */
.job-action form { margin: 0; display: inline-block; }
/* The "why not" note is wider than the button it replaces, so it wraps —
   and it is the one thing in this column worth reading in full. */
.job-action-note {
  display: inline-block;
  max-width: 220px;
  white-space: normal;
  text-align: left;
  font-size: 11px;
  line-height: 1.45;
  color: var(--text-secondary);
}

/* ── Two-factor enrolment card ──────────────────────────────────────────
   Shared by /org/2fa and /platform/2fa. It lives here rather than in a
   page stylesheet because it is one component on two admin surfaces —
   CLAUDE.md's rule for "an admin page (org or platform)".

   These rules started as an inline <style> block in org/two_factor.html.
   Inline blocks are invisible to scripts/ui_lint.py, which is how three
   hardcoded hexes and a var(--paper) that nothing declares survived in
   there; moving them into a real stylesheet is what puts them under the
   linter. */
.tfa-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 24px;
  max-width: 560px;
}
.tfa-card h3 { margin: 0 0 6px; font-size: 16px; }
.tfa-card p { color: var(--text-secondary); font-size: 14px; margin: 0 0 16px; }
.tfa-steps { margin: 0 0 18px; padding-left: 20px; color: var(--text-secondary); font-size: 14px; }
.tfa-steps li { margin-bottom: 6px; }
/* Always a light plate behind the QR, in either theme: a dark-on-dark QR
   is unreadable to every scanner. */
.tfa-qr {
  display: inline-block;
  padding: 12px;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: 10px;
}
.tfa-qr svg { width: 180px; height: 180px; display: block; }
.tfa-secret {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 13px;
  background: var(--bg-subtle);
  padding: 6px 10px;
  border-radius: 6px;
  letter-spacing: 1px;
  word-break: break-all;
}
.tfa-form { display: flex; gap: 12px; align-items: flex-end; margin-top: 18px; flex-wrap: wrap; }
.tfa-form label { display: block; font-size: 12px; font-weight: 600; margin-bottom: 5px; color: var(--text-secondary); }
.tfa-form input {
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  font: inherit;
  letter-spacing: 3px;
  width: 140px;
  text-align: center;
}
.tfa-status { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; color: var(--success); margin-bottom: 12px; }
.tfa-status .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--success); }

/* Backup codes, shown once right after enrolment. Each <li> reuses
   .tfa-secret's look (monospace, subtle background) — this rule is only
   the grid wrapping it into two columns. */
.tfa-backup-codes { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; list-style: none; padding: 0; margin: 0 0 4px; }
.tfa-backup-codes li { text-align: center; }

/* One-field search above an admin list — the platform contractor roster and
   the demos table both grew past the point where "it's on this page
   somewhere" is true (the roster pages at 25 and caps at 200, and there are
   more businesses than that), and neither had any way to find one row.
   Shared rather than page-local because both pages are admin surfaces and a
   third will want it: see CLAUDE.md's "Where a new rule goes". */
.list-search { display: flex; gap: 8px; align-items: flex-end; margin-bottom: 16px; flex-wrap: wrap; }
.list-search label {
  display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-muted); margin-bottom: 4px;
}
.list-search input {
  padding: 7px 10px; border: 1px solid var(--border); border-radius: 8px;
  font-size: 13px; background: var(--surface); color: inherit; min-width: 260px;
}
.list-search-clear { align-self: center; font-size: 13px; }
