/* Certorio SPA — clean-room reproduction of the TrustHub look.
   Design tokens, layout, tables, badges and modals match the reference. */

:root {
  --sidebar-bg: #ffffff;
  --sidebar-text: #0B1621;
  --sidebar-active-bg: #0B1621;
  --sidebar-active-text: #f4f9ff;
  --accent-orange: #E7691D;
  --body-bg: #f8fafc;
  --border-color: #e2e8f0;
  --text-main: #334155;
  --text-muted: #64748b;
  --primary-blue: #577294;
  --primary-hover: #7092BE;
  --alert-red: #dc2626;
  --alert-red-hover: #b91c1c;
  --success-green: #16a34a;
  --warning-amber: #f59e0b;
  --card-radius: 16px;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

/* The `hidden` attribute must actually hide things.
   Browsers implement `hidden` as `[hidden] { display: none }` in their USER
   AGENT stylesheet, and any author `display` declaration — at any specificity,
   on any selector — outranks the whole UA sheet. So every rule in this file
   that sets `display` silently breaks `el.hidden = true` on the elements it
   matches, with no error and nothing to see until someone hits that state.

   That is not hypothetical. It broke the Free-tenant plan gate: applyPageGate()
   sets `hidden` on a view's panels, but the sticky-table rule further down
   (`.view-section.active:has(...) > .inventory-panel:last-child`) sets
   `display: flex` on the last panel, so Discovery and the Watch List rendered
   their table header over the top of the "This is a Pro feature" card with an
   empty body beneath it. Three narrower versions of this same bug were already
   patched one at a time (.modal-extra[hidden], .signup-error[hidden],
   .disc-scan-status[hidden]); this is the general fix, so there is no fourth.

   `!important` is what makes it reliable, and it is safe here: nothing in this
   app wants a `hidden` element rendered. The one author `display: !important`
   is `#print-compiled-sandbox` under @media print, which is both a
   higher-specificity ID selector and an element that hides itself with an
   inline style rather than this attribute — so printing is unaffected. */
[hidden] { display: none !important; }

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
  background-color: var(--body-bg);
  color: var(--text-main);
  /* Column, not row: the countdown banner is the first row and the sidebar +
     content sit in .app-shell below it, so a banner pushes the entire app
     down instead of covering it or being boxed into the content column.
     Without a banner .app-shell takes the full height and this is invisible. */
  display: flex;
  flex-direction: column;
  height: 100vh;
  overflow: hidden;
}

/* Sidebar + content side by side, filling the height the banner leaves.
   min-height: 0 is load-bearing: without it this flex item refuses to shrink
   below its content's height, and .main-container's own scrolling breaks —
   the page grows past the viewport instead of scrolling inside it. */
.app-shell {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
}

