/* Admin console styles.
   Served from the app origin so the Content-Security-Policy can use
   `style-src 'self'` without `'unsafe-inline'`. All page-specific rules live
   here too (scoped by class), so no template needs an inline <style> block. */

:root {
  --luci-accent: #6AC8C2;
  --luci-accent-dark: #55AFA9;
  --luci-ink: #232B33;
  --luci-muted: #52606D;
  --luci-border: #D9E2EC;
  --luci-surface: #FFFFFF;
  --luci-bg: #EFF3F3;
  --luci-error: #A40000;
  --luci-success: #005A00;
  /* Chart/status colors, validated for CVD separation and contrast on white.
     The brand teal is too light and low-chroma to carry chart marks, so marks
     use a darker step of the same hue; the accent stays for interactive UI. */
  --luci-warn: #9A6700;
  --luci-chart: #0D9488;
  --luci-chart-soft: #9AA7B4;
}

* { box-sizing: border-box; }

body {
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  margin: 0;
  min-height: 100vh;
  line-height: 1.5;
  color: var(--luci-ink);
  background:
    radial-gradient(circle at top right, rgba(106, 200, 194, 0.16), transparent 28rem),
    var(--luci-bg);
}

/* Base link style. Without this, any anchor not caught by a container rule
   (.sidebar a, table.data a, .breadcrumb a, …) falls back to the browser's
   blue/purple default — visibly foreign to the teal palette. This is the
   inline tier of the link system (see the two-tier note on table.data links):
   inline/secondary links carry the accent; structural links override to ink. */
a {
  color: var(--luci-accent-dark);
  text-decoration: none;
}

a:hover { text-decoration: underline; }

/* Chrome links (nav, buttons, tabs, row-action + profile dropdowns, the logo,
   the health summary) give their own hover feedback — a background or colour
   shift — so the inline underline above would be wrong on them. Each selector
   here outweighs `a:hover`, neutralising it for that property only; the inline
   text links that should underline (breadcrumb, rowlink, queue) declare it in
   their own :hover and are deliberately absent. */
.sidebar a:hover,
.mainnav a:hover,
.brand:hover,
.button:hover,
.profile-dropdown a:hover,
.actions-dropdown a:hover,
.tabs a:hover,
a.health-summary:hover { text-decoration: none; }

.page {
  margin: 0 auto;
  max-width: 28rem;
  padding: 2.5rem 1.25rem 3rem;
}

.page--wide {
  max-width: 60rem;
}

/* Fluid full-width layout: expand to the viewport, keeping side gutters.
   Used for wide data tables (e.g. the schedule list). */
.page--full {
  max-width: none;
  padding-left: 2rem;
  padding-right: 2rem;
}

/* --- Shell: sidebar + page (signed-in layout) --- */
/* Not centred: the sidebar anchors to the left edge so it never shifts as
   pages of different widths are opened; page width caps left-align beside it. */
.shell {
  display: flex;
  gap: 1.5rem;
  align-items: flex-start;
  padding: 1.5rem 2rem 3rem;
}

/* Inside the shell the page no longer centres itself; width caps still apply. */
.shell .page {
  flex: 1;
  min-width: 0;
  margin: 0;
  padding: 0;
}


.sidebar {
  position: sticky;
  top: 1.5rem;
  flex: none;
  width: 13rem;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  padding: 0.5rem;
  background: var(--luci-surface);
  border: 1px solid var(--luci-border);
  border-radius: 1rem;
  box-shadow: 0 12px 32px rgba(35, 43, 51, 0.06);
}

.sidebar a {
  display: block;
  padding: 0.5rem 0.75rem;
  border-radius: 0.625rem;
  text-decoration: none;
  color: var(--luci-muted);
  font-weight: 600;
  font-size: 0.9375rem;
}

.sidebar a:hover { background: var(--luci-bg); color: var(--luci-ink); }
.sidebar a.active { background: rgba(106, 200, 194, 0.16); color: var(--luci-accent-dark); }

@media (max-width: 52rem) {
  .shell {
    flex-direction: column;
    padding: 1rem 1rem 2.5rem;
    gap: 1rem;
  }
  .sidebar {
    position: static;
    width: 100%;
    flex-direction: row;
    flex-wrap: wrap;
  }
}

/* --- App bar (top menu + profile) --- */
.appbar {
  background: var(--luci-surface);
  border-bottom: 1px solid var(--luci-border);
  box-shadow: 0 1px 3px rgba(35, 43, 51, 0.04);
}

.appbar-inner {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  padding: 0.625rem 2rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  text-decoration: none;
  color: var(--luci-ink);
}

