/* ===========================================================================
   Typeface — Inter, self-hosted (vendored like Bootstrap/jQuery/HTMX rather than
   linked from Google's CDN, consistent with this project having no build step and
   no runtime dependency on third-party origins). Single variable-font file covers
   the whole 100-900 weight axis, latin subset only (matches this dashboard's
   English-only UI copy — see CLAUDE.md). Chosen for a data-dense ops dashboard:
   a tall x-height and open apertures keep small tabular text (log rows, stat
   values) legible, and it ships true tabular figures so numbers in adjacent rows
   still align — the same reason system-ui dashboards (Linear, Vercel, GitHub)
   converge on it. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/static/fonts/inter-variable-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ===========================================================================
   Design tokens — single source of truth for color/spacing/radius across the
   dashboard, logs, and modal views. Swap these to re-theme the whole app.
   =========================================================================== */
:root {
  /* Overrides Bootstrap's own --bs-body-font-family (declared in its own :root
     block in bootstrap.min.css, loaded before this file) — every Bootstrap
     component (.btn, .form-control, .card, ...) reads that variable directly
     rather than just inheriting body's font-family, so this is the one place
     that needs to change to re-font the whole app, tokens included. */
  --bs-body-font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

  --ink: #1c2128;
  --ink-soft: #57606c;
  --ink-muted: #8a94a6;

  --surface: #ffffff;
  --surface-sunken: #f6f7f9;
  --border: #e6e9ef;

  --accent: #4338ca;
  --accent-soft: rgba(67, 56, 202, .10);

  --good: #157347;
  --good-soft: rgba(21, 115, 71, .12);
  --warning: #8a5a00;
  --warning-soft: rgba(245, 165, 36, .20);
  --critical: #c1121f;
  --critical-soft: rgba(193, 18, 31, .10);
  --neutral: #57606c;
  --neutral-soft: rgba(87, 96, 108, .10);
  --webhook: #0e7490;
  --webhook-soft: rgba(8, 145, 178, .12);
  --app: #2563eb;
  --app-soft: rgba(37, 99, 235, .12);
  --info: #0369a1;
  --info-soft: rgba(14, 165, 233, .14);

  /* Solid indicator colors — health bar segments, legend dots, and the live-status
     dot all point at the same five values; previously each repeated its own literal
     hex, so the three drifted independently. Kept apart from --good/--warning/etc.
     above because those are tuned as *text-on-soft-tint* pairs (need to stay dark
     enough to read as text), while these are solid fills on an already-colored
     track and want to stay vivid in both themes — no dark-mode override needed. */
  --status-success: #22c55e;
  --status-cancelled: #8b93a7;
  --status-error: #f59e0b;
  --status-failed: #e5484d;
  --status-idle: #c7ccd6;

  --radius-lg: 16px;
  --radius-md: 10px;
  --shadow-sm: 0 1px 2px rgba(16, 24, 40, .04), 0 1px 3px rgba(16, 24, 40, .06);

  /* Sidebar/mobile-topbar background — deliberately fixed across both themes (the
     sidebar has always been permanently dark, in light mode too) rather than tied to
     --ink, which flips to a *light* color in dark mode since there it means "text on
     a dark surface". Reusing --ink here made the sidebar invert to near-white in dark
     mode instead of staying dark. Not re-declared in the dark override block below —
     that's what keeps it constant. */
  --sidebar-bg: #1c2128;
}

/* ===========================================================================
   Dark theme — overrides the same token set for [data-bs-theme="dark"] (set on
   <html> by the inline bootstrap script in base.jinja/login.jinja, before first
   paint, and flipped at runtime by theme.js). Every rule in this file is written
   against the tokens above, so this block is the only place dark mode needs to
   be taught anything; stock Bootstrap components (card/modal/table/form-control)
   already reskin themselves for this attribute on their own.
   =========================================================================== */
:root[data-bs-theme="dark"] {
  --ink: #e6e8eb;
  --ink-soft: #9aa4b2;
  --ink-muted: #717d8c;

  --surface: #1c2129;
  --surface-sunken: #12151a;
  --border: #2b3038;

  --accent: #8b93ff;
  --accent-soft: rgba(139, 147, 255, .18);

  --good: #3fb97a;
  --good-soft: rgba(63, 185, 122, .16);
  --warning: #e2a33d;
  --warning-soft: rgba(226, 163, 61, .18);
  --critical: #ef6a6a;
  --critical-soft: rgba(239, 106, 106, .16);
  --neutral: #9aa4b2;
  --neutral-soft: rgba(154, 164, 178, .14);
  --webhook: #3ec4dc;
  --webhook-soft: rgba(62, 196, 220, .16);
  --app: #6fa6ff;
  --app-soft: rgba(111, 166, 255, .16);
  --info: #4fc0f0;
  --info-soft: rgba(79, 192, 240, .16);

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4), 0 1px 3px rgba(0, 0, 0, .5);
}

