/*
 * Omnipotent UI baseline stylesheet.
 *
 * First-party (unlike the vendored htmx / Redoc bundles) — the design baseline
 * every server-rendered page inherits through the `layout()` shell in
 * `crates/backend/src/ui.rs`. Kept deliberately small: page-specific styling
 * lands with each UI page row, this is only the chrome, typography, and form/
 * table/button primitives those pages compose. No external fonts or assets, so
 * the page references no third-party origin (the UI shell test asserts this).
 */

:root {
  /*
   * Declare the schemes this page supports so the UA paints system-colored UI
   * in the active one: form-control internals, scrollbars, and — critically —
   * the `Canvas`/`CanvasText` keywords the UA stylesheet gives top-layer
   * popovers (`[popover] { background: Canvas; color: CanvasText }`). Without
   * this, those system colors stay light even under `prefers-color-scheme: dark`
   * (`CanvasText` resolves to black), so a popover descendant with no explicit
   * color renders black-on-dark. Pinned to `light` here and to `dark` in both
   * dark blocks below so it tracks the token swap (and the `data-theme` seam).
   */
  color-scheme: light;
  --color-bg: #f6f7f9;
  --color-surface: #ffffff;
  --color-border: #d8dce1;
  --color-text: #1b1f24;
  --color-text-muted: #5a6470;
  --color-accent: #2f5bea;
  --color-accent-text: #ffffff;
  --color-danger: #c4314b;
  --color-danger-bg: #fbe6ea;
  --color-ok: #1f8b4c;
  /*
   * The tint behind an "added" diff field — the green counterpart to
   * `--color-danger-bg`, which tints a "removed" one. Both stay subtle so the
   * value text (`--color-text`) keeps its contrast; the dark-mode block below
   * swaps the pair alongside the rest of the tokens.
   */
  --color-ok-bg: #e6f4ec;
  /*
   * The middle status tier between ok (green) and danger/critical (red): the
   * environment-hierarchy view's warn chips and rollup tints. Same fg/bg
   * pairing idiom as the ok and danger pairs above.
   */
  --color-warn: #9a6700;
  --color-warn-bg: #f8efd8;

  --radius: 6px;
  /*
   * The drop shadow lifting a floating overlay (the account menu panel) off the
   * page. Routed through a token so the lone hard-coded shadow has one home — the
   * dark-mode block below swaps it (darker, more opaque) alongside the colors.
   */
  --shadow-menu: 0 6px 18px rgba(27, 31, 36, 0.16);
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 1rem;
  --space-4: 1.5rem;
  --space-5: 2.5rem;
  --max-width: 72rem;

  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial,
    sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    monospace;
}

/*
 * ---- Dark mode (token swap) -------------------------------------------
 *
 * Every component reads `var(--color-*)` / `var(--shadow-*)`, so dark mode is a
 * pure token override — no component rule changes. The palette mirrors the light
 * tokens role for role (surfaces darken, text lightens, accents brighten for
 * legibility on dark) and keeps body text at WCAG AA contrast or better.
 *
 * Two entry points share the same overrides:
 *   - `@media (prefers-color-scheme: dark)` — the automatic, JS-free default.
 *     Scoped to `:root:not([data-theme="light"])` so a future toggle that pins
 *     light (`data-theme="light"`) wins over the OS preference.
 *   - `:root[data-theme="dark"]` — an explicit hook so a future JS toggle can
 *     force dark regardless of the OS preference (the seam; no JS ships today).
 * The override list is intentionally duplicated across the two selectors: CSS
 * cannot share a declaration block across the media boundary, and the small
 * repetition is clearer than the indirection. Keep the two lists in sync.
 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --color-bg: #0d1117;
    --color-surface: #161b22;
    --color-border: #30363d;
    --color-text: #e6edf3;
    --color-text-muted: #9198a1;
    --color-accent: #4493f8;
    --color-accent-text: #ffffff;
    --color-danger: #f76e7e;
    --color-danger-bg: #3a1d22;
    --color-ok: #3fb950;
    --color-ok-bg: #12261a;
    --color-warn: #d29922;
    --color-warn-bg: #2b2210;
    /* Shadows need more weight on dark surfaces to read at all: a darker,
       more opaque rgba than the light token's soft lift. */
    --shadow-menu: 0 6px 18px rgba(1, 4, 9, 0.6);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --color-bg: #0d1117;
  --color-surface: #161b22;
  --color-border: #30363d;
  --color-text: #e6edf3;
  --color-text-muted: #9198a1;
  --color-accent: #4493f8;
  --color-accent-text: #ffffff;
  --color-danger: #f76e7e;
  --color-danger-bg: #3a1d22;
  --color-ok: #3fb950;
  --color-ok-bg: #12261a;
  --color-warn: #d29922;
  --color-warn-bg: #2b2210;
  --shadow-menu: 0 6px 18px rgba(1, 4, 9, 0.6);
}

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

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.5;
}

/* ---- Layout chrome ---------------------------------------------------- */

.app-header {
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
}