/* ---- Sidebar ---- */
.sidebar {
  width: 180px;
  background: var(--sidebar-bg);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  flex-shrink: 0;
  padding-top: 15px;
  border-right: 1px solid var(--border-color);
}
.sidebar-menu { list-style: none; margin-top: 36px; }
.sidebar-brand {
  font-size: 24px; font-weight: 700; color: #0B1621;
  padding: 24px 24px 2px; letter-spacing: 0.2px; text-align: center;
}
.sidebar-brand img {
  max-height: 48px; width: auto; height: auto;
  object-fit: contain; vertical-align: middle;
}
.sidebar-version { font-size: 11px; color: var(--sidebar-text); padding: 2px 24px 8px; opacity: 0.7; text-align: center; }
.sidebar-item {
  padding: 12px 24px; cursor: pointer; font-size: 14px; color: var(--sidebar-text);
  transition: all 0.15s ease; display: flex; align-items: center; gap: 10px;
}
.sidebar-item:hover, .sidebar-item.active { background: var(--sidebar-active-bg); color: var(--sidebar-active-text); }
.sidebar-item.active { font-weight: 600; }
.sidebar-profile {
  padding: 16px 0 8px;
  border-top: 1px solid var(--border-color);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.sidebar-profile-card {
  display: flex; flex-direction: column; align-items: center; gap: 4px; width: 100%; padding: 0 20px;
}
.profile-avatar {
  width: 44px; height: 44px; border-radius: 50%; flex-shrink: 0; cursor: pointer;
  background: var(--sidebar-active-bg); color: var(--sidebar-active-text);
  background-size: cover; background-position: center; background-repeat: no-repeat;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
  font-size: 18px; font-weight: 700; text-transform: uppercase;
  transition: opacity 0.15s ease;
}
.profile-avatar:hover { opacity: 0.85; }
/* When a picture is uploaded, the image fills the circle and the initial is hidden. */
.profile-avatar.has-image { color: transparent; }
.profile-name {
  max-width: 100%; font-size: 13px; font-weight: 600; color: var(--sidebar-text);
  text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.profile-email {
  max-width: 100%; font-size: 12px; color: var(--text-muted);
  text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Name + email are the Account page's entry point (the avatar is not — it
   opens the picture picker). Underline on hover/focus is the whole
   affordance: the sidebar profile block is deliberately quieter than a
   .sidebar-item, so a full hover fill would pull attention away from the nav.
   :focus-visible keeps the keyboard path visible without ringing the element
   on a mouse click. */
.profile-account-link { cursor: pointer; }
.profile-account-link:hover { color: var(--sidebar-text); text-decoration: underline; }
.profile-account-link:focus-visible {
  outline: 2px solid var(--primary-blue); outline-offset: 2px; border-radius: 3px;
}
/* Account page open: the nav has no .sidebar-item to highlight (the entry
   point lives down here), so the profile block itself shows where you are. */
.sidebar-profile.is-active .profile-name,
.sidebar-profile.is-active .profile-email { color: var(--sidebar-text); font-weight: 600; }
.profile-logout-btn {
  width: 100%; padding: 12px 24px; font-family: inherit; font-size: 14px; opacity: 0.7;
  color: var(--sidebar-text); background: transparent; cursor: pointer; border: none;
  display: flex; align-items: center; justify-content: center; gap: 8px; transition: all 0.15s ease;
}
.profile-logout-btn:hover { background: var(--sidebar-active-bg); color: var(--sidebar-active-text); opacity: 1; }

/* ---- Main / views ---- */
.main-container { flex-grow: 1; display: flex; flex-direction: column; overflow-y: scroll; background: #fafbfc; }
.view-section { display: none; padding: 30px; }
.view-section.active { display: block; }
.view-title { font-size: 22px; font-weight: 600; color: #0f172a; margin-bottom: 6px; }
.view-subtitle { font-size: 13px; color: var(--text-muted); margin-bottom: 24px; }

/* Site-wide countdown banner. First row of the <body> flex column, so it spans
   the FULL viewport width above the sidebar as well as the content — this
   warns about data being hidden and an account heading for deletion, and
   burying it in one column undersells that. In normal flow, so it pushes the
   app down rather than overlaying it: a warning that covers what you are
   reading is worse than one that moves it. */
.countdown-banner {
  display: flex; align-items: flex-start; gap: 14px;
  padding: 12px 30px; font-size: 13px; line-height: 1.55;
  border-bottom: 1px solid transparent; flex-shrink: 0;
}
.countdown-banner-msg { flex: 1; }
.countdown-banner-close {
  flex-shrink: 0; background: none; border: none; cursor: pointer;
  font-size: 20px; line-height: 1; padding: 0 2px; opacity: 0.65;
  color: inherit;
}
.countdown-banner-close:hover { opacity: 1; }
/* Info: a status line, not a warning. A trial running normally is not a
   problem, and a month of alarm-coloured banner would train people to ignore
   the one that IS an emergency. */
.countdown-banner.is-info { background: #eff6ff; border-bottom-color: #bfdbfe; color: #1e3a5f; }
/* Amber: a deadline approaching on a plan that still works. */
.countdown-banner.is-amber { background: #fffbeb; border-bottom-color: #fde68a; color: #78350f; }
/* Red: data is already hidden and on a clock to deletion. White text, since
   this one has to read as more serious than the amber state at a glance. */
.countdown-banner.is-red { background: #b91c1c; border-bottom-color: #991b1b; color: #fff; }

/* Plan + Billing history side by side (index.html's .billing-top-row).
   Billing history will grow into a long invoice list over time, so it reads
   better as a side column than a full-width card that keeps getting taller.
   Equal width by default (min-width:0 lets each .account-card shrink below
   its own 720px max-width, which would otherwise fight the split); stacks on
   narrow viewports since two 720px-capable cards side by side has no
   sensible minimum width to hold a fixed 2-column layout at. */
.billing-top-row { display: flex; gap: 20px; margin-bottom: 20px; flex-wrap: wrap; }
.billing-top-row > .account-card { flex: 1 1 320px; min-width: 0; }
/* Billing actions. Deliberately plain — just enough to host the two buttons
   until the real panel lands; see the comment in index.html. */
.billing-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 20px; }
.billing-plan-row { display: flex; align-items: center; gap: 10px; }
.billing-plan-name { font-size: 20px; font-weight: 600; color: #0f172a; }
.billing-trial-line { font-size: 13px; color: var(--text-muted); margin-top: 6px; }
/* PayPal Subscribe card (Phase 1 payment plumbing) — matches .account-empty-sub's
   secondary-text treatment rather than inventing a third greyed-text style. */
.account-card-sub { font-size: 13px; color: var(--text-muted); margin-top: 6px; max-width: 62ch; line-height: 1.5; }
/* Title + payment-method tag, one line, vertically centered. gap:20px is
   literally "20px to the right of the title".
   VERIFIED against a real Playwright render (not assumed): the first attempt
   at this — a plain `.billing-card-title-row h3 { margin-bottom: 0; }`
   override — never actually took effect. `.account-card h3` (margin-bottom:
   18px) has the SAME specificity (one class + one type selector) and sits
   LATER in this file, so it won the cascade on source order alone despite
   the override reading correctly. Measured computed style confirmed h3's
   margin-bottom was still 18px, which — being a bottom-only margin included
   in the flex item's cross-axis margin box — pushed the row's own height to
   34px and left the h3 *content* sitting at the top of that box while the
   tag (no such margin) sat genuinely centered: a 9px offset, exactly half of
   18px. Qualifying with `.account-card` raises this rule to two classes +
   one type selector, which reliably beats `.account-card h3` regardless of
   source order. Re-measured after the fix: both elements' vertical centers
   land on the same pixel. */
.billing-card-title-row { display: flex; align-items: center; gap: 20px; margin-bottom: 18px; }
.account-card .billing-card-title-row h3 { margin-bottom: 0; line-height: 1; }
/* Payment-method status tag — light-bg + colored-text pill, matching the
   Certificates table's Status column look (#certificates-view .badge-valid /
   .status-tag.badge-alert): same green/red triads and exact box model
   (padding 3px 10px, radius 14px, 1px border, 12px/14px type), not a bold
   solid-fill badge. Copied into its own class rather than reusing
   .badge-valid/.badge-alert directly since those are scoped to specific view
   ids (#certificates-view etc.) this tag isn't part of. */
.billing-payment-tag { display: inline-flex; align-items: center; justify-content: center; min-width: 72px; padding: 3px 10px; border-radius: 14px; font-size: 12px; line-height: 14px; white-space: nowrap; }
.billing-payment-tag.is-active { background: #f0fdf4; border: 1px solid #bbf7d0; color: #15803d; }
.billing-payment-tag.is-missing { background: #fef2f2; border: 1px solid #fecaca; color: #b91c1c; }
/* Basic/Pro plan picker for the PayPal Subscribe card. Track/shape borrowed
   from .cat-type-toggle's segmented pill (#f1f5f9 track, radius 14/10, 2px
   padding), but the ACTIVE state is deliberately its own look — solid
   var(--success-green)/white rather than that pattern's white-chip-on-gray —
   because this selects which plan gets bought, not which panel is showing,
   and it needs to read as a choice rather than as navigation. Same green/
   hover-darken pair as .btn.btn-success elsewhere in this file. */
.billing-plan-toggle { display: flex; gap: 2px; background: #f1f5f9; border-radius: 14px; padding: 2px; margin-top: 18px; max-width: 320px; }
.billing-plan-btn { flex: 1; border: none; border-radius: 10px; background: transparent; color: #64748b; font-family: inherit; font-size: 13px; font-weight: 500; padding: 8px 10px; white-space: nowrap; cursor: pointer; transition: background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease; }
.billing-plan-btn:not(.is-inactive), .billing-plan-btn:not(.is-inactive):hover { background: var(--success-green); color: #fff; font-weight: 700; box-shadow: 0 1px 2px rgba(0,0,0,0.08); }
.billing-plan-btn.is-inactive { font-weight: 500; color: #64748b; }
.billing-plan-btn.is-inactive:hover { color: #334155; }
/* PayPal + card buttons, side by side, equal size — the row a B2B buyer sees
   below the toggle.
   Fixed 150px per button, not a shrunk flex:1 container: the earlier
   ~50px-wide attempt (25% of the old 460px row) caused a real overlap bug —
   confirmed via PayPal's own open-source SDK, paypal/paypal-checkout-
   components src/ui/buttons/config.js. For layout:"horizontal" the SDK's
   MINIMUM_SIZE maps to BUTTON_SIZE.SMALL, whose BUTTON_SIZE_STYLE is
   { minWidth: 150, maxWidth: 200 } — the button's own iframe renders at that
   floor regardless of how small its container is squeezed, so a ~50px mount
   just clipped/overlapped a 150px-wide button instead of ever shrinking one.
   150px is the smallest PayPal will actually go for this layout, so the
   container now matches it exactly rather than fighting it: each mount fixed
   at 150px (flex-grow/shrink both 0, so it can't be squeezed back below the
   floor by a narrower row), row width = 150 + 150 + 12px gap = 312px.
   Height still comes from the shared explicit SDK height (see
   renderPaypalButtons in app.js) — that one PayPal does honor from style.height.
   margin-top tripled (14px -> 42px) for more air under the toggle. */
.billing-paypal-row { display: flex; gap: 12px; margin-top: 42px; width: 312px; }
.billing-paypal-mount { flex: 0 0 150px; width: 150px; }
/* Persistent notice, not a toast — it stays until the state it describes
   changes. Amber rather than red: Free is a valid state, not an error. */
.billing-notice {
  margin-top: 16px; padding: 12px 14px; border-radius: var(--card-radius);
  background: #fffbeb; border: 1px solid #fde68a; color: #78350f;
  font-size: 13px; line-height: 1.55;
}
/* Type-to-confirm block inside the shared confirm modal (openConfirm's
   `requirePhrase`). Hidden via the `hidden` attribute unless opted into. */
.confirm-phrase { margin: 14px 0 4px; text-align: left; }
.confirm-phrase label { display: block; font-size: 13px; color: #475569; margin-bottom: 6px; }
.confirm-phrase code { background: #f1f5f9; padding: 1px 6px; border-radius: 3px; font-weight: 600; }
.confirm-phrase input { width: 100%; }

/* Pro-feature page gate (Discovery, Watch List — see applyPageGate in app.js).
   Replaces a whole view-section's content for a Free tenant; styled like
   .report-control-card so it reads as part of the page rather than an error. */
.pro-feature-gate {
  background: #fff; border: 1px solid var(--border-color); border-radius: var(--card-radius);
  padding: 40px 30px; text-align: center; max-width: 480px; margin: 40px auto;
}
.pro-feature-gate .view-title { margin-bottom: 10px; }
.pro-feature-gate-msg { font-size: 14px; color: var(--text-muted); }

/* ---- Cards ---- */
.cert-card {
  background: #fff; border: 1px solid var(--border-color); border-radius: var(--card-radius);
  padding: 30px; box-shadow: 0 1px 3px rgba(0,0,0,0.02); margin-bottom: 30px;
}
.cert-card h2 { font-size: 20px; font-weight: 600; color: #0f172a; margin-bottom: 24px; }

/* Tighten these views' top spacing only (global .view-section stays 30px):
   minimal breathing gap above the header, and above the content below it.
   Every view with a page header shares this value — keep them on one rule so
   the pages cannot drift apart. Note the last four carry only this rule: they
   are card-layout pages that keep the stacked h1-over-subtitle header, not the
   .view-header single-line row the table pages use below. */
#snapshot-view,
#certificates-view, #domains-view, #watchlist-view,
#discovery-view, #agent-manage-view, #agents-view,
#alert-log-view, #audit-view,
#csr-view, #reports-view, #integration-view, #alerts-view, #account-view { padding-top: 10px; }
/* Snapshot alone sits 50px from the sidebar instead of the global 30px, so its
   content clears the nav by more than a table page needs to. Deliberately
   scoped to this one view: navigating to any other page shifts the content
   20px left, which is accepted. An id (1,0,0) to beat .view-section (0,1,0)
   without having to restate the other three sides. */
#snapshot-view { padding-left: 50px; }
/* Pull the summary cards up under the subtitle (global .view-subtitle stays 24px).
   Superseded on both views by the .view-header rule below, which zeroes the
   subtitle's own margin; kept because the selector is shared with nothing else
   and removing it would be a no-op churn.
   Do NOT de-scope the zeroing rule below to a bare `.view-header .view-subtitle`
   to shorten it: that is (0,2,0) and would lose to this (1,1,0) rule, quietly
   restoring 8px under the Certificates/Domains headers. The id-scoped list is
   verbose on purpose. */
#certificates-view .view-subtitle, #domains-view .view-subtitle { margin-bottom: 8px; }
/* Shared page-header row: title and subtitle share one baseline-aligned line,
   and that line sits 5px above whatever follows it — the status bar on
   Certificates/Domains, the table card itself on Watch List/Agents/Alert Log/
   Audit Log, the first "Mission" card on Discovery. The wrapper div is not
   cosmetic — when any of these views is active it matches the :has() rule
   further down and becomes a flex column, so .view-title/.view-subtitle are
   flex items and `display: inline` on them would be blockified and silently
   ignored. The wrapper is what makes the single line possible at all.
   (#agent-manage-view is the exception on both counts: it does not match the
   :has() rule, and its .view-header is nested inside .panel-toolbar rather
   than sitting above it — see the override below.)
   The 5px is box-to-box; the h1's line box carries ~2px of half-leading below
   the glyphs, so the gap reads slightly larger than 5px optically. */
#certificates-view .view-header,
#domains-view .view-header,
#watchlist-view .view-header,
#discovery-view .view-header,
#agent-manage-view .view-header,
#agents-view .view-header,
#alert-log-view .view-header,
#audit-view .view-header {
  display: flex;
  align-items: baseline;
  /* Wraps to two lines on a narrow viewport rather than crushing the subtitle. */
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 5px;
}
#certificates-view .view-header .view-title,
#certificates-view .view-header .view-subtitle,
#domains-view .view-header .view-title,
#domains-view .view-header .view-subtitle,
#watchlist-view .view-header .view-title,
#watchlist-view .view-header .view-subtitle,
#discovery-view .view-header .view-title,
#discovery-view .view-header .view-subtitle,
#agent-manage-view .view-header .view-title,
#agent-manage-view .view-header .view-subtitle,
#agents-view .view-header .view-title,
#agents-view .view-header .view-subtitle,
#alert-log-view .view-header .view-title,
#alert-log-view .view-header .view-subtitle,
#audit-view .view-header .view-title,
#audit-view .view-header .view-subtitle { margin-bottom: 0; }
/* Manage puts its header inside the toolbar row instead of above it, so the
   shared 5px would push the group off the toolbar's vertical centre and add
   dead space inside the row. The gap below the header on this page is the
   toolbar's own margin, not the header's. */
#agent-manage-view .view-header { margin-bottom: 0; }
.status-icon { display: inline-block; width: 18px; height: 18px; vertical-align: middle; }
/* Fixed-width slot for the Status column's leading icon (Certificates/
   Domains tables) — reserves the same 18px + gap space whether it holds a
   real icon (Expiring/Expired/Unknown) or is left blank (Valid), so the
   status-tag that follows always starts at the same left position. */
.status-icon-slot { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; flex-shrink: 0; }

/* ---- Buttons / links ---- */
.btn {
  background: var(--primary-blue); color: #fff; border: none; padding: 8px 16px; border-radius: 4px;
  cursor: pointer; font-size: 13px; font-weight: 500; white-space: nowrap; transition: background-color 0.15s ease;
}
.btn:hover { background: var(--primary-hover); }
.btn.btn-grey { background: #64748b; }
.btn.btn-grey:hover { background: var(--primary-hover); }
/* Discovery's certificate-detail modal dismiss button. Near-black rather than
   the shared slate, per spec. Id-scoped at (1,0,0), which clears
   .btn.btn-grey's (0,2,0) — every other modal's Cancel keeps the slate.
   The hover has to be restated: (1,0,0) outranks .btn.btn-grey:hover's (0,3,0)
   too, so without this line the resting fill would also win on mouse-over and
   the button would sit inert. Lightened rather than darkened, since #0B1621 is
   already near-black and has nowhere darker to go. */
#disc-modal-close { background: #0B1621; }
#disc-modal-close:hover { background: #1e2c3d; }
/* Pill geometry only. The near-black above is a deliberate colour choice per
   spec, not an oversight, so it is left exactly as it is and only the shape
   is brought into line with every other dialog button. The 1px border matches
   its own fill so the box is the same height as a bordered pill rather than
   2px shorter. */
#disc-cert-modal #disc-modal-close {
  border-radius: 999px;
  border: 1px solid #0B1621;
}
/* The border follows the fill on hover, as it does on every other pill here.
   Without this the ring stays at the resting near-black while the face
   lightens, drawing a visible outline the other buttons never show. */
#disc-cert-modal #disc-modal-close:hover { border-color: #1e2c3d; }
/* The Edit certificate / Edit domain dialogs used to paint both buttons
   near-black here. They now take the outline pill instead, with the rest of
   the modals in this app — see the #modal entries in the pill lists far
   below. The rule is gone rather than overridden: the pill selectors are the
   same (2,0,0), so a leftover near-black rule would be beaten only by source
   order, which is exactly the kind of thing that breaks when blocks move. */
.btn.btn-outline { background: #fff; color: var(--primary-blue); border: 1px solid var(--primary-blue); }
/* Test buttons (Slack/Teams "Send test message", "Test my mail", Alerts
   "Send test alert") plus the Reports page's Print / Send to Mail pair, which
   reuse the same .btn-outline look: hover fills --sb-ink navy with white text
   rather than the default light-blue .btn:hover. The navy is shared with the
   pill buttons' hover (see the .btn-grey/.btn-wide/#dom-onboard-btn rule far
   below) so every white-outline control in the app inverts to the same color;
   this was #64748b slate, which read as a second, near-miss hover treatment.
   #trigger-email-dispatch-btn carries an inline background-color in its
   unconfigured "Save Settings" state (see refreshSnapshotBadge in app.js),
   which outranks this rule — that state keeps its red fill on hover by
   design, and the white text below is what keeps it legible either way. */
#int-test-btn:hover, #int-teams-test-btn:hover, #al-mailbox-test-btn:hover, #al-test-btn:hover,
#trigger-print-btn:hover, #trigger-email-dispatch-btn:hover { background: var(--sb-ink); border-color: var(--sb-ink); color: #fff; }
.btn.btn-success { background: var(--success-green); }
.btn.btn-success:hover { background: #15803d; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Category type tabs (Certificate / Domain) on Manage Categories — same
   design.png gray-track/white-raised-chip segmented-pill pattern as the
   Watch List refresh-interval toggle (.watch-interval-slider/-opt above).
   ACTIVE = white chip + near-black bold text + soft shadow; INACTIVE = plain
   gray text on the track, darkening on hover (no background fill). */
.cat-type-toggle { display: flex; gap: 2px; background: #f1f5f9; border-radius: 14px; padding: 2px; margin-bottom: 16px; }
.cat-type-btn { flex: 1; border: none; border-radius: 10px; background: transparent; color: #64748b; font-size: 13px; font-weight: 500; padding: 8px 10px; white-space: nowrap; cursor: pointer; transition: background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease; }
.cat-type-btn:not(.is-inactive), .cat-type-btn:not(.is-inactive):hover { background: #fff; color: #0f172a; font-weight: 700; box-shadow: 0 1px 2px rgba(0,0,0,0.08); }
.cat-type-btn.is-inactive { font-weight: 500; color: #64748b; }
.cat-type-btn.is-inactive:hover { color: #334155; }
.edit-link { font-size: 12px; color: var(--primary-blue); cursor: pointer; font-weight: 500; text-decoration: none; }
.edit-link:hover { text-decoration: underline; }
.delete-link { font-size: 12px; color: var(--alert-red); cursor: pointer; font-weight: 500; text-decoration: none; margin-left: 8px; }
.delete-link:hover { text-decoration: underline; color: var(--alert-red-hover); }
/* Manage Categories drag handle (replaces the old ▲▼ arrows). SortableJS
   binds to #cat-tbody itself, so the cell just needs to look grabbable. */
.cat-drag-handle-cell { width: 28px; padding-right: 0; }
.cat-drag-handle { display: inline-block; color: #94a3b8; font-size: 14px; line-height: 1; cursor: grab; padding: 4px 6px; user-select: none; }
.cat-drag-handle:hover { color: #64748b; }
.cat-drag-handle.is-owner-locked-link { cursor: not-allowed; }
/* Sortable's own state classes during a drag. */
#cat-tbody .sortable-chosen { background: #f8fafc; }
#cat-tbody .sortable-ghost { opacity: 0.4; }
#cat-tbody tr:active .cat-drag-handle { cursor: grabbing; }
/* Actions-column button set (Edit / WHOIS / Delete / Watch on the Certificates,
   Domains and Watch List tables). All four sit inside the .row-actions
   gray-track frame below, transparent at rest, and fill with their own
   per-action color on hover — see the per-action :hover rules under
   .icon-act-btn further down. */
/* Explicit line-height so button height is fixed by the line box, not the
   font-size — lets the "Watching" variant shrink its font to fit 62px without
   ending up shorter than the others. */
.act-btn { display: inline-block; vertical-align: middle; min-width: 62px; text-align: center; background: #fff; font-size: 12px; line-height: 14px; font-weight: 500; padding: 3px 10px; border-radius: 4px; border: 1px solid #CBD4E3; color: #64748b; text-decoration: none; cursor: pointer; transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease; }
.act-btn + .act-btn { margin-left: 8px; }
.act-btn:hover { background: #64748b; border-color: #64748b; color: #fff; }
/* Certificates/Domains Actions-column icon buttons (Edit/Delete/WHOIS/Watch):
   sit borderless/transparent at rest inside the .row-actions pill track (see
   below) — same design.png raised-chip group used for the Watch List
   refresh-interval toggle and Manage Categories tabs, just without a
   permanently-"active" chip since no one action is ever selected. Hover
   fills solid with the action's color and turns the glyph white
   (currentColor) — unchanged from before, see the per-action :hover rules
   below. */
.icon-act-btn {
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; text-decoration: none; vertical-align: middle;
  box-sizing: border-box; background: transparent; border: none; border-radius: 10px;
  color: #64748b;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.icon-act-btn--edit:hover { background: var(--success-green); border-color: var(--success-green); color: #fff; }
.icon-act-btn--delete:hover { background: var(--alert-red); border-color: var(--alert-red); color: #fff; }
/* WHOIS and unwatched-Watch icons (Domains Actions column): same hover blue,
   since both are plain actionable lookups rather than a destructive/edit
   action — matches the blue these two used for their own identity color
   before this rest/hover redesign. */
.icon-act-btn--whois:hover, .icon-act-btn--watch:hover, .icon-act-btn--download:hover,
.icon-act-btn--view:hover { background: #238BB9; border-color: #238BB9; color: #fff; }
/* Discovery's Add / Add all: green, same semantic as --edit/--status (they
   write to the record set) rather than the neutral-lookup blue that Discovery's
   View shares with WHOIS above. Undo takes the red of --delete: it removes
   inventory certificates this page created, so it is the destructive one of
   the four even though it is phrased as a reversal. */
.icon-act-btn--add:hover, .icon-act-btn--add-all:hover { background: var(--success-green); border-color: var(--success-green); color: #fff; }
.icon-act-btn--undo:hover { background: var(--alert-red); border-color: var(--alert-red); color: #fff; }
/* CSR Status icon: green hover, matching Edit's semantic (it opens a modal
   that modifies a field on the row) rather than the neutral-lookup blue
   above. */
.icon-act-btn--status:hover { background: var(--success-green); border-color: var(--success-green); color: #fff; }
/* In-flight state for the Discovery actions, replacing the old "…" text swap
   that discRunAction used to do — assigning textContent to an icon-only
   control destroys its <svg>, so the busy state has to be purely visual. See
   the note on discRunAction in app.js. pointer-events stops the mouse; the
   aria-disabled attribute it is paired with is what stops keyboard activation
   (checked in the handler). */
.icon-act-btn.is-busy, .agent-add-inventory.is-busy { opacity: 0.4; pointer-events: none; }
/* Post-add state on the Domains row: a non-interactive filled-iris eye that
   replaces the outline "Watch" icon once the domain is on the Watch List
   (removal is Watch-List-only, from the Watch List page). Permanently green
   (not the neutral rest state above) — matches .act-btn--watching below
   (Certificates' "Managed" badge) for one consistent "already done" look
   across the app. No hover change — signals "already done" rather than
   actionable. */
.icon-act-btn--watching, .icon-act-btn--watching:hover {
  background: #f0fdf4; border-color: #f0fdf4; color: #15803d; cursor: default;
}
/* Certificates row "Managed by a connected agent" state: a faded,
   non-interactive badge in the Actions column, styled as an .act-btn (not an
   icon — there's no Edit/Delete/WHOIS/Watch equivalent for agent-managed
   certs, just this one label). Same green as .icon-act-btn--watching above,
   for one consistent "already done" color across the app.

   SHAPED AS A TAG, NOT A BUTTON. It sits in a row alongside two outlined
   pills — the SOURCE column's "Agent" (#certificates-view .badge-agent) and
   the STATUS column's "Valid" (#certificates-view .badge-valid) — which share
   one recipe on this page: radius 14px, padding 3px 10px, 12px/14px type at
   weight 600, min-width 72px, centred, 1px border. This label is not
   interactive, so reading as a squared-off 4px button among them was the odd
   one out. Only the box changes here; the colors are untouched, and they
   already happen to be .badge-valid's exact triad (#f0fdf4 / #bbf7d0 /
   #15803d) — so with the shape aligned this now renders identically to the
   Valid tag, which is the intent.

   padding (3px 10px), line-height (14px), text-align (center), display
   (inline-block) and the 1px border already come from .act-btn and already
   match that recipe, so only the four properties that diverged are set below.
   The old rule shrank the font to 11px and trimmed side padding to 2px to
   keep "Managed" inside a 62px min-width; the wider 72px pill removes the
   reason for both, and min-width is a floor, so the tag simply sizes to its
   text. */
.act-btn--watching, .act-btn--watching:hover {
  background: #f0fdf4; border-color: #bbf7d0; color: #15803d;
  cursor: default;
  border-radius: 14px; font-size: 12px; font-weight: 600; min-width: 72px;
}
/* Actions-column button group frame (Certificates/Domains/Watch List/CSR/
   Agents): the design.png gray-track/raised-chip shell — same
   background/radius/padding tokens as .watch-interval-slider and
   .cat-type-toggle above — replacing the old per-button white-bg-plus-border
   frame so Edit/Delete/WHOIS/Watch read as one grouped control rather than
   separate bordered rectangles. Also centers the icon buttons on one axis
   (and aligns with the middle-aligned Status column on Domains). */
.row-actions { display: inline-flex; align-items: center; gap: 2px; background: #f1f5f9; border-radius: 14px; padding: 2px; }
.transfer-link { color: var(--alert-red); font-weight: 600; cursor: pointer; text-decoration: none; }

/* ---- Toolbar (search + actions above tables) ---- */
.panel-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
.panel-toolbar h3 { font-size: 16px; font-weight: 600; color: #0f172a; }
.toolbar-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* Leading group, opposite .toolbar-actions: keeps a primary button and any
   icon buttons that belong with it together on the left, instead of letting
   .panel-toolbar's space-between push them apart. */
.toolbar-lead { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.search-input {
  padding: 8px 12px; border: 1px solid #cbd5e1; border-radius: 4px; font-size: 13px;
  min-width: 240px; outline: none; color: var(--text-main);
}
.search-input:focus { border-color: var(--primary-blue); }
/* Watch List global refresh-interval control — a 3-stop segmented slider
   (6h/12h/24h) sitting at the top-left of the table, left of the search bar. */
.watch-interval { display: inline-flex; align-items: center; gap: 10px; }
.watch-interval-label { font-size: 12px; font-weight: 600; color: #475569; }
/* design.png pattern: a single pill-shaped gray track holding a raised white
   "chip" on the active stop (macOS-style appearance switcher), rather than
   individually bordered buttons. Track bg reuses the app's existing
   slate-100 token (#f1f5f9); the active chip is plain white + a soft shadow
   to read as raised, no border on either state. */
.watch-interval-slider { display: inline-flex; align-items: center; gap: 2px; background: #f1f5f9; border-radius: 14px; padding: 2px; }
.watch-interval-opt { border: none; border-radius: 10px; background: transparent; color: #64748b; font-size: 12px; font-weight: 500; padding: 5px 14px; cursor: pointer; transition: background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease; }
.watch-interval-opt:hover { color: #334155; }
.watch-interval-opt.active { background: #fff; color: #0f172a; font-weight: 700; box-shadow: 0 1px 2px rgba(0,0,0,0.08); }
.watch-interval-opt.active:hover { background: #fff; color: #0f172a; }
/* Shared icon-in-pill search box (Certificates + Domains inventory search) —
   identical height / magnifier icon / pill styling. Padding 8px 12px + 1px
   border matches the plain .search-input height so the two align exactly. */
.search-box { display: flex; align-items: center; gap: 8px; background: #fff; border: 1px solid #e2e8f0; border-radius: 6px; padding: 8px 12px; min-width: 240px; }
.search-box svg { flex-shrink: 0; color: #94a3b8; }
.search-box input { border: none; outline: none; background: transparent; font-size: 13px; color: #334155; width: 100%; }
/* Expiry-filter toggle (Certificates + Domains inventory, left of the search
   box): clicking it hides "Valid" rows, keeping About to expire, Expired,
   and Unknown (unreadable expiry date). Same bell icon as Settings ->
   Expiry Alerts. Active state fills red (matching --alert-red, the same
   color the Expired/About-to-expire badges already use) so it reads as
   "alert filter is on". */
/* height: 33px matches .search-box's rendered height exactly (8px vertical
   padding there + its 1px border, border-box) so the two sit flush as a pair. */
.expiry-filter-btn,
.icon-filter-btn { display: flex; align-items: center; justify-content: center; width: 38px; height: 33px; flex-shrink: 0; box-sizing: border-box; background: #fff; border: 1px solid #e2e8f0; border-radius: 6px; color: #94a3b8; cursor: pointer; transition: all 0.15s ease; }
.expiry-filter-btn:hover { border-color: var(--alert-red); color: var(--alert-red); }
.expiry-filter-btn.active { background: var(--alert-red); border-color: var(--alert-red); color: #fff; }
/* TLS / OS filter toggles (Manage + Agents, left of the search box). Shares the
   box metrics above but NOT the bell's solid-fill active state: the Windows and
   Linux glyphs carry hardcoded multicolor fills and cannot invert via
   currentColor, so a filled button would leave blue squares and an orange Tux
   sitting on a colored background. A light-green tint keeps every icon legible
   and reuses .badge-tls's exact tokens (#dcfce7 / #15803d), so an active filter
   matches the TLS badges already rendered in the cards below it. */
.icon-filter-btn:hover { border-color: #15803d; color: #15803d; }
.icon-filter-btn.active { background: #dcfce7; border-color: #15803d; color: #15803d; }
/* The Agents toolbar's help "?" is a bare glyph rather than a framed control:
   it opens the install guide, so it is a one-shot action, not a toggle like
   the filters it borrows .icon-filter-btn from. Those keep their frame — they
   need somewhere to show .active — so this is scoped to the id, leaving the
   other five users of that class (two OS filters here, three on Manage, plus
   .expiry-filter-btn sharing the metrics) untouched. (1,0,0) already clears
   the class rule's (0,1,0); no view scoping needed, unlike the pill buttons.
   The frame is three properties, not one — background, border and radius all
   have to go — and the fixed 38x33 with it, or the enlarged 27px glyph (see
   ICON_HELP in app.js) would be pinned inside a box the frame no longer
   draws. */
#agent-help-btn {
  width: auto; height: auto; padding: 0;
  background: none; border: none; border-radius: 0;
}
/* The class hover recolors the border, which is inert once there is no border
   to color. Restate it as a glyph tint so the bare icon still answers the
   cursor; (1,0,1) beats the class's (0,1,1). Same green the filters use. */
#agent-help-btn:hover { color: #15803d; }

/* ---- Upload dropzone ---- */
.upload-dropzone-btn {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  border: 2px dashed #cbd5e1; padding: 14px 24px; background: #fff; border-radius: 6px;
  cursor: pointer; transition: all 0.2s ease; text-align: center; margin-bottom: 16px;
}
.upload-dropzone-btn.dragover, .upload-dropzone-btn:hover { border-color: var(--primary-blue); background: #f4f9ff; }
.upload-dropzone-btn strong { font-size: 14px; color: var(--primary-blue); font-weight: 700; }
.upload-dropzone-btn span { font-size: 12px; color: var(--text-muted); margin-top: 4px; }

/* ---- Inventory panel + table ---- */
.inventory-panel { background: #fff; border: 1px solid var(--border-color); border-radius: var(--card-radius); padding: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.02); }
/* min-height reserves the header row (~42px: 12+12 padding + ~16px text +
   2px border) plus 5 skeleton-ish data rows (~41px each: 12+12 padding +
   ~16px text + 1px border) = ~247px, rounded to 246px, so the table area
   doesn't collapse to just its header while loadCertificates()/loadDomains()
   are still awaiting their API calls, then jump once rows arrive. Applies to
   both the base rule here (non-:has() fallback) and the :has() override
   below, which otherwise sets min-height: 0 on this same element and would
   silently cancel this out on any browser where that selector matches. */
.table-scroll-frame { width: 100%; overflow: auto; max-height: calc(100vh - 320px); min-height: 246px; }
/* On a page whose last panel is a table, let that panel grow to fill the
   remaining height of the view instead of sizing the table off a fixed
   100vh guess. This makes the empty space below the table equal to
   .view-section's own 30px padding — the same 30px that already forms the
   gap between the sidebar and the table on the left. */
.view-section.active:has(> .inventory-panel:last-child .table-scroll-frame) {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
.view-section.active:has(> .inventory-panel:last-child .table-scroll-frame) > .inventory-panel:last-child {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.view-section.active:has(> .inventory-panel:last-child .table-scroll-frame) > .inventory-panel:last-child > .table-scroll-frame {
  flex: 1 1 auto;
  /* Was min-height: 0 (the standard flex fix for the min-height:auto
     shrink-to-content trap). 246px instead: still shrinkable below its
     natural content size for the overflow:auto scrolling this rule exists
     for, but with the same loading-state floor as the base rule above, which
     this more specific selector would otherwise silently override back to 0
     on any :has()-supporting browser — exactly the pages (Certificates,
     Domains) this fix is for. */
  min-height: 246px;
  max-height: none;
}
.metadata-table { width: 100%; border-collapse: separate; border-spacing: 0; text-align: left; font-size: 13px; }
.metadata-table thead th { position: sticky; top: 0; background: #f8fafc; z-index: 2; box-shadow: 0 1px 0 #e2e8f0; }
.metadata-table th {
  background: #f8fafc; color: #475569; font-weight: 600; padding: 12px 16px;
  border-bottom: 2px solid var(--border-color); white-space: nowrap; cursor: pointer;
  user-select: none; transition: background-color 0.15s ease;
}
.metadata-table th.no-sort { cursor: default; }
/* drag-to-resize handle: thin strip on the header's right edge (anchored to
   the sticky th, which is a positioned element). */
.col-resize-handle {
  position: absolute; top: 0; right: 0; width: 6px; height: 100%;
  cursor: col-resize; user-select: none; z-index: 3;
}
/* The last column's handle: same boundary, approached from the other side.
   It has no right neighbour to resize against, so it hangs off its own left
   edge instead — see the note in app.js where it is created. */
.col-resize-handle--left { right: auto; left: 0; }
.col-resize-handle:hover { background: var(--primary-blue); opacity: 0.5; }
.metadata-table th:hover:not(.no-sort) { background: #f1f5f9; color: #0f172a; }
.metadata-table th .sort-icon { display: inline-block; margin-left: 6px; font-size: 11px; color: #cbd5e1; }
.metadata-table th.active-sort { color: var(--primary-blue); background: #f4f9ff; }
.metadata-table th.active-sort .sort-icon { color: var(--primary-blue); }
.metadata-table td {
  padding: 12px 16px; border-bottom: 1px solid var(--border-color); color: #334155;
  max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle;
}
/* Actions cell holds the .act-btn set; it must size to its buttons rather than
   inherit the 220px / overflow:hidden clamp above (which clipped the last button
   — e.g. Watch on the Domains row — once .act-btn gained a fixed min-width).
   Left-aligned (explicit, though it matches the table's own default) so the
   Actions column reads the same as every other column (Associated Domain,
   Category, Source, Status, …) — both the header label and the button group
   hug the column's left edge, rather than being singled out as the one
   right-anchored column. Targeted via an explicit .actions-cell class on the
   <td> (set in app.js's row templates), not :has(.row-actions) — :has()
   silently fails to match on browsers that don't support it. */
.metadata-table td.actions-cell { max-width: none; overflow: visible; text-align: left; }
/* No header override needed: .metadata-table's own text-align:left already
   covers the Actions header (and every other header), so it reads the same
   as Associated Domain/Category/Source/Status instead of being singled out. */
.metadata-table tbody tr:hover { background: #fafbfc; }
tr.row-expired td, tr.row-expired td strong { color: var(--alert-red); }
.empty-row td { text-align: center; color: #94a3b8; padding: 28px; font-style: italic; }

/* ---- Source / status badges ---- */
.badge { padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; display: inline-block; }
.badge-uploaded   { background: #faeeda; color: #854f0b; }
/* Certificates promoted to Inventory from the Discovery page (source
   "Discovery" — discovery_promote.DISCOVERY_SOURCE). Teal, deliberately NOT the
   green this class used to carry: #15803d is the app's "this is good" signal
   (.badge-valid, .badge-tls, .disc-source-tag--live, .act-btn--watching), and a
   SOURCE tag wearing it reads as a status rather than a provenance. Teal keeps
   the source palette — amber / indigo / sky / teal — legible as one family with
   nothing borrowed from the valid/expiring/expired vocabulary.
   The class name is historical; the source value is "Discovery". */
.badge-discovered { background: #ccfbf1; color: #0f766e; }
.badge-manually   { background: #faeeda; color: #854f0b; }
.badge-whois      { background: #e0f2fe; color: #0369a1; }
/* Certificates claimed by a connected agent (source="Agent") — its own color,
   distinct from Discovery's above. (An earlier note here said "Discovered" was
   reserved for a still-unbuilt outbound-scan promote-to-Inventory mechanism.
   That mechanism was built: it is Discovery, per Master_Plan §4.2/4.3, whose
   source enum lists `discovered`. Only the spelling differs.) */
.badge-agent      { background: #e0e7ff; color: #4338ca; }
/* Agents-page status tag: Pending is neutral/grey (nothing to signal yet);
   Connected/Disconnected reuse the existing valid/alert badge colors. */
.badge-neutral    { background: #f1f5f9; color: #64748b; }
/* Agents table OS column: platform icon + label centered on one line, the
   same 6px icon/text gap statusInner() uses. flex-shrink pins the 18px SVG
   so narrowing the column with the resizer squeezes the text, not the icon
   — the same reason .agent-card-header-left pins its own icon. */
.os-cell          { display: inline-flex; align-items: center; gap: 6px; }
.os-cell > svg    { flex-shrink: 0; }
/* Manage page binding rows: TLS tag — flat green, deliberately not
   .badge-valid (that class is overridden to a bordered pill inside
   #agent-manage-view for the agent Connected status) so this stays the
   same flat shape as its .badge-neutral "No TLS" counterpart. */
.badge-tls        { background: #dcfce7; color: #15803d; }
/* Shared fixed width for the TLS/No TLS tags — "No TLS" is the longer of
   the two strings, so without this "TLS" renders narrower and the
   HTTPS/HTTP label after it starts at a different x position per row. */
.agent-cert-tag   { min-width: 54px; text-align: center; }
.badge-email      { background: #eff6ff; color: #1d4ed8; }
.badge-slack      { background: #f3e8ff; color: #7e22ce; }
/* Watch List change alert (alert_log alert_type "Watch") — orange, matching the
   Watch feature's accent. */
.badge-watch      { background: #fdead9; color: #c2410c; }
/* Watch List row "Changed" badge — amber, an at-a-glance attention signal that
   the last scheduled re-check found an unresolved change/flag. */
.badge-changed    { background: #fef3c7; color: #92400e; }
/* Watch List Domain Name cell: name left, "Changed" badge pinned to the column's
   right edge (consistent position regardless of name length). Name truncates
   with an ellipsis; the badge never shrinks. */
.watch-name-cell { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.watch-name-cell > strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.watch-name-cell > .badge { flex-shrink: 0; }
/* WHOIS reconciliation: amber = source mismatch, red = seizure/hold signal. */
.badge-warning    { background: #fef3c7; color: #92400e; }
.badge-alert      { background: #fee2e2; color: #b91c1c; }
.badge-valid      { background: #dcfce7; color: #15803d; }
/* Certificates/Domains Status column: Valid/Expiring/Expired share this fixed
   width (sized for the widest of "Expired" / "N days") so all three tags
   render identically sized, keeping the column tidy regardless of which
   state — and regardless of an icon in front of it — appears in a row. */
.status-tag       { min-width: 68px; text-align: center; }
.status-valid     { color: var(--success-green); font-weight: 600; }
.status-expired   { color: var(--alert-red); font-weight: 600; }
.status-pending   { background: #fef3c7; color: #854f0b; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
.status-signed    { background: #f0fdf4; color: #15803d; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
.status-revoked   { background: #fef2f2; color: #b91c1c; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }

/* ---- Forms ---- */
.field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 16px; }
.field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; }
.field label { font-size: 12px; font-weight: 600; color: #475569; }
.field input, .field select, .report-input-field {
  padding: 9px 12px; border: 1px solid #cbd5e1; border-radius: 4px; font-size: 13px; color: var(--text-main); outline: none;
}
.field input:focus, .field select:focus { border-color: var(--primary-blue); }
.report-control-card { background: #fff; border: 1px solid var(--border-color); border-radius: var(--card-radius); padding: 24px; max-width: 368px; margin-bottom: 20px; }
.report-control-card h3 { font-size: 16px; font-weight: 600; color: #0f172a; margin-bottom: 18px; display: flex; align-items: center; gap: 8px; }
/* Integration page: Slack + Teams cards side by side; wraps on narrow screens. */
.integration-card-row { display: flex; gap: 20px; flex-wrap: wrap; align-items: flex-start; }
/* Keep each card at its original fixed 368px (identical to .report-control-card's
   max-width; border-box makes basis == total width): don't grow, don't shrink —
   wrap to a new line on narrow screens instead of resizing the cards. Only the
   in-row cards (Slack, Teams) are targeted; the Mail card sits outside the row
   and is untouched. */
.integration-card-row > .report-control-card { flex: 0 0 368px; }
.status-badge-line { font-size: 12px; font-weight: 600; margin-top: 8px; }

/* Reports layout: stack control cards consistently (matches TrustHub) */
.reports-synchronized-layout { display: flex; flex-direction: column; gap: 20px; max-width: 368px; }
.report-form-item { margin-bottom: 16px; }
.report-form-item label { display: block; font-size: 13px; font-weight: 600; color: #475569; margin-bottom: 8px; }
.dropdown-select { padding: 8px 12px; border: 1px solid #cbd5e1; border-radius: 4px; background-color: #fff; font-size: 13px; color: var(--text-main); outline: none; cursor: pointer; min-width: 160px; }
.dropdown-select:focus { border-color: var(--primary-blue); }

/* ---- Modal ---- */
.modal-overlay {
  position: fixed; inset: 0; background: rgba(15,23,42,0.4); display: flex;
  justify-content: center; align-items: center; z-index: 9999; opacity: 0; visibility: hidden; transition: all 0.2s ease;
}
.modal-overlay.active { opacity: 1; visibility: visible; }
.modal-box { background: #fff; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 460px; max-width: 92%; padding: 24px; transform: scale(0.95); transition: all 0.2s ease; max-height: 88vh; overflow-y: auto; }
.modal-overlay.active .modal-box { transform: scale(1); }
.modal-box h4 { font-size: 16px; font-weight: 600; color: #0f172a; margin-bottom: 14px; }
.modal-body { font-size: 14px; color: var(--text-muted); line-height: 1.5; margin-bottom: 20px; }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 8px; }
/* Optional extra action (Create Agent's "Download Agent"). margin-right:auto
   pushes it to the far left while Cancel/Save stay right-aligned, so the
   shared .modal-actions rule above needs no change. The [hidden] rule is
   required: an explicit display would otherwise beat the hidden attribute and
   the button would show on every modal that doesn't opt in.
   Styled as .btn, taking the outline-pill look through
   #modal #modal-extra above so it matches Save and Cancel beside it
   and "Generate CSR" on the CSR page. No font-family here on purpose: .btn
   sets none either, so leaving it unset is what makes this button resolve to
   the same face as every other .btn rather than to the body stack. */
.modal-extra { display: inline-flex; align-items: center; gap: 6px; margin-right: auto; }
.modal-extra[hidden] { display: none; }
/* ICON_OS_WINDOWS is filled var(--primary-blue) #577294. The pill's hover fill
   is --sb-ink #021F59, against which that blue is far too close to read, so the
   mark still has to be knocked out to white on hover. At rest the pill is #fff,
   where #577294 reads fine. `rect` targets the Windows mark alone: Tux is built
   from circle/ellipse/path and carries its own white belly and amber feet, so
   it stays legible on both fills. The icons' inline fill="" is a presentation
   attribute, so this rule wins without !important. */
.modal-extra:hover svg rect { fill: #fff; }

/* ---- Toast ---- */
.toast {
  position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 10000; background: #0f172a; color: #fff;
  padding: 14px 18px; border-radius: 8px; font-size: 14px; box-shadow: 0 8px 30px rgba(0,0,0,0.25);
  display: none; max-width: 360px;
}
.toast.error { background: var(--alert-red); }
.toast.success { background: var(--success-green); }
.toast.show { display: block; }

/* ---- Progress (discovery) ---- */
.progress-track { height: 4px; background: #e2e8f0; border-radius: 2px; overflow: hidden; margin: 12px 0; }
.progress-bar { height: 100%; background: var(--primary-blue); width: 0%; transition: width 0.3s ease; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
.csr-output { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; padding: 12px 14px; white-space: pre-wrap; word-break: break-all; font-family: ui-monospace, Menlo, monospace; font-size: 11px; max-height: 220px; overflow: auto; }

/* ---- Manage page: one compact card per connected agent ---- */
/* Border + shadow give each card a clear, unambiguous boundary; margin-
   bottom is deliberately tight (not the usual ~16-20px card spacing) since
   users may be scanning many agents at once and full-size gaps between
   every one would waste a lot of vertical space over a long list. */
.agent-card {
  background: #fff; border: 1px solid var(--border-color); border-radius: 8px;
  margin-bottom: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); overflow: hidden;
}
.agent-card:last-child { margin-bottom: 0; }
/* Header is two stacked rows, not one — deliberately, so the identity +
   status + kebab (the "top" row) can NEVER be pushed off-screen or require
   scrolling to reach, at any window width: the stats live in their own row
   below, free to wrap onto multiple lines on narrow windows without
   affecting the top row at all. */
.agent-card-header {
  padding: 12px 18px; border-bottom: 1px solid var(--border-color);
}
.agent-card-header-top {
  display: flex; align-items: center; flex-wrap: nowrap; gap: 14px;
}
.agent-card-header-top > .badge { flex-shrink: 0; }
/* Identity is the only thing allowed to shrink/truncate on the top row —
   status pill and kebab both stay flex-shrink:0 (full size, always visible)
   regardless of how narrow the window gets. */
.agent-card-header-left {
  display: flex; align-items: center; gap: 10px;
  min-width: 40px; flex-shrink: 1; overflow: hidden;
}
.agent-card-header-left > svg { flex-shrink: 0; }
.agent-card-os-host { font-size: 13px; color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 20px; flex-shrink: 1; }
.agent-card-sep { color: var(--border-color); flex-shrink: 0; }
.agent-card-label { font-size: 14px; color: #0f172a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 20px; flex-shrink: 1; }
.agent-card-header-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; margin-left: auto; }

.agent-card-stats-inline { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 22px; margin-top: 10px; }
.agent-stat { display: flex; align-items: baseline; gap: 6px; white-space: nowrap; }
.agent-stat-label { font-size: 10px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: #94a3b8; }
.agent-stat-value { font-size: 13px; color: #0f172a; }

.agent-card-body { padding: 4px 18px; }

.agent-empty-state { display: flex; align-items: center; gap: 12px; padding: 18px 4px; }
.agent-empty-title { font-size: 13px; font-weight: 600; color: #0f172a; }
.agent-empty-sub { font-size: 12px; color: #94a3b8; margin-top: 2px; }

.agent-cert-row {
  display: flex; align-items: center; gap: 10px; padding: 10px 4px;
  border-bottom: 1px solid #f1f5f9;
}
.agent-cert-row:last-child { border-bottom: none; }
.agent-cert-domain { flex: 1; min-width: 0; font-size: 13px; color: #0f172a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Protocol label (HTTPS/HTTP) and the trailing (address, host=...) portion
   of a binding row — both secondary to the site name, so muted relative
   to .agent-cert-domain's default text color. */
.agent-cert-proto { font-weight: 400; color: #64748b; margin-right: 6px; }
.agent-cert-addr { color: #94a3b8; font-weight: 400; margin-left: 6px; }
.agent-cert-expiry { font-size: 12px; color: #94a3b8; flex-shrink: 0; }

/* "Not in Inventory" cert rows on the Manage page: a real, visibly-labeled
   button (not just an icon a user has to hover to understand), so the add
   affordance reads at a glance.

   Also worn by Discovery's certificate-modal "Add to Inventory" — same action,
   same words, so it gets the same control instead of the blue-filled .btn it
   used to be. That one is a <button>, which is why font-family/line-height are
   set explicitly below: unlike the <a> this rule was written for, a <button>
   does not inherit the page font, and left alone it rendered in the UA default
   at a different size from the identical control on Manage. */
.agent-add-inventory {
  display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0;
  padding: 4px 10px; border-radius: 14px; cursor: pointer; text-decoration: none;
  font-family: inherit; line-height: 1.4;
  font-size: 12px; font-weight: 600; color: var(--primary-blue);
  background: #eff6ff; border: 1px solid #dbeafe;
}
.agent-add-inventory:hover { background: #dbeafe; border-color: var(--primary-blue); color: var(--primary-hover); }

/* Matched/in-inventory indicator for a resolved binding, at the end of the
   row after "expires in N days" — text is either "Already in Inventory"
   (passive Tier 2 domain-overlap match) or "Added to Inventory" (user
   clicked the Add-to-Inventory button, above), per agents._match_label.
   Same pill recipe as the Connected tag (#agent-manage-view .badge-valid,
   above) and the Add-to-Inventory pill (sibling in this same row), but a
   darker green triad so it reads as a distinct, more specific status than
   the agent-level Connected tag. Replaces the old bare circular checkmark. */
.agent-cert-managed {
  display: inline-flex; align-items: center; flex-shrink: 0;
  padding: 4px 10px; border-radius: 14px;
  font-size: 12px; font-weight: 600;
  background: #dcfce7; border: 1px solid #86efac; color: #166534;
}
/* "Not in Inventory" — same pill recipe, red triad (matches the
   Expiring/Expired pill color used elsewhere, e.g. #certificates-view
   .status-tag.badge-alert), shown alongside the Add-to-Inventory button
   rather than the old neutral outline dot. */
.agent-cert-unmanaged {
  display: inline-flex; align-items: center; flex-shrink: 0;
  padding: 4px 10px; border-radius: 14px;
  font-size: 12px; font-weight: 600;
  background: #fef2f2; border: 1px solid #fecaca; color: #b91c1c;
}
/* Confusable/homoglyph-lookalike flag (issue #286) — same pill recipe,
   amber triad so it reads as "worth a look" without the "Not in Inventory"
   pill's implication that something is simply missing. Passive only: this
   never appears instead of .agent-cert-unmanaged, always alongside it. */
.agent-cert-confusable {
  display: inline-flex; align-items: center; gap: 4px; flex-shrink: 0;
  padding: 4px 10px; border-radius: 14px;
  font-size: 12px; font-weight: 600;
  background: #fffbeb; border: 1px solid #fde68a; color: #92400e;
}

/* Kebab actions menu — small popover, closed by default. */
.agent-kebab { position: relative; }
.agent-kebab-btn {
  background: none; border: none; cursor: pointer; font-size: 16px; line-height: 1;
  color: #64748b; padding: 4px 8px; border-radius: 4px;
}
.agent-kebab-btn:hover { background: #f1f5f9; }
.agent-kebab-menu {
  display: none; position: absolute; top: 100%; right: 0; margin-top: 4px;
  background: #fff; border: 1px solid var(--border-color); border-radius: 6px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.08); min-width: 120px; z-index: 5; overflow: hidden;
}
.agent-kebab.open .agent-kebab-menu { display: block; }
.agent-kebab-menu a {
  display: block; padding: 8px 14px; font-size: 13px; color: var(--alert-red);
  text-decoration: none; cursor: pointer;
}
.agent-kebab-menu a:hover { background: #fef2f2; }
/* "Connected" pill on Manage's own agent cards, matching the same outlined-
   pill size/shape now used for Agents' Connected and Watch List's WHOIS/
   Valid (14px radius, 3px 10px padding, 12px font, 14px line-height, 72px
   min-width) — was previously the flat filled default .badge-valid look,
   now consistent across all three pages. Status tags only; Manage's own
   Delete lives in the kebab menu and is unaffected. */
#agent-manage-view .badge-valid {
  background: #f0fdf4; border: 1px solid #bbf7d0; color: #15803d; border-radius: 14px;
  padding: 3px 10px; font-size: 12px; line-height: 14px; min-width: 72px; text-align: center;
}

/* ---- Agents page: restyled to match the Manage page's card aesthetic
   (colors, spacing, typography, radius/shadow from the recent Manage
   redesign) — purely visual. The underlying <table> markup, columns,
   labels, buttons and JS are untouched; everything here is scoped to
   #agents-view so Certificates/Domains/Watch List/CSR — which share the
   same .metadata-table/.act-btn/.inventory-panel classes — keep their
   existing look. ---- */
#agents-view .inventory-panel {
  border-radius: 8px;
  box-shadow: 0 1px 2px rgba(0,0,0,0.04);
  padding: 18px;
}
#agents-view .panel-toolbar { margin-bottom: 12px; }
#agents-view .btn { border-radius: 6px; }
/* Column headers as small uppercase "eyebrow" labels, matching the
   agent-stat-label treatment on the Manage cards. */
#agents-view .metadata-table thead th {
  background: #f8fafc;
  color: #475569;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 10px 16px;
  border-bottom: 1px solid #f1f5f9;
  box-shadow: none;
}
#agents-view .metadata-table th:hover:not(.no-sort) { background: #fafbfc; color: var(--primary-blue); }
#agents-view .metadata-table th.active-sort { background: #fff; color: var(--primary-blue); }
/* Lighter row dividers (matches .agent-cert-row's #f1f5f9 hairlines rather
   than the heavier table border used elsewhere) and a tighter row height. */
#agents-view .metadata-table td {
  padding: 10px 16px;
  border-bottom: 1px solid #f1f5f9;
  font-size: 13px;
  color: #0f172a;
}
/* Host Label column: same size bump as elsewhere, but explicitly regular
   weight — <strong> is bold by default and this column should read as
   plain (not emphasized) text. */
#agents-view .metadata-table td strong { font-size: 14px; color: #0f172a; font-weight: 400; }
#agents-view .metadata-table tbody tr:hover { background: #fafbfc; }
/* Delete is a functional button, not a status tag — it stays the same
   rectangular, bordered .act-btn style used for Edit/Delete on
   Certificates/Domains (no page-specific override). Only "Connected"
   (a status tag, not a button) gets the pill treatment below, so tags and
   buttons stay visually distinct. */
/* "Connected" as a light-bg + colored-border + colored-text pill (same
   triad as .act-btn--watching, Domains' "Watching" pill) instead of the
   flat filled badge used elsewhere — status tags are pills, buttons
   (Delete, above) are rectangles. */
#agents-view .badge-valid {
  background: #f0fdf4; border: 1px solid #bbf7d0; color: #15803d; border-radius: 14px;
  padding: 3px 10px; font-size: 12px; line-height: 14px; min-width: 72px; text-align: center;
}

/* ---- Watch List page: restyled to match the Manage/Agents card aesthetic
   (colors, spacing, typography, radius/shadow), per the mockup — purely
   visual. The underlying <table> markup, columns, labels, buttons and JS
   are untouched; everything here is scoped to #watchlist-view so other
   pages sharing the same classes keep their existing look. ---- */
#watchlist-view .inventory-panel {
  border-radius: 8px;
  box-shadow: 0 1px 2px rgba(0,0,0,0.04);
  padding: 18px;
}
#watchlist-view .panel-toolbar { margin-bottom: 12px; }
#watchlist-view .btn { border-radius: 6px; }
/* Column headers as small uppercase "eyebrow" labels, matching the
   agent-stat-label treatment on the Manage cards. */
#watchlist-view .metadata-table thead th {
  background: #f8fafc;
  color: #475569;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 10px 16px;
  border-bottom: 1px solid #f1f5f9;
  box-shadow: none;
}
#watchlist-view .metadata-table th:hover:not(.no-sort) { background: #fafbfc; color: var(--primary-blue); }
#watchlist-view .metadata-table th.active-sort { background: #fff; color: var(--primary-blue); }
/* Lighter row dividers (matches .agent-cert-row's #f1f5f9 hairlines rather
   than the heavier table border used elsewhere) and a tighter row height. */
#watchlist-view .metadata-table td {
  padding: 10px 16px;
  border-bottom: 1px solid #f1f5f9;
  font-size: 13px;
  color: #0f172a;
}
/* Domain Name column: same size bump as elsewhere, but explicitly regular
   weight — <strong> is bold by default and this column should read as
   plain (not emphasized) text. */
#watchlist-view .metadata-table td strong { font-size: 14px; color: #0f172a; font-weight: 400; }
/* Preserve the Expired-row red highlight (tr.row-expired td strong), which
   the generic strong color override above would otherwise clobber since an
   ID-scoped selector outranks the unscoped tr.row-expired rule. */
#watchlist-view .metadata-table tr.row-expired td strong { color: var(--alert-red); }
/* Delete is a functional button, not a status tag — it stays the same
   rectangular, bordered .act-btn style used for Edit/Delete on
   Certificates/Domains (no page-specific override). Only WHOIS/Valid
   (status tags, not buttons) get the pill treatment below, so tags and
   buttons stay visually distinct. */
/* "WHOIS Now" was visibly taller than the 6h/12h/24h slider buttons next to
   it (.btn's 8px vertical padding + its font-size's default line-height vs.
   the slider's smaller font-size + padding). Both are pinned so the two
   heights are deterministic and equal at 28px: the slider is 5+5+14=24px per
   option plus the track's own 2px top/bottom padding, and this button is now
   held there by an explicit height rather than by padding arithmetic.
   The height is explicit because padding maths no longer determines it: the
   pill restyle added a 1px border and a 17px globe icon, and the icon (not
   the 14px line-height) is now the tallest thing in the row, which silently
   took the old 7+7+14 formula to 33px. An explicit height is immune to both.
   17px icon + 2px border = 19px, so it centres inside 28px without clipping. */
#watch-refresh-now { padding: 0 16px; height: 28px; line-height: 14px; }
#watchlist-view .watch-interval-opt { line-height: 14px; }
/* "WHOIS" (Source) and "Valid" (Status) as light-bg + colored-border +
   colored-text pills instead of the flat filled badge used elsewhere —
   status tags are pills, buttons (Delete, above) are rectangles. Valid
   reuses the same green triad as .act-btn--watching (Domains' "Watching"
   pill); WHOIS gets the equivalent sky-blue triad. */
#watchlist-view .badge-whois {
  background: #f0f9ff; border: 1px solid #bae6fd; color: #0369a1; border-radius: 14px;
  padding: 3px 10px; font-size: 12px; line-height: 14px; min-width: 72px; text-align: center;
}
#watchlist-view .badge-valid,
#certificates-view .badge-valid,
#domains-view .badge-valid {
  background: #f0fdf4; border: 1px solid #bbf7d0; color: #15803d; border-radius: 14px;
  padding: 3px 10px; font-size: 12px; line-height: 14px; min-width: 72px; text-align: center;
}
/* Expiring/Expired (badge-alert) on the same three pages' Status column,
   matching Valid's exact pill dimensions above — same box model, same
   min-width (not a fixed width: "Expiring" renders as "N days" and needs
   room to grow past 72px for larger day counts, same as the original
   unpilled .status-tag's min-width:68px did). Colors kept (existing
   badge-alert red), border added to match the outlined-pill recipe used
   everywhere else (bg-50/border-200/text-700 style triad). Qualified with
   .status-tag (always paired with badge-valid/badge-alert by statusInner())
   rather than bare .badge-alert, since Domains/Watch List also use
   .badge-alert for the WHOIS seizure/hijack warning badge in the
   Nameservers column — a completely different, unrelated tag that must
   NOT be reshaped into this pill. This is a deliberate, explicit exception
   to the earlier "leave the Status column untouched" instruction for
   Certificates/Domains — dimensions must now exactly match Watch List's
   tags, per instruction. */
#watchlist-view .status-tag.badge-alert,
#certificates-view .status-tag.badge-alert,
#domains-view .status-tag.badge-alert {
  background: #fef2f2; border: 1px solid #fecaca; color: #b91c1c; border-radius: 14px;
  padding: 3px 10px; font-size: 12px; line-height: 14px; min-width: 72px; text-align: center;
}

/* ---- CSR Generator page: restyled to match the Manage/Agents/Watch List
   card aesthetic (colors, spacing, typography, radius/shadow) — purely
   visual, scoped to #csr-view so other pages keep their existing look.
   Only the "CSR Requests" inventory-panel/table is touched (the top
   generate-CSR/output form cards use a different, unrelated .cert-card
   component with no equivalent on Manage/Agents/Watch List, so left as-is).
   The underlying markup, columns, labels, buttons and JS are untouched. ---- */
#csr-view .inventory-panel {
  border-radius: 8px;
  box-shadow: 0 1px 2px rgba(0,0,0,0.04);
  padding: 18px;
}
#csr-view .panel-toolbar { margin-bottom: 12px; }
/* Column headers as small uppercase "eyebrow" labels, matching the
   agent-stat-label treatment on the Manage cards. */
#csr-view .metadata-table thead th {
  background: #f8fafc;
  color: #475569;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 10px 16px;
  border-bottom: 1px solid #f1f5f9;
  box-shadow: none;
}
#csr-view .metadata-table th:hover:not(.no-sort) { background: #fafbfc; color: var(--primary-blue); }
#csr-view .metadata-table th.active-sort { background: #fff; color: var(--primary-blue); }
/* Lighter row dividers (matches .agent-cert-row's #f1f5f9 hairlines rather
   than the heavier table border used elsewhere) and a tighter row height. */
#csr-view .metadata-table td {
  padding: 10px 16px;
  border-bottom: 1px solid #f1f5f9;
  font-size: 13px;
  color: #0f172a;
}
/* Domain (CN) column: same size bump as elsewhere, but explicitly regular
   weight — <strong> is bold by default and this column should read as
   plain (not emphasized) text. */
#csr-view .metadata-table td strong { font-size: 14px; color: #0f172a; font-weight: 400; }
#csr-view .metadata-table tbody tr:hover { background: #fafbfc; }
/* Right-anchored, scoped to this view only — overrides the shared
   .metadata-table td.actions-cell { text-align: left } (every other table's
   Actions column stays left-aligned; this one alone needs it flipped). The
   .row-actions pill is inline-flex and sized to its own content, so a row
   with fewer icons than another would otherwise leave Delete — always the
   last icon — sitting at a different X. Self-managed and ACME-managed rows
   both carry exactly 3 icons today (see csrTable above), so this isn't
   presently load-bearing, but it's kept as an explicit guarantee rather
   than relying on both branches happening to match in count. */
#csr-view .metadata-table td.actions-cell { text-align: right; }
/* Status (Pending/Signed/Revoked) as light-bg + colored-border +
   colored-text pills — same size/shape recipe as WHOIS/Valid/Connected
   elsewhere (14px radius, 3px 10px padding, 12px font, 72px min-width) —
   instead of the flat filled tag used before. Colors kept, just Signed's
   green triad now matches .act-btn--watching/Connected/Valid exactly, and
   Revoked's red triad matches the app's existing red-pill recipe. */
#csr-view .status-pending, #csr-view .status-signed, #csr-view .status-revoked {
  border-radius: 14px; padding: 3px 10px; font-size: 12px; line-height: 14px;
  min-width: 72px; text-align: center; display: inline-block;
}
#csr-view .status-pending { background: #fef3c7; border: 1px solid #fde68a; color: #854f0b; }
#csr-view .status-signed  { background: #f0fdf4; border: 1px solid #bbf7d0; color: #15803d; }
#csr-view .status-revoked { background: #fef2f2; border: 1px solid #fecaca; color: #b91c1c; }
/* Domain verification tag — TWO elements sharing this class, not one:
   #csr-verify-tag-dns (last child of the DNS card's row: Copy Host — Copy
   Value — Verify Domain — tag) and #csr-verify-tag-csr (in the CSR card's
   row instead, pushed right of the three download buttons via its own
   margin-left:auto — see index.html). Exactly one is ever visible, since
   exactly one of the two rows is ever shown (setCardMode in app.js), but
   both exist so the tag PERSISTS once verification succeeds and the CSR
   card takes over, rather than vanishing along with the DNS row that used
   to be its only home. renderVerifyTag() keeps both in sync — same pill
   recipe as the Status column above, just binary: "verified" covers the
   ACME order's issuing AND signed statuses alike (DNS-01 passed, whether
   or not the certificate itself has finalized yet), everything else (no
   order, pending, validating, failed) reads as "not verified". Both
   hidden entirely until the first Generate Certificate click — showing
   red before anything was attempted would misread as a failed check that
   never happened. min-width, not an exact match to any one button — just
   enough that neither row visibly resizes when the label flips between
   "Domain Not Verified"/"Domain Verified". */
.verify-tag {
  border-radius: 14px; padding: 3px 10px; font-size: 12px; line-height: 14px;
  min-width: 150px; text-align: center; display: inline-block;
}
.verify-tag.status-not-verified { background: #fef2f2; border: 1px solid #fecaca; color: #b91c1c; }
.verify-tag.status-verified     { background: #f0fdf4; border: 1px solid #bbf7d0; color: #15803d; }
/* Clear All — same pill geometry as its Generate CSR/Generate Certificate
   row-mates, but a light-red triad (the same one used above for "Not
   Verified"/Revoked) rather than white or grey: grey would have read as a
   second "disabled" control once Generate Certificate starts fading out
   while verification is pending, so red signals reset/destructive while
   staying visibly clickable. */
#csr-view #csr-clear-btn {
  background: #fef2f2; color: #b91c1c; border: 1px solid #fecaca; border-radius: 999px;
}
/* Verify Domain keeps its own green (.btn-success, generic rule) rather
   than joining the white-pill list below, but still needs that list's
   pill radius — a bare .btn-success is only border-radius:4px (the base
   .btn default), which read as sharper/more rectangular than every other
   button on this page. */
#csr-view #csr-verify-domain-btn { border-radius: 999px; }
#csr-view #csr-clear-btn:hover { background: var(--alert-red); border-color: var(--alert-red); color: #fff; }
/* DNS card's Host/Value fields (index.html) — copy-to-clipboard used to be
   separate "Copy Host"/"Copy Value" buttons below the fields; now it's an
   icon inset into the field itself. .csr-output already sets padding:12px
   14px — padding-left alone is widened here to make room for the icon,
   which is absolutely positioned within the field (position:relative) so
   it never affects the field's own width (box-sizing:border-box applies
   app-wide, so padding changes never grow the box regardless). */
#csr-view .dns-field-copy { position: relative; padding-left: 36px; }
#csr-view .dns-field-copy-icon {
  position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 4px; color: #64748b; cursor: pointer;
}
#csr-view .dns-field-copy-icon:hover { color: var(--primary-blue); background: #e2e8f0; }
/* Download/Status/Delete moved from plain .edit-link/.delete-link text links
   to the shared .icon-act-btn treatment (see .icon-act-btn and its
   --download/--status/--delete hover rules above) — no CSR-specific button
   styling needed here anymore. */

/* ---- Certificates & Domains pages: restyled to match the Manage/Agents/
   Watch List/CSR card aesthetic (panel radius/shadow, eyebrow column
   headers, lighter row dividers) — purely visual, scoped to
   #certificates-view/#domains-view so nothing else is affected. Only the
   bottom Certificate/Domain Inventory panel is touched — the top summary
   area uses the unrelated .status-bar classes and is left completely
   alone, as is everything about the Status column (its
   .badge.status-tag colors/sizing, the .status-icon-slot reserved-space
   alignment, and the Expiring/Expired icons), and the Actions column's
   left-alignment (.actions-cell already sets text-align:left — untouched).
   Edit/Delete are icon buttons (.icon-act-btn, green/red via currentColor —
   see the Buttons/links section above) rather than text .act-btn pills.
   The underlying markup, columns, labels, and JS are untouched. ---- */
#certificates-view .inventory-panel,
#domains-view .inventory-panel {
  border-radius: 8px;
  box-shadow: 0 1px 2px rgba(0,0,0,0.04);
  padding: 18px;
}
#certificates-view .panel-toolbar,
#domains-view .panel-toolbar { margin-bottom: 12px; }
/* Column headers as small uppercase "eyebrow" labels, matching the
   agent-stat-label treatment on the Manage cards. */
#certificates-view .metadata-table thead th,
#domains-view .metadata-table thead th {
  background: #f8fafc;
  color: #475569;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 10px 16px;
  border-bottom: 1px solid #f1f5f9;
  box-shadow: none;
}
#certificates-view .metadata-table th:hover:not(.no-sort),
#domains-view .metadata-table th:hover:not(.no-sort) { background: #fafbfc; color: var(--primary-blue); }
#certificates-view .metadata-table th.active-sort,
#domains-view .metadata-table th.active-sort { background: #fff; color: var(--primary-blue); }
/* Lighter row dividers (matches .agent-cert-row's #f1f5f9 hairlines rather
   than the heavier table border used elsewhere) and a tighter row height.
   Cell padding/border only — does not touch the Status/Actions cells'
   inner content, sizing, or alignment. */
#certificates-view .metadata-table td,
#domains-view .metadata-table td {
  padding: 10px 16px;
  border-bottom: 1px solid #f1f5f9;
  font-size: 13px;
  color: #0f172a;
}
/* Common Name / Domain Name column: same size bump as elsewhere, but
   explicitly regular weight — <strong> is bold by default and this column
   should read as plain (not emphasized) text. */
#certificates-view .metadata-table td strong,
#domains-view .metadata-table td strong { font-size: 14px; color: #0f172a; font-weight: 400; }
/* Preserve the Expired-row red highlight (tr.row-expired td strong), which
   the generic strong color override above would otherwise clobber since an
   ID-scoped selector outranks the unscoped tr.row-expired rule. */
#certificates-view .metadata-table tr.row-expired td strong,
#domains-view .metadata-table tr.row-expired td strong { color: var(--alert-red); }
#certificates-view .metadata-table tbody tr:hover,
#domains-view .metadata-table tbody tr:hover { background: #fafbfc; }
/* Source tags (Uploaded/Manually/Agent/Discovery) as light-bg + colored-border +
   colored-text pills, matching WHOIS/Valid's treatment on Watch List —
   colors kept, just adding a border + pill radius. WHOIS is NOT included here
   (not named in the request) and is left as the flat filled badge. The Status
   column's Valid/Expiring/Expired tags are a completely separate class
   (.status-tag/.badge-valid/.badge-alert) and are untouched by this rule.

   .badge-discovered joins the list because Discovery-sourced rows now reach it
   at all — sourceBadge's lookup key was "Discovered" while the backend writes
   "Discovery", so the class never matched and those rows rendered as plain
   text. Without adding it here it would render as a flat square badge beside
   Agent's pill, which is the difference that was reported. */
#certificates-view .badge-uploaded, #certificates-view .badge-manually, #certificates-view .badge-agent, #certificates-view .badge-discovered,
#domains-view .badge-uploaded, #domains-view .badge-manually, #domains-view .badge-agent, #domains-view .badge-discovered {
  border-radius: 14px; padding: 3px 10px; font-size: 12px; line-height: 14px;
  min-width: 72px; text-align: center; display: inline-block;
}
#certificates-view .badge-uploaded, #certificates-view .badge-manually,
#domains-view .badge-uploaded, #domains-view .badge-manually { border: 1px solid #fde68a; }
#certificates-view .badge-agent, #domains-view .badge-agent { border: 1px solid #c7d2fe; }
#certificates-view .badge-discovered, #domains-view .badge-discovered { border: 1px solid #99f6e4; }

/* ---- Discovery / Alert Log / Audit Log: full card aesthetic — same
   treatment as Manage/Agents/Watch List/CSR/Certificates/Domains (panel
   radius/shadow, eyebrow column headers w/ restored gray band, lighter row
   dividers, darkened text, pill-shaped tags). None of these three pages has
   a row-level Edit/Delete Actions column (they're read-only scan results /
   logs) — only toolbar buttons (Run scan, Query crt.sh, Apply), which get
   the same 6px radius touch as the other pages' .btn but have no Edit-vs-
   Delete semantics to color. Markup/JS untouched throughout. ---- */
#discovery-view .inventory-panel, #alert-log-view .inventory-panel, #audit-view .inventory-panel {
  border-radius: 8px;
  box-shadow: 0 1px 2px rgba(0,0,0,0.04);
  padding: 18px;
}
#alert-log-view .panel-toolbar, #audit-view .panel-toolbar { margin-bottom: 12px; }
/* Discovery's toolbar is the ONLY thing in its panel now (the progress bar
   below it is display:none unless a scan is running), so any bottom margin
   here is pure asymmetry: the panel's own 18px padding is what sets the gap
   above the toolbar, and 12px + 18px made the gap below it 30px. Zero here
   makes both sides 18px. The bar supplies its own 12px margins while visible,
   so mid-scan spacing is unaffected. Split out of the shared rule above rather
   than overridden after it — Alert Log and Audit still have content under
   their toolbars and still want the 12px. */
#discovery-view .panel-toolbar { margin-bottom: 0; }
#discovery-view .btn, #alert-log-view .btn, #audit-view .btn { border-radius: 6px; }
/* Column headers as small uppercase "eyebrow" labels, with the gray band
   restored (matches the Certificates/Domains fix). */
#discovery-view .metadata-table thead th, #alert-log-view .metadata-table thead th, #audit-view .metadata-table thead th {
  background: #f8fafc;
  color: #475569;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 10px 16px;
  border-bottom: 1px solid #f1f5f9;
  box-shadow: none;
}
#discovery-view .metadata-table th:hover:not(.no-sort), #alert-log-view .metadata-table th:hover:not(.no-sort), #audit-view .metadata-table th:hover:not(.no-sort) { background: #fafbfc; color: var(--primary-blue); }
#discovery-view .metadata-table th.active-sort, #alert-log-view .metadata-table th.active-sort, #audit-view .metadata-table th.active-sort { background: #fff; color: var(--primary-blue); }
#discovery-view .metadata-table td, #alert-log-view .metadata-table td, #audit-view .metadata-table td {
  padding: 10px 16px;
  border-bottom: 1px solid #f1f5f9;
  font-size: 13px;
  color: #0f172a;
}
#discovery-view .metadata-table tbody tr:hover, #alert-log-view .metadata-table tbody tr:hover, #audit-view .metadata-table tbody tr:hover { background: #fafbfc; }
/* Discovery's Domain column: explicitly regular weight — <strong> is bold
   by default and this column should read as plain (not emphasized) text.
   Alert Log / Audit Log have no <strong> column at all (nothing to unbold). */
#discovery-view .metadata-table td strong { font-size: 14px; color: #0f172a; font-weight: 400; }
/* Discovery's TCP-check tag (Success/Failed) was plain colored text with no
   background at all — now a pill, same green/red triads used everywhere
   else (Signed/Connected/Valid green; Revoked/Expired-style red). */
#discovery-view .status-valid, #discovery-view .status-expired {
  display: inline-block; border-radius: 14px; padding: 3px 10px; font-size: 12px;
  line-height: 14px; width: 72px; text-align: center; white-space: nowrap;
}
#discovery-view .status-valid { background: #f0fdf4; border: 1px solid #bbf7d0; color: #15803d; }
#discovery-view .status-expired { background: #fef2f2; border: 1px solid #fecaca; color: #b91c1c; }
/* "Already in Inventory" in the Discovery detail modal (#disc-cert-modal)
   reuses .status-valid — same class, same green tokens as the table's
   Success pill above — but not the fixed 72px column width: that's sized for
   short check-cell words ("Success"/"Blocked"), and "Already in Inventory"
   is a modal caption, not a table column that needs to align down a page. */
#disc-cert-modal .status-valid {
  display: inline-block; border-radius: 14px; padding: 3px 10px; font-size: 12px;
  line-height: 14px; white-space: nowrap; background: #f0fdf4; border: 1px solid #bbf7d0; color: #15803d;
}
/* Alert Log's Type tag (Slack/Watch/Email) — same pill treatment, colors
   kept, just adding a border + pill radius per hue. */
#alert-log-view .badge-slack, #alert-log-view .badge-watch, #alert-log-view .badge-email {
  border-radius: 14px; padding: 3px 10px; font-size: 12px; line-height: 14px;
  min-width: 72px; text-align: center; display: inline-block;
}
#alert-log-view .badge-slack { border: 1px solid #e9d5ff; }
#alert-log-view .badge-watch { border: 1px solid #fed7aa; }
#alert-log-view .badge-email { border: 1px solid #bfdbfe; }

/* ---- Print: show only the compiled inventory table (hide sidebar/nav/cards) ---- */
@media print {
  body * { visibility: hidden; }
  #print-compiled-sandbox, #print-compiled-sandbox * { visibility: visible; }
  #print-compiled-sandbox {
    display: block !important;
    visibility: visible !important;
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    background: #ffffff;
    color: #000000;
    padding: 40px;
  }
  .print-table { width: 100%; border-collapse: collapse; margin-top: 20px; }
  .print-table th, .print-table td { border: 1px solid #000000; padding: 8px 12px; text-align: left; font-size: 12px; }
}

/* ==========================================================================
   Inventory status bar (Certificates + Domains page headers)
   --------------------------------------------------------------------------
   Replaces the older stat-card pair (count + pills + semicircle gauge). Counts
   come from GET /api/stats/inventory, so nothing here classifies expiry dates.

   The --sb-* tokens live on :root (below). They were originally scoped to
   .status-bar; that scoping was never about preventing collisions — the names
   are uniquely prefixed — but about signalling that the design spec's amber
   (#F3A220) and link blue (#0069D9) differ from the app-wide --warning-amber
   (#f59e0b) and --primary-blue (#577294). That still holds: defining --sb-*
   globally does NOT retune those globals, which are used across the rest of
   the UI. Green (#16A34A) and red (#DC2626) already match the globals exactly.

   The spec calls for Poppins; we deliberately stay on the app's system stack
   rather than add a webfont for one component.
   ========================================================================== */
/* These are global rather than scoped, and deliberately so. They used to sit
   on a selector list — .status-bar, .panel-brand, then one id per button that
   borrowed the status bar's pill styling from elsewhere in the app. That list
   was a trap: a consumer added outside it did not fail loudly, it failed
   asymmetrically. `color: var(--sb-ink)` is an inherited property, so an
   unresolved lookup degrades to the inherited color and the REST state still
   looks right; `background: var(--sb-ink)` is not inherited, so it degrades to
   `transparent` and only the HOVER breaks — white text on a white background,
   invisible under the cursor. That shipped once on the Watch List's "WHOIS
   Now" and was one forgotten id away from shipping three more times.

   :root removes the failure mode instead of documenting it: any element can
   read these, so styling can be shared without also remembering to extend a
   token list. The prefix keeps them from colliding with the app-wide globals
   in the block above.

   --sb-chan-knockout is NOT here on purpose: it is a per-state override set
   inside .sb-chan.is-off, not a palette entry, and it must stay scoped. */
:root {
  --sb-ink: #021F59;
  --sb-label: #223B6E;
  --sb-muted: #5C667A;
  --sb-faint: #8A93A6;
  --sb-hairline: #E6E9EF;
  --sb-tint: #FAFAFE;
  --sb-track: #EEF1F6;
  --sb-green: #16A34A;
  --sb-green-dark: #15803D;
  --sb-green-bg: #ECFDF3;
  --sb-green-border: #C4EED4;
  --sb-amber: #F3A220;
  --sb-amber-ink: #96690C;
  --sb-amber-bg: #FFFBEB;
  --sb-amber-border: #F7E3AE;
  --sb-red: #DC2626;
  --sb-red-ink: #B42318;
  --sb-red-bg: #FEF2F2;
  --sb-red-border: #F8D2D2;
  /* Blue completes the set for Snapshot's Domains badge. Built the same way as
     the three families above — base, darker ink, pale fill, pale border — so a
     blue element sits at the same weight as its green/amber/red siblings.
     Distinct from --sb-link below, which is an interaction color, not a status
     one: reusing the link blue for a static badge would make it look clickable. */
  --sb-blue: #2563EB;
  --sb-blue-ink: #1D4ED8;
  --sb-blue-bg: #EFF6FF;
  --sb-blue-border: #C7DBFB;
  /* --sb-neutral is gone with the Snapshot rings: it colored their "neither
     healthy nor a fault" arcs (no expiry date, a never-installed agent). The
     bar's equivalent bucket is .sb-seg-unknown, which uses --sb-faint. */
  --sb-link: #0069D9;
  --sb-link-hover: #7C39F4;
}

.status-bar {
  background: #ffffff;
  border: 1px solid var(--sb-hairline);
  /* Matches the inventory panel directly below it (the 8px override at
     "Certificates & Domains pages: restyled…" above, not the global
     --card-radius token, which these two views deliberately opt out of).
     Change one and check the other — the two cards sit 5px apart and any
     mismatch reads immediately. */
  border-radius: 8px;
  box-shadow: 0 10px 28px rgba(2, 31, 89, 0.08);
  /* No `overflow: hidden` here. It existed only to clip children to the card
     radius, but on at least one real browser the bar collapsed to the height of
     its header strip and clipped .sb-body away entirely — the metric panels
     rendered with full content (verified in the DOM) and were simply not
     painted. Do not reinstate it without re-checking the card body still
     renders. The children do NOT fit the radius on their own — this comment
     used to claim they did, which is why the corners rendered square. The
     clipping that fixes that lives on .sb-body instead; see the note there. */
  /* Sole source of the gap to the inventory panel below — that panel sets no
     margin of its own, and the active view is a flex column so nothing
     collapses. At this distance the shadow above lands on the panel (a later
     sibling with an opaque background) and is mostly painted over; that is
     accepted, not an oversight. */
  margin-bottom: 5px;
}

/* ---------- live-inventory branding ----------
   Was the status bar's own header strip; it now heads the inventory table
   panel instead (.panel-brand in .panel-toolbar), so the dot and eyebrow are
   the only parts that survived. .sb-eyebrow stays a child span rather than
   classes on the <h3>: `.panel-toolbar h3` is specificity 0,1,1 and would
   otherwise win over .sb-eyebrow's 0,1,0 on font-size and color. */
.panel-brand { display: flex; align-items: center; gap: 9px; }
.sb-dot {
  width: 7px; height: 7px; border-radius: 999px;
  background: var(--sb-green); flex-shrink: 0;
  animation: sbBlip 2s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) { .sb-dot { animation: none; } }
@keyframes sbBlip { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
.sb-eyebrow {
  font-size: 11px; font-weight: 700; letter-spacing: 0.14em;
  color: var(--sb-label); text-transform: uppercase;
}

/* ---------- body grid ---------- */
/* Only the fixed third track is tunable: the metric columns are 1fr 1fr and
   split what's left, so each narrows by half of any increase here (330 vs the
   previous 300 = 15px off each metric card). Setting fixed widths on the metric
   tracks instead would strand the freed space at the end of the row rather than
   giving it to Data Actions. */
/* The clipping lives here, on the grid — NOT on .status-bar, where
   `overflow: hidden` once collapsed the card to its header strip (see the
   warning in that rule). .status-bar has no padding, so .sb-metric's opaque
   --sb-tint background runs square into the card's rounded corners and squares
   them off visually; the card's border-radius alone therefore rendered as no
   rounding at all on the left corners. `inherit` tracks .status-bar's radius so
   the two can never drift, and it stays correct at both the 1180px and 760px
   breakpoints, where .sb-actions reflows and different children own the
   corners. */
.sb-body {
  display: grid; grid-template-columns: 1fr 1fr 330px; align-items: stretch;
  border-radius: inherit; overflow: hidden;
}
/* min-height reserves this panel's own computed content height (~185px: 40px
   padding + 44px head + 14px gap + 36px stat tiles + 28px gap/margin + ~23px
   footer pills) so the tinted background doesn't visibly collapse while
   renderStatusBar() is still awaiting GET /api/stats/inventory, then pop to
   full size once it resolves. A no-op once populated, since content already
   reaches this height; grid stretch (.sb-body's align-items: stretch) likely
   already keeps this panel full height regardless, since .sb-actions's
   static content exceeds it — this is a cheap defensive floor in case that
   ever isn't true, not the primary mechanism holding the card's shape. */
.sb-metric {
  padding: 20px 24px;
  display: flex; flex-direction: column; gap: 14px;
  background: var(--sb-tint);
  border-right: 1px solid var(--sb-hairline);
  min-width: 0; min-height: 186px;
}
/* .sb-total aligns to the SUBTITLE's baseline, not the title's. .sb-titles is a
   column flex container, so plain `baseline` resolves to its FIRST item
   (.sb-title) — that is what sat the number too high. `last baseline` resolves
   to its LAST item (.sb-sub) instead. Declared after a `flex-end` fallback:
   engines that don't parse `last baseline` drop the second declaration and
   bottom-align, which lands the number a few px high rather than breaking the
   row. Metric-driven on purpose — a fixed offset on .sb-total would need a
   second value for the 36px .sb-total at the 760px breakpoint. */
.sb-head {
  display: flex; gap: 12px;
  align-items: flex-end;
  align-items: last baseline;
}
/* `min-width: 3ch` reserves three digits' worth of width. tabular-nums equalises
   digit widths but not digit COUNTS, and .sb-bar now sizes off whatever space
   the row has left — so the day one card's total reaches 3 digits while the
   other is still on 2, the two columns would diverge by ~26px and take the bar
   lengths with them. This holds the row stable to 999 on both cards. It is in ch
   so it tracks the 36px .sb-total at the 760px breakpoint without a second
   value. `text-align: center` splits the reserved slack either side of the
   digits while the total is short of 3 digits, so it reads as symmetric padding
   rather than as a gap hanging off the right of the number. */
.sb-total {
  font-size: 44px; font-weight: 700; color: var(--sb-ink);
  line-height: 1; letter-spacing: -0.03em; font-variant-numeric: tabular-nums;
  min-width: 3ch; text-align: center;
}
/* `flex: 1 1 auto` rather than shrink-to-fit: it makes .sb-titles resolve to the
   same width on both metric cards (their subtitle strings differ in length), so
   the two .sb-bar strips line up with each other. .sb-total is tabular-nums, so
   the left column matches too. */
.sb-titles { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
.sb-title { font-size: 15px; font-weight: 600; color: var(--sb-ink); letter-spacing: -0.01em; }
/* The bar shares this row with the subtitle. `baseline` is deliberate over
   `center`: .sb-head aligns .sb-total to .sb-titles' LAST baseline, which now
   resolves through this row — baseline keeps .sb-sub defining it, so the big
   number stays aligned to the subtitle. Switching to `center` would leave the
   row's baseline synthesized and drift the number. */
.sb-subrow { display: flex; align-items: baseline; gap: 15px; min-width: 0; }
/* Truncates rather than squeezing the bar: the bar's min-width wins and the
   string ellipsises, which below roughly a 1310px viewport is what keeps the
   strip legible at the narrow end of the 3-column layout. */
.sb-sub {
  font-size: 11.5px; color: var(--sb-muted);
  min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ---------- segmented bar ---------- */
/* Fills whatever the subtitle leaves, holding the row's 15px gap off the text.
   This deliberately replaced a fixed 120px that kept both cards' strips exactly
   equal: bar = row − subtitle − gap, and the two subtitle strings differ by
   ~11px, so a gap measured from the glyphs and equal bar lengths cannot both
   hold. The ~11px was accepted because each bar is self-normalising — its
   segments are flex-weighted within their own total, so it shows a proportion,
   not an absolute — and both bars still end on the same right edge. Below about
   1310px both strings ellipsise and both bars clamp to min-width, i.e. they are
   identical exactly where the row is tightest.
   `overflow: hidden` clips the segments to the pill radius (this is not the
   .status-bar overflow that collapsed the card body — different element). */
.sb-bar {
  display: flex; height: 10px; gap: 2px;
  border-radius: 999px; overflow: hidden; background: var(--sb-track);
  flex: 1 1 auto; min-width: 80px;
}
.sb-seg-active { background: linear-gradient(90deg, var(--sb-green-dark), var(--sb-green)); }
.sb-seg-expiring { background: var(--sb-amber); }
.sb-seg-expired { background: var(--sb-red); }
.sb-seg-unknown { background: var(--sb-faint); }

/* ---------- stat tiles ---------- */
.sb-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.sb-stats.has-unknown { grid-template-columns: repeat(4, 1fr); }
/* `color` drives both the left border and the icon fill (currentColor), so the
   zero-state can mute both by setting one property. */
.sb-stat {
  display: flex; flex-direction: column; gap: 3px;
  padding-left: 10px; border-left: 3px solid currentColor; min-width: 0;
}
.sb-stat.is-active { color: var(--sb-green); }
.sb-stat.is-expiring { color: var(--sb-amber); }
.sb-stat.is-expired { color: var(--sb-red); }
.sb-stat.is-unknown { color: var(--sb-faint); }
.sb-stat.is-zero { color: var(--sb-faint); }
/* No justify-content: the icon belongs to the label and reads as part of it, so
   it sits one `gap` away rather than being pushed to the tile's far edge.
   `space-between` was doing exactly that — and because it hands ALL free space
   to the middle, the 6px gap below could never take effect; the real spacing was
   whatever the grid column happened to leave over. Left-packed, the gap governs,
   and 6px is the icon-to-text step already used by .os-cell and .agent-stat.
   Not wrapped: no flex-wrap here, and .sb-stat-icon is flex-shrink: 0, so the
   pair stays on one line. */
.sb-stat-row { display: flex; align-items: center; gap: 6px; }
.sb-stat-label {
  font-size: 10.5px; font-weight: 600; letter-spacing: 0.06em;
  color: var(--sb-muted); text-transform: uppercase;
}
.sb-stat-icon { width: 14px; height: 14px; flex-shrink: 0; display: block; }
.sb-stat-value {
  font-size: 19px; font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums; color: var(--sb-ink);
}
.sb-stat.is-expired .sb-stat-value { color: var(--sb-red-ink); }
.sb-stat.is-zero .sb-stat-value { color: var(--sb-faint); }

/* ---------- footer row ---------- */
.sb-foot { display: flex; align-items: center; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
.sb-link {
  font-size: 12.5px; font-weight: 600; color: var(--sb-link);
  display: inline-flex; align-items: center; gap: 6px;
  text-decoration: none; cursor: pointer;
}
.sb-link:hover { color: var(--sb-link-hover); }
.sb-link .sb-arrow { font-size: 13px; }
.sb-pill {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11px; font-weight: 700;
  padding: 4px 9px; border-radius: 7px; border: 1px solid;
}
.sb-pill.is-green { background: var(--sb-green-bg); border-color: var(--sb-green-border); color: var(--sb-green-dark); }
.sb-pill.is-red { background: var(--sb-red-bg); border-color: var(--sb-red-border); color: var(--sb-red-ink); }
/* Empty inventory: "0% healthy" is not a failure, so it does not read as one. */
.sb-pill.is-zero { background: #f4f6f9; border-color: var(--sb-hairline); color: var(--sb-faint); }
/* One rule for both footer labels so "Alert:" and "Agents" cannot drift. */
.sb-alert-label, .sb-agents-label { font-size: 11.5px; color: var(--sb-muted); }

/* Per-channel alert chips (Slack / Teams / Mail). These replaced a single
   Active/Inactive pill, which reported email recipients only and so read
   "Inactive" on a tenant whose Slack and Teams alerts were firing normally.

   Sizing: 30x22px, so 3x30px + 2x5px = 100px for the group, against ~52px for
   the single pill they replaced. That extra ~48px is what pushes .sb-foot
   toward wrapping on a narrow panel — the row has flex-wrap, so it degrades to
   two lines rather than overflowing, but this is the piece to re-measure if
   anything else is ever added to the footer.

   The 22px height is deliberate: it is the original chip height, chosen to sit
   level with the "% healthy" .sb-pill next to it (11px text, 4px padding a
   side, 1px border — which lands near 22-23px once line-height is applied) so
   the chips add no height to the footer. Widen freely; revisit the height only
   alongside .sb-pill. The glyph inside stays 12px.

   State must not be carried by hue alone: "off" is a solid filled block against
   the other two states' pale backgrounds, and every chip has a title/aria-label
   spelling the state out (sbAlertChips in app.js). */
.sb-chans { display: inline-flex; align-items: center; gap: 5px; }
.sb-chan {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 22px;
  border-radius: 7px; border: 1px solid; flex-shrink: 0;
}
.sb-chan .sb-chan-icon { width: 12px; height: 12px; display: block; }
/* Configured and enabled — alerts will be sent here. */
.sb-chan.is-active { background: var(--sb-green-bg); border-color: var(--sb-green-border); color: var(--sb-green-dark); }
/* Configured, but the master toggle is off. Muted rather than red: the channel
   is set up and one switch from working, which is not the same failure as
   never having been configured. */
.sb-chan.is-paused { background: #f4f6f9; border-color: var(--sb-hairline); color: var(--sb-faint); }
/* Not configured. Solid --sb-red — the same variable .sb-seg-expired uses, so
   "unconfigured" and "expired" read as the same red on one page. White icon on
   top for contrast, which needs the knockout detail inside the Teams and Mail
   glyphs to become red (see SB_CHANNEL_ICONS) or it would vanish into them.

   The filled block also replaces the dashed border this state used to carry as
   its non-color cue: solid-vs-pale is a stronger distinction from the other two
   states than a dashed edge was, and dashes on a solid fill just read as noise.
   The title/aria-label still names the state in words.

   Border matches the fill, so the chip reads as one solid block. A white frame
   was tried here and removed: .status-bar is #ffffff, so it was invisible
   against the card and merely inset the red by 1px a side. */
.sb-chan.is-off {
  background: var(--sb-red); border-color: var(--sb-red);
  color: #fff; --sb-chan-knockout: var(--sb-red);
}

/* "Agents" label + a Connected/Disconnected pill, Certificates card only.
   The pill is a plain .sb-pill with .is-green/.is-red, the same element and
   the same tokens as the "% healthy" pill beside it — reused rather than
   restyled, so the two can never drift apart.

   margin-left: auto pushes this to the far right of .sb-foot, absorbing all
   the free space between it and the alert chips. It does not shrink the row's
   minimum width: when the footer is too narrow the auto margin has nothing to
   absorb and the group wraps to a second line, where it stays right-aligned.

   MEASURED COST: the group is 117.6px ("Connected") or 132.9px
   ("Disconnected") including its flex gap, so the footer runs ~440-455px in
   total. That keeps it on one row only above roughly a 1565-1595px viewport
   — appreciably tighter than the dot this replaced (~373px, ~1431px). If the
   row needs to fit narrower screens again, this pill is the thing to revisit,
   not the chips or the card width. */
.sb-agents {
  display: inline-flex; align-items: center; gap: 5px;
  margin-left: auto;
}

/* ---------- data actions column ---------- */
/* .sb-body is a stretch-aligned grid with no fixed row height, so it sizes the
   whole row (and the status-bar card with it) to whichever column's natural
   content is tallest — .sb-metric's content (head + stat tiles + footer pills)
   lands around 185px. Keep this column's total content height under that, or
   the card grows to match it. A prior attempt fixed a growth regression here
   by hard-pinning .sb-actions to 240px; that just made the card taller on
   purpose instead of by accident, which was not the goal — reverted. If this
   column ever genuinely needs to be taller than .sb-metric, that is a
   .sb-metric sizing decision to make deliberately, not a side effect to
   absorb here. */
.sb-actions {
  padding: 20px; display: flex; flex-direction: column; gap: 10px;
  background: #ffffff; min-width: 0;
}
.sb-actions-label {
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.14em;
  color: var(--sb-label); text-transform: uppercase;
}
.sb-actions .btn,
.sb-actions .upload-dropzone-btn { width: 100%; }
.sb-actions .btn { height: 32px; }
/* Outline-pill restyle for the Data Actions buttons (Import/Export CSV, Add
   Manually, and WHOIS below) per the reference screenshot: white fill, navy
   text/hover-invert, and a pill border reusing .act-btn's existing accent
   border tone rather than a new color. Compounded with .btn-grey/.btn-wide so
   this beats their plain .btn.btn-grey/.btn.btn-wide rules on specificity,
   not just source order — .btn-grey alone is still used app-wide beyond the
   CSR page (modal Cancel buttons, etc.), so it stays deliberately untouched;
   the CSR download buttons opt in individually below instead of via the
   bare class.

   The Watch List's "WHOIS Now", the Agents page's "+ Create Agent", the CSR
   page's "Generate CSR"/"Generate Certificate"/"⬇ request.csr"/
   "⬇ private_key.pem"/"⬇ ZIP bundle"/"⬇ certificate.zip"/"Copy Host"/
   "Copy Value", My Team's "Invite Member", Billing's "Unsubscribe"/
   "Downgrade to Basic"/"Upgrade to Pro", the Alert Log's/Audit Log's
   "↻ Apply" and Discovery's "Run Discovery"/"Reset" share this look, so
   they are added to these lists rather than given duplicate rulesets — one
   place to change the pill. Clear All and Verify Domain (also on the CSR
   page) deliberately do NOT join this list — Clear All needs a different
   fill (its own rule, just below) and Verify Domain needs to keep its
   green .btn-success fill and only borrows the radius (its own rule, near
   .verify-tag).

   All are written view-scoped — `#watchlist-view #watch-refresh-now`,
   `#agents-view #agent-create-btn`, `#csr-view #csr-generate-btn` (and its
   Generate Certificate, four download-button, and two DNS-copy siblings),
   `#alert-log-view #alert-log-refresh`,
   `#audit-view #audit-refresh`, `#discovery-view #disc-scan-btn`/
   `#disc-reset-btn` — at (2,0,0) rather than as bare ids at (1,0,0).
   #watchlist-view, #agents-view, #alert-log-view, #audit-view and
   #discovery-view each carry their own `#<view> .btn { border-radius: 6px }`
   rule at (1,1,0) (the last three share one rule), which would
   otherwise win the radius and leave the button half-restyled: navy-on-white
   but still a rectangle. #csr-view has no such rule today, so its bare ids
   would in fact be enough — they are written scoped anyway so the whole list
   reads one way and so it stays correct if that rule is ever added. Any
   further page adopting this pill should be checked against its own
   view-scoped .btn rules first.

   #modal is the single element openModal() reuses for all eight of its
   callers — Add/Edit certificate, Add/Edit domain, Add/Edit category, Update
   CSR status and Create Agent. All eight take the pill, so the entries here
   are scoped to `#modal #<id>` and repainting every caller is the intent
   rather than the hazard it would once have been.

   These three lines previously read `#modal.is-agent #<id>` and
   `#modal.is-edit #<id>`, back when only Create Agent and the two Edit
   dialogs were pills. Those variant classes still exist: openModal() sets and
   clears .is-edit / .is-agent on every open, and the two Edit call sites and
   Create Agent still pass them. Nothing in this stylesheet reads them any
   more — deliberately kept, not overlooked. They are the mechanism for
   styling one caller differently, and the moment one dialog needs to diverge
   again it is two lines of CSS rather than a JS change.

   #modal-extra is in the list although only Create Agent ever shows it: it
   carries [hidden] for every other caller, so styling it unconditionally
   costs nothing and avoids a second variant-scoped rule.

   #onb-cancel is the WHOIS onboarding dialog's secondary action and
   #import-errors-close its dialog's only action, so both take the plain
   outline pill. That dialog's two PRIMARY actions do not — see the filled
   navy rule below — and Discovery's Close keeps its own colour, taking the
   geometry alone further down. That
   is why the .btn-grey note above still holds: Cancel opts in here by id for
   this modal only and .btn-grey itself stays untouched, so every other modal's
   Cancel keeps the slate fill.

   The Agent Enrollment Code modal that follows Create Agent is a separate
   overlay (#agent-enroll-modal), not another #modal caller, so the is-agent
   variant cannot reach it and its three buttons are listed by id instead.
   Two ids, (2,0,0), like the view-scoped entries — a bare id would already
   beat .btn.btn-grey, but the whole list stays written one way. The modal sits
   outside <main>, so no `#<view> .btn { border-radius: 6px }` rule applies to
   it and the pill radius lands unopposed.

   #agent-enroll-install-copy is the container-flow twin of #agent-enroll-copy
   ("Copy command" rather than "Copy code"); only one of the two is ever
   visible, but both need the look.

   #confirm-modal is the app's ONE generic confirmation dialog, opened by
   openConfirm() from eight call sites (Delete certificate / domain / CSR /
   category / private key, Remove member, Remove from Watch List, and the
   Make owner / Make member role change). It has no variant mechanism and none
   is added for this: it is a single shared component and it renders as one
   thing everywhere. Only its Cancel is here; its confirm button is geometry-
   only below, for the same inline-style reason as Delete Agent's.

   #agent-delete-modal is the same shape of problem — its own overlay outside
   <main>, so scoped by id here too — but only its Cancel is in this list. Its
   confirm button takes the pill's radius alone, further down, and keeps its
   red; adding it here would not even work, because its red comes from an
   inline style="background:var(--alert-red)" in index.html that outranks any
   stylesheet declaration. The background would stay red while colour and
   border went navy, leaving navy text on a red fill.

   Only buttons WITH an icon go in the display:inline-flex rule further down.
   "+ Create Agent", "+ Add manually", the three CSR download buttons and the
   two "↻ Apply" buttons carry a literal glyph in their text, not an svg, so
   they stay out of it. Generate CSR/Generate Certificate now carry svgs (the
   sidebar's CSRs and Certificates icons respectively) and are in that rule.
   Discovery's two DO carry svgs, so they are in it. */
.sb-actions .btn.btn-grey,
.sb-actions .btn.btn-wide,
#dom-onboard-btn,
#watchlist-view #watch-refresh-now,
#agents-view #agent-create-btn,
#csr-view #csr-generate-btn,
#csr-view #csr-issue-btn,
#csr-view #csr-dl-csr,
#csr-view #csr-dl-key,
#csr-view #csr-dl-all,
#csr-view #csr-dl-cert,
#csr-view #csr-dns-host-copy,
#csr-view #csr-dns-value-copy,
#alert-log-view #alert-log-refresh,
#audit-view #audit-refresh,
#discovery-view #disc-scan-btn,
#discovery-view #disc-reset-btn,
#account-view #account-invite-submit,
#account-view #billing-unsubscribe-btn,
#account-view #billing-downgrade-btn,
#account-view #billing-upgrade-pro-btn,
#modal #modal-save,
#modal #modal-cancel,
#modal #modal-extra,
#agent-enroll-modal #agent-enroll-copy,
#agent-enroll-modal #agent-enroll-install-copy,
#agent-enroll-modal #agent-enroll-done,
#agent-delete-modal #agent-delete-cancel,
#confirm-modal #confirm-cancel,
#dom-onboard-modal #onb-cancel,
#import-errors-modal #import-errors-close {
  background: #fff;
  color: var(--sb-ink);
  border: 1px solid #CBD4E3;
  border-radius: 999px;
}
.sb-actions .btn.btn-grey:hover,
.sb-actions .btn.btn-wide:hover,
#dom-onboard-btn:hover,
#watchlist-view #watch-refresh-now:hover,
#agents-view #agent-create-btn:hover,
#csr-view #csr-generate-btn:hover,
#csr-view #csr-issue-btn:hover,
#csr-view #csr-dl-csr:hover,
#csr-view #csr-dl-key:hover,
#csr-view #csr-dl-all:hover,
#csr-view #csr-dl-cert:hover,
#csr-view #csr-dns-host-copy:hover,
#csr-view #csr-dns-value-copy:hover,
#alert-log-view #alert-log-refresh:hover,
#audit-view #audit-refresh:hover,
#discovery-view #disc-scan-btn:hover,
#discovery-view #disc-reset-btn:hover,
#account-view #account-invite-submit:hover,
#account-view #billing-unsubscribe-btn:hover,
#account-view #billing-downgrade-btn:hover,
#account-view #billing-upgrade-pro-btn:hover,
#modal #modal-save:hover,
#modal #modal-cancel:hover,
#modal #modal-extra:hover,
#agent-enroll-modal #agent-enroll-copy:hover,
#agent-enroll-modal #agent-enroll-install-copy:hover,
#agent-enroll-modal #agent-enroll-done:hover,
#agent-delete-modal #agent-delete-cancel:hover,
#confirm-modal #confirm-cancel:hover,
#dom-onboard-modal #onb-cancel:hover,
#import-errors-modal #import-errors-close:hover {
  background: var(--sb-ink);
  border-color: var(--sb-ink);
  color: #fff;
}
/* Billing's three pills keep their resting look while disabled, hover or not.
   A disabled button still matches :hover, so without this the whole row
   inverts to navy under the cursor while being unclickable — which reads as
   "this works" at exactly the moment it does not. It matters here more than
   anywhere else because during a trial ALL THREE are disabled at once, so the
   entire row would light up together.

   Written per id at (2,2,0) rather than as `.billing-actions .btn:disabled`
   (1,4,0), which the shared (2,0,0) hover rule above would have outranked.

   Scoped to Billing deliberately. The same hover-while-disabled quirk applies
   to every other pill in that shared list (Generate Certificate, for one), but
   fixing it globally is a change to controls nobody reported and is not what
   this was scoped to. */
#account-view #billing-unsubscribe-btn:disabled:hover,
#account-view #billing-downgrade-btn:disabled:hover,
#account-view #billing-upgrade-pro-btn:disabled:hover {
  background: #fff;
  border-color: #CBD4E3;
  color: var(--sb-ink);
}
/* Delete Agent's confirm button: the pill's SHAPE only, so it lines up with the
   Cancel pill beside it, while its meaning stays red.
   
   Colour is deliberately absent. The red is an inline style in index.html, and
   the faded look before the user types DELETE is .btn:disabled's opacity: 0.4
   — both already work, and neither is this rule's business. Only the geometry
   is set: the radius, and a 1px border matching the fill so the box is exactly
   the height of the bordered pill next to it rather than 2px shorter and
   relying on .modal-actions' flex stretch to hide the difference.
   
   No :hover entry either: .btn:hover's fill loses to that inline background, so
   this button has never had a hover state and does not gain one here. */
#agent-delete-modal #agent-delete-ok {
  border-radius: 999px;
  border: 1px solid var(--alert-red);
}

/* The shared confirmation dialog's OK button, on the same terms: shape only.
   Its fill is an inline style="background:var(--alert-red)" in index.html that
   outranks any stylesheet declaration, so colour is not this rule's to set —
   and deliberately so. That inline red is applied to every openConfirm()
   caller including "Make owner"/"Make member", which are not destructive; that
   is a real problem, but a pre-existing one, tracked separately. Setting only
   the geometry here leaves it exactly as wrong as it was rather than
   entrenching it in the stylesheet.

   Unlike #agent-delete-ok this button is never disabled — openConfirm() has no
   type-to-confirm gate and no input — so there is no faded state to preserve.
   The 1px border matches the fill so the box is the same height as the Cancel
   pill beside it rather than 2px shorter. */
#confirm-modal #confirm-ok {
  border-radius: 999px;
  border: 1px solid var(--alert-red);
}

/* The WHOIS onboarding dialog's two primary actions, as a FILLED navy pill:
   the same geometry as the outline pill beside them, with the rest and hover
   fills swapped so the primary action reads as primary rather than as a third
   identical outline.
   
   This is the first resting navy fill in the app — until now --sb-ink was only
   ever a hover state — so the hover has to go somewhere, and lighter is the
   only direction available, the same reasoning #disc-modal-close:hover already
   applies to near-black. --sb-label is reused rather than a new hex invented:
   it is the existing lighter navy in the same family, already on :root.
   
   #onb-lookup and #onb-save are listed together because they share the
   actions row and the same role. #onb-lookup starts disabled and #onb-save
   starts display:none — the dialog swaps them as it progresses — so styling
   only the visible one would leave the row inconsistent the moment it
   advanced. .btn:disabled's opacity still fades #onb-lookup before a domain is
   queued.

   #onb-add is the third: "+ Add", the primary action of the queue-a-domain
   row above, previously the one .btn-outline in any dialog. It is NOT in the
   actions row, so it does not take the pill's 33px height — its flex parent
   stretches it to the input it sits beside (35px, and it should keep matching
   that input rather than the buttons below). Everything else in the signature
   — radius, padding, border width, font size — is shared. It also had no
   hover at all until now: .btn.btn-outline's white fill outranks .btn:hover
   on source order, so the button sat inert; this rule gives it the same hover
   as its two siblings. */
#dom-onboard-modal #onb-add,
#dom-onboard-modal #onb-lookup,
#dom-onboard-modal #onb-save {
  background: var(--sb-ink);
  color: #fff;
  border: 1px solid var(--sb-ink);
  border-radius: 999px;
}
#dom-onboard-modal #onb-add:hover,
#dom-onboard-modal #onb-lookup:hover,
#dom-onboard-modal #onb-save:hover {
  background: var(--sb-label);
  border-color: var(--sb-label);
}

/* No height override here: WHOIS takes its height from .sb-actions-primary
   below, same as the Certificates dropzone. Removing an earlier
   #dom-onboard-btn { height: 32px } from this spot was a no-op — it only
   handed the cascade to `.sb-actions .btn { height: 32px }` at the identical
   value. The real conflict is a specificity one and is fixed at
   .sb-actions-primary itself; see the note there. */
/* The dropzone's default padding/margin is sized for a full-width card; tighten
   it to sit in a 330px column alongside the buttons (the column uses gap). The
   vertical padding is small because .sb-actions-primary below fixes the height —
   don't try to tune the card height from here. */
.sb-actions .upload-dropzone-btn { padding: 2px 14px; margin-bottom: 0; }
/* The Certificates and Domains status bars differ in exactly one place: this
   slot is a two-line dashed dropzone on one page and a one-line WHOIS button on
   the other. Everything else in .sb-actions is character-identical between them,
   and renderStatusBar() renders the same two metric panels on both pages — so
   pinning this one slot to a shared height makes the whole card row identical by
   construction. That is the point of doing it here: there is no total-height
   constant to keep in sync, and it stays correct if the label, the button grid
   or the footer ever change.
   44px = the dropzone's floor (4px border + ~35px of two text lines) plus a
   couple of px of breathing room. The Domains page pays for any increase 1:1,
   since its natural content is ~8px shorter than the Certificates page's.

   TWO selectors, and the second is load-bearing. The Certificates dropzone is
   a plain div, so the bare class is enough for it. The Domains WHOIS button
   is `class="btn sb-actions-primary"`, so `.sb-actions .btn { height: 32px }`
   above — (0,2,0) against this rule's (0,1,0) — silently won the cascade and
   held WHOIS at 32px while the dropzone sat at 44px. Browser measurement
   confirmed it: primary slot 44px on Certificates vs 32px on Domains, taking
   the whole card 411px vs 399px with it. `.sb-actions .btn.sb-actions-primary`
   is (0,3,0) and outranks the 32px rule. Do not collapse these back into one
   bare-class selector, and if the button grid's height rule above is ever
   re-scoped, re-check that this still outranks it. */
.sb-actions-primary,
.sb-actions .btn.sb-actions-primary { height: 44px; }
/* Icon + label layout, shared by the two globe-icon WHOIS buttons and
   Discovery's play-icon "Run Discovery" / circular-arrow "Reset". The Watch
   List one needs no extra specificity here (nothing else sets its display),
   but it is kept id-scoped to read consistently with the pill rules above.
   Without this the svg sits on the text baseline rather than centered on it,
   which is what left the Discovery pair looking bottom-heavy next to the
   search box. */
#dom-onboard-btn,
#discovery-view #disc-scan-btn,
#discovery-view #disc-reset-btn,
#watchlist-view #watch-refresh-now,
#csr-view #csr-generate-btn,
#csr-view #csr-issue-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
/* `margin-top: auto` inherits the job the removed .sb-actions-foot used to do:
   it pins the grid to the bottom of the column so any slack (this column is a
   stretched grid item, so it can be taller than its content) collects above the
   grid rather than under it. That keeps "+ Add manually" a flat 20px — the
   column's bottom padding — off the card's bottom edge whichever column happens
   to be the tallest in the row. */
.sb-actions-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: auto; }
.sb-actions-grid .btn-wide { grid-column: span 2; }

/* ---------- responsive ---------- */
@media (max-width: 1180px) {
  .sb-body { grid-template-columns: 1fr 1fr; }
  .sb-actions {
    grid-column: span 2;
    border-top: 1px solid var(--sb-hairline);
    flex-direction: row; align-items: center; flex-wrap: wrap; gap: 8px;
  }
  .sb-actions-label { width: 100%; }
  /* .sb-actions is a row here, so the auto top margin would fight
     `align-items: center` and drop the grid to the bottom of the line. */
  .sb-actions-grid { flex: 1 1 320px; margin-top: 0; }
}
@media (max-width: 760px) {
  .sb-body { grid-template-columns: 1fr; }
  .sb-metric { border-right: none; border-bottom: 1px solid var(--sb-hairline); }
  .sb-actions { grid-column: span 1; }
  .sb-total { font-size: 36px; }
  .sb-stats, .sb-stats.has-unknown { grid-template-columns: repeat(2, 1fr); }
}

/* ==========================================================================
   Snapshot (landing page)
   --------------------------------------------------------------------------
   Reuses the status bar's component outright, not just its --sb-* palette: the
   two resource sections are .sb-head + .sb-stats, rendered by the same
   sbHead()/sbStats() the Certificates and Domains panels call. Those rules are
   top-level .sb-* selectors, not descendants of .status-bar or .sb-metric, so
   they apply here with nothing repeated — this file only styles what differs
   (the resource badge, the section rhythm, and the page's two-column shell).
   Each section is framed as a card (see .snap-res), but the frame is the
   Inspector rail's — white, hairline, 10px — not .sb-metric's tinted one: these
   two cards and the rail sit 5px apart in one row, so they have to read as the
   same kind of surface. What is still NOT reused from .sb-metric is its tint,
   its min-height and .sb-foot — the landing page has no use for a "Manage →"
   link or a second copy of the alert chips.
   ========================================================================== */
/* Content column | Inspector rail. align-items: flex-start keeps the rail at
   its own content height rather than stretching it to the content's — a
   stretched rail has no room to move inside its containing block and
   `position: sticky` on it would silently do nothing. */
/* The cap is what the ring geometry used to do. The old layout was content-
   sized end to end: fixed one-ring columns gave the group a right edge of its
   own, and the box gap to the rail was therefore the gap you actually saw. A
   full-width health bar has no intrinsic width to stop at, so without a cap the
   content column would grow to whatever the monitor is and the bars would
   stretch to ~1600px on a wide screen. 1075px is the mockup's content width;
   the leftover collects to the right of the rail, which is the one place it is
   not sitting between two things.
   The gap is 5px, and it is now a real edge-to-edge distance rather than the
   approximation a box gap used to be: the resource sections are framed cards
   (below), so the content column's right edge is a border you can see, 5px from
   the rail's own border. That 5px is the app's existing stacked-card spacing —
   the same distance .status-bar keeps from .inventory-panel on Certificates.
   The cap is unchanged, so shrinking the gap widens the cards rather than
   moving the rail: the rail stays exactly where it is and the cards grow out to
   meet it. */
.snap-page { display: flex; align-items: flex-start; gap: 5px; max-width: 1075px; }
/* flex: 1 1 auto, not 0 1 auto: the column now fills the space the cap allows
   instead of shrinking to its content, because the bars are what want the
   width. min-width: 0 so it can still shrink under the rail rather than forcing
   the row wider than the viewport. */
.snap-layout { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 26px; }

/* Wrapper kept so .snap-resources sizes as a flex child rather than a block. */
.snap-row-top { display: flex; align-items: flex-start; gap: 16px; }
/* The bottom half is one row, border-top separating it from the resource
   stack above. Its only child is .snap-bottom-cols — kept as a distinct
   wrapper (rather than folding straight into .snap-row-bottom) purely so the
   two share this file's existing selector, not because there's a second
   thing to stack in .snap-row-bottom itself. */
.snap-row-bottom {
  display: flex; flex-direction: column;
  border-top: 1px solid var(--sb-hairline); padding-top: 26px;
}
/* Two columns, each its own top/bottom pair pushed apart by
   justify-content: space-between (set on .snap-alerts-col and
   .snap-agents-col below) — align-items: stretch is what gives both columns
   the same height to distribute that space across, which is what makes the
   two tops ("1 item needs attention" and "Agents 7") land level with each
   other, and the two bottoms (the alert-channel cards and the platform icon
   row) land level with each other, regardless of either column's internal
   content height. */
.snap-bottom-cols { display: flex; align-items: stretch; gap: 56px; flex-wrap: wrap; }
/* Width, not a flex basis: a plain width so `justify-content: space-between`
   on the column has something fixed to distribute space within. */
.snap-callouts { width: 200px; }
.snap-resources { flex: 1 1 auto; min-width: 0; }
/* The left column: attention callout on top, "Alert channels" + its cards on
   the bottom — space-between is what pins the callout to the column's top
   edge and the cards to its bottom edge, matching the right column's own
   top/bottom pair below. gap: 20px is a floor under space-between, not a
   replacement for it: this column is the taller of the two (its content sets
   .snap-bottom-cols' stretched height), so before this gap the callout and
   the cards sat flush with zero space between them. The 20px floor grows
   this column by 20px, which — because the shorter .snap-agents-col is
   stretched to match — grows the row's shared height by the same 20px and
   pushes .snap-agents-foot (bottom-pinned there the same way) down with the
   cards, keeping the two bottoms level without touching that column's own
   rule. */
.snap-alerts-col { display: flex; flex-direction: column; justify-content: space-between; gap: 20px; }
.snap-channels-block { min-width: 0; }

/* ---- resource badge + stat pair ---- */
/* Now used only by the agent block — the two resource sections take their badge
   from .snap-res-head and their total from .sb-total. No justify-content: the
   row starts at its container's left edge, which is what lines the badge up
   with "Agent status" beneath it. No margin-top: .snap-agents-col's own
   space-between pins this to the column's top edge, level with the attention
   callout beside it — nothing here needs to compensate for that anymore. */
.snap-stat { display: flex; align-items: center; gap: 12px; }
.snap-badge {
  width: 38px; height: 38px; flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid transparent;
}
.snap-badge svg { width: 19px; height: 19px; display: block; }
/* The badge tint names the resource, not a health state — a certificate badge
   is green because certificates are green throughout the app, not because the
   certificates are healthy. The bars carry health.
   Keyed off the wrapper rather than the badge itself. The two resource sections
   carry .snap-res, the agent block carries .snap-stat — snapStatRow() is only
   ever called for agents now, so those are the only two forms that exist. */
.snap-res.is-certificates .snap-badge { background: var(--sb-green-bg); border-color: var(--sb-green-border); color: var(--sb-green-dark); }
.snap-res.is-domains .snap-badge { background: var(--sb-blue-bg); border-color: var(--sb-blue-border); color: var(--sb-blue-ink); }
.snap-stat.is-agents .snap-badge { background: var(--sb-amber-bg); border-color: var(--sb-amber-border); color: var(--sb-amber-ink); }
.snap-stat-text { display: flex; flex-direction: column; line-height: 1.15; min-width: 0; }
.snap-stat-label { font-size: 11px; font-weight: 500; color: var(--sb-muted); letter-spacing: 0.02em; }
.snap-stat-value { font-size: 26px; font-weight: 500; color: var(--sb-ink); font-variant-numeric: tabular-nums; }

/* ---- attention callout (bottom row, left) ---- */
.snap-callouts { display: flex; flex-direction: column; gap: 10px; }
.snap-callout {
  display: flex; align-items: flex-start; gap: 9px;
  padding: 11px 12px;
  border: 1px solid transparent; border-radius: 10px;
  font-size: 12px; line-height: 1.4;
}
.snap-callout svg { width: 16px; height: 16px; flex-shrink: 0; margin-top: 1px; }
.snap-callout strong { font-weight: 600; }
.snap-callout.is-alert { background: var(--sb-red-bg); border-color: var(--sb-red-border); color: var(--sb-red-ink); }
.snap-callout.is-ok { background: var(--sb-green-bg); border-color: var(--sb-green-border); color: var(--sb-green-dark); }

/* ---- resource sections ---- */
/* 5px, where this used to be 60px. The 60 was doing the containing: with no
   border or tint around a section, whitespace was the only thing separating one
   resource from the next, and at any tighter spacing the two read as one
   six-tile block. The card frame below does that job now, so the whitespace no
   longer has to — and 60px of it between two bordered cards reads as two
   unrelated panels rather than one inventory. 5px is what stacked cards use
   elsewhere in the app (.status-bar to .inventory-panel on Certificates). */
.snap-resources { display: flex; flex-direction: column; gap: 5px; }
/* The head's own internals — .sb-head, .sb-total, .sb-titles, .sb-bar, .sb-stats
   and the tiles — are the status bar's rules, applying here unchanged because
   they are top-level selectors rather than descendants of .status-bar. This
   block only sets what is different on this page: the badge, and the spacing
   between the two blocks. */
/* The card frame. White + hairline + 10px radius is .snap-inspector's chrome,
   copied deliberately: this card's right border and the rail's left border sit
   5px apart in the same row, and two different treatments at that distance read
   as a mistake. No box-shadow for the same reason — the rail has none, and
   .status-bar's heavy one would make these cards float off a panel that does
   not. Padding is .sb-metric's 20px 24px, because the contents ARE .sb-metric's
   contents (sbHead + sbStats). */
.snap-res {
  display: flex; flex-direction: column; gap: 22px;
  background: #fff;
  border: 1px solid var(--sb-hairline);
  border-radius: 10px;
  padding: 20px 24px;
}
/* The badge sits outside .sb-head rather than inside it: .sb-head aligns its
   total to the titles' LAST baseline, and a 38px badge in that flex row would
   be dragged onto that baseline instead of centred on the block. */
.snap-res-head { display: flex; align-items: center; gap: 18px; }
/* The head has to take the row's remaining width, or .sb-bar (flex: 1 1 auto
   within it) has nothing to grow into and collapses to its 80px min-width. */
.snap-res-head .sb-head { flex: 1 1 auto; min-width: 0; }

/* ---- agent block (bottom row, right) ---- */
/* The right column: "Agents" + its count on top, "Agent status" + the
   platform icon row on the bottom — the mirror of .snap-alerts-col beside it.
   space-between pins the count to the column's top edge (level with the
   attention callout) and the status/icon group to its bottom edge (level with
   the alert-channel cards) — see the comment on .snap-bottom-cols. flex: 1 1
   auto so this column can grow to take the row's remaining width. */
.snap-agents-col { display: flex; flex-direction: column; align-items: flex-start; text-align: left; justify-content: space-between; flex: 1 1 auto; }
/* Status line, then the single icon row beneath it — stacked, not
   side-by-side, so the row reads as "Agent status: Connected" with the
   platform split underneath it rather than beside it. gap: 30px (was 10px,
   then 20px): the status line and the icon row read as one label above its
   own value, so the extra space is just breathing room between them, not a
   split into two groups — .snap-agents-foot still sits as one unit,
   bottom-pinned in .snap-agents-col the same way it always was. */
.snap-agents-foot { display: flex; flex-direction: column; gap: 30px; }
/* Title and its pill, baseline-aligned so their text reads on one line
   despite the pill's own padding sitting below its text while the title has
   none. */
.snap-status-line { display: flex; align-items: baseline; gap: 8px; }
.snap-card-title { font-size: 12px; font-weight: 600; color: var(--sb-label); margin: 0; }

/* ---- agent platform legend ---- */
/* One row, all four platforms — Windows, Linux, Containers, Kubernetes, left
   to right, matching the design reference. */
.snap-legend { list-style: none; margin: 0; padding: 0; display: flex; }
.snap-legend-count { font-weight: 600; color: var(--sb-ink); font-variant-numeric: tabular-nums; }
/* Row direction is the flex default now that the legend no longer stacks under
   a ring, so only the spacing between the two platforms is set here. */
.snap-legend-os { gap: 16px; }
/* 24px icons, 16.5px count text: both 1.5x their original 16px/11px, scaled
   together so the icon and the number beside it grow at the same rate. */
.snap-os { display: flex; align-items: center; gap: 5px; font-size: 16.5px; }
.snap-os-icon { display: inline-flex; }
.snap-os-icon svg { display: block; width: 24px; height: 24px; }

/* ---- alert channel cards ---- */
/* Deliberately not .sb-chan: that is a 22px icon-only chip sized for a status
   bar. These are labelled cards that have to carry a name and a state word. */
.snap-section-label { font-size: 12px; font-weight: 600; color: var(--sb-label); margin: 0 0 10px; }
.snap-channels { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; max-width: 460px; }
.snap-chan {
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  padding: 12px 10px;
  background: #fff;
  border: 1px solid var(--sb-hairline); border-radius: 10px;
}
/* The wrapper is inline-flex so the svg inside it sits on no text baseline —
   an inline span would add a descender gap under every channel icon. */
.snap-chan-icon { display: inline-flex; }
.snap-chan .sb-chan-icon { width: 18px; height: 18px; display: block; }
.snap-chan-name { font-size: 12px; font-weight: 600; color: var(--sb-ink); }
/* The state is a pill rather than plain colored text, and it is a plain
   .sb-pill with .is-green/.is-red/.is-zero — the same element and the same
   tokens as the "% healthy" and Connected/Disconnected pills, reused rather
   than restyled so they cannot drift apart. It carries its own colors, so
   nothing below needs to reach into it.
   The icon still follows the state color; the card stays white so three cards
   in a row read as one set rather than three competing blocks. */
.snap-chan.is-active { color: var(--sb-green-dark); }
/* Configured, but the master toggle is off. Muted rather than red — and now
   muted the same way .sb-chan.is-paused is in the status bar, instead of the
   amber it used to carry here: the channel is set up and one switch from
   working, which is not the same failure as never having been configured. */
.snap-chan.is-paused { color: var(--sb-faint); }
.snap-chan.is-off { color: var(--sb-red-ink); }

/* ==========================================================================
   Inspector rail (Snapshot, right-hand side)
   --------------------------------------------------------------------------
   STATIC MOCKUP. Every value in this panel is hardcoded in index.html for
   data the backend does not model yet — there is no profiles or billing
   endpoint, and nothing here is rendered by app.js.

   Enterprise-console treatment (GitHub / Azure Portal): one flat surface with
   bold section headers and hairline rules between sections. Deliberately no
   nested cards — a card inside a card is the thing that makes these panels
   read as cluttered, so sections are separated by a 1px rule and spacing only.

   Sticky, not fixed: .main-container is the scrolling element (body is
   `overflow: hidden`, the container is `overflow-y: scroll`), so a sticky rail
   pins against that scrollport and needs no magic numbers for the sidebar.
   `top` clears #snapshot-view's own 10px padding-top; the max-height is the
   viewport less that offset and a matching gap at the bottom, which is what
   gives the rail its independent scrollbar when the invoice list grows. */
/* 340px, up from 300 — for room, not for fit. Measured, because the arithmetic
   is not obvious: the invoice row's four fields (id, amount, status pill,
   download) plus their gaps stop the id ellipsing from 280px up, so the old 300
   did already hold them on one line. What it did not leave is any air — the id
   ended ~28px short of the amount, and the money line ran nearly edge to edge.
   340 puts that at ~68px, which is the "comfortably, without crowding" this was
   widened for. 280 is the floor if the panel ever has to give width back.
   The page cap (.snap-page's 1075px) is deliberately NOT raised to match, so
   the 40px comes out of the resource cards beside it (770 -> 730 at full width)
   rather than widening the page. The rail's right edge therefore stays where it
   is and the panel grows leftwards, which is the direction that reads as "this
   panel got wider" rather than "the page moved". */
.snap-inspector {
  flex: 0 0 340px;
  position: sticky;
  top: 10px;
  max-height: calc(100vh - 20px);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: #fff;
  border: 1px solid var(--sb-hairline);
  border-radius: 10px;
  padding: 4px 16px 16px;
}
/* The rule between sections belongs to the section that follows it, so the
   first one does not open with a stray line under the panel's own top edge. */
.insp-section + .insp-section { border-top: 1px solid var(--sb-hairline); margin-top: 16px; padding-top: 16px; }
/* Bold and small-caps-ish: the section header carries the hierarchy here, since
   there are no card edges to do it. */
.insp-title {
  font-size: 12px; font-weight: 700; color: var(--sb-ink);
  letter-spacing: 0.04em; text-transform: uppercase;
  margin: 12px 0 10px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--sb-hairline);
}
/* Total count next to the title, e.g. "PROFILES (5)" — normal case/weight
   so it doesn't inherit the title's small-caps treatment. */
.insp-title-count {
  text-transform: none; letter-spacing: normal;
  font-weight: 500; color: var(--sb-muted);
  margin-left: 6px;
  font-variant-numeric: tabular-nums;
}
.insp-list { list-style: none; margin: 0; padding: 0; }

/* ---- Profiles: name left, count right ---- */
.insp-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 7px 0;
  font-size: 12.5px;
}
.insp-row-name { color: var(--sb-muted); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.insp-row-value { color: var(--sb-ink); font-weight: 600; font-variant-numeric: tabular-nums; flex-shrink: 0; }

/* ---- Billing ---- */
/* Current plan, right-aligned on the Billing title row ("BILLING … PLAN: PRO").
   Floated rather than making .insp-title a flex row: that would also push the
   Profiles title's "(5)" count to the far right, which belongs beside its word.

   Position only, deliberately. Every text property — family, size, weight,
   letter-spacing, uppercase and colour — is inherited from .insp-title, so
   the plan reads as part of the header rather than as a secondary note beside
   it, and cannot drift from the title it sits on: restyling .insp-title
   restyles this with it. This previously re-declared weight and
   letter-spacing (redundantly, both inherit) and overrode the colour to
   --sb-muted, which is what made it look like a subtitle.

   Contrast .insp-title-count above, which opts OUT of the title treatment on
   purpose — "PROFILES (5)" wants the count to read as an aside. The plan does
   not: it is the other half of the row. */
.insp-title-plan { float: right; }
.insp-summary { display: flex; flex-direction: column; gap: 2px; margin: 0 0 12px; }
.insp-summary-plan { font-size: 13px; font-weight: 600; color: var(--sb-ink); }
.insp-summary-note { font-size: 11.5px; color: var(--sb-muted); }

/* Billing history's empty state. Deliberately not .account-empty: that is
   written for a full-width card on the Account page and its type sizes read
   oversized in a ~300px rail. Same words, rail proportions. */
.insp-empty { padding: 10px 0 2px; }
.insp-empty-title { font-size: 12.5px; font-weight: 600; color: var(--sb-ink); margin-bottom: 3px; }
.insp-empty-sub { font-size: 11.5px; color: var(--sb-muted); line-height: 1.45; }
/* Two rows per invoice: id + amount + status + download on the first, the date
   alone on the second. The money line carries everything you act on — what it
   costs, whether it is settled, and how to get the PDF — so those three read
   left to right as one statement; the date is the only field that qualifies
   rather than states, and it drops beneath.
   The amount and the pill used to be stacked, on the reasoning that four fields
   could not share a line at 300px. Measured rather than assumed, that turns out
   to be false — they fit from 280px up — so the stack was buying nothing the
   panel could not afford. The rail was widened anyway (see .snap-inspector),
   for air around the line rather than to make it possible.
   Explicit areas rather than auto-placement: the markup order is the reading
   order (id, date, amount, status, download), which auto-flow would drop into
   the grid as id|date / amount|status — the wrong pairing. Naming the cells lets
   the DOM keep the order a screen reader wants while the eye gets the other one.
   The download sits IN the money line rather than spanning both rows. Spanning
   was tried and is wrong: a glyph centred on a two-line block lands between the
   lines, half a line below the amount it belongs to, and reads as floating
   between the invoice and the one under it. The second row's trailing cell is

/* Where the rail drops below the content instead of beside it. This is no
   longer the arithmetic floor it was under the ring layout — a health bar has
   no intrinsic width, so the side-by-side layout no longer has a width it
   cannot go under (at 1200px the content column still resolves to ~620px, which
   is comfortable — it gained the 75px the page gap gave up). It is kept at 1200
   because that is the tested value for this page, and because what used to look
   cramped here was a bar running loose towards the rail; the bars now stop at a
   card border a uniform 5px short of it, which is the gutter stacked cards use
   everywhere else in the app. */
@media (max-width: 1200px) {
  .snap-page { flex-direction: column; gap: 22px; max-width: none; }
  /* align-self, because .snap-page keeps align-items: flex-start for the rail's
     sake even in column mode, and a flex-start child is sized fit-content. */
  .snap-layout { align-self: stretch; }
  .snap-inspector {
    flex: 1 1 auto; align-self: stretch;
    position: static; max-height: none; overflow-y: visible;
  }
}

@media (max-width: 860px) {
  .snap-layout { gap: 22px; }
  .snap-row-top { gap: 18px; }
  .snap-row-bottom { gap: 18px; padding-top: 22px; }
  /* .snap-resources keeps its 5px at every width now. The step-downs that used
     to live here (40px, and 32px at 520) were scaling the whitespace that stood
     in for a card edge; there is a card edge, so there is nothing to scale. */
  .snap-bottom-cols { gap: 32px; }
  /* Back to shrink-to-fit: the 200px is only there to line the callout up with
     the section above it, and that section is full width at this size. */
  .snap-callouts { width: auto; }
}
@media (max-width: 520px) {
  /* The tile row drops to two columns and .sb-total steps to 36px at the status
     bar's own 760px breakpoint, which every viewport this narrow also matches —
     shared rules, so nothing to repeat here. Only the badge pair is ours. */
  /* The card frame keeps its border and radius at this width; only the inset
     comes down, so a 24px gutter each side is not eating a phone's width. */
  .snap-res { padding: 16px; }
  .snap-res-head { gap: 12px; }
  .snap-badge { width: 34px; height: 34px; }
  .snap-stat { gap: 8px; }
  .snap-stat-value { font-size: 22px; }
}

/* ============================================================
   Account page (My Space / My Team / Billing)
   ============================================================ */
/* Tab strip — the same design.png gray-track/white-raised-chip segmented pill
   as .cat-type-toggle and .watch-interval-slider above, and it follows
   .cat-type-toggle's variant specifically: equal-width flex:1 word labels,
   not the compact fixed-width chips the 6h/12h/24h slider uses. Tokens
   (#f1f5f9 track, radius 14/10, 2px padding, white chip + soft shadow) are
   restated rather than shared, matching how the other two instances are
   written — see the note on .row-actions about this being a copy-per-instance
   convention rather than a component. max-width keeps the three tabs from
   stretching the full page width on a wide viewport. */
.account-tabs {
  display: flex; gap: 2px; background: #f1f5f9; border-radius: 14px; padding: 2px;
  margin-bottom: 20px; max-width: 420px;
}
.account-tab {
  flex: 1; border: none; border-radius: 10px; background: transparent; color: #64748b;
  font-family: inherit; font-size: 13px; font-weight: 500; padding: 8px 10px;
  white-space: nowrap; cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}
.account-tab:not(.is-inactive), .account-tab:not(.is-inactive):hover {
  background: #fff; color: #0f172a; font-weight: 700; box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}
.account-tab.is-inactive { font-weight: 500; color: #64748b; }
.account-tab.is-inactive:hover { color: #334155; }

/* Pane card. Wider than .report-control-card's fixed 368px because My Team
   grows a member table here in Phase 3; otherwise the same white/border/
   radius/padding treatment so it reads as the same family of card. */
.account-card {
  background: #fff; border: 1px solid var(--border-color); border-radius: var(--card-radius);
  padding: 24px; max-width: 720px;
}
.account-card h3 {
  font-size: 16px; font-weight: 600; color: #0f172a; margin-bottom: 18px;
  display: flex; align-items: center; gap: 8px;
}

/* Label/value rows. A <dl> because these are name/value pairs; the wrapper
   div per pair is what lets the grid put dt and dd on one line. */
.account-fields { display: flex; flex-direction: column; gap: 14px; }
.account-field {
  display: grid; grid-template-columns: 140px 1fr; align-items: baseline; gap: 12px;
}
.account-field dt { font-size: 12px; font-weight: 600; color: #475569; }
.account-field dd { font-size: 13px; color: var(--text-main); word-break: break-word; }
/* Roles are stored lowercase ("owner"/"member"); capitalize for display rather
   than transforming the value in JS, so the DOM keeps what the API returned. */
.account-role { text-transform: capitalize; }

/* Empty states — same two-line title/sub treatment and values as
   .agent-empty-state on the Manage page. */
.account-empty { padding: 4px 0 2px; }
.account-empty-title { font-size: 13px; font-weight: 600; color: #0f172a; }
.account-empty-sub { font-size: 12px; color: #94a3b8; margin-top: 4px; max-width: 62ch; line-height: 1.5; }

@media (max-width: 520px) {
  /* Stack label over value rather than squeezing a 140px label column. */
  .account-field { grid-template-columns: 1fr; gap: 2px; }
  .account-card { padding: 16px; }
}

/* My Space (Phase 2) — cards stack vertically within the pane. */
.account-pane > .account-card + .account-card { margin-top: 20px; }
.account-card-lede { font-size: 12px; color: var(--text-muted); margin: -8px 0 16px; max-width: 62ch; line-height: 1.5; }
/* Name field: input and its Save button share one baseline row. The input is
   capped rather than full-width so the button sits beside it instead of being
   pushed to the far edge of a 720px card. */
.account-edit-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.account-edit-row .report-input-field { flex: 1 1 220px; max-width: 320px; min-width: 0; }
.account-edit-row .btn { flex-shrink: 0; }
/* Sub-label under a value, for the read-only fields' "why you can't edit this"
   line and the name field's hint. */
.account-field-note { font-size: 11px; color: #94a3b8; margin-top: 5px; line-height: 1.45; max-width: 52ch; }
/* Customer ID row: tenant id (left) and "Joined <date>" (right), same line.
   This row IS the secondary/muted text — same size and color as
   .account-field-note — rather than a value with a note underneath, so no
   top margin here. The id is .mono like every other raw identifier in the
   app (CSR output, etc.); its font-size is pulled down to match the joined
   date beside it instead of .mono's usual 12px. */
.account-customerid-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; font-size: 11px; color: #94a3b8; line-height: 1.45; }
.account-customerid-row .mono { font-size: 11px; }
/* Confirmation line under the password button. Green like the other inline
   save confirmations in Reports/Integration, not a toast — it needs to stay on
   screen long enough to read a sentence. */
.account-inline-msg { font-size: 12px; color: var(--success-green); font-weight: 500; margin-top: 10px; line-height: 1.5; }
.account-notif-foot { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 18px; }
.account-notif-foot .account-field-note { margin-top: 0; }

/* My Team (Phase 3) — wider than the My Space cards because it holds a table. */
.account-card--wide { max-width: 860px; }
/* Invite form sits between the header and the table, only for owners. */
.account-invite { border: 1px solid var(--border-color); border-radius: 10px; padding: 16px; margin-bottom: 18px; background: #f8fafc; }
.account-invite-row { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
.account-invite-field { display: flex; flex-direction: column; gap: 5px; flex: 1 1 220px; min-width: 0; }
.account-invite-field--role { flex: 0 0 140px; }
.account-invite-field > span { font-size: 12px; font-weight: 600; color: #475569; }
.account-invite .btn { flex-shrink: 0; }
.account-invite .account-field-note { margin-top: 10px; max-width: 62ch; }
.account-inline-msg--error { color: var(--alert-red); }
/* Table sits flush inside the card rather than carrying its own outer frame. */
.account-team-table { margin-top: 4px; }
.account-team-who { display: inline-flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.account-team-email { font-size: 12px; color: var(--text-muted); font-weight: 400; margin-top: 2px; }
/* "You" marker on the caller's own row — the row that deliberately has no
   action buttons, since the API refuses self-role-change and self-removal. */
.account-team-you {
  font-size: 10px; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase;
  color: var(--primary-blue); background: #eef2f7; border-radius: 10px; padding: 2px 7px;
}
/* Role dropdown — sized well below .report-input-field's 9px padding, which
   would stretch every row of a table whose other cells are plain text.
   text-transform is reset because the .account-role cell capitalizes its text
   value, and the option labels are already written capitalized. */
.account-role-select {
  padding: 3px 6px; border: 1px solid #cbd5e1; border-radius: 4px;
  font-size: 12px; font-family: inherit; text-transform: none;
  color: var(--text-main); background: #fff; outline: none; cursor: pointer;
}
.account-role-select:focus { border-color: var(--primary-blue); }
/* Holds Resend + the trash icon. Was 260px while "Make owner" also lived here;
   role-switching moved to the Role column, so the column no longer needs it. */
.account-team-actions-col { width: 150px; }

@media (max-width: 620px) {
  .account-invite-field, .account-invite-field--role { flex: 1 1 100%; }
}

/* Owner-only fields locked for members (see applyRoleGating in app.js). Matches
   the greyed treatment refreshSnapshotBadge already applies to its locked
   recipient inputs, so a member sees one consistent "not yours to edit" look
   rather than two near-miss styles. Buttons need nothing here — .btn:disabled
   already carries opacity 0.4 + not-allowed. */
.is-owner-locked,
select.is-owner-locked,
input.is-owner-locked {
  background-color: #f1f5f9;
  color: #94a3b8;
  border-color: #e2e8f0;
  cursor: not-allowed;
}
/* Checkboxes get the cursor and a knocked-back look, but no fill override —
   restyling the box itself would fight the native control. */
input[type="checkbox"].is-owner-locked { background-color: transparent; opacity: 0.5; }
/* "Change Settings" disclosure links, locked for members. An anchor has no
   disabled property, so the fade and the click block both live here.
   pointer-events: none also stops the inline onmouseover underline these links
   carry, and — unlike a disabled button — leaves the parent receiving hover, so
   its explanatory title still shows. */
.is-owner-locked-link {
  opacity: 0.4;
  pointer-events: none;
  cursor: not-allowed;
  text-decoration: none;
}

/* Inspector hidden for members (applyRoleGating in app.js).
   visibility, not display: the rail is flex: 0 0 340px in a row alongside
   .snap-layout (flex: 1 1 auto). display:none would drop it from the row and
   let the resource cards stretch across the freed width, so a member's
   Snapshot would lay out differently from an owner's. visibility keeps the
   340px column and the row gap reserved — same card widths either way, just
   empty space where the rail sits — while still removing it from the
   accessibility tree and the tab order. */
.snap-inspector.is-role-hidden { visibility: hidden; }

/* Below 1200px .snap-page turns into a column and the rail becomes a
   full-width block stacked UNDER the content (see the earlier breakpoint), so
   there is no column width left to preserve. Reserving its box there would
   just leave a tall empty gap below the page, so remove it outright. This rule
   is deliberately last: it and the one above are both (0,2,0), so source order
   is what lets it win inside the query. */
@media (max-width: 1200px) {
  .snap-inspector.is-role-hidden { display: none; }
}

/* ---- First-run tenant setup ("Create your organization") ----
   Shown by auth.js when the signed-in user has a valid Auth0 session but no
   users row yet — the self-service signup case (POST /api/signup/provision).
   Before this existed, that state revealed the dashboard shell and then failed
   every data call with a 403, which read as a broken app rather than an
   expected next step.

   Fixed overlay rather than a view inside the app shell: it must render while
   <html class="auth-pending"> still hides the body (the shell has no tenant
   data and must never be shown), so it carries its own visibility:visible —
   visibility is inherited but can be overridden per-element, which is what
   lets one child of a hidden body paint. */
#signup-setup {
  visibility: visible;
  position: fixed; inset: 0; z-index: 3000;
  background: var(--body-bg);
  display: flex; align-items: center; justify-content: center;
  padding: 24px; overflow-y: auto;
}

/* Same "paint through a hidden body" constraint as #signup-setup above —
   auth.js's _showBlockedAccountCard appends this before _revealApp() has
   run (both when an already-authenticated session turns out blocked, and
   on a hard reload back from Auth0's own error=unauthorized denial), so
   <html class="auth-pending"> still hides body. Missing this rule left the
   card invisible — a blank page rather than an error — confirmed live
   2026-09-19; #signup-setup already carried the fix this needed. */
#blocked-account {
  visibility: visible;
  position: fixed; inset: 0; z-index: 3000;
  background: var(--body-bg);
  display: flex; align-items: center; justify-content: center;
  padding: 24px; overflow-y: auto;
}
.signup-card {
  width: 100%; max-width: 440px; background: #fff;
  border: 1px solid var(--border-color); border-radius: var(--card-radius);
  padding: 32px; box-shadow: 0 4px 20px rgba(15, 23, 42, 0.06);
}
.signup-card .signup-logo { height: 48px; width: auto; display: block; margin: 0 auto 24px; }
.signup-card h1 { font-size: 20px; font-weight: 700; color: var(--sidebar-text); margin-bottom: 8px; }
.signup-card .signup-sub { font-size: 13px; line-height: 1.6; color: var(--text-muted); margin-bottom: 24px; }
.signup-card label { display: block; font-size: 13px; font-weight: 600; color: var(--text-main); margin-bottom: 6px; }
.signup-card input[type="text"],
.signup-card input[type="email"],
.signup-card input[type="password"] {
  width: 100%; padding: 9px 12px; font-size: 14px; font-family: inherit;
  color: var(--text-main); background: #fff;
  border: 1px solid var(--border-color); border-radius: 4px;
}
.signup-card input[type="text"]:focus,
.signup-card input[type="email"]:focus,
.signup-card input[type="password"]:focus { outline: none; border-color: var(--primary-blue); }
.signup-hint { font-size: 12px; color: var(--text-muted); margin-top: 8px; line-height: 1.5; }
/* Multi-field form (the self-service signup card below _collectTenantName's
   single-field one) — vertical rhythm between stacked label/input pairs.
   _collectTenantName doesn't use this wrapper, so it's unaffected. */
.signup-field + .signup-field { margin-top: 16px; }
/* Live password-policy checklist. Client-side mirror of the Auth0 connection's
   configured policy, for UX only — see PASSWORD_RULES in auth.js for why
   Auth0's own rejection message is still the last word if the two drift. */
.signup-password-rules { list-style: none; margin: 8px 0 0; padding: 0; font-size: 12px; color: var(--text-muted); line-height: 1.7; }
.signup-password-rules li { position: relative; padding-left: 18px; }
.signup-password-rules li::before { content: "○"; position: absolute; left: 0; top: 0; }
.signup-password-rules li.ok { color: var(--success-green); }
.signup-password-rules li.ok::before { content: "✓"; color: var(--success-green); }
.signup-actions { margin-top: 24px; display: flex; align-items: center; gap: 12px; }
.signup-actions .btn { flex: 1; padding: 10px 16px; font-size: 14px; }
/* Near-black rather than --primary-blue, matching the Auth0 login screen's
   own button and the app's other near-black controls (see #disc-modal-close
   above) — a deliberate spec choice for this one CTA, not a change to .btn's
   default. Hover lightens rather than darkens for the same reason given
   there: #0B1621 is already near-black and has nowhere darker to go. Scoped
   to the self-service signup form's own button id, not .signup-actions .btn,
   so _collectTenantName's "Create organization" button (same markup pattern,
   different flow) is unaffected. */
#signup-create-account { background: #0B1621; }
#signup-create-account:hover { background: #1e2c3d; }
/* Errors keep their space reserved only when populated, so the card doesn't
   jump on the first failed submit. */
.signup-error {
  margin-top: 16px; padding: 10px 12px; border-radius: 4px; font-size: 13px; line-height: 1.5;
  background: #fef2f2; border: 1px solid #fecaca; color: var(--alert-red-hover);
}
.signup-error[hidden] { display: none; }
.signup-signout {
  margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border-color);
  font-size: 12px; color: var(--text-muted); text-align: center;
}
.signup-signout button {
  background: none; border: none; padding: 0; font: inherit; color: var(--primary-blue);
  cursor: pointer; text-decoration: underline;
}

/* ---- Discovery: unified table ----
   The page used to be two stacked panels (live TCP / crt.sh), each with its own
   scan button and table. One row per domain now joins both, so these styles
   cover the status dots, the progress label and the legend that replaced them. */
/* .disc-completed (a standalone green "Completed" badge in the header) is gone.
   Its job — reporting the outcome of the last run — belongs to
   .disc-scan-state below, which sits beside the progress bar and also covers
   the in-progress and failed cases the badge could not express. */
/* .disc-progress-label lived here and is gone with the element it styled — the
   scan's phase text and result line are a toast now. See the note in
   index.html where the <p> used to be. */

/* Status dot. One class per state so the legend and the table cells cannot
   drift apart — both render the same four classes. */
.disc-dot {
  display: inline-block; width: 9px; height: 9px; border-radius: 50%;
  margin-right: 7px; flex-shrink: 0; vertical-align: middle;
}
.disc-dot-match { background: var(--success-green); }
.disc-dot-new   { background: var(--warning-amber); }
.disc-dot-none  { background: var(--primary-blue); }
.disc-dot-fail  { background: var(--alert-red); }

.disc-status { display: inline-flex; align-items: center; white-space: nowrap; font-size: 12px; font-weight: 600; }

/* Secondary text inside Discovery cells — the "—" placeholders, expiry
   sub-lines and the "+N more in CT logs" note. Defined here rather than as a
   global utility because Discovery is the only page that uses it. */
.muted { color: var(--text-muted); }

/* .btn-sm (a compact .btn sized to sit inside a table row) lived here and has
   been removed: Discovery's four Actions buttons were its only users anywhere
   in the app, and they are .icon-act-btn chips now. Re-add it here if a page
   ever needs a small text .btn again — don't reach for it from the Actions
   column, which has .icon-act-btn/.act-btn for that. */

.disc-legend {
  padding: 12px 16px 14px; border-top: 1px solid var(--border-color); background: var(--body-bg);
  font-size: 12px; color: var(--text-muted); display: flex; flex-wrap: wrap; gap: 16px; align-items: center;
}
.disc-legend-title { font-weight: 600; color: var(--text-main); }
/* The "Tracked" legend is a second .disc-legend block directly under the status
   one (see index.html for why it is not merged into it). Dropping the repeated
   top border and top padding is what keeps the pair reading as a single legend
   area rather than two stacked panels. */
.disc-legend + .disc-legend { border-top: none; padding-top: 0; }
/* The legend's globe and dash sit in <i> elements, which .disc-legend's flex
   layout would otherwise baseline-align against the text inconsistently — the
   dots above get away with it because they are fixed-size blocks. */
.disc-legend .disc-tracked,
.disc-legend .disc-legend-dash { margin-right: 7px; font-style: normal; }

/* Discovery "Tracked" column: this domain is in the Domains list, as opposed to
   being on the page only because a certificate names it. Blue rather than the
   muted grey of the "—" it replaces, so the column can be scanned down at a
   glance; the glyph is filled in currentColor and inherits it, which is the
   whole point of currentColor here — the colour belongs to this rule, not to
   the drawing, so the same glyph works anywhere else it is dropped later.
   The flex box is what vertically centres the glyph against the row's text — a
   bare inline <svg> sits on the text baseline and rides visually low. 17x16,
   not the 16x16 square this started at: still 16 tall, matching every other
   inline cell glyph, but the WWW wordmark overhangs the globe on both sides so
   the drawing is wider than it is tall. */
.disc-tracked {
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--primary-blue); vertical-align: middle;
}

/* Rows are clickable (they open the certificate detail modal), so they need to
   look it — the other tables in the app are inert and deliberately don't. */
#disc-table tbody tr { cursor: pointer; }
#disc-table tbody tr:hover { background: var(--body-bg); }
#disc-table tbody tr.empty-row { cursor: default; }
#disc-table tbody tr.empty-row:hover { background: transparent; }

/* The Actions cell's own layout comes from the shared .row-actions track and
   td.actions-cell rule (see the Buttons/links section), the same pair the
   Certificates and Domains Actions columns use. The group is LEFT-anchored,
   matching that shared default. Stopping a row click from also opening the
   modal is handled in app.js, keyed on .row-actions.

   A `min-width: 207px` used to sit here, reserving the FOUR-icon case (a
   domain whose certificate rotated after being added from Discovery shows
   View + Add + Add all + Undo at once — the Undo condition keys off the
   inventory row's source while `match` keys off the *discovered* serial, see
   discovery_rows._classify). It has been removed, for two reasons.

   First, it was solving a problem nothing had: enableColumnResize measures
   each column's natural width in AUTO layout before pinning table-layout to
   fixed, and auto layout already sizes a column to its widest actual cell. A
   four-icon row present at measure time is therefore accommodated without any
   reserve.

   Second, it cost every other row. Sized for the widest case and left-
   anchored, a one-icon row carried ~138px of dead space between its icon and
   the table's right edge — a permanent empty gutter on the majority of rows to
   protect a rare one.

   Known edge case, accepted: the natural widths are captured once per page
   load, so a row that gains its fourth icon AFTER the first render (via Add or
   Undo) can overflow its column and be clipped by the frame's
   overflow-x:hidden until a reload. Dragging the column wider also fixes it,
   which is now possible on this page — see the note in loadDiscovery. */
#discovery-view .metadata-table td.actions-cell,
#discovery-view .metadata-table thead th:last-child {
  text-align: left;
  /* 5px both sides, against .metadata-table td's 16px. The left inset is the
     gap the icon group sits off the column's left border; the right one is
     what stops a wide empty gutter opening between the last icon and the
     table's edge. Neither affects alignment within the group, so the icons
     still line up in the same column from row to row. */
  padding-left: 5px;
  padding-right: 5px;
}

/* The two certificate columns (Discovered / Inventory), header and cells.
   Left-aligned with an 8px inset rather than .metadata-table td's 16px, per
   spec. These carry the only free-text of arbitrary length in the table, so
   they are the two columns worth dragging wider — which is also why their
   content is a single inline run now (see discCertCell in app.js): stacked
   <div>s ignored the td's nowrap/ellipsis and wrapped instead of truncating,
   which made widening the column change nothing.

   Correction to an earlier note here, which claimed these columns "have always
   had drag handles": they had none. enableColumnResize is what creates them,
   and Discovery was the one table page that never called it — so until that
   was fixed (see loadDiscovery) this page had no resizing at all, and the
   single-line change above could not yet pay off. It can now. */
#discovery-view .metadata-table .disc-cert-col { text-align: left; padding-left: 8px; }
/* The name takes the slack and is the only part that truncates; the trailing
   metadata ("exp <date>", "+N more in CT logs") keeps its natural size.
   The ellipsis has to be declared on .disc-cert-cn itself — the td's own
   text-overflow cannot reach inside a flex container, so without this the name
   would be hard-clipped mid-glyph with no "…". Both min-width:0 declarations
   are load-bearing: flex items default to min-width:auto, which refuses to
   shrink below content size and would push the cell wider than its column
   instead of truncating. */
.disc-cert-line { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.disc-cert-cn { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.disc-cert-line > .disc-cert-meta { flex-shrink: 0; }
.disc-cert-meta { font-size: 11.5px; }

/* Scan status row inside .view-header — see the note on the markup in
   index.html for why it lives there. flex-basis:100% makes it wrap onto its
   own line and span exactly the title+description width, so the bar stops
   where "…discover live certificates." does. */
.disc-scan-status { flex-basis: 100%; position: relative; margin-top: 8px; }
.disc-scan-status[hidden] { display: none; }
/* Overrides .progress-track's shared 12px block margins: this one is spaced by
   its wrapper above, and green rather than the default blue. */
#discovery-view .progress-track { margin: 0; }
#discovery-view .progress-bar { background: var(--success-green); }
/* Positioned just past the bar's right edge rather than being a flex sibling:
   as a sibling it would take width from the track and stop the bar short of
   the description text, which is the one thing this layout is for. */
.disc-scan-state {
  position: absolute; left: calc(100% + 10px); top: 50%; transform: translateY(-50%);
  display: inline-flex; align-items: center; gap: 6px;
  white-space: nowrap; font-size: 12px; font-weight: 600;
}
.disc-scan-state--running { color: var(--text-muted); }
.disc-scan-state--done { color: var(--success-green); }
.disc-scan-state--failed { color: var(--alert-red); }
.disc-spinner {
  width: 13px; height: 13px; flex-shrink: 0; border-radius: 50%;
  border: 2px solid #cbd5e1; border-top-color: var(--success-green);
  animation: disc-spin 0.7s linear infinite;
}
@keyframes disc-spin { to { transform: rotate(360deg); } }
/* Respect a reduced-motion preference — the colour and label already carry the
   state, so the rotation is decoration. */
@media (prefers-reduced-motion: reduce) { .disc-spinner { animation: none; } }

/* Modal cert blocks */
.disc-cert-block {
  border: 1px solid var(--border-color); border-radius: 8px; padding: 14px; margin-bottom: 12px; background: #fff;
}
.disc-cert-block h5 { margin: 0 0 10px; font-size: 13px; font-weight: 700; color: var(--sidebar-text); }
.disc-cert-row { display: flex; gap: 10px; font-size: 12.5px; line-height: 1.7; }
.disc-cert-row span:first-child { color: var(--text-muted); min-width: 108px; flex-shrink: 0; }
.disc-cert-empty { font-size: 13px; color: var(--text-muted); padding: 8px 0; }
.disc-source-tag {
  display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600;
  background: #eff6ff; color: #1d4ed8;
}
/* "Live" — read off the host during the last run, as opposed to recovered from
   a CT log. Green, reusing .badge-tls/.badge-valid's exact tokens (#dcfce7 /
   #15803d) so it matches every other "this is good / this is current" pill in
   the app, including the .icon-filter-btn.active state on the toolbar filter
   that surfaces these same reachable domains. */
.disc-source-tag--live { background: #dcfce7; color: #15803d; }