.brand-logo {
  display: block;
  height: 1.75rem;
  width: auto;
  border: 0;
  margin: 0;
}

.brand-tag {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--luci-muted);
}

/* Environment badge: quiet on dev and local, unmissable on prod. It replaces
   the ceremony the TUI's --config flag used to provide — on prod the badge is
   the one visual cue that a queued job will run against client data. */
.env-badge {
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  padding: 0.1875rem 0.5625rem;
  border-radius: 999px;
  border: 1px solid var(--luci-border);
  color: var(--luci-muted);
  background: transparent;
  white-space: nowrap;
}

.env-badge--prod {
  color: #fff;
  background: var(--luci-error);
  border-color: var(--luci-error);
}

.mainnav {
  display: flex;
  gap: 0.25rem;
  margin-left: 0.5rem;
}

.mainnav--end { margin-left: auto; }

.mainnav a {
  padding: 0.4rem 0.75rem;
  border-radius: 0.5rem;
  text-decoration: none;
  color: var(--luci-muted);
  font-weight: 600;
  font-size: 0.9375rem;
}

.mainnav a:hover { background: var(--luci-bg); color: var(--luci-ink); }
.mainnav a.active { background: rgba(106, 200, 194, 0.16); color: var(--luci-accent-dark); }

.profile-menu { margin-left: auto; position: relative; }

.profile-menu > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
}
.profile-menu > summary::-webkit-details-marker { display: none; }
.profile-menu > summary::marker { content: ""; }

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: 999px;
  background: var(--luci-accent);
  color: var(--luci-ink);
  font-weight: 700;
  font-size: 0.9375rem;
}

.profile-dropdown {
  position: absolute;
  right: 0;
  top: calc(100% + 0.5rem);
  min-width: 14rem;
  background: var(--luci-surface);
  border: 1px solid var(--luci-border);
  border-radius: 0.75rem;
  box-shadow: 0 12px 32px rgba(35, 43, 51, 0.12);
  padding: 0.5rem;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.profile-id {
  margin: 0.25rem 0.5rem 0.5rem;
  padding-bottom: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  align-items: flex-start;
  border-bottom: 1px solid var(--luci-border);
}

.profile-email { font-weight: 600; font-size: 0.875rem; word-break: break-all; }

.profile-dropdown a,
.profile-dropdown .linklike {
  display: block;
  width: 100%;
  text-align: left;
  padding: 0.5rem;
  margin: 0;
  border: 0;
  border-radius: 0.5rem;
  background: transparent;
  color: var(--luci-ink);
  font: inherit;
  text-decoration: none;
  cursor: pointer;
}

.profile-dropdown a:hover,
.profile-dropdown .linklike:hover { background: var(--luci-bg); }
.profile-dropdown form { margin: 0; }

/* --- Breadcrumb --- */
.breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem;
  margin-bottom: 1.25rem;
  font-size: 0.875rem;
  color: var(--luci-muted);
}

.breadcrumb a { color: var(--luci-muted); text-decoration: none; }
.breadcrumb a:hover { color: var(--luci-ink); text-decoration: underline; }
.breadcrumb .sep { color: var(--luci-border); }
.breadcrumb [aria-current="page"] { color: var(--luci-ink); font-weight: 600; }

.panel {
  background: var(--luci-surface);
  border: 1px solid var(--luci-border);
  border-radius: 1rem;
  padding: 1.5rem;
  box-shadow: 0 12px 32px rgba(35, 43, 51, 0.06);
  /* Grow with content wider than the page's width cap (the data tables) so
     rows never bleed past the panel edge. A .table-wrap scroll container is
     not an option on those tables because the row-action dropdown menus must
     overflow the table. */
  width: fit-content;
  min-width: 100%;
}

.panel h1 {
  margin: 0 0 0.75rem;
  font-size: 1.5rem;
  line-height: 1.2;
}

/* --- Impersonation bar ---
   Full width, above the appbar, on every page for as long as a staff member is
   viewing a client. Deliberately not `.banner` (an inline pill) and deliberately
   not a toast: it must not be dismissible, and it must never be ambiguous which
   identity is live. Amber rather than error red — nothing is wrong, but nothing
   on the page is your own account either. */
.impersonation-bar {
  background: var(--luci-warn);
  color: #FFFFFF;
}

/* Gutters match .appbar-inner, not a content column. The bar is chrome sitting
   directly on top of the appbar, so the stop control has to line up with the
   account avatar below it; a max-width of its own left it inset by the centring
   margin on any wide page (.page--full runs to a 2rem gutter) and read as
   misaligned against everything it touches. */
.impersonation-bar-inner {
  padding: 0.5rem 2rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  align-items: center;
  justify-content: space-between;
  font-size: 0.875rem;
}