.app-header__inner {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: var(--space-3);
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.app-brand {
  font-weight: 700;
  font-size: 1.125rem;
  color: var(--color-text);
  text-decoration: none;
}

.app-nav {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1;
}

/*
 * The branch-scoped group, rendered as a breadcrumb: the header picker is the
 * root and the three branch-relative destinations (Catalog, Environments,
 * Schema) hang off it past a chevron (`.app-nav__crumb-sep`), so the header
 * states "these follow the current branch" through hierarchy rather than a
 * boxed-in surface. On an anonymous context the picker and chevron are both
 * absent, leaving just the three links.
 */
.app-nav__branch {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

/*
 * The breadcrumb chevron between the branch picker (root) and its scoped links.
 * Purely decorative (`aria-hidden`), muted and slightly dimmed so it reads as a
 * separator rather than a link, and pulled snug against its neighbors so the
 * root and its views read as one path.
 */
.app-nav__crumb-sep {
  color: var(--color-text-muted);
  opacity: 0.7;
  margin: 0 calc(-1 * var(--space-1));
  user-select: none;
}

/*
 * The global group: branch-independent destinations (Branches, Pull requests),
 * pushed to the trailing edge of the nav so they sit apart from the branch
 * breadcrumb and beside the account control (the `.app-header__divider` hairline
 * separates the two).
 */
.app-nav__global {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

/*
 * The nav links, scoped to each group's *direct* anchors so the muted/accent
 * treatment covers the destinations without bleeding into the branch picker's
 * popover entries (which live deeper, under `.branch-entry__list`, and keep the
 * base `a` accent color).
 */
.app-nav__branch > a,
.app-nav__global > a {
  color: var(--color-text-muted);
  text-decoration: none;
  font-size: 0.95rem;
}

.app-nav__branch > a:hover,
.app-nav__branch > a[aria-current="page"],
.app-nav__global > a:hover,
.app-nav__global > a[aria-current="page"] {
  color: var(--color-accent);
}

/* A hairline separating the global nav from the trailing account control. It
 * stretches to the header row's content height rather than sitting as a dot on
 * the centered baseline. */
.app-header__divider {
  align-self: stretch;
  width: 1px;
  background: var(--color-border);
}

/*
 * The auth control sits in its own section, pushed to the header's trailing
 * edge by the main nav's `flex: 1`. Signed out it is a plain "Login" link;
 * signed in it is the account menu, whose `<summary>` trigger reuses the same
 * muted-link look (`.app-auth__action`) so the two states read as one control.
 */
.app-auth {
  display: flex;
  align-items: center;
}

.app-auth__action {
  color: var(--color-text-muted);
  font-size: 0.95rem;
  text-decoration: none;
  font-family: inherit;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.app-auth__action:hover {
  color: var(--color-accent);
}

/*
 * The header branch picker: a split control (`.branch-switch`) pairing a link to
 * the current branch's detail page with a caret <button> that toggles a
 * `popover="auto"` switcher, the branch links switching the current branch while
 * staying on the same screen. It sits in the header chrome, so it sheds
 * .branch-entry's block margin. The browser light-dismisses the panel (an outside
 * click or Escape); app.js anchors it under the control. On a screen with no
 * branches to offer (a global screen) the current branch renders as a plain, muted
 * label instead.
 */
.app-branch.branch-entry {
  margin-bottom: 0;
}

/*
 * The split branch control (Option A): the current branch as a link to its
 * overview page (`.branch-switch__name`) joined to a caret button that opens the
 * switcher popover (`.branch-switch__caret`). A single outer border on the wrapper
 * plus one inner hairline (the caret's `border-left`) keeps the two halves in exact
 * alignment — no per-child borders to seam or double up, and equal `--space-2`
 * block padding on both halves gives them one shared height under `align-items:
 * stretch`. `overflow: hidden` clips the halves to the wrapper's rounded corners;
 * the popover is a top-layer `position: fixed` panel, so it escapes that clip.
 */
.branch-switch {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  overflow: hidden;
}

.branch-switch__name {
  display: inline-flex;
  align-items: center;
  padding: var(--space-2) var(--space-3);
  font: inherit;
  font-weight: 600;
  color: var(--color-text);
  text-decoration: none;
}

.branch-switch__caret {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-2);
  border: 0;
  border-left: 1px solid var(--color-border);
  background: transparent;
  color: var(--color-text-muted);
  font: inherit;
  cursor: pointer;
}

.branch-switch__chevron {
  display: block;
}

/*
 * Each half tints on hover; the caret also lights up while its popover is open
 * (app.js mirrors the open state onto `aria-expanded`). The chevron strokes with
 * `currentColor`, so it follows the caret's accent.
 */
.branch-switch__name:hover,
.branch-switch__caret:hover,
.branch-switch__caret[aria-expanded="true"] {
  background: var(--color-bg);
  color: var(--color-accent);
}

/*
 * The focus ring is inset (negative offset) so the wrapper's `overflow: hidden`
 * does not clip it against the rounded corner.
 */
.branch-switch__name:focus-visible,
.branch-switch__caret:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

/*
 * The panel reuses .branch-entry__list, but as a top-layer popover it is placed
 * against the viewport by app.js — its top at the header row's baseline, its left
 * edge under the trigger (left-aligned via `data-popover-align="start"`, since the
 * picker now leads the nav at the header's left edge) — so the UA
 * `inset: 0; margin: auto` (which would otherwise center it) is reset here.
 */
.app-branch .branch-entry__list {
  position: fixed;
  inset: auto;
  margin: 0;
  z-index: 1;
  padding: var(--space-2);
  flex-direction: column;
  background: var(--color-surface);
  /* Own the text color from the token rather than the UA `[popover]` system
     color (`CanvasText`) — see `.menu__list`. The branch links color themselves
     (`a` → `--color-accent`), but the panel owns its default for any plain text. */
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
}

/*
 * .branch-entry__list sets `display: flex` unconditionally; as an author rule it
 * would override the UA stylesheet's hide-when-closed rule and leave the popover
 * always visible. Re-hide it while closed so the popover actually toggles. (The
 * account menu's .menu__list sets no `display`, so it needs no such rule.)
 */
.app-branch .branch-entry__list:not(:popover-open) {
  display: none;
}

.app-branch__current {
  color: var(--color-text-muted);
  font-size: 0.95rem;
}

/*
 * The header account menu: a <button> trigger that toggles a `popover="auto"`
 * panel of account actions, the same idiom as the branch picker. The browser
 * opens the panel from the trigger and light-dismisses it on an outside click or
 * Escape; app.js only positions it. The trigger reuses .app-auth__action's
 * muted-link look, so no trigger-specific rules are needed here.
 *
 * The panel is a surface card lifted off the page by --shadow-menu. As a top-layer
 * popover it is placed against the viewport by app.js — its top at the header row's
 * baseline, its right edge under the trigger — so the UA `inset: 0; margin: auto`
 * (which would otherwise center it) is reset here. It sets no `display`, so the
 * popover's own show/hide governs visibility.
 */
.menu__list {
  position: fixed;
  inset: auto;
  margin: 0;
  z-index: 2;
  min-width: 12rem;
  list-style: none;
  padding: var(--space-1) 0;
  background: var(--color-surface);
  /* Own the panel's text color from the token rather than inheriting the UA
     `[popover] { color: CanvasText }`: a descendant with no explicit color (the
     `.menu__header strong` display name) would otherwise pick up the system
     color instead of `--color-text`. The actions set their own color below. */
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-menu);
}

/* The identity header above the actions: display name over a muted role line. */
.menu__header {
  padding: var(--space-2) var(--space-3);
  font-size: 0.9rem;
}

.menu__header strong {
  display: block;
}

.menu__role {
  color: var(--color-text-muted);
  font-size: 0.85rem;
}

/* A menu action: a full-width link or button reading as one row. */
.menu__item {
  display: block;
  width: 100%;
  padding: var(--space-2) var(--space-3);
  font: inherit;
  font-size: 0.9rem;
  text-align: left;
  text-decoration: none;
  color: var(--color-text);
  background: none;
  border: 0;
  cursor: pointer;
}

.menu__item:hover {
  background: var(--color-bg);
}

/* The destructive action (Logout): tinted to read as a state change. */
.menu__item--danger {
  color: var(--color-danger);
}

.menu__item--danger:hover {
  background: var(--color-danger-bg);
}

/* A hairline rule separating the destructive action from the rest. */
.menu__separator {
  height: 1px;
  margin: var(--space-1) 0;
  border: 0;
  background: var(--color-border);
}

/* The logout form is a plain row host: no margin, no chrome of its own. */
.menu__form {
  margin: 0;
}

/*
 * ---- Tab strip --------------------------------------------------------
 *
 * A horizontal sub-navigation for a resource's sub-pages (the PR viewer's
 * Details / Commits / Resources / Diff tabs). A flex row of muted links sitting
 * on a bottom rule; the active tab — marked server-side with
 * `aria-current="page"` — is underlined in the accent color, the same idiom the
 * header `.app-nav` uses for its active link (`a[aria-current="page"]`). No JS:
 * each tab is its own URL, so the strip works as a plain set of links.
 */
.tab-strip {
  display: flex;
  gap: var(--space-4);
  border-bottom: 1px solid var(--color-border);
  margin: 0 0 var(--space-4);
}

.tab-strip a {
  color: var(--color-text-muted);
  text-decoration: none;
  padding: var(--space-2) 0;
  /* The active tab's underline is drawn by a transparent border that the
   * accent color fills in below, so switching tabs does not shift the row. */
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

.tab-strip a:hover {
  color: var(--color-accent);
}

.tab-strip a[aria-current="page"] {
  color: var(--color-text);
  border-bottom-color: var(--color-accent);
}

.app-main {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: var(--space-4) var(--space-3) var(--space-5);
}

/* ---- Typography ------------------------------------------------------- */

h1,
h2,
h3 {
  line-height: 1.25;
  margin: 0 0 var(--space-3);
}

h1 {
  font-size: 1.6rem;
}

h2 {
  font-size: 1.25rem;
}

p {
  margin: 0 0 var(--space-3);
}

a {
  color: var(--color-accent);
}

code,
pre {
  font-family: var(--font-mono);
  font-size: 0.875em;
}

.text-muted {
  color: var(--color-text-muted);
}

/* ---- Surfaces --------------------------------------------------------- */

.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-4);
  margin-bottom: var(--space-4);
}

/* ---- Forms ------------------------------------------------------------ */

label {
  display: block;
  font-size: 0.9rem;
  font-weight: 600;
  margin-bottom: var(--space-1);
}

input,
select,
textarea {
  width: 100%;
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  font: inherit;
  background: var(--color-surface);
  color: var(--color-text);
}

input:focus,
select:focus,
textarea:focus,
.btn:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

.field {
  margin-bottom: var(--space-3);
}

/* A read-only attribute display (e.g. an edit form's immutable resource type,
 * data type, or namespace): the value is shown without an editable control, so
 * it reads as fixed. Styled like a disabled input — a bordered box on a muted
 * surface — rather than as plain body text, so the form still reads as a grid of
 * fields. */
.readonly-value {
  margin: 0;
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface-muted, var(--color-surface));
  color: var(--color-text-muted, var(--color-text));
}