body {
  color: var(--ink);
  background: var(--surface-sunken);
  /* Inter (like most grotesque sans faces) renders a touch heavier than system
     fonts at small sizes without this — standard pairing, not Inter-specific. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ===========================================================================
   Card polish — applies sitewide (dashboard, logs, settings, modal) so every
   card shares the same corner radius / border / shadow language.
   =========================================================================== */
.card {
  background-color: var(--surface);
  border-color: var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  /* Rounded corners don't clip a card's content on their own — without this, a
     flush-edge child like the log stream's <table> (its header row paints its own
     background right into the corner) pokes a sharp square corner out past the
     card's rounded one. */
  overflow: hidden;
}

/* ===========================================================================
   Section labels — small uppercase headers above a KPI group.
   =========================================================================== */
.section-label {
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.api-keys-toggle-icon {
  transition: transform .15s ease;
}

[aria-expanded="true"] .api-keys-toggle-icon {
  transform: rotate(180deg);
}

/* ===========================================================================
   Live indicator — static dot + label, honest signal that the page is
   websocket-driven rather than a static snapshot (no pulse animation).
   =========================================================================== */
.live-indicator {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  font-size: .78rem;
  font-weight: 600;
  color: var(--ink-soft);
}
.live-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--status-success);
}
/* Pending variant — shown while the socket is still connecting/reconnecting, so
   the dot only turns green once data is actually flowing. */
.live-dot--pending {
  background: var(--ink-muted);
}
/* Sub-text next to the live label — how long since the last push, not just
   whether the socket is open. Lighter weight so it doesn't compete with the
   "live"/"reconnecting…" label itself. */
.live-updated {
  font-weight: 400;
  color: var(--ink-muted);
}

/* ===========================================================================
   Stat rows — icon chip + label + value, stacked inside a KPI card.
   =========================================================================== */
.stat-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  padding: .6rem 0;
  border-bottom: 1px solid var(--border);
}
.stat-row:last-child { border-bottom: none; padding-bottom: 0; }
.stat-row:first-child { padding-top: 0; }
.stat-row__left {
  display: flex;
  align-items: center;
  gap: .65rem;
  min-width: 0;
}
.stat-icon {
  width: 34px;
  height: 34px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  background: var(--accent-soft);
  color: var(--accent);
}
.stat-icon svg { width: 18px; height: 18px; }
.stat-label {
  font-size: .85rem;
  color: var(--ink-soft);
  white-space: nowrap;
}
.stat-value {
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.stat-value.text-critical { color: var(--critical); }
.stat-value--muted { font-size: .85rem; font-weight: 500; color: var(--ink-muted); }

/* Health-tinted stat-icon variants — same chip, colored by the stat's own value
   (success rate, error count) instead of always the default accent tint, so a
   panel's health reads at a glance instead of requiring the number to be parsed. */
.stat-icon--good { background: var(--good-soft); color: var(--good); }
.stat-icon--warning { background: var(--warning-soft); color: var(--warning); }
.stat-icon--critical { background: var(--critical-soft); color: var(--critical); }

/* ===========================================================================
   Cron health widget — stacked segment bar + legend rows, replaces three
   separate "active/idle/error" cards with one proportional visualization.
   =========================================================================== */
.health-bar {
  display: flex;
  gap: 2px;
  height: 10px;
  border-radius: 999px;
  overflow: hidden;
  background: var(--surface-sunken);
  margin-bottom: 1rem;
}
.health-bar__seg { height: 100%; transition: flex-basis .3s ease; }
.health-bar__seg--success { background: var(--status-success); }
.health-bar__seg--cancelled { background: var(--status-cancelled); }
.health-bar__seg--error { background: var(--status-error); }
.health-bar__seg--failed { background: var(--status-failed); }
/* Empty-state only, when there are no logs at all yet to show a proportion of. */
.health-bar__seg--idle { background: var(--status-idle); }

.legend-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex: 0 0 auto;
}
.legend-dot--success { background: var(--status-success); }
.legend-dot--cancelled { background: var(--status-cancelled); }
.legend-dot--error { background: var(--status-error); }
.legend-dot--failed { background: var(--status-failed); }
.legend-dot--idle { background: var(--status-idle); }