.impersonation-bar-text strong { font-weight: 700; }

.impersonation-bar form { margin: 0; }

/* margin:0 is load-bearing, not tidiness. The base `button` rule sets
   margin-top: 1.25rem for buttons that end a form, and a flex item establishes
   a BFC — so that margin cannot collapse out of the <form> around this button.
   It made the form 20px taller than its button, and `align-items: center`
   centres the *form*, leaving the button pinned at its bottom edge and sitting
   visibly below the sentence beside it. The profile dropdown's .linklike
   resets margin for the same reason. */
.impersonation-bar .linklike {
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: #FFFFFF;
  font: inherit;
  font-weight: 600;
  text-decoration: underline;
  cursor: pointer;
}

.banner {
  display: inline-block;
  margin: 0 0 1rem;
  padding: 0.25rem 0.625rem;
  border-radius: 999px;
  background: rgba(106, 200, 194, 0.16);
  color: var(--luci-accent-dark);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.error { color: var(--luci-error); margin: 0 0 1rem; }
.notice { color: var(--luci-success); margin: 0 0 1rem; }

label {
  display: block;
  margin-top: 1rem;
  font-weight: 600;
  font-size: 0.9375rem;
}

input {
  width: 100%;
  padding: 0.625rem 0.75rem;
  margin-top: 0.375rem;
  border: 1px solid var(--luci-border);
  border-radius: 0.625rem;
  font: inherit;
  background: #fff;
}

input:focus {
  outline: 2px solid rgba(106, 200, 194, 0.35);
  border-color: var(--luci-accent);
}

button,
.button {
  display: inline-block;
  margin-top: 1.25rem;
  padding: 0.625rem 1rem;
  border: 0;
  border-radius: 0.625rem;
  background: var(--luci-accent);
  color: var(--luci-ink);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
}

button:hover,
.button:hover {
  background: var(--luci-accent-dark);
}

button.secondary,
.button.secondary {
  background: transparent;
  color: var(--luci-muted);
  border: 1px solid var(--luci-border);
}

button.secondary:hover,
.button.secondary:hover {
  background: var(--luci-bg);
  color: var(--luci-ink);
}

/* Standalone destructive button (dropdown items restyle it themselves). */
button.danger {
  background: transparent;
  color: var(--luci-error);
  border: 1px solid var(--luci-error);
}

button.danger:hover {
  background: rgba(164, 0, 0, 0.08);
}

/* Checkbox row: keep the box beside its text instead of full-width. */
label.checkbox {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

label.checkbox input {
  width: auto;
  margin-top: 0;
}

code { word-break: break-all; font-size: 0.85em; }

.panel img {
  display: block;
  margin-top: 0.5rem;
  border: 1px solid var(--luci-border);
  border-radius: 0.75rem;
}

.table-wrap { overflow-x: auto; }

table.data {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9375rem;
}

table.data th,
table.data td {
  padding: 0.625rem 0.75rem;
  text-align: left;
  border-bottom: 1px solid var(--luci-border);
  vertical-align: middle;
  white-space: nowrap;
}

table.data th {
  font-size: 0.8125rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--luci-muted);
}

/* Denser variant for a list that is scanned and picked from rather than read
   across — the whole table wants to be visible at once. Only the padding and
   the row-action button shrink; type size and borders stay, so it still reads
   as the same table. */
table.data--tight th,
table.data--tight td {
  padding: 0.3125rem 0.75rem;
}

table.data--tight td .secondary {
  margin-top: 0;
  padding: 0.25rem 0.625rem;
  font-size: 0.8125rem;
}

/* Subtle zebra banding. :where() keeps specificity (0,1,2) — below the
   per-status row shading and :target/.row-target highlight, so those still win. */
:where(table.data) tbody tr:nth-child(even) {
  background: rgba(15, 34, 45, 0.025);
}

/* Two-tier link colour, applied consistently across the app:
     • inline / secondary links (a jump within a cell, a hint, body copy) carry
       the accent — this rule and the base `a` rule above.
     • structural links (the primary cell that opens the row's detail page)
       carry ink — the a.rowlink rule below — so the whole row reads as one
       clickable object rather than a coloured link.
   Same table, two colours, but the difference now encodes link role. */
table.data td a:not(.button):not(.rowlink) {
  color: var(--luci-accent-dark);
  font-weight: 600;
  text-decoration: none;
}

table.data td a:not(.button):not(.rowlink):hover {
  text-decoration: underline;
}

/* Structural tier: rows open their detail page via the first column. */
table.data a.rowlink {
  font-weight: 600;
  color: var(--luci-ink);
  text-decoration: none;
}

table.data a.rowlink:hover {
  color: var(--luci-accent-dark);
  text-decoration: underline;
}

/* Whole-row click affordance. app.js adds .rowlink-row to any row it has wired
   — a row whose first cell is a primary link, or one whose action is a submit
   button — and forwards a plain click there; the pointer and a translucent
   hover wash advertise it. The wash sits on the cells (not the row) as a
   translucent layer so it stacks over the zebra striping and per-status row
   tint rather than replacing them. */
.rowlink-row { cursor: pointer; }
.rowlink-row:hover > td { background-color: rgba(35, 43, 51, 0.045); }

.pill {
  display: inline-block;
  padding: 0.125rem 0.5rem;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  background: rgba(106, 200, 194, 0.16);
  color: var(--luci-accent-dark);
}

.pill--muted { background: var(--luci-bg); color: var(--luci-muted); }
.pill--warn { background: rgba(164, 0, 0, 0.1); color: var(--luci-error); }
.pill--overdue { background: rgba(154, 103, 0, 0.12); color: var(--luci-warn); }
.pill--ok { background: rgba(0, 90, 0, 0.1); color: var(--luci-success); }

.row-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
}