/* A custom field's label (`field_label_parts`): just the human display name (or
 * bare field name). Its canonical `<namespace>:<field_name>` key and description
 * live in a native `title` tooltip, so no extra styling is needed — the cursor
 * hint suffices to signal the tooltip. */
.field-label[title] {
  cursor: help;
}

/* ---- Buttons ---------------------------------------------------------- */

.btn {
  display: inline-block;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
}

.btn:hover {
  border-color: var(--color-accent);
}

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

/* A destructive action (e.g. deleting a merged PR's source branch): a muted
 * outline that turns danger-tinted on hover, so the button reads as reversible
 * (it is — the delete is soft) rather than alarming. */
.btn--danger {
  color: var(--color-danger);
  border-color: var(--color-danger);
}

.btn--danger:hover {
  background: var(--color-danger-bg);
  border-color: var(--color-danger);
}

/* A horizontal row of action controls with even spacing — `.row-actions` for a
 * table row's links and form-wrapped buttons, `.form-actions` for a row of form
 * submit buttons that belong to different forms (e.g. the approval-expression
 * editor's Save + Reset, each its own POST form associated by the button's
 * `form` attribute). Without this the <form> around a button is block-level and
 * breaks onto its own line, stacking the actions vertically; `gap` supersedes
 * the literal whitespace separators in the markup (collapsed between flex
 * items). Shared declarations so the two cannot drift. */