/* ===========================================================================
   Soft status/kind badges — tinted pill instead of solid Bootstrap bg-*, used
   consistently across the dashboard table, logs stream, and detail modal.
   =========================================================================== */
.badge-soft-success  { background: var(--good-soft);     color: var(--good); }
.badge-soft-danger   { background: var(--critical-soft); color: var(--critical); }
.badge-soft-warning  { background: var(--warning-soft);  color: var(--warning); }
.badge-soft-info     { background: var(--info-soft);     color: var(--info); }
.badge-soft-secondary{ background: var(--neutral-soft);  color: var(--neutral); }
.badge-soft-light    { background: var(--surface-sunken); color: var(--ink-soft); border: 1px solid var(--border); }

/* ===========================================================================
   Kind badges — one color per log kind (cron/webhook/app) so the "Kind"/
   "Type" column is scannable at a glance instead of every row using the same
   neutral badge-soft-light.
   =========================================================================== */
.badge-kind-cron    { background: var(--accent-soft);  color: var(--accent); }
.badge-kind-webhook { background: var(--webhook-soft); color: var(--webhook); }
.badge-kind-app     { background: var(--app-soft);     color: var(--app); }

/* ===========================================================================
   Log row wash — the whole row is tinted by its status so it reads at a
   glance while scanning, without needing the badge text. Previously this was
   a hand-authored kind*status matrix of 15 flat hexes pre-blended against
   white — it couldn't adapt to dark mode and the kind axis mostly duplicated
   what the kind badge already shows. Simplified to one tint per status,
   computed off the same tokens as everything else (so it re-tints for free
   in dark mode) via color-mix against --surface, the row's actual card
   background. Colors are injected via Bootstrap's --bs-table-bg because
   Bootstrap paints each td with that variable and would cover any tr
   background. .log-row--kind-* classes are still emitted by the templates
   (kept for the kind badge and any future per-kind styling) but no longer
   drive the row background. */
.log-row--status-success   { --bs-table-bg: color-mix(in srgb, var(--good) 10%, var(--surface)); }
.log-row--status-danger    { --bs-table-bg: color-mix(in srgb, var(--critical) 10%, var(--surface)); }
.log-row--status-warning   { --bs-table-bg: color-mix(in srgb, var(--warning) 14%, var(--surface)); }
.log-row--status-info      { --bs-table-bg: color-mix(in srgb, var(--info) 10%, var(--surface)); }
.log-row--status-secondary { --bs-table-bg: color-mix(in srgb, var(--neutral) 8%, var(--surface)); }

/* ===========================================================================
   Activity table — row spacing/hover polish, reused inside each service's
   "Recent activity" sub-list.
   =========================================================================== */
.service-avatar {
  width: 26px;
  height: 26px;
  border-radius: 8px;
  background: var(--accent-soft);
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: .72rem;
  font-weight: 700;
  margin-right: .5rem;
  flex: 0 0 auto;
}
/* Larger variant for panel headers (job/webhook activity) where the avatar sits
   beside an h5 title rather than inline in a table row. */
.service-avatar--lg {
  width: 42px;
  height: 42px;
  border-radius: var(--radius-md);
  font-size: 1rem;
  margin-right: 0;
}

/* ===========================================================================
   Inbound webhook URL — the copyable endpoint address in the webhook activity
   panel header. Capped and scrollable rather than wrapping, so a long host
   can't push the header card's height around; the copy button takes the whole
   value regardless of what's visible.
   =========================================================================== */
.webhook-url__value {
  display: block;
  max-width: 26rem;
  overflow-x: auto;
  white-space: nowrap;
  scrollbar-width: thin;
  padding: .35rem .55rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background-color: var(--surface-sunken);
  color: var(--ink);
  font-size: .82rem;
}
@media (max-width: 575.98px) {
  .webhook-url__value { max-width: 100%; }
}
/* Full-width variant: in the webhook panel's setup card the URL owns the row rather
   than sharing a header with the service identity, so the 26rem clamp above (which
   keeps it from crowding that header — see _api_handler_panel.jinja) is dropped and
   the value only scrolls once it genuinely outgrows the card. */
.webhook-url--block { flex: 1 1 auto; min-width: 0; }
.webhook-url--block .webhook-url__value { max-width: 100%; }