.row-actions form { display: inline; margin: 0; }

.row-actions button,
.row-actions .button {
  margin-top: 0;
  padding: 0.3125rem 0.625rem;
  font-size: 0.8125rem;
}

.toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  margin-bottom: 1rem;
}

/* Right-hand button group inside a toolbar; margin keeps it right-aligned
   even when it is the toolbar's only child (org tab toolbars). */
.toolbar-actions { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; }

/* Intro copy sharing a toolbar's row with the control on the right, instead of
   the control taking a row of its own and leaving a band of empty space above
   the table. Capped so a long sentence keeps its own comfortable measure rather
   than running the full page width up to the filter box. */
.toolbar-intro {
  margin: 0;
  max-width: 46rem;
}

/* "Updated Nm ago" freshness label beside the refresh button. */
.toolbar-age {
  color: var(--luci-muted);
  font-size: 0.8125rem;
  white-space: nowrap;
  margin-right: 0.25rem;
}
.toolbar-actions .button { margin-top: 0; }
.toolbar .button { margin-top: 0; }

/* Compact live-filter search box sitting in a table toolbar (overrides the
   full-width base input rule). */
.table-filter {
  width: auto;
  min-width: 15rem;
  margin-top: 0;
  padding: 0.5rem 0.7rem;
}

/* Compact live-filter search box sitting in a table toolbar (overrides the
   full-width base input rule). */
.table-filter {
  width: auto;
  min-width: 15rem;
  margin-top: 0;
  padding: 0.5rem 0.7rem;
}

/* --- Confirmation dialog ---
   Replaces window.confirm (see app.js). The native dialog is drawn by the
   browser against the top of the tab next to the origin name, where it reads as
   a warning about the site rather than a question about the row just clicked,
   and no stylesheet can reach it. A modal <dialog> is centred by the UA's own
   `inset: 0; margin: auto`, which is why nothing here positions it — the rules
   below only undo the UA's border and padding and dress it as a portal card. */
.confirm-dialog {
  max-width: min(28rem, calc(100vw - 2rem));
  padding: 1.5rem;
  border: 1px solid var(--luci-border);
  border-radius: 1rem;
  background: var(--luci-surface);
  color: var(--luci-ink);
  box-shadow: 0 20px 48px rgba(35, 43, 51, 0.18);
}

.confirm-dialog::backdrop { background: rgba(35, 43, 51, 0.45); }

.confirm-dialog-message { margin: 0; }

.confirm-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 1.5rem;
}

/* The base button rule's margin-top is for a button ending a form; these sit in
   a row of their own. Smaller than a page action, too: at full size two short
   words filled half the dialog and read as the weightiest thing on screen,
   which the question above them should be. */
.confirm-dialog-actions button {
  margin-top: 0;
  padding: 0.375rem 0.875rem;
  font-size: 0.875rem;
}