.row-actions,
.form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* A `.form-actions` row carries no bottom margin, so a section heading placed
 * directly below one (the PagerDuty admin actions above the "Fields" heading)
 * sits flush against it — the reset `h2`/`h3` have no top margin of their own.
 * Restore the standard between-subsection gap so the heading lines up with its
 * siblings. `h4` keeps its UA top margin, so only the reset headings need this. */
.form-actions + h2,
.form-actions + h3 {
  margin-top: var(--space-3);
}

/* A `<form>` is block-level with no bottom margin, so a section heading placed
 * directly after one — the "Delete" subsection below the resource edit form's
 * "Stage changes" button — sits flush against the form's last control (the reset
 * headings have no top margin). Restore the standard between-subsection gap. */
form + h2,
form + h3 {
  margin-top: var(--space-3);
}

/* The PR Details "Actions" card button row: lays the merge and close controls
 * side by side with even spacing, each wrapped in its own POST form. Without this
 * the <form> around each button is block-level and the buttons stack vertically. */
.pr-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* ---- Tables ----------------------------------------------------------- */

.table {
  width: 100%;
  border-collapse: collapse;
  background: var(--color-surface);
}

.table th,
.table td {
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--color-border);
}

.table th {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--color-text-muted);
}

/*
 * A `.table` (and the results `.pager`) carries no bottom margin, so a sibling
 * section placed directly below one sits flush against it with no gap. Restore
 * the standard between-section spacing for the adjacencies where that happens:
 * the staged-changes table (or its pager) above the finalize card, the
 * changed-resources table above a "Schema changes" heading — the staged screen
 * and the commit / pull-request change lists — and the open-a-PR page, where the
 * "Changes" table is followed by the create `form` (or, for a viewer who can't
 * author, the role-gated note `p`). On the type-detail page the custom-fields
 * table is followed by the "Checks" `h2`, so that adjacency needs it too. A table
 * that *ends* a card needs nothing (the card's own padding is the gap), which is
 * why this targets the following section rather than giving every table a bottom
 * margin.
 */
.table + h2,
.table + h3,
.table + .card,
.table + form,
.table + p,
.pager + .card {
  margin-top: var(--space-4);
}

/* ---- Badges ----------------------------------------------------------- */