/* api::handler header: service identity on the left, the base URL boxed in the right
   corner. Both sides need an explicit flex basis *and* min-width:0 — without it the
   identity block's widest child (the service description, a full sentence for some
   sources) sets its min-content width, the block claims the whole row, and flex-wrap
   drops the URL box to a full-width second line at the bottom of the card, which is the
   last place you'd look for the one value you came to copy. The description carries its
   own max-width so it wraps to a readable measure instead of running the card's width. */
.api-header__identity { flex: 1 1 20rem; min-width: 0; }
.api-header__identity > div { min-width: 0; }
.api-header__desc { max-width: 46rem; }
.api-header__url { flex: 0 1 32rem; min-width: 0; }
.api-header__url .webhook-url__value { max-width: 100%; }

.table > :not(caption) > * > * { padding-top: .65rem; padding-bottom: .65rem; }
.table tbody tr[role="button"]:hover { background-color: var(--surface-sunken); cursor: pointer; }

/* ===========================================================================
   Icon-only button — square hit target, no label, for header-level actions like
   Configure (sidebar's per-service gear).
   =========================================================================== */
.btn-icon-only {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--neutral);
  background: transparent;
}
.btn-icon-only:hover { color: var(--bs-body-color); background: var(--neutral-soft); }
.btn-icon-only svg { width: 15px; height: 15px; }

/* Theme toggle — both icons always render (see _sidebar.jinja); only the one
   naming the theme you'd switch *to* is shown, so the icon itself doubles as
   the label instead of needing JS to swap markup on click. */
.theme-toggle__icon { display: none; }
:root:not([data-bs-theme="dark"]) #theme-toggle .theme-toggle__icon--dark { display: block; }
:root[data-bs-theme="dark"] #theme-toggle .theme-toggle__icon--light { display: block; }

/* ===========================================================================
   App shell — persistent left sidebar + main content column, replaces the old
   top navbar entirely. Full viewport height so the sidebar's footer (logout)
   can pin to the bottom regardless of how much nav content is above it.
   =========================================================================== */
.app-shell {
  display: flex;
  align-items: stretch;
  min-height: 100vh;
}
.main-content {
  flex: 1 1 auto;
  min-width: 0;
  padding: 1.5rem;
}

/* ===========================================================================
   Sidebar — dark to match the old navbar's palette, fixed width. Pinned to the
   viewport height (not the page's content height, which can be much taller once
   the service list or the log stream grows) so its own nav scrolls internally
   and the footer never gets pushed below the fold.
   =========================================================================== */
.sidebar {
  flex: 0 0 264px;
  width: 264px;
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--sidebar-bg);
  color: rgba(255, 255, 255, .85);
}
.sidebar-brand {
  flex: 0 0 auto;
  padding: 1rem 1.1rem;
  font-weight: 700;
  border-bottom: 1px solid rgba(255, 255, 255, .1);
}
.sidebar-brand a {
  color: #fff;
  text-decoration: none;
}
.sidebar-nav {
  flex: 1 1 auto;
  /* Without this, a flex item's default min-height:auto stops it shrinking
     below its content size, so overflow-y:auto would never actually kick in
     and the footer below would get pushed off the pinned sidebar's height. */
  min-height: 0;
  overflow-y: auto;
  padding: .75rem 0;
}
.sidebar-footer {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  padding: .7rem 1.1rem;
  border-top: 1px solid rgba(255, 255, 255, .1);
}
.sidebar-footer .live-indicator {
  color: rgba(255, 255, 255, .65);
  font-size: .74rem;
}

/* Top-level links (Overview/Logs) — same active-state language the old navbar
   used (a solid block, not a pill), just laid out vertically now. */