/* Inline "Reapply DB security" form in the Security tab toolbar. */
.reapply-form {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* Icon-only toolbar buttons (tooltip carries the label). */
.button-refresh,
.button-add {
  font-size: 1.125rem;
  line-height: 1;
  padding: 0.5rem 0.7rem;
}

.field-hint { color: var(--luci-muted); font-size: 0.875rem; margin: 0.25rem 0 0; }

/* An input with a helper button beside it (e.g. "Now" on next run at). */
.input-row {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  margin-top: 0.375rem;
}

.input-row input { flex: 1; margin-top: 0; }
.input-row button { margin-top: 0; white-space: nowrap; }

/* --- Toasts (transient flash notifications) --- */
.toast-region {
  position: fixed;
  top: 1rem;
  right: 1rem;
  z-index: 50;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  max-width: min(24rem, calc(100vw - 2rem));
}

.toast {
  padding: 0.75rem 1rem;
  border-radius: 0.625rem;
  background: var(--luci-ink);
  color: #fff;
  font-size: 0.9375rem;
  box-shadow: 0 12px 32px rgba(35, 43, 51, 0.24);
  cursor: pointer;
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.toast--ok { background: var(--luci-success); }
.toast.toast--hide { opacity: 0; transform: translateY(-0.5rem); }

/* Form select/textarea controls (schedule form). Kept alongside the input
   rules above so text fields, selects, and textareas render consistently. */
select,
textarea {
  width: 100%;
  padding: 0.625rem 0.75rem;
  margin-top: 0.375rem;
  border: 1px solid var(--luci-border);
  border-radius: 0.625rem;
  font: inherit;
  background: #fff;
}

textarea {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.875rem;
  min-height: 8rem;
  resize: vertical;
}

select:focus,
textarea:focus {
  outline: 2px solid rgba(106, 200, 194, 0.35);
  border-color: var(--luci-accent);
}

/* --- Health/status page --- */
.status-grid {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.75rem 1.5rem;
  margin: 1.25rem 0;
  align-items: center;
}
.status-grid dt { color: var(--luci-muted); font-weight: 600; }
.status-grid dd { margin: 0; }

/* --- Schedule list --- */
/* Subtle per-status row shading, keyed to core_schedule.status. */
table.data tr.status-running   { background: rgba(106, 200, 194, 0.12); }
table.data tr.status-complete  { background: rgba(0, 90, 0, 0.06); }
table.data tr.status-paused    { background: var(--luci-bg); }
table.data tr.status-cancelled { background: var(--luci-bg); }
table.data tr.status-failed    { background: rgba(164, 0, 0, 0.07); }
/* pending keeps the default surface — it is the normal active state. */

.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 1rem;
  margin-bottom: 1rem;
}
.filters label {
  margin: 0;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8125rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--luci-muted);
}
.filters select {
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--luci-border);
  border-radius: 0.5rem;
  font: inherit;
  background: #fff;
}
.filters button { margin-top: 0; }
.filters .filters-meta {
  align-self: center;
  font-size: 0.875rem;
  color: var(--luci-muted);
}

/* --- Dashboard --- */
/* Status banner: the page's one-line verdict, always first. */
.banner-status {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin: 1rem 0 1.25rem;
  padding: 0.85rem 1.1rem;
  border-radius: 0.75rem;
}

.banner-status--warn {
  background: rgba(154, 103, 0, 0.09);
  border: 1px solid rgba(154, 103, 0, 0.25);
}

.banner-status--ok {
  background: rgba(13, 148, 136, 0.08);
  border: 1px solid rgba(13, 148, 136, 0.3);
}

/* Neutral variant: something is queued and visible, but nothing is wrong —
   a schema job politely waiting out a reporting run should not read as an
   incident, or the warn colour stops meaning anything. */
.banner-status--info {
  background: var(--luci-bg);
  border: 1px solid var(--luci-border);
}

.banner-status-head { margin: 0; font-weight: 700; }
.banner-status-sub { margin: 0; font-size: 0.875rem; color: var(--luci-muted); }
.banner-status-sub a { color: inherit; font-weight: 600; }
.banner-status-cta { margin-top: 0; margin-left: auto; white-space: nowrap; }

/* A job's captured output, on the banner and the job page. Migration previews
   put a whole DDL statement or sentence-length summary on one line each —
   wrap rather than stretch the card into a page-wide horizontal scroll. */
pre.output {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Live job banner: the submitting page's own view of the job it just queued
   (see _job_banner.html). Top-aligned rather than centred because a finished
   preview brings its output <pre> with it; the spinner is the visible "this
   is refreshing" cue for the [data-poll-src] poller. */
.job-banner { align-items: flex-start; }
.job-banner-main { flex: 1; min-width: 0; }
.job-banner .output { margin: 0.75rem 0 0; max-height: 24rem; overflow: auto; }
.job-banner form { margin: 0.6rem 0 0; }
.job-banner [data-credential-slot] .detail-grid { margin: 0.75rem 0; }
.job-banner-spinner {
  flex: none;
  width: 1rem;
  height: 1rem;
  margin-top: 0.2rem;
  border: 2px solid var(--luci-border);
  border-top-color: var(--luci-accent-dark);
  border-radius: 50%;
  animation: job-banner-spin 0.8s linear infinite;
}

/* The card's dismiss ×: quiet by design — it undoes nothing, it only clears
   the card, so it must not compete with the real actions inside it. */
.job-banner-close {
  flex: none;
  align-self: flex-start;
  margin: 0;
  padding: 0 0.25rem;
  border: 0;
  border-radius: 0.375rem;
  background: transparent;
  color: var(--luci-muted);
  font-size: 1.25rem;
  line-height: 1;
  font-weight: 400;
}

.job-banner-close:hover {
  background: transparent;
  color: var(--luci-ink);
}

@keyframes job-banner-spin {
  to { transform: rotate(360deg); }
}

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: 1rem;
  margin: 1rem 0 0.5rem;
}