/* A small inline status pill, e.g. a protected branch in the branch list. */
.badge {
  display: inline-block;
  padding: 0 var(--space-2);
  border-radius: var(--radius);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

/*
 * Semantic tier modifiers: one pill vocabulary shared by the approval-policy
 * status on a PR and the environment-hierarchy status chips. Each tier reuses
 * the semantic ok/warn/danger token pairs, so dark mode follows the token swap
 * automatically. The tiers are tinted-fill-led — the border stays subtle so a
 * `.badge--stale` dashed border reads as "old data" without competing with the
 * fill of every chip. A leading `currentColor` dot is additive (never the sole
 * signal; the text label stays), so it rides on the semantic tiers only — the
 * neutral base `.badge` and the dotless `.badge--type` carry none.
 */
.badge--ok {
  color: var(--color-ok);
  background: var(--color-ok-bg);
  border-color: transparent;
}

.badge--warn {
  color: var(--color-warn);
  background: var(--color-warn-bg);
  border-color: transparent;
}

.badge--critical {
  color: var(--color-danger);
  background: var(--color-danger-bg);
  border-color: transparent;
}

/* Unknown ("cannot be judged"): the muted palette, subtly bordered. */
.badge--unknown {
  color: var(--color-text-muted);
  background: var(--color-bg);
  border-color: var(--color-text-muted);
}

/* A stale dynamic value keeps its tier's colors but switches to a dashed
   border — visibly "old data". */
.badge--stale {
  border-style: dashed;
  border-color: var(--color-text-muted);
}

/* The boundary-type label ("region", "cluster"): a quiet, fully rounded,
   dotless neutral pill so it reads as subordinate to the status chip beside
   it. Reuses the plain badge palette. */
.badge--type {
  border-radius: 999px;
  font-size: 0.75rem;
  color: var(--color-text-muted);
  text-transform: none;
}

/* The leading status dot — one rule for every semantic tier. `currentColor`
   tracks the tier's text color, so no per-tier duplication. Additive only:
   the neutral base `.badge` and `.badge--type` are dotless. */
.badge--ok::before,
.badge--warn::before,
.badge--critical::before,
.badge--unknown::before {
  content: "";
  display: inline-block;
  width: 0.5em;
  height: 0.5em;
  border-radius: 50%;
  background: currentColor;
  margin-right: 0.4em;
}

/* The approval-trace value pills carry a ✓ / ✗ / — glyph already, so they
   suppress the dot to avoid a redundant double marker. */
.trace-value::before {
  content: none;
}

/* ---- Approval-expression trace --------------------------------------- */

/* The annotated expression tree: each predicate line carries its ✓ / ✗ / —
   value pill next to the authored call, and and/or/not groups indent their
   children so the structure reads like the design-doc example. */
.approval-trace {
  margin: var(--space-2) 0;
  font-size: 0.9rem;
}

.trace-children {
  margin-left: var(--space-3);
  border-left: 1px solid var(--color-border);
  padding-left: var(--space-3);
}

.trace-leaf,
.trace-op {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1);
  padding: 1px 0;
}

/* The fixed-width value pill keeps the ✓ / ✗ / — marks aligned down the tree. */
.trace-value {
  min-width: 1.4rem;
  text-align: center;
}

.trace-keyword {
  font-family: var(--font-mono, monospace);
  color: var(--color-text-muted);
}

.trace-detail {
  font-size: 0.85rem;
}

/* The derived one-line verdict beneath the tree. */
.trace-verdict {
  margin-top: var(--space-2);
  font-weight: 500;
}

/* The muted note shown when the root value is indeterminate (Unknown). */
.approval-indeterminate {
  margin-top: var(--space-1);
  font-size: 0.85rem;
  color: var(--color-text-muted);
  font-style: italic;
}

/* ---- Alerts ----------------------------------------------------------- */

.alert {
  padding: var(--space-2) var(--space-3);
  border: 1px solid transparent;
  border-radius: var(--radius);
  margin-bottom: var(--space-3);
}

.alert--error {
  background: var(--color-danger-bg);
  border-color: var(--color-danger);
  color: var(--color-danger);
}

/* An alert rendered as a list — a form's validation problems (`ul.alert`) — keeps
 * its markers inside the padded box. With the browser default (`outside`) the
 * bullets hang past the alert's left padding and collide with its border, since
 * `.alert` replaces the list's wide default `padding-left`. Also drop the list's
 * default top margin: `.alert` already supplies the bottom gap. */
ul.alert,
ol.alert {
  margin-top: 0;
  list-style-position: inside;
}

/* An informational banner, e.g. the "viewing as of commit X" time-pin note. */
.alert--note {
  background: var(--color-bg);
  border-color: var(--color-border);
  color: var(--color-text-muted);
}

/* ---- Commit log ------------------------------------------------------- */

/* The full commit message on the per-commit page: preserve its wrapping but let
   long lines wrap rather than overflow the card. */
.commit-message {
  white-space: pre-wrap;
  word-break: break-word;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-3);
  margin: 0;
}

/* On the PR Details page the description box sits directly above the reviews
   card. The shared `.commit-message` omits a bottom margin (on the per-commit
   page an `h2` follows and supplies the gap), so restore it here to match the
   spacing between the cards below. */
.commit-message.pr-description {
  margin-bottom: var(--space-4);
}

/*
 * A reviewer's comment, shown full-width in a row beneath its review (the
 * `.review-comment-row`, which carries no top border so it reads as part of the
 * row above — that row drops its bottom border via `.has-comment`). The text
 * preserves the author's line breaks without the boxed framing of a commit
 * message, since it already sits inside a table cell.
 */
.table tr.has-comment > td {
  border-bottom: none;
}

.review-comment-row > td {
  padding-top: 0;
}

.review-comment {
  white-space: pre-wrap;
  word-break: break-word;
  margin: 0;
  color: var(--color-text-muted);
}

/* ---- Auth pages ------------------------------------------------------- */

/* The login and bootstrap forms are narrow and centered in the content area. */
.auth {
  max-width: 26rem;
  margin: 0 auto;
}