.sidebar-toplink {
  display: block;
  padding: .5rem 1.1rem;
  font-weight: 600;
  color: rgba(255, 255, 255, .7);
  text-decoration: none;
}
.sidebar-toplink:hover { color: #fff; }
.sidebar-toplink.active {
  color: #fff;
  background: rgba(255, 255, 255, .14);
}

.sidebar-services { margin-top: .75rem; }

/* Service header — non-interactive label (a status dot + name), not a link;
   see _sidebar.jinja. The Configure gear is the only clickable element in it. */
.sidebar-service { margin-bottom: .35rem; }
.sidebar-service__header {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .6rem 1.1rem .3rem;
  font-size: .78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: rgba(255, 255, 255, .5);
}
.sidebar-service__label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Icon-only buttons on the dark sidebar (service Configure gear, footer Log out)
   need the light-on-dark treatment .btn-icon-only doesn't have by default. */
.sidebar .btn-icon-only {
  width: 22px;
  height: 22px;
  border-color: rgba(255, 255, 255, .2);
  color: rgba(255, 255, 255, .6);
}
.sidebar .btn-icon-only:hover { color: #fff; background: rgba(255, 255, 255, .1); }
.sidebar .btn-icon-only svg { width: 13px; height: 13px; }

/* Jobs + webhook — the actual clickable nav items, indented under their service. */
.sidebar-item {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .4rem 1.1rem .4rem 1.6rem;
  font-size: .84rem;
  color: rgba(255, 255, 255, .75);
  text-decoration: none;
}
.sidebar-item:hover { color: #fff; background: rgba(255, 255, 255, .06); }
.sidebar-item.active { color: #fff; background: rgba(255, 255, 255, .14); }
.sidebar-item--inactive { opacity: .5; }

/* Per-job status glyph — play/refresh/stop, colored via `color` since the icon's
   own <svg> uses fill/stroke="currentColor" (see sidebar_status_icon macro in
   _sidebar_status_icon.jinja). Blue while a run is in flight, gray once idle, red on failure. */
.sidebar-item__status-icon { width: 12px; height: 12px; flex: 0 0 auto; }
.sidebar-item__status-icon svg { display: block; width: 100%; height: 100%; }
.sidebar-item__status-icon--running { color: #6d8bff; }
.sidebar-item__status-icon--idle { color: #6b7280; }
.sidebar-item__status-icon--error { color: #f87171; }

/* Webhook/api::handler rows have no status dot, so give them a small glyph
   in its place — keeps every row's leading slot filled instead of the text
   sitting flush against the indent. Inherits currentColor, so it dims/lights
   with the row exactly like the label text already does. */
.sidebar-item__icon { width: 13px; height: 13px; flex: 0 0 auto; }
.sidebar-item--empty {
  padding: .3rem 1.1rem .5rem 1.6rem;
  font-size: .78rem;
  color: rgba(255, 255, 255, .4);
}
.sidebar-item__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ===========================================================================
   Mobile topbar + off-canvas sidebar — below the lg breakpoint (matches the
   row-cols-lg-*/d-lg-none breakpoint already used sitewide) the sidebar stops
   taking up a permanent 264px column and instead becomes a fixed overlay,
   opened by a hamburger button in a topbar that only exists at this width.
   Hidden via .d-lg-none/.d-none.d-lg-flex utility classes in the markup;
   this block only holds the behavior plain utility classes can't express
   (the slide transform and its open state, and the backdrop). See sidebar.js.
   =========================================================================== */
.mobile-topbar {
  align-items: center;
  gap: .75rem;
  padding: .65rem 1rem;
  background: var(--sidebar-bg);
  color: #fff;
  position: sticky;
  top: 0;
  z-index: 1030;
}
.mobile-topbar__brand {
  color: #fff;
  font-weight: 700;
  text-decoration: none;
}
.sidebar-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(15, 18, 22, .55);
  z-index: 1044;
}
.sidebar-backdrop.is-open { display: block; }
/* Guards against the overlay's backdrop lingering if the viewport is resized
   back above the breakpoint while it's open (e.g. rotating a tablet, or a
   dev-tools resize) — the sidebar itself is unaffected since position:fixed
   only applies inside the max-width query below. */
@media (min-width: 992px) {
  .sidebar-backdrop { display: none !important; }
}

@media (max-width: 991.98px) {
  .sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: 1045;
    transform: translateX(-100%);
    transition: transform .2s ease;
  }
  .sidebar.is-open { transform: translateX(0); }
  .main-content { padding: 1rem; }
}

/* ===========================================================================
   Execution detail modal — compact key/value grid, a parsed error-summary
   card (type/message/location at a glance), and wrapped/scrollable log
   blocks so long tracebacks/JSON never get clipped by the modal's edge.
   =========================================================================== */
.detail-grid {
  display: grid;
  grid-template-columns: 140px 1fr;
  row-gap: .55rem;
  column-gap: 1rem;
  font-size: .9rem;
}
.detail-grid dt { color: var(--ink-muted); font-weight: 600; }
.detail-grid dd { margin: 0; color: var(--ink); }

.error-summary {
  display: flex;
  gap: .75rem;
  padding: .85rem 1rem;
  border-radius: var(--radius-md);
  background: var(--critical-soft);
  border: 1px solid color-mix(in srgb, var(--critical) 30%, transparent);
}
.error-summary__icon { color: var(--critical); flex: 0 0 auto; margin-top: .1rem; }
.error-summary__body { min-width: 0; }
.error-summary__type {
  font-weight: 700;
  color: var(--critical);
  font-size: .74rem;
  text-transform: uppercase;
  letter-spacing: .04em;
}
.error-summary__message {
  color: var(--ink);
  font-size: .92rem;
  margin-top: .15rem;
  word-break: break-word;
}
.error-summary__location {
  color: var(--ink-muted);
  font-size: .76rem;
  margin-top: .4rem;
  font-family: var(--bs-font-monospace, monospace);
}

.log-pre {
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: .75rem .9rem;
  font-size: .78rem;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 320px;
  overflow-y: auto;
  margin: 0;
}

.progress-history { display: flex; flex-direction: column; gap: .3rem; }
.progress-history__row { display: flex; gap: .6rem; align-items: baseline; }
.progress-history__time { flex: 0 0 auto; font-variant-numeric: tabular-nums; }
.progress-history__msg { color: var(--ink); word-break: break-word; }

/* ===========================================================================
   Settings page — credential form, centered on the page instead of pinned to
   the left edge of the container-fluid shell used everywhere else.
   =========================================================================== */
.settings-page {
  display: flex;
  justify-content: center;
  padding-top: 1.5rem;
}
.settings-card-wrap {
  width: 100%;
  max-width: 34rem;
}
.settings-card-wrap .form-label {
  font-size: .82rem;
  font-weight: 600;
  color: var(--ink-soft);
}
.settings-card-wrap .form-control {
  border-color: var(--border);
}
.settings-card-wrap .section-label:not(:first-child) {
  margin-top: .5rem;
}
.log-pre--error { color: var(--critical); }

/* ===========================================================================
   Form focus ring — accent-colored, sitewide (was previously scoped to just
   the settings page's credential form; the log stream's filter bar wants it
   too, and there's no reason every other form-control on the site shouldn't
   match).
   =========================================================================== */
.form-control:focus, .form-select:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 .2rem var(--accent-soft);
}

/* ===========================================================================
   Filter field — icon chip fused to the left edge of a select/input, used by
   the log stream's filter bar so each control's subject (service/kind/date)
   reads at a glance instead of relying solely on its placeholder option text.
   =========================================================================== */
.filter-field {
  display: flex;
  align-items: stretch;
}
.filter-field__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  flex: 0 0 auto;
  border: 1px solid var(--border);
  border-right: none;
  border-radius: 6px 0 0 6px;
  background: var(--surface-sunken);
  color: var(--ink-muted);
}
.filter-field__icon svg { width: 14px; height: 14px; }
.filter-field .form-select,
.filter-field .form-control {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}

/* ===========================================================================
   Recent activity table — fixed layout (widths set via <colgroup> in
   overview_body.jinja) so a row gaining/losing its live-progress content
   (below) never reflows the Type/Name/When/etc. columns next to it.
   =========================================================================== */
.activity-table { table-layout: fixed; }
.activity-table td { overflow: hidden; }

/* Live progress — shown inside a row's status cell while its run/event is
   still in flight (status running/received). Replaces the old standalone
   Active Processes panel: the row itself now carries the "what's happening
   right now" affordance instead of a duplicate card at the page top. */
.activity-row__progress {
  margin-top: .3rem;
  max-width: 100%;
}
.activity-row__msg {
  font-size: .78rem;
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  display: block;
}

/* Shared with the log-detail modal's live progress block (_log_progress.jinja),
   which keeps its own elapsed-timer element ticked by localtime.js. */
.active-process-card__elapsed {
  font-size: .8rem;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Indeterminate progress affordance — no percentage is available (page count
   isn't a reliable total), so a sweeping fill over a soft-tint track communicates
   "working" without implying a knowable endpoint. Same soft-track + solid-fill
   grammar as .health-bar. Used by the Recent Activity row above and by the
   log-detail modal's live progress block. */
.indeterminate {
  position: relative;
  flex: 1;
  height: 4px;
  border-radius: 999px;
  overflow: hidden;
  background: var(--accent-soft);
}
.indeterminate::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 35%;
  border-radius: inherit;
  background: var(--accent);
  animation: indeterminate 1.15s ease-in-out infinite;
}

@keyframes indeterminate {
  0%   { transform: translateX(-120%); }
  100% { transform: translateX(320%); }
}
@media (prefers-reduced-motion: reduce) {
  .indeterminate::before { animation: none; }
}