/* Hero variant: the run tile leads and gets the widest column. */
.stat-grid--hero { grid-template-columns: 1.5fr 1fr 1fr 1fr; }

@media (max-width: 44rem) {
  .stat-grid--hero { grid-template-columns: 1fr 1fr; }
  .stat-tile--hero { grid-column: 1 / -1; }
}

.stat-tile {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  padding: 1rem;
  border: 1px solid var(--luci-border);
  border-radius: 0.75rem;
  background: var(--luci-surface);
}

.stat-value {
  font-size: 1.75rem;
  font-weight: 700;
  line-height: 1.1;
}

.stat-hero {
  font-size: 2.75rem;
  font-weight: 700;
  line-height: 1.1;
}

.stat-hero-of { font-size: 1.0625rem; font-weight: 600; color: var(--luci-muted); }

.stat-label {
  font-size: 0.8125rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--luci-muted);
}

.stat-hint { font-size: 0.8125rem; color: var(--luci-muted); }

.stat-live {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--luci-muted);
}

.live-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--luci-chart);
}

@media (prefers-reduced-motion: no-preference) {
  .live-dot { animation: live-pulse 2s ease-in-out infinite; }
  @keyframes live-pulse { 50% { opacity: 0.35; } }
}

.dash-section { margin-top: 1.75rem; }

.dash-section h2 {
  margin: 0 0 0.5rem;
  font-size: 1.125rem;
  line-height: 1.2;
}

.dash-section .toolbar { margin-bottom: 0.5rem; }
.dash-section .toolbar h2 { margin: 0; }

/* Run activity strip: 24 hourly columns, complete stacked under failed. */
.activity-strip {
  display: block;
  width: 100%;
  height: 5.5rem;
}

.activity-strip .bar-ok { fill: var(--luci-chart); }
.activity-strip .bar-failed { fill: var(--luci-error); }
.activity-strip g:hover .bar-ok { fill: var(--luci-accent-dark); }

.strip-axis {
  display: flex;
  justify-content: space-between;
  margin-top: 0.375rem;
  font-size: 0.75rem;
  color: var(--luci-muted);
  font-variant-numeric: tabular-nums;
}

.strip-legend {
  display: inline-flex;
  gap: 1rem;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--luci-muted);
}

.strip-legend > span { display: inline-flex; align-items: center; gap: 0.35rem; }

.key {
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 0.2rem;
  display: inline-block;
}

.key--ok { background: var(--luci-chart); }
.key--failed { background: var(--luci-error); }

/* Attention queue: one row per unhealthy schedule, action last. */
table.queue td { white-space: normal; }
.queue-what a { font-weight: 600; color: var(--luci-ink); text-decoration: none; }
.queue-what a:hover { text-decoration: underline; }
.queue-meta { display: block; color: var(--luci-muted); font-size: 0.875rem; }
td.queue-when { color: var(--luci-muted); font-size: 0.8125rem; white-space: nowrap; }

/* Row-actions dropdown: one compact trigger per row instead of a button strip.
   Plain <details>, so it works without JS; app.js adds close-on-outside-click. */
.actions-menu { position: relative; display: inline-block; }

.actions-menu > summary {
  list-style: none;
  cursor: pointer;
  display: inline-block;
  padding: 0.3125rem 0.625rem;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--luci-muted);
  border: 1px solid var(--luci-border);
  border-radius: 0.625rem;
  background: var(--luci-surface);
  white-space: nowrap;
}
.actions-menu > summary::-webkit-details-marker { display: none; }
.actions-menu > summary::marker { content: ""; }
.actions-menu > summary:hover { background: var(--luci-bg); color: var(--luci-ink); }
.actions-menu[open] > summary { background: var(--luci-bg); color: var(--luci-ink); }