.auth__aside {
  margin-top: var(--space-3);
  margin-bottom: 0;
  font-size: 0.9rem;
}

/* ---- Browse views ----------------------------------------------------- */

/* The per-type browse heading: title plus its one-line description. */
.page-head {
  margin-bottom: var(--space-4);
}

/*
 * The h2 form is the PR-scoped commit page's head, where the PR chrome owns
 * the page's h1 and the commit title steps down a level — same tight spacing
 * to the hash line beneath it either way.
 */
.page-head h1,
.page-head h2 {
  margin-bottom: var(--space-1);
}

/*
 * The header branch picker's wrapper (`.branch-entry`) and its popover list
 * (`.branch-entry__list`, below). The picker's positioning and open/close
 * behavior live with the header chrome above (`.app-branch …`); these supply the
 * wrapper's block margin (shed to 0 there) and the panel's bulletless column of
 * branch links, the entries spaced by `gap`.
 */
.branch-entry {
  margin-bottom: var(--space-4);
}

.branch-entry__list {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/*
 * The switcher popover's footer: a link to the Branches index, set off from the
 * branch entries above by a hairline. The `gap` already spaces it from the last
 * entry; the `padding-top` balances that space below the rule so it reads as a
 * separated footer rather than another entry. The entries above are `switch_path`
 * links (accent by the base `a` rule); this one navigates to `/branches`.
 */
.branch-entry__all {
  padding-top: var(--space-2);
  border-top: 1px solid var(--color-border);
}

/*
 * The filter/sort toolbar is a horizontal row of controls. The baseline form
 * inputs stretch to 100%; inside the toolbar they size to their content so the
 * controls sit side by side, wrapping on narrow viewports.
 */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.toolbar .field {
  margin-bottom: 0;
}

.toolbar input,
.toolbar select {
  width: auto;
}

.toolbar input[type="search"] {
  min-width: 16rem;
}

/* The "no results" message, set apart from the table that would otherwise be. */
.empty-state {
  margin: 0;
  padding: var(--space-4);
  text-align: center;
  color: var(--color-text-muted);
}

/* Previous/next paging controls below the results table. */
.pager {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

/* ---- Dashboard activity feed ------------------------------------------ */

/*
 * The authenticated home page's activity feed: a vertical list of events, each a
 * one-line "<actor> <verb> · <time>" header with an optional muted sub-line (a PR
 * title, or — for a folded push — one short-hash + message per commit). An
 * unstyled list (no bullets); rows are separated by a hairline rule.
 */
.feed {
  list-style: none;
  margin: 0;
  padding: 0;
}

.feed__item {
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--color-border);
}

.feed__item:last-child {
  border-bottom: none;
}

.feed__actor {
  font-weight: 600;
}

/* The relative-time label is muted and carries the absolute UTC stamp on hover. */
.feed__time {
  color: var(--color-text-muted);
}

/* The PR-title / commit-message sub-line under an event's header line. */
.feed__sub {
  margin-top: var(--space-1);
  font-size: 0.9rem;
}

/* A folded push block lists each commit on its own muted line. */
.feed__commits {
  list-style: none;
  margin: var(--space-1) 0 0;
  padding: 0;
}

.feed__commit {
  font-size: 0.9rem;
  color: var(--color-text-muted);
}

/* ---- Detail view ------------------------------------------------------ */

/* The "back to the type list" link above a resource's detail heading. */
.detail-back {
  margin-bottom: var(--space-3);
  font-size: 0.9rem;
}

/*
 * A resource's identity / built-in / custom fields as label–value rows. A
 * two-column grid: the label column sizes to its content within a sensible
 * range, the value column takes the rest; on a narrow viewport the value wraps
 * under its label.
 */
.detail-list {
  display: grid;
  grid-template-columns: minmax(8rem, 16rem) 1fr;
  gap: var(--space-2) var(--space-3);
  margin: 0 0 var(--space-4);
}

.detail-list dt {
  margin: 0;
  font-weight: 600;
  color: var(--color-text-muted);
}

.detail-list dd {
  margin: 0;
  overflow-wrap: anywhere;
}

/* ---- Diff tab: before→after per-field change -------------------------- */

/*
 * The PR Diff tab colors each changed field by how it moved between a resource's
 * before and after state. The scheme is built from the existing status tokens so
 * it dark-mode-swaps for free (the dark override block at the top of the file):
 *   - added   → `--color-ok` (green): a field gained a value (created, or set).
 *   - removed → `--color-danger` (red): a field lost its value (unset).
 *   - changed → `--color-accent` (neutral emphasis): a value moved old → new.
 * Color alone never carries the meaning: an accent/danger/ok left border plus a
 * subtle tint mark the cell, the old→new arrow is explicit, and the legend names
 * each class — so the distinction survives for color-blind and monochrome
 * readers. An unchanged field carries no modifier and renders de-emphasized so
 * the eye lands on what moved.
 *
 * Applied to a `.detail-list dd` (the value cell): a left accent stripe and a
 * tint, padded so the stripe reads without shifting the grid.
 */
.detail-list dd.diff-field--added,
.detail-list dd.diff-field--removed,
.detail-list dd.diff-field--changed {
  padding: var(--space-1) var(--space-2);
  border-left: 3px solid transparent;
  border-radius: var(--radius);
}

.detail-list dd.diff-field--added {
  border-left-color: var(--color-ok);
  background: var(--color-ok-bg);
}

.detail-list dd.diff-field--removed {
  border-left-color: var(--color-danger);
  background: var(--color-danger-bg);
}

.detail-list dd.diff-field--changed {
  border-left-color: var(--color-accent);
}

/* The old→new separator between a changed field's before and after values. */
.diff-field__arrow {
  color: var(--color-text-muted);
  font-weight: 600;
}

/*
 * Inline class chips in the Diff tab legend ("added", "removed", "changed"):
 * small colored labels so the legend shows the same coloring the field cells
 * use, naming each class for readers who cannot rely on color.
 */
.diff-field--added,
.diff-field--removed,
.diff-field--changed {
  font-weight: 600;
}

span.diff-field--added {
  color: var(--color-ok);
}

span.diff-field--removed {
  color: var(--color-danger);
}

span.diff-field--changed {
  color: var(--color-accent);
}

/* ---- Pull request merge ----------------------------------------------- */

/*
 * The unresolved-conflict list in a PR's merge card (the conflict surface). Each
 * conflict is a machine-recognizable, monospace line so the section name and
 * identity stay legible; the list sits below the "must be resolved" banner.
 */
.conflict-list {
  margin: 0 0 var(--space-3);
  padding-left: var(--space-4);
}

.conflict-list li {
  font-family: var(--font-mono);
  font-size: 0.9rem;
  overflow-wrap: anywhere;
  margin-bottom: var(--space-1);
}

/* ---- htmx feedback ---------------------------------------------------- */

/*
 * htmx sets `.htmx-request` on an element while its request is in flight; the
 * indicator (opt-in via `.htmx-indicator`) fades in for the duration. This is
 * the baseline wiring; individual pages attach indicators where they apply.
 */
.htmx-indicator {
  opacity: 0;
  transition: opacity 150ms ease-in;
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
  opacity: 1;
}

/* ---- Environment hierarchy view ---------------------------------------- */

/*
 * Tree mode: nested <details> rows. Each row is a flex line — the label side
 * grows, the per-field value cells are fixed-width and right-anchored so the
 * columns stay aligned at every nesting depth (the cells never indent; only
 * the label side does, via the children container's padding).
 */
.hier-columns,
.hier-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
}

.hier-columns {
  padding: var(--space-1) 0;
  border-bottom: 1px solid var(--color-border);
  color: var(--color-text-muted);
  font-size: 0.85rem;
  /* Match the app's table headers: uppercase, lightly tracked. */
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* Data rows carry no divider — only the column header keeps its underline
   (the mockup separates rows by a hover tint, not a rule per row). */
.hier-row {
  padding: var(--space-1) var(--space-2);
}

.hier-row:hover {
  background: var(--color-bg);
}

/* The row's `display: flex` suppresses the native <summary> disclosure marker,
   so draw an explicit caret as the row's first flex item — a triangle that
   points right when collapsed and rotates down when the <details> is open. */
summary.hier-row {
  cursor: pointer;
  list-style: none;
}

summary.hier-row::-webkit-details-marker {
  display: none;
}

summary.hier-row::before {
  content: "";
  flex: 0 0 auto;
  align-self: center;
  width: 0;
  height: 0;
  border-left: 0.4em solid var(--color-text-muted);
  border-top: 0.3em solid transparent;
  border-bottom: 0.3em solid transparent;
  transition: transform 0.15s ease;
}

details[open] > summary.hier-row::before {
  transform: rotate(90deg);
}

.hier-row__main {
  flex: 1;
  min-width: 0;
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  flex-wrap: wrap;
}

/* One fixed-width, right-anchored value cell per selected field. */
.hier-cell {
  flex: 0 0 9rem;
  width: 9rem;
  text-align: right;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Indent only the label side of descendants: the children container pads,
   the value cells stay on the shared right-anchored grid. A guide line down
   the left edge makes depth legible — the same idiom as the approval trace's
   `.trace-children`, so the two tree-shaped views match. */
.hier-children {
  border-left: 1px solid var(--color-border);
  padding-left: var(--space-3);
}

/* Instance names are `service/instance` natural-key-shaped identifiers, so
   they render mono; boundary names stay in the sans face. */
.hier-row--instance .hier-row__main a {
  font-family: var(--font-mono);
}

.hier-rollup {
  color: var(--color-text-muted);
  font-size: 0.85rem;
  white-space: nowrap;
}

/* Rollup severity counts: an emphasized, tier-colored number in an otherwise
   muted line. The tokens carry the dark-mode swap, so no theme override is
   needed. The unknown bucket stays muted (no dedicated severity color). */
.rollup-count--crit,
.rollup-count--warn,
.rollup-count--unknown {
  font-weight: 600;
}

.rollup-count--crit {
  color: var(--color-danger);
}

.rollup-count--warn {
  color: var(--color-warn);
}

.rollup-count--unknown {
  color: var(--color-text-muted);
}

.hier-owner {
  color: var(--color-text-muted);
  font-size: 0.85rem;
}

/* A collapsed boundary's lazy-expand affordance: the deep subtree is elided
   behind its rollup, and this is the "load the rest" link. It is a plain
   anchor (the no-JS fallback navigates to the re-rooted view), enhanced with
   htmx to swap the panel in place. Muted, small — a secondary affordance below
   the boundary's own row/head. */
.hier-collapsed {
  margin: var(--space-1) 0 0;
  font-size: 0.85rem;
}

.hier-expand {
  color: var(--color-text-muted);
  cursor: pointer;
}

.hier-expand:hover {
  color: var(--color-accent);
}

/* Numbers render mono per the value-formatting conventions. */
.hier-number {
  font-family: var(--font-mono);
}

/* The "as of …" age note beside a stale dynamic value. */
.hier-stale-age {
  color: var(--color-text-muted);
  font-size: 0.8rem;
  white-space: nowrap;
}

/* The breadcrumb up the boundary hierarchy, from the composite natural key. */
.hier-breadcrumb {
  margin: 0 0 var(--space-2);
  color: var(--color-text-muted);
  font-size: 0.9rem;
}

/*
 * Map mode: recursive flex boxes. A boundary is a bordered box whose edge
 * tints to its subtree's worst status; instances are status chips laid out
 * in a wrapping row. Box sizes fall out of normal document flow — no layout
 * math anywhere.
 */
.map-children {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: flex-start;
}

.map-box {
  border: 2px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-2);
  background: var(--color-surface);
  min-width: 10rem;
}

.map-box--ok {
  border-color: var(--color-ok);
}

.map-box--warn {
  border-color: var(--color-warn);
}

.map-box--critical {
  border-color: var(--color-danger);
}

.map-box--unknown {
  border-color: var(--color-text-muted);
}

/* An inset severity stripe atop the border tint, so a critical or warning box
   pops on a glanced-at wall display far harder than a border alone. Tokens
   carry the dark-mode swap, so no theme override is needed. */
.map-box--warn {
  box-shadow: inset 3px 0 0 var(--color-warn);
}

.map-box--critical {
  box-shadow: inset 3px 0 0 var(--color-danger);
}

/* Depth cue: odd nesting levels ride a subtly shaded background so adjacent
   levels read apart. It rides the token background, so dark mode follows. */
.map-box--depth-alt {
  background: var(--color-bg);
}

/* A collapsed boundary in map mode: a dashed edge marks the elided subtree,
   distinct from the solid tint of an expanded box, so a wall display reads
   "there is more here" at a glance. */
.map-box--collapsed {
  border-style: dashed;
}

.map-box__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: var(--space-1);
}