.actions-dropdown {
  position: absolute;
  right: 0;
  top: calc(100% + 0.25rem);
  min-width: 10rem;
  background: var(--luci-surface);
  border: 1px solid var(--luci-border);
  border-radius: 0.625rem;
  box-shadow: 0 12px 32px rgba(35, 43, 51, 0.16);
  padding: 0.375rem;
  z-index: 20;
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.actions-dropdown form { margin: 0; }

.actions-dropdown a,
.actions-dropdown button {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0.45rem 0.6rem;
  border: 0;
  border-radius: 0.5rem;
  background: transparent;
  color: var(--luci-ink);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
}

.actions-dropdown a:hover,
.actions-dropdown button:hover { background: var(--luci-bg); }
.actions-dropdown button.danger { color: var(--luci-error); }
.actions-dropdown button.danger:hover { background: rgba(164, 0, 0, 0.08); }

/* The attention queue links here with #s-<id>; ring the row rather than
   recolouring it, so the status tint (usually failed-red) stays visible.
   app.js swaps the :target match for the .row-target class and strips the
   hash from the URL, so the ring is gone after a refresh; :target remains as
   the no-JS fallback. */
table.data tr:target,
table.data tr.row-target {
  outline: 2px solid var(--luci-accent-dark);
  outline-offset: -2px;
  scroll-margin-top: 1rem;
}

/* Sortable column headers (tables with data-sortable): click affordance and
   direction indicator on the active column. */
table[data-sortable] thead th:not([data-nosort]) {
  cursor: pointer;
  user-select: none;
}
table[data-sortable] thead th:not([data-nosort]):hover { color: var(--luci-ink); }
table[data-sortable] thead th[aria-sort="ascending"]::after { content: " ▲"; font-size: 0.625rem; }
table[data-sortable] thead th[aria-sort="descending"]::after { content: " ▼"; font-size: 0.625rem; }

/* Heartbeat age shown in place of the next run while a report is running. */
.heartbeat { color: var(--luci-accent-dark); font-weight: 600; white-space: nowrap; }

/* Run history: error text may be long; wrap it rather than stretch the table
   past its panel. Must out-rank the table.data td nowrap default. */
table.data td.run-error {
  white-space: normal;
  overflow-wrap: anywhere;
  color: var(--luci-muted);
}

/* --- Entity detail pages (org / schedule / input / output) --- */
.entity-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
}

.entity-head h1 { margin: 0; }
.entity-head .spacer { flex: 1; }
.entity-code { color: var(--luci-muted); font-weight: 500; font-size: 1rem; }

/* A section heading carrying that section's state on the right — the org
   Security tab's three stages. Same shape as .entity-head one level down, so a
   page whose sections each have a status reads as a column of states rather
   than a column of headings. */
.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.25rem;
}

.section-head h2 { margin: 0; }
.section-head .spacer { flex: 1; }

/* A hint that carries a warning rather than an explanation (e.g. a scope that
   reaches organisations the page it is on cannot show). Amber, not the error
   red: it is a fact worth noticing, not something that went wrong. */
.field-hint--warn { color: var(--luci-warn); }
/* A hint standing on its own above body content, not under a form field: it
   needs the paragraph spacing .error has rather than the field one. */
.field-hint--block { margin: 0 0 1rem; }

/* A second, optional action folded into a form — ticked by default, and
   answering a question the operator would otherwise have had to come back for.
   The accent edge marks it as part of the form's outcome rather than a note
   about it. */
.paired-action {
  margin-top: 1.25rem;
  padding: 0.875rem 1rem;
  border: 1px solid var(--luci-border);
  border-left: 3px solid var(--luci-accent);
  border-radius: 0.5rem;
  background: rgba(106, 200, 194, 0.06);
}

.paired-action .field-hint { padding-left: 1.6rem; }

/* Tab bar under an entity header; tabs are plain links to sibling routes. */
.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.125rem;
  margin: 1rem 0 1.375rem;
  border-bottom: 1px solid var(--luci-border);
}

.tabs a {
  padding: 0.45rem 0.875rem;
  margin-bottom: -1px;
  text-decoration: none;
  color: var(--luci-muted);
  font-weight: 600;
  font-size: 0.9375rem;
  border-bottom: 2px solid transparent;
  border-radius: 0.375rem 0.375rem 0 0;
}

.tabs a:hover { color: var(--luci-ink); background: var(--luci-bg); }
.tabs a[aria-current="page"] {
  color: var(--luci-accent-dark);
  border-bottom-color: var(--luci-accent);
}

.tabs .count {
  margin-left: 0.375rem;
  padding: 0 0.45rem;
  border-radius: 999px;
  background: var(--luci-bg);
  color: var(--luci-muted);
  font-size: 0.75rem;
  font-weight: 700;
}

.tabs a[aria-current="page"] .count {
  background: rgba(106, 200, 194, 0.16);
  color: var(--luci-accent-dark);
}