.map-instances {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-bottom: var(--space-1);
}

/* An instance chip in map mode is a link; keep the tier's color, not the
   default link accent. Its name is a `service/instance` natural-key-shaped
   identifier, so it renders mono to match the tree. */
.map-instances a.badge {
  text-decoration: none;
  font-family: var(--font-mono);
  /* Keep the name and its inline value on one line — the chips wrap as whole
     units via the container's flex-wrap, never mid-chip (the base `.badge`
     dropped the retired `.chip`'s nowrap). */
  white-space: nowrap;
}

/* The first selected field's raw value, surfaced inline on the chip beside the
   name (decision 4): mono and muted, subordinate to the identifier. */
.map-instance-value {
  font-family: var(--font-mono);
  color: var(--color-text-muted);
}

/*
 * The view toolbar: the root picker / field multi-select / view control form.
 * Reuses the browse `.toolbar` flex idiom; the field checkboxes wrap in a row
 * of compact labels.
 */
.hier-fields {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
}

.hier-fields label {
  font-weight: 400;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  white-space: nowrap;
}

/*
 * Segmented control — a shared primitive for a binary/either-or toggle built
 * from native radios (here: the Tree/Map view switch). Deliberately generic
 * (`.segmented`, not `.hier-*`) so any later either/or toggle reuses it.
 *
 * No-JS-first (decision 5): the active segment is filled in the accent purely
 * via `:has()`, and the native radio is only *visually* hidden — it stays in
 * the accessibility tree and keyboard-operable, and a `:focus-visible` ring
 * tracks keyboard focus. Where `:has()` is unsupported the radios render plain
 * and the control still works (graceful degradation).
 */
.segmented {
  display: inline-flex;
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
}

.segmented label {
  font-weight: 400;
  margin: 0;
  padding: var(--space-1) var(--space-3);
  cursor: pointer;
  white-space: nowrap;
  color: var(--color-text-muted);
}

/* Joined cells: a divider between adjacent segments, not around the group. */
.segmented label + label {
  border-left: 1px solid var(--color-border);
}

/* Visually hide the native radio but keep it in the a11y tree and focusable —
 * `position: absolute; opacity: 0`, never `display: none`. */
.segmented input {
  position: absolute;
  opacity: 0;
}

/* The checked segment fills the accent. Degrades gracefully: without `:has()`
 * support this rule is dropped and the radios render plain. */
.segmented label:has(input:checked) {
  background: var(--color-accent);
  color: var(--color-accent-text);
}

/* Keyboard focus lands on the hidden radio; show a ring on its enclosing cell
 * so the focused segment is visible. */
.segmented label:has(input:focus-visible) {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}