/* Read-only field grid on detail pages. */
dl.detail-grid {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.625rem 1.5rem;
  margin: 1.25rem 0 0;
  align-items: baseline;
}

dl.detail-grid dt { color: var(--luci-muted); font-weight: 600; font-size: 0.875rem; }
dl.detail-grid dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }

/* Pretty-printed JSONB config on detail pages. */
pre.json {
  margin: 0.5rem 0 0;
  padding: 0.875rem 1rem;
  background: var(--luci-bg);
  border: 1px solid var(--luci-border);
  border-radius: 0.625rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8125rem;
  line-height: 1.55;
  overflow-x: auto;
}

/* Credential keys shown name-only; values never round-trip to the browser.
   A table (not stacked flex rows) so the key column sizes to the widest key on
   the page and every value lines up in one column, regardless of key length or
   the "protected" pill. */
.secret-list {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.875rem;
}
.secret-row { border-bottom: 1px solid var(--luci-border); }
.secret-row:last-child { border-bottom: 0; }
.secret-key {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8125rem;
  font-weight: 400;
  text-align: left;
  vertical-align: baseline;
  white-space: nowrap;      /* keep name + "protected" pill together */
  width: 1px;               /* shrink key column to its content */
  padding: 0.45rem 1.5rem 0.45rem 0;
}
.secret-val {
  color: var(--luci-muted);
  letter-spacing: 0.15em;
  vertical-align: baseline;
  padding: 0.45rem 0;
}

/* Facility-code chips (inputs' text[] column, detail grids, tables). */
.fac-list {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

/* Org list health rollup: issue pills linking to the org's Schedules tab. */
a.health-summary {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  text-decoration: none;
}

a.health-summary:hover .pill { text-decoration: underline; }

/* Facility checkbox group on the input form. */
.checkbox-group {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  margin-top: 0.375rem;
}

.checkbox-group label.checkbox {
  margin-top: 0;
  font-weight: 500;
}

/* The schema list flows into columns once there is room for them. Scoped to
   .schema-set rather than every .checkbox-group: the scope org picker's rows
   carry a name, a code and sometimes a pill, so they need the full width and
   would wrap raggedly. Columns keep the whole selection on screen at a glance,
   which is the property the "show it whole" note below depends on. */
.schema-set .checkbox-group {
  columns: 12rem auto;
  column-gap: 1.5rem;
  display: block;
}

.schema-set .checkbox-group label.checkbox {
  break-inside: avoid;
  padding: 0.125rem 0;
}

/* RLS schema selection: a preset picker above the schemas it ticks. The list is
   short enough to show whole — scrolling it would hide grants an operator is
   about to revoke. */
.schema-set {
  border: 1px solid var(--luci-border);
  border-radius: 4px;
  padding: 0.5rem 1rem 1rem;
  margin-top: 0.75rem;
}

.schema-set legend {
  padding: 0 0.35rem;
  font-weight: 600;
}

.schema-set .checkbox-group {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
  gap: 0.25rem 1rem;
}

/* A hint indented under the radio/checkbox option it explains. */
.field-hint--nested {
  margin: 0 0 0.25rem 1.6rem;
}

h2.detail-section {
  margin: 1.75rem 0 0.25rem;
  font-size: 1.05rem;
}

/* --- Small layout utilities (replace former inline style attributes) --- */
/* Wraps a form's submit/cancel buttons, spacing them from the fields above. */
.form-actions { margin-top: 1.25rem; }
/* An .error paragraph that follows body content and needs top spacing. */
.error--top { margin-top: 1rem; }
/* Section divider between a form and a related action group. */
.divider {
  margin: 1.5rem 0;
  border: none;
  border-top: 1px solid var(--luci-border);
}

/* Validator output on the Database tabs. The schema and security validators
   already emit aligned, indented text for the terminal console; it is rendered
   verbatim so both consoles word findings identically. Wraps rather than
   scrolls horizontally: findings are prose-length, not code. */
pre.report {
  margin: 1rem 0 0;
  padding: 0.875rem 1rem;
  background: var(--luci-bg);
  border: 1px solid var(--luci-border);
  border-radius: 0.625rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8125rem;
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Deferred-panel loading state (Database schema/security checks). The panel is
   fetched after the page paints, so the tab is usable while a multi-second
   validator runs. */
.check-loading {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  margin: 1rem 0 0;
  color: var(--luci-muted);
  font-size: 0.9375rem;
}
.spinner {
  flex: none;
  width: 1rem;
  height: 1rem;
  border: 2px solid var(--luci-border);
  border-top-color: var(--luci-accent-dark);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Respect a reduced-motion preference: keep the indicator, drop the spin. */
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; }
}

